fix(native-chat): preserve initial chat mode on paired-host launches

This commit is contained in:
Jinjing
2026-07-13 06:24:56 -07:00
parent 9a77f56671
commit c7cb6a2167
13 changed files with 93 additions and 22 deletions
+8 -3
View File
@@ -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()
+7
View File
@@ -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
}
@@ -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.
@@ -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
})
})
@@ -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
+8 -1
View File
@@ -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',
+10 -6
View File
@@ -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
@@ -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)
@@ -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,
@@ -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,
@@ -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
@@ -56,6 +56,7 @@ export async function createWebRuntimeSessionTerminal(args: {
launchConfig?: SleepingAgentLaunchConfig
agent?: TuiAgent
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
selectWorktree?: boolean
}): Promise<boolean> {
@@ -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
+1
View File
@@ -490,6 +490,7 @@ type RuntimeTerminalCreateBaseRequestPayload = {
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
startupCommandDelivery?: StartupCommandDelivery
title?: string
activate?: boolean