a code editor panel remembers its tabs across a reload

Refreshing the page emptied it. You came back to "Open a file from the explorer to
start editing" with no trace of what you had been doing — two tabs open, one of
them the notes you were writing in.

The terminal solved this shape years ago: a `panelId -> sessionId` map kept in
dashboard state, keyed `ws-{prefix}-{dashboardId}`. This is that map with a
different payload, deliberately the same shape so there is one pattern rather than
two to compare. `editor` is registered in PANEL_STATE_PREFIXES — an unmatched key
is a 400, not a silent 200, which is the branch that exists precisely so a new
prefix cannot vanish quietly.

── Paths, not contents ──

The disk is the truth. Restoring a remembered BODY would show a version that may
be hours stale, or resurrect a file something has since deleted. Each path is read
fresh on restore, in the order you left them, and one that no longer reads is
dropped rather than opened as an empty buffer named after a missing file.

The cost, stated rather than hidden: unsaved edits still do not survive a reload.
They did not before either — the whole tab went. Keeping them means persisting
buffers, which has its own hard question (the disk AND the buffer have both
changed — now what?) and is not smuggled in here.

── The two ways this goes wrong, both guarded ──

Reading the saved set more than once would fight the user: every tab they open
writes into that same bag, so a later read arrives as an update telling them to
reopen what they just closed. It is claimed once, on the first render that has it.

Writing before restoring would persist an empty list over the state it is about to
read — first render, no tabs yet. The write is guarded on the restore having run.

Also: a panel closed for good drops its entry, via `usePanelClose`, which fires on
a real close and not on a drag, a swap or a mobile panel switch.

Per PANEL, not per dashboard, so two editors side by side keep separate strips.
Not wired on the /code-editor screen: `?file=` already survives a reload there.

tsgo clean, frontend builds, 846 pass / 7 fail unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 22:49:43 +00:00
co-authored by Claude Opus 5
parent dc3841a877
commit 95b83fb29b
4 changed files with 143 additions and 3 deletions
+4 -1
View File
@@ -17,7 +17,10 @@ export const dashboardsRouter = createRouter();
// `ws-<statePrefix>-<dashboardId>`. This is an allow-list because `ws-<prefix>-<id>` cannot be split
// without one — both halves may contain dashes. Keep it in step with the `statePrefix` props in
// `apps/Terminal/index.tsx`; the 400 at the bottom of the PATCH loop is what tells you when it is not.
const PANEL_STATE_PREFIXES = ['tmux', 'nvim', 'claude-code'];
// `editor` joined 2026-08-15: which files a code-editor panel has open, and which is in front. Note an
// unmatched key is a 400 rather than a silent 200 — so a new panel prefix MUST be added here or its state
// vanishes loudly instead of quietly, which is the whole reason that branch exists.
const PANEL_STATE_PREFIXES = ['tmux', 'nvim', 'claude-code', 'editor'];
// A `LayoutNode` is an object. Everything from the PATCH body down to jsonb is `unknown`, so without this
// the only thing standing between a wrong-shaped body and the layout columns is the CHECK constraint —