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) + } + } ) }