docs/superpowers/specs/2026-09-01-gone-panes-design.md
Ref: Size: 6.6 KiB History
# Gone panes: a restored pane outlives its session
2026-09-01. Status: approved design, pre-implementation.
## Problem
The seed (2026-08 restore work) made a reopened wall paint its saved cut
first: pending panes stand in their saved rects, a dark host's panes wear
`unreachable` indefinitely, and a wall whose sessions all came back re-cuts
zero times. One path still tears the layout: a host that answers its poll
*without* a saved pane's session. `planHostDiff` appends that pane to
`vanish`, `applyHostList` collapses it, and `relayout` re-cuts the wall about
one second after the saved cut painted.
Measured 2026-09-01 on a real pty rig (ptyclient + render oracle, ReleaseSafe
binaries): after a daemon restart, the two-pane saved cut paints at byte 46 of
the capture and collapses to one full-width pane on the first poll answer.
That is the "flash / sudden rearrange" the user reported from that morning's
session, where a fresh local daemon (started 09:56) answered with only the
default session. The daemon-restart-after-reboot case makes every saved local
pane meet this path at once.
The user's stated expectation: opening mux shows every window that was open
last time, placeholders included, with no flash and no rearrange.
## Decisions (settled in brainstorm)
- **Gone placeholders, not auto-start.** Chosen over auto-creating fresh
shells ("smaller change, autostart piggybacks later" — the user's words).
Auto-start layers on top of this design as "Enter on all of them" and is
out of scope here.
- **Only a seeded pane can meet `gone`.** A tile that has ever been live
keeps today's lifecycle: a shell's `exit` still removes its tile, the
one-list grace for live pumps stays. `gone` is strictly the fate of a
sidecar-restored pane whose reachable host does not name its session.
- **`gone` is a dressing, not a tombstone.** The tile stays `pending`, so
the existing bind path wakes it if the session appears on a later list
(created from another wall, for example). Symmetric with `unreachable`,
which it sits beside: `unreachable` is "the host cannot answer", `gone` is
"the host answered no".
- **Enter births in place.** On a focused gone pane, Enter re-arms that same
tile as a CREATING attach — same host, same session name, same rect,
`pending = false`, spawn the pump. No tree edit, no flatten, zero re-cuts.
Every other byte on a gone pane is eaten (it has no pump, so no key could
reach a session anyway). `x` closes it locally via the existing never-up
path.
- **Persistence is the status quo.** `saveLayoutTo` already serializes every
present tile, pending included. An unrevived gone pane survives
detach→reattach for free: it re-seeds as waiting and the next poll dresses
it gone again. It lives until revived or dismissed — the alternative
(dropping it from the sidecar) would mean one absent-minded detach forgets
the shape, which defeats the feature.
- **No re-creation without a keypress.** Enter is a user ask, exactly like
picker Enter on a host row. The "nothing re-creates a session" rule holds.
## The rule change
`planHostDiff` today gives a pane owned by a reachable host three exits:
bind (the list names a pending pane), keep (the list names a live tile),
vanish (the list names neither — with one list's grace for a live pump).
The change is one arrow: a **pending** pane the reachable list does not name
is dressed `.gone` instead of appended to `vanish`. Live tiles keep the
grace-then-vanish path untouched.
`dressSilent` (poll failure) and the bind path are unchanged. A pane may
move between `unreachable` and `gone` across polls as the host flaps; both
are pending dressings and both yield to a bind.
## State and keys
- `Tile` state enum gains `.gone` beside `.unreachable`; the bar word is
`gone`. The state is written under `paint_mu` by the keyboard thread
(poll application already runs there), and repainted the way
`dressSilent` repaints: `paintDeadBarsLocked`, since a pending pane has
no pump to doorbell.
- Focusing a gone pane shows a one-line notice naming the two keys
(Enter starts the session anew, x closes the pane).
- Enter's re-arm reuses the bind shape (`bindTile` is the model): clear
`pending`, set `creates = true`, `state = .connecting`, reset
`pump_done`/`alive`, spawn the pump. The attach is sized (the rect is
real) and creating; if the create races a birth elsewhere and the name
now exists, the attach joins — both outcomes are the pane coming live.
- `x` on a gone pane: the existing local-close path for tiles that have
never come up already covers it (a pending pane has no pump to read an
ask). Verify, don't re-implement.
## Invariant edits (CLAUDE.md, one paragraph)
"The wall shows sessions and nothing else" already carries one exception —
a saved pane on a dark host stays, wearing `unreachable`. `gone` is the same
exception with the opposite cause, and the hosts-file invariant paragraph
gains one sentence saying so. A wall of only gone panes is not empty: it
does not auto-open the picker, and it does not end mux on a terminal.
## Tests
Unit (`wall_test_host.zig`, plural fixtures at nonzero offsets per the
working rules):
- `planHostDiff`: two pending panes plus one live tile on the same reachable
host; a list naming none of them dresses the pending pair `gone` and gives
the live tile its grace, then vanishes it on the second list. The pending
panes stay through both lists.
- gone → bind: a later list that names a gone pane's session binds it.
- flap: poll failure dresses a gone pane `unreachable`; recovery without the
session dresses it `gone` again.
- Enter re-arm: the tile leaves `pending`, creates, and occupies the same
rect (no relayout call observed).
- `x` on a gone pane closes it locally and frees the slot.
E2E (new leg beside the seed legs in `e2e_12_panes.sh`): build a 3-pane
wall (sessions 0, b, c — plural, so a fate can differ per pane), detach,
stop the daemon, start a fresh one on the same socket, reattach. The fresh
daemon has only the entry's session, so panes b and c go gone. Assert
through the render oracle: the saved cut paints first and never moves
(the rail columns across the whole capture are exactly the saved ones —
the assertion that fails today), both dead panes wear `gone` on the grid,
Enter in pane b starts a fresh shell whose marker lands in the same rect
with the rails still unmoved, and `x` on pane c collapses it only on that
keypress.
## Out of scope
- Auto-start of gone panes (piggybacks later as a layer over Enter).
- The user's remote box (`alex@192.168.0.207`) carrying a pre-rename
`muxd` and an ssh PATH without `~/.local/bin` — a deployment chore; its
pane correctly reads `unreachable` today.