mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(mobile): keep a headless host's chat tab through snapshot rebuilds (#30028)
On a host with no window, chat tabs live only in the runtime's session snapshot. A forced rebuild from the saved session (closing a saved editor tab, an expired SSH reattach) dropped them, and every terminal-only group merge removed them from the tab order. Refs #29968 Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RuntimeMobileSessionSnapshotTab } from '../../shared/runtime-types'
|
||||
import { appendChatTabOrder, keepSnapshotChatTabs } from './mobile-session-tab-merge'
|
||||
|
||||
const chat: RuntimeMobileSessionSnapshotTab = {
|
||||
type: 'agent-session',
|
||||
id: 'agent-session:claude_a',
|
||||
title: 'Claude',
|
||||
sessionId: 'claude_a',
|
||||
agent: 'claude',
|
||||
isActive: true
|
||||
}
|
||||
const terminal: RuntimeMobileSessionSnapshotTab = {
|
||||
type: 'terminal',
|
||||
id: 'term::leaf',
|
||||
parentTabId: 'term',
|
||||
leafId: 'leaf',
|
||||
title: 'Terminal',
|
||||
isActive: true
|
||||
}
|
||||
|
||||
describe('chat tabs across a rebuild from persisted state', () => {
|
||||
it('keeps the chat the only active tab when it was the one shown', () => {
|
||||
const tabs = keepSnapshotChatTabs([terminal], { tabs: [chat], activeTabId: chat.id })
|
||||
|
||||
expect(tabs.map((tab) => [tab.id, tab.isActive])).toEqual([
|
||||
[terminal.id, false],
|
||||
[chat.id, true]
|
||||
])
|
||||
})
|
||||
|
||||
it('returns a chat to the place it held, not the end', () => {
|
||||
const groups = appendChatTabOrder(
|
||||
[{ id: 'g', activeTabId: 'term', tabOrder: ['term', 'other'] }],
|
||||
[{ id: 'g', activeTabId: chat.id, tabOrder: [chat.id, 'term', 'editor', 'other'] }],
|
||||
[chat, terminal],
|
||||
chat.id
|
||||
)
|
||||
|
||||
expect(groups).toEqual([
|
||||
{ id: 'g', activeTabId: chat.id, tabOrder: [chat.id, 'term', 'other'] }
|
||||
])
|
||||
})
|
||||
|
||||
it('follows the tab it sat after when that tab survives', () => {
|
||||
const groups = appendChatTabOrder(
|
||||
[{ id: 'g', activeTabId: 'term', tabOrder: ['term', 'other'] }],
|
||||
[{ id: 'g', activeTabId: 'term', tabOrder: ['term', chat.id, 'other'] }],
|
||||
[{ ...chat, isActive: false }, terminal]
|
||||
)
|
||||
|
||||
expect(groups[0]?.tabOrder).toEqual(['term', chat.id, 'other'])
|
||||
expect(groups[0]?.activeTabId).toBe('term')
|
||||
})
|
||||
})
|
||||
@@ -129,3 +129,73 @@ export function mergeMobileSessionTabGroups(
|
||||
})
|
||||
.filter((group) => group.tabOrder.length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* A tab list rebuilt from persisted state, plus the chat tabs only `prior` holds. Chat tabs exist
|
||||
* only in the runtime's snapshot, so no persisted terminal or layout state can recreate them.
|
||||
*/
|
||||
export function keepSnapshotChatTabs(
|
||||
rebuilt: readonly RuntimeMobileSessionSnapshotTab[],
|
||||
prior:
|
||||
| { tabs: readonly RuntimeMobileSessionSnapshotTab[]; activeTabId: string | null }
|
||||
| undefined
|
||||
): RuntimeMobileSessionSnapshotTab[] {
|
||||
const chatTabs = prior?.tabs.filter((tab) => tab.type === 'agent-session') ?? []
|
||||
if (chatTabs.length === 0) {
|
||||
return [...rebuilt]
|
||||
}
|
||||
// One active tab: a rebuilt terminal's own flag must not outrank the chat the snapshot showed.
|
||||
const activeChatId = chatTabs.find((tab) => tab.id === prior?.activeTabId)?.id ?? null
|
||||
return [
|
||||
...rebuilt.map((tab) => (activeChatId && tab.isActive ? { ...tab, isActive: false } : tab)),
|
||||
...chatTabs.map((tab) => ({ ...tab, isActive: tab.id === activeChatId }))
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts chat tabs back into the group order a terminal-only merge rebuilt without them: each in the
|
||||
* group that listed it before (else the first), after the tab it followed there.
|
||||
*/
|
||||
export function appendChatTabOrder(
|
||||
groups: readonly RuntimeMobileSessionTabGroup[],
|
||||
priorGroups: readonly RuntimeMobileSessionTabGroup[] | undefined,
|
||||
tabs: readonly RuntimeMobileSessionSnapshotTab[],
|
||||
activeTabId: string | null = tabs.find((tab) => tab.isActive)?.id ?? null
|
||||
): RuntimeMobileSessionTabGroup[] {
|
||||
const chatTabIds = tabs.filter((tab) => tab.type === 'agent-session').map((tab) => tab.id)
|
||||
if (chatTabIds.length === 0) {
|
||||
return [...groups]
|
||||
}
|
||||
const next = groups.map((group) => ({ ...group, tabOrder: [...group.tabOrder] }))
|
||||
for (const tabId of chatTabIds) {
|
||||
const priorGroup = priorGroups?.find((group) => group.tabOrder.includes(tabId))
|
||||
const target =
|
||||
next.find((group) => group.tabOrder.includes(tabId)) ??
|
||||
next.find((group) => group.id === priorGroup?.id) ??
|
||||
next[0]
|
||||
if (!target) {
|
||||
continue
|
||||
}
|
||||
if (!target.tabOrder.includes(tabId)) {
|
||||
const at = chatInsertIndex(target.tabOrder, priorGroup?.tabOrder ?? [], tabId)
|
||||
target.tabOrder.splice(at, 0, tabId)
|
||||
}
|
||||
if (tabId === activeTabId) {
|
||||
target.activeTabId = tabId
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function chatInsertIndex(
|
||||
order: readonly string[],
|
||||
priorOrder: readonly string[],
|
||||
tabId: string
|
||||
): number {
|
||||
const priorIndex = priorOrder.indexOf(tabId)
|
||||
if (priorIndex === -1) {
|
||||
return order.length
|
||||
}
|
||||
const previous = priorOrder.slice(0, priorIndex).findLast((id) => order.includes(id))
|
||||
return previous === undefined ? 0 : order.indexOf(previous) + 1
|
||||
}
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
TEST_WORKTREE_ID,
|
||||
TEST_WORKTREE_PATH,
|
||||
makeHeadlessTerminalLayout,
|
||||
makeRuntimeStoreWithWorkspaceSession,
|
||||
makeWorkspaceSessionWithHeadlessTerminal
|
||||
} from '../orca-runtime-test-fixtures.spec'
|
||||
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
|
||||
|
||||
// #29968: on a host with no window a chat tab lives only in the runtime's snapshot; rebuilding that
|
||||
// snapshot from the saved terminals and editors must not drop it.
|
||||
const WORKTREE = `id:${TEST_WORKTREE_ID}`
|
||||
const README = `${TEST_WORKTREE_PATH}/README.md`
|
||||
const CHAT_TAB_ID = 'agent-session:claude_29968'
|
||||
const SERVE_PTY_ID = 'serve-29968-pty'
|
||||
|
||||
function sessionWithTerminalAndEditor(): WorkspaceSessionState {
|
||||
return makeWorkspaceSessionWithHeadlessTerminal({
|
||||
activeTabId: 'serve-tab',
|
||||
activeTabIdByWorktree: { [TEST_WORKTREE_ID]: 'serve-tab' },
|
||||
tabsByWorktree: {
|
||||
[TEST_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'serve-tab',
|
||||
ptyId: SERVE_PTY_ID,
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
title: 'Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
'serve-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: SERVE_PTY_ID })
|
||||
},
|
||||
openFilesByWorktree: {
|
||||
[TEST_WORKTREE_ID]: [
|
||||
{
|
||||
filePath: README,
|
||||
relativePath: 'README.md',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
language: 'markdown',
|
||||
runtimeEnvironmentId: null
|
||||
}
|
||||
]
|
||||
},
|
||||
unifiedTabs: {
|
||||
[TEST_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'tab-readme',
|
||||
entityId: README,
|
||||
groupId: 'group-1',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
contentType: 'editor',
|
||||
label: 'README.md',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 1,
|
||||
createdAt: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function headlessRuntimeWithChat() {
|
||||
const harness = makeRuntimeStoreWithWorkspaceSession(sessionWithTerminalAndEditor())
|
||||
const runtime = new OrcaRuntimeService(harness.runtimeStore)
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: vi.fn(async () => [{ id: SERVE_PTY_ID, cwd: TEST_WORKTREE_PATH, title: 'sh' }])
|
||||
})
|
||||
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
|
||||
return runtime
|
||||
}
|
||||
|
||||
async function openChat(runtime: OrcaRuntimeService): Promise<void> {
|
||||
await runtime.listMobileSessionTabs(WORKTREE)
|
||||
runtime.projectStructuredAgentSessionTab({
|
||||
workspaceId: TEST_WORKTREE_ID,
|
||||
sessionId: 'claude_29968',
|
||||
agent: 'claude',
|
||||
activate: true
|
||||
})
|
||||
}
|
||||
|
||||
describe('a chat tab on a host with no window', () => {
|
||||
it('survives the rebuild that closing a saved editor tab forces', async () => {
|
||||
const runtime = headlessRuntimeWithChat()
|
||||
await openChat(runtime)
|
||||
expect((await runtime.listMobileSessionTabs(WORKTREE)).tabs.map((tab) => tab.id)).toContain(
|
||||
CHAT_TAB_ID
|
||||
)
|
||||
|
||||
await runtime.closeMobileSessionTab(WORKTREE, 'tab-readme', { reason: 'user' })
|
||||
|
||||
const listed = await runtime.listMobileSessionTabs(WORKTREE)
|
||||
expect(listed.tabs.map((tab) => tab.id)).toContain(CHAT_TAB_ID)
|
||||
expect(listed.tabs.map((tab) => tab.id)).not.toContain('tab-readme')
|
||||
expect(listed.tabGroups?.flatMap((group) => group.tabOrder)).toContain(CHAT_TAB_ID)
|
||||
// The chat stays the tab the host shows, not the terminal the rebuild re-marked active.
|
||||
expect(listed.activeTabId).toBe(CHAT_TAB_ID)
|
||||
expect(listed.tabs.filter((tab) => tab.isActive).map((tab) => tab.id)).toEqual([CHAT_TAB_ID])
|
||||
})
|
||||
|
||||
it('keeps its place in the tab order when the host lists the workspace again', async () => {
|
||||
const runtime = headlessRuntimeWithChat()
|
||||
await openChat(runtime)
|
||||
|
||||
const listed = await runtime.listMobileSessionTabs(WORKTREE)
|
||||
|
||||
expect(listed.tabGroups?.flatMap((group) => group.tabOrder)).toEqual([
|
||||
'serve-tab',
|
||||
'tab-readme',
|
||||
CHAT_TAB_ID
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -86,6 +86,7 @@ await import('./orca-runtime-tests/mobile-session-tabs-part-13.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-part-14.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-cold-serve-hydrate.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-empty-headless-snapshot.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-headless-chat-rebuild.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-shell-selection.spec')
|
||||
await import('./orca-runtime-tests/mobile-session-tabs-windowless-sleep-resume.spec')
|
||||
await import('./orca-runtime-tests/exit-retirement-activation.spec')
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
pickRestoredActiveGroupId
|
||||
} from '../mobile-session-layout-projection'
|
||||
import {
|
||||
appendChatTabOrder,
|
||||
keepSnapshotChatTabs,
|
||||
mergeMobileSessionSnapshotTabs,
|
||||
mergeMobileSessionTabGroups
|
||||
} from '../mobile-session-tab-merge'
|
||||
@@ -243,7 +245,7 @@ export abstract class OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspa
|
||||
const mergedTabs =
|
||||
options.onlyRuntimeOwnedTerminals === true && existing
|
||||
? mergeMobileSessionSnapshotTabs(existing.tabs, tabs)
|
||||
: tabs
|
||||
: keepSnapshotChatTabs(tabs, existing)
|
||||
const mergedActiveTab =
|
||||
existing?.tabs.find((tab) => tab.id === existing.activeTabId) ??
|
||||
activeTab ??
|
||||
@@ -269,7 +271,7 @@ export abstract class OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspa
|
||||
? mergedActiveTab.parentTabId
|
||||
: mergedActiveTab.id
|
||||
: null
|
||||
const nextTabGroups: RuntimeMobileSessionTabGroup[] = hasPersistedSplit
|
||||
const rebuiltTabGroups: RuntimeMobileSessionTabGroup[] = hasPersistedSplit
|
||||
? appendBrowserTabOrder(
|
||||
distributeHeadlessTabsAcrossGroups(
|
||||
persistedGroups.map((group) => ({
|
||||
@@ -306,6 +308,7 @@ export abstract class OrcaRuntimeWithHydrateHeadlessMobileSessionTabsFromWorkspa
|
||||
tabOrder
|
||||
}
|
||||
]
|
||||
const nextTabGroups = appendChatTabOrder(rebuiltTabGroups, existing?.tabGroups, mergedTabs)
|
||||
// Why: merging runtime tabs INTO a renderer publication must not reclass
|
||||
// the snapshot as headless-built — the preservation predicate would then
|
||||
// treat the renderer's own tabs as runtime-owned and resurrect tabs the
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { OrcaRuntimeWithStoredMobileSnapshotHasStalePreservedTab as StoredMobileSnapshotHasStalePreservedTabMethods } from './mobile-session-publication-preservation'
|
||||
import { OrcaRuntimeWithSyncMobileSessionTabs } from './mobile-session-reconciliation'
|
||||
import type {
|
||||
RuntimeMobileSessionSnapshotTab,
|
||||
RuntimeMobileSessionTabGroup,
|
||||
RuntimeMobileSessionTabsSnapshot,
|
||||
RuntimeMobileSessionTerminalTab
|
||||
} from '../../../shared/runtime-types'
|
||||
import {
|
||||
appendChatTabOrder,
|
||||
getMobileSessionSnapshotTabIdentityKeys,
|
||||
mergeMobileSessionSnapshotTabs,
|
||||
mergeMobileSessionTabGroups
|
||||
@@ -74,7 +73,7 @@ export abstract class OrcaRuntimeWithMergePreservedHeadlessMobileSessionTabs ext
|
||||
activeGroupId: snapshot.activeGroupId ?? existing.activeGroupId,
|
||||
activeTabId: activeTab?.id ?? null,
|
||||
activeTabType: activeTab?.type ?? null,
|
||||
tabGroups: this.mergeStructuredAgentSessionTabGroups(
|
||||
tabGroups: appendChatTabOrder(
|
||||
tabGroups,
|
||||
existing.tabGroups ?? [],
|
||||
normalizedPreservedTabs,
|
||||
@@ -84,30 +83,6 @@ export abstract class OrcaRuntimeWithMergePreservedHeadlessMobileSessionTabs ext
|
||||
}
|
||||
}
|
||||
|
||||
protected mergeStructuredAgentSessionTabGroups(
|
||||
groups: readonly RuntimeMobileSessionTabGroup[],
|
||||
existingGroups: readonly RuntimeMobileSessionTabGroup[],
|
||||
preservedTabs: readonly RuntimeMobileSessionSnapshotTab[],
|
||||
activeTabId: string | null
|
||||
): RuntimeMobileSessionTabGroup[] {
|
||||
const structuredTabs = preservedTabs.filter((tab) => tab.type === 'agent-session')
|
||||
if (structuredTabs.length === 0) {
|
||||
return [...groups]
|
||||
}
|
||||
const next = groups.map((group) => ({ ...group, tabOrder: [...group.tabOrder] }))
|
||||
for (const tab of structuredTabs) {
|
||||
const priorGroupId = existingGroups.find((group) => group.tabOrder.includes(tab.id))?.id
|
||||
const target = next.find((group) => group.id === priorGroupId) ?? next[0]
|
||||
if (target && !target.tabOrder.includes(tab.id)) {
|
||||
target.tabOrder.push(tab.id)
|
||||
}
|
||||
if (target && tab.id === activeTabId) {
|
||||
target.activeTabId = tab.id
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
// Why: omitted from the renderer's publication AND from persistence = durably closed; a create in flight is not.
|
||||
protected listRendererRetiredTerminalTabs(
|
||||
incoming: RuntimeMobileSessionTabsSnapshot,
|
||||
@@ -204,11 +179,16 @@ export abstract class OrcaRuntimeWithMergePreservedHeadlessMobileSessionTabs ext
|
||||
activeGroupId: existing.activeGroupId ?? getHeadlessMobileSessionGroupId(existing.worktree),
|
||||
activeTabId: activeTab?.id ?? null,
|
||||
activeTabType: activeTab?.type ?? null,
|
||||
tabGroups: mergeMobileSessionTabGroups(
|
||||
existing.worktree,
|
||||
tabGroups: appendChatTabOrder(
|
||||
mergeMobileSessionTabGroups(
|
||||
existing.worktree,
|
||||
existing.tabGroups ?? [],
|
||||
terminalTabs,
|
||||
activeTab?.type === 'terminal' ? activeTab : null
|
||||
),
|
||||
existing.tabGroups ?? [],
|
||||
terminalTabs,
|
||||
activeTab?.type === 'terminal' ? activeTab : null
|
||||
tabs,
|
||||
activeTab?.id ?? null
|
||||
),
|
||||
tabs
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getHeadlessMobileSessionGroupId
|
||||
} from '../mobile-session-layout-projection'
|
||||
import {
|
||||
appendChatTabOrder,
|
||||
mergeMobileSessionSnapshotTabs,
|
||||
mergeMobileSessionTabGroups
|
||||
} from '../mobile-session-tab-merge'
|
||||
@@ -147,11 +148,16 @@ export abstract class OrcaRuntimeWithPublishPtyBackedMobileSessionTerminal exten
|
||||
activeGroupId: existing?.activeGroupId ?? getHeadlessMobileSessionGroupId(worktreeId),
|
||||
activeTabId: activeTab?.id ?? null,
|
||||
activeTabType: activeTab?.type ?? null,
|
||||
tabGroups: mergeMobileSessionTabGroups(
|
||||
worktreeId,
|
||||
tabGroups: appendChatTabOrder(
|
||||
mergeMobileSessionTabGroups(
|
||||
worktreeId,
|
||||
existing?.tabGroups ?? [],
|
||||
terminalTabs,
|
||||
activeTab?.type === 'terminal' ? activeTab : null
|
||||
),
|
||||
existing?.tabGroups ?? [],
|
||||
terminalTabs,
|
||||
activeTab?.type === 'terminal' ? activeTab : null
|
||||
tabs,
|
||||
activeTab?.id ?? null
|
||||
),
|
||||
...(existing?.tabGroupLayout ? { tabGroupLayout: existing.tabGroupLayout } : {}),
|
||||
tabs
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* #29968: a phone paired straight to a headless `orca serve` keeps a Claude chat tab, in its place
|
||||
* in the tab order, while the phone closes and opens terminals around it.
|
||||
*/
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { launchHeadlessPairedRuntimeHost } from './helpers/headless-paired-runtime-host'
|
||||
import { pairMobileClient } from './helpers/paired-mobile-client'
|
||||
import {
|
||||
createStructuredAgentSessionId,
|
||||
structuredAgentSessionCreateParams
|
||||
} from '../../src/shared/structured-agent-session-create'
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../src/shared/runtime-session-contracts'
|
||||
import type { RuntimeWorktreePsSummary } from '../../src/shared/runtime-worktree-contracts'
|
||||
|
||||
test('a headless host keeps the phone its chat tab as terminals come and go', async ({
|
||||
testRepoPath
|
||||
}) => {
|
||||
test.setTimeout(240_000)
|
||||
const host = await launchHeadlessPairedRuntimeHost({ pairingScope: 'mobile' })
|
||||
const phone = pairMobileClient(host.offer)
|
||||
try {
|
||||
await host.client.call('repo.add', { path: testRepoPath, kind: 'git' })
|
||||
let worktreeId = ''
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const { worktrees } = await phone.call<{ worktrees: RuntimeWorktreePsSummary[] }>(
|
||||
'worktree.ps',
|
||||
{ limit: 1_000 }
|
||||
)
|
||||
worktreeId = worktrees.find((row) => row.path === testRepoPath)?.worktreeId ?? ''
|
||||
return worktreeId
|
||||
},
|
||||
{ timeout: 30_000 }
|
||||
)
|
||||
.not.toBe('')
|
||||
const worktree = `id:${worktreeId}`
|
||||
const { tab: oldTerminal } = await phone.call<{ tab: { parentTabId: string } }>(
|
||||
'session.tabs.createTerminal',
|
||||
{ worktree, clientMutationId: 'old', activate: true, select: true, navigation: 'caller' }
|
||||
)
|
||||
const sessionId = createStructuredAgentSessionId('claude', randomUUID)
|
||||
await phone.call(
|
||||
'agentSession.create',
|
||||
structuredAgentSessionCreateParams({
|
||||
sessionId,
|
||||
worktree,
|
||||
agent: 'claude',
|
||||
randomUuid: randomUUID
|
||||
})
|
||||
)
|
||||
const chatTabId = `agent-session:${sessionId}`
|
||||
|
||||
// The reported sequence: close the old terminal, then a setup terminal opens beside the chat.
|
||||
await phone.call('session.tabs.close', {
|
||||
worktree,
|
||||
tabId: oldTerminal.parentTabId,
|
||||
reason: 'user'
|
||||
})
|
||||
await host.client.call('terminal.create', { worktree, command: 'echo setup' })
|
||||
await phone.call('session.tabs.createTerminal', {
|
||||
worktree,
|
||||
clientMutationId: 'new',
|
||||
activate: true,
|
||||
select: true,
|
||||
navigation: 'caller'
|
||||
})
|
||||
|
||||
const listed = await phone.call<RuntimeMobileSessionTabsResult>('session.tabs.list', {
|
||||
worktree
|
||||
})
|
||||
expect(listed.tabs.map((tab) => tab.id)).toContain(chatTabId)
|
||||
expect(listed.tabGroups?.flatMap((group) => group.tabOrder)).toContain(chatTabId)
|
||||
} finally {
|
||||
await host.dispose()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user