diff --git a/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts b/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts index a86e9901835..f72a249b9c7 100644 --- a/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts +++ b/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts @@ -40,6 +40,7 @@ describe('launchDashboardAgent', () => { expect(mocks.getKnownWorktreeById).toHaveBeenCalledWith('folder:docs', 'ssh:docs') expect(mocks.setActiveWorktree).toHaveBeenCalledWith('folder:docs', 'ssh:docs') expect(mocks.launchAgentInNewTab).toHaveBeenCalledWith({ + requestId: expect.any(String), agent: 'codex', worktreeId: 'folder:docs', launchSource: 'unknown' diff --git a/src/renderer/src/components/dashboard/launch-dashboard-agent.ts b/src/renderer/src/components/dashboard/launch-dashboard-agent.ts index be3b644e5a6..43ff278bd7c 100644 --- a/src/renderer/src/components/dashboard/launch-dashboard-agent.ts +++ b/src/renderer/src/components/dashboard/launch-dashboard-agent.ts @@ -1,4 +1,5 @@ import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' +import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import { useAppStore } from '@/store' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' import type { DashboardSpawnAgentArgs } from '../../../../shared/dashboard-snapshot' @@ -15,6 +16,7 @@ export function launchDashboardAgent({ worktreeId, agent }: DashboardSpawnAgentA state.setActiveWorktree(worktreeId, executionHostId) return ( launchAgentInNewTab({ + requestId: newAgentLaunchRequestId(), agent, worktreeId, launchSource: 'unknown' diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx index 2a16207c5f4..da9710b965d 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx @@ -174,6 +174,7 @@ describe('FloatingTerminalWindowControls default-agent launch', () => { clickLaunch() expect(mocks.launchAgentInNewTab).toHaveBeenCalledExactlyOnceWith({ + requestId: expect.any(String), agent: 'claude', worktreeId: FLOATING_TERMINAL_WORKTREE_ID, launchSource: 'shortcut' diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx index 3889a5f342f..28fa197c050 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx @@ -6,6 +6,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { getAgentCatalog, AgentIcon } from '@/lib/agent-catalog' import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' +import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import { useAppStore } from '@/store' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import { @@ -67,6 +68,7 @@ export function FloatingTerminalWindowControls({ // Floating resolves the terminal-backed lane: a chat view over a PTY when the chat default is // on, never a structured session. const result = launchAgentInNewTab({ + requestId: newAgentLaunchRequestId(), agent: defaultAgent, worktreeId: FLOATING_TERMINAL_WORKTREE_ID, launchSource: 'shortcut' diff --git a/src/renderer/src/components/native-chat/use-native-chat-provisional-launch.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-provisional-launch.test.tsx index a2055b79e06..3ef80d69955 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-provisional-launch.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-provisional-launch.test.tsx @@ -127,8 +127,8 @@ describe('a chat pane over its own launch', () => { it('keeps the selection its create seeded when a pick in another chat saves a new one', () => { saveSelection('gpt-5.5') - startStructuredAgentLaunch('wt-first', 'codex') - const second = startStructuredAgentLaunch('wt-second', 'codex') + startStructuredAgentLaunch('wt-first', 'codex', { requestId: 'request-1' }) + const second = startStructuredAgentLaunch('wt-second', 'codex', { requestId: 'request-2' }) const { result, rerender } = renderLaunchedChat('wt-second', second.sessionId) expect(currentModel(result.current.optionSnapshot)).toBe('gpt-5.5') @@ -156,7 +156,7 @@ describe('a chat pane over its own launch', () => { }) : new Promise(() => {}) ) - const launch = startStructuredAgentLaunch('wt-refused', 'codex') + const launch = startStructuredAgentLaunch('wt-refused', 'codex', { requestId: 'request-3' }) const { sessionId } = launch const { result, rerender } = renderLaunchedChat('wt-refused', sessionId) await act(async () => { diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts index 577b700a98c..d9e2b3e1d4d 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts @@ -16,6 +16,7 @@ import { prepareAiVaultSessionContinuation } from './ai-vault-session-continuati import type { AgentSessionContinuationRequest } from '@/lib/agent-session-continuation' import { activateAiVaultStructuredSession } from '@/lib/activate-ai-vault-structured-session' import { isAgentSessionHandleProvider } from '../../../../shared/agent-session-provider-handle' +import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import { activateAiVaultResumeWorkspace, resumeAiVaultSessionInNewChat @@ -161,7 +162,12 @@ export function useAiVaultSessionLaunchActions({ ) return } - void resumeAiVaultSessionInNewChat(session, session.agent, worktreeId) + void resumeAiVaultSessionInNewChat( + session, + session.agent, + worktreeId, + newAgentLaunchRequestId() + ) }, [activeWorktree?.id, activeWorktreeId] ) diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts index f0c307ae0ca..0dbc81ae4a4 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts @@ -83,7 +83,7 @@ describe('resumeAiVaultSessionInNewChat', () => { return { sessionId: 'session-1', tab: { id: 'agent-session:session-1' }, settlement } }) - await resumeAiVaultSessionInNewChat(session, 'codex', 'worktree-1') + await resumeAiVaultSessionInNewChat(session, 'codex', 'worktree-1', 'resume-click') expect(mocks.beginStructuredAgentSessionProvisionalLaunch).toHaveBeenCalledWith( expect.objectContaining({ @@ -104,7 +104,7 @@ describe('resumeAiVaultSessionInNewChat', () => { settlement: Promise.resolve({ kind: 'failed', error }) }) - await resumeAiVaultSessionInNewChat(session, 'codex', 'worktree-1') + await resumeAiVaultSessionInNewChat(session, 'codex', 'worktree-1', 'resume-click') await vi.waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith( 'Another chat is already holding this conversation.' @@ -119,7 +119,7 @@ describe('resumeAiVaultSessionInNewChat', () => { settlement: Promise.resolve({ kind: 'visibility-unknown', sessionId: 'session-1' }) }) - await resumeAiVaultSessionInNewChat(session, 'codex', 'worktree-1') + await resumeAiVaultSessionInNewChat(session, 'codex', 'worktree-1', 'resume-click') await Promise.resolve() expect(mocks.toastError).not.toHaveBeenCalled() }) diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.ts index dbf98a13cfb..b309dd31de3 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.ts @@ -7,6 +7,7 @@ import { hasRuntimeRpcErrorCode } from '../../../../shared/runtime-rpc-error-cod import { parseWorkspaceKey } from '../../../../shared/workspace-scope' import { prepareAiVaultSessionForResume } from '@/lib/ai-vault-session-resume-preparation' import { adoptAgentSessionLaunchVerdict } from '@/lib/agent-session-launch-plan' +import type { AgentLaunchRequestId } from '@/lib/agent-launch-request-id' import { activateAndRevealFolderWorkspace, activateAndRevealWorktree @@ -28,7 +29,8 @@ export function activateAiVaultResumeWorkspace(workspaceId: string): boolean { export async function resumeAiVaultSessionInNewChat( session: AiVaultSession, agent: AgentSessionHandleProvider, - worktreeId: string + worktreeId: string, + requestId: AgentLaunchRequestId ): Promise { try { // Codex rows can live under a shared legacy home; the same preparation the terminal resume @@ -36,6 +38,7 @@ export async function resumeAiVaultSessionInNewChat( const preparedSession = await prepareAiVaultSessionForResume(session) const plan = adoptAgentSessionLaunchVerdict({ route: 'structured-native-chat', + requestId, agent, worktreeId, resumeFrom: { providerSessionId: preparedSession.sessionId } diff --git a/src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.ts b/src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.ts index 2f5c044ab1d..c01d43da511 100644 --- a/src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.ts +++ b/src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.ts @@ -1,5 +1,6 @@ import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' +import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { Repo } from '../../../../shared/repo-types' import type { TuiAgent } from '../../../../shared/tui-agent' @@ -102,6 +103,7 @@ export async function runSourceControlAgentActionStart({ } } else if (worktreeId) { const result = launchAgentInNewTab({ + requestId: newAgentLaunchRequestId(), agent: selectedAgent, worktreeId, groupId: groupId ?? worktreeId, diff --git a/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts b/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts index b13bf2cac5a..44e844bc4d6 100644 --- a/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts @@ -1,12 +1,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ - planAgentSessionLaunch: vi.fn(), + resolveAgentSessionLaunchRoute: vi.fn(), getState: vi.fn(() => ({})) })) vi.mock('@/lib/agent-session-launch-plan', () => ({ - planAgentSessionLaunch: mocks.planAgentSessionLaunch + resolveAgentSessionLaunchRoute: mocks.resolveAgentSessionLaunchRoute })) vi.mock('@/store', () => ({ useAppStore: { getState: mocks.getState } })) @@ -15,7 +15,7 @@ import { sourceControlLaunchAppliesAgentArgs } from './source-control-launch-age describe('sourceControlLaunchAppliesAgentArgs', () => { beforeEach(() => { vi.clearAllMocks() - mocks.planAgentSessionLaunch.mockReturnValue({ route: 'terminal-tui' }) + mocks.resolveAgentSessionLaunchRoute.mockReturnValue('terminal-tui') }) it('applies arguments when the user launches into a terminal by default', () => { @@ -28,7 +28,7 @@ describe('sourceControlLaunchAppliesAgentArgs', () => { }) it('drops arguments only when this launch would really be a structured session', () => { - mocks.planAgentSessionLaunch.mockReturnValue({ route: 'structured-native-chat' }) + mocks.resolveAgentSessionLaunchRoute.mockReturnValue('structured-native-chat') expect( sourceControlLaunchAppliesAgentArgs({ agent: 'codex', @@ -39,7 +39,7 @@ describe('sourceControlLaunchAppliesAgentArgs', () => { it('keeps arguments for a chat-by-default user whose launch falls back to a terminal', () => { // A remote host, an agent without a structured session, or a floating workspace all land here. - mocks.planAgentSessionLaunch.mockReturnValue({ route: 'legacy-native-chat' }) + mocks.resolveAgentSessionLaunchRoute.mockReturnValue('legacy-native-chat') expect( sourceControlLaunchAppliesAgentArgs({ agent: 'codex', @@ -55,7 +55,7 @@ describe('sourceControlLaunchAppliesAgentArgs', () => { worktreeId: 'wt-1' }) ).toBe(true) - expect(mocks.planAgentSessionLaunch).not.toHaveBeenCalled() + expect(mocks.resolveAgentSessionLaunchRoute).not.toHaveBeenCalled() }) it('names the repo and its host when the workspace does not exist yet', () => { @@ -64,7 +64,7 @@ describe('sourceControlLaunchAppliesAgentArgs', () => { repoId: 'repo-1', executionHostId: 'ssh:build-box' }) - expect(mocks.planAgentSessionLaunch).toHaveBeenCalledWith( + expect(mocks.resolveAgentSessionLaunchRoute).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ workspace: { diff --git a/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.ts b/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.ts index ea16d595ae3..20a53621097 100644 --- a/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.ts +++ b/src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.ts @@ -4,7 +4,7 @@ import { workspaceKindForWorktreeId, type ProspectiveWorkspace } from '@/lib/agent-launch-route-input' -import { planAgentSessionLaunch } from '@/lib/agent-session-launch-plan' +import { resolveAgentSessionLaunchRoute } from '@/lib/agent-session-launch-plan' import { useAppStore } from '@/store' export type SourceControlLaunchAgentArgsApplicabilityInput = { @@ -47,9 +47,9 @@ export function sourceControlLaunchAppliesAgentArgs( return true } return ( - planAgentSessionLaunch(useAppStore.getState(), { + resolveAgentSessionLaunchRoute(useAppStore.getState(), { agent: input.agent, workspace: prospectiveWorkspace(input) - }).route !== 'structured-native-chat' + }) !== 'structured-native-chat' ) } diff --git a/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts b/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts index 4a055a613ee..dd2979ce031 100644 --- a/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts +++ b/src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts @@ -2,6 +2,7 @@ import { toast } from 'sonner' import type { AppState } from '@/store' import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' +import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import { getConnectionId } from '@/lib/connection-context' import { planAgentCliArgsSuffix } from '@/lib/tui-agent-startup' import { @@ -156,6 +157,7 @@ export async function launchSourceControlRecoveryAgentWithDefault({ return false } const result = launchAgentInNewTab({ + requestId: newAgentLaunchRequestId(), agent, worktreeId: activeWorktreeId, groupId: activeGroupId ?? activeWorktreeId, diff --git a/src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-commit-actions.ts b/src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-commit-actions.ts index bd755652350..36d4fec9313 100644 --- a/src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-commit-actions.ts +++ b/src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-commit-actions.ts @@ -9,6 +9,7 @@ import { import { getConnectionId } from '@/lib/connection-context' import { detectLanguage } from '@/lib/language-detect' import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' +import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import { resolveDefaultAgentForNewTab } from '@/lib/agent-tab-shortcuts' import { translate } from '@/i18n/i18n' import type { GitHistoryItem } from '../../../../../../shared/git-history' @@ -280,6 +281,7 @@ export function useGitHistoryCommitActions({ `Run \`git show --no-ext-diff ${item.id}\` to inspect the full diff, then summarize what changed and why at a high level, calling out the most important files and any risks.` ].join('\n') launchAgentInNewTab({ + requestId: newAgentLaunchRequestId(), agent, worktreeId: activeWorktreeId, prompt: explainPrompt, 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 c4857877e11..40d65b759e2 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts @@ -18,6 +18,7 @@ import { toFolderWorkspaceLinkedTask } from './folder-workspace-composer-helpers' import { planAgentSessionLaunch } from '@/lib/agent-session-launch-plan' +import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import { beginStructuredAgentSessionProvisionalLaunch } from '@/lib/structured-agent-session-provisional-tab' import { getNewWorkspaceProjectGroupHostId } from '@/lib/new-workspace-project-options' import { useAppStore } from '@/store' @@ -132,6 +133,7 @@ export async function submitFolderWorkspaceCreate({ quickAgent && linkedWorkItem ? resolveFolderWorkspaceLaunchDraft(linkedWorkItem, note) : null const plan = quickAgent ? planAgentSessionLaunch(useAppStore.getState(), { + requestId: newAgentLaunchRequestId(), agent: quickAgent, workspace: { kind: 'folder', diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx index c410d4234d4..404c6499227 100644 --- a/src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx +++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx @@ -4,11 +4,7 @@ import type { ReactNode } from 'react' import { cleanup, fireEvent, render } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { StructuredLaunchState } from '@/lib/structured-agent-session-launch-registry' -import { - BLANK_STRUCTURED_LAUNCH_REQUEST, - structuredLaunchRequest, - type StructuredLaunchAttempt -} from '@/lib/structured-agent-session-launch-request' +import type { StructuredLaunchAttempt } from '@/lib/structured-agent-session-launch-request' vi.mock('@/hooks/useDetectedAgents', () => ({ useDetectedAgents: () => ({ detectedIds: ['claude', 'codex'] }) @@ -69,7 +65,8 @@ function registerLaunch( outcome: 'pending' | 'failed', attempt: StructuredLaunchAttempt = { kind: 'first', - request: BLANK_STRUCTURED_LAUNCH_REQUEST, + requestId: `${agent}-pick`, + blank: true, stagedEntry: null } ): void { @@ -114,14 +111,15 @@ function agentRowDisabled(label: string): string | null | undefined { ?.getAttribute('aria-disabled') } -describe('QuickLaunchAgentMenuItems launch status', () => { +describe('QuickLaunchAgentMenuItems launches', () => { beforeEach(() => { localStorage.clear() resetStructuredAgentLaunchRegistryForTests() }) afterEach(cleanup) - it('keeps an agent whose chat failed to start launchable while a starting one waits', () => { + // Each pick is its own request, so a chat starting, failing or retrying never blocks one. + it('keeps every agent launchable while chats start, fail or retry', () => { registerLaunch('claude', 'pending') registerLaunch('codex', 'failed') @@ -132,52 +130,43 @@ describe('QuickLaunchAgentMenuItems launch status', () => { onFocusTerminal={vi.fn()} /> ) - - expect(agentRowDisabled('Claude')).toBe('true') + expect(agentRowDisabled('Claude')).toBe('false') expect(agentRowDisabled('Codex')).toBe('false') - }) + cleanup() - // A pick then opens a new chat; only a new start's own create would be joined. - it("keeps an agent launchable while a failed chat's Retry is in flight", () => { - registerLaunch('claude', 'pending') registerLaunch('codex', 'pending', { kind: 'retry' }) - render( ) - - expect(agentRowDisabled('Claude')).toBe('true') expect(agentRowDisabled('Codex')).toBe('false') }) - // A pick joins only a start of the same request; any other opens its own chat. - it('disables an agent only for the request its starting chat carries', () => { - registerLaunch('codex', 'pending', { - kind: 'first', - request: structuredLaunchRequest({ prompt: 'review notes' }), - stagedEntry: null + it('gives each pick its own request, so a second pick opens its own chat', () => { + launchMock.mockReset() + launchMock.mockReturnValue({ + surface: { kind: 'local-agent-session', tabId: 'tab-1', sessionId: 'codex-session' } }) - const menu = (prompt?: string) => ( + render( ) + const codexRow = document.querySelector('[title="Launch Codex in a new terminal"]')! + fireEvent.click(codexRow) + fireEvent.click(codexRow) - render(menu()) - expect(agentRowDisabled('Codex')).toBe('false') - cleanup() - render(menu('other notes')) - expect(agentRowDisabled('Codex')).toBe('false') - cleanup() - render(menu('review notes')) - expect(agentRowDisabled('Codex')).toBe('true') + const requestIds = launchMock.mock.calls.map(([args]) => args.requestId) + expect(requestIds).toHaveLength(2) + expect(requestIds[0]).toEqual(expect.any(String)) + expect(requestIds[1]).not.toBe(requestIds[0]) }) // Why: a new chat saves the notes' keys with its message; the launch's own result holds them diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx index 411baccf287..65adf068165 100644 --- a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx +++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react' -import { Loader2, Settings as SettingsIcon } from 'lucide-react' +import { Settings as SettingsIcon } from 'lucide-react' import { toast } from 'sonner' import { DropdownMenuItem, DropdownMenuShortcut } from '@/components/ui/dropdown-menu' import { getAgentCatalog, AgentIcon } from '@/lib/agent-catalog' @@ -8,7 +8,7 @@ import { useAgentDetectionTargetForWorktree } from '@/hooks/useAgentDetectionTar import { useDetectedAgents } from '@/hooks/useDetectedAgents' import { useOptionalShortcutLabel } from '@/hooks/useShortcutLabel' import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' -import { isAgentSessionHandleProvider } from '../../../../shared/agent-session-provider-handle' +import { newAgentLaunchRequestId } from '@/lib/agent-launch-request-id' import type { TuiAgent } from '../../../../shared/tui-agent' import type { LaunchSource } from '../../../../shared/telemetry-events' import { @@ -16,8 +16,6 @@ import { filterEnabledTuiAgents } from '../../../../shared/tui-agent-selection' import { translate } from '@/i18n/i18n' -import { useStructuredAgentLaunchStatus } from '@/lib/structured-agent-session-launch' -import { structuredLaunchRequest } from '@/lib/structured-agent-session-launch-request' import type { NotesSendHandOff } from '@/lib/notes-send-in-flight' export type QuickLaunchAgentMenuItemsProps = { @@ -127,13 +125,6 @@ function QuickLaunchAgentMenuItemsInner({ const openSettingsPage = useAppStore((s) => s.openSettingsPage) const openSettingsTarget = useAppStore((s) => s.openSettingsTarget) const newAgentShortcut = useOptionalShortcutLabel('tab.newAgent') - // One hook per structured provider: the launch registry is keyed by agent, and hooks cannot run - // inside the agent list's render loop. Only a start of this menu's own request is joined. - const launchRequest = structuredLaunchRequest({ prompt, promptDelivery }) - const structuredLaunchStatusByAgent = { - claude: useStructuredAgentLaunchStatus(worktreeId, 'claude', launchRequest), - codex: useStructuredAgentLaunchStatus(worktreeId, 'codex', launchRequest) - } const openAgentSettings = useCallback(() => { openSettingsTarget({ pane: 'agents', repoId: null }) @@ -148,6 +139,7 @@ function QuickLaunchAgentMenuItemsInner({ const entry = getCatalogEntry(agent) const label = entry?.label ?? agent const result = launchAgentInNewTab({ + requestId: newAgentLaunchRequestId(), agent, worktreeId, groupId, @@ -230,14 +222,12 @@ function QuickLaunchAgentMenuItemsInner({ {agents.map((agent) => { const entry = getCatalogEntry(agent) const label = entry?.label ?? agent - const isStructuredLaunchPending = - isAgentSessionHandleProvider(agent) && structuredLaunchStatusByAgent[agent] === 'pending' const showsDefaultAgentShortcut = newAgentShortcut !== null && defaultAgent !== 'blank' && agent === defaultAgent return ( runLaunch(agent)} className="gap-2 rounded-[7px] px-2 py-1.5 text-[12px] leading-5 font-medium" title={translate( @@ -246,11 +236,7 @@ function QuickLaunchAgentMenuItemsInner({ { value0: label } )} > - {isStructuredLaunchPending ? ( -