fix(tabs): retain headless order and placeholder titles

This commit is contained in:
Neil
2026-08-31 05:43:09 -07:00
parent d706c49752
commit e6fa94836c
4 changed files with 247 additions and 6 deletions
@@ -0,0 +1,121 @@
import { describe, expect, it } from 'vitest'
import { OrcaRuntimeService } from './orca-runtime'
import type {
RuntimeMobileSessionSnapshotTab,
RuntimeMobileSessionTabsSnapshot,
RuntimeMobileSessionTerminalTab
} from '../../shared/runtime-types'
const WT = 'repo-1::/home/orca/worktree'
const GROUP = `headless-terminals:${WT}`
const terminalTab = (n: number, isActive = false): RuntimeMobileSessionTerminalTab => ({
type: 'terminal',
id: `tab-${n}::leaf-${n}`,
title: `terminal ${n}`,
parentTabId: `tab-${n}`,
leafId: `leaf-${n}`,
ptyId: `pty-${n}`,
isActive
})
const snapshotOf = (
tabs: RuntimeMobileSessionSnapshotTab[],
tabOrder: string[],
activeTabId: string | null
): RuntimeMobileSessionTabsSnapshot => ({
worktree: WT,
publicationEpoch: 'headless:seed',
snapshotVersion: 1,
activeGroupId: GROUP,
activeTabId,
activeTabType: 'terminal',
tabGroups: [{ id: GROUP, activeTabId: activeTabId?.split('::')[0] ?? null, tabOrder }],
tabs
})
describe('headless tab order stability', () => {
it('retains order when activating a re-appended surface', () => {
const runtime = new OrcaRuntimeService(null) as unknown as {
mobileSessionTabsByWorktree: Map<string, RuntimeMobileSessionTabsSnapshot>
activateHeadlessMobileSessionTerminalTab: (
worktreeId: string,
snapshot: RuntimeMobileSessionTabsSnapshot,
activeTab: RuntimeMobileSessionTerminalTab
) => void
emitMobileSessionTabsSnapshot: (snapshot: RuntimeMobileSessionTabsSnapshot) => void
persistHeadlessTerminalActiveLeaf: (...args: unknown[]) => void
}
runtime.emitMobileSessionTabsSnapshot = () => {}
runtime.persistHeadlessTerminalActiveLeaf = () => {}
const tabs = [terminalTab(1), terminalTab(3), terminalTab(4), terminalTab(2, true)]
const snapshot = snapshotOf(tabs, ['tab-1', 'tab-2', 'tab-3', 'tab-4'], 'tab-2::leaf-2')
runtime.mobileSessionTabsByWorktree.set(WT, snapshot)
runtime.activateHeadlessMobileSessionTerminalTab(WT, snapshot, tabs[3]!)
expect(runtime.mobileSessionTabsByWorktree.get(WT)?.tabGroups?.[0]?.tabOrder).toEqual([
'tab-1',
'tab-2',
'tab-3',
'tab-4'
])
})
it('retains stored order when a materialized surface is re-appended', () => {
const runtime = new OrcaRuntimeService(null) as unknown as {
mergeMobileSessionTabGroups: (
worktreeId: string,
groups: { id: string; activeTabId: string | null; tabOrder: string[] }[],
terminalTabs: RuntimeMobileSessionTerminalTab[],
activeTab: RuntimeMobileSessionTerminalTab | null
) => { id: string; tabOrder: string[] }[]
}
const reappended = [terminalTab(1), terminalTab(3), terminalTab(4), terminalTab(2, true)]
const merged = runtime.mergeMobileSessionTabGroups(
WT,
[{ id: GROUP, activeTabId: 'tab-1', tabOrder: ['tab-1', 'tab-2', 'tab-3', 'tab-4'] }],
reappended,
reappended[3]!
)
expect(merged[0]!.tabOrder).toEqual(['tab-1', 'tab-2', 'tab-3', 'tab-4'])
})
it('appends only genuinely new tabs after retained order', () => {
const runtime = new OrcaRuntimeService(null) as unknown as {
mergeMobileSessionTabGroups: (
worktreeId: string,
groups: { id: string; activeTabId: string | null; tabOrder: string[] }[],
terminalTabs: RuntimeMobileSessionTerminalTab[],
activeTab: RuntimeMobileSessionTerminalTab | null
) => { id: string; tabOrder: string[] }[]
}
const tabs = [terminalTab(3), terminalTab(1), terminalTab(5, true)]
const merged = runtime.mergeMobileSessionTabGroups(
WT,
[{ id: GROUP, activeTabId: 'tab-1', tabOrder: ['tab-1', 'tab-2', 'tab-3'] }],
tabs,
tabs[2]!
)
expect(merged[0]!.tabOrder).toEqual(['tab-1', 'tab-3', 'tab-5'])
})
it('retains order independently in split groups', () => {
const runtime = new OrcaRuntimeService(null) as unknown as {
buildHeadlessMobileSessionTabGroups: (
worktreeId: string,
tabs: RuntimeMobileSessionSnapshotTab[],
activeTab: RuntimeMobileSessionSnapshotTab | null,
existingGroups?: RuntimeMobileSessionTabsSnapshot['tabGroups']
) => RuntimeMobileSessionTabsSnapshot['tabGroups']
}
const tabs = [terminalTab(2), terminalTab(1), terminalTab(4), terminalTab(3)]
const groups = runtime.buildHeadlessMobileSessionTabGroups(WT, tabs, tabs[0]!, [
{ id: 'left', activeTabId: 'tab-1', tabOrder: ['tab-1', 'tab-2'] },
{ id: 'right', activeTabId: 'tab-3', tabOrder: ['tab-3', 'tab-4'] }
])
expect(groups?.find((group) => group.id === 'left')?.tabOrder).toEqual(['tab-1', 'tab-2'])
expect(groups?.find((group) => group.id === 'right')?.tabOrder).toEqual(['tab-3', 'tab-4'])
})
})
+37 -1
View File
@@ -8566,9 +8566,25 @@ export class OrcaRuntimeService {
const activeGroupId =
(activeParentId ? ownerGroupId.get(activeParentId) : undefined) ?? nextGroups[0]!.id
const retainedOrder = new Map<string, string[]>(nextGroups.map((group) => [group.id, []]))
// Why: tabOrder is the canonical user-visible order, so it must survive a republish.
// A materialized idle surface can move to the end of terminalTabs; retaining the
// stored order prevents activation from rotating the tab bar.
const placed = new Set<string>()
for (const group of nextGroups) {
for (const tabId of group.tabOrder) {
if (liveTabIds.has(tabId) && !placed.has(tabId)) {
retainedOrder.get(group.id)?.push(tabId)
placed.add(tabId)
}
}
}
for (const tabId of parentTabOrder) {
if (placed.has(tabId)) {
continue
}
const groupId = ownerGroupId.get(tabId) ?? activeGroupId
retainedOrder.get(groupId)?.push(tabId)
placed.add(tabId)
}
return nextGroups
.map((group) => {
@@ -9367,7 +9383,27 @@ export class OrcaRuntimeService {
): RuntimeMobileSessionTabGroup[] {
// Why: order across terminals and browsers in their actual array order so a
// tab opened after a browser tab lands to its right, not regrouped before it.
const tabOrder = this.collectHeadlessTopLevelTabOrder(tabs)
const arrivalOrder = this.collectHeadlessTopLevelTabOrder(tabs)
// Why: tabOrder is the user-visible order and must survive a republish. A
// materialized idle surface can move to the end of the incoming array, so
// retain stored positions and append only genuinely new ids.
const liveTopLevelIds = new Set(arrivalOrder)
const tabOrder: string[] = []
const placed = new Set<string>()
for (const group of existingGroups ?? []) {
for (const tabId of group.tabOrder) {
if (liveTopLevelIds.has(tabId) && !placed.has(tabId)) {
tabOrder.push(tabId)
placed.add(tabId)
}
}
}
for (const tabId of arrivalOrder) {
if (!placed.has(tabId)) {
tabOrder.push(tabId)
placed.add(tabId)
}
}
const topLevelOf = (tab: RuntimeMobileSessionSnapshotTab): string =>
tab.type === 'terminal' ? tab.parentTabId : tab.id
const activeTopLevelId =
@@ -540,6 +540,84 @@ describe('applyWebSessionTabsSnapshot', () => {
expect(patch.activeTabId).toBe(mirroredId)
})
it('retains a known title while a pending surface reports its placeholder', () => {
const mirroredId = toWebTerminalSurfaceTabId('host-tab-1')
const priorPtyId = 'remote:web-env-1@@terminal-1'
const existingTab: TerminalTab = {
id: mirroredId,
ptyId: priorPtyId,
worktreeId: WT,
title: 'pnpm dev',
defaultTitle: 'pnpm dev',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: NOW
}
const patch = applyWebSessionTabsSnapshot(
makeState({
tabsByWorktree: { [WT]: [existingTab] },
ptyIdsByTabId: { [mirroredId]: [priorPtyId] }
}),
makeSnapshot([
{
type: 'terminal',
id: HOST_SURFACE_ID,
title: 'Terminal',
parentTabId: 'host-tab-1',
leafId: LEAF_ID,
isActive: true,
status: 'pending-handle',
terminal: null
}
]),
ENV,
NOW + 1
) as Partial<WebSessionTabsSyncState>
expect(patch.tabsByWorktree?.[WT]?.[0]?.title).toBe('pnpm dev')
})
it('adopts a real title after a pending surface becomes ready', () => {
const mirroredId = toWebTerminalSurfaceTabId('host-tab-1')
const priorPtyId = 'remote:web-env-1@@terminal-1'
const existingTab: TerminalTab = {
id: mirroredId,
ptyId: priorPtyId,
worktreeId: WT,
title: 'pnpm dev',
defaultTitle: 'pnpm dev',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: NOW
}
const patch = applyWebSessionTabsSnapshot(
makeState({
tabsByWorktree: { [WT]: [existingTab] },
ptyIdsByTabId: { [mirroredId]: [priorPtyId] }
}),
makeSnapshot([
{
type: 'terminal',
id: HOST_SURFACE_ID,
title: 'gal@host: ~/dev',
parentTabId: 'host-tab-1',
leafId: LEAF_ID,
isActive: true,
status: 'ready',
terminal: 'terminal-1'
}
]),
ENV,
NOW + 1
) as Partial<WebSessionTabsSyncState>
expect(patch.tabsByWorktree?.[WT]?.[0]?.title).toBe('gal@host: ~/dev')
})
it('retains the exact prior pane binding while a mirrored surface is pending', () => {
const mirroredId = toWebTerminalSurfaceTabId('host-tab-1')
const priorPtyId = 'remote:web-env-1@@terminal-1'
@@ -1480,17 +1480,23 @@ function buildMirroredTerminalTabs(
siblingHookAgent: surfaces.find((surface) => surface.agentStatus?.agentType)?.agentStatus
?.agentType
})
const title = normalizeCompatibleAgentTitleForOwner(
activeSurface.title.trim() || surfaces[0]?.title.trim() || 'Terminal',
ownerRecord?.agent,
{ ownerIsLaunch: ownerRecord?.ownerIsLaunch === true }
)
const existing =
existingById.get(localTabId) ??
existingById.get(parentTabId) ??
surfaces
.map((surface) => existingById.get(toWebTerminalSurfaceTabId(surface.id)))
.find((tab): tab is TerminalTab => Boolean(tab))
// Why: a headless host publishes the literal "Terminal" while an idle pane
// has no live PTY. Keep the client's known title until a ready surface reports one.
const hostTitle = activeSurface.title.trim() || surfaces[0]?.title.trim() || ''
const hostTitleIsPlaceholder =
hostTitle === '' || (activeSurface.status === 'pending-handle' && hostTitle === 'Terminal')
const retainedTitle = existing?.title?.trim() || existing?.defaultTitle?.trim() || ''
const title = normalizeCompatibleAgentTitleForOwner(
(hostTitleIsPlaceholder ? retainedTitle || hostTitle : hostTitle) || 'Terminal',
ownerRecord?.agent,
{ ownerIsLaunch: ownerRecord?.ownerIsLaunch === true }
)
const quickCommandLabel =
activeSurface.quickCommandLabel?.trim() ||
surfaces.find((surface) => surface.quickCommandLabel?.trim())?.quickCommandLabel?.trim() ||