a73x

a6ebfe9a

docs: hosts live in a picker now

a73x   2026-08-28 19:53

Commit message
docs: hosts live in a picker now

The chord table's `:` row becomes the `s` row, the wall section says what
the popup is for and what an emptied wall does, and the invariant list
gains the picker beside the wall file it reads. `docs/decisions.md` keeps
the three that will be asked about: why a popup and not stripes, why
tiles pause under it, and why `x` and Enter close it.

CLAUDE.md
Old New
@@ -71,9 +71,7 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
71 (claim). ONE interaction loop, a tile pump: `mux TARGET` is the wall 71 (claim). ONE interaction loop, a tile pump: `mux TARGET` is the wall
72 zoomed on the tile it just attached to, and a wall of one tile whose rect 72 zoomed on the tile it just attached to, and a wall of one tile whose rect
73 is the whole terminal only when there is no terminal at all. A chord-born 73 is the whole terminal only when there is no terminal at all. A chord-born
74 tile inherits the focused tile's target and its `-A`; `Ctrl-\ :` takes a 74 tile inherits the focused tile's target and its `-A`.
75 spelling but adds a HOST, not a tile — its sessions arrive on that
76 host's first poll like any other host's.
77 - **Resize is gain-only.** `layout.Tree.resize` never shrinks a pane; the 75 - **Resize is gain-only.** `layout.Tree.resize` never shrinks a pane; the
78 shrink keys grow a neighbor at the focus's expense in `wallview.doResize`. 76 shrink keys grow a neighbor at the focus's expense in `wallview.doResize`.
79 - **Rails are painted from `relayout` and tiles cannot reach them.** Every 77 - **Rails are painted from `relayout` and tiles cannot reach them.** Every
@@ -99,7 +97,7 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
99 connection, polled once a second — never from the file — so a birth by 97 connection, polled once a second — never from the file — so a birth by
100 anyone appears and an exit disappears, and `n`/`p`/digits walk the one 98 anyone appears and an exit disappears, and `n`/`p`/digits walk the one
101 list. A host with no live session contributes NOTHING — the wall shows 99 list. A host with no live session contributes NOTHING — the wall shows
102 sessions and nothing else, and a wall with none says it is empty; tiles 100 sessions and nothing else, and a wall with none says so; tiles
103 ride out a blip rather than vanishing. `--via` and `muxa` record no host. 101 ride out a blip rather than vanishing. `--via` and `muxa` record no host.
104 Nothing re-creates a session: a daemon restart heals to what that daemon 102 Nothing re-creates a session: a daemon restart heals to what that daemon
105 now has, which for a fresh `muxd` is session `0`. 103 now has, which for a fresh `muxd` is session `0`.
@@ -108,6 +106,21 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
108 repair tool can read what the loader will not. The hub is phase 2: it 106 repair tool can read what the loader will not. The hub is phase 2: it
109 still reads the old `wall` file (`client.hydratedCreates`, the `ended` 107 still reads the old `wall` file (`client.hydratedCreates`, the `ended`
110 latch), which nothing on the CLI writes any more. 108 latch), which nothing on the CLI writes any more.
109 - **Hosts live in the picker, not on the wall.** `Ctrl-\ s` is a MODE of
110 `interact.PrefixFilter` (`picking`), like the spelling editor it now
111 contains: every byte is the popup's, so a key can never reach a session,
112 and the table is unit-testable without a terminal. Rows are the hosts
113 file's daemons in file order with the POLLER's last answer beside each
114 (`N sessions` / `no sessions` / `unreachable` / `connecting`); Enter
115 births on the selected host — a tile that CREATES at `client.nextFreeName`
116 off that host's own list, so a host with no tile is still one you can
117 start work on — `x` forgets a host (the file line, its poller, its tiles;
118 the sessions keep running), `a` is the old `:` editor. Tiles do not paint
119 while it is open (`Shared.picker_open`); the close bumps `repaint_gen`.
120 An empty wall opens it ONCE by itself, so an Esc there leaves the
121 one-line text; and on a TERMINAL the last tile's exit leaves that empty
122 wall rather than ending mux — a piped `mux` still exits with the shell's
123 code, because scripts read it.
111 - **`Ctrl-\ x` ends a session; the daemon owns the two-step.** `end_req` 124 - **`Ctrl-\ x` ends a session; the daemon owns the two-step.** `end_req`
112 `0x11` / `end_reply` `0x94`, observer verbs. The daemon refuses a first 125 `0x11` / `end_reply` `0x94`, observer verbs. The daemon refuses a first
113 `x` on a session other clients hold and answers the count; the client 126 `x` on a session other clients hold and answers the count; the client
README.md
Old New
@@ -91,7 +91,7 @@ The focused tile has the terminal, and only the prefix is held back:
91 | `Ctrl-\` `1`-`9` | focus tile N — the digit on that tile's label bar | 91 | `Ctrl-\` `1`-`9` | focus tile N — the digit on that tile's label bar |
92 | `Ctrl-\` `x` | end the focused session — the daemon hangs up its shell and every attached client sees the exit. With other clients on it the daemon refuses the first press and says how many; a second `x` within 3 seconds forces it | 92 | `Ctrl-\` `x` | end the focused session — the daemon hangs up its shell and every attached client sees the exit. With other clients on it the daemon refuses the first press and says how many; a second `x` within 3 seconds forces it |
93 | `Ctrl-\` `w` | zoom out to the wall | 93 | `Ctrl-\` `w` | zoom out to the wall |
94 | `Ctrl-\` `:` | add a daemon by spelling: type `HOST`, `quic://HOST[:PORT]` or `--sock PATH`, Enter puts it on the wall and its sessions arrive as tiles; Esc or Ctrl-C cancels. The line editor takes printable ASCII only, so a path or hostname with any other byte in it goes in the hosts file or on argv instead | 94 | `Ctrl-\` `s` | the host picker: a popup listing every daemon on the wall with what its last poll said — `N sessions`, `no sessions`, `unreachable`, `connecting`. `j`/`k` or the arrows move, `1`-`9` pick a row, Enter or `c` starts a session on the selected machine and closes, `x` forgets it (the file line goes, the sessions keep running), `a` adds one by spelling — `HOST`, `quic://HOST[:PORT]` or `--sock PATH`, Enter adds and Esc returns to the rows. Esc or `s` closes. The spelling editor takes printable ASCII only, so a path or hostname with any other byte in it goes in the hosts file or on argv instead. While the picker is open every key is its own: nothing reaches a session |
95 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) | 95 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) |
96 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) | 96 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) |
97 | drag with the left button | select what it crosses, copy on release | 97 | drag with the left button | select what it crosses, copy on release |
@@ -397,6 +397,21 @@ show until someone is born into it again. A wall on which no host has a live
397 session is empty, and says so on one line rather than showing a blank screen 397 session is empty, and says so on one line rather than showing a blank screen
398 that reads as hung. 398 that reads as hung.
399 399
400 That is what the **host picker** is for. The wall shows live sessions and
401 nothing else, so the machines behind them live in a popup over it: `Ctrl-\`
402 `s` lists every daemon in the hosts file with what its last poll said, and
403 Enter starts a session on the one you select — including a machine that
404 currently has none, which is the whole point of `no sessions` being a row
405 you can act on. `x` there forgets a host (the same edit `mux hosts rm`
406 makes, and it ends nothing), and `a` adds one. An empty wall opens the
407 picker by itself, because a blank screen is no place to act from; Esc
408 leaves the one-line text standing.
409
410 `Ctrl-\` `x` on the last tile of a terminal wall therefore leaves you on
411 that empty wall with the picker open, not back in your shell — `x` ends a
412 session, never mux. A piped `mux` is unchanged: it is a wall of one and it
413 still exits with the shell's exit code.
414
400 **A daemon too old to be asked shows nothing too.** Listing a daemon's live 415 **A daemon too old to be asked shows nothing too.** Listing a daemon's live
401 sessions is a new question on the wire, and a daemon from before this change 416 sessions is a new question on the wire, and a daemon from before this change
402 answers it with the silence it answers every unknown frame with. It is up, 417 answers it with the silence it answers every unknown frame with. It is up,
@@ -413,7 +428,8 @@ just started, is one session called `0`.
413 `mux hosts add`/`rm` are file operations only: neither dials, and `rm` never 428 `mux hosts add`/`rm` are file operations only: neither dials, and `rm` never
414 touches the session — the shells on that machine go on running and `mux 429 touches the session — the shells on that machine go on running and `mux
415 HOST` reaches them again. A wall that is already up does not notice the 430 HOST` reaches them again. A wall that is already up does not notice the
416 edit; the running wall's own way to add a machine is `Ctrl-\` `:`. 431 edit; the running wall's own way to add a machine is the picker's `a`
432 (`Ctrl-\` `s`, then `a`), and its own way to drop one is the picker's `x`.
417 433
418 There is no `mux wall` any more — the wall has no separate door to open, so 434 There is no `mux wall` any more — the wall has no separate door to open, so
419 `mux wall` is read as a HOST called `wall` like any other bare word, and ssh 435 `mux wall` is read as a HOST called `wall` like any other bare word, and ssh
@@ -435,8 +451,8 @@ the `Ctrl-\` prefix is held back. `Ctrl-\` `1`-`9` focuses a tile, `Ctrl-\`
435 `n`/`p` walk them, `Ctrl-\` `c` creates a session on the focused tile's 451 `n`/`p` walk them, `Ctrl-\` `c` creates a session on the focused tile's
436 daemon, `Ctrl-\` `h`/`j`/`k`/`l` move focus between panes, `Ctrl-\` `|`/`-` 452 daemon, `Ctrl-\` `h`/`j`/`k`/`l` move focus between panes, `Ctrl-\` `|`/`-`
437 split right or below, `Ctrl-\` `f` fullscreen, `Ctrl-\` `r` resize mode, 453 split right or below, `Ctrl-\` `f` fullscreen, `Ctrl-\` `r` resize mode,
438 `Ctrl-\` `:` adds a daemon, `Ctrl-\` `x` ends the focused session, `Ctrl-\` 454 `Ctrl-\` `s` opens the host picker, `Ctrl-\` `x` ends the focused session,
439 `w` zooms out, `Ctrl-\` `d` leaves. Any other command key after the prefix 455 `Ctrl-\` `w` zooms out, `Ctrl-\` `d` leaves. Any other command key after the prefix
440 is swallowed along with it, as in a plain client. 456 is swallowed along with it, as in a plain client.
441 457
442 Every tile's label bar opens with its chord digit, and the focused tile's 458 Every tile's label bar opens with its chord digit, and the focused tile's
docs/decisions.md
Old New
@@ -7321,3 +7321,32 @@ machine is the real use.
7321 supervisor reads a nonzero exit on a clean stop as a crash. Nonzero is a boot 7321 supervisor reads a nonzero exit on a clean stop as a crash. Nonzero is a boot
7322 failure only, stated in `main.run`'s doc block. The 130 that imitated SIGINT 7322 failure only, stated in `main.run`'s doc block. The 130 that imitated SIGINT
7323 went with the shell's code. 7323 went with the shell's code.
7324
7325 ## 2026-08-28 — a popup, because a wall of sessions is a bad place for a host
7326
7327 **A stripe was a host pretending to be a tile, and every reflex was wrong on
7328 it.** `x` did nothing (a stripe has no session to end), the cursor stayed in
7329 the previous tile (no pump paints a stripe), and ending a daemon's last
7330 session turned that host into one. So the wall shows live sessions and
7331 nothing else, and the hosts — all of them, with their state — moved into a
7332 popup: `Ctrl-\ s`. A row you cannot type into is a row no reflex misfires on.
7333
7334 **A mode, not a chord.** The picker is `interact.PrefixFilter.picking`, the
7335 shape the `:` prompt already had, so every byte is the popup's and the key
7336 table is unit-testable without a terminal. `s` and `a` deliberately do NOT
7337 end the read the way one-shot chords do: the bytes behind them were typed at
7338 the mode the same keystroke opened, and `\x1c s a --sock ...` in one read
7339 has to reach the line editor.
7340
7341 **Tiles pause while it is open.** Every tile paint asks `Shared.picker_open`
7342 first; the replicas stay hot and the close bumps `repaint_gen`, so every
7343 rect comes back. Painting under the box was the alternative, and it is not
7344 one: a tile that repaints its rect erases the rows the user is choosing
7345 from, and the popup is not any tile's to redraw. The frame is hashed and an
7346 identical one is not rewritten — the pollers repaint this box once a second
7347 per host, and a terminal that never goes quiet also breaks every `settle` in
7348 the suite.
7349
7350 **`x` in the picker closes it.** Forgetting a host takes its tiles off the
7351 wall, and the user is owed the wall that made rather than a popup over it.
7352 A birth closes for the same reason. Only `a` and the moves keep it up.