diff --git a/src/renderer/src/lib/agent-launch-routing-caller-census.test.ts b/src/renderer/src/lib/agent-launch-routing-caller-census.test.ts
index 40857e8a55d..3e91c5c8ec0 100644
--- a/src/renderer/src/lib/agent-launch-routing-caller-census.test.ts
+++ b/src/renderer/src/lib/agent-launch-routing-caller-census.test.ts
@@ -8,6 +8,9 @@ const CENSUS_FILE = 'src/renderer/src/lib/agent-launch-routing-caller-census.tes
const LAUNCH_AGENT_IN_NEW_TAB_CALLERS = [
'src/renderer/src/components/dashboard/launch-dashboard-agent.ts',
+ // Joined the funnel rather than appearing beside it: this button used to hand-roll the helper's
+ // terminal arm against `queueTabStartupCommand`. A line going up here is a converged bypass.
+ 'src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx',
'src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.ts',
'src/renderer/src/components/right-sidebar/source-control/ai/recovery-launch.ts',
'src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-commit-actions.ts',
diff --git a/src/renderer/src/lib/agent-launch-routing.test.ts b/src/renderer/src/lib/agent-launch-routing.test.ts
index 941d184602b..e3f7a65938d 100644
--- a/src/renderer/src/lib/agent-launch-routing.test.ts
+++ b/src/renderer/src/lib/agent-launch-routing.test.ts
@@ -111,6 +111,9 @@ describe('resolveAgentLaunchRoute', () => {
}
)
+ // Why floating is here and not with the structured kinds: it has no workspace a session can
+ // be filed under, but the chat view is a pane-level rendering the panel already hosts, so the
+ // chat default still applies — terminal-backed, not structured.
it('keeps floating, WSL, and repair-required launches terminal-backed', () => {
expect(route({ workspaceKind: 'floating' })).toBe('legacy-native-chat')
expect(route({ agent: 'claude', workspaceKind: 'floating' })).toBe('legacy-native-chat')
diff --git a/src/renderer/src/lib/launch-agent-execution-context.ts b/src/renderer/src/lib/launch-agent-execution-context.ts
new file mode 100644
index 00000000000..4d9cf4b41c8
--- /dev/null
+++ b/src/renderer/src/lib/launch-agent-execution-context.ts
@@ -0,0 +1,54 @@
+import type { AgentStartupShell } from '../../../shared/tui-agent-startup-shell'
+import { resolveLocalWindowsAgentStartupShell } from '../../../shared/windows-terminal-shell'
+import { CLIENT_PLATFORM } from '@/lib/new-workspace'
+import { getAgentLaunchPlatformForRepo } from '@/lib/agent-launch-platform'
+import { getConnectionIdFromState } from '@/lib/connection-context'
+import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
+import type { useAppStore } from '@/store'
+
+/** Where a new-tab agent launch runs, and the quoting rules that follow from it. */
+export type AgentLaunchExecutionContext = {
+ /** `undefined` means rival host rows disagree, which is not evidence of a remote. */
+ worktreeSshConnectionId: string | null | undefined
+ resolvedLaunchPlatform: NodeJS.Platform
+ isRemote: boolean
+ /** Only set for a local Windows launch; remote targets need their own shell signal. */
+ queuedShell: AgentStartupShell | undefined
+}
+
+export function resolveAgentLaunchExecutionContext(
+ store: ReturnType
,
+ args: { worktreeId: string; launchPlatform?: NodeJS.Platform }
+): AgentLaunchExecutionContext {
+ const worktree = store
+ .allWorktrees?.()
+ .find((entry: { id: string }) => entry.id === args.worktreeId)
+ const repo = worktree ? store.repos?.find((entry) => entry.id === worktree.repoId) : null
+ // Why: `store.repos.find` is host-blind and the same repo id can exist on local, SSH and runtime
+ // hosts, so the row it returns can belong to a different host than the worktree names (#11163).
+ // The shared resolver answers from the worktree's own host; `undefined` (rival rows disagree) is
+ // not evidence of a remote, and main rejects that launch anyway.
+ const worktreeSshConnectionId = getConnectionIdFromState(store, args.worktreeId)
+ const resolvedLaunchPlatform =
+ args.launchPlatform ??
+ (repo
+ ? getAgentLaunchPlatformForRepo(
+ repo,
+ worktreeSshConnectionId
+ ? undefined
+ : getLocalProjectExecutionRuntimeContext(store, args.worktreeId)
+ )
+ : CLIENT_PLATFORM)
+ // Why: SSH remotes deploy the shim as plain `orca`, so skip the Linux-only `orca-ide` rename for remote launches.
+ const isRemote = Boolean(worktreeSshConnectionId)
+ return {
+ worktreeSshConnectionId,
+ resolvedLaunchPlatform,
+ isRemote,
+ queuedShell: resolveLocalWindowsAgentStartupShell({
+ platform: resolvedLaunchPlatform,
+ isRemote,
+ terminalWindowsShell: store.settings?.terminalWindowsShell
+ })
+ }
+}
diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts
new file mode 100644
index 00000000000..fc5dd6d97eb
--- /dev/null
+++ b/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts
@@ -0,0 +1,143 @@
+// Caller-owned placement coverage for launchAgentInNewTab, split from
+// launch-agent-in-new-tab.test.ts to keep both files within the lines budget.
+
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
+
+const mockCreateTab = vi.fn()
+const mockQueueTabStartupCommand = vi.fn()
+const mockSetActiveTabType = vi.fn()
+const mockSeedNativeChatAppliedSessionOptions = vi.fn()
+
+type PlacementSettings = {
+ agentCmdOverrides: Record
+ agentDefaultArgs: Record
+ agentDefaultEnv: Record>
+ activeRuntimeEnvironmentId: string | null
+ experimentalNativeChat?: boolean
+ experimentalStructuredNativeChat?: boolean
+ openAgentTabsInChatByDefault?: boolean
+ nativeChatSessionOptions?: Record<
+ string,
+ { model?: string; valuesByModel?: Record> }
+ >
+}
+
+function placementSettings(overrides: Partial = {}): PlacementSettings {
+ return {
+ agentCmdOverrides: {},
+ agentDefaultArgs: {},
+ agentDefaultEnv: {},
+ activeRuntimeEnvironmentId: null,
+ ...overrides
+ }
+}
+
+const store = {
+ settings: placementSettings(),
+ repos: [],
+ allWorktrees: vi.fn(() => []),
+ tabsByWorktree: { 'wt-1': [{ id: 'tab-1' }] },
+ openFiles: [],
+ browserTabsByWorktree: {},
+ tabBarOrderByWorktree: {},
+ createTab: mockCreateTab,
+ queueTabInitialCwd: vi.fn(),
+ queueTabStartupCommand: mockQueueTabStartupCommand,
+ setActiveTabType: mockSetActiveTabType,
+ setTabBarOrder: vi.fn()
+}
+
+vi.mock('@/store', () => ({
+ useAppStore: { getState: () => store }
+}))
+
+vi.mock('@/lib/new-workspace', () => ({ CLIENT_PLATFORM: 'darwin' }))
+
+vi.mock('@/lib/connection-context', () => ({
+ getConnectionIdFromState: () => null
+}))
+
+vi.mock('@/lib/native-chat-transcript-readability', () => ({
+ isNativeChatTranscriptLocalReadable: () => true
+}))
+
+vi.mock('@/runtime/web-runtime-session', () => ({
+ isWebRuntimeSessionActive: () => false
+}))
+
+vi.mock('@/lib/worktree-runtime-owner', () => ({
+ getExecutionHostIdForWorktree: () => 'local',
+ getRuntimeEnvironmentIdForWorktree: () => null
+}))
+
+vi.mock('@/components/tab-bar/reconcile-order', () => ({
+ reconcileTabOrder: (_stored: unknown, terminalIds: string[]) => terminalIds
+}))
+
+vi.mock('@/lib/telemetry', () => ({
+ track: vi.fn(),
+ tuiAgentToAgentKind: (agent: string) => agent
+}))
+
+vi.mock('@/components/native-chat/native-chat-session-option-cache', () => ({
+ seedNativeChatAppliedSessionOptions: mockSeedNativeChatAppliedSessionOptions
+}))
+
+describe('launchAgentInNewTab terminal tab activation', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ store.settings = placementSettings()
+ mockCreateTab.mockReturnValue({ id: 'tab-1' })
+ })
+
+ it('takes the global selection by default', async () => {
+ const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
+
+ launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
+
+ expect(mockCreateTab.mock.calls[0]?.[3]).not.toHaveProperty('activate')
+ expect(mockSetActiveTabType).toHaveBeenCalledExactlyOnceWith('terminal')
+ })
+
+ it('honours the chat default in a floating launch while keeping it out of the global selection', async () => {
+ store.settings = placementSettings({
+ experimentalNativeChat: true,
+ experimentalStructuredNativeChat: true,
+ openAgentTabsInChatByDefault: true,
+ nativeChatSessionOptions: {
+ codex: {
+ model: 'gpt-5.2-codex',
+ valuesByModel: { 'gpt-5.2-codex': { effort: 'medium' } }
+ }
+ }
+ })
+ const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
+
+ launchAgentInNewTab({
+ agent: 'codex',
+ worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
+ activate: false
+ })
+
+ // Why: the floating workspace selects within its own group; activating here would move the
+ // main window's active tab to a tab it does not show.
+ expect(mockCreateTab).toHaveBeenCalledWith(
+ FLOATING_TERMINAL_WORKTREE_ID,
+ undefined,
+ undefined,
+ {
+ launchAgent: 'codex',
+ activate: false,
+ viewMode: 'chat'
+ }
+ )
+ expect(mockSetActiveTabType).not.toHaveBeenCalled()
+ // Why: the panel hosts the chat pane itself, so the launch carries the user's model/effort
+ // preferences the same way a main-window launch does.
+ expect(mockSeedNativeChatAppliedSessionOptions).toHaveBeenCalledWith('tab-1', 'codex', {
+ model: 'gpt-5.2-codex',
+ effort: 'medium'
+ })
+ })
+})
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 d76cff1092e..cd823a86f8c 100644
--- a/src/renderer/src/lib/launch-agent-in-new-tab.ts
+++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts
@@ -1,8 +1,6 @@
import { useAppStore } from '@/store'
import type { AgentStartupPlan } from '@/lib/tui-agent-startup'
import { planLaunchAgentStartupPrompt } from '@/lib/launch-agent-startup-prompt-plan'
-import { CLIENT_PLATFORM } from '@/lib/new-workspace'
-import { getAgentLaunchPlatformForRepo } from '@/lib/agent-launch-platform'
import { persistAgentLaunchTabOrder } from '@/lib/launch-agent-tab-order'
import { tuiAgentToAgentKind } from '@/lib/telemetry'
import { createPasteReadinessTimeoutNotice } from '@/lib/launch-agent-paste-timeout-notice'
@@ -13,19 +11,17 @@ import {
import { initialAgentTabViewModeProps } from '@/lib/native-chat-initial-view-mode'
import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
-import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
import { isWebRuntimeSessionActive } from '@/runtime/web-runtime-session'
import { launchAgentInWebHostTab } from '@/lib/launch-agent-web-host-tab'
import {
resolveTuiAgentLaunchArgs,
resolveTuiAgentLaunchEnv
} from '../../../shared/tui-agent-launch-defaults'
-import { resolveLocalWindowsAgentStartupShell } from '../../../shared/windows-terminal-shell'
import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config'
import { seedCommandCodeSubmittedPromptStatus } from '@/lib/command-code-prompt-status-seed'
import type { TuiAgent } from '../../../shared/tui-agent'
import type { LaunchSource } from '../../../shared/telemetry-events'
-import { getConnectionIdFromState } from '@/lib/connection-context'
+import { resolveAgentLaunchExecutionContext } from '@/lib/launch-agent-execution-context'
import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options'
import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache'
import { launchAgentInStructuredNewTab } from '@/lib/launch-agent-in-new-tab-structured'
@@ -56,6 +52,13 @@ export type LaunchAgentInNewTabArgs = {
launchPlatform?: NodeJS.Platform
/** Called after the prompt is actually delivered to the agent input path. */
onPromptDelivered?: () => void
+ /**
+ * Whether the new terminal tab takes the global selection. The floating workspace passes `false`
+ * and selects within its own group instead, so launching there does not move the main window's
+ * active tab. Terminal surface only — the structured and host-published routes own their own
+ * activation.
+ */
+ activate?: boolean
/** Keeps a preflighted route authoritative across workspace creation. */
agentSessionLaunchPlan?: AgentSessionLaunchPlan
/** Lets a workspace reveal itself before the selected surface opens. */
@@ -111,33 +114,15 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent
launchPlatform,
onPromptDelivered,
agentSessionLaunchPlan,
- beforeSurfaceOpen
+ beforeSurfaceOpen,
+ activate
} = args
const store = useAppStore.getState()
- const worktree = store.allWorktrees?.().find((entry: { id: string }) => entry.id === worktreeId)
- const repo = worktree ? store.repos?.find((entry) => entry.id === worktree.repoId) : null
- // Why: `store.repos.find` is host-blind and the same repo id can exist on local, SSH and runtime
- // hosts, so the row it returns can belong to a different host than the worktree names (#11163).
- // The shared resolver answers from the worktree's own host; `undefined` (rival rows disagree) is
- // not evidence of a remote, and main rejects that launch anyway.
- const worktreeSshConnectionId = getConnectionIdFromState(store, worktreeId)
- const resolvedLaunchPlatform =
- launchPlatform ??
- (repo
- ? getAgentLaunchPlatformForRepo(
- repo,
- worktreeSshConnectionId
- ? undefined
- : getLocalProjectExecutionRuntimeContext(store, worktreeId)
- )
- : CLIENT_PLATFORM)
- // Why: SSH remotes deploy the shim as plain `orca`, so skip the Linux-only `orca-ide` rename for remote launches.
- const isRemote = Boolean(worktreeSshConnectionId)
- const queuedShell = resolveLocalWindowsAgentStartupShell({
- platform: resolvedLaunchPlatform,
- isRemote,
- terminalWindowsShell: store.settings?.terminalWindowsShell
- })
+ const { worktreeSshConnectionId, resolvedLaunchPlatform, isRemote, queuedShell } =
+ resolveAgentLaunchExecutionContext(store, {
+ worktreeId,
+ ...(launchPlatform ? { launchPlatform } : {})
+ })
const cmdOverrides = store.settings?.agentCmdOverrides ?? {}
const effectiveAgentArgs =
agentArgs !== undefined
@@ -147,6 +132,7 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent
const trimmedPrompt = prompt?.trim() ?? ''
const hasPrompt = trimmedPrompt.length > 0
const isFollowupPath = TUI_AGENT_CONFIG[agent].promptInjectionMode === 'stdin-after-start'
+ const workspaceKind = workspaceKindForWorktreeId(worktreeId)
// Why: the remote host can't infer this client's draft/default view choice, so decide it here for paired tabs too.
const viewModePromptDelivery =
hasPrompt && isFollowupPath && promptDelivery === 'auto-submit' ? 'draft' : promptDelivery
@@ -216,7 +202,7 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent
agentSessionLaunchPlan ??
planAgentSessionLaunch(store, {
agent,
- workspace: { kind: workspaceKindForWorktreeId(worktreeId), worktreeId },
+ workspace: { kind: workspaceKind, worktreeId },
prompt: trimmedPrompt,
promptDelivery: viewModePromptDelivery,
tuiCustomization: { cwd: initialCwd },
@@ -260,6 +246,7 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent
const tab = store.createTab(worktreeId, groupId, undefined, {
launchAgent: agent,
quickCommandLabel,
+ ...(activate === false ? { activate: false } : {}),
...initialViewModeProps
})
seedNativeChatAppliedSessionOptions(tab.id, agent, startupPlan.sessionOptions)
@@ -329,8 +316,10 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent
onPromptDelivered?.()
}
- // Why: without setActiveTabType('terminal') a worktree showing an editor keeps rendering it and the new tab stays hidden.
- store.setActiveTabType('terminal')
+ // Why: without setActiveTabType('terminal') an activated launch can stay hidden behind an editor.
+ if (activate !== false) {
+ store.setActiveTabType('terminal')
+ }
// Why: persist tab-bar order so reconcileTabOrder doesn't fall back to terminals-first and jump the new tab to index 0.
persistAgentLaunchTabOrder(worktreeId, tab.id)