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

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

* fix(native-chat): keep paired launch mode authoritative

* fix(native-chat): preserve mode through PTY materialization
This commit is contained in:
Jinjing
2026-07-13 13:06:59 -07:00
committed by GitHub
parent fc458f2b30
commit 476b6f97d0
17 changed files with 272 additions and 45 deletions
+58 -8
View File
@@ -17066,6 +17066,16 @@ describe('OrcaRuntimeService', () => {
it('creates mobile session terminals in a headless runtime server', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'pty-headless' })
const runtime = new OrcaRuntimeService(store)
const persistViewMode = vi.spyOn(
runtime as unknown as {
persistHeadlessSessionTabProps: (
worktreeId: string,
tabId: string,
props: { viewMode: 'terminal' | 'chat' }
) => void
},
'persistHeadlessSessionTabProps'
)
runtime.setPtyController({
spawn,
write: () => true,
@@ -17074,7 +17084,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,8 +17102,12 @@ describe('OrcaRuntimeService', () => {
type: 'terminal',
status: 'ready',
terminal: expect.stringMatching(/^term_/),
viewMode: 'chat',
isActive: true
})
expect(persistViewMode).toHaveBeenCalledWith(TEST_WORKTREE_ID, result.tab.parentTabId, {
viewMode: 'chat'
})
const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
expect(listed.tabs).toEqual([
@@ -20138,7 +20154,8 @@ describe('OrcaRuntimeService', () => {
})
const result = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
activate: false
activate: false,
viewMode: 'chat'
})
expect(send).toHaveBeenCalledWith(
@@ -20146,7 +20163,8 @@ describe('OrcaRuntimeService', () => {
expect.objectContaining({
worktreeId: TEST_WORKTREE_ID,
activate: false,
source: 'runtime-session'
source: 'runtime-session',
viewMode: 'chat'
})
)
expect(focusTerminal).not.toHaveBeenCalled()
@@ -20504,7 +20522,8 @@ describe('OrcaRuntimeService', () => {
})
const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
activate: true
activate: true,
viewMode: 'terminal'
})
let settled = false
const settledCreate = create.finally(() => {
@@ -20550,6 +20569,7 @@ describe('OrcaRuntimeService', () => {
leafId: pendingLeafId,
status: 'ready',
terminal: expect.stringMatching(/^term_/),
viewMode: 'terminal',
isActive: true
})
expect(spawn).toHaveBeenCalledWith(
@@ -20567,7 +20587,8 @@ describe('OrcaRuntimeService', () => {
expect.objectContaining({
ptyId: 'pty-materialized',
tabId: 'tab-pending',
leafId: pendingLeafId
leafId: pendingLeafId,
viewMode: 'terminal'
})
)
expect(closeTerminal).not.toHaveBeenCalled()
@@ -20761,7 +20782,8 @@ describe('OrcaRuntimeService', () => {
})
const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
activate: true
activate: true,
viewMode: 'chat'
})
let settled = false
const settledCreate = create.finally(() => {
@@ -20769,8 +20791,35 @@ describe('OrcaRuntimeService', () => {
})
await vi.waitFor(() => expect(send).toHaveBeenCalledTimes(1))
// A shell-only renderer snapshot can win the first race but still omit
// launch props. The later PTY rescue must fill the explicit mode.
runtime.syncWindowGraph(1, {
tabs: [],
leaves: [],
mobileSessionTabs: [
{
worktree: TEST_WORKTREE_ID,
publicationEpoch: 'renderer-shell',
snapshotVersion: 1,
activeGroupId: 'group-1',
activeTabId: `tab-alive::${leafId}`,
activeTabType: 'terminal',
tabs: [
{
type: 'terminal',
id: `tab-alive::${leafId}`,
parentTabId: 'tab-alive',
leafId,
title: 'Terminal',
isActive: true
}
]
}
]
})
// The renderer's own PTY spawn registers with the tab binding — the same
// call the pty IPC layer now makes — without any mobileSessionTabs sync.
// call the pty IPC layer now makes — after the shell-only snapshot.
runtime.registerPty('pty-alive', TEST_WORKTREE_ID, null, {
tabId: 'tab-alive',
leafId
@@ -20786,7 +20835,8 @@ describe('OrcaRuntimeService', () => {
parentTabId: 'tab-alive',
leafId,
status: 'ready',
terminal: expect.stringMatching(/^term_/)
terminal: expect.stringMatching(/^term_/),
viewMode: 'chat'
})
expect(closeTerminal).not.toHaveBeenCalled()
} finally {
+48 -6
View File
@@ -1020,6 +1020,7 @@ type TerminalCreateOptions = {
launchConfig?: WorktreeStartupLaunch['launchConfig']
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
telemetry?: WorktreeStartupLaunch['telemetry']
title?: string
@@ -1318,6 +1319,7 @@ type RuntimeNotifier = {
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
presentation?: RuntimeTerminalPresentation
tabId?: string
@@ -2139,7 +2141,11 @@ export class OrcaRuntimeService {
// creates so ordinary renderer spawns never publish here.
private pendingMobileTerminalCreatesByKey = new Map<
string,
{ activate: boolean; selectIfNoActiveTab: boolean }
{
activate: boolean
selectIfNoActiveTab: boolean
viewMode?: 'terminal' | 'chat'
}
>()
private mobileSessionTabListeners = new Set<(snapshot: RuntimeMobileSessionTabsResult) => void>()
// Why: coalesces title/status-driven session.tabs emits so spinner churn
@@ -3663,6 +3669,7 @@ export class OrcaRuntimeService {
activate: boolean
selectIfNoActiveTab?: boolean
startupCwd?: string
viewMode?: 'terminal' | 'chat'
split?: { splitFromLeafId: string; direction: 'horizontal' | 'vertical' }
}
): void {
@@ -3694,6 +3701,17 @@ export class OrcaRuntimeService {
baseLayout,
args.split
)
// Why: a main-side PTY rescue or split publication must not erase the
// host's explicit tab mode before the renderer graph catches up.
const viewMode =
args.viewMode ??
existingTab?.viewMode ??
existing?.tabs.find(
(candidate): candidate is RuntimeMobileSessionTerminalTab =>
candidate.type === 'terminal' &&
candidate.parentTabId === args.tabId &&
candidate.viewMode !== undefined
)?.viewMode
const tab: RuntimeMobileSessionTerminalTab = {
type: 'terminal',
id: `${args.tabId}::${args.leafId}`,
@@ -3703,6 +3721,7 @@ export class OrcaRuntimeService {
title,
...(pty.launchAgent ? { launchAgent: pty.launchAgent } : {}),
...(args.startupCwd ? { startupCwd: args.startupCwd } : {}),
...(viewMode ? { viewMode } : {}),
parentLayout,
isActive:
args.activate || (args.selectIfNoActiveTab !== false && existing?.activeTabId == null)
@@ -17718,6 +17737,7 @@ export class OrcaRuntimeService {
// Why: explicit background presentation may carry legacy activate
// metadata from an already-owned renderer pane; don't select it on mobile.
selectIfNoActiveTab: presentation !== 'background',
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
...(cwd !== workspace.path ? { startupCwd: cwd } : {})
})
}
@@ -17736,6 +17756,7 @@ export class OrcaRuntimeService {
...(effectiveLaunchConfig ? { launchConfig: effectiveLaunchConfig } : {}),
...(launchToken ? { launchToken } : {}),
...(launchOpts.launchAgent ? { launchAgent: launchOpts.launchAgent } : {}),
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
activate: presentation === 'focused',
...(presentation ? { presentation } : {}),
tabId,
@@ -17811,6 +17832,7 @@ export class OrcaRuntimeService {
...(launchOpts.launchConfig ? { launchConfig: launchOpts.launchConfig } : {}),
...(launchOpts.launchToken ? { launchToken: launchOpts.launchToken } : {}),
...(launchOpts.launchAgent ? { launchAgent: launchOpts.launchAgent } : {}),
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
startupCommandDelivery: launchOpts.startupCommandDelivery,
title: launchOpts.title,
activate: presentation === 'focused',
@@ -17869,6 +17891,7 @@ export class OrcaRuntimeService {
agent?: TuiAgent
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
clientMutationId?: string
signal?: AbortSignal
@@ -17913,6 +17936,7 @@ export class OrcaRuntimeService {
agent?: TuiAgent
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
clientMutationId?: string
signal?: AbortSignal
@@ -17946,6 +17970,7 @@ export class OrcaRuntimeService {
env: startupCommand.env,
startupCommandDelivery: startupCommand.startupCommandDelivery,
launchAgent: startupCommand.launchAgent,
viewMode: opts.viewMode,
targetGroupId: opts.targetGroupId,
launchConfig: startupCommand.launchConfig
}
@@ -17994,6 +18019,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
@@ -18012,7 +18038,8 @@ export class OrcaRuntimeService {
// requested group, so any wrong-group placement is cosmetic and stall-window-only.
this.pendingMobileTerminalCreatesByKey.set(pendingCreateKey, {
activate: opts.activate !== false,
selectIfNoActiveTab: true
selectIfNoActiveTab: true,
...(opts.viewMode ? { viewMode: opts.viewMode } : {})
})
try {
// Why: the PTY spawn and the tabCreate reply race on independent IPC
@@ -18057,6 +18084,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 +18208,7 @@ export class OrcaRuntimeService {
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
identity?: { tabId: string; leafId: string; sessionId?: string }
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
targetGroupId?: string
launchConfig?: SleepingAgentLaunchConfig
} = {}
@@ -18197,6 +18226,7 @@ export class OrcaRuntimeService {
env: opts.env,
...(opts.launchConfig ? { launchConfig: opts.launchConfig } : {}),
...(opts.launchAgent ? { launchAgent: opts.launchAgent } : {}),
...(opts.viewMode ? { viewMode: opts.viewMode } : {}),
startupCommandDelivery: opts.startupCommandDelivery,
...(opts.identity
? {
@@ -18218,6 +18248,11 @@ export class OrcaRuntimeService {
}
const parentTabId = livePty.pty.tabId ?? `pty:${livePty.pty.ptyId}`
const leafId = parsePaneKey(livePty.pty.paneKey ?? '')?.leafId ?? randomUUID()
if (opts.viewMode) {
// Why: the runtime-owned binding must survive a serve restart with the
// same initial mode, not fall back to a later client's local default.
this.persistHeadlessSessionTabProps(worktreeId, parentTabId, { viewMode: opts.viewMode })
}
const existing = this.mobileSessionTabsByWorktree.get(worktreeId)
const existingSurface =
existing?.tabs.find(
@@ -18240,6 +18275,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
}
@@ -18386,21 +18422,27 @@ export class OrcaRuntimeService {
return null
}
const existing = this.findMobileTerminalSurface(worktreeId, tabId)
if (existing) {
// Why: the renderer's own publication already landed; stay idempotent.
if (
existing &&
this.isReadyMobileTerminalSurface(existing) &&
(pending.viewMode === undefined || existing.tab.viewMode === pending.viewMode)
) {
// Why: the renderer's ready publication already landed with the intended
// mode; only a pending shell still needs the main-side PTY rescue.
return existing
}
const pty = this.findLiveRegisteredPtyForRendererTab(worktreeId, tabId)
const leafId = pty ? parsePaneKey(pty.paneKey ?? '')?.leafId : undefined
if (!pty || !leafId) {
return null
return existing
}
this.publishPtyBackedMobileSessionTerminal(worktreeId, pty, {
tabId,
leafId,
title: null,
activate: pending.activate,
selectIfNoActiveTab: pending.selectIfNoActiveTab
selectIfNoActiveTab: pending.selectIfNoActiveTab,
...(pending.viewMode ? { viewMode: pending.viewMode } : {})
})
// Why: waitForMobileTerminalSurface's check closures are drained only inside
// syncWindowGraph; a main-side publish must drain them too or the pending
@@ -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
@@ -631,13 +631,20 @@ describe('attachMainWindowServices', () => {
const notifier = runtime.setNotifier.mock.calls[0][0] as {
revealTerminalSession: (
worktreeId: string,
opts: { ptyId: string; title?: string; cwd?: string; activate?: boolean }
opts: {
ptyId: string
title?: string
cwd?: string
viewMode?: 'terminal' | 'chat'
activate?: boolean
}
) => Promise<{ tabId: string; title?: string }>
}
const revealPromise = notifier.revealTerminalSession('wt-1', {
ptyId: 'pty-1',
title: 'SSH tmux',
cwd: '/repo/packages/web'
cwd: '/repo/packages/web',
viewMode: 'chat'
})
const sentPayload = sendMock.mock.calls.find(
([channel]) => channel === 'ui:createTerminal'
@@ -645,7 +652,7 @@ describe('attachMainWindowServices', () => {
const handler = onMock.mock.calls.find(
([channel]) => channel === 'terminal:tabCreateReply'
)?.[1]
expect(sentPayload.cwd).toBe('/repo/packages/web')
expect(sentPayload).toMatchObject({ cwd: '/repo/packages/web', viewMode: 'chat' })
handler?.(
{ sender: { send: vi.fn() } },
@@ -328,6 +328,7 @@ function registerRuntimeWindowLifecycle(
...(opts.launchConfig ? { launchConfig: opts.launchConfig } : {}),
...(opts.launchToken ? { launchToken: opts.launchToken } : {}),
...(opts.launchAgent ? { launchAgent: opts.launchAgent } : {}),
...(opts.viewMode ? { viewMode: opts.viewMode } : {}),
activate: opts.activate !== false,
...(opts.presentation ? { presentation: opts.presentation } : {}),
// Why: pre-minted tabId from main keeps the renderer's tab id aligned
+1
View File
@@ -2800,6 +2800,7 @@ export type PreloadApi = {
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
title?: string
ptyId?: string
activate?: boolean
+2
View File
@@ -3445,6 +3445,7 @@ const api = {
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
title?: string
ptyId?: string
activate?: boolean
@@ -3467,6 +3468,7 @@ const api = {
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
title?: string
ptyId?: string
activate?: boolean
+40 -1
View File
@@ -1732,6 +1732,7 @@ describe('useIpcEvents updater integration', () => {
command?: string
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
title?: string
ptyId?: string
activate?: boolean
@@ -1760,6 +1761,7 @@ describe('useIpcEvents updater integration', () => {
cwd?: string
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
title?: string
activate?: boolean
presentation?: 'background' | 'focused'
@@ -1877,6 +1879,7 @@ describe('useIpcEvents updater integration', () => {
command?: string
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
title?: string
ptyId?: string
activate?: boolean
@@ -1906,6 +1909,7 @@ describe('useIpcEvents updater integration', () => {
cwd?: string
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
title?: string
activate?: boolean
presentation?: 'background' | 'focused'
@@ -2223,11 +2227,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',
@@ -2364,6 +2373,36 @@ describe('useIpcEvents updater integration', () => {
}
)
createTab.mockClear()
createTerminalListenerRef.current({
worktreeId: 'wt-2',
ptyId: 'pty-explicit-terminal',
launchAgent: 'codex',
viewMode: 'terminal'
})
expect(createTab).toHaveBeenCalledWith('wt-2', undefined, undefined, {
initialPtyId: 'pty-explicit-terminal',
activate: false,
launchAgent: 'codex',
viewMode: 'terminal'
})
createTab.mockClear()
storeState.settings.openAgentTabsInChatByDefault = false
createTerminalListenerRef.current({
worktreeId: 'wt-2',
ptyId: 'pty-explicit-chat',
launchAgent: 'codex',
viewMode: 'chat'
})
expect(createTab).toHaveBeenCalledWith('wt-2', undefined, undefined, {
initialPtyId: 'pty-explicit-chat',
activate: false,
launchAgent: 'codex',
viewMode: 'chat'
})
storeState.settings.openAgentTabsInChatByDefault = true
createTab.mockClear()
setActiveView.mockClear()
setActiveWorktree.mockClear()
+22 -13
View File
@@ -1434,6 +1434,7 @@ export function useIpcEvents(): void {
launchConfig,
launchToken,
launchAgent,
viewMode,
title,
ptyId,
activate,
@@ -1502,13 +1503,17 @@ export function useIpcEvents(): void {
...(launchAgent
? {
launchAgent,
...initialAgentTabViewModeProps(store.settings, {
agent: launchAgent,
nativeChatTranscriptIsLocalReadable:
isNativeChatTranscriptLocalReadable(
getConnectionIdFromState(store, worktreeId)
)
})
// Why: a paired client resolved explicit mode before
// PTY materialization; only omitted mode uses host defaults.
...(viewMode
? { viewMode }
: initialAgentTabViewModeProps(store.settings, {
agent: launchAgent,
nativeChatTranscriptIsLocalReadable:
isNativeChatTranscriptLocalReadable(
getConnectionIdFromState(store, worktreeId)
)
}))
}
: {}),
...(cwd ? { startupCwd: cwd } : {}),
@@ -1684,16 +1689,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
@@ -300,7 +300,8 @@ describe('launchAgentInNewTab', () => {
environmentId: 'web-runtime',
targetGroupId: 'group-1',
activate: true,
agent: 'claude'
agent: 'claude',
viewMode: 'terminal'
})
expect(mockCreateTab).not.toHaveBeenCalled()
expect(mockQueueTabStartupCommand).not.toHaveBeenCalled()
@@ -345,12 +346,61 @@ describe('launchAgentInNewTab', () => {
agentArgs: '--model gpt-5 --reasoning-effort high',
agentEnv: { CODEX_PROFILE: 'captured' }
},
launchAgent: 'codex'
launchAgent: 'codex',
viewMode: 'terminal'
})
expect(mockCreateTab).not.toHaveBeenCalled()
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('propagates the resolved terminal mode to paired web runtime launches', async () => {
mockIsWebRuntimeSessionActive.mockReturnValue(true)
store.settings = {
agentCmdOverrides: {},
agentDefaultArgs: {},
agentDefaultEnv: {},
activeRuntimeEnvironmentId: 'web-runtime',
experimentalNativeChat: true,
openAgentTabsInChatByDefault: false
}
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: 'terminal'
})
)
})
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,9 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
groupId,
hasPrompt,
startupPlan,
// Why: omission means terminal locally, but would let a paired host apply
// its own default; send the client's resolved terminal choice explicitly.
viewMode: initialViewModeProps.viewMode ?? 'terminal',
onPromptDelivered
})
return { tabId: null, startupPlan, pasteDraftAfterLaunch: false }
@@ -224,18 +239,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