Files
orca/src/shared/agent-session-option-catalog-claude-codex.ts
T
Jinjing 5b75a87dd3 feat(native-chat): add per-model session option pickers with verified Claude switch (#9085)
* Add per-model session-option pickers (model/effort/fast-mode) to native

Introduces a shared agent session-option catalog (Claude/Codex/Gemini/Cursor)
with model-scoped options, launch-command composition, mid-session dispatch
via slash commands, and per-model persisted defaults. Wires the new
NativeChatSessionOptionPickers UI into the composer, threads sessionOptions
through every startup-plan builder (worktree creation, onboarding, source
control actions, folder workspace, direct work-item launches), and adds
localized strings and tests across the affected surfaces.

* Add per-model session-option pickers with a verified Claude model switch

Native chat's model/effort picker now dispatches option commands through a
body-then-verified-Enter write path and, for Claude, arms a PTY observer that
classifies the cached-history confirmation prompt as applied/rejected/needs
interaction before the picker returns — falling back to the terminal only
when Claude genuinely requires manual input, and clearing stale truth
otherwise. Also reorders the composer's model/effort pills, disambiguates
their tooltip labels, and switches disabled-reason strings to a closed enum
so producer and localized copy can't drift.

* Add live model/effort detection from Claude's TUI header

- Reads the mounted xterm's main-buffer snapshot (falling back to the
  rendered screen when the alternate screen owns the buffer) to parse
  Claude's header for the currently active model and effort, so the
  native chat picker reflects reality instead of only dispatched state
- Extracts command-apply recording and reported-value application into
  dedicated modules, and factors file-link click handling into a hook,
  to keep native-chat-pty-session-options.ts focused
2026-07-16 17:12:44 -07:00

154 lines
4.5 KiB
TypeScript

import type { AgentSessionOptionCatalog, CatalogOption } from './agent-session-option-catalog-types'
function hasFlag(tokens: readonly string[], flags: readonly string[]): boolean {
return tokens.some((token) =>
flags.some(
(flag) =>
token === flag ||
token.startsWith(`${flag}=`) ||
(flag.startsWith('-') && !flag.startsWith('--') && token.startsWith(flag))
)
)
}
function hasCodexEffortOverride(tokens: readonly string[]): boolean {
if (hasFlag(tokens, ['--reasoning-effort'])) {
return true
}
return tokens.some((token, index) => {
const previous = tokens[index - 1]
return (
(token.startsWith('model_reasoning_effort=') &&
(previous === '-c' || previous === '--config')) ||
token.startsWith('-cmodel_reasoning_effort=') ||
token.startsWith('-c=model_reasoning_effort=') ||
token.startsWith('--config=model_reasoning_effort=')
)
})
}
const STANDARD_EFFORT_CHOICES = [
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' }
]
const EXTENDED_EFFORT_CHOICES = [
...STANDARD_EFFORT_CHOICES,
{ value: 'xhigh', label: 'Extra high' },
{ value: 'max', label: 'Max' }
]
function claudeEffort(extended: boolean): CatalogOption {
return {
id: 'effort',
label: 'Effort',
category: 'thought_level',
kind: {
type: 'select',
choices: extended ? EXTENDED_EFFORT_CHOICES : STANDARD_EFFORT_CHOICES,
defaultValue: 'high'
},
apply: {
launchArgs: (value) => ['--effort', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--effort']),
midSession: { kind: 'command', build: (value) => `/effort ${String(value)}` }
}
}
}
const CLAUDE_FAST_MODE: CatalogOption = {
id: 'fastMode',
label: 'Fast mode',
category: 'mode',
kind: { type: 'boolean', defaultValue: false },
apply: { midSession: { kind: 'toggle-command', command: '/fast' } }
}
export const CLAUDE_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
models: [
{
id: 'fable',
label: 'Fable 5',
options: [claudeEffort(true)]
},
{
id: 'opus',
label: 'Opus 4.8',
options: [claudeEffort(true), CLAUDE_FAST_MODE]
},
{
id: 'sonnet',
label: 'Sonnet 5',
isDefault: true,
options: [claudeEffort(true)]
},
{
id: 'haiku',
label: 'Haiku',
options: []
}
],
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--model']),
midSession: {
kind: 'command',
build: (value) => `/model ${String(value)}`,
pickerCommand: '/model',
// Why: Claude sometimes confirms a cached-history switch. Detect the
// actual prompt so ordinary model changes stay in native chat.
detectAgentInteraction: 'claude-model-switch-confirmation'
}
}
}
const CODEX_EFFORT_CHOICES = [
{ value: 'minimal', label: 'Minimal' },
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
{ value: 'xhigh', label: 'Extra high' }
]
function codexEffort(includeExtraHigh: boolean): CatalogOption {
return {
id: 'effort',
label: 'Reasoning effort',
category: 'thought_level',
kind: {
type: 'select',
choices: includeExtraHigh
? CODEX_EFFORT_CHOICES
: CODEX_EFFORT_CHOICES.filter((choice) => choice.value !== 'xhigh'),
defaultValue: 'medium'
},
apply: {
launchArgs: (value) => ['-c', `model_reasoning_effort=${String(value)}`],
agentArgsOverride: hasCodexEffortOverride,
midSession: { kind: 'agent-picker', command: '/model' }
}
}
}
export const CODEX_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
// Why: Codex model access depends on auth. Keep this seed short and allow
// unknown persisted ids to pass through instead of claiming a complete list.
models: [
{ id: 'gpt-5.6-sol', label: 'GPT-5.6 Sol', options: [codexEffort(true)] },
{ id: 'gpt-5.6-terra', label: 'GPT-5.6 Terra', options: [codexEffort(true)] },
{ id: 'gpt-5.6-luna', label: 'GPT-5.6 Luna', options: [codexEffort(false)] },
{ id: 'gpt-5.5', label: 'GPT-5.5', options: [codexEffort(true)] },
{
id: 'gpt-5.2-codex',
label: 'GPT-5.2 Codex',
options: [codexEffort(true)]
}
],
modelApply: {
launchArgs: (value) => ['-m', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['-m', '--model']),
midSession: { kind: 'agent-picker', command: '/model' }
}
}