mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
* Add host-owned model and effort defaults to agent settings * Unify terminal defaults and raw argument precedence * Keep agent model defaults typed through creation * Preserve quick-create prompt through preparation * fix: complete execution-host preference boundary and CI contracts * Preserve raw options until a replacement is emitted * Complete launch choice ownership and consumer contracts * Align launch contracts with compiler boundaries and consolidate proof * Name exported fixture mocks and verify ordered chip removal * Preserve typed choices across compound legacy publication edits * Preserve non-emitted composed launch choices through legacy edits * Preserve rollback omissions and settle agent launches before success * Carry terminal draft ownership through launch completion * Keep inline draft delivery independent of terminal projection * test: consolidate launch ownership contracts * test: check web launch fixture array types * test: align launch fixtures and conformity records * test: import the shell-ready marker from its declaring module * test: give caller routing the web host tab's completion contract
295 lines
11 KiB
TypeScript
295 lines
11 KiB
TypeScript
import {
|
|
removeOverriddenAgentSessionArgs,
|
|
resolveAgentSessionOptionLaunch
|
|
} from './agent-session-option-launch'
|
|
import { resolveAgentChoiceLayers, yieldAgentChoicesToArgs } from './agent-choice-layers'
|
|
import { resolveTuiAgentLaunchRawArgs } from './tui-agent-launch-defaults'
|
|
import {
|
|
hasExtraAgentArgs,
|
|
parseExtraAgentArgs,
|
|
mergeParsedExtraAgentArgs
|
|
} from './automation-extra-agent-args'
|
|
import type { AgentLaunchProfileSettings } from './tui-agent-launch-defaults'
|
|
import { readLaunchPermissionChoice } from './tui-agent-permission-choice'
|
|
import type { SessionOptionValue } from './native-chat/input/native-chat-session-options'
|
|
import { agentArgTerminatorIndex } from './agent-session-option-agent-args'
|
|
import { getTuiAgentLaunchCommand, TUI_AGENT_CONFIG } from './tui-agent-config'
|
|
import {
|
|
quoteStartupArg,
|
|
tokenizeStartupCommand,
|
|
type AgentStartupShell
|
|
} from './tui-agent-startup-shell'
|
|
import type { TuiAgent } from './tui-agent'
|
|
|
|
// Why: Enter on Codex's "Update available" screen picks "Update now", which installs and exits.
|
|
const STARTUP_UPDATE_PROMPT_SUPPRESSION_ARGS: Partial<Record<TuiAgent, readonly string[]>> = {
|
|
codex: ['-c', 'check_for_update_on_startup=false'],
|
|
aider: ['--no-check-update', '--no-show-release-notes'],
|
|
cursor: ['--disable-auto-update']
|
|
}
|
|
|
|
const STARTUP_UPDATE_PROMPT_SUPPRESSION_ENV: Partial<
|
|
Record<TuiAgent, Readonly<Record<string, string>>>
|
|
> = {
|
|
claude: { DISABLE_AUTOUPDATER: '1' },
|
|
'claude-agent-teams': { DISABLE_AUTOUPDATER: '1' },
|
|
opencode: { OPENCODE_DISABLE_AUTOUPDATE: 'true' },
|
|
opencode2: { OPENCODE_DISABLE_AUTOUPDATE: 'true' },
|
|
antigravity: { AGY_CLI_DISABLE_AUTO_UPDATE: 'true' },
|
|
grok: { GROK_DISABLE_AUTOUPDATER: '1' },
|
|
pi: { PI_SKIP_VERSION_CHECK: '1' },
|
|
kimi: { KIMI_CODE_NO_AUTO_UPDATE: '1', KIMI_CLI_NO_AUTO_UPDATE: '1' }
|
|
}
|
|
|
|
// Why not win32: Codex there reads console input records and needs its burst for pastes (windowsInputRecordPasteNewline).
|
|
// Elsewhere Orca's terminals bracket every paste, so the burst can only misread relayed typing (#34496).
|
|
function agentTerminalInputArgs(
|
|
agent: TuiAgent,
|
|
platform: NodeJS.Platform | null
|
|
): readonly string[] {
|
|
return agent === 'codex' && platform !== null && platform !== 'win32'
|
|
? ['-c', 'disable_paste_burst=true']
|
|
: []
|
|
}
|
|
|
|
function isCodexExecutable(token: string | undefined): boolean {
|
|
const basename = token?.split(/[\\/]/).pop()?.toLowerCase()
|
|
return basename !== undefined && /^codex(?:\.(?:exe|cmd|bat|ps1))?$/.test(basename)
|
|
}
|
|
|
|
/** Appends the per-launch terminal input flags to a recorded agent command. */
|
|
export function withAgentTerminalInputArgs(
|
|
command: string,
|
|
agent: TuiAgent,
|
|
/** OS that runs the agent; null when the host has not reported it. */
|
|
platform: NodeJS.Platform | null,
|
|
shell: AgentStartupShell
|
|
): string {
|
|
const inputArgs = agentTerminalInputArgs(agent, platform)
|
|
const tokens = tokenizeStartupCommand(command, shell)
|
|
// Why: only a lone Codex invocation is safe; a wrapper may not forward -c, a compound line would
|
|
// hand it to another command, and the command may already set it or end its options with `--`.
|
|
return inputArgs.length &&
|
|
!/[;&|<>`$()\n]/.test(command) &&
|
|
tokens.ok &&
|
|
isCodexExecutable(tokens.tokens[0]) &&
|
|
!command.includes('disable_paste_burst') &&
|
|
!tokens.tokens.includes('--')
|
|
? `${command} ${inputArgs.map((arg) => quoteStartupArg(arg, shell)).join(' ')}`
|
|
: command
|
|
}
|
|
|
|
export type ResolvedAgentLaunchCommand =
|
|
| {
|
|
ok: true
|
|
command: string
|
|
resumeLaunch: { agentCommand: string; agentArgs?: string }
|
|
appliedSessionOptions: Record<string, SessionOptionValue>
|
|
startupEnv?: Readonly<Record<string, string>>
|
|
}
|
|
| { ok: false; error: string }
|
|
|
|
export function resolveAgentLaunchCommand(args: {
|
|
agent: TuiAgent
|
|
cmdOverrides: Partial<Record<TuiAgent, string>>
|
|
platform: NodeJS.Platform
|
|
shell: AgentStartupShell
|
|
agentArgs?: string | null
|
|
sessionOptions?: Record<string, SessionOptionValue>
|
|
sessionOptionsOverrideAgentArgs?: boolean
|
|
isRemote?: boolean
|
|
/** Launch-only, like session options: a resumed session must not inherit it. */
|
|
suppressStartupUpdatePrompt?: boolean
|
|
/** OS that runs the agent when `platform` only picks quoting; null when unreported. */
|
|
agentHostPlatform?: NodeJS.Platform | null
|
|
}): ResolvedAgentLaunchCommand {
|
|
const override = args.cmdOverrides[args.agent]
|
|
const command =
|
|
override ||
|
|
getTuiAgentLaunchCommand(TUI_AGENT_CONFIG[args.agent], args.platform, {
|
|
isRemote: args.isRemote
|
|
})
|
|
const suppressionArgs = args.suppressStartupUpdatePrompt
|
|
? (STARTUP_UPDATE_PROMPT_SUPPRESSION_ARGS[args.agent] ?? [])
|
|
: []
|
|
const prefixSuppressionArgs = args.agent === 'aider' ? [] : suppressionArgs
|
|
const launchHead = withAgentTerminalInputArgs(
|
|
prefixSuppressionArgs.length
|
|
? `${command} ${prefixSuppressionArgs.map((arg) => quoteStartupArg(arg, args.shell)).join(' ')}`
|
|
: command,
|
|
args.agent,
|
|
args.agentHostPlatform === undefined ? args.platform : args.agentHostPlatform,
|
|
args.shell
|
|
)
|
|
const trailingTokens = args.agentArgs?.trim()
|
|
? tokenizeStartupCommand(args.agentArgs.trim(), args.shell)
|
|
: { ok: true as const, tokens: [], spans: [] }
|
|
if (!trailingTokens.ok) {
|
|
return { ok: false, error: `CLI arguments are invalid: ${trailingTokens.error}` }
|
|
}
|
|
const resolvedOptions = resolveAgentSessionOptionLaunch(
|
|
args.agent,
|
|
args.sessionOptions,
|
|
args.sessionOptionsOverrideAgentArgs ? [] : trailingTokens.tokens,
|
|
!args.sessionOptionsOverrideAgentArgs,
|
|
trailingTokens.tokens
|
|
)
|
|
if (override && args.sessionOptionsOverrideAgentArgs) {
|
|
const overrideTokens = tokenizeStartupCommand(override, args.shell)
|
|
if (!overrideTokens.ok) {
|
|
return { ok: false, error: `Agent command override is invalid: ${overrideTokens.error}` }
|
|
}
|
|
const commandOverrideOptions = resolveAgentSessionOptionLaunch(
|
|
args.agent,
|
|
args.sessionOptions,
|
|
overrideTokens.tokens,
|
|
false
|
|
)
|
|
if (
|
|
Object.entries(resolvedOptions.appliedValues).some(
|
|
([key, value]) => commandOverrideOptions.appliedValues[key] !== value
|
|
)
|
|
) {
|
|
return {
|
|
ok: false,
|
|
error:
|
|
'Agent command override conflicts with the requested launch preferences. Remove model, effort or permission flags from the command override.'
|
|
}
|
|
}
|
|
}
|
|
const rawTokens = trailingTokens.tokens
|
|
const keptTokens = args.sessionOptionsOverrideAgentArgs
|
|
? removeOverriddenAgentSessionArgs(args.agent, resolvedOptions.appliedValues, rawTokens)
|
|
: rawTokens
|
|
const render = (tokens: readonly string[]): string =>
|
|
tokens.map((token) => quoteStartupArg(token, args.shell)).join(' ')
|
|
const withArgs = (suffix: string, head = command): string => (suffix ? `${head} ${suffix}` : head)
|
|
const initialTokens = args.sessionOptionsOverrideAgentArgs
|
|
? insertBeforeTerminator(args.agent, keptTokens, resolvedOptions.args)
|
|
: [...resolvedOptions.args, ...keptTokens]
|
|
// Aider's last boolean flag wins, even over configured --check-update.
|
|
const launchTokens =
|
|
args.agent === 'aider'
|
|
? insertBeforeTerminator(args.agent, initialTokens, suppressionArgs)
|
|
: initialTokens
|
|
// Restart retains explicit policy and surviving raw pins, while providers restore model seeds.
|
|
const resumeArgs = render(
|
|
insertBeforeTerminator(args.agent, keptTokens, resolvedOptions.permissionArgs ?? [])
|
|
)
|
|
return {
|
|
ok: true,
|
|
command: withArgs(render(launchTokens), launchHead),
|
|
resumeLaunch: {
|
|
agentCommand: withArgs(resumeArgs),
|
|
...(keptTokens.length !== rawTokens.length ||
|
|
keptTokens.some((token, index) => token !== rawTokens[index]) ||
|
|
resolvedOptions.permissionArgs
|
|
? { agentArgs: resumeArgs }
|
|
: {})
|
|
},
|
|
appliedSessionOptions: resolvedOptions.appliedValues,
|
|
...(args.suppressStartupUpdatePrompt && STARTUP_UPDATE_PROMPT_SUPPRESSION_ENV[args.agent]
|
|
? { startupEnv: STARTUP_UPDATE_PROMPT_SUPPRESSION_ENV[args.agent] }
|
|
: {})
|
|
}
|
|
}
|
|
|
|
function insertBeforeTerminator(
|
|
agent: TuiAgent,
|
|
tokens: readonly string[],
|
|
inserted: readonly string[]
|
|
): string[] {
|
|
const terminator = agentArgTerminatorIndex(agent, tokens)
|
|
return [...tokens.slice(0, terminator), ...inserted, ...tokens.slice(terminator)]
|
|
}
|
|
|
|
/** Resolve authored defaults against the original raw text before compiling either into argv. */
|
|
export function terminalSessionOptionInputs(args: {
|
|
agent: TuiAgent
|
|
settings?: AgentLaunchProfileSettings | null
|
|
sessionOptions: Readonly<Record<string, SessionOptionValue>> | undefined
|
|
yieldToAgentArgs?: boolean
|
|
agentArgs: string | null | undefined
|
|
shell: AgentStartupShell
|
|
command?: string
|
|
extraAgentArgs?: string
|
|
}): {
|
|
agentArgs?: string | null
|
|
sessionOptions?: Record<string, SessionOptionValue>
|
|
sessionOptionsOverrideAgentArgs?: true
|
|
} {
|
|
const permission = readLaunchPermissionChoice(args.sessionOptions)
|
|
let raw =
|
|
args.agentArgs === undefined
|
|
? resolveTuiAgentLaunchRawArgs(
|
|
args.agent,
|
|
args.settings,
|
|
permission ? 'own' : undefined,
|
|
args.shell
|
|
)
|
|
: args.agentArgs
|
|
const extras = hasExtraAgentArgs(args.extraAgentArgs)
|
|
? parseExtraAgentArgs({ agent: args.agent, extraAgentArgs: args.extraAgentArgs })
|
|
: undefined
|
|
if (extras && !extras.ok) {
|
|
throw new Error(extras.error)
|
|
}
|
|
if (extras?.ok) {
|
|
const merged = mergeParsedExtraAgentArgs({
|
|
agent: args.agent,
|
|
defaultArgs: raw,
|
|
extras,
|
|
shell: args.shell
|
|
})
|
|
if (!merged.ok) {
|
|
throw new Error(merged.error)
|
|
}
|
|
raw = merged.agentArgs
|
|
}
|
|
const authored = args.settings?.agentLaunchChoices?.[args.agent]
|
|
let defaultValues = {
|
|
...authored?.options,
|
|
...(authored?.model ? { model: authored.model } : {})
|
|
}
|
|
if (Object.keys(defaultValues).length) {
|
|
const command = tokenizeStartupCommand(args.command ?? '', args.shell)
|
|
defaultValues = yieldAgentChoicesToArgs(args.agent, defaultValues, [
|
|
...(command.ok ? command.tokens : []),
|
|
...(extras?.ok ? extras.tokens : [])
|
|
])
|
|
}
|
|
const { model: defaultModel, ...defaultOptions } = defaultValues
|
|
const choices = resolveAgentChoiceLayers({
|
|
agent: args.agent,
|
|
defaults: { ...(defaultModel ? { model: defaultModel } : {}), options: defaultOptions },
|
|
action: args.sessionOptions
|
|
? {
|
|
...(typeof args.sessionOptions.model === 'string'
|
|
? { model: args.sessionOptions.model }
|
|
: {}),
|
|
options: Object.fromEntries(
|
|
Object.entries(args.sessionOptions)
|
|
.filter(([id]) => !['model', 'mode', 'permissionMode'].includes(id))
|
|
.map(([id, value]) => [id, String(value)])
|
|
),
|
|
...(permission ? { permissionMode: permission } : {})
|
|
}
|
|
: undefined,
|
|
argumentOwner: args.agentArgs === undefined ? 'defaults' : 'launch',
|
|
userArgs: raw,
|
|
launchPreferencesYieldToAgentArgs: args.yieldToAgentArgs,
|
|
shell: args.shell
|
|
})
|
|
const sessionOptions = {
|
|
...choices.options,
|
|
...(choices.model ? { model: choices.model } : {}),
|
|
...(choices.permissionMode ? { permissionMode: choices.permissionMode } : {})
|
|
}
|
|
return {
|
|
agentArgs: raw,
|
|
...(Object.keys(sessionOptions).length > 0
|
|
? { sessionOptions, sessionOptionsOverrideAgentArgs: true as const }
|
|
: {})
|
|
}
|
|
}
|