diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 9aa5c00a853..208e4530a92 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -17074,7 +17074,9 @@ describe('OrcaRuntimeService', () => { }) runtime.syncWindowGraph(0, { tabs: [], leaves: [] }) - const result = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`) + const result = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { + viewMode: 'chat' + }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ @@ -17090,6 +17092,7 @@ describe('OrcaRuntimeService', () => { type: 'terminal', status: 'ready', terminal: expect.stringMatching(/^term_/), + viewMode: 'chat', isActive: true }) @@ -20138,7 +20141,8 @@ describe('OrcaRuntimeService', () => { }) const result = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, { - activate: false + activate: false, + viewMode: 'chat' }) expect(send).toHaveBeenCalledWith( @@ -20146,7 +20150,8 @@ describe('OrcaRuntimeService', () => { expect.objectContaining({ worktreeId: TEST_WORKTREE_ID, activate: false, - source: 'runtime-session' + source: 'runtime-session', + viewMode: 'chat' }) ) expect(focusTerminal).not.toHaveBeenCalled() diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 09185299c82..a4ad656b4c4 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -17869,6 +17869,7 @@ export class OrcaRuntimeService { agent?: TuiAgent launchConfig?: SleepingAgentLaunchConfig launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' activate?: boolean clientMutationId?: string signal?: AbortSignal @@ -17913,6 +17914,7 @@ export class OrcaRuntimeService { agent?: TuiAgent launchConfig?: SleepingAgentLaunchConfig launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' activate?: boolean clientMutationId?: string signal?: AbortSignal @@ -17946,6 +17948,7 @@ export class OrcaRuntimeService { env: startupCommand.env, startupCommandDelivery: startupCommand.startupCommandDelivery, launchAgent: startupCommand.launchAgent, + viewMode: opts.viewMode, targetGroupId: opts.targetGroupId, launchConfig: startupCommand.launchConfig } @@ -17994,6 +17997,7 @@ export class OrcaRuntimeService { ...(startupCommand.env ? { env: startupCommand.env } : {}), ...(startupCommand.launchConfig ? { launchConfig: startupCommand.launchConfig } : {}), ...(startupCommand.launchAgent ? { launchAgent: startupCommand.launchAgent } : {}), + ...(opts.viewMode ? { viewMode: opts.viewMode } : {}), startupCommandDelivery: startupCommand.startupCommandDelivery, source: 'runtime-session', activate: opts.activate @@ -18057,6 +18061,7 @@ export class OrcaRuntimeService { startupCommandDelivery: startupCommand.startupCommandDelivery, identity: { tabId: pendingSurface.tab.parentTabId, leafId: pendingSurface.tab.leafId }, launchAgent: startupCommand.launchAgent, + viewMode: opts.viewMode, targetGroupId: opts.targetGroupId, launchConfig: startupCommand.launchConfig } @@ -18180,6 +18185,7 @@ export class OrcaRuntimeService { startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] identity?: { tabId: string; leafId: string; sessionId?: string } launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' targetGroupId?: string launchConfig?: SleepingAgentLaunchConfig } = {} @@ -18240,6 +18246,7 @@ export class OrcaRuntimeService { title: terminal.title ?? livePty.pty.title ?? 'Terminal', ...(cwd ? { startupCwd: cwd } : {}), ...(opts.launchAgent ? { launchAgent: opts.launchAgent } : {}), + ...(opts.viewMode ? { viewMode: opts.viewMode } : {}), parentLayout, isActive: activate } diff --git a/src/main/runtime/rpc/methods/session-tabs-schemas.ts b/src/main/runtime/rpc/methods/session-tabs-schemas.ts index 0c21af7de72..1b8afbd2f46 100644 --- a/src/main/runtime/rpc/methods/session-tabs-schemas.ts +++ b/src/main/runtime/rpc/methods/session-tabs-schemas.ts @@ -130,6 +130,7 @@ export const CreateTerminalTab = WorktreeTabSelector.extend({ message: 'Unknown launch agent' }) .optional(), + viewMode: z.enum(['terminal', 'chat']).optional(), activate: z.boolean().optional(), // Why: idempotency key so a retried create (double-tap, reconnect replay) // returns the in-flight operation instead of spawning a duplicate terminal. diff --git a/src/main/runtime/rpc/methods/session-tabs.test.ts b/src/main/runtime/rpc/methods/session-tabs.test.ts index 625959f6d64..d81faa169cd 100644 --- a/src/main/runtime/rpc/methods/session-tabs.test.ts +++ b/src/main/runtime/rpc/methods/session-tabs.test.ts @@ -148,6 +148,7 @@ describe('session tab RPC methods', () => { agentEnv: { CODEX_PROFILE: 'captured' } }, launchAgent: 'codex', + viewMode: 'chat', activate: true }) ) @@ -167,6 +168,7 @@ describe('session tab RPC methods', () => { agentEnv: { CODEX_PROFILE: 'captured' } }, launchAgent: 'codex', + viewMode: 'chat', activate: true }) }) diff --git a/src/main/runtime/rpc/methods/session-tabs.ts b/src/main/runtime/rpc/methods/session-tabs.ts index d27d771f321..55be4d0b21e 100644 --- a/src/main/runtime/rpc/methods/session-tabs.ts +++ b/src/main/runtime/rpc/methods/session-tabs.ts @@ -53,6 +53,7 @@ export const SESSION_TAB_METHODS: RpcAnyMethod[] = [ ...(params.launchConfig ? { launchConfig: params.launchConfig } : {}), ...(params.launchToken ? { launchToken: params.launchToken } : {}), ...(params.launchAgent ? { launchAgent: params.launchAgent } : {}), + ...(params.viewMode ? { viewMode: params.viewMode } : {}), activate: params.activate, clientMutationId: params.clientMutationId, // Why: a dead client connection must cancel the surface wait instead diff --git a/src/renderer/src/hooks/useIpcEvents.test.ts b/src/renderer/src/hooks/useIpcEvents.test.ts index dee27778244..a60b781d664 100644 --- a/src/renderer/src/hooks/useIpcEvents.test.ts +++ b/src/renderer/src/hooks/useIpcEvents.test.ts @@ -1760,6 +1760,7 @@ describe('useIpcEvents updater integration', () => { cwd?: string launchConfig?: SleepingAgentLaunchConfig launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' title?: string activate?: boolean presentation?: 'background' | 'focused' @@ -1906,6 +1907,7 @@ describe('useIpcEvents updater integration', () => { cwd?: string launchConfig?: SleepingAgentLaunchConfig launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' title?: string activate?: boolean presentation?: 'background' | 'focused' @@ -2223,11 +2225,16 @@ describe('useIpcEvents updater integration', () => { targetGroupId: 'group-left', title: 'Runtime Terminal', command: 'codex', + launchAgent: 'codex', + viewMode: 'terminal', activate: true, source: 'runtime-session' }) - expect(createTab).toHaveBeenCalledWith('wt-2', 'group-left', undefined, undefined) + expect(createTab).toHaveBeenCalledWith('wt-2', 'group-left', undefined, { + launchAgent: 'codex', + viewMode: 'terminal' + }) expect(replyTerminalCreate).toHaveBeenCalledWith({ requestId: 'req-runtime-session', tabId: 'tab-new', diff --git a/src/renderer/src/hooks/useIpcEvents.ts b/src/renderer/src/hooks/useIpcEvents.ts index b93f3d1610b..e9475d61dae 100644 --- a/src/renderer/src/hooks/useIpcEvents.ts +++ b/src/renderer/src/hooks/useIpcEvents.ts @@ -1684,16 +1684,20 @@ export function useIpcEvents(): void { if (shouldActivate) { activateTerminalInitiatedWorktree(store, worktreeId) } + // Why: the paired launch client already resolved the initial mode, so + // its explicit choice must win over this host renderer's local default. const tabOptions = data.launchAgent ? { ...(shouldActivate ? {} : { activate: false, recordInteraction: false }), launchAgent: data.launchAgent, - ...initialAgentTabViewModeProps(store.settings, { - agent: data.launchAgent, - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - getConnectionIdFromState(store, worktreeId) - ) - }), + ...(data.viewMode + ? { viewMode: data.viewMode } + : initialAgentTabViewModeProps(store.settings, { + agent: data.launchAgent, + nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( + getConnectionIdFromState(store, worktreeId) + ) + })), ...(data.cwd ? { startupCwd: data.cwd } : {}) } : shouldActivate diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts index fa7283725c4..055ee8b3d19 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts @@ -351,6 +351,30 @@ describe('launchAgentInNewTab', () => { expect(mockQueueTabStartupCommand).not.toHaveBeenCalled() }) + it('propagates the default chat mode to paired web runtime launches', async () => { + mockIsWebRuntimeSessionActive.mockReturnValue(true) + store.settings = { + agentCmdOverrides: {}, + agentDefaultArgs: {}, + agentDefaultEnv: {}, + activeRuntimeEnvironmentId: 'web-runtime', + experimentalNativeChat: true, + openAgentTabsInChatByDefault: true + } + const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') + + launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' }) + + expect(mockCreateWebRuntimeSessionTerminal).toHaveBeenCalledWith( + expect.objectContaining({ + worktreeId: 'wt-1', + environmentId: 'web-runtime', + agent: 'codex', + viewMode: 'chat' + }) + ) + }) + it('surfaces a toast when host agent launch fails in paired web clients', async () => { mockIsWebRuntimeSessionActive.mockReturnValue(true) mockCreateWebRuntimeSessionTerminal.mockResolvedValue(false) 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 52255ac16cf..6b8a0e5ae4e 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -201,6 +201,18 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI return null } + // Why: host-owned paired tabs must receive the same initial-view decision as + // local tabs; the remote host cannot infer this client's draft/default choice. + const viewModePromptDelivery = + hasPrompt && isFollowupPath && promptDelivery === 'auto-submit' ? 'draft' : promptDelivery + const initialViewModeProps = initialAgentTabViewModeProps(store.settings, { + agent, + promptDelivery: viewModePromptDelivery, + nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( + getConnectionIdFromState(store, worktreeId) + ) + }) + const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(store, worktreeId) if (isWebRuntimeSessionActive(runtimeEnvironmentId) && pasteDraftAfterLaunch === null) { launchAgentInWebHostTab({ @@ -210,6 +222,7 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI groupId, hasPrompt, startupPlan, + ...initialViewModeProps, onPromptDelivered }) return { tabId: null, startupPlan, pasteDraftAfterLaunch: false } @@ -224,18 +237,10 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI // stays false), so gate the initial chat view like a `draft` launch — // otherwise a default `auto-submit` followup would open native chat with no // submitted turn to render. - const viewModePromptDelivery = - hasPrompt && isFollowupPath && promptDelivery === 'auto-submit' ? 'draft' : promptDelivery const tab = store.createTab(worktreeId, groupId, undefined, { launchAgent: agent, quickCommandLabel, - ...initialAgentTabViewModeProps(store.settings, { - agent, - promptDelivery: viewModePromptDelivery, - nativeChatTranscriptIsLocalReadable: isNativeChatTranscriptLocalReadable( - getConnectionIdFromState(store, worktreeId) - ) - }) + ...initialViewModeProps }) store.queueTabStartupCommand(tab.id, { command: startupPlan.launchCommand, diff --git a/src/renderer/src/lib/launch-agent-web-host-tab.ts b/src/renderer/src/lib/launch-agent-web-host-tab.ts index b95ccd8aba1..c685dea9968 100644 --- a/src/renderer/src/lib/launch-agent-web-host-tab.ts +++ b/src/renderer/src/lib/launch-agent-web-host-tab.ts @@ -5,7 +5,7 @@ import { createWebRuntimeSessionTerminal, isWebTerminalSurfaceTabId } from '@/runtime/web-runtime-session' -import type { TuiAgent } from '../../../shared/types' +import type { Tab, TuiAgent } from '../../../shared/types' import { translate } from '@/i18n/i18n' function removeStaleLocalAgentTabsForWebHostLaunch(worktreeId: string): void { @@ -32,16 +32,26 @@ export function launchAgentInWebHostTab(args: { groupId?: string hasPrompt: boolean startupPlan: AgentStartupPlan + viewMode?: Tab['viewMode'] onPromptDelivered?: () => void }): void { - const { agent, worktreeId, environmentId, groupId, hasPrompt, startupPlan, onPromptDelivered } = - args + const { + agent, + worktreeId, + environmentId, + groupId, + hasPrompt, + startupPlan, + viewMode, + onPromptDelivered + } = args removeStaleLocalAgentTabsForWebHostLaunch(worktreeId) void createWebRuntimeSessionTerminal({ worktreeId, environmentId, targetGroupId: groupId, activate: true, + ...(viewMode ? { viewMode } : {}), ...(hasPrompt ? { command: startupPlan.launchCommand, diff --git a/src/renderer/src/runtime/web-runtime-session.test.ts b/src/renderer/src/runtime/web-runtime-session.test.ts index 85025022fea..af47e5a0350 100644 --- a/src/renderer/src/runtime/web-runtime-session.test.ts +++ b/src/renderer/src/runtime/web-runtime-session.test.ts @@ -490,6 +490,7 @@ describe('createWebRuntimeSessionTerminal', () => { agentEnv: { CODEX_PROFILE: 'captured' } }, launchAgent: 'codex', + viewMode: 'chat', activate: true }) ).resolves.toBe(true) @@ -510,6 +511,7 @@ describe('createWebRuntimeSessionTerminal', () => { agentEnv: { CODEX_PROFILE: 'captured' } }, launchAgent: 'codex', + viewMode: 'chat', activate: true }, timeoutMs: 15_000 diff --git a/src/renderer/src/runtime/web-runtime-session.ts b/src/renderer/src/runtime/web-runtime-session.ts index 338e92e8656..8cfc23eb19c 100644 --- a/src/renderer/src/runtime/web-runtime-session.ts +++ b/src/renderer/src/runtime/web-runtime-session.ts @@ -56,6 +56,7 @@ export async function createWebRuntimeSessionTerminal(args: { launchConfig?: SleepingAgentLaunchConfig agent?: TuiAgent launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' activate?: boolean selectWorktree?: boolean }): Promise { @@ -85,6 +86,7 @@ export async function createWebRuntimeSessionTerminal(args: { ...(args.launchConfig ? { launchConfig: args.launchConfig } : {}), agent: args.agent, ...(args.launchAgent ? { launchAgent: args.launchAgent } : {}), + ...(args.viewMode ? { viewMode: args.viewMode } : {}), activate: args.activate !== false }, timeoutMs: 15_000 diff --git a/src/shared/runtime-types.ts b/src/shared/runtime-types.ts index 9f91e7dcc8e..028327eb226 100644 --- a/src/shared/runtime-types.ts +++ b/src/shared/runtime-types.ts @@ -490,6 +490,7 @@ type RuntimeTerminalCreateBaseRequestPayload = { launchConfig?: SleepingAgentLaunchConfig launchToken?: string launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' startupCommandDelivery?: StartupCommandDelivery title?: string activate?: boolean