fix(mobile): send an AI button's saved agent arguments with its launch

The desktop's direct launches for "Fix checks", "Resolve conflicts" and commit
recovery pass the action's saved agent arguments to agent.launch; the phone
honoured the saved agent but dropped its arguments. It now sends them the same
way: absent when none are saved, so the host keeps the user's configured
defaults. A host that predates the field ignores it.
This commit is contained in:
Brennan Benson
2026-09-25 12:05:44 -07:00
parent ddcdc2eead
commit fe85e0346f
5 changed files with 35 additions and 10 deletions
@@ -50,6 +50,7 @@ export async function launchAgentInExistingWorkspace(args: {
hostCapabilities: readonly string[] | null | undefined
worktreeId: string
agent: TuiAgent
agentArgs?: string
prompt?: AgentLaunchPrompt
launchSource?: string
// Injected in tests; each call is one new operation, so a later tap never replays this one.
@@ -62,6 +63,7 @@ export async function launchAgentInExistingWorkspace(args: {
agent: args.agent,
worktreeId: args.worktreeId,
operationId: (args.mintOperationId ?? structuredSessionOperationId)(),
...(args.agentArgs !== undefined ? { agentArgs: args.agentArgs } : {}),
...(args.prompt ? { prompt: args.prompt } : {}),
...(args.launchSource ? { launchSource: args.launchSource } : {})
})
@@ -5,7 +5,10 @@
import type { GlobalSettings } from '../../../src/shared/global-settings-types'
import type { Repo } from '../../../src/shared/repo-types'
import type { SourceControlLaunchActionId } from '../../../src/shared/source-control-ai-actions'
import type {
SourceControlActionRecipe,
SourceControlLaunchActionId
} from '../../../src/shared/source-control-ai-actions'
import { resolveSourceControlActionRecipe } from '../../../src/shared/source-control-ai'
import {
pickSourceControlLaunchAgent,
@@ -17,8 +20,8 @@ import { isMobileTuiAgent } from '../tasks/mobile-tui-agents'
import type { MobileAgentLaunchContext } from './mobile-new-tab-agent-loader'
export type MobileSourceControlLaunchAgent =
| { kind: 'agent'; agent: TuiAgent }
| { kind: 'unavailable'; message: string }
/** `agentArgs`: the recipe's saved launch arguments, sent as the desktop sends them. */
{ kind: 'agent'; agent: TuiAgent; agentArgs?: string } | { kind: 'unavailable'; message: string }
type LaunchAgentSettings = Pick<
GlobalSettings,
@@ -32,7 +35,8 @@ export function resolveMobileSourceControlLaunchAgent(
): MobileSourceControlLaunchAgent {
const settings = readLaunchAgentSettings(context.settings)
const detectedAgents = context.detectedAgents.filter(isMobileTuiAgent)
const savedAgent = actionId ? readSavedAgent(settings, context.repo, actionId) : null
const recipe = actionId ? readSavedRecipe(settings, context.repo, actionId) : null
const savedAgent = readSourceControlLaunchRecipeAgentId(recipe)
// Why: the phone has no pre-launch dialog showing the pick, so a saved agent that can't run here
// is an error, as on the desktop's direct launch, rather than a silent swap to another agent.
if (
@@ -51,7 +55,11 @@ export function resolveMobileSourceControlLaunchAgent(
disabledAgents: settings?.disabledTuiAgents
})
return agent
? { kind: 'agent', agent }
? {
kind: 'agent',
agent,
...(recipe?.agentArgs !== undefined ? { agentArgs: recipe.agentArgs } : {})
}
: { kind: 'unavailable', message: 'No enabled AI agent was detected on this workspace host.' }
}
@@ -63,15 +71,13 @@ function readLaunchAgentSettings(settings: unknown): LaunchAgentSettings | null
return settings as LaunchAgentSettings
}
function readSavedAgent(
function readSavedRecipe(
settings: LaunchAgentSettings | null,
repo: unknown,
actionId: SourceControlLaunchActionId
): TuiAgent | null {
): SourceControlActionRecipe | null {
try {
return readSourceControlLaunchRecipeAgentId(
resolveSourceControlActionRecipe({ settings, repo: readRepoOverrides(repo), actionId })
)
return resolveSourceControlActionRecipe({ settings, repo: readRepoOverrides(repo), actionId })
} catch {
// A recipe the phone cannot read must not block the button; the default agent still applies.
return null
@@ -106,6 +106,8 @@ describe('launchAgentWithPrompt', () => {
prompt: { text: 'Fix the failing checks', delivery: 'submit' },
launchSource: 'task_page'
})
// No saved arguments: the host applies the user's configured defaults.
expect(launchParams(sendRequest)).not.toHaveProperty('agentArgs')
expect(sendRequest.mock.calls.some(([method]) => method === 'terminal.send')).toBe(false)
expect(
sendRequest.mock.calls.some(([method]) => method === 'session.tabs.createTerminal')
@@ -124,6 +126,18 @@ describe('launchAgentWithPrompt', () => {
expect(launchParams(sendRequest)).toMatchObject({ agent: 'claude' })
})
it("sends the action's saved agent arguments, as the desktop does", async () => {
const { client, sendRequest } = hostClient({
settings: {
defaultTuiAgent: 'claude',
sourceControlAi: { actions: { fixChecks: { agentArgs: '--model opus' } } }
},
launch: launchedWith({ delivery: 'submit', outcome: 'handed-to-terminal' })
})
await run(client)
expect(launchParams(sendRequest)).toMatchObject({ agent: 'claude', agentArgs: '--model opus' })
})
it('refuses rather than swaps when the saved agent is not on this host', async () => {
const { client, sendRequest } = hostClient({
settings: { sourceControlAi: { actions: { fixChecks: { agentId: 'gemini' } } } },
@@ -52,6 +52,7 @@ export async function launchAgentWithPrompt(args: {
hostCapabilities: args.hostCapabilities,
worktreeId: args.worktreeId,
agent: resolved.agent,
...(resolved.agentArgs !== undefined ? { agentArgs: resolved.agentArgs } : {}),
prompt: { text: args.prompt, delivery: 'submit' },
launchSource: args.launchSource
})
+2
View File
@@ -80,6 +80,7 @@ export function agentLaunchExistingParams(args: {
agent: TuiAgent
worktreeId: string
operationId: string
agentArgs?: string
prompt?: AgentLaunchPrompt
launchSource?: string
}): RpcSendParams<'agent.launchReplay'> {
@@ -87,6 +88,7 @@ export function agentLaunchExistingParams(args: {
agent: args.agent,
operationId: args.operationId,
target: { kind: 'existing', worktree: `id:${args.worktreeId}` },
...(args.agentArgs !== undefined ? { agentArgs: args.agentArgs } : {}),
...(args.prompt ? { prompt: args.prompt } : {}),
...(args.launchSource ? { launchSource: args.launchSource } : {})
}