From 3e5466f454115cc2bbefc400fda4b86dc2b8b4b1 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Fri, 11 Sep 2026 15:24:29 -0700 Subject: [PATCH] fix: preserve centralized agent launch behavior --- .../folder-workspace-composer-submit.ts | 2 + .../composer-state/full-creation-execution.ts | 2 + .../src/lib/launch-agent-in-new-tab.ts | 2 + .../src/lib/launch-agent-session-terminal.ts | 58 ++++---- .../src/lib/launch-agent-session.test.ts | 67 +++++++++ src/renderer/src/lib/launch-agent-session.ts | 130 +++++++++++------- ...nch-work-item-direct-agent-routing.test.ts | 3 +- .../launch-work-item-direct-agent-routing.ts | 3 +- .../src/lib/launch-work-item-direct.ts | 4 + .../src/lib/onboarding-folder-agent-launch.ts | 2 + ...eb-runtime-worktree-terminal-after-wake.ts | 2 + .../lib/worktree-activation-store-contract.ts | 2 + .../worktree-activation-surface-selection.ts | 2 + src/renderer/src/lib/worktree-activation.ts | 17 ++- .../src/lib/worktree-creation-flow-execute.ts | 3 +- ...rktree-creation-structured-session.test.ts | 3 +- .../worktree-creation-structured-session.ts | 15 ++ .../src/lib/worktree-default-terminal-tabs.ts | 43 +++--- .../lib/worktree-initial-terminal-seeding.ts | 2 +- 19 files changed, 259 insertions(+), 103 deletions(-) diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts index 7573c2a75dc..05dc8620202 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts @@ -216,6 +216,8 @@ export async function submitFolderWorkspaceCreate({ tuiCustomization: { agentArgs }, launchPlatform, initialSessionOptions: startupPlan?.sessionOptions, + ...(plan ? { launchPlan: plan } : {}), + ...(startup ? { terminalStartup: startup } : {}), visibility: 'reveal', launchSource, pendingFirstAgentMessageRename diff --git a/src/renderer/src/hooks/composer-state/full-creation-execution.ts b/src/renderer/src/hooks/composer-state/full-creation-execution.ts index 06b81f3a3fa..740b6449951 100644 --- a/src/renderer/src/hooks/composer-state/full-creation-execution.ts +++ b/src/renderer/src/hooks/composer-state/full-creation-execution.ts @@ -237,6 +237,8 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) { prompt: startupPlan?.draftPrompt ?? submitStartupPrompt, promptDelivery: startupPlan?.draftPrompt ? 'draft' : 'auto-submit', initialSessionOptions: startupPlan?.sessionOptions, + launchPlan, + ...(startup ? { terminalStartup: startup } : {}), visibility: 'reveal', launchSource: telemetrySource === 'onboarding' ? 'onboarding' : 'new_workspace_composer', 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 f2a7ca886cc..90911c0c5d1 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -145,11 +145,13 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent const structuredSettlement = launchAgentSession(store, { agent, workspaceId: worktreeId, + ...(groupId ? { groupId } : {}), prompt: trimmedPrompt, promptDelivery: viewModePromptDelivery, tuiCustomization: { cwd: initialCwd, agentArgs }, initialSessionOptions: startupPlan.sessionOptions, onPromptDelivered, + launchPlan: plan, visibility: 'reveal', launchSource: launchSource ?? 'tab_bar_quick_launch' }).then((outcome) => { diff --git a/src/renderer/src/lib/launch-agent-session-terminal.ts b/src/renderer/src/lib/launch-agent-session-terminal.ts index 2804568f69a..7fbec6983fd 100644 --- a/src/renderer/src/lib/launch-agent-session-terminal.ts +++ b/src/renderer/src/lib/launch-agent-session-terminal.ts @@ -36,9 +36,6 @@ function terminalStartup( ...(request.promptDelivery === 'draft' && prepared.trimmedPrompt ? { launchDraftText: prepared.trimmedPrompt } : {}), - ...(prepared.pasteDraftAfterLaunch !== null - ? { draftPrompt: prepared.pasteDraftAfterLaunch } - : {}), ...(request.agent === 'command-code' && prepared.hasPrompt ? { initialAgentStatus: { agent: request.agent, prompt: prepared.trimmedPrompt } } : {}), @@ -150,28 +147,30 @@ export async function launchTerminalSession( if (request.signal?.aborted) { return { tabId: null } } - const prepared = prepareAgentInNewTabLaunch({ - agent: request.agent, - worktreeId: request.workspaceId, - ...(request.prompt !== undefined ? { prompt: request.prompt } : {}), - ...(request.promptDelivery ? { promptDelivery: request.promptDelivery } : {}), - ...(request.tuiCustomization?.agentArgs !== undefined - ? { agentArgs: request.tuiCustomization.agentArgs } - : {}), - ...(request.tuiCustomization?.cwd !== undefined - ? { initialCwd: request.tuiCustomization.cwd } - : {}), - ...(request.launchPlatform ? { launchPlatform: request.launchPlatform } : {}), - ...(request.initialSessionOptions - ? { initialSessionOptions: request.initialSessionOptions } - : {}), - launchSource: request.launchSource, - ...(request.onPromptDelivered ? { onPromptDelivered: request.onPromptDelivered } : {}) - }) - if (!prepared) { + const prepared = request.terminalStartup + ? null + : prepareAgentInNewTabLaunch({ + agent: request.agent, + worktreeId: request.workspaceId, + ...(request.prompt !== undefined ? { prompt: request.prompt } : {}), + ...(request.promptDelivery ? { promptDelivery: request.promptDelivery } : {}), + ...(request.tuiCustomization?.agentArgs !== undefined + ? { agentArgs: request.tuiCustomization.agentArgs } + : {}), + ...(request.tuiCustomization?.cwd !== undefined + ? { initialCwd: request.tuiCustomization.cwd } + : {}), + ...(request.launchPlatform ? { launchPlatform: request.launchPlatform } : {}), + ...(request.initialSessionOptions + ? { initialSessionOptions: request.initialSessionOptions } + : {}), + launchSource: request.launchSource, + ...(request.onPromptDelivered ? { onPromptDelivered: request.onPromptDelivered } : {}) + }) + if (!prepared && !request.terminalStartup) { return { tabId: null, error: new Error('Could not build the agent startup command.') } } - const startup = terminalStartup(request, prepared) + const startup = request.terminalStartup ?? terminalStartup(request, prepared!) const state = useAppStore.getState() const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(state, request.workspaceId) let tabId: string | null = null @@ -181,6 +180,7 @@ export async function launchTerminalSession( startup, agent: request.agent, cwd: request.tuiCustomization?.cwd, + targetGroupId: request.groupId, activate: request.visibility === 'reveal' }) tabId = created?.hostTabId ?? null @@ -188,6 +188,7 @@ export async function launchTerminalSession( activateAndRevealWorkspace(request.workspaceId, { revealInSidebar: true, sidebarRevealBehavior: 'auto', + ...(request.groupId ? { targetGroupId: request.groupId } : {}), providesInitialSurface: true }) } @@ -199,6 +200,7 @@ export async function launchTerminalSession( revealInSidebar: true, sidebarRevealBehavior: 'auto', createNewTerminalForStartup: true, + ...(request.groupId ? { targetGroupId: request.groupId } : {}), startup, providesInitialSurface: true, ...(request.tuiCustomization?.cwd ? { initialCwd: request.tuiCustomization.cwd } : {}) @@ -212,9 +214,15 @@ export async function launchTerminalSession( undefined, undefined, undefined, - { activateCreatedTabs: false, createNewTerminalForStartup: true } + { + activateCreatedTabs: false, + createNewTerminalForStartup: true, + ...(request.groupId ? { targetGroupId: request.groupId } : {}) + } ) } - const promptDeliveryResult = deliverTerminalPrompt(request, prepared, tabId) + const promptDeliveryResult = prepared + ? deliverTerminalPrompt(request, prepared, tabId) + : undefined return { tabId, ...(promptDeliveryResult ? { promptDeliveryResult } : {}) } } diff --git a/src/renderer/src/lib/launch-agent-session.test.ts b/src/renderer/src/lib/launch-agent-session.test.ts index c3d81d596bb..6c458241b27 100644 --- a/src/renderer/src/lib/launch-agent-session.test.ts +++ b/src/renderer/src/lib/launch-agent-session.test.ts @@ -62,6 +62,53 @@ describe('launchAgentSession', () => { expect(mocks.launchTerminalSession).toHaveBeenCalledWith(request) }) + it('passes the requested split group through terminal fallback launches', async () => { + mocks.planAgentSessionLaunch.mockReturnValue({ route: 'terminal-tui' }) + mocks.launchTerminalSession.mockResolvedValue({ tabId: 'terminal-1' }) + + await launchAgentSession({} as never, { ...request, groupId: 'group-1' }) + + expect(mocks.launchTerminalSession).toHaveBeenCalledWith( + expect.objectContaining({ groupId: 'group-1' }) + ) + }) + + it('refuses to turn a structured resume into a fresh terminal launch', async () => { + mocks.planAgentSessionLaunch.mockReturnValue({ route: 'terminal-tui' }) + + await expect( + launchAgentSession({} as never, { + ...request, + resumeFrom: { providerSessionId: 'provider-1' } + }) + ).resolves.toMatchObject({ + kind: 'failed', + error: expect.any(Error) + }) + expect(mocks.launchTerminalSession).not.toHaveBeenCalled() + }) + + it('honors an explicit no-fallback request when structured support disappears', async () => { + mocks.planAgentSessionLaunch.mockReturnValue({ route: 'terminal-tui' }) + + await expect( + launchAgentSession({} as never, { ...request, terminalFallback: false }) + ).resolves.toMatchObject({ kind: 'failed', error: expect.any(Error) }) + expect(mocks.launchTerminalSession).not.toHaveBeenCalled() + }) + + it('uses a caller-planned route and targets the created workspace', async () => { + const launch = vi.fn().mockResolvedValue({ kind: 'structured', sessionId: 'session-1' }) + const launchPlan = { route: 'structured-native-chat', launch } as never + + await expect( + launchAgentSession({} as never, { ...request, launchPlan }) + ).resolves.toMatchObject({ kind: 'structured', sessionId: 'session-1' }) + + expect(mocks.planAgentSessionLaunch).not.toHaveBeenCalled() + expect(launch).toHaveBeenCalledWith(expect.anything(), { worktreeId: request.workspaceId }) + }) + it('activates a folder workspace before selecting its structured chat', async () => { const order: string[] = [] mocks.activateAndRevealWorkspace.mockImplementation(() => { @@ -107,6 +154,26 @@ describe('launchAgentSession', () => { }) }) + it('reports a terminal fallback error instead of accepting the refusal', async () => { + const launch = vi.fn(async (hooks) => { + await expect(hooks.legacyFallback()).rejects.toThrow('terminal failed') + return { kind: 'failed', error: new Error('terminal failed') } + }) + mocks.planAgentSessionLaunch.mockReturnValue({ + route: 'structured-native-chat', + launch + }) + mocks.launchTerminalSession.mockResolvedValue({ + tabId: null, + error: new Error('terminal failed') + }) + + await expect(launchAgentSession({} as never, request)).resolves.toMatchObject({ + kind: 'failed', + error: expect.any(Error) + }) + }) + it('suppresses generic failure ownership when terminal fallback is disabled', async () => { const launch = vi.fn().mockResolvedValue({ kind: 'failed', error: new Error('refused') }) mocks.planAgentSessionLaunch.mockReturnValue({ route: 'structured-native-chat', launch }) diff --git a/src/renderer/src/lib/launch-agent-session.ts b/src/renderer/src/lib/launch-agent-session.ts index 63be5acb667..7c3a3c572f1 100644 --- a/src/renderer/src/lib/launch-agent-session.ts +++ b/src/renderer/src/lib/launch-agent-session.ts @@ -3,7 +3,7 @@ import type { LaunchSource } from '../../../shared/telemetry-events' import type { TuiAgent } from '../../../shared/tui-agent' import type { SessionOptionValue } from '../../../shared/native-chat-session-options' import { workspaceKindForWorktreeId, type AgentLaunchRouteStore } from './agent-launch-route-input' -import { planAgentSessionLaunch } from './agent-session-launch-plan' +import { planAgentSessionLaunch, type AgentSessionLaunchPlan } from './agent-session-launch-plan' import type { NativeChatLaunchPromptDelivery } from './native-chat-initial-view-mode' import { activateStructuredAgentSessionById } from './structured-agent-session-tab-activation' import type { StructuredPromptDeliveryResult } from './structured-agent-session-launch-prompt' @@ -13,10 +13,13 @@ import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector' import { useAppStore } from '@/store' import { activateAndRevealWorkspace } from './worktree-activation' import { launchTerminalSession } from './launch-agent-session-terminal' +import type { WorktreeStartupPayload } from './worktree-startup-payload' export type AgentSessionLaunchRequest = { agent: TuiAgent workspaceId: string + /** Existing tab group for a quick-launch fallback; terminal creation must stay in that pane. */ + groupId?: string prompt?: string promptDelivery?: NativeChatLaunchPromptDelivery tuiCustomization?: { cwd?: string | null; agentArgs?: string | null } @@ -28,6 +31,10 @@ export type AgentSessionLaunchRequest = { signal?: AbortSignal launchSource: LaunchSource terminalFallback?: boolean + /** A launch decided before the workspace row existed. Reuse it instead of re-planning. */ + launchPlan?: AgentSessionLaunchPlan + /** Startup payload already built by the caller for a terminal refusal fallback. */ + terminalStartup?: WorktreeStartupPayload pendingFirstAgentMessageRename?: boolean reconcileUnknownLaunch?: boolean } @@ -72,25 +79,40 @@ export async function launchAgentSession( store: AgentLaunchRouteStore, request: AgentSessionLaunchRequest ): Promise { - const plan = planAgentSessionLaunch(store, { - agent: request.agent, - workspace: { - kind: workspaceKindForWorktreeId(request.workspaceId), - worktreeId: request.workspaceId - }, - ...(request.prompt !== undefined ? { prompt: request.prompt } : {}), - ...(request.promptDelivery ? { promptDelivery: request.promptDelivery } : {}), - ...(request.tuiCustomization ? { tuiCustomization: request.tuiCustomization } : {}), - ...(request.initialSessionOptions - ? { initialSessionOptions: request.initialSessionOptions } - : {}), - ...(request.resumeFrom ? { resumeFrom: request.resumeFrom } : {}), - ...(request.onPromptDelivered ? { onPromptDelivered: request.onPromptDelivered } : {}), - ...(request.terminalFallback === false ? { notifyFailure: false } : {}), - ...(request.reconcileUnknownLaunch !== undefined - ? { reconcileUnknownLaunch: request.reconcileUnknownLaunch } - : {}) - }) + const plan = + request.launchPlan ?? + planAgentSessionLaunch(store, { + agent: request.agent, + workspace: { + kind: workspaceKindForWorktreeId(request.workspaceId), + worktreeId: request.workspaceId + }, + ...(request.prompt !== undefined ? { prompt: request.prompt } : {}), + ...(request.promptDelivery ? { promptDelivery: request.promptDelivery } : {}), + ...(request.tuiCustomization ? { tuiCustomization: request.tuiCustomization } : {}), + ...(request.initialSessionOptions + ? { initialSessionOptions: request.initialSessionOptions } + : {}), + ...(request.resumeFrom ? { resumeFrom: request.resumeFrom } : {}), + ...(request.onPromptDelivered ? { onPromptDelivered: request.onPromptDelivered } : {}), + ...(request.terminalFallback === false ? { notifyFailure: false } : {}), + ...(request.reconcileUnknownLaunch !== undefined + ? { reconcileUnknownLaunch: request.reconcileUnknownLaunch } + : {}) + }) + if ( + plan.route !== 'structured-native-chat' && + (request.resumeFrom !== undefined || request.terminalFallback === false) + ) { + return { + kind: 'failed', + error: new Error( + request.resumeFrom + ? 'Resuming an agent session requires structured chat support.' + : 'Structured chat launch is unavailable for this workspace.' + ) + } + } if (plan.route !== 'structured-native-chat') { try { const terminal = await launchTerminalSession(request) @@ -113,39 +135,45 @@ export async function launchAgentSession( let viaRefusal = false let fallback: TerminalLaunchResult | null = null try { - const settlement = await plan.launch({ - ...(request.terminalFallback === false - ? {} - : { - legacyFallback: async () => { - viaRefusal = true - fallback = await launchTerminalSession(request) - return { - primaryTabId: fallback.tabId, - ...(fallback.promptDeliveryResult - ? { promptDeliveryResult: fallback.promptDeliveryResult } - : {}) + const settlement = await plan.launch( + { + ...(request.terminalFallback === false + ? {} + : { + legacyFallback: async () => { + viaRefusal = true + fallback = await launchTerminalSession(request) + if (fallback.error) { + throw fallback.error + } + return { + primaryTabId: fallback.tabId, + ...(fallback.promptDeliveryResult + ? { promptDeliveryResult: fallback.promptDeliveryResult } + : {}) + } + } + }), + ...(request.visibility === 'reveal' + ? { + onStructuredReady: (sessionId: string) => { + activateAndRevealWorkspace(request.workspaceId, { + providesInitialSurface: true + }) + activateStructuredAgentSessionById({ worktreeId: request.workspaceId, sessionId }) + structuredTabId = + useAppStore + .getState() + .unifiedTabsByWorktree[request.workspaceId]?.find( + (tab) => tab.contentType === 'agent-session' && tab.entityId === sessionId + )?.id ?? `agent-session:${sessionId}` } } - }), - ...(request.visibility === 'reveal' - ? { - onStructuredReady: (sessionId: string) => { - activateAndRevealWorkspace(request.workspaceId, { - providesInitialSurface: true - }) - activateStructuredAgentSessionById({ worktreeId: request.workspaceId, sessionId }) - structuredTabId = - useAppStore - .getState() - .unifiedTabsByWorktree[request.workspaceId]?.find( - (tab) => tab.contentType === 'agent-session' && tab.entityId === sessionId - )?.id ?? `agent-session:${sessionId}` - } - } - : {}), - ...(request.signal ? { signal: request.signal } : {}) - }) + : {}), + ...(request.signal ? { signal: request.signal } : {}) + }, + request.launchPlan ? { worktreeId: request.workspaceId } : undefined + ) if (!settlement) { return { kind: 'failed', error: new Error('Structured launch did not settle.') } } diff --git a/src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts b/src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts index 00c3ecd336c..aaa8c0629c8 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts @@ -54,7 +54,8 @@ describe('settleDirectWorkItemStructuredLaunch', () => { prompt: 'Fix the route', promptDelivery: 'draft', visibility: 'reveal', - launchSource: 'task_page' + launchSource: 'task_page', + launchPlan: structuredPlan }) }) diff --git a/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts b/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts index 5514a5af549..25d53b18cb1 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent-routing.ts @@ -148,6 +148,7 @@ export async function settleDirectWorkItemStructuredLaunch(args: { ...(plan.promptDelivery ? { promptDelivery: plan.promptDelivery } : {}), ...(args.agentArgs !== undefined ? { tuiCustomization: { agentArgs: args.agentArgs } } : {}), ...(args.launchPlatform ? { launchPlatform: args.launchPlatform } : {}), + launchPlan: plan, visibility: 'reveal', launchSource: args.launchSource }) @@ -173,7 +174,7 @@ export async function settleDirectWorkItemStructuredLaunch(args: { completed: false, structuredLaunch: false, visibilityUnknown: false, - failed: false, + failed: settlement.tabId === null, primaryTabId: settlement.tabId } case 'visibility-unknown': diff --git a/src/renderer/src/lib/launch-work-item-direct.ts b/src/renderer/src/lib/launch-work-item-direct.ts index a5fddde9c97..9bf9c486838 100644 --- a/src/renderer/src/lib/launch-work-item-direct.ts +++ b/src/renderer/src/lib/launch-work-item-direct.ts @@ -280,6 +280,10 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom primaryTabId = structuredResult.primaryTabId } + if (structuredResult.failed) { + return false + } + if (startupPlanFailed) { toast.error(agentLaunchCommandErrorMessage()) return false diff --git a/src/renderer/src/lib/onboarding-folder-agent-launch.ts b/src/renderer/src/lib/onboarding-folder-agent-launch.ts index e6bfae43225..b0c054b3ab9 100644 --- a/src/renderer/src/lib/onboarding-folder-agent-launch.ts +++ b/src/renderer/src/lib/onboarding-folder-agent-launch.ts @@ -78,6 +78,8 @@ export async function revealOnboardingFolderWithAgentLaunch(args: { await launchAgentSession(useAppStore.getState(), { agent: args.launch.agent, workspaceId: args.worktreeId, + ...(plan ? { launchPlan: plan } : {}), + ...(args.launch.fallbackStartup ? { terminalStartup: args.launch.fallbackStartup } : {}), initialSessionOptions: (args.launch.startup ?? args.launch.fallbackStartup)?.sessionOptions, visibility: 'reveal', launchSource: 'onboarding' diff --git a/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts b/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts index 683863b50da..553c7841da4 100644 --- a/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts +++ b/src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts @@ -27,6 +27,7 @@ export async function spawnWebRuntimeAgentSurface( startup?: WorktreeStartupPayload agent?: TuiAgent | null cwd?: string | null + targetGroupId?: string activate?: boolean } ): Promise { @@ -109,6 +110,7 @@ export async function spawnWebRuntimeAgentSurface( ? { agent: launchAgent } : {}), ...(opts?.cwd ? { cwd: opts.cwd } : {}), + ...(opts?.targetGroupId ? { targetGroupId: opts.targetGroupId } : {}), activate: opts?.activate !== false, selectWorktree: false }) diff --git a/src/renderer/src/lib/worktree-activation-store-contract.ts b/src/renderer/src/lib/worktree-activation-store-contract.ts index 63ce180ffed..df49e915f32 100644 --- a/src/renderer/src/lib/worktree-activation-store-contract.ts +++ b/src/renderer/src/lib/worktree-activation-store-contract.ts @@ -61,6 +61,8 @@ export type WorktreeActivationStore = Partial & { } export type InitialTerminalOptions = { + /** Prefer this existing split group when creating an explicit startup terminal. */ + targetGroupId?: string activateCreatedTabs?: boolean backendStartupTerminalSpawned?: boolean /** Create a preserved fallback startup beside setup/default terminals. */ diff --git a/src/renderer/src/lib/worktree-activation-surface-selection.ts b/src/renderer/src/lib/worktree-activation-surface-selection.ts index ef4b5c1386a..28bf62679ad 100644 --- a/src/renderer/src/lib/worktree-activation-surface-selection.ts +++ b/src/renderer/src/lib/worktree-activation-surface-selection.ts @@ -16,6 +16,8 @@ export type WorktreeActivationSurfaceSelection = { } export type WorktreeActivationOptions = WorktreeActivationSurfaceSelection & { + /** Prefer this existing split group when creating an explicit startup terminal. */ + targetGroupId?: string startup?: WorktreeStartupPayload initialCwd?: string setup?: WorktreeSetupLaunch diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts index 520178f49e3..fa0f71bd33c 100644 --- a/src/renderer/src/lib/worktree-activation.ts +++ b/src/renderer/src/lib/worktree-activation.ts @@ -51,7 +51,8 @@ export type ActivateAndRevealResult = { function ensureFolderWorkspaceInitialTerminal( folderWorkspace: FolderWorkspace, startup?: WorktreeStartupPayload, - providesInitialSurface?: boolean + providesInitialSurface?: boolean, + targetGroupId?: string ): string | null { if (providesInitialSurface === true && startup === undefined) { return null @@ -65,7 +66,10 @@ function ensureFolderWorkspaceInitialTerminal( undefined, undefined, undefined, - { reseedEmptiedWorkspace: providesInitialSurface !== true } + { + reseedEmptiedWorkspace: providesInitialSurface !== true, + ...(targetGroupId ? { targetGroupId } : {}) + } ) return primaryTabId } @@ -83,6 +87,7 @@ export function activateAndRevealFolderWorkspace( opts?: WorktreeActivationSurfaceSelection & { sidebarRevealBehavior?: PendingSidebarWorktreeReveal['behavior'] revealInSidebar?: boolean + targetGroupId?: string startup?: WorktreeStartupPayload runtimeEnvironmentId?: string | null executionHostId?: ExecutionHostId @@ -156,7 +161,12 @@ export function activateAndRevealFolderWorkspace( } const primaryTabId = shouldGateAgentActivation ? null - : ensureFolderWorkspaceInitialTerminal(folderWorkspace, opts?.startup, providesInitialSurface) + : ensureFolderWorkspaceInitialTerminal( + folderWorkspace, + opts?.startup, + providesInitialSurface, + opts?.targetGroupId + ) if (opts?.revealInSidebar !== false) { state.revealWorktreeInSidebar( @@ -267,6 +277,7 @@ export function activateAndRevealWorktree( opts?.issueCommand, opts?.defaultTabs, { + ...(opts?.targetGroupId ? { targetGroupId: opts.targetGroupId } : {}), ...(opts?.backendStartupTerminalSpawned ? { backendStartupTerminalSpawned: true } : {}), ...(opts?.createNewTerminalForStartup ? { createNewTerminalForStartup: true } : {}), ...(providesInitialSurface ? { callerProvidesSurface: true } : {}), diff --git a/src/renderer/src/lib/worktree-creation-flow-execute.ts b/src/renderer/src/lib/worktree-creation-flow-execute.ts index e98e0b660b8..df1c123d4b1 100644 --- a/src/renderer/src/lib/worktree-creation-flow-execute.ts +++ b/src/renderer/src/lib/worktree-creation-flow-execute.ts @@ -220,7 +220,8 @@ export async function executeWorktreeCreation( worktreeId: worktree.id, shouldActivateOnCompletion, activation, - primaryTabId + primaryTabId, + fallbackStartupOpt }) structuredLaunchAccepted = structuredSession.accepted activation = structuredSession.activation diff --git a/src/renderer/src/lib/worktree-creation-structured-session.test.ts b/src/renderer/src/lib/worktree-creation-structured-session.test.ts index 0dc2313bd42..5c658582943 100644 --- a/src/renderer/src/lib/worktree-creation-structured-session.test.ts +++ b/src/renderer/src/lib/worktree-creation-structured-session.test.ts @@ -91,7 +91,8 @@ describe('launchStructuredWorktreeSession', () => { launchSource: 'new_workspace_composer', pendingFirstAgentMessageRename: true, reconcileUnknownLaunch: undefined, - signal: expect.any(AbortSignal) + signal: expect.any(AbortSignal), + launchPlan: expect.objectContaining({ route: 'structured-native-chat' }) }) }) diff --git a/src/renderer/src/lib/worktree-creation-structured-session.ts b/src/renderer/src/lib/worktree-creation-structured-session.ts index ad7d12a0109..357ec1ce650 100644 --- a/src/renderer/src/lib/worktree-creation-structured-session.ts +++ b/src/renderer/src/lib/worktree-creation-structured-session.ts @@ -5,6 +5,7 @@ import type { AgentLaunchRoute } from '@/lib/agent-launch-routing' import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation' import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload' import { launchAgentSession } from '@/lib/launch-agent-session' +import { adoptAgentSessionLaunchVerdict } from '@/lib/agent-session-launch-plan' export type WorktreeCreationStructuredSessionResult = { accepted: boolean @@ -42,6 +43,18 @@ export async function launchStructuredWorktreeSession( if (!useAppStore.getState().pendingWorktreeCreations[args.creationId]) { return { ...settled, cancelled: true, activation, primaryTabId } } + const launchPlan = adoptAgentSessionLaunchVerdict({ + route: args.agentLaunchRoute, + agent, + worktreeId: args.worktreeId, + ...(args.recoverUnknownLaunch + ? {} + : { + prompt: args.request.launchDraftPrompt ?? args.request.quickPrompt, + ...(args.request.promptDelivery ? { promptDelivery: args.request.promptDelivery } : {}) + }), + ...(args.recoverUnknownLaunch ? { reconcileUnknownLaunch: args.recoverUnknownLaunch } : {}) + }) const abandoned = new AbortController() const unsubscribe = useAppStore.subscribe((state) => { if (!state.pendingWorktreeCreations[args.creationId]) { @@ -61,6 +74,8 @@ export async function launchStructuredWorktreeSession( }), visibility: args.shouldActivateOnCompletion ? 'reveal' : 'background', launchSource: args.request.quickTelemetry?.launch_source ?? 'new_workspace_composer', + launchPlan, + ...(args.fallbackStartupOpt ? { terminalStartup: args.fallbackStartupOpt } : {}), pendingFirstAgentMessageRename: args.request.pendingFirstAgentMessageRename, reconcileUnknownLaunch: args.recoverUnknownLaunch, signal: abandoned.signal diff --git a/src/renderer/src/lib/worktree-default-terminal-tabs.ts b/src/renderer/src/lib/worktree-default-terminal-tabs.ts index 5a9d48723f0..84fb8e01940 100644 --- a/src/renderer/src/lib/worktree-default-terminal-tabs.ts +++ b/src/renderer/src/lib/worktree-default-terminal-tabs.ts @@ -48,25 +48,30 @@ export function applyDefaultTerminalTabs( : isStartupTab && startup?.telemetry ? (agentKindToTuiAgent(startup.telemetry.agent_kind) ?? undefined) : undefined - const tab = store.createTab(worktreeId, undefined, undefined, { - pendingActivationSpawn: true, - recordInteraction: false, - ...(launchAgent - ? { - launchAgent, - ...initialAgentTabViewModeProps(store.settings ?? null, { - agent: launchAgent, - ...draftViewModeProps( - isStartupTab ? resolveStartupLaunchDraftText(startup) : undefined - ), - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - getConnectionId(worktreeId) - ) - }) - } - : {}), - ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) - }) + const tab = store.createTab( + worktreeId, + index === 0 ? opts?.targetGroupId : undefined, + undefined, + { + pendingActivationSpawn: true, + recordInteraction: false, + ...(launchAgent + ? { + launchAgent, + ...initialAgentTabViewModeProps(store.settings ?? null, { + agent: launchAgent, + ...draftViewModeProps( + isStartupTab ? resolveStartupLaunchDraftText(startup) : undefined + ), + nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( + getConnectionId(worktreeId) + ) + }) + } + : {}), + ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) + } + ) if (index === 0) { firstTabId = tab.id } diff --git a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts index e36a79cb364..ae149849d81 100644 --- a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts +++ b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts @@ -218,7 +218,7 @@ export function ensureWorktreeHasInitialTerminal( (sequencedStartup?.telemetry ? (agentKindToTuiAgent(sequencedStartup.telemetry.agent_kind) ?? undefined) : undefined) - const terminalTab = store.createTab(worktreeId, undefined, undefined, { + const terminalTab = store.createTab(worktreeId, opts?.targetGroupId, undefined, { pendingActivationSpawn: true, ...(launchAgent ? {