mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
fix(native-chat): keep paired launch mode authoritative
This commit is contained in:
@@ -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,
|
||||
@@ -17095,6 +17105,9 @@ describe('OrcaRuntimeService', () => {
|
||||
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([
|
||||
@@ -20766,7 +20779,8 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
|
||||
const create = runtime.createMobileSessionTerminal(`id:${TEST_WORKTREE_ID}`, {
|
||||
activate: true
|
||||
activate: true,
|
||||
viewMode: 'chat'
|
||||
})
|
||||
let settled = false
|
||||
const settledCreate = create.finally(() => {
|
||||
@@ -20774,8 +20788,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
|
||||
@@ -20791,7 +20832,8 @@ describe('OrcaRuntimeService', () => {
|
||||
parentTabId: 'tab-alive',
|
||||
leafId,
|
||||
status: 'ready',
|
||||
terminal: expect.stringMatching(/^term_/)
|
||||
terminal: expect.stringMatching(/^term_/),
|
||||
viewMode: 'chat'
|
||||
})
|
||||
expect(closeTerminal).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
|
||||
@@ -2139,7 +2139,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 +3667,7 @@ export class OrcaRuntimeService {
|
||||
activate: boolean
|
||||
selectIfNoActiveTab?: boolean
|
||||
startupCwd?: string
|
||||
viewMode?: 'terminal' | 'chat'
|
||||
split?: { splitFromLeafId: string; direction: 'horizontal' | 'vertical' }
|
||||
}
|
||||
): void {
|
||||
@@ -3694,6 +3699,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 +3719,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)
|
||||
@@ -18016,7 +18033,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
|
||||
@@ -18224,6 +18242,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(
|
||||
@@ -18393,21 +18416,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
|
||||
|
||||
@@ -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,7 +346,8 @@ 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()
|
||||
@@ -375,6 +377,30 @@ describe('launchAgentInNewTab', () => {
|
||||
)
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
@@ -222,7 +222,9 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
|
||||
groupId,
|
||||
hasPrompt,
|
||||
startupPlan,
|
||||
...initialViewModeProps,
|
||||
// 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 }
|
||||
|
||||
Reference in New Issue
Block a user