a6ebfe9a
docs: hosts live in a picker now
a73x 2026-08-28 19:53
Commit message
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. | ||