fix: prune empty restored panes (#268)
Co-authored-by: wangqiong <wangqiong@yy.com>
This commit is contained in:
co-authored by
wangqiong
parent
71c5413e5c
commit
38a3d94410
+24
-5
@@ -825,8 +825,9 @@ export function sanitizeState(parsed: unknown): SidebarState | undefined {
|
||||
// bottom tree.
|
||||
const seen = new Set<string>()
|
||||
const reid = new Map<string, string>()
|
||||
const splits = sanitizeNode(record.splits, seen, reid)
|
||||
if (splits === undefined) return undefined
|
||||
const restoredSplits = sanitizeNode(record.splits, seen, reid)
|
||||
if (restoredSplits === undefined) return undefined
|
||||
const splits = pruneEmptyPanes(restoredSplits)
|
||||
// Bottom-panel fields arrived in a later build: a missing or malformed
|
||||
// value on an OLDER persisted state defaults (closed / default height /
|
||||
// empty pane) so existing layouts keep loading, like nextBrowser.
|
||||
@@ -840,15 +841,23 @@ export function sanitizeState(parsed: unknown): SidebarState | undefined {
|
||||
? record.bottomHeight
|
||||
: BOTTOM_DEFAULT
|
||||
const bottomHeight = Math.min(bottomCap, Math.max(BOTTOM_MIN, Math.round(rawHeight)))
|
||||
const bottomSplits = sanitizeNode(record.bottomSplits, seen, reid)
|
||||
?? { kind: 'leaf' as const, id: uid('pane'), tabs: [], active: null }
|
||||
const bottomSplits = pruneEmptyPanes(sanitizeNode(record.bottomSplits, seen, reid)
|
||||
?? { kind: 'leaf' as const, id: uid('pane'), tabs: [], active: null })
|
||||
const requestedActivePane = typeof record.activePane === 'string'
|
||||
? (reid.get(record.activePane) ?? record.activePane)
|
||||
: null
|
||||
const activePane = requestedActivePane === null
|
||||
? null
|
||||
: treeHasId(splits, requestedActivePane) || treeHasId(bottomSplits, requestedActivePane)
|
||||
? requestedActivePane
|
||||
: firstLeaf(splits).id
|
||||
const maxWidth = typeof window !== 'undefined' ? window.innerWidth : Infinity
|
||||
return {
|
||||
panelOpen: record.panelOpen,
|
||||
width: Math.max(PANEL_MIN, Math.min(record.width, maxWidth)),
|
||||
// A stale duplicate pane id may have been re-ided; follow the rename so
|
||||
// new tabs still land in the pane the user was using.
|
||||
activePane: typeof record.activePane === 'string' ? (reid.get(record.activePane) ?? record.activePane) : null,
|
||||
activePane,
|
||||
nextTerminal: record.nextTerminal,
|
||||
nextBrowser,
|
||||
expanded: record.expanded as string[],
|
||||
@@ -863,6 +872,16 @@ export function sanitizeState(parsed: unknown): SidebarState | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
/** Collapse persisted split panes left empty after ephemeral diff tabs are dropped. */
|
||||
function pruneEmptyPanes(node: SplitNode): SplitNode {
|
||||
const leaves = allLeaves(node)
|
||||
if (!leaves.some(leaf => leaf.tabs.length > 0)) return node
|
||||
return leaves.reduce(
|
||||
(tree, leaf) => leaf.tabs.length === 0 ? removeLeafAt(tree, leaf.id) : tree,
|
||||
node,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* One tree node id, deduplicated against the ids already seen in this
|
||||
* state. Duplicates are exactly the pre-seeding counter-reset corruption
|
||||
|
||||
+28
-5
@@ -188,6 +188,29 @@ describe('sidebar state', () => {
|
||||
expect((onlyDiff?.splits as { tabs: SidebarTab[] }).tabs).toEqual([])
|
||||
})
|
||||
|
||||
it('sanitize removes a pane emptied by ephemeral diff tabs', () => {
|
||||
const valid = sanitizeState({
|
||||
panelOpen: true,
|
||||
width: 400,
|
||||
nextTerminal: 1,
|
||||
activePane: 'pane:diff',
|
||||
expanded: [],
|
||||
splits: {
|
||||
kind: 'split',
|
||||
id: 'split:1',
|
||||
dir: 'col',
|
||||
sizes: [0.5, 0.5],
|
||||
children: [
|
||||
{ kind: 'leaf', id: 'pane:git', active: 'git', tabs: [{ id: 'git', type: 'git', title: 'Git' }] },
|
||||
{ kind: 'leaf', id: 'pane:diff', active: 'd1', tabs: [{ id: 'd1', type: 'diff', title: 'a.ts' }] },
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(valid?.splits.kind).toBe('leaf')
|
||||
expect((valid?.splits as { id: string; tabs: SidebarTab[] }).id).toBe('pane:git')
|
||||
expect(valid?.activePane).toBe('pane:git')
|
||||
})
|
||||
|
||||
it('dedupes the single-instance subagent tab (focuses instead of duplicating)', () => {
|
||||
let s = state()
|
||||
s = openTabInActivePane(s, { id: 'subagent', type: 'subagent', title: 'Subagents' })
|
||||
@@ -716,11 +739,11 @@ describe('persisted state sanitization', () => {
|
||||
const clean = sanitizeState(corrupted)
|
||||
expect(clean).toBeDefined()
|
||||
const leaves = allLeaves(clean!.splits)
|
||||
// The first occurrence keeps its id; the repeat gets a fresh unique one
|
||||
// (exact suffix depends on the module-level uid counter, so assert shape).
|
||||
expect(leaves[0]!.id).toBe('pane:1')
|
||||
expect(new Set(leaves.map(leaf => leaf.id)).size).toBe(2)
|
||||
expect(clean!.activePane).toBe(leaves[1]!.id)
|
||||
// The empty first occurrence is pruned; the populated repeat keeps its
|
||||
// fresh unique id and becomes active.
|
||||
expect(leaves).toHaveLength(1)
|
||||
expect(leaves[0]!.id).not.toBe('pane:1')
|
||||
expect(clean!.activePane).toBe(leaves[0]!.id)
|
||||
// And an open must land in exactly one pane of the healed tree.
|
||||
const opened = openTabInActivePane(clean!, { id: 'editor:/a.ts', type: 'editor', title: 'a.ts', path: '/a.ts' })
|
||||
const owners = allLeaves(opened.splits).filter(leaf => leaf.tabs.some(tab => tab.path === '/a.ts'))
|
||||
|
||||
Reference in New Issue
Block a user