mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(terminal): recovering one split pane keeps its healthy sibling in the saved layout (#29961)
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
|
||||
import { makePaneKey } from '../../shared/stable-pane-id'
|
||||
import type { RuntimeTerminalOrphanAdoptionRequest } from '../../shared/runtime-types'
|
||||
import { collectLayoutLeafIdsInOrder } from '../../shared/workspace-layout/terminal-pane-tree'
|
||||
import {
|
||||
GIT_KEY,
|
||||
addWorkspace,
|
||||
emptySession,
|
||||
leaf
|
||||
} from '../../shared/workspace-layout/workspace-layout-session.test-fixture'
|
||||
import { buildRuntimeTerminalOrphanSession } from './runtime-terminal-orphan-session-adoption'
|
||||
import type { RuntimePtyWorktreeRecord } from './runtime-terminal-state-records'
|
||||
|
||||
const TAB = 'tab-1'
|
||||
const HEALTHY = leaf(1)
|
||||
const ORPHAN = leaf(2)
|
||||
|
||||
function adopt(existingLeaves: [string, string?][]) {
|
||||
const session = addWorkspace(emptySession(), GIT_KEY, [
|
||||
{ id: 'group-1', tabs: [{ id: TAB, leaves: existingLeaves }] }
|
||||
])
|
||||
const claim = {
|
||||
terminal: 'terminal-orphan',
|
||||
ptyId: 'pty-orphan',
|
||||
incarnationId: 'incarnation-orphan',
|
||||
tabId: TAB,
|
||||
leafId: ORPHAN
|
||||
}
|
||||
const topology: NonNullable<RuntimeTerminalOrphanAdoptionRequest['topology']> = {
|
||||
tabs: [
|
||||
{
|
||||
tabId: TAB,
|
||||
root: { type: 'leaf', leafId: ORPHAN },
|
||||
activeLeafId: ORPHAN,
|
||||
expandedLeafId: null
|
||||
}
|
||||
],
|
||||
groups: [{ id: 'group-1', activeTabId: TAB, tabOrder: [TAB] }]
|
||||
}
|
||||
const request: RuntimeTerminalOrphanAdoptionRequest = {
|
||||
worktree: GIT_KEY,
|
||||
expectedTopologyRevision: 0,
|
||||
claims: [claim],
|
||||
topology
|
||||
}
|
||||
return buildRuntimeTerminalOrphanSession({
|
||||
hostId: LOCAL_EXECUTION_HOST_ID,
|
||||
session,
|
||||
sessionWorktreeId: GIT_KEY,
|
||||
worktreeId: GIT_KEY,
|
||||
request,
|
||||
validated: [
|
||||
{
|
||||
claim,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the tab already exists, so the builder never reads the PTY record.
|
||||
pty: {} as RuntimePtyWorktreeRecord,
|
||||
paneKey: makePaneKey(TAB, ORPHAN)
|
||||
}
|
||||
],
|
||||
topologyTabsById: new Map(topology.tabs.map((tab) => [tab.tabId, tab])),
|
||||
topologyGroups: topology.groups,
|
||||
getDisplayTitle: () => null
|
||||
})
|
||||
}
|
||||
|
||||
it('keeps the healthy sibling pane and its PTY when one split pane is recovered', () => {
|
||||
const layout = adopt([[HEALTHY, 'pty-healthy']]).terminalLayoutsByTabId[TAB]
|
||||
|
||||
expect(collectLayoutLeafIdsInOrder(layout?.root)).toEqual([HEALTHY, ORPHAN])
|
||||
expect(layout?.ptyIdsByLeafId).toEqual({ [HEALTHY]: 'pty-healthy', [ORPHAN]: 'pty-orphan' })
|
||||
expect(layout?.activeLeafId).toBe(ORPHAN)
|
||||
})
|
||||
|
||||
it('keeps the saved split when it already holds the recovered pane', () => {
|
||||
const layout = adopt([[HEALTHY, 'pty-healthy'], [ORPHAN]]).terminalLayoutsByTabId[TAB]
|
||||
|
||||
expect(collectLayoutLeafIdsInOrder(layout?.root)).toEqual([HEALTHY, ORPHAN])
|
||||
expect(layout?.ptyIdsByLeafId).toEqual({ [HEALTHY]: 'pty-healthy', [ORPHAN]: 'pty-orphan' })
|
||||
})
|
||||
@@ -6,6 +6,11 @@ import { nameBasedLoadContext } from '../../shared/workspace-layout/workspace-la
|
||||
import { saveWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-save'
|
||||
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
||||
import { collectPersistedTerminalLeafIds } from './mobile-session-layout-projection'
|
||||
import {
|
||||
collectLayoutLeafIdsInOrder,
|
||||
removeLayoutLeaf
|
||||
} from '../../shared/workspace-layout/terminal-pane-tree'
|
||||
import type { TerminalPaneLayoutNode } from '../../shared/terminal-tab-types'
|
||||
import type { RuntimePtyWorktreeRecord } from './runtime-terminal-state-records'
|
||||
import { mergeTerminalOrphanGroupLayout } from './terminal-orphan-topology'
|
||||
import { canonicalizeTerminalSessionWorktreeId } from './workspace-session-worktree-id'
|
||||
@@ -62,7 +67,7 @@ export function buildRuntimeTerminalOrphanSession(args: {
|
||||
next.terminalLayoutsByTabId[claim.tabId] = topologyTab
|
||||
? {
|
||||
...existingLayout,
|
||||
root: topologyTab.root,
|
||||
root: mergeAdoptedTopologyRoot(existingLayout?.root ?? null, topologyTab.root),
|
||||
activeLeafId: topologyTab.activeLeafId,
|
||||
expandedLeafId: topologyTab.expandedLeafId,
|
||||
ptyIdsByLeafId: {
|
||||
@@ -196,3 +201,29 @@ export function buildRuntimeTerminalOrphanSession(args: {
|
||||
loadWorkspaceLayoutWithoutReport(hostId, persisted, nameBasedLoadContext())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The client's topology names only the panes it adopts; the tab's other panes keep their place
|
||||
* beside them, so recovering one split never drops a healthy sibling.
|
||||
*/
|
||||
function mergeAdoptedTopologyRoot(
|
||||
existingRoot: TerminalPaneLayoutNode | null,
|
||||
adoptedRoot: TerminalPaneLayoutNode
|
||||
): TerminalPaneLayoutNode {
|
||||
if (!existingRoot) {
|
||||
return adoptedRoot
|
||||
}
|
||||
const existingLeafIds = collectLayoutLeafIdsInOrder(existingRoot)
|
||||
const adoptedLeafIds = collectLayoutLeafIdsInOrder(adoptedRoot)
|
||||
if (adoptedLeafIds.every((leafId) => existingLeafIds.includes(leafId))) {
|
||||
return existingRoot
|
||||
}
|
||||
const siblings = adoptedLeafIds.reduce<TerminalPaneLayoutNode | null>(
|
||||
(root, leafId) => removeLayoutLeaf(root, leafId),
|
||||
existingRoot
|
||||
)
|
||||
if (!siblings) {
|
||||
return adoptedRoot
|
||||
}
|
||||
return { type: 'split', direction: 'vertical', first: siblings, second: adoptedRoot }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user