From 6200ae137032dd09df9926dc10eacd4700cf45f5 Mon Sep 17 00:00:00 2001 From: hanger Date: Sat, 30 May 2026 12:41:00 +0800 Subject: [PATCH] feat(web): align Claude effort options with Claude Code --effort levels (#731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Claude effort selector (New Session config + in-session composer) only offered auto/medium/high/max, missing `low` and `xhigh` — yet `claude --effort` actually accepts low/medium/high/xhigh/max. Add the two missing levels in both places so the selector faithfully mirrors the CLI. Extract the level list + labels into one shared constant (@hapi/protocol: shared/src/effort.ts, mirroring CLAUDE_MODEL_PRESETS) so the two UIs derive from a single source and can't drift again. No backend change: the effort string is free-form end-to-end through to the --effort flag. ultracode is intentionally excluded — it is a TUI-only /effort session setting, not an --effort value (the CLI rejects `--effort ultracode`). --- cli/src/claude/effort.test.ts | 2 ++ shared/src/effort.test.ts | 14 ++++++++++++++ shared/src/effort.ts | 12 ++++++++++++ shared/src/index.ts | 1 + .../AssistantChat/claudeEffortOptions.test.ts | 4 ++++ .../AssistantChat/claudeEffortOptions.ts | 13 ++++--------- web/src/components/NewSession/types.test.ts | 2 ++ web/src/components/NewSession/types.ts | 10 +++++----- 8 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 shared/src/effort.test.ts create mode 100644 shared/src/effort.ts diff --git a/cli/src/claude/effort.test.ts b/cli/src/claude/effort.test.ts index 67c781a7..18baf4cd 100644 --- a/cli/src/claude/effort.test.ts +++ b/cli/src/claude/effort.test.ts @@ -14,8 +14,10 @@ describe('normalizeClaudeSessionEffort', () => { }) it('normalizes supported effort values', () => { + expect(normalizeClaudeSessionEffort('low')).toBe('low') expect(normalizeClaudeSessionEffort('medium')).toBe('medium') expect(normalizeClaudeSessionEffort('high')).toBe('high') + expect(normalizeClaudeSessionEffort('xhigh')).toBe('xhigh') expect(normalizeClaudeSessionEffort('max')).toBe('max') expect(normalizeClaudeSessionEffort(' High ')).toBe('high') }) diff --git a/shared/src/effort.test.ts b/shared/src/effort.test.ts new file mode 100644 index 00000000..b56fdc3b --- /dev/null +++ b/shared/src/effort.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from 'bun:test' +import { CLAUDE_EFFORT_LABELS, CLAUDE_EFFORT_LEVELS } from './effort' + +describe('Claude effort constants', () => { + test('exposes the Claude Code --effort levels in ascending order', () => { + expect(CLAUDE_EFFORT_LEVELS).toEqual(['low', 'medium', 'high', 'xhigh', 'max']) + }) + + test('every CLAUDE_EFFORT_LEVEL has a label', () => { + for (const level of CLAUDE_EFFORT_LEVELS) { + expect(CLAUDE_EFFORT_LABELS[level]).toBeDefined() + } + }) +}) diff --git a/shared/src/effort.ts b/shared/src/effort.ts new file mode 100644 index 00000000..ca140111 --- /dev/null +++ b/shared/src/effort.ts @@ -0,0 +1,12 @@ +// Effort levels Claude Code's `--effort` flag accepts, in ascending order. +// "auto"/null is hapi's sentinel for omitting --effort (model default), not a level here. +export const CLAUDE_EFFORT_LABELS = { + low: 'Low', + medium: 'Medium', + high: 'High', + xhigh: 'XHigh', + max: 'Max' +} as const + +export type ClaudeEffortLevel = keyof typeof CLAUDE_EFFORT_LABELS +export const CLAUDE_EFFORT_LEVELS = Object.keys(CLAUDE_EFFORT_LABELS) as ClaudeEffortLevel[] diff --git a/shared/src/index.ts b/shared/src/index.ts index 879ad35d..30ed8620 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,6 +1,7 @@ export * from './apiTypes' export * from './messages' export * from './buildInfo' +export * from './effort' export * from './flavors' export * from './models' export * from './modes' diff --git a/web/src/components/AssistantChat/claudeEffortOptions.test.ts b/web/src/components/AssistantChat/claudeEffortOptions.test.ts index 15490cd0..a4e41ed9 100644 --- a/web/src/components/AssistantChat/claudeEffortOptions.test.ts +++ b/web/src/components/AssistantChat/claudeEffortOptions.test.ts @@ -6,8 +6,10 @@ describe('getClaudeComposerEffortOptions', () => { expect(getClaudeComposerEffortOptions('ultra')).toEqual([ { value: null, label: 'Auto' }, { value: 'ultra', label: 'Ultra' }, + { value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }, + { value: 'xhigh', label: 'XHigh' }, { value: 'max', label: 'Max' }, ]) }) @@ -15,8 +17,10 @@ describe('getClaudeComposerEffortOptions', () => { it('does not duplicate preset Claude effort values', () => { expect(getClaudeComposerEffortOptions('high')).toEqual([ { value: null, label: 'Auto' }, + { value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }, + { value: 'xhigh', label: 'XHigh' }, { value: 'max', label: 'Max' }, ]) }) diff --git a/web/src/components/AssistantChat/claudeEffortOptions.ts b/web/src/components/AssistantChat/claudeEffortOptions.ts index 9bab1502..f73e13cc 100644 --- a/web/src/components/AssistantChat/claudeEffortOptions.ts +++ b/web/src/components/AssistantChat/claudeEffortOptions.ts @@ -1,15 +1,10 @@ +import { CLAUDE_EFFORT_LABELS, CLAUDE_EFFORT_LEVELS, type ClaudeEffortLevel } from '@hapi/protocol' + export type ClaudeComposerEffortOption = { value: string | null label: string } -const CLAUDE_EFFORT_PRESETS = ['medium', 'high', 'max'] as const -const CLAUDE_EFFORT_LABELS: Record<(typeof CLAUDE_EFFORT_PRESETS)[number], string> = { - medium: 'Medium', - high: 'High', - max: 'Max' -} - function normalizeClaudeComposerEffort(effort?: string | null): string | null { const trimmedEffort = effort?.trim().toLowerCase() if (!trimmedEffort || trimmedEffort === 'auto' || trimmedEffort === 'default') { @@ -32,7 +27,7 @@ export function getClaudeComposerEffortOptions(currentEffort?: string | null): C if ( normalizedCurrentEffort - && !CLAUDE_EFFORT_PRESETS.includes(normalizedCurrentEffort as typeof CLAUDE_EFFORT_PRESETS[number]) + && !CLAUDE_EFFORT_LEVELS.includes(normalizedCurrentEffort as ClaudeEffortLevel) ) { options.push({ value: normalizedCurrentEffort, @@ -40,7 +35,7 @@ export function getClaudeComposerEffortOptions(currentEffort?: string | null): C }) } - options.push(...CLAUDE_EFFORT_PRESETS.map((effort) => ({ + options.push(...CLAUDE_EFFORT_LEVELS.map((effort) => ({ value: effort, label: CLAUDE_EFFORT_LABELS[effort] }))) diff --git a/web/src/components/NewSession/types.test.ts b/web/src/components/NewSession/types.test.ts index 51a499f1..d2e040f3 100644 --- a/web/src/components/NewSession/types.test.ts +++ b/web/src/components/NewSession/types.test.ts @@ -24,8 +24,10 @@ describe('Claude effort options', () => { it('matches supported effort presets in expected order', () => { expect(CLAUDE_EFFORT_OPTIONS).toEqual([ { value: 'auto', label: 'Auto' }, + { value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }, + { value: 'xhigh', label: 'XHigh' }, { value: 'max', label: 'Max' }, ]) }) diff --git a/web/src/components/NewSession/types.ts b/web/src/components/NewSession/types.ts index aa31e5d5..eaa3c823 100644 --- a/web/src/components/NewSession/types.ts +++ b/web/src/components/NewSession/types.ts @@ -1,15 +1,17 @@ import { + CLAUDE_EFFORT_LABELS, + CLAUDE_EFFORT_LEVELS, CLAUDE_MODEL_LABELS, CLAUDE_MODEL_PRESETS, GEMINI_MODEL_LABELS, GEMINI_MODEL_PRESETS } from '@hapi/protocol' -import type { AgentFlavor } from '@hapi/protocol' +import type { AgentFlavor, ClaudeEffortLevel } from '@hapi/protocol' export type AgentType = AgentFlavor export type SessionType = 'simple' | 'worktree' export type CodexReasoningEffort = 'default' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' -export type ClaudeEffort = 'auto' | 'medium' | 'high' | 'max' +export type ClaudeEffort = 'auto' | ClaudeEffortLevel function modelPresetOptions( presets: readonly TModel[], @@ -48,7 +50,5 @@ export const CODEX_REASONING_EFFORT_OPTIONS: { value: CodexReasoningEffort; labe export const CLAUDE_EFFORT_OPTIONS: { value: ClaudeEffort; label: string }[] = [ { value: 'auto', label: 'Auto' }, - { value: 'medium', label: 'Medium' }, - { value: 'high', label: 'High' }, - { value: 'max', label: 'Max' }, + ...CLAUDE_EFFORT_LEVELS.map((value) => ({ value, label: CLAUDE_EFFORT_LABELS[value] })), ]