a73x

d2e7a1f8

docs: the gone-panes spec — a restored pane outlives its session

a73x   2026-09-01 09:16

Commit message
docs: the gone-panes spec — a restored pane outlives its session

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

docs/superpowers/specs/2026-09-01-gone-panes-design.md
Old New
@@ -0,0 +1,126 @@
1 # Gone panes: a restored pane outlives its session
2
3 2026-09-01. Status: approved design, pre-implementation.
4
5 ## Problem
6
7 The seed (2026-08 restore work) made a reopened wall paint its saved cut
8 first: pending panes stand in their saved rects, a dark host's panes wear
9 `unreachable` indefinitely, and a wall whose sessions all came back re-cuts
10 zero times. One path still tears the layout: a host that answers its poll
11 *without* a saved pane's session. `planHostDiff` appends that pane to
12 `vanish`, `applyHostList` collapses it, and `relayout` re-cuts the wall about
13 one second after the saved cut painted.
14
15 Measured 2026-09-01 on a real pty rig (ptyclient + render oracle, ReleaseSafe
16 binaries): after a daemon restart, the two-pane saved cut paints at byte 46 of
17 the capture and collapses to one full-width pane on the first poll answer.
18 That is the "flash / sudden rearrange" the user reported from that morning's
19 session, where a fresh local daemon (started 09:56) answered with only the
20 default session. The daemon-restart-after-reboot case makes every saved local
21 pane meet this path at once.
22
23 The user's stated expectation: opening mux shows every window that was open
24 last time, placeholders included, with no flash and no rearrange.
25
26 ## Decisions (settled in brainstorm)
27
28 - **Gone placeholders, not auto-start.** Chosen over auto-creating fresh
29 shells ("smaller change, autostart piggybacks later" — the user's words).
30 Auto-start layers on top of this design as "Enter on all of them" and is
31 out of scope here.
32 - **Only a seeded pane can meet `gone`.** A tile that has ever been live
33 keeps today's lifecycle: a shell's `exit` still removes its tile, the
34 one-list grace for live pumps stays. `gone` is strictly the fate of a
35 sidecar-restored pane whose reachable host does not name its session.
36 - **`gone` is a dressing, not a tombstone.** The tile stays `pending`, so
37 the existing bind path wakes it if the session appears on a later list
38 (created from another wall, for example). Symmetric with `unreachable`,
39 which it sits beside: `unreachable` is "the host cannot answer", `gone` is
40 "the host answered no".
41 - **Enter births in place.** On a focused gone pane, Enter re-arms that same
42 tile as a CREATING attach — same host, same session name, same rect,
43 `pending = false`, spawn the pump. No tree edit, no flatten, zero re-cuts.
44 Every other byte on a gone pane is eaten (it has no pump, so no key could
45 reach a session anyway). `x` closes it locally via the existing never-up
46 path.
47 - **Persistence is the status quo.** `saveLayoutTo` already serializes every
48 present tile, pending included. An unrevived gone pane survives
49 detach→reattach for free: it re-seeds as waiting and the next poll dresses
50 it gone again. It lives until revived or dismissed — the alternative
51 (dropping it from the sidecar) would mean one absent-minded detach forgets
52 the shape, which defeats the feature.
53 - **No re-creation without a keypress.** Enter is a user ask, exactly like
54 picker Enter on a host row. The "nothing re-creates a session" rule holds.
55
56 ## The rule change
57
58 `planHostDiff` today gives a pane owned by a reachable host three exits:
59 bind (the list names a pending pane), keep (the list names a live tile),
60 vanish (the list names neither — with one list's grace for a live pump).
61 The change is one arrow: a **pending** pane the reachable list does not name
62 is dressed `.gone` instead of appended to `vanish`. Live tiles keep the
63 grace-then-vanish path untouched.
64
65 `dressSilent` (poll failure) and the bind path are unchanged. A pane may
66 move between `unreachable` and `gone` across polls as the host flaps; both
67 are pending dressings and both yield to a bind.
68
69 ## State and keys
70
71 - `Tile` state enum gains `.gone` beside `.unreachable`; the bar word is
72 `gone`. The state is written under `paint_mu` by the keyboard thread
73 (poll application already runs there), and repainted the way
74 `dressSilent` repaints: `paintDeadBarsLocked`, since a pending pane has
75 no pump to doorbell.
76 - Focusing a gone pane shows a one-line notice naming the two keys
77 (Enter starts the session anew, x closes the pane).
78 - Enter's re-arm reuses the bind shape (`bindTile` is the model): clear
79 `pending`, set `creates = true`, `state = .connecting`, reset
80 `pump_done`/`alive`, spawn the pump. The attach is sized (the rect is
81 real) and creating; if the create races a birth elsewhere and the name
82 now exists, the attach joins — both outcomes are the pane coming live.
83 - `x` on a gone pane: the existing local-close path for tiles that have
84 never come up already covers it (a pending pane has no pump to read an
85 ask). Verify, don't re-implement.
86
87 ## Invariant edits (CLAUDE.md, one paragraph)
88
89 "The wall shows sessions and nothing else" already carries one exception —
90 a saved pane on a dark host stays, wearing `unreachable`. `gone` is the same
91 exception with the opposite cause, and the hosts-file invariant paragraph
92 gains one sentence saying so. A wall of only gone panes is not empty: it
93 does not auto-open the picker, and it does not end mux on a terminal.
94
95 ## Tests
96
97 Unit (`wall_test_host.zig`, plural fixtures at nonzero offsets per the
98 working rules):
99 - `planHostDiff`: two pending panes plus one live tile on the same reachable
100 host; a list naming none of them dresses the pending pair `gone` and gives
101 the live tile its grace, then vanishes it on the second list. The pending
102 panes stay through both lists.
103 - gone → bind: a later list that names a gone pane's session binds it.
104 - flap: poll failure dresses a gone pane `unreachable`; recovery without the
105 session dresses it `gone` again.
106 - Enter re-arm: the tile leaves `pending`, creates, and occupies the same
107 rect (no relayout call observed).
108 - `x` on a gone pane closes it locally and frees the slot.
109
110 E2E (new leg beside the seed legs in `e2e_12_panes.sh`): build a 3-pane
111 wall (sessions 0, b, c — plural, so a fate can differ per pane), detach,
112 stop the daemon, start a fresh one on the same socket, reattach. The fresh
113 daemon has only the entry's session, so panes b and c go gone. Assert
114 through the render oracle: the saved cut paints first and never moves
115 (the rail columns across the whole capture are exactly the saved ones —
116 the assertion that fails today), both dead panes wear `gone` on the grid,
117 Enter in pane b starts a fresh shell whose marker lands in the same rect
118 with the rails still unmoved, and `x` on pane c collapses it only on that
119 keypress.
120
121 ## Out of scope
122
123 - Auto-start of gone panes (piggybacks later as a layer over Enter).
124 - The user's remote box (`alex@192.168.0.207`) carrying a pre-rename
125 `muxd` and an ssh PATH without `~/.local/bin` — a deployment chore; its
126 pane correctly reads `unreachable` today.