fix: address the release-prep review of #688

Give a client that fails the M-1..M-9 guard its key back (send-keys <key> as the if-shell else argument), so a browser tab's Esc then digit reaches its pane instead of being eaten, also after a TUI killed before restore(); only M-1..M-9 are accepted since the key is now interpolated into that command
Test it: the unit test pins the else argument and the key check, and the real-tmux test runs cat -v in alpha and waits for the other client's Esc 2 to echo there, both before and after the TUI dies
Say so in docs/architecture-invariants.md and docs/tui.md, and reword the killed-TUI and failed-arm notes that said the keys do nothing

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-24 14:45:34 +02:00
1 parent 311d01c52b
commit 46774b71f9
5 files changed
+39 -9

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ Implementation detail extracted from `CLAUDE.md` so that file stays small enough
### The prefix table on Codeman's socket holds only send-prefix
**Browser tabs are tmux clients, so tmux's prefix table must not reach `switch-client`** (`src/tmux-prefix-table.ts`, called from `createSession` before `respawn-pane`, and from `reconcileSessions()` at boot whenever the socket lists a pane, so sessions that survived an upgrade are covered). Every keystroke typed into a browser terminal reaches the `tmux attach-session` client behind it, and app.js deliberately lets Ctrl+B through to the terminal (readline, Claude Code's "ctrl+b ctrl+b"). With tmux's stock prefix table, Ctrl+B then `(` `)` `L` `s` `w` moved that client onto ANOTHER session, which the tab then showed and typed into under the original's label; `d` detached it, `$` renamed the session, `:` opened tmux's prompt. So the table is reduced to `<prefix> send-prefix` (the socket's configured prefix, read with `show-options -gv prefix`), once per tmux server in practice: the reset only runs while `list-keys -T prefix` shows anything else. The same reduction is applied when Codeman creates the nested tmux behind a Docker case (`-L codeman-docker`) or an owned remote session (`-L codeman-remote`). Those servers set `prefix C-q` and used to keep tmux's stock prefix table, so inside the pane Ctrl+Q then `)` `(` `s` `w` or `L` ran `switch-client` / `choose-tree` on the inner server and moved its client onto a sibling session. `nestedPrefixTableResetCommands()` (bind `C-q send-prefix`, then `unbind-key -a -T prefix`, then bind it again, so a user config that left the prefix table empty cannot make `unbind-key -a` abort the `\;` chain) is appended to both create chains, before `remain-on-exit failed` on the remote chain so a remote tmux that rejects `failed` cannot drop it. Those sockets are dedicated to Codeman, so the server-global table is the right scope. A non-owned remote attach joins another Codeman's `-L codeman` and does not run it. ⚠️ Keep `send-prefix`: Claude Code inside tmux backgrounds a command with "ctrl+b ctrl+b". ⚠️ Never touch the `root` table, and tolerate (and re-bind after a reset) `C-<letter> detach-client`: `codeman tui`'s attach claims F1 and Alt+1..9 in `root` and the held-Ctrl alias `C-d` in `prefix` (`beginAttachHandoff`), and a session created mid-attach must not take that way out away. ⚠️ Key tables are server-wide. F1 and the held-Ctrl `C-d` still detach every client for the length of an attach (a way out, not a session move). **`M-1`..`M-9` do not move other clients.** `bindSwitchKey` binds them to `if-shell -F` (`tuiSwitchGuard`) so `switch-client` runs only when `#{client_tty}` is the TUI's terminal and `#{client_pid}` equals `@codeman-tui-pid`. A one-shot `client-attached[90]` hook (`tuiSwitchAttachHook`) sets that option as the TUI's client attaches, then removes itself. It is armed as the handoff's last tmux call before `spawnSync`, after the keys and the per-session bars, so only a TUI killed during the attach's own startup can leave that hook armed for its tty. A browser tab does not match, including Esc followed by a digit within `escape-time` (500 ms on tmux 3.4), which arrives as the same `M-<digit>`. If the TUI is killed before `restore()`, the binding can remain, but the stored pid is the dead client and the hook will not re-arm a pty that reuses the number, so the key does nothing. `restore()` removes the keys only while they still name this tty, and the next attach that claims the strip releases any of these bindings first. A second TUI attach replaces the keys (one binding per key). Escape-time on Codeman's socket stays at tmux's default: lowering it would change Esc delivery for every pane, and the guard already refuses other clients. ⚠️ The consequence for every terminal attach: `Ctrl+B d` does not exist on this socket. Anything that attaches a user's terminal claims its own way out for the attach, as `beginAttachHandoff` and `scripts/tmux-manager.sh` (F1 in `root`) do, and never advertises tmux's stock chord. Tests: `test/tmux-prefix-table.test.ts` (real tmux on a private socket, driven through a pty attach client, including the nested `C-q` table), `test/tui/tui-switch-guard.test.ts` (the guarded Alt keys against a second client), the wiring in `test/tmux-manager.test.ts`, the script in `test/tmux-manager-script-attach.test.ts`.
**Browser tabs are tmux clients, so tmux's prefix table must not reach `switch-client`** (`src/tmux-prefix-table.ts`, called from `createSession` before `respawn-pane`, and from `reconcileSessions()` at boot whenever the socket lists a pane, so sessions that survived an upgrade are covered). Every keystroke typed into a browser terminal reaches the `tmux attach-session` client behind it, and app.js deliberately lets Ctrl+B through to the terminal (readline, Claude Code's "ctrl+b ctrl+b"). With tmux's stock prefix table, Ctrl+B then `(` `)` `L` `s` `w` moved that client onto ANOTHER session, which the tab then showed and typed into under the original's label; `d` detached it, `$` renamed the session, `:` opened tmux's prompt. So the table is reduced to `<prefix> send-prefix` (the socket's configured prefix, read with `show-options -gv prefix`), once per tmux server in practice: the reset only runs while `list-keys -T prefix` shows anything else. The same reduction is applied when Codeman creates the nested tmux behind a Docker case (`-L codeman-docker`) or an owned remote session (`-L codeman-remote`). Those servers set `prefix C-q` and used to keep tmux's stock prefix table, so inside the pane Ctrl+Q then `)` `(` `s` `w` or `L` ran `switch-client` / `choose-tree` on the inner server and moved its client onto a sibling session. `nestedPrefixTableResetCommands()` (bind `C-q send-prefix`, then `unbind-key -a -T prefix`, then bind it again, so a user config that left the prefix table empty cannot make `unbind-key -a` abort the `\;` chain) is appended to both create chains, before `remain-on-exit failed` on the remote chain so a remote tmux that rejects `failed` cannot drop it. Those sockets are dedicated to Codeman, so the server-global table is the right scope. A non-owned remote attach joins another Codeman's `-L codeman` and does not run it. ⚠️ Keep `send-prefix`: Claude Code inside tmux backgrounds a command with "ctrl+b ctrl+b". ⚠️ Never touch the `root` table, and tolerate (and re-bind after a reset) `C-<letter> detach-client`: `codeman tui`'s attach claims F1 and Alt+1..9 in `root` and the held-Ctrl alias `C-d` in `prefix` (`beginAttachHandoff`), and a session created mid-attach must not take that way out away. ⚠️ Key tables are server-wide. F1 and the held-Ctrl `C-d` still detach every client for the length of an attach (a way out, not a session move). **`M-1`..`M-9` do not move other clients.** `bindSwitchKey` binds them to `if-shell -F` (`tuiSwitchGuard`) so `switch-client` runs only when `#{client_tty}` is the TUI's terminal and `#{client_pid}` equals `@codeman-tui-pid`. A one-shot `client-attached[90]` hook (`tuiSwitchAttachHook`) sets that option as the TUI's client attaches, then removes itself. It is armed as the handoff's last tmux call before `spawnSync`, after the keys and the per-session bars, so only a TUI killed during the attach's own startup can leave that hook armed for its tty. A browser tab does not match, including Esc followed by a digit within `escape-time` (500 ms on tmux 3.4), which arrives as the same `M-<digit>`. A client that fails the guard gets the key back: the `if-shell` else argument is `send-keys M-<digit>`, which writes Esc and the digit into that client's own pane, so a vim `<Esc>2j` in a browser tab is not eaten by the root binding. If the TUI is killed before `restore()`, the binding can remain, but the stored pid is the dead client and the hook will not re-arm a pty that reuses the number, so the key never switches and every client gets it in its pane through the else branch. `restore()` removes the keys only while they still name this tty, and the next attach that claims the strip releases any of these bindings first. A second TUI attach replaces the keys (one binding per key). Escape-time on Codeman's socket stays at tmux's default: lowering it would change Esc delivery for every pane, and the guard already refuses other clients. ⚠️ The consequence for every terminal attach: `Ctrl+B d` does not exist on this socket. Anything that attaches a user's terminal claims its own way out for the attach, as `beginAttachHandoff` and `scripts/tmux-manager.sh` (F1 in `root`) do, and never advertises tmux's stock chord. Tests: `test/tmux-prefix-table.test.ts` (real tmux on a private socket, driven through a pty attach client, including the nested `C-q` table), `test/tui/tui-switch-guard.test.ts` (the guarded Alt keys against a second client), the wiring in `test/tmux-manager.test.ts`, the script in `test/tmux-manager-script-attach.test.ts`.
### Reverse-proxy base path
+2 -1
View File
@@ -195,7 +195,8 @@ That is the **session strip**: the other sessions stay visible from inside a pan
numbered exactly as the dashboard numbers them, with the one you are in inverted.
`Alt+1`..`Alt+9` switch between them without going back to the dashboard first. Those keys
run `switch-client` only for this terminal's tmux client, so a browser tab on the
same socket cannot use them to jump sessions, Esc followed by a digit included. With
same socket cannot use them to jump sessions, Esc followed by a digit included (that
tab's pane still receives the Esc and the digit). With
more sessions than fit, the strip shows a window around the current one and marks
each cut end with `…`; the way-out hint is measured first and always keeps its space.