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'