Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 58 additions & 8 deletions src/main/runtime/orca-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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([
Expand Down Expand Up @@ -20138,15 +20154,17 @@ describe('OrcaRuntimeService', () => {
})

const result = await runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
activate: false
activate: false,
viewMode: 'chat'
})

expect(send).toHaveBeenCalledWith(
'terminal:requestTabCreate',
expect.objectContaining({
worktreeId: TEST_WORKTREE_ID,
activate: false,
source: 'runtime-session'
source: 'runtime-session',
viewMode: 'chat'
})
)
expect(focusTerminal).not.toHaveBeenCalled()
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -20550,6 +20569,7 @@ describe('OrcaRuntimeService', () => {
leafId: pendingLeafId,
status: 'ready',
terminal: expect.stringMatching(/^term_/),
viewMode: 'terminal',
isActive: true
})
expect(spawn).toHaveBeenCalledWith(
Expand All @@ -20567,7 +20587,8 @@ describe('OrcaRuntimeService', () => {
expect.objectContaining({
ptyId: 'pty-materialized',
tabId: 'tab-pending',
leafId: pendingLeafId
leafId: pendingLeafId,
viewMode: 'terminal'
})
)
expect(closeTerminal).not.toHaveBeenCalled()
Expand Down Expand Up @@ -20761,16 +20782,44 @@ describe('OrcaRuntimeService', () => {
})

const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
activate: true
activate: true,
viewMode: 'chat'
})
let settled = false
const settledCreate = create.finally(() => {
settled = true
})
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
Expand All @@ -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 {
Expand Down
54 changes: 48 additions & 6 deletions src/main/runtime/orca-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,7 @@ type TerminalCreateOptions = {
launchConfig?: WorktreeStartupLaunch['launchConfig']
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
telemetry?: WorktreeStartupLaunch['telemetry']
title?: string
Expand Down Expand Up @@ -1318,6 +1319,7 @@ type RuntimeNotifier = {
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
presentation?: RuntimeTerminalPresentation
tabId?: string
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3663,6 +3669,7 @@ export class OrcaRuntimeService {
activate: boolean
selectIfNoActiveTab?: boolean
startupCwd?: string
viewMode?: 'terminal' | 'chat'
split?: { splitFromLeafId: string; direction: 'horizontal' | 'vertical' }
}
): void {
Expand Down Expand Up @@ -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}`,
Expand All @@ -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)
Expand Down Expand Up @@ -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 } : {})
})
}
Expand All @@ -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,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -17869,6 +17891,7 @@ export class OrcaRuntimeService {
agent?: TuiAgent
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
clientMutationId?: string
signal?: AbortSignal
Expand Down Expand Up @@ -17913,6 +17936,7 @@ export class OrcaRuntimeService {
agent?: TuiAgent
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
clientMutationId?: string
signal?: AbortSignal
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
} = {}
Expand All @@ -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
? {
Expand All @@ -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(
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/main/runtime/rpc/methods/session-tabs-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/main/runtime/rpc/methods/session-tabs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ describe('session tab RPC methods', () => {
agentEnv: { CODEX_PROFILE: 'captured' }
},
launchAgent: 'codex',
viewMode: 'chat',
activate: true
})
)
Expand All @@ -167,6 +168,7 @@ describe('session tab RPC methods', () => {
agentEnv: { CODEX_PROFILE: 'captured' }
},
launchAgent: 'codex',
viewMode: 'chat',
activate: true
})
})
Expand Down
1 change: 1 addition & 0 deletions src/main/runtime/rpc/methods/session-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading