Leave adopted live terminals' tabs to the layout updates in the layout-owner build (dark, build switch off by default) (#29710)

* Leave adopted live terminals' tabs to main's frames in a layout-owner build (dark, build switch off by default)

* Adopt only into a pane main's frames hold for that terminal; test a tab-bar id that differs from its terminal id

* Count only a pane main binds to the adopted terminal; check the pane key's tab

* Read frame-held panes through a small module so window-owned builds keep the frame sync out
This commit is contained in:
Jinwoo Hong
2026-10-16 09:14:17 -07:00
committed by GitHub
parent 964e6ed52c
commit 99c72ffa65
4 changed files with 236 additions and 2 deletions
@@ -0,0 +1,189 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import { loadWorkspaceLayoutWithoutReport } from '../../../shared/workspace-layout/workspace-layout-load'
import { nameBasedLoadContext } from '../../../shared/workspace-layout/workspace-layout-minted-ids'
import { publishWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-published'
import {
addWorkspace,
emptySession,
GIT_KEY,
leaf
} from '../../../shared/workspace-layout/workspace-layout-session.test-fixture'
import type { WorkspaceLayoutChangeFrame } from '../../../shared/workspace-layout/workspace-layout-stream-frames'
import { bootWindowFromPartitions } from '../app-shell/startup-window-view.test-fixture'
import { provideLayoutFrameTerminalPanes } from '../runtime/layout-frame-terminal-panes'
import { openWorkspaceLayoutFrameSync } from '../runtime/workspace-layout-frame-sync'
import type { WorkspaceLayoutFrameSync } from '../runtime/workspace-layout-frame-sync'
import { reportWindowSessionWriteActivity } from './window-session-write-activity'
import { createStoreSessionMockApi } from '../store/slices/store-session-test-harness'
import { createTestStore } from '../store/slices/store-test-helpers'
import { adoptLiveWorkspacePtySurfaces } from './worktree-agent-live-surface-adoption'
import type { LiveTerminalSurfaceOwnerIndex } from './worktree-live-terminal-surface-owners'
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
type FrameDouble = { onFrame: ((frame: WorkspaceLayoutChangeFrame) => void) | null }
const frames = vi.hoisted((): FrameDouble => ({ onFrame: null }))
vi.mock('../runtime/runtime-layout-client', () => ({
subscribeRuntimeLayout: (
_target: unknown,
_params: unknown,
onFrame: (frame: WorkspaceLayoutChangeFrame) => void
) => {
frames.onFrame = onFrame
return { close: () => {} }
}
}))
createStoreSessionMockApi()
const OTHER_PTY = `${GIT_KEY}@@aaaa0001`
const LEFT_PTY = `${GIT_KEY}@@aaaa0002`
const RIGHT_PTY = `${GIT_KEY}@@aaaa0003`
type TerminalSpec = {
id: string
entityId?: string
leaves: [string, string?][]
split?: 'vertical'
}
/** The window's copy: one shell tab. Main's: that tab and a split agent tab the window lacks,
* whose tab-bar id differs from its terminal id, as for a tab adopted from another window. */
function sessions(splitPtyIds: [string?, string?] = [LEFT_PTY, RIGHT_PTY]): {
window: WorkspaceSessionState
main: WorkspaceSessionState
} {
const other: TerminalSpec = { id: 'tab-other', leaves: [[leaf(1), OTHER_PTY]] }
const split: TerminalSpec = {
id: 'entry-split',
entityId: 'tab-split',
leaves: [
[leaf(2), splitPtyIds[0]],
[leaf(3), splitPtyIds[1]]
],
split: 'vertical'
}
return {
window: addWorkspace(emptySession(), GIT_KEY, [{ id: 'group-a', tabs: [other] }]),
main: addWorkspace(emptySession(), GIT_KEY, [{ id: 'group-a', tabs: [other, split] }])
}
}
function snapshotOf(session: WorkspaceSessionState): WorkspaceLayoutChangeFrame {
const { layout } = loadWorkspaceLayoutWithoutReport('local', session, nameBasedLoadContext())
return {
type: 'snapshot',
workspaces: Object.entries(layout.workspaces).map(([key, workspace]) => ({
key,
layout: publishWorkspaceLayout(workspace, 'local')
}))
}
}
const owners: LiveTerminalSurfaceOwnerIndex = new Map([
[LEFT_PTY, { tabId: 'tab-split', paneKey: `tab-split:${leaf(2)}`, ptyId: LEFT_PTY }],
[RIGHT_PTY, { tabId: 'tab-split', paneKey: `tab-split:${leaf(3)}`, ptyId: RIGHT_PTY }]
])
const flushMicrotasks = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
describe('adopting live terminals in a layout-owner build', () => {
let sync: WorkspaceLayoutFrameSync | null = null
beforeEach(() => {
reportWindowSessionWriteActivity(false)
sync?.close()
sync = null
provideLayoutFrameTerminalPanes(() => false)
})
it('makes no tab or pane of its own while frames are held; the frame then draws the bound tab', async () => {
const { window: windowSession, main } = sessions()
const store = createTestStore()
await bootWindowFromPartitions(store, [{ hostId: 'local', session: windowSession }])
sync = openWorkspaceLayoutFrameSync(store)
provideLayoutFrameTerminalPanes(sync.holdsTerminalPane)
sync.start()
// The window owes main a save, so main's tree waits.
reportWindowSessionWriteActivity(true)
frames.onFrame!(snapshotOf(main))
await flushMicrotasks()
const createTab = vi.spyOn(store.getState(), 'createTab')
const setTabLayout = vi.spyOn(store.getState(), 'setTabLayout')
const adoption = await adoptLiveWorkspacePtySurfaces(
store.getState,
GIT_KEY,
[LEFT_PTY, RIGHT_PTY],
async () => owners
)
expect(adoption).toEqual({ surfaced: true, declinedPtyIds: [] })
expect(createTab).not.toHaveBeenCalled()
expect(setTabLayout).not.toHaveBeenCalled()
expect(store.getState().tabsByWorktree[GIT_KEY]!.map((tab) => tab.id)).toEqual(['tab-other'])
reportWindowSessionWriteActivity(false)
await flushMicrotasks()
const layout = store.getState().terminalLayoutsByTabId['tab-split']
expect(layout?.root).toMatchObject({
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: leaf(2) },
second: { type: 'leaf', leafId: leaf(3) }
})
expect(layout?.ptyIdsByLeafId).toEqual({ [leaf(2)]: LEFT_PTY, [leaf(3)]: RIGHT_PTY })
// A later sweep finds both terminals owned.
await expect(
adoptLiveWorkspacePtySurfaces(store.getState, GIT_KEY, [LEFT_PTY, RIGHT_PTY], async () => {
throw new Error('not asked: nothing is unbound')
})
).resolves.toEqual({ surfaced: true, declinedPtyIds: [] })
})
it.each([
['binds to another terminal', [`${GIT_KEY}@@aaaa0008`, `${GIT_KEY}@@aaaa0009`]],
['leaves unbound', [undefined, undefined]]
] satisfies [string, [string?, string?]][])(
'declines a terminal whose pane main %s',
async (_, ptyIds) => {
const { window: windowSession, main } = sessions(ptyIds)
const store = createTestStore()
await bootWindowFromPartitions(store, [{ hostId: 'local', session: windowSession }])
sync = openWorkspaceLayoutFrameSync(store)
provideLayoutFrameTerminalPanes(sync.holdsTerminalPane)
sync.start()
reportWindowSessionWriteActivity(true)
frames.onFrame!(snapshotOf(main))
await flushMicrotasks()
await expect(
adoptLiveWorkspacePtySurfaces(store.getState, GIT_KEY, [LEFT_PTY], async () => owners)
).resolves.toEqual({ surfaced: false, declinedPtyIds: [LEFT_PTY] })
}
)
it('declines a terminal whose tab main does not hold, rather than making one', async () => {
const { window: windowSession } = sessions()
const store = createTestStore()
await bootWindowFromPartitions(store, [{ hostId: 'local', session: windowSession }])
sync = openWorkspaceLayoutFrameSync(store)
provideLayoutFrameTerminalPanes(sync.holdsTerminalPane)
sync.start()
frames.onFrame!(snapshotOf(windowSession))
await flushMicrotasks()
const createTab = vi.spyOn(store.getState(), 'createTab')
const adoption = await adoptLiveWorkspacePtySurfaces(
store.getState,
GIT_KEY,
[LEFT_PTY],
async () => owners
)
expect(adoption).toEqual({ surfaced: false, declinedPtyIds: [LEFT_PTY] })
expect(createTab).not.toHaveBeenCalled()
})
})
@@ -1,6 +1,8 @@
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
import { parsePaneKey } from '../../../shared/stable-pane-id'
import { worktreeIdsEqual } from '../../../shared/worktree/id'
import type { useAppStore } from '@/store'
import { layoutFramesHoldTerminalPane } from '@/runtime/layout-frame-terminal-panes'
import { resolveTerminalTabPtyOwnership } from './terminal-tab-for-pty-id'
import type {
LiveTerminalSurfaceOwner,
@@ -105,6 +107,15 @@ function adoptHostOwnedSurface(
): boolean {
const store = getState()
const known = tabExists(store, owner.tabId)
// Switch point: main's frames draw a pane main holds for this terminal; the window makes no tab
// or pane of its own. Declined when main lacks it (main-first follow-up) or before the snapshot.
if (ORCA_LAYOUT_OWNER_IS_MAIN && !known) {
const pane = parsePaneKey(owner.paneKey)
return (
pane?.tabId === owner.tabId &&
layoutFramesHoldTerminalPane(worktreeId, { ...owner, leafId: pane.leafId })
)
}
if (bindLivePtyToExactSurface(store, worktreeId, owner)) {
if (!known) {
materializedTabIds.add(owner.tabId)
@@ -0,0 +1,17 @@
// Which terminal panes main's layout frames hold, for readers outside the frame sync. Its own
// module so those readers do not pull the frame sync into window-owned builds.
type TerminalPane = { tabId: string; leafId: string; ptyId: string }
type TerminalPaneQuery = (worktreeId: string, pane: TerminalPane) => boolean
let query: TerminalPaneQuery | null = null
/** Set by the window's layout stream once it opens. */
export function provideLayoutFrameTerminalPanes(next: TerminalPaneQuery): void {
query = next
}
/** Whether main's frames will draw this terminal's pane; false before the window's stream opens. */
export function layoutFramesHoldTerminalPane(worktreeId: string, pane: TerminalPane): boolean {
return query?.(worktreeId, pane) ?? false
}
@@ -35,6 +35,7 @@ import { stableJson } from '../../../shared/workspace-layout/workspace-layout-lo
import { nameBasedLoadContext } from '../../../shared/workspace-layout/workspace-layout-minted-ids'
import type { PublishedWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-published'
import { endLayoutFramePositions, noteLayoutFramePosition } from './layout-frame-position'
import { provideLayoutFrameTerminalPanes } from './layout-frame-terminal-panes'
import { subscribeRuntimeLayout } from './runtime-layout-client'
type FrameSyncStore = {
@@ -46,6 +47,12 @@ export type WorkspaceLayoutFrameSync = {
/** Shows every workspace the frames own once the snapshot is in, then keeps them current. */
start: () => void
close: () => void
/** Whether main's latest frames show this terminal in this pane, drawn yet or not. An unbound
* pane does not count: nothing binds a frame-drawn pane to a live terminal later. */
holdsTerminalPane: (
worktreeId: string,
pane: { tabId: string; leafId: string; ptyId: string }
) => boolean
}
/** A bucket, not a host: the view read-back routes the workspaces a rebuild skips here. */
@@ -235,7 +242,14 @@ export function openWorkspaceLayoutFrameSync(store: FrameSyncStore): WorkspaceLa
live = false
stopIdleListener()
stream.close()
}
},
holdsTerminalPane: (worktreeId, { tabId, leafId, ptyId }) =>
cache[worktreeId]?.tabs.some(
(tab) =>
tab.entityId === tabId &&
(tab.terminal?.panes.some((pane) => pane.leafId === leafId && pane.ptyId === ptyId) ??
false)
) ?? false
}
}
@@ -244,6 +258,9 @@ let startupFrames: WorkspaceLayoutFrameSync | null = null
/** After restore: the window's one layout stream shows the frames' workspaces from now on. */
export function showStartupLayoutFrames(): void {
// Kept for the document's life: a local stream ends only with main, and a re-run boot reuses it.
startupFrames ??= openWorkspaceLayoutFrameSync(useAppStore)
if (!startupFrames) {
startupFrames = openWorkspaceLayoutFrameSync(useAppStore)
provideLayoutFrameTerminalPanes(startupFrames.holdsTerminalPane)
}
startupFrames.start()
}