From 923ef7cff142abb4deeece9dfa64518926960993 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:47:31 -0700 Subject: [PATCH] fix(native-chat): refuse a Command that can't run instead of silently running the stock CLI (#25667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(native-chat): honor custom Claude and Codex launch commands * fix(native-chat): sync launch failure localization catalogs * feat(native-chat): run Settings → Agents → Command as the native chat program Native chat ignored the Command setting, so a user who pointed it at a custom Claude or Codex build still got the stock CLI. The host now reads the Command per acquisition and per model-catalog probe, resolves it on the execution host as a program (an absolute path, ~ path, or a name on PATH), and spawns it with the normal structured arguments. A set Command that is not a runnable program refuses the start with a new agentCommandNotRunnable failure that names the setting; it never falls back to the stock CLI. * fix(native-chat): find a configured Claude Command on the launch PATH; clearer copy A Claude Command given as a bare name was looked up on Orca's own PATH, not the PATH the Claude child launches with (login shell plus Settings → Agents environment), so a name only that environment provides was refused. Codex already resolved against its launch environment. Claude now reads its launch env first and resolves a configured name against its PATH and home; the stock lookup with no Command is unchanged. The failure copy now states the rule (a program path or name, no arguments or variables) and names the Settings control (Reset). The catalog fingerprint doc notes that the program is not keyed, so a chat on an older program keeps refreshing the shared entry until it ends. * fix(native-chat): one PATH key for the Claude lookup and the child on Windows On Windows, an inherited `Path` and a Settings → Agents env `PATH` both reached the Claude child. The configured-program lookup read the first inserted twin (inherited), while Node's child_process keeps the lexicographically first (`PATH`, the user's), so a bare-name Command found only on the Settings PATH was refused. The Claude launch env now drops inherited case-twins of the overlay's variables (the rule the structured shell env already applies), so the lookup and the child read one PATH. pathEnvOf follows Node's win32 key rule, and Codex's lookup uses it too, so both agents read PATH one way. es/fr/ja copy now says the Command must be, as the English does. * fix(native-chat): refuse a Command that can't run instead of running the stock CLI A saved Settings → Agents → Command that did not resolve to a program (a command line such as `codex --profile work`, `FOO=1 claude` or `npx …`, a missing or relative path, a non-executable file) silently ran the stock Claude/Codex in native chat and its model-catalog probe. The user's choice was ignored with no sign of it. A set Command that does not resolve now refuses the start with a new agentCommandNotRunnable failure naming the setting, beside the existing Retry; the catalog probe refuses the same way and never lists the stock CLI. A blank Command keeps the stock lookup. On Windows a resolved file without a spawnable extension (.exe/.com/.cmd/.bat) refuses too, instead of failing to spawn with no word about the setting. * chore(native-chat): correct comments that say native chat ignores the Command After #25721 native chat runs a runnable Command, and this PR refuses one it can't run; four comments still said the Command applies to terminal launches only. The pre-spawn test fixture also drops the saved value from its error message, matching the resolver. --- .../claude/claude-model-catalog-probe.test.ts | 14 ++- .../codex/codex-model-catalog-probe.test.ts | 23 ++++- .../structured-agent-session-adapter.ts | 1 + ...uctured-agent-session-failure-text.test.ts | 4 + .../structured-agent-session-failure-text.ts | 3 +- ...ent-session-pre-spawn-first-answer.test.ts | 6 ++ ...tructured-agent-command-resolution.test.ts | 91 ++++++++++++++++++- .../structured-agent-command-resolution.ts | 27 +++++- ...-structured-launch-settings-wiring.test.ts | 6 +- .../orchestration-worker-start-mode.test.ts | 2 +- ...veChatLaunchRetry.command-failure.test.tsx | 43 +++++++++ .../agent-session-failure-words-text.ts | 6 ++ .../native-chat-session-option-discovery.ts | 2 +- .../src/i18n/en-runtime-required.json | 1 + src/renderer/src/i18n/locales/en.json | 1 + src/renderer/src/i18n/locales/es.json | 1 + src/renderer/src/i18n/locales/fr.json | 1 + src/renderer/src/i18n/locales/ja.json | 1 + src/renderer/src/i18n/locales/ko.json | 1 + src/renderer/src/i18n/locales/zh.json | 1 + .../src/lib/agent-launch-route-input.test.ts | 2 +- src/shared/agent-session-failure-copy.ts | 2 + src/shared/agent-session-failure-words.ts | 2 + src/shared/agent-session-failure.ts | 1 + src/shared/agent-session-refusal-details.ts | 2 + .../agent-session-refusal-reason-words.ts | 1 + ...ctured-agent-session-dispatch-rejection.ts | 2 + .../structured-native-chat-launch-route.ts | 2 +- 28 files changed, 231 insertions(+), 18 deletions(-) create mode 100644 src/renderer/src/components/native-chat/NativeChatLaunchRetry.command-failure.test.tsx diff --git a/src/main/claude/claude-model-catalog-probe.test.ts b/src/main/claude/claude-model-catalog-probe.test.ts index 3f7792b33de..e77d63d90b0 100644 --- a/src/main/claude/claude-model-catalog-probe.test.ts +++ b/src/main/claude/claude-model-catalog-probe.test.ts @@ -59,8 +59,8 @@ function listedResult(): DiscoverResult { } describe('claude model catalog probe', () => { - it.each([`"${process.execPath}"`, 'npx claude', '/missing/claude', './claude'])( - 'lists with the session executable for Command %s', + it.each([`"${process.execPath}"`, ''])( + 'lists with the session executable for Command %j', async (command) => { const deps = probeDeps(command) const captured: DiscoverInput[] = [] @@ -134,6 +134,16 @@ describe('claude model catalog probe', () => { await expect(probe('/homes/a')).rejects.toThrow(/listed no models/) }) + it.each(['npx claude', 'claude --verbose', '/missing/claude', './claude'])( + 'lists nothing, and never probes the stock CLI, for Command %s', + async (command) => { + const discover = vi.fn() + const probe = createClaudeModelCatalogProbe({ ...probeDeps(command), discover }) + await expect(probe('/homes/a')).rejects.toMatchObject({ reason: 'agentCommandNotRunnable' }) + expect(discover).not.toHaveBeenCalled() + } + ) + it.runIf(process.platform !== 'win32')( 'lists through a supervised one-shot that answers after its input ends', async () => { diff --git a/src/main/codex/codex-model-catalog-probe.test.ts b/src/main/codex/codex-model-catalog-probe.test.ts index d7f3edff47b..a3aeeebc4af 100644 --- a/src/main/codex/codex-model-catalog-probe.test.ts +++ b/src/main/codex/codex-model-catalog-probe.test.ts @@ -13,8 +13,8 @@ const MODEL_ROW = { } describe('codex model catalog probe', () => { - it.each([`"${process.execPath}"`, 'npx codex', '/missing/codex', './codex'])( - 'lists with the session executable for Command %s', + it.each([`"${process.execPath}"`, ''])( + 'lists with the session executable for Command %j', async (command) => { // The env a user's shell/config resolves for sessions, PATH included. const resolveEnvironment = async (): Promise => ({ @@ -111,4 +111,23 @@ describe('codex model catalog probe', () => { }) await expect(probe('/homes/a')).rejects.toThrow(/listed no models/) }) + + it.each(['npx codex', 'codex --profile work', '/missing/codex', './codex'])( + 'lists nothing, and never probes the stock CLI, for Command %s', + async (command) => { + const runSession = vi.fn() + const probe = createCodexModelCatalogProbe({ + resolveEnvironment: async () => ({ PATH: '/bin' }), + resolveCommand: (options) => + resolveStructuredAgentCommand( + 'codex', + { agentCmdOverrides: { codex: command } }, + options + ), + runSession + }) + await expect(probe('/homes/a')).rejects.toMatchObject({ reason: 'agentCommandNotRunnable' }) + expect(runSession).not.toHaveBeenCalled() + } + ) }) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts index b05e89136f0..f111081ab44 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts @@ -133,6 +133,7 @@ export type AgentSessionPreSpawnReason = Extract< | 'accountSwitchInProgress' | 'managedAccountUnsupported' | 'launchFolderMissing' + | 'agentCommandNotRunnable' > /** Acquisition failed with first-hand proof that no provider process existed. */ diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts index b508cc9e757..c49b79b06e6 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts @@ -99,6 +99,10 @@ describe('structuredAgentSessionStartFailure', () => { [ 'managedAccountUnsupported', 'While a Claude account is added in WSL, Claude chats need a Windows Claude account. Choose or add one in Claude Accounts settings, then send your message again.' + ], + [ + 'agentCommandNotRunnable', + "Claude's Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it." ] ] as const)('words a start refused for %s by that situation', (reason, sentence) => { const code = 'agent_session_operation_invalid' diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts index 75f7769e7a7..a48848fb717 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts @@ -29,7 +29,8 @@ const TYPED_START_REFUSALS = [ 'managedAccountEnvOverride', 'accountSwitchInProgress', 'managedAccountUnsupported', - 'launchFolderMissing' + 'launchFolderMissing', + 'agentCommandNotRunnable' ] as const satisfies readonly (AgentSessionFailureKind & AgentSessionRefusalReason<'agent_session_operation_invalid'>)[] diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts index e6c3cead931..7dfd624b496 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts @@ -163,6 +163,12 @@ describe('a create that fails before any process spawns', () => { 'launchFolderMissing', 'The folder this chat ran in no longer exists. Restore it to continue this chat.' ], + [ + 'a Command setting that names no runnable program', + 'the claude Command setting is not a runnable program', + 'agentCommandNotRunnable', + "Claude's Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it." + ], [ "Orca's own reason", 'claude sessions pin CLAUDE_CONFIG_DIR, not CODEX_HOME', diff --git a/src/main/native-chat/structured-agent-command-resolution.test.ts b/src/main/native-chat/structured-agent-command-resolution.test.ts index 3fa758cfe5b..a2fc166a1a3 100644 --- a/src/main/native-chat/structured-agent-command-resolution.test.ts +++ b/src/main/native-chat/structured-agent-command-resolution.test.ts @@ -5,6 +5,11 @@ import { afterEach, describe, expect, it } from 'vitest' import { resolveStructuredAgentCommand } from './structured-agent-command-resolution' import { resolveCliCommand } from '../../shared/node-cli-command-resolution' +const NOT_RUNNABLE = expect.objectContaining({ + name: 'AgentSessionPreSpawnError', + reason: 'agentCommandNotRunnable' +}) + const scratch: string[] = [] afterEach(() => { for (const dir of scratch.splice(0)) { @@ -62,7 +67,12 @@ describe('structured agent executable resolution', () => { expect(resolveStructuredAgentCommand('claude', settings)).toBe(second.command) }) - it('uses the stock executable for missing files, directories, relative paths and shell lines', () => { + it.each(['', ' ', undefined])('uses the stock executable when the Command is %j', (command) => { + const settings = command === undefined ? {} : { agentCmdOverrides: { claude: command } } + expect(resolveStructuredAgentCommand('claude', settings)).toBe(resolveCliCommand('claude')) + }) + + it('refuses missing files, directories, relative paths and command lines instead of the stock executable', () => { const { directory } = executable() const folder = join(directory, 'folder') mkdirSync(folder) @@ -71,18 +81,89 @@ describe('structured agent executable resolution', () => { folder, './claude', 'npx claude', - 'wrapper --flag' + 'wrapper --flag', + 'FOO=1 claude', + '$HOME/bin/claude' ]) { const settings = { agentCmdOverrides: { claude: command } } - expect(resolveStructuredAgentCommand('claude', settings)).toBe(resolveCliCommand('claude')) + expect(() => resolveStructuredAgentCommand('claude', settings)).toThrow(NOT_RUNNABLE) } }) it.skipIf(process.platform === 'win32')('requires executable permission on Unix', () => { const { command } = executable() chmodSync(command, 0o644) - expect( + expect(() => resolveStructuredAgentCommand('claude', { agentCmdOverrides: { claude: `"${command}"` } }) - ).toBe(resolveCliCommand('claude')) + ).toThrow(NOT_RUNNABLE) + }) + + it('keeps the saved value out of the refusal, since a command line can carry a secret', () => { + expect(() => + resolveStructuredAgentCommand('codex', { + agentCmdOverrides: { codex: 'TOKEN=sk-secret codex' } + }) + ).toThrow(expect.not.objectContaining({ message: expect.stringContaining('sk-secret') })) + }) + + describe('on Windows', () => { + function fileIn(directory: string, name: string): string { + const file = join(directory, name) + writeFileSync(file, '') + return file + } + + it('refuses an extensionless path, which Windows cannot spawn', () => { + const { directory } = executable() + const command = fileIn(directory, 'claude') + fileIn(directory, 'claude.cmd') + expect(() => + resolveStructuredAgentCommand( + 'claude', + { agentCmdOverrides: { claude: command } }, + { platform: 'win32' } + ) + ).toThrow(NOT_RUNNABLE) + }) + + it('refuses a name whose only match on PATH is extensionless', () => { + const { directory } = executable() + fileIn(directory, 'my-claude') + expect(() => + resolveStructuredAgentCommand( + 'claude', + { agentCmdOverrides: { claude: 'my-claude' } }, + { platform: 'win32', pathEnv: directory, homePath: directory } + ) + ).toThrow(NOT_RUNNABLE) + }) + + it.each(['claude.exe', 'claude.com', 'claude.cmd', 'claude.bat'])( + 'runs an explicit %s path', + (name) => { + const { directory } = executable() + const command = fileIn(directory, name) + expect( + resolveStructuredAgentCommand( + 'claude', + { agentCmdOverrides: { claude: `"${command}"` } }, + { platform: 'win32' } + ) + ).toBe(command) + } + ) + + it('finds the .cmd shim for a bare name, as the stock lookup does', () => { + const { directory } = executable() + fileIn(directory, 'my-claude') + const shim = fileIn(directory, 'my-claude.cmd') + expect( + resolveStructuredAgentCommand( + 'claude', + { agentCmdOverrides: { claude: 'my-claude' } }, + { platform: 'win32', pathEnv: directory, homePath: directory } + ) + ).toBe(shim) + }) }) }) diff --git a/src/main/native-chat/structured-agent-command-resolution.ts b/src/main/native-chat/structured-agent-command-resolution.ts index 7ce9afcb77b..9cc1f28bd01 100644 --- a/src/main/native-chat/structured-agent-command-resolution.ts +++ b/src/main/native-chat/structured-agent-command-resolution.ts @@ -4,7 +4,11 @@ import { resolveCliCommand, resolveExecutableCommand } from '../../shared/node-cli-command-resolution' -import { structuredAgentCommandToken } from '../../shared/tui-agent-launch-command-override' +import { + hasExplicitTuiLaunchCommand, + structuredAgentCommandToken +} from '../../shared/tui-agent-launch-command-override' +import { AgentSessionPreSpawnError } from './agent-session-wire/structured-agent-session-adapter' type CommandSettings = Partial> type CommandOptions = NonNullable[1]> @@ -24,11 +28,28 @@ function resolveOverride( : null } -/** Re-read the existing setting for every session acquisition and catalog probe. */ +// Windows starts .exe/.com directly and .cmd/.bat through Orca's shim handling; an extensionless +// file would fail to spawn with no word about the setting. +function spawnableOn(platform: NodeJS.Platform, command: string): boolean { + return platform !== 'win32' || /\.(exe|com|cmd|bat)$/i.test(command) +} + +/** Re-read the existing setting for every session acquisition and catalog probe. A set Command + * that names no runnable program refuses the start rather than quietly running the stock CLI. */ export function resolveStructuredAgentCommand( agent: 'claude' | 'codex', settings: CommandSettings, options: CommandOptions = {} ): string { - return resolveOverride(agent, settings, options) ?? resolveCliCommand(agent, options) + if (!hasExplicitTuiLaunchCommand(settings, agent)) { + return resolveCliCommand(agent, options) + } + const command = resolveOverride(agent, settings, options) + if (!command || !spawnableOn(options.platform ?? process.platform, command)) { + // The value stays out of the message: a saved command line can carry a secret. + throw new AgentSessionPreSpawnError(`the ${agent} Command setting is not a runnable program`, { + reason: 'agentCommandNotRunnable' + }) + } + return command } diff --git a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts index e9738615b5f..dd81e303b41 100644 --- a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts +++ b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts @@ -37,9 +37,13 @@ describe('execution host structured launch settings wiring', () => { settings.agentDefaultArgs = { claude: '--model second', codex: '' } expect(await deps?.resolveLaunchArgs?.('claude')).toEqual(['--model', 'second']) expect(await deps?.resolveLaunchArgs?.('codex')).toEqual([]) + const notRunnable = expect.objectContaining({ reason: 'agentCommandNotRunnable' }) settings.agentCmdOverrides = { claude: 'wrapper --arg' } - expect(deps?.resolveClaudeCommand?.()).toBe(resolveCliCommand('claude')) + expect(() => deps?.resolveClaudeCommand?.()).toThrow(notRunnable) settings.agentCmdOverrides = { codex: '/missing/codex' } + expect(() => deps?.resolveCodexCommand?.()).toThrow(notRunnable) + settings.agentCmdOverrides = {} + expect(deps?.resolveClaudeCommand?.()).toBe(resolveCliCommand('claude')) expect(deps?.resolveCodexCommand?.()).toBe(resolveCliCommand('codex')) }) }) diff --git a/src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts index 93e9bb25a8b..aee3bfefb6d 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts @@ -87,7 +87,7 @@ describe('a structured default this dispatch cannot honour', () => { expect(decide({ params: { agent: 'codex', worktree: 'current' } }).mode).toBe('structured') }) - // A custom launch command applies to terminal launches only; native chat ignores it. + // A custom launch command never decides the surface; native chat runs it as the program. it.each([ ['claude', 'claude-wrapper'], ['codex', 'codex-nightly'] diff --git a/src/renderer/src/components/native-chat/NativeChatLaunchRetry.command-failure.test.tsx b/src/renderer/src/components/native-chat/NativeChatLaunchRetry.command-failure.test.tsx new file mode 100644 index 00000000000..74faee5fcff --- /dev/null +++ b/src/renderer/src/components/native-chat/NativeChatLaunchRetry.command-failure.test.tsx @@ -0,0 +1,43 @@ +// @vitest-environment happy-dom +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, expect, it } from 'vitest' +import { sayAgentSessionFailureEnglish } from '../../../../shared/agent-session-failure-copy' +import { agentSessionFailureSentence } from '../../../../shared/agent-session-failure-words' +import { agentSessionRefusalFailure } from '../../../../shared/agent-session-write-failure' +import { NativeChatLaunchRetry } from './NativeChatLaunchRetry' + +afterEach(cleanup) + +it('names the Command setting when it is not a program Orca can run', () => { + const failure = agentSessionRefusalFailure({ + code: 'agent_session_operation_invalid', + details: { reason: 'agentCommandNotRunnable' } + }) + render( + {}} + /> + ) + expect( + screen.getByText( + "Chat could not be started. Claude's Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it." + ) + ).toBeTruthy() + expect(screen.getByRole('button', { name: 'Retry' })).toBeTruthy() +}) + +it('reads naturally where no agent name is known', () => { + expect( + agentSessionFailureSentence( + { kind: 'agentCommandNotRunnable' }, + 'row', + {}, + sayAgentSessionFailureEnglish + ) + ).toBe( + "The agent's Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it." + ) +}) diff --git a/src/renderer/src/components/native-chat/agent-session-failure-words-text.ts b/src/renderer/src/components/native-chat/agent-session-failure-words-text.ts index 2c455769a78..0fa34b19aaa 100644 --- a/src/renderer/src/components/native-chat/agent-session-failure-words-text.ts +++ b/src/renderer/src/components/native-chat/agent-session-failure-words-text.ts @@ -108,6 +108,12 @@ const PIECES: Record + translate( + 'components.native-chat.failureWords.agentCommandNotRunnable', + COPY.agentCommandNotRunnable, + values + ), chooseClaudeAccount: () => translate( 'components.native-chat.failureWords.chooseClaudeAccount', diff --git a/src/renderer/src/components/native-chat/native-chat-session-option-discovery.ts b/src/renderer/src/components/native-chat/native-chat-session-option-discovery.ts index b4520a225c0..ac43cd4c74b 100644 --- a/src/renderer/src/components/native-chat/native-chat-session-option-discovery.ts +++ b/src/renderer/src/components/native-chat/native-chat-session-option-discovery.ts @@ -127,7 +127,7 @@ export async function discoverNativeChatCatalogModels( const hostCatalogAgent = agent === 'claude' ? ('claude' as const) : agent === 'codex' ? ('codex' as const) : null // Only `local` proves a native pane: a paired runtime's key also covers its SSH/WSL worktrees. - // A custom launch command runs a binary the structured catalog never lists; the CLI listing honors it. + // Terminal-backed chat runs the full custom command line, which only the CLI listing models. if ( hostCatalogAgent && hostKey === LOCAL_COMMIT_MESSAGE_HOST_KEY && diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index a36edc792f1..2933c7538f5 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2796,6 +2796,7 @@ "managedAccountEnvOverride": "This Claude launch sets its own Anthropic sign-in variables. Remove them to use a managed Claude account.", "managedAccountUnsupported": "While a Claude account is added in WSL, Claude chats need a Windows Claude account.", "launchFolderMissing": "The folder this chat ran in no longer exists. Restore it to continue this chat.", + "agentCommandNotRunnable": "{{agent}}'s Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it.", "noTurnToStop": "{{agent}} had no turn running to stop.", "notDelivered": "This message was not delivered.", "notDeliveredSendAgain": "This message was not delivered. Send it again to continue.", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 8816b3a4dbc..a8320eb8c96 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -17798,6 +17798,7 @@ "accountSwitchInProgress": "A Claude account switch is in progress. Try again after it finishes.", "managedAccountUnsupported": "While a Claude account is added in WSL, Claude chats need a Windows Claude account.", "launchFolderMissing": "The folder this chat ran in no longer exists. Restore it to continue this chat.", + "agentCommandNotRunnable": "{{agent}}'s Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it.", "chooseClaudeAccount": "Choose or add one in Claude Accounts settings.", "chooseClaudeAccountThenRunCommand": "Choose or add one in Claude Accounts settings, then run /{{command}} again.", "chooseClaudeAccountThenSend": "Choose or add one in Claude Accounts settings, then send your message again.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 9d39b2425eb..5af0be4c76b 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -17573,6 +17573,7 @@ "accountSwitchInProgress": "Hay un cambio de cuenta de Claude en curso. Vuelva a intentarlo cuando termine.", "managedAccountUnsupported": "Mientras haya una cuenta de Claude agregada en WSL, los chats de Claude necesitan una cuenta de Claude de Windows.", "launchFolderMissing": "La carpeta en la que se ejecutaba este chat ya no existe. Restáurela para continuar este chat.", + "agentCommandNotRunnable": "{{agent}}: el Comando en Ajustes → Agentes debe ser la ruta o el nombre de un programa que Orca pueda encontrar, sin argumentos ni variables. Cámbielo o restablézcalo con Reiniciar.", "chooseClaudeAccount": "Elija o agregue una en la configuración de Cuentas de Claude.", "chooseClaudeAccountThenRunCommand": "Elija o agregue una en la configuración de Cuentas de Claude y vuelva a ejecutar /{{command}}.", "chooseClaudeAccountThenSend": "Elija o agregue una en la configuración de Cuentas de Claude y vuelva a enviar su mensaje.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 9df7cf8896d..93954938d4c 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -17674,6 +17674,7 @@ "accountSwitchInProgress": "Un changement de compte Claude est en cours. Réessayez une fois qu'il sera terminé.", "managedAccountUnsupported": "Tant qu'un compte Claude est ajouté dans WSL, les chats Claude nécessitent un compte Claude Windows.", "launchFolderMissing": "Le dossier dans lequel ce chat s'exécutait n'existe plus. Restaurez-le pour poursuivre ce chat.", + "agentCommandNotRunnable": "{{agent}} : la Commande dans Paramètres → Agents doit être le chemin ou le nom d'un programme qu'Orca peut trouver, sans arguments ni variables. Modifiez-la ou réinitialisez-la.", "chooseClaudeAccount": "Choisissez-en un ou ajoutez-en un dans les paramètres Comptes Claude.", "chooseClaudeAccountThenRunCommand": "Choisissez-en un ou ajoutez-en un dans les paramètres Comptes Claude, puis relancez /{{command}}.", "chooseClaudeAccountThenSend": "Choisissez-en un ou ajoutez-en un dans les paramètres Comptes Claude, puis renvoyez votre message.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 22c7c495c2d..676b1d728d9 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -17608,6 +17608,7 @@ "accountSwitchInProgress": "Claude アカウントの切り替え中です。完了してからもう一度お試しください。", "managedAccountUnsupported": "WSL に Claude アカウントが追加されている間、Claude チャットには Windows の Claude アカウントが必要です。", "launchFolderMissing": "このチャットが実行されていたフォルダーは存在しなくなりました。このチャットを続けるには、フォルダーを復元してください。", + "agentCommandNotRunnable": "[設定] → [Agent] にある {{agent}} の [コマンド] には、Orca が見つけられるプログラムのパスまたは名前を、引数や変数なしで指定する必要があります。変更するか、[リセット] してください。", "chooseClaudeAccount": "Claude アカウントの設定で選択または追加してください。", "chooseClaudeAccountThenRunCommand": "Claude アカウントの設定で選択または追加してから、/{{command}} をもう一度実行してください。", "chooseClaudeAccountThenSend": "Claude アカウントの設定で選択または追加してから、メッセージをもう一度送信してください。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index a101803edc1..829fa949ea6 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -17608,6 +17608,7 @@ "accountSwitchInProgress": "Claude 계정 전환이 진행 중입니다. 완료된 후 다시 시도하세요.", "managedAccountUnsupported": "WSL에 Claude 계정이 추가되어 있는 동안에는 Claude 채팅에 Windows Claude 계정이 필요합니다.", "launchFolderMissing": "이 채팅이 실행되던 폴더가 더 이상 존재하지 않습니다. 이 채팅을 계속하려면 폴더를 복원하세요.", + "agentCommandNotRunnable": "{{agent}}의 설정 → 에이전트 → 명령은 Orca가 찾을 수 있는 프로그램 경로나 이름이어야 하며, 인수나 변수를 포함할 수 없습니다. 변경하거나 재설정하세요.", "chooseClaudeAccount": "Claude 계정 설정에서 선택하거나 추가하세요.", "chooseClaudeAccountThenRunCommand": "Claude 계정 설정에서 선택하거나 추가한 다음 /{{command}}을(를) 다시 실행하세요.", "chooseClaudeAccountThenSend": "Claude 계정 설정에서 선택하거나 추가한 다음 메시지를 다시 보내세요.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index b76846075ad..48e88654708 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -17573,6 +17573,7 @@ "accountSwitchInProgress": "正在切换 Claude 账户。请在切换完成后重试。", "managedAccountUnsupported": "当 WSL 中添加了 Claude 账户时,Claude 聊天需要 Windows Claude 账户。", "launchFolderMissing": "此聊天运行所在的文件夹已不存在。请恢复该文件夹以继续此聊天。", + "agentCommandNotRunnable": "{{agent}} 在设置 → 智能体中的命令必须是 Orca 能找到的程序路径或名称,且不能包含参数或变量。请修改或重置。", "chooseClaudeAccount": "请在 Claude 账户设置中选择或添加一个。", "chooseClaudeAccountThenRunCommand": "请在 Claude 账户设置中选择或添加一个,然后重新运行 /{{command}}。", "chooseClaudeAccountThenSend": "请在 Claude 账户设置中选择或添加一个,然后重新发送您的消息。", diff --git a/src/renderer/src/lib/agent-launch-route-input.test.ts b/src/renderer/src/lib/agent-launch-route-input.test.ts index 47738bde69b..8a1ac92a224 100644 --- a/src/renderer/src/lib/agent-launch-route-input.test.ts +++ b/src/renderer/src/lib/agent-launch-route-input.test.ts @@ -415,7 +415,7 @@ describe('buildAgentLaunchRouteInput', () => { } }) - // A launch command override applies to terminal launches only, here or on the server. + // A launch command override never decides the surface, here or on the server. it("does not route on this machine's launch command override for the server", () => { const settings = { ...STRUCTURED_SETTINGS, agentCmdOverrides: { claude: 'claude-wrapper' } } expect( diff --git a/src/shared/agent-session-failure-copy.ts b/src/shared/agent-session-failure-copy.ts index a55978aea29..4575485b456 100644 --- a/src/shared/agent-session-failure-copy.ts +++ b/src/shared/agent-session-failure-copy.ts @@ -37,6 +37,8 @@ export const AGENT_SESSION_FAILURE_COPY = { 'While a Claude account is added in WSL, Claude chats need a Windows Claude account.', launchFolderMissing: 'The folder this chat ran in no longer exists. Restore it to continue this chat.', + agentCommandNotRunnable: + "{{agent}}'s Command in Settings → Agents must be a program path or name Orca can find, with no arguments or variables. Change it or reset it.", chooseClaudeAccount: 'Choose or add one in Claude Accounts settings.', chooseClaudeAccountThenRunCommand: 'Choose or add one in Claude Accounts settings, then run /{{command}} again.', diff --git a/src/shared/agent-session-failure-words.ts b/src/shared/agent-session-failure-words.ts index abb601ff1b9..b8dacdfd813 100644 --- a/src/shared/agent-session-failure-words.ts +++ b/src/shared/agent-session-failure-words.ts @@ -220,6 +220,8 @@ const FAILURE_SENTENCES = { managedAccountEnvOverride: (_context, _fact, _surface, say) => say('managedAccountEnvOverride'), accountSwitchInProgress: (_context, _fact, _surface, say) => say('accountSwitchInProgress'), launchFolderMissing: (_context, _fact, _surface, say) => say('launchFolderMissing'), + agentCommandNotRunnable: (context, _fact, _surface, say) => + say('agentCommandNotRunnable', agent(say, context)), managedAccountUnsupported: (context, _fact, _surface, say) => joinSentences([ say('managedAccountUnsupported'), diff --git a/src/shared/agent-session-failure.ts b/src/shared/agent-session-failure.ts index fe3180be6c8..fe69dd2243e 100644 --- a/src/shared/agent-session-failure.ts +++ b/src/shared/agent-session-failure.ts @@ -30,6 +30,7 @@ export const AGENT_SESSION_FAILURE_KINDS = [ 'accountSwitchInProgress', 'managedAccountUnsupported', 'launchFolderMissing', + 'agentCommandNotRunnable', 'providerExited', 'restartFailed', 'providerRejected', diff --git a/src/shared/agent-session-refusal-details.ts b/src/shared/agent-session-refusal-details.ts index f296e42df35..b9ef64f2c47 100644 --- a/src/shared/agent-session-refusal-details.ts +++ b/src/shared/agent-session-refusal-details.ts @@ -57,6 +57,8 @@ export const AGENT_SESSION_REFUSAL_REASONS = { 'managedAccountUnsupported', /** A floating chat resumes only in the folder it ran in, and that folder is gone. */ 'launchFolderMissing', + /** Settings → Agents → Command names no program this host can run. */ + 'agentCommandNotRunnable', /** The agent started, then Orca could not open the chat's conversation for it. */ 'attachFailed' ], diff --git a/src/shared/agent-session-refusal-reason-words.ts b/src/shared/agent-session-refusal-reason-words.ts index d8756a3c9ec..d6c1b0354fb 100644 --- a/src/shared/agent-session-refusal-reason-words.ts +++ b/src/shared/agent-session-refusal-reason-words.ts @@ -97,6 +97,7 @@ const REASON_WORDS = { accountSwitchInProgress: { fact: 'accountSwitchInProgress', action: 'wait' }, managedAccountUnsupported: { fact: 'managedAccountUnsupported', action: 'actFirst' }, launchFolderMissing: { fact: 'launchFolderMissing', action: 'actFirst' }, + agentCommandNotRunnable: { fact: 'agentCommandNotRunnable', action: 'actFirst' }, attachFailed: codeWords('retry') }, agent_session_ownership_unknown: { diff --git a/src/shared/structured-agent-session-dispatch-rejection.ts b/src/shared/structured-agent-session-dispatch-rejection.ts index a67b76cd3b8..69bd7760aea 100644 --- a/src/shared/structured-agent-session-dispatch-rejection.ts +++ b/src/shared/structured-agent-session-dispatch-rejection.ts @@ -91,6 +91,7 @@ const KIND_CATEGORY = { accountSwitchInProgress: 'startFailed', managedAccountUnsupported: 'startFailed', launchFolderMissing: 'startFailed', + agentCommandNotRunnable: 'startFailed', restartFailed: 'startFailed', providerRejected: 'content', attachmentInvalid: 'content', @@ -117,6 +118,7 @@ const KIND_VERDICT = { accountSwitchInProgress: 'failure', managedAccountUnsupported: 'failure', launchFolderMissing: 'failure', + agentCommandNotRunnable: 'failure', restartFailed: 'failure', providerRejected: 'failure', attachmentInvalid: 'failure', diff --git a/src/shared/structured-native-chat-launch-route.ts b/src/shared/structured-native-chat-launch-route.ts index e1627c305dc..f92bac17e84 100644 --- a/src/shared/structured-native-chat-launch-route.ts +++ b/src/shared/structured-native-chat-launch-route.ts @@ -30,7 +30,7 @@ export type StructuredNativeChatBlocker = | 'reused-terminal' | 'agent-without-structured-session' /** The launch names a start directory outside its workspace, which only a PTY can apply. The - * configured launch command and arguments are not read: they apply to terminal launches only. */ + * configured launch command and arguments never decide the surface. */ | 'custom-start-directory' | 'remote-execution-host' | 'project-runtime'