Skip to content

Commit 6200ae1

Browse files
authored
feat(web): align Claude effort options with Claude Code --effort levels (#731)
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`).
1 parent ec3722a commit 6200ae1

8 files changed

Lines changed: 44 additions & 14 deletions

File tree

cli/src/claude/effort.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ describe('normalizeClaudeSessionEffort', () => {
1414
})
1515

1616
it('normalizes supported effort values', () => {
17+
expect(normalizeClaudeSessionEffort('low')).toBe('low')
1718
expect(normalizeClaudeSessionEffort('medium')).toBe('medium')
1819
expect(normalizeClaudeSessionEffort('high')).toBe('high')
20+
expect(normalizeClaudeSessionEffort('xhigh')).toBe('xhigh')
1921
expect(normalizeClaudeSessionEffort('max')).toBe('max')
2022
expect(normalizeClaudeSessionEffort(' High ')).toBe('high')
2123
})

shared/src/effort.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import { CLAUDE_EFFORT_LABELS, CLAUDE_EFFORT_LEVELS } from './effort'
3+
4+
describe('Claude effort constants', () => {
5+
test('exposes the Claude Code --effort levels in ascending order', () => {
6+
expect(CLAUDE_EFFORT_LEVELS).toEqual(['low', 'medium', 'high', 'xhigh', 'max'])
7+
})
8+
9+
test('every CLAUDE_EFFORT_LEVEL has a label', () => {
10+
for (const level of CLAUDE_EFFORT_LEVELS) {
11+
expect(CLAUDE_EFFORT_LABELS[level]).toBeDefined()
12+
}
13+
})
14+
})

shared/src/effort.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Effort levels Claude Code's `--effort` flag accepts, in ascending order.
2+
// "auto"/null is hapi's sentinel for omitting --effort (model default), not a level here.
3+
export const CLAUDE_EFFORT_LABELS = {
4+
low: 'Low',
5+
medium: 'Medium',
6+
high: 'High',
7+
xhigh: 'XHigh',
8+
max: 'Max'
9+
} as const
10+
11+
export type ClaudeEffortLevel = keyof typeof CLAUDE_EFFORT_LABELS
12+
export const CLAUDE_EFFORT_LEVELS = Object.keys(CLAUDE_EFFORT_LABELS) as ClaudeEffortLevel[]

shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './apiTypes'
22
export * from './messages'
33
export * from './buildInfo'
4+
export * from './effort'
45
export * from './flavors'
56
export * from './models'
67
export * from './modes'

web/src/components/AssistantChat/claudeEffortOptions.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@ describe('getClaudeComposerEffortOptions', () => {
66
expect(getClaudeComposerEffortOptions('ultra')).toEqual([
77
{ value: null, label: 'Auto' },
88
{ value: 'ultra', label: 'Ultra' },
9+
{ value: 'low', label: 'Low' },
910
{ value: 'medium', label: 'Medium' },
1011
{ value: 'high', label: 'High' },
12+
{ value: 'xhigh', label: 'XHigh' },
1113
{ value: 'max', label: 'Max' },
1214
])
1315
})
1416

1517
it('does not duplicate preset Claude effort values', () => {
1618
expect(getClaudeComposerEffortOptions('high')).toEqual([
1719
{ value: null, label: 'Auto' },
20+
{ value: 'low', label: 'Low' },
1821
{ value: 'medium', label: 'Medium' },
1922
{ value: 'high', label: 'High' },
23+
{ value: 'xhigh', label: 'XHigh' },
2024
{ value: 'max', label: 'Max' },
2125
])
2226
})

web/src/components/AssistantChat/claudeEffortOptions.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1+
import { CLAUDE_EFFORT_LABELS, CLAUDE_EFFORT_LEVELS, type ClaudeEffortLevel } from '@hapi/protocol'
2+
13
export type ClaudeComposerEffortOption = {
24
value: string | null
35
label: string
46
}
57

6-
const CLAUDE_EFFORT_PRESETS = ['medium', 'high', 'max'] as const
7-
const CLAUDE_EFFORT_LABELS: Record<(typeof CLAUDE_EFFORT_PRESETS)[number], string> = {
8-
medium: 'Medium',
9-
high: 'High',
10-
max: 'Max'
11-
}
12-
138
function normalizeClaudeComposerEffort(effort?: string | null): string | null {
149
const trimmedEffort = effort?.trim().toLowerCase()
1510
if (!trimmedEffort || trimmedEffort === 'auto' || trimmedEffort === 'default') {
@@ -32,15 +27,15 @@ export function getClaudeComposerEffortOptions(currentEffort?: string | null): C
3227

3328
if (
3429
normalizedCurrentEffort
35-
&& !CLAUDE_EFFORT_PRESETS.includes(normalizedCurrentEffort as typeof CLAUDE_EFFORT_PRESETS[number])
30+
&& !CLAUDE_EFFORT_LEVELS.includes(normalizedCurrentEffort as ClaudeEffortLevel)
3631
) {
3732
options.push({
3833
value: normalizedCurrentEffort,
3934
label: formatEffortLabel(normalizedCurrentEffort)
4035
})
4136
}
4237

43-
options.push(...CLAUDE_EFFORT_PRESETS.map((effort) => ({
38+
options.push(...CLAUDE_EFFORT_LEVELS.map((effort) => ({
4439
value: effort,
4540
label: CLAUDE_EFFORT_LABELS[effort]
4641
})))

web/src/components/NewSession/types.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ describe('Claude effort options', () => {
2424
it('matches supported effort presets in expected order', () => {
2525
expect(CLAUDE_EFFORT_OPTIONS).toEqual([
2626
{ value: 'auto', label: 'Auto' },
27+
{ value: 'low', label: 'Low' },
2728
{ value: 'medium', label: 'Medium' },
2829
{ value: 'high', label: 'High' },
30+
{ value: 'xhigh', label: 'XHigh' },
2931
{ value: 'max', label: 'Max' },
3032
])
3133
})

web/src/components/NewSession/types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import {
2+
CLAUDE_EFFORT_LABELS,
3+
CLAUDE_EFFORT_LEVELS,
24
CLAUDE_MODEL_LABELS,
35
CLAUDE_MODEL_PRESETS,
46
GEMINI_MODEL_LABELS,
57
GEMINI_MODEL_PRESETS
68
} from '@hapi/protocol'
7-
import type { AgentFlavor } from '@hapi/protocol'
9+
import type { AgentFlavor, ClaudeEffortLevel } from '@hapi/protocol'
810

911
export type AgentType = AgentFlavor
1012
export type SessionType = 'simple' | 'worktree'
1113
export type CodexReasoningEffort = 'default' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'
12-
export type ClaudeEffort = 'auto' | 'medium' | 'high' | 'max'
14+
export type ClaudeEffort = 'auto' | ClaudeEffortLevel
1315

1416
function modelPresetOptions<TModel extends string>(
1517
presets: readonly TModel[],
@@ -48,7 +50,5 @@ export const CODEX_REASONING_EFFORT_OPTIONS: { value: CodexReasoningEffort; labe
4850

4951
export const CLAUDE_EFFORT_OPTIONS: { value: ClaudeEffort; label: string }[] = [
5052
{ value: 'auto', label: 'Auto' },
51-
{ value: 'medium', label: 'Medium' },
52-
{ value: 'high', label: 'High' },
53-
{ value: 'max', label: 'Max' },
53+
...CLAUDE_EFFORT_LEVELS.map((value) => ({ value, label: CLAUDE_EFFORT_LABELS[value] })),
5454
]

0 commit comments

Comments
 (0)