Replace agentArgsOverride with unified removeAgentArgs approach (#25091)

Consolidate override detection and removal into removeAgentArgs. Previously
catalogs used agentArgsOverride to detect conflicts and removeAgentArgs to
strip them; now removeAgentArgs handles both by returning stripped tokens.
This eliminates redundancy and makes the intent clearer.

Enhance removeAgentArgOption with optional value filtering to support
selective removal for complex cases like Codex config overrides.
This commit is contained in:
Jinjing
2026-10-03 15:25:43 -07:00
committed by GitHub
parent 752871bd67
commit 9eb73a608f
14 changed files with 139 additions and 120 deletions
+14 -25
View File
@@ -1,35 +1,24 @@
import { findOptionOccurrence } from './command-option-occurrence'
export function agentArgOptionTokens(tokens: readonly string[]): readonly string[] {
const terminator = tokens.indexOf('--')
return terminator === -1 ? tokens : tokens.slice(0, terminator)
}
/** Removes each occurrence before `--`, or only those whose value `matchesValue` accepts. */
export function removeAgentArgOption(
tokens: readonly string[],
aliases: readonly string[]
aliases: readonly string[],
matchesValue: (value: string | undefined) => boolean = () => true
): string[] {
const result: string[] = []
for (let index = 0; index < tokens.length; index += 1) {
const token = tokens[index]
if (token === '--') {
result.push(...tokens.slice(index))
break
}
const exact = aliases.includes(token)
const matched = aliases.some(
(alias) =>
token.startsWith(`${alias}=`) ||
(alias.startsWith('-') &&
!alias.startsWith('--') &&
token.startsWith(alias) &&
token.length > alias.length)
)
if (!exact && !matched) {
result.push(token)
continue
}
if (exact && tokens[index + 1] && !tokens[index + 1].startsWith('-')) {
index += 1
}
const kept: string[] = []
let rest = tokens
let found = findOptionOccurrence(rest, aliases, true)
while (found) {
const end = found.index + found.consumed
kept.push(...rest.slice(0, matchesValue(found.value) ? found.index : end))
rest = rest.slice(end)
found = findOptionOccurrence(rest, aliases, true)
}
return result
return [...kept, ...rest]
}
@@ -1,4 +1,3 @@
import { hasFlag } from './agent-cli-flag-detection'
import { removeAgentArgOption } from './agent-session-option-agent-args'
import type { AgentSessionOptionCatalog, CatalogOption } from './agent-session-option-catalog-types'
@@ -17,7 +16,6 @@ const ANTIGRAVITY_EFFORT: CatalogOption = {
},
apply: {
launchArgs: (value) => ['--effort', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--effort']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--effort']),
midSession: { kind: 'command', build: (value) => `/effort ${String(value)}` }
}
@@ -29,7 +27,6 @@ export const ANTIGRAVITY_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
models: [],
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--model']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--model']),
midSession: { kind: 'agent-picker', command: '/model' }
},
@@ -3,55 +3,24 @@ import type {
CatalogModel,
CatalogOption
} from './agent-session-option-catalog-types'
import { agentArgOptionTokens, removeAgentArgOption } from './agent-session-option-agent-args'
import { removeAgentArgOption } from './agent-session-option-agent-args'
import {
CLAUDE_MODEL_LIST_ARGS,
CLAUDE_MODEL_LIST_STDIN,
parseClaudeModelList
} from './claude-model-list-probe'
import { hasFlag } from './agent-cli-flag-detection'
function hasCodexEffortOverride(tokens: readonly string[]): boolean {
if (hasFlag(tokens, ['--reasoning-effort'])) {
return true
}
const optionTokens = agentArgOptionTokens(tokens)
return optionTokens.some((token, index) => {
const previous = optionTokens[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=')
)
})
}
function removeCodexEffortOverride(tokens: readonly string[]): string[] {
const withoutFlag = removeAgentArgOption(tokens, ['--reasoning-effort'])
const result: string[] = []
for (let index = 0; index < withoutFlag.length; index += 1) {
const token = withoutFlag[index]
if (token === '--') {
result.push(...withoutFlag.slice(index))
break
}
const next = withoutFlag[index + 1]
if ((token === '-c' || token === '--config') && next?.startsWith('model_reasoning_effort=')) {
index += 1
continue
}
if (
token.startsWith('-cmodel_reasoning_effort=') ||
token.startsWith('-c=model_reasoning_effort=') ||
token.startsWith('--config=model_reasoning_effort=')
) {
continue
}
result.push(token)
}
return result
// Why: Codex also reads any setting from a `-c key=value` config override.
function removeCodexAgentArgs(
tokens: readonly string[],
flags: readonly string[],
configKey: string
): string[] {
return removeAgentArgOption(
removeAgentArgOption(tokens, flags),
['-c', '--config'],
(value) => value?.startsWith(`${configKey}=`) === true
)
}
const STANDARD_EFFORT_CHOICES = [
@@ -84,7 +53,6 @@ function claudeEffortWithChoices(choices: typeof EXTENDED_EFFORT_CHOICES): Catal
},
apply: {
launchArgs: (value) => ['--effort', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--effort']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--effort']),
midSession: { kind: 'command', build: (value) => `/effort ${String(value)}` }
}
@@ -162,7 +130,6 @@ export const CLAUDE_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
],
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--model']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--model']),
midSession: {
kind: 'command',
@@ -204,8 +171,8 @@ function codexEffort(ceiling: 'xhigh' | 'max' | 'ultra'): CatalogOption {
},
apply: {
launchArgs: (value) => ['-c', `model_reasoning_effort=${String(value)}`],
agentArgsOverride: hasCodexEffortOverride,
removeAgentArgs: removeCodexEffortOverride,
removeAgentArgs: (tokens) =>
removeCodexAgentArgs(tokens, ['--reasoning-effort'], 'model_reasoning_effort'),
midSession: { kind: 'agent-picker', command: '/model', delivery: 'type' }
}
}
@@ -228,8 +195,7 @@ export const CODEX_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
],
modelApply: {
launchArgs: (value) => ['-m', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['-m', '--model']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['-m', '--model']),
removeAgentArgs: (tokens) => removeCodexAgentArgs(tokens, ['-m', '--model'], 'model'),
// Codex classifies multi-character writes as pasted prose; type the bare
// command and let its own picker apply the account-supported model.
midSession: { kind: 'agent-picker', command: '/model', delivery: 'type' }
@@ -1,4 +1,3 @@
import { hasFlag } from './agent-cli-flag-detection'
import { removeAgentArgOption } from './agent-session-option-agent-args'
import type { AgentSessionOptionCatalog, CatalogOption } from './agent-session-option-catalog-types'
@@ -20,7 +19,6 @@ const CODEBUDDY_EFFORT: CatalogOption = {
},
apply: {
launchArgs: (value) => ['--effort', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--effort']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--effort'])
}
}
@@ -37,7 +35,6 @@ export const CODEBUDDY_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
].map((model) => ({ ...model, options: [CODEBUDDY_EFFORT] })),
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--model']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--model'])
},
unknownModelOptions: [CODEBUDDY_EFFORT]
@@ -1,4 +1,3 @@
import { hasFlag } from './agent-cli-flag-detection'
import type {
AgentSessionOptionCatalog,
CatalogModel,
@@ -6,7 +5,8 @@ import type {
} from './agent-session-option-catalog-types'
import { removeAgentArgOption } from './agent-session-option-agent-args'
const hasModelFlag = (tokens: readonly string[]): boolean => hasFlag(tokens, ['-m', '--model'])
const removeModelFlag = (tokens: readonly string[]): string[] =>
removeAgentArgOption(tokens, ['-m', '--model'])
export const GEMINI_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
models: [
@@ -17,7 +17,7 @@ export const GEMINI_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
],
modelApply: {
launchArgs: (value) => ['-m', String(value)],
agentArgsOverride: hasModelFlag,
removeAgentArgs: removeModelFlag,
midSession: { kind: 'agent-picker', command: '/model' }
}
}
@@ -86,8 +86,7 @@ export const CURSOR_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
],
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: hasModelFlag,
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['-m', '--model']),
removeAgentArgs: removeModelFlag,
midSession: { kind: 'command', build: (value) => `/model ${String(value)}` }
},
composeModelValue: (modelId, values) => {
@@ -179,32 +179,37 @@ describe('grok launch args', () => {
})
})
describe('grok agentArgsOverride', () => {
const modelOverride = GROK_SESSION_OPTION_CATALOG.modelApply.agentArgsOverride!
const effortOverride = grokEffortOption().apply.agentArgsOverride!
describe('grok agent-arg overrides', () => {
const removeModel = GROK_SESSION_OPTION_CATALOG.modelApply.removeAgentArgs!
const removeEffort = grokEffortOption().apply.removeAgentArgs!
it('detects a user-supplied model flag in every spelling', () => {
it('strips a user-supplied model flag in every spelling', () => {
for (const tokens of [
['-m', 'grok-build'],
['-mgrok-build'],
['--model', 'grok-build'],
['--model=grok-build']
]) {
expect(modelOverride(tokens)).toBe(true)
expect(removeModel(tokens)).toEqual([])
}
})
it('does not fire on a different flag or a positional that contains -m', () => {
expect(modelOverride(['--model-context', '8000'])).toBe(false)
expect(modelOverride(['summarize-my-diff'])).toBe(false)
expect(modelOverride(['--reasoning-effort', 'low'])).toBe(false)
expect(modelOverride([])).toBe(false)
for (const tokens of [
['--model-context', '8000'],
['summarize-my-diff'],
['--reasoning-effort', 'low'],
[]
]) {
expect(removeModel(tokens)).toEqual(tokens)
}
})
it('detects both effort spellings', () => {
expect(effortOverride(['--effort', 'low'])).toBe(true)
expect(effortOverride(['--reasoning-effort=low'])).toBe(true)
expect(effortOverride(['--effortless'])).toBe(false)
it('strips both effort spellings', () => {
expect(removeEffort(['--effort', 'low', '--reasoning-effort=high', '--keep'])).toEqual([
'--keep'
])
expect(removeEffort(['--effortless'])).toEqual(['--effortless'])
})
it('drops only the overridden key from the launch record', () => {
@@ -1,9 +1,9 @@
import { hasFlag } from './agent-cli-flag-detection'
import type {
AgentSessionOptionCatalog,
CatalogModel,
CatalogOption
} from './agent-session-option-catalog-types'
import { removeAgentArgOption } from './agent-session-option-agent-args'
import { parseGrokModelList } from './grok-model-list-probe'
// The offered slice of grok's canonical ladder, low to high. Its `none` tier is
@@ -35,7 +35,8 @@ function grokEffort(ceiling: 'high' | 'xhigh'): CatalogOption {
},
apply: {
launchArgs: (value) => ['--reasoning-effort', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--effort', '--reasoning-effort']),
// Why: grok reads both spellings.
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--effort', '--reasoning-effort']),
midSession: { kind: 'command', build: (value) => `/effort ${String(value)}` }
}
}
@@ -67,7 +68,7 @@ export const GROK_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
],
modelApply: {
launchArgs: (value) => ['-m', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['-m', '--model']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['-m', '--model']),
// Why: `agent-picker` would replace the whole model list with "Choose in
// agent picker…" and never persist a model, so `-m` would never be emitted.
midSession: { kind: 'command', build: (value) => `/model ${String(value)}` }
@@ -1,4 +1,3 @@
import { hasFlag } from './agent-cli-flag-detection'
import { removeAgentArgOption } from './agent-session-option-agent-args'
import type { AgentSessionOptionCatalog, CatalogOption } from './agent-session-option-catalog-types'
@@ -22,7 +21,6 @@ const MUSE_EFFORT: CatalogOption = {
},
apply: {
launchArgs: (value) => ['--reasoning-effort', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--reasoning-effort']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--reasoning-effort'])
}
}
@@ -34,7 +32,6 @@ export const MUSE_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
models: [],
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--model']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--model'])
},
unknownModelOptions: [MUSE_EFFORT]
@@ -116,13 +116,16 @@ describe('omp session option catalog', () => {
})
it('yields to a user --model in the launch args, in either spelling', () => {
const override = OMP_SESSION_OPTION_CATALOG.modelApply.agentArgsOverride!
expect(override(['--model', 'opus'])).toBe(true)
expect(override(['--model=openai/gpt-5.5'])).toBe(true)
expect(override(['--no-extensions'])).toBe(false)
const remove = OMP_SESSION_OPTION_CATALOG.modelApply.removeAgentArgs!
expect(remove(['--model', 'opus', '--model=openai/gpt-5.5', '--no-extensions'])).toEqual([
'--no-extensions'
])
// `--models` scopes Ctrl+P cycling; it does not pick a model. omp has no `-m`.
expect(override(['--models=anthropic/*'])).toBe(false)
expect(override(['-m', 'opus'])).toBe(false)
expect(remove(['--models=anthropic/*', '-m', 'opus'])).toEqual([
'--models=anthropic/*',
'-m',
'opus'
])
})
it('switches mid-session with /orca-model <selector>, which omp resolves exactly', () => {
@@ -1,4 +1,4 @@
import { hasFlag } from './agent-cli-flag-detection'
import { removeAgentArgOption } from './agent-session-option-agent-args'
import type { AgentSessionOptionCatalog, CatalogModel } from './agent-session-option-catalog-types'
import { parseOmpModelList } from './omp-model-list-probe'
@@ -18,7 +18,7 @@ export const OMP_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
models: [],
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--model']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--model']),
// OMP only opens its TUI picker for /model; our extension applies the exact selector.
midSession: { kind: 'command', build: (value) => `/orca-model ${String(value)}` }
},
@@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest'
import { getAgentSessionOptionCatalog } from './agent-session-option-catalog'
import { removeAgentArgOption } from './agent-session-option-agent-args'
import {
removeOverriddenAgentSessionArgs,
resolveAgentSessionOptionLaunch
} from './agent-session-option-launch'
const removeCodexModel = getAgentSessionOptionCatalog('codex')!.modelApply.removeAgentArgs!
describe('catalog removers', () => {
it.each([
[['-c', 'model=o3']],
[['--config', 'model=o3']],
[['-c=model=o3']],
[['-cmodel=o3']],
[['--config=model=o3']]
])('strips Codex model config %j and yields the picked model to it', (tokens) => {
expect(removeCodexModel([...tokens, '--search'])).toEqual(['--search'])
expect(
resolveAgentSessionOptionLaunch('codex', { model: 'gpt-5.5' }, tokens).appliedValues
).toEqual({})
})
it('keeps other Codex config, including effort, when stripping the model', () => {
const tokens = [
'-c',
'model_reasoning_effort=high',
'-cmodel_reasoning_effort=low',
'-csandbox=x'
]
expect(removeCodexModel(tokens)).toEqual(tokens)
})
it('leaves everything after a terminator', () => {
const tokens = ['--', '-c', 'model=o3']
expect(removeCodexModel(tokens)).toEqual(tokens)
})
it('lets a chat pick replace a configured gemini model on server launches', () => {
expect(
removeOverriddenAgentSessionArgs('gemini', { model: 'gemini-2.5-pro' }, [
'-m',
'a',
'--model=b',
'-mc',
'--yolo'
])
).toEqual(['--yolo'])
})
})
describe('removeAgentArgOption', () => {
it('consumes an empty value', () => {
expect(removeAgentArgOption(['--model', '', '--yolo'], ['--model'])).toEqual(['--yolo'])
})
it('keeps a following flag', () => {
expect(removeAgentArgOption(['--model', '--yolo'], ['--model'])).toEqual(['--yolo'])
})
})
@@ -21,10 +21,8 @@ export type CatalogMidSessionApply =
export type CatalogOptionApply = {
launchArgs?: (value: SessionOptionValue) => string[]
/** Why: later free-form args win, so the launch record must discard any
* picker value that those args may have replaced. */
agentArgsOverride?: (tokens: readonly string[]) => boolean
/** Removes conflicting defaults before a more specific launch choice is inserted. */
/** Strips the free-form args that set this option. Why: later free-form args win, so
* anything this strips also tells the launch record to discard the picker value. */
removeAgentArgs?: (tokens: readonly string[]) => string[]
composedIntoModel?: true
midSession?: CatalogMidSessionApply
@@ -75,7 +73,7 @@ export type AgentSessionOptionCatalog = {
*
* Known gap: "no model flag" is unverified. A user `-m` in `agentArgs` launches that
* model while the picker, which never reads launch args, still names the CLI default.
* A real fix means threading `modelApply.agentArgsOverride` through to the surface. */
* A real fix means threading `modelApply.removeAgentArgs` through to the surface. */
defaultModelIsCliDefault?: true
listModels?: {
command: string
+8 -2
View File
@@ -1,5 +1,6 @@
import type { AgentType } from './agent-status-types'
import { findCatalogModel, getAgentSessionOptionCatalog } from './agent-session-option-catalog'
import type { CatalogOptionApply } from './agent-session-option-catalog-types'
import type { SessionOptionValue } from './native-chat-session-options'
export type ResolvedSessionOptionLaunch = {
@@ -7,6 +8,11 @@ export type ResolvedSessionOptionLaunch = {
appliedValues: Record<string, SessionOptionValue>
}
function isOverriddenByAgentArgs(apply: CatalogOptionApply, tokens: readonly string[]): boolean {
const kept = apply.removeAgentArgs?.(tokens)
return kept !== undefined && kept.length < tokens.length
}
export function removeOverriddenAgentSessionArgs(
agent: AgentType,
values: Record<string, SessionOptionValue> | null | undefined,
@@ -63,7 +69,7 @@ export function resolveAgentSessionOptionLaunch(
const composedModelId = catalog.composeModelValue
? catalog.composeModelValue(modelId, modelValues)
: modelId
const modelOverridden = catalog.modelApply.agentArgsOverride?.(trailingAgentArgs) === true
const modelOverridden = isOverriddenByAgentArgs(catalog.modelApply, trailingAgentArgs)
if (catalog.modelApply.launchArgs) {
args.push(...catalog.modelApply.launchArgs(composedModelId))
@@ -86,7 +92,7 @@ export function resolveAgentSessionOptionLaunch(
continue
}
args.push(...option.apply.launchArgs(value))
if (!modelOverridden && !option.apply.agentArgsOverride?.(trailingAgentArgs)) {
if (!modelOverridden && !isOverriddenByAgentArgs(option.apply, trailingAgentArgs)) {
appliedValues[option.id] = value
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ function matchesOption(token: string, aliases: readonly string[]): boolean {
}
export function findOptionOccurrence(
tokens: string[],
tokens: readonly string[],
aliases: readonly string[],
stopAtTerminator: boolean
): { index: number; consumed: number; value?: string } | null {