mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 16:03:03 +00:00
fix(pane-manager): bump nextPaneId past highest persisted pane:N on replay
Why: PaneManager.nextPaneId is a per-instance monotonic counter that resets to 1 when the app restarts. replayTerminalLayout rewrites the persisted pane:N leafIds verbatim, so the next user-triggered split would mint a leafId colliding with an existing one. With the new deterministic sessionId path that collision matters materially: both panes would derive the same (worktreeId, tabId, leafId) triple and share one history directory, interleaving their scrollback. Adds PaneManager.advanceNextPaneIdTo(min) (forward-only) and a strict /^pane:(\d+)$/ parser that walks the replayed snapshot. Non-matching leafIds (legacy or corrupted) are skipped rather than thrown on — the counter advance is best-effort safety. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -283,6 +283,43 @@ export function restoreScrollbackBuffers(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Why: the snapshot stores leafIds as `pane:<N>` strings rewritten verbatim
|
||||
* on restore. After replay we must advance PaneManager.nextPaneId past
|
||||
* `max(N)` so the next user-triggered split does not mint a pane whose
|
||||
* `paneLeafId` collides with a persisted leaf — a collision would collapse
|
||||
* the determinism of sessionIds derived from `(worktreeId, tabId, leafId)`.
|
||||
*
|
||||
* Parser: `/^pane:(\d+)$/`. Non-matching entries (e.g. a legacy custom
|
||||
* leafId shape, or a corrupted snapshot) are skipped rather than thrown
|
||||
* on — the counter advance is a best-effort safety; if the highest
|
||||
* numeric leaf is missed we fall back to the per-instance monotonic
|
||||
* default, which at worst surfaces the original collision bug, not a
|
||||
* new crash.
|
||||
*/
|
||||
function highestPaneNInSnapshot(node: TerminalPaneLayoutNode | null | undefined): number {
|
||||
if (!node) {
|
||||
return 0
|
||||
}
|
||||
let max = 0
|
||||
const visit = (n: TerminalPaneLayoutNode): void => {
|
||||
if (n.type === 'leaf') {
|
||||
const match = /^pane:(\d+)$/.exec(n.leafId)
|
||||
if (match) {
|
||||
const parsed = Number(match[1])
|
||||
if (Number.isFinite(parsed) && parsed > max) {
|
||||
max = parsed
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
visit(n.first)
|
||||
visit(n.second)
|
||||
}
|
||||
visit(node)
|
||||
return max
|
||||
}
|
||||
|
||||
export function replayTerminalLayout(
|
||||
manager: PaneManager,
|
||||
snapshot: TerminalLayoutSnapshot | null | undefined,
|
||||
@@ -315,5 +352,11 @@ export function replayTerminalLayout(
|
||||
}
|
||||
|
||||
restoreNode(snapshot.root, initialPane.id)
|
||||
|
||||
// Why: bump PaneManager's counter past the highest persisted `pane:N`
|
||||
// before any user-triggered split runs. See highestPaneNInSnapshot.
|
||||
const maxPersisted = highestPaneNInSnapshot(snapshot.root)
|
||||
manager.advanceNextPaneIdTo(maxPersisted + 1)
|
||||
|
||||
return paneByLeafId
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/* eslint-disable max-lines -- Why: PaneManager owns the DOM, WebGL,
|
||||
drag-reorder, and counter-advance state for the terminal-pane tree. Splitting
|
||||
further would scatter invariants (active pane focus, nextPaneId monotonicity,
|
||||
divider styles) across files with no cleaner ownership seam. */
|
||||
import type {
|
||||
PaneManagerOptions,
|
||||
PaneStyleOptions,
|
||||
@@ -62,6 +66,15 @@ export class PaneManager {
|
||||
this.renderingSuspended = options.initialRenderingSuspended === true
|
||||
}
|
||||
|
||||
// Why: nextPaneId resets to 1 on restart; persisted pane:N leafIds are
|
||||
// replayed verbatim. Bumping past max(N) prevents the next split from
|
||||
// colliding — otherwise two panes derive the same deterministic history dir.
|
||||
advanceNextPaneIdTo(minNextId: number): void {
|
||||
if (minNextId > this.nextPaneId) {
|
||||
this.nextPaneId = minNextId
|
||||
}
|
||||
}
|
||||
|
||||
createInitialPane(opts?: { focus?: boolean }): ManagedPane {
|
||||
const pane = this.createPaneInternal()
|
||||
Object.assign(pane.container.style, {
|
||||
|
||||
Reference in New Issue
Block a user