Fix remote runtime terminal sharing with mobile (#4901)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong
2026-06-08 14:26:07 -07:00
committed by GitHub
co-authored by Orca
parent 90ac74d9b9
commit f5efdd5945
14 changed files with 1133 additions and 119 deletions
+342 -2
View File
@@ -39,6 +39,7 @@ import {
OrcaRuntimeService,
type RuntimeTerminalAgentStatusEvent
} from './orca-runtime'
import type { RuntimeMobileSessionTabsResult } from '../../shared/runtime-types'
import {
registerSshFilesystemProvider,
unregisterSshFilesystemProvider
@@ -5056,12 +5057,14 @@ describe('OrcaRuntimeService', () => {
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
const { handle } = await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`)
await expect(runtime.renameTerminal(handle, 'Worker')).resolves.toMatchObject({
const renamed = await runtime.renameTerminal(handle, 'Worker')
expect(renamed).toMatchObject({
handle,
tabId: 'pty:pty-bg',
title: 'Worker'
})
expect(renamed.tabId).not.toContain(':')
await expect(runtime.showTerminal(handle)).resolves.toMatchObject({
tabId: renamed.tabId,
title: 'Worker'
})
})
@@ -7190,6 +7193,343 @@ describe('OrcaRuntimeService', () => {
])
})
it('publishes laptop-created remote runtime terminals to phone session tabs', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' })
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [],
leaves: [],
mobileSessionTabs: [
{
worktree: TEST_WORKTREE_ID,
publicationEpoch: 'renderer-empty',
snapshotVersion: 1,
activeGroupId: null,
activeTabId: null,
activeTabType: null,
tabs: []
}
]
})
const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
command: "claude 'work on the issue'",
tabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID
})
runtime.onPtyData('laptop-created-pty', '\x1b]0;Codex working\x07', Date.now())
runtime.onPtyData('laptop-created-pty', 'Claude is working...\r\n', Date.now())
const phoneTabs = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
expect(laptopTerminal.surface).toBe('background')
expect(phoneTabs.tabs).toEqual([
expect.objectContaining({
type: 'terminal',
parentTabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID,
status: 'ready',
terminal: laptopTerminal.handle,
agentStatus: expect.objectContaining({
state: 'working',
paneKey: `laptop-tab:${HEADLESS_LEAF_ID}`,
terminalHandle: laptopTerminal.handle
})
})
])
await expect(runtime.readTerminal(laptopTerminal.handle)).resolves.toMatchObject({
tail: ['Claude is working...']
})
})
it('replaces pending phone session tabs when a laptop-created remote PTY becomes live', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' })
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [],
leaves: [],
mobileSessionTabs: [
{
worktree: TEST_WORKTREE_ID,
publicationEpoch: 'renderer-pending',
snapshotVersion: 1,
activeGroupId: 'group-1',
activeTabId: `laptop-tab::${HEADLESS_LEAF_ID}`,
activeTabType: 'terminal',
tabGroups: [{ id: 'group-1', activeTabId: 'laptop-tab', tabOrder: ['laptop-tab'] }],
tabs: [
{
type: 'terminal',
id: `laptop-tab::${HEADLESS_LEAF_ID}`,
parentTabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID,
title: 'Starting Claude',
isActive: true
}
]
}
]
})
const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
tabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID
})
const phoneTabs = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
expect(phoneTabs.tabs).toHaveLength(1)
expect(phoneTabs.tabs[0]).toMatchObject({
type: 'terminal',
id: `laptop-tab::${HEADLESS_LEAF_ID}`,
status: 'ready',
terminal: laptopTerminal.handle
})
})
it('publishes laptop-created remote runtime split terminals to phone session tabs', async () => {
const spawn = vi
.fn()
.mockResolvedValueOnce({ id: 'laptop-created-pty' })
.mockResolvedValueOnce({ id: 'laptop-split-pty' })
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
tabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID
})
const split = await runtime.splitTerminal(laptopTerminal.handle, {
direction: 'vertical'
})
const phoneTabs = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)
const terminalTabs = phoneTabs.tabs.filter((tab) => tab.type === 'terminal')
expect(split.tabId).toBe('laptop-tab')
expect(terminalTabs).toHaveLength(2)
expect(terminalTabs).toEqual(
expect.arrayContaining([
expect.objectContaining({
parentTabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID,
status: 'ready',
terminal: laptopTerminal.handle
}),
expect.objectContaining({
parentTabId: 'laptop-tab',
status: 'ready',
terminal: split.handle
})
])
)
})
it('pushes PTY-backed mobile session tab title and agent status changes to subscribers', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' })
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const events: RuntimeMobileSessionTabsResult[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot))
const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
tabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID
})
events.length = 0
runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07', 123)
runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude waiting for permission\x07', 124)
expect(events).toEqual([
expect.objectContaining({
tabs: [
expect.objectContaining({
type: 'terminal',
title: 'Claude working',
agentStatus: expect.objectContaining({
state: 'working',
terminalHandle: laptopTerminal.handle
})
})
]
}),
expect.objectContaining({
tabs: [
expect.objectContaining({
type: 'terminal',
agentStatus: expect.objectContaining({
state: 'blocked',
terminalHandle: laptopTerminal.handle
})
})
]
})
])
expect(events[1]!.snapshotVersion).toBeGreaterThan(events[0]!.snapshotVersion)
unsubscribe()
})
it('pushes PTY-backed mobile session readiness changes when a server PTY exits', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' })
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const events: RuntimeMobileSessionTabsResult[] = []
runtime.onMobileSessionTabsChanged((snapshot) => events.push(snapshot))
const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
tabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID
})
events.length = 0
runtime.onPtyExit('laptop-created-pty', 0)
expect(events).toEqual([
expect.objectContaining({
tabs: [
expect.objectContaining({
type: 'terminal',
parentTabId: 'laptop-tab',
status: 'pending-handle',
terminal: null
})
]
})
])
await expect(runtime.readTerminal(laptopTerminal.handle)).resolves.toMatchObject({
status: 'exited'
})
})
it('operates PTY-backed mobile session terminals without a renderer graph', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' })
const kill = vi.fn(() => true)
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn,
write: () => true,
kill,
getForegroundProcess: async () => null
})
const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
tabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID
})
await expect(runtime.renameTerminal(laptopTerminal.handle, 'Shared Claude')).resolves.toEqual({
handle: laptopTerminal.handle,
tabId: 'laptop-tab',
title: 'Shared Claude'
})
await expect(runtime.focusTerminal(laptopTerminal.handle)).resolves.toEqual({
handle: laptopTerminal.handle,
tabId: 'laptop-tab',
worktreeId: TEST_WORKTREE_ID
})
await expect(runtime.closeTerminal(laptopTerminal.handle)).resolves.toEqual({
handle: laptopTerminal.handle,
tabId: 'laptop-tab',
ptyKilled: true
})
expect(kill).toHaveBeenCalledWith('laptop-created-pty')
})
it('lists PTY-backed mobile session terminals without a renderer graph', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' })
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
tabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID
})
runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07hello\r\n', 123)
await expect(runtime.listTerminals(`id:${TEST_WORKTREE_ID}`)).resolves.toMatchObject({
terminals: [
expect.objectContaining({
handle: laptopTerminal.handle,
worktreeId: TEST_WORKTREE_ID,
title: 'Claude working',
connected: true,
preview: 'hello'
})
],
totalCount: 1,
truncated: false
})
})
it('shows and resolves active PTY-backed mobile session terminals without a renderer graph', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'laptop-created-pty' })
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
const laptopTerminal = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, {
tabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID,
activate: true
})
runtime.onPtyData('laptop-created-pty', '\x1b]0;Claude working\x07hello\r\n', 123)
await expect(runtime.resolveActiveTerminal(`id:${TEST_WORKTREE_ID}`)).resolves.toBe(
laptopTerminal.handle
)
await expect(runtime.showTerminal(laptopTerminal.handle)).resolves.toMatchObject({
handle: laptopTerminal.handle,
tabId: 'laptop-tab',
leafId: HEADLESS_LEAF_ID,
worktreeId: TEST_WORKTREE_ID,
title: 'Claude working',
connected: true,
ptyId: 'laptop-created-pty'
})
})
it('keeps split sibling headless mobile terminal leaves when a desktop renderer omits them', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.syncWindowGraph(0, {