From 03e85259408dd75b3a2d61d2fc102fae46c8de6c Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:55:44 -0700 Subject: [PATCH] fix(runtime): only override a paired host's agent args when the client chose one Forwarding the client's fully-resolved args made an unconfigured client push the built-in YOLO default onto a host whose own Agent Permissions say Manual. Send the override only when the caller passed args or the user actually configured a mode for this agent; otherwise stay silent and keep the host's choice. Manual's empty string still rides the wire, which is the #15373 fix. --- ...aunch-agent-in-new-tab-web-runtime.test.ts | 23 +++++++++++++++++-- .../src/lib/launch-agent-in-new-tab.ts | 13 ++++++++--- src/shared/tui-agent-launch-defaults.ts | 20 +++++++++++----- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts index 171f5988747..2c0e337a3ae 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts @@ -93,10 +93,11 @@ describe('launchAgentInNewTab paired web runtime', () => { activate: true, agentSessionKind: 'fresh', agent: 'claude', - // Why: the client resolves its default explicitly so the host cannot apply its own. - agentArgs: '--dangerously-skip-permissions', viewMode: 'terminal' }) + // Why (#15373): an unconfigured client must not push the built-in YOLO default onto a host + // whose own Agent Permissions say Manual; omission keeps the host's choice. + expect(mocks.createWebRuntimeSessionTerminal.mock.calls[0]?.[0]).not.toHaveProperty('agentArgs') expect(mocks.createTab).not.toHaveBeenCalled() await Promise.resolve() expect(mocks.setActiveTabType).toHaveBeenCalledWith('terminal') @@ -159,4 +160,22 @@ describe('launchAgentInNewTab paired web runtime', () => { }) ) }) + + it('forwards caller-supplied agent args even when the client has no configured default', async () => { + const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') + + launchAgentInNewTab({ + agent: 'claude', + worktreeId: 'wt-1', + groupId: 'group-1', + agentArgs: '--permission-mode plan' + }) + + expect(mocks.createWebRuntimeSessionTerminal).toHaveBeenCalledWith( + expect.objectContaining({ + agent: 'claude', + agentArgs: '--permission-mode plan' + }) + ) + }) }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.ts b/src/renderer/src/lib/launch-agent-in-new-tab.ts index 0c45a11c544..04cd65e90a2 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -17,6 +17,7 @@ import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-co import { isWebRuntimeSessionActive } from '@/runtime/web-runtime-session' import { launchAgentInWebHostTab } from '@/lib/launch-agent-web-host-tab' import { + hasConfiguredTuiAgentLaunchArgs, resolveTuiAgentLaunchArgs, resolveTuiAgentLaunchEnv } from '../../../shared/tui-agent-launch-defaults' @@ -106,6 +107,14 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI agentArgs !== undefined ? agentArgs : resolveTuiAgentLaunchArgs(agent, store.settings?.agentDefaultArgs) + // Why (#15373): a paired host re-resolves omitted args against its own settings, so a client that + // actually chose a permission mode must send it — including Manual's empty string. An unconfigured + // client has chosen nothing, so stay silent rather than forcing the built-in YOLO default on the host. + const pairedHostAgentArgs = + agentArgs !== undefined || + hasConfiguredTuiAgentLaunchArgs(agent, store.settings?.agentDefaultArgs) + ? effectiveAgentArgs + : undefined const agentEnv = resolveTuiAgentLaunchEnv(agent, store.settings?.agentDefaultEnv) const trimmedPrompt = prompt?.trim() ?? '' const hasPrompt = trimmedPrompt.length > 0 @@ -157,9 +166,7 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI promptDelivery, pastePromptAfterReady: pasteDraftAfterLaunch, submitPastedPrompt, - // Why: the raw param is undefined on ordinary UI launches, so omission let a headless host apply its - // own YOLO default (#15373). Send the resolved value — present-but-empty is an explicit no-args override. - agentArgs: effectiveAgentArgs, + agentArgs: pairedHostAgentArgs, // Why: omission means terminal locally, but would let a paired host apply // its own default; send the client's resolved terminal choice explicitly. viewMode: initialViewModeProps.viewMode ?? 'terminal', diff --git a/src/shared/tui-agent-launch-defaults.ts b/src/shared/tui-agent-launch-defaults.ts index 6e0dc16f08d..a9608a265a4 100644 --- a/src/shared/tui-agent-launch-defaults.ts +++ b/src/shared/tui-agent-launch-defaults.ts @@ -79,16 +79,24 @@ export function getTuiAgentDefaultEnv(agent: TuiAgent): Record { return { ...DEFAULT_TUI_AGENT_ENV[agent] } } +/** Manual is stored as `''`, so the key's presence — not its truthiness — is the user's explicit choice. */ +export function hasConfiguredTuiAgentLaunchArgs( + agent: TuiAgent, + configuredArgs: Partial> | null | undefined +): boolean { + return Boolean( + configuredArgs && + Object.hasOwn(configuredArgs, agent) && + typeof configuredArgs[agent] === 'string' + ) +} + export function resolveTuiAgentLaunchArgs( agent: TuiAgent, configuredArgs: Partial> | null | undefined ): string { - if ( - configuredArgs && - Object.hasOwn(configuredArgs, agent) && - typeof configuredArgs[agent] === 'string' - ) { - return configuredArgs[agent] ?? '' + if (hasConfiguredTuiAgentLaunchArgs(agent, configuredArgs)) { + return configuredArgs?.[agent] ?? '' } return getTuiAgentDefaultArgs(agent) }