From 05458416afd8ebe7c8b138f15b9d1ff08c309ffe Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:15:02 -0700 Subject: [PATCH] Revert "+" new-tab agent launches to the window route (reverts #26489) (#27173) #26489 started no-prompt "+" launches through the host lane that is built for terminals no window shows, into a pane the window already owns. That lane differs from the window lane in start size, env, telemetry, failure handling and Windows runtime resolution. Restore main's window route until the two terminal lanes are converged, then move "+" onto the single lane. This reverts commit 7634bbfffe0f1739761738386bc641a3a8456512. --- .../dashboard/launch-dashboard-agent.test.ts | 3 +- .../dashboard/launch-dashboard-agent.ts | 3 +- .../FloatingTerminalWindowControls.test.tsx | 3 +- .../FloatingTerminalWindowControls.tsx | 3 +- .../components/tab-bar/QuickLaunchButton.tsx | 3 +- .../use-tab-bar-create-menu-controller.ts | 3 +- .../components/use-terminal-create-actions.ts | 3 +- .../src/lib/agent-launch-through-host.test.ts | 17 --- .../src/lib/agent-launch-through-host.ts | 5 +- ...nch-agent-fresh-new-tab-host-route.test.ts | 101 ----------------- ...launch-agent-in-new-tab-fresh-host.test.ts | 102 ------------------ .../src/lib/launch-agent-in-new-tab.ts | 20 ---- .../lib/launch-agent-new-tab-host-route.ts | 57 ++-------- 13 files changed, 18 insertions(+), 305 deletions(-) delete mode 100644 src/renderer/src/lib/launch-agent-fresh-new-tab-host-route.test.ts delete mode 100644 src/renderer/src/lib/launch-agent-in-new-tab-fresh-host.test.ts 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 204c4678475..f72a249b9c7 100644 --- a/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts +++ b/src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts @@ -43,8 +43,7 @@ describe('launchDashboardAgent', () => { requestId: expect.any(String), agent: 'codex', worktreeId: 'folder:docs', - launchSource: 'unknown', - freshNewTab: true + 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 88f65bd2ba2..43ff278bd7c 100644 --- a/src/renderer/src/components/dashboard/launch-dashboard-agent.ts +++ b/src/renderer/src/components/dashboard/launch-dashboard-agent.ts @@ -19,8 +19,7 @@ export function launchDashboardAgent({ worktreeId, agent }: DashboardSpawnAgentA requestId: newAgentLaunchRequestId(), agent, worktreeId, - launchSource: 'unknown', - freshNewTab: true + launchSource: 'unknown' }) !== null ) } diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx index 482279ac756..ac4bb5eed80 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx @@ -182,8 +182,7 @@ describe('FloatingTerminalWindowControls default-agent launch', () => { agent: 'claude', worktreeId: FLOATING_TERMINAL_WORKTREE_ID, launchSource: 'shortcut', - activate: false, - freshNewTab: true + activate: false }) // Why: the whole point of the migration. The shared launcher owns the startup plan and the // tab it lands in, so this button must not reach past it into the tab store. diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx index 6a1b6288008..8536d4e5912 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx @@ -77,8 +77,7 @@ export function FloatingTerminalWindowControls({ // Why: `agent-auto-ack-targets` relies on the floating panel's active tab never becoming the // global `activeTabId`; activating here would also flip the main view off an open editor. // This selects within the floating group below instead. - activate: false, - freshNewTab: true + activate: false }) if (!result) { toast.error( diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx index 553c41b1eac..8622e567160 100644 --- a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx +++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx @@ -149,8 +149,7 @@ function QuickLaunchAgentMenuItemsInner({ ...(launchSource !== undefined ? { launchSource } : {}), ...(onPromptDelivered !== undefined ? { onPromptDelivered } : {}), // Notes keep their text until it goes out, so the new chat's composer never gets a copy. - ...(onPromptHandedOff ? { promptKeptByCaller: true as const } : {}), - freshNewTab: true + ...(onPromptHandedOff ? { promptKeptByCaller: true as const } : {}) }) if (!result) { toast.error( diff --git a/src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts b/src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts index 0cfdba3c75a..dcbc45df788 100644 --- a/src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts +++ b/src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts @@ -228,8 +228,7 @@ export function useTabBarCreateMenuController({ agent, worktreeId, groupId: resolvedGroupId, - launchSource: 'tab_bar_quick_launch', - freshNewTab: true + launchSource: 'tab_bar_quick_launch' }) if (!result) { toast.error( diff --git a/src/renderer/src/components/use-terminal-create-actions.ts b/src/renderer/src/components/use-terminal-create-actions.ts index d358baffd8c..b503d97252c 100644 --- a/src/renderer/src/components/use-terminal-create-actions.ts +++ b/src/renderer/src/components/use-terminal-create-actions.ts @@ -76,8 +76,7 @@ export function useTerminalCreateActions(controller: TerminalColdActivationContr agent, worktreeId: activeWorktreeId, groupId: targetGroupId, - launchSource: 'shortcut', - freshNewTab: true + launchSource: 'shortcut' }) if (!result) { toast.error( diff --git a/src/renderer/src/lib/agent-launch-through-host.test.ts b/src/renderer/src/lib/agent-launch-through-host.test.ts index e8bbc762e2e..abccb529cc4 100644 --- a/src/renderer/src/lib/agent-launch-through-host.test.ts +++ b/src/renderer/src/lib/agent-launch-through-host.test.ts @@ -132,23 +132,6 @@ describe('a desktop launch through the host', () => { }) }) - // Why: a floating launch must leave the main window's selection alone, as main's own tab does. - it('keeps a launch that must not take the selection in the background', () => { - callRuntimeRpc.mockReturnValue(new Promise(() => {})) - const before = store.getState().activeTabId - - const { tabId } = launchAgentThroughHost({ - agent: 'claude', - worktreeId: WT, - prompt: '', - activate: false - }) - - expect(launchTab(tabId)).toBeDefined() - expect(store.getState().activeTabId).toBe(before) - expect(lastParams().presentation).toBe('background') - }) - it('names every click as its own operation', () => { callRuntimeRpc.mockReturnValue(new Promise(() => {})) launch() diff --git a/src/renderer/src/lib/agent-launch-through-host.ts b/src/renderer/src/lib/agent-launch-through-host.ts index 2d15f7a99e7..59b8e202cd4 100644 --- a/src/renderer/src/lib/agent-launch-through-host.ts +++ b/src/renderer/src/lib/agent-launch-through-host.ts @@ -46,8 +46,6 @@ export type HostAgentLaunchArgs = { pendingActivationSpawn?: boolean /** The view the tab opens in, decided as for any new agent tab. */ viewMode?: Tab['viewMode'] - /** False keeps a floating launch from taking the main window's selection, as main's tab does. */ - activate?: boolean } /** What became of the launch, as this window must tell it. */ @@ -104,7 +102,7 @@ function launchParams(args: HostAgentLaunchArgs) { ...stringSessionOptions(args.sessionOptions), ...(args.launchSource ? { launchSource: args.launchSource } : {}), ...(args.groupId ? { placement: { groupId: args.groupId } } : {}), - presentation: args.activate === false ? 'background' : 'focused' + presentation: 'focused' } } @@ -166,7 +164,6 @@ export function launchAgentThroughHost(args: HostAgentLaunchArgs): { launchAgent: args.agent, quickCommandLabel: args.quickCommandLabel, ...(args.pendingActivationSpawn ? { pendingActivationSpawn: true } : {}), - ...(args.activate === false ? { activate: false } : {}), ...(args.viewMode ? { viewMode: args.viewMode } : {}) }) rememberAgentLaunchPanePrompt(tabId, args.prompt) diff --git a/src/renderer/src/lib/launch-agent-fresh-new-tab-host-route.test.ts b/src/renderer/src/lib/launch-agent-fresh-new-tab-host-route.test.ts deleted file mode 100644 index 982dd6b102b..00000000000 --- a/src/renderer/src/lib/launch-agent-fresh-new-tab-host-route.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { HostAgentLaunchOutcome } from './agent-launch-through-host' - -const host = vi.hoisted(() => ({ - launchAgentThroughHost: vi.fn(), - windowMakesHostLaunchTab: vi.fn(() => true) -})) -vi.mock('@/lib/agent-launch-through-host', () => host) -vi.mock('@/lib/launch-agent-tab-prompt-paste', () => ({ - pasteAgentLaunchPromptOnceReady: vi.fn() -})) -const state = vi.hoisted(() => { - const initial: { - settings: { disabledTuiAgents: string[] } - connectionId: string | null | undefined - } = { settings: { disabledTuiAgents: [] }, connectionId: null } - return initial -}) -vi.mock('@/store', () => ({ useAppStore: { getState: () => ({ settings: state.settings }) } })) -vi.mock('@/lib/connection-context', () => ({ - getConnectionIdFromState: () => state.connectionId -})) -vi.mock('@/lib/new-workspace', () => ({ CLIENT_PLATFORM: 'win32' })) -const toast = vi.hoisted(() => ({ error: vi.fn() })) -vi.mock('sonner', () => ({ toast })) - -const { freshNewTabLaunchesThroughHost, launchFreshNewTabThroughHost } = - await import('./launch-agent-new-tab-host-route') - -beforeEach(() => { - vi.clearAllMocks() - host.windowMakesHostLaunchTab.mockReturnValue(true) - state.settings = { disabledTuiAgents: [] } - state.connectionId = null -}) - -describe('which plain new-tab launches start through the host', () => { - function startsThroughHost( - overrides: { prompt?: string; freshNewTab?: true } = {}, - launchPlatform: NodeJS.Platform = 'win32' - ): boolean { - return freshNewTabLaunchesThroughHost( - { freshNewTab: true, worktreeId: 'wt-1', agent: 'claude', ...overrides }, - launchPlatform - ) - } - - it('a local workspace with an enabled agent, no prompt and the chat default off', () => { - expect(startsThroughHost()).toBe(true) - }) - - it('only for the plain new-tab callers, and only with no prompt', () => { - expect(startsThroughHost({ freshNewTab: undefined })).toBe(false) - expect(startsThroughHost({ prompt: 'fix it' })).toBe(false) - }) - - it('keeps main launch where the host could open a chat', () => { - host.windowMakesHostLaunchTab.mockReturnValue(false) - expect(startsThroughHost()).toBe(false) - }) - - // Why: the host refuses a disabled agent; main's window never read the disabled list here. - it('keeps main launch for a disabled agent', () => { - state.settings = { disabledTuiAgents: ['claude'] } - expect(startsThroughHost()).toBe(false) - }) - - // Why: main's pane connects an SSH target first and waits for the remote shell before typing. - it.each([['ssh-1'], [undefined]])('keeps main launch on SSH or an unknown host (%s)', (id) => { - state.connectionId = id - expect(startsThroughHost()).toBe(false) - }) - - // Why: the window launches a WSL path as Linux; the host would quote it for Windows. - it('keeps main launch where the window launches on another platform', () => { - expect(startsThroughHost({}, 'linux')).toBe(false) - }) -}) - -describe('a plain new tab started through the host', () => { - function launch(outcome: HostAgentLaunchOutcome): string { - host.launchAgentThroughHost.mockReturnValue({ - tabId: 'tab-1', - outcome: Promise.resolve(outcome) - }) - return launchFreshNewTabThroughHost({ agent: 'claude', worktreeId: 'wt-1', prompt: '' }) - } - - it('says nothing once the agent started', async () => { - expect(launch({ kind: 'started' })).toBe('tab-1') - await Promise.resolve() - await Promise.resolve() - expect(toast.error).not.toHaveBeenCalled() - }) - - it('says a launch that never started, with no copy action for a prompt it never had', async () => { - launch({ kind: 'not-started', unconfirmed: false, code: 'worktree_not_found' }) - await vi.waitFor(() => expect(toast.error).toHaveBeenCalledOnce()) - expect(toast.error.mock.calls[0]?.[1]).toBeUndefined() - }) -}) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-fresh-host.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-fresh-host.test.ts deleted file mode 100644 index 633a9eb9d1c..00000000000 --- a/src/renderer/src/lib/launch-agent-in-new-tab-fresh-host.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -// Real-store coverage: which plain new-tab launches start through the host's agent.launch. - -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { getDefaultSettings } from '../../../shared/constants' -import { createTestStore, makeWorktree, seedStore } from '../store/slices/store-test-helpers' -import { createStoreCascadesMockApi } from '../store/slices/store-cascades-test-harness' - -const storeBox = vi.hoisted(() => { - const box: { store: unknown } = { store: null } - return box -}) -vi.mock('sonner', () => ({ - toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn(), message: vi.fn() } -})) -vi.mock('@/store', () => ({ - get useAppStore() { - return storeBox.store - } -})) -const callRuntimeRpc = vi.hoisted(() => - vi.fn<(target: unknown, method: string, params: Record) => Promise>() -) -vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) => ({ - ...(await importOriginal()), - callRuntimeRpc -})) - -createStoreCascadesMockApi() - -const WT = 'repo1::/path/wt1' - -type Settings = Partial> - -function seed(settings: Settings = {}) { - const store = createTestStore() - storeBox.store = store - seedStore(store, { - settings: { ...getDefaultSettings('/tmp'), ...settings }, - worktreesByRepo: { repo1: [makeWorktree({ id: WT, repoId: 'repo1', path: '/path/wt1' })] }, - activeWorktreeId: WT - }) - return store -} - -function launchCalls() { - return callRuntimeRpc.mock.calls.filter(([, method]) => method === 'agent.launchReplay') -} - -beforeEach(() => { - vi.clearAllMocks() - callRuntimeRpc.mockReturnValue(new Promise(() => {})) -}) - -describe('a plain new-tab launch', () => { - it('starts through the host with no prompt and queues no startup command of its own', async () => { - const store = seed() - const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') - - const result = launchAgentInNewTab({ - requestId: 'request-1', - agent: 'claude', - worktreeId: WT, - freshNewTab: true - }) - - const tabId = result?.surface.kind === 'local-terminal' ? result.surface.tabId : '' - expect(launchCalls()).toHaveLength(1) - expect(launchCalls()[0]?.[2]).toMatchObject({ - agent: 'claude', - target: { kind: 'existing', worktree: `id:${WT}` }, - launchSource: 'tab_bar_quick_launch', - presentation: 'focused' - }) - expect(launchCalls()[0]?.[2]).not.toHaveProperty('prompt') - expect(store.getState().pendingStartupByTabId[tabId]).toBeUndefined() - expect(store.getState().activeTabId).toBe(tabId) - }) - - const kept: [string, { prompt?: string; freshNewTab?: true }, Settings][] = [ - ['a prompt', { prompt: 'fix it' }, {}], - ['no opt-in', { freshNewTab: undefined }, {}], - ['a disabled agent', {}, { disabledTuiAgents: ['claude'] }], - ['the chat default on', {}, { experimentalNativeChat: true }] - ] - it.each(kept)('keeps main launch with %s', async (_label, extra, settings) => { - const store = seed(settings) - const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') - - launchAgentInNewTab({ - requestId: 'request-2', - agent: 'claude', - worktreeId: WT, - freshNewTab: true, - ...extra - }) - - expect(launchCalls()).toHaveLength(0) - if (!('experimentalNativeChat' in settings)) { - expect(Object.keys(store.getState().pendingStartupByTabId)).toHaveLength(1) - } - }) -}) 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 a1426578be4..51e5de31816 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -6,8 +6,6 @@ import { tuiAgentToAgentKind } from '@/lib/telemetry' import { seedNativeChatLaunchDraftForAgentTab } from '@/lib/agent-launch-prompt-delivery' import { pasteAgentLaunchPromptOnceReady } from '@/lib/launch-agent-tab-prompt-paste' import { - freshNewTabLaunchesThroughHost, - launchFreshNewTabThroughHost, launchNewTabPromptThroughHost, newTabPromptLaunchesThroughHost } from '@/lib/launch-agent-new-tab-host-route' @@ -73,9 +71,6 @@ export type LaunchAgentInNewTabArgs = LaunchAgentInNewTabRequest & { onPromptDeliveryUnconfirmed?: () => void /** Keep terminal launches in a floating workspace from taking global selection. */ activate?: boolean - /** A plain new-tab pick ("+", Quick Launch, floating panel, dashboard): with no prompt it starts - * through the host where that matches main's own launch. */ - freshNewTab?: true /** The launch seeds a workspace being opened, so its PTY spawn must not reshuffle Recent. */ pendingActivationSpawn?: boolean /** Lets a workspace reveal itself before the selected surface opens. */ @@ -270,21 +265,6 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent promptDeliveryResult: launched.promptDeliveryResult } } - if (args.freshNewTab && freshNewTabLaunchesThroughHost(args, resolvedLaunchPlatform)) { - const tabId = launchFreshNewTabThroughHost({ - agent, - worktreeId, - ...(groupId ? { groupId } : {}), - prompt: '', - ...(agentArgs !== undefined ? { agentArgs } : {}), - ...(initialCwd?.trim() ? { cwd: initialCwd } : {}), - launchSource: launchSource ?? 'tab_bar_quick_launch', - quickCommandLabel, - ...(pendingActivationSpawn ? { pendingActivationSpawn: true } : {}), - ...(activate === false ? { activate: false } : {}) - }) - return { surface: { kind: 'local-terminal', tabId }, startupPlan, pasteDraftAfterLaunch: false } - } // Why: queue startup BEFORE TerminalPane mounts — it snapshots pendingStartupByTabId in useState on first render. const tab = store.createTab(worktreeId, groupId, undefined, { launchAgent: agent, diff --git a/src/renderer/src/lib/launch-agent-new-tab-host-route.ts b/src/renderer/src/lib/launch-agent-new-tab-host-route.ts index b6e493ee9e1..47a7f2675e2 100644 --- a/src/renderer/src/lib/launch-agent-new-tab-host-route.ts +++ b/src/renderer/src/lib/launch-agent-new-tab-host-route.ts @@ -8,11 +8,6 @@ import { type HostAgentLaunchOutcome } from '@/lib/agent-launch-through-host' import { pasteAgentLaunchPromptOnceReady } from '@/lib/launch-agent-tab-prompt-paste' -import { useAppStore } from '@/store' -import { getConnectionIdFromState } from '@/lib/connection-context' -import { isTuiAgentEnabled } from '../../../shared/tui-agent-selection' -import { CLIENT_PLATFORM } from '@/lib/new-workspace' -import type { LaunchAgentInNewTabArgs } from '@/lib/launch-agent-in-new-tab' /** * Whether a new agent tab starts through the host's `agent.launch`: an AI button's launch, whose @@ -29,37 +24,7 @@ export function newTabPromptLaunchesThroughHost(args: { ) } -/** - * Whether a plain new-tab launch with no prompt ("+", Quick Launch, the floating panel, the - * dashboard) starts through the host. Only where the host's launch is main's own: the window - * decides the terminal, the agent is enabled (main's window never read the disabled list), the - * workspace is not on SSH (main's pane connects first and waits for the remote shell), and the - * window launches it on this machine's own platform (a WSL path launches as Linux here, which the - * host does not infer). Temporary: the rest keeps main's launch until the host matches it. - */ -export function freshNewTabLaunchesThroughHost( - args: Pick, - launchPlatform: NodeJS.Platform -): boolean { - const state = useAppStore.getState() - return ( - args.freshNewTab === true && - !args.prompt?.trim() && - launchPlatform === CLIENT_PLATFORM && - windowMakesHostLaunchTab() && - isTuiAgentEnabled(args.agent, state.settings?.disabledTuiAgents) && - getConnectionIdFromState(state, args.worktreeId) === null - ) -} - -/** Starts a no-prompt new tab through the host; a launch that never started says so in a notice. */ -export function launchFreshNewTabThroughHost(args: HostAgentLaunchArgs): string { - const { tabId, outcome } = launchAgentThroughHost(args) - void outcome.then((launched) => showLaunchNotStartedNotice(launched, args.prompt)) - return tabId -} - -/** The tab is gone, so the pane's own words go in a notice, with its prompt to copy if it had one. */ +/** The tab is gone, so the pane's own words go in a notice, with its prompt to copy. */ function showLaunchNotStartedNotice(outcome: HostAgentLaunchOutcome, prompt: string): void { if (outcome.kind !== 'not-started') { return @@ -70,17 +35,15 @@ function showLaunchNotStartedNotice(outcome: HostAgentLaunchOutcome, prompt: str ? { kind: 'unconfirmed' } : { kind: 'not-started', code: outcome.code ?? '' } ), - prompt - ? { - action: { - label: translate( - 'auto.components.terminal.pane.AgentLaunchPaneNotice.copyPrompt', - 'Copy prompt' - ), - onClick: () => void window.api.ui.writeClipboardText(prompt) - } - } - : undefined + { + action: { + label: translate( + 'auto.components.terminal.pane.AgentLaunchPaneNotice.copyPrompt', + 'Copy prompt' + ), + onClick: () => void window.api.ui.writeClipboardText(prompt) + } + } ) }