d2e7a1f8
docs: the gone-panes spec — a restored pane outlives its session
a73x 2026-09-01 09:16
Commit message
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. | ||