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.
This commit is contained in:
Neil
2026-08-21 23:55:44 -07:00
parent 4932ed84a7
commit 03e8525940
3 changed files with 45 additions and 11 deletions
@@ -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'
})
)
})
})
@@ -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',
+14 -6
View File
@@ -79,16 +79,24 @@ export function getTuiAgentDefaultEnv(agent: TuiAgent): Record<string, string> {
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<Record<TuiAgent, string>> | null | undefined
): boolean {
return Boolean(
configuredArgs &&
Object.hasOwn(configuredArgs, agent) &&
typeof configuredArgs[agent] === 'string'
)
}
export function resolveTuiAgentLaunchArgs(
agent: TuiAgent,
configuredArgs: Partial<Record<TuiAgent, string>> | 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)
}