muxr 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f553caf555f455f8c89689ed8ef73de3158467171551f177a27009363055091
4
- data.tar.gz: b26779047961944af58a74593773b876af42c4e512ed956e0c7cb7a9becead99
3
+ metadata.gz: 03650b7088c85aa8fbe993dbfe17e7f8aac8334f617c3ff148589964d83bb701
4
+ data.tar.gz: 9b878beb1c05f1c83273f3f1c8e51944be7c75b3fdcd13935c96f851a2aa8405
5
5
  SHA512:
6
- metadata.gz: 4ab01bd3c63531f1e9b64aecb2532e79eecc4e3736d7970cb1040ee5e9db0fd9f316b06358f1bd758bc93d413ed5e70d17d884ce85ebb4f593cc4d556b180ee3
7
- data.tar.gz: '09abcbbdec950405196dc6015dc00c201c7200c890db0de49ba0c3b703923b78125c0cce458b950c1921970e569e9eb209b1ffd1930ee9cda83ebeb8f88d8afa'
6
+ metadata.gz: 91ccd01254428f3a2333e3abe94cd3d039598813c484730a4819c938f9f0d733ab611bd9c975314ca5bfb0af07c243c69814ed918fc02315e7652436454170b8
7
+ data.tar.gz: c58966f41d4a62978e3bfd7560d72ef132b6fcbdededddf129cf94216d77689b1f1f52c44cf7660cab9780a7ec8f3180fe138a9f2f75f8ec741548595c415665
data/CHANGELOG.md CHANGED
@@ -6,6 +6,76 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.7] - 2026-05-20
10
+
11
+ ### Added
12
+ - Scrollback search via `/` (forward) and `?` (backward). Enter commits
13
+ the query, `n` / `N` cycle through matches with wrap. Smart-case
14
+ matching scans both scrollback and the live buffer; the chosen match
15
+ is centered in the viewport and every match is highlighted in yellow
16
+ until the user exits scrollback.
17
+ - Arrow / page keys in scrollback. `↑`/`↓` scroll a line, `PgUp`/`PgDn`
18
+ half-page, `Home`/`End` jump to top/bottom. `InputHandler#feed` now
19
+ peeks for CSI escape sequences so a bare `Esc` still exits scrollback
20
+ the way it always has.
21
+ - Shift-`H`/`J`/`K`/`L` swaps the focused pane with the spatial
22
+ neighbor in that direction (linear next/prev fallback in monocle).
23
+ Focus tracks the moved pane so you can keep dragging it; swapping
24
+ into position 0 of tall/grid promotes it to master.
25
+
26
+ ### Changed
27
+ - Pane close is now confirmed. The close binding moved from `K` to
28
+ `x` (in both normal mode and the Ctrl-a prefix) so shift-`HJKL` is
29
+ free for the new move action. Close routes through a `:confirm_close`
30
+ state that flashes `close pane? (y/n)` — drawer hide stays
31
+ prompt-free since it's reversible.
32
+
33
+ ## [0.1.6] - 2026-05-15
34
+
35
+ ### Added
36
+ - Vim-style `:normal` mode as the default startup state. Single keys
37
+ (hjkl, c, K, t/g/m, s, etc.) act directly without the Ctrl-a prefix;
38
+ `i` drops into the historical `:passthrough` mode and `Ctrl-a Esc`
39
+ returns to normal.
40
+ - Spatial hjkl navigation via a new `LayoutManager.neighbor` that
41
+ computes pane adjacency from layout rects, so focus moves the way the
42
+ eye expects in tall and grid layouts (monocle falls back to linear).
43
+ - `[MODE]` chip rendered in the top-right corner of the focused
44
+ container, plus a per-mode color palette on the focused pane border
45
+ and status chip (cyan normal, green passthrough, orange scrollback,
46
+ magenta selection, yellow command, red quit-confirm, blue help). The
47
+ `:prefix` substate shares the passthrough green so the border doesn't
48
+ flicker when Ctrl-a is pressed.
49
+ - Foreground-command annotation in pane titles. A 750ms background
50
+ poller (`Application#start_foreground_poller`) walks each pane's
51
+ foreground process group and stamps the name onto
52
+ `pane.foreground_command`; titles now read `#1 abc123 · npm test`
53
+ when the shell isn't itself in the foreground. Lookup goes through
54
+ `/proc/<pid>/stat` on Linux and `ps` on macOS, off the event loop.
55
+ - OSC 8 hyperlink passthrough. The Terminal buffers OSC payloads,
56
+ extracts OSC 8 link bodies (interned per terminal), and stamps the
57
+ active link onto every cell. The Renderer carries hyperlink through
58
+ its Cell struct and wraps each contiguous run with one open/close
59
+ pair, so Ghostty et al. treat a wrapped URL as one clickable link.
60
+ - Space as a thumb-friendly alias for `v` in selection mode (toggle
61
+ linear anchor).
62
+
63
+ ### Changed
64
+ - Focused pane title now shows the mode label
65
+ (`[NORMAL]`/`[PASS]`/etc.) instead of the redundant layout name —
66
+ the status bar already shows the active layout.
67
+ - `[MODE]` chip moved out of the title and into the top-right corner
68
+ of the focused container, freeing the left-side title for the
69
+ foreground command.
70
+ - Dropped the old `space → page-down` mapping in scrollback so space
71
+ is available for the new selection-mode alias.
72
+
73
+ ### Fixed
74
+ - Exiting scrollback now restores the previous base mode (normal or
75
+ passthrough) instead of always landing in normal. A
76
+ passthrough → scrollback → exit round-trip now leaves you back in
77
+ passthrough.
78
+
9
79
  ## [0.1.5] - 2026-05-13
10
80
 
11
81
  ### Added
data/README.md CHANGED
@@ -7,22 +7,33 @@ like tiling-window-manager clients — you never resize them by hand;
7
7
  the active layout decides geometry.
8
8
 
9
9
  ```
10
- #1 ★ (tall) ──────────────┬ #2 ──────────────────────────┐
11
- │ master pane │ stacked slave pane
12
-
13
- ├──────────────────────────────┤
14
- stacked slave pane
15
-
16
- └───────────────────────────┴──────────────────────────────┘
17
- ┌ Drawer ─────────────────────────────────────────────────┐
18
- │ persistent overlay shell, opens from the bottom
19
- └─────────────────────────────────────────────────────────┘
20
- [default] panes:3 layout:tall focused:#1 drawer:shown muxr ^a ?
10
+ ┌─ #1 a3f9b2 · npm test ──── [NORMAL] ─┬─ #2 c2e810 ──────────────┐
11
+ │ master pane (running npm test) │ stacked slave pane
12
+
13
+ ├──────────────────────────┤
14
+ #3 9b1d04 [P]
15
+ private pane (MCP-hidden)
16
+ └────────────────────────────────────────┴──────────────────────────┘
17
+ ┌ Drawer ────────────────────────────────────────────────────────────┐
18
+ │ persistent overlay shell, opens from the bottom
19
+ └────────────────────────────────────────────────────────────────────┘
20
+ [NORMAL] [default] panes:3 layout:tall focused:#1 drawer:shown muxr ^a ?
21
21
  ```
22
22
 
23
+ Each pane shows its slot (`#1`, `#2`, …) plus a stable 6-hex id
24
+ (`a3f9b2`). The slot is positional and shifts when panes are created,
25
+ killed, or promoted; the id is generated once and survives layout
26
+ changes, detach/reattach, and cold-restart from the session JSON. `[P]`
27
+ marks a private pane that the MCP control surface refuses to read or
28
+ drive (see [MCP control surface](#mcp-control-surface) below). The
29
+ focused pane's title shows the foreground command running in its PTY
30
+ (e.g. `· npm test`) when something other than the shell is in the
31
+ foreground, and the `[NORMAL]` chip in the top-right corner — along
32
+ with the border color — tracks the current [input mode](#modes).
33
+
23
34
  ## Screenshots
24
35
 
25
- The three built-in layouts (cycle with `C-a Tab`):
36
+ The three built-in layouts (pick directly with `t`/`g`/`m` in normal mode, or cycle with `Tab` / `C-a Tab`):
26
37
 
27
38
  <table>
28
39
  <tr>
@@ -37,42 +48,107 @@ The three built-in layouts (cycle with `C-a Tab`):
37
48
  </tr>
38
49
  </table>
39
50
 
40
- The Quake-style drawer overlay (`C-a ~`):
51
+ The Quake-style drawer overlay (`~` in normal mode, `C-a ~` in passthrough):
41
52
 
42
53
  ![drawer overlay](docs/screenshots/04-drawer.png)
43
54
 
44
55
  ## Install / run
45
56
 
46
57
  ```bash
47
- git clone https://github.com/roelbondoc/muxr
48
- cd muxr
49
- bin/muxr # attach the "default" session (auto-spawn if needed)
50
- bin/muxr work # attach (or start) a named session
51
- bin/muxr --list # list saved sessions and exit
52
- bin/muxr --help
58
+ gem install muxr
59
+ muxr # attach the "default" session (auto-spawn if needed)
60
+ muxr work # attach (or start) a named session
61
+ muxr --list # list running sessions and exit
62
+ muxr --install-skill # install the MCP skill into ~/.claude/skills
63
+ muxr --help
53
64
  ```
54
65
 
55
66
  Requires **Ruby ≥ 3.4**. No runtime gems — just `PTY`, `IO.console`, `JSON`,
56
67
  `Socket`, and `FileUtils` from stdlib.
57
68
 
58
- `bin/muxr` is the client. The first invocation for a session daemonizes a
69
+ `muxr` is the client. The first invocation for a session daemonizes a
59
70
  server in the background; subsequent invocations attach to it over a Unix
60
- socket. `Ctrl-a d` detaches the client and leaves the server (and every
61
- shell it owns) running, so reattaching gives you back the exact same panes
62
- with their full history.
71
+ socket. `d` (normal mode) / `C-a d` (passthrough) detaches the client
72
+ and leaves the server (and every shell it owns) running, so reattaching
73
+ gives you back the exact same panes with their full history.
63
74
 
64
- ## Keybindings (Ctrl-a prefix)
75
+ ### From source
76
+
77
+ To run the latest unreleased code or hack on muxr locally, clone the repo
78
+ and use `bin/muxr` directly — it puts `lib/` on `$LOAD_PATH` itself:
79
+
80
+ ```bash
81
+ git clone https://github.com/roelbondoc/muxr
82
+ cd muxr
83
+ bin/muxr # same flags as the installed `muxr` executable
84
+ ```
85
+
86
+ ## Modes
87
+
88
+ muxr has two top-level input modes, modeled on vim:
89
+
90
+ - **Normal** (default at startup) — single keys act on the multiplexer.
91
+ `hjkl` moves focus between panes, `HJKL` moves the focused pane
92
+ itself, `c`/`x` create/close panes, `t`/`g`/`m` set the layout, etc.
93
+ No prefix needed.
94
+ - **Passthrough** (entered with `i`) — every keystroke is forwarded to
95
+ the focused pane, exactly like a regular terminal. muxr commands are
96
+ reached via the historical `Ctrl-a` prefix. `Ctrl-a Esc` returns to
97
+ normal mode.
98
+
99
+ The active mode appears as a `[MODE]` chip in the top-right corner of
100
+ the focused pane (and the leftmost slot of the status bar). The
101
+ focused pane's border is colored by mode — cyan for normal, green for
102
+ passthrough, orange for scrollback (and its `/` search prompt),
103
+ magenta for selection, yellow for the command prompt, red during the
104
+ kill-session or close-pane confirmation, blue while help is open.
105
+ Unfocused panes always render with the grey unfocused border,
106
+ regardless of mode.
107
+
108
+ ### Normal mode
109
+
110
+ | Keys | Action |
111
+ |----------------------|-----------------------------------------------------|
112
+ | `h` / `j` / `k` / `l`| focus pane left / down / up / right (spatial) |
113
+ | `H` / `J` / `K` / `L`| move focused pane left / down / up / right |
114
+ | `i` | drop into passthrough mode |
115
+ | `c` / `x` | new / close focused pane (close asks `y/n`) |
116
+ | `t` / `g` / `m` | layout: tall / grid / monocle |
117
+ | `Tab` / `Enter` | cycle layout / promote focused to master |
118
+ | `a` / `1` … `9` | toggle last pane / jump to pane by number |
119
+ | `s` | enter scrollback / copy-mode |
120
+ | `~` / `C` / `P` | drawer / Claude drawer / toggle private flag |
121
+ | `]` | paste internal yank buffer into focused pane |
122
+ | `:` / `?` | command prompt / help |
123
+ | `d` / `q` | detach / kill session (asks `y/n`) |
124
+
125
+ `h`/`j`/`k`/`l` does true spatial navigation — it inspects the current
126
+ layout's rectangles and picks the closest neighbor in the requested
127
+ direction. In monocle (where every pane occupies the full area) it
128
+ falls back to linear next/previous so the keys still do something
129
+ useful.
130
+
131
+ `H`/`J`/`K`/`L` swap the focused pane with the spatial neighbor in
132
+ that direction, then keep focus on the moved pane so you can keep
133
+ dragging it. Swapping into position 0 of `tall`/`grid` promotes the
134
+ pane to master (the layout master is always `panes[0]`). In monocle
135
+ the move falls back to linear next/prev shuffling.
136
+
137
+ ### Passthrough mode (`Ctrl-a` prefix)
65
138
 
66
139
  | Keys | Action |
67
140
  |----------------|---------------------------------------------------------|
141
+ | `C-a Esc` | return to normal mode |
68
142
  | `C-a c` | new pane |
69
- | `C-a n` / `p` | focus next / previous pane |
143
+ | `C-a n` / `p` | focus next / previous pane (linear) |
70
144
  | `C-a a` | toggle last (previously focused) pane |
71
145
  | `C-a 1` … `9` | jump to pane by its label |
72
- | `C-a k` | close focused pane (or hide drawer) |
146
+ | `C-a x` | close focused pane (asks `y/n`; hides drawer with no prompt) |
73
147
  | `C-a Tab` | cycle layout (`tall` → `grid` → `monocle`) |
74
148
  | `C-a Enter` | promote focused pane to master |
75
- | `C-a ~` | toggle drawer |
149
+ | `C-a ~` | toggle drawer (shell) |
150
+ | `C-a C` | toggle Claude Code drawer (MCP-aware) |
151
+ | `C-a P` | toggle private flag on focused pane (hides from MCP) |
76
152
  | `C-a [` | enter scrollback / copy-mode |
77
153
  | `C-a ]` | paste internal yank buffer into focused pane |
78
154
  | `C-a d` | detach (server keeps running) |
@@ -83,17 +159,25 @@ with their full history.
83
159
 
84
160
  ### Scrollback and copy-mode
85
161
 
86
- Each pane keeps a bounded (5000-row) scrollback ring. `C-a [` enters
87
- scrollback with vi-style navigation; the status bar shows a key hint and
88
- the pane title gains `[scrollback N/M]`.
162
+ Each pane keeps a bounded (5000-row) scrollback ring. `s` in normal
163
+ mode (or `C-a [` in passthrough) enters scrollback with vi-style
164
+ navigation; the status bar shows a key hint and the pane title gains
165
+ `[scrollback N/M]`.
89
166
 
90
167
  | Keys | Action |
91
168
  |-------------------------|-------------------------------------|
92
- | `j` / `k` | scroll one line |
93
- | `d` / `u` (or `C-d`/`C-u`) | half page |
169
+ | `j` / `k` or `↓` / `↑` | scroll one line |
170
+ | `d` / `u` (or `C-d`/`C-u`, `PgDn`/`PgUp`) | half page |
94
171
  | `f` / Space (or `C-f`/`C-b`) | full page |
95
- | `g` / `G` | top / bottom |
96
- | `q` / `Esc` / `C-c` | exit back to live view |
172
+ | `g` / `G` (or `Home` / `End`) | top / bottom |
173
+ | `/` *query* `Enter` | search forward (toward newer); `?` searches backward |
174
+ | `n` / `N` | next / previous match in the search direction (wraps) |
175
+ | `q` / `Esc` / `C-c` | exit back to normal mode |
176
+
177
+ Search uses smart-case (case-insensitive unless the query has an
178
+ uppercase letter), scans both scrollback and the live buffer, and
179
+ centers the chosen match in the viewport. Matches stay highlighted in
180
+ yellow while you're in scrollback; exiting clears the highlight.
97
181
 
98
182
  Press `v` inside scrollback to enter a movable-cursor selection mode.
99
183
  Vim-style motions are supported:
@@ -109,21 +193,23 @@ Vim-style motions are supported:
109
193
  | `H` / `M` / `L` | top / middle / bottom of viewport |
110
194
  | `C-d`/`C-u`, `C-f`/`C-b`, Space | half / full page |
111
195
  | `v` / `C-v` | anchor char / block selection (toggle) |
112
- | `y` or Enter | yank and exit to live shell |
196
+ | `y` or Enter | yank and return to normal mode |
113
197
  | `q` / `Esc` / `C-c` | cancel back to scrollback |
114
198
 
115
199
  `v` and `C-v` toggle between character and block (rectangular) selection
116
200
  — switching between the two preserves the anchor. `y` or Enter yanks the
117
201
  selection into an internal buffer, pipes it to `pbcopy` in the background
118
- (silent no-op when `pbcopy` is unavailable), and drops you straight back
119
- to the live shell. `C-a ]` writes the yank buffer back into the focused
120
- pane.
202
+ (silent no-op when `pbcopy` is unavailable), and returns to normal mode.
203
+ `]` (normal) / `C-a ]` (passthrough) writes the yank buffer back into the
204
+ focused pane.
121
205
 
122
- ## Commands (typed after `C-a :`)
206
+ ## Commands (typed after `:` in normal mode, or `C-a :` in passthrough)
123
207
 
124
208
  ```
125
209
  layout {tall|grid|monocle} # also: layout (no arg) → cycle
126
210
  drawer {toggle|show|hide|reset}
211
+ claude # toggle the Claude Code drawer
212
+ private # toggle private flag on focused pane
127
213
  save # persist session to ~/.muxr/sessions/<name>.json
128
214
  restore # show path to saved session
129
215
  sessions | ls # list saved sessions
@@ -131,6 +217,58 @@ new | close | next | prev | master
131
217
  detach | quit # quit asks for y/n confirmation
132
218
  ```
133
219
 
220
+ ## MCP control surface
221
+
222
+ muxr exposes a second listener at `~/.muxr/sockets/<name>.ctrl.sock`
223
+ that accepts multiple concurrent NDJSON clients over a small JSON-RPC
224
+ surface (`session.get`, `panes.list`, `pane.read`, `pane.send_input`,
225
+ `pane.run`, `pane.subscribe`, `pane.kill`, `layout.set`, `drawer.*`,
226
+ …). The control socket is independent of TTY attach — programmatic
227
+ clients never count as "attached", so a Claude Code session and a human
228
+ can drive the multiplexer concurrently.
229
+
230
+ `pane.run` waits for the PTY to go idle before responding: it sends the
231
+ input, polls for output, and returns once no bytes have arrived for
232
+ `idle_ms` (default 500). Server-side idle detection avoids the
233
+ send-then-poll race that plagues naive client-side automation.
234
+
235
+ `pane.send_input`, `pane.run`, and `drawer.send_input` accept a `keys`
236
+ array of vim-style `<name>` tokens (`<esc>`, `<c-c>`, `<cr>`, arrows,
237
+ etc.) interleaved with literal text — callers don't have to remember
238
+ that Escape is `"\e"` and Ctrl-C is `"\x03"`. Bracketed-paste wrapping
239
+ still applies to literal segments only.
240
+
241
+ ### Claude Code integration
242
+
243
+ ```bash
244
+ muxr --install-skill # copies skills/muxr-control into ~/.claude/skills
245
+ # and prints the `claude mcp add` registration line
246
+ ```
247
+
248
+ `bin/muxr-mcp` is the standalone MCP-over-stdio bridge that translates
249
+ Claude Code tool calls into NDJSON requests on the control socket. It
250
+ auto-detects the target session from `MUXR_CONTROL_SOCKET` or
251
+ `MUXR_SESSION` env vars.
252
+
253
+ `C` (normal) / `C-a C` (passthrough) / `:claude` opens a drawer whose shell is `claude`, with
254
+ `MUXR_SESSION`, `MUXR_CONTROL_SOCKET`, `MUXR_FOCUSED_PANE`, and
255
+ `MUXR_DRAWER_SELF=1` injected into its environment. The bridge picks
256
+ those up automatically; you get a Quake-style Claude Code overlay that
257
+ already knows what session it's in. `MUXR_DRAWER_SELF` makes the bridge
258
+ refuse `drawer.*` methods, so a claude drawer can't recurse into its
259
+ own PTY.
260
+
261
+ ### Private panes
262
+
263
+ `P` (normal) / `C-a P` (passthrough) / `:private` flips the private flag on the focused pane.
264
+ Private panes are hidden from programmatic callers: `panes.list` strips
265
+ cwd/rows/cols, and `pane.read`, `pane.send_input`, `pane.run`,
266
+ `pane.subscribe`, and `pane.kill` refuse with an error message pointing
267
+ the human at the TTY (`P` / `C-a P`) to expose it. The flag is persisted in session
268
+ JSON and shown as `[P]` in the pane title bar. The MCP surface
269
+ intentionally has no method to flip the flag — only a human at the TTY
270
+ can mark a pane public again.
271
+
134
272
  ## Architecture
135
273
 
136
274
  muxr runs as **two processes** that talk over a Unix domain socket at
@@ -144,26 +282,37 @@ Client (foreground, owns the TTY) Server (daemon, owns the PTYs)
144
282
  ├─ SIGWINCH → RESIZE frame ├─ Session ─ Window ─ Pane[ ] ─ Terminal + PTYProcess
145
283
  │ │ └─ Drawer ─ Pane
146
284
  └─ Protocol ├─ Renderer – diff-emits ANSI as OUTPUT frames
147
- ◄── OUTPUT bytes ──── Renderer ◄────────────┤ InputHandler – Ctrl-a state machine
285
+ ◄── OUTPUT bytes ──── Renderer ◄────────────┤ InputHandler – normal/passthrough mode state machine
148
286
  ──── INPUT bytes ───► InputHandler ├─ CommandDispatcher – parses ":"-prefixed commands
149
287
  ──── HELLO/RESIZE ──► apply_size ├─ LayoutManager – pure (layout, count, area) → [Rect]
150
- ◄── BYE ───────────── disconnect_client └─ UNIXServer listener (one client at a time)
288
+ ◄── BYE ───────────── disconnect_client ├─ UNIXServer (TTY socket, one client at a time)
289
+ └─ UNIXServer (.ctrl.sock, many NDJSON clients)
151
290
  ```
152
291
 
153
292
  Frames are length-prefixed (`[1-byte type][4-byte BE length][payload]`):
154
293
  `H` hello, `I` input, `R` resize, `B` bye, `O` output.
155
294
 
156
- The server's event loop is single-threaded `IO.select` over the listening
157
- socket, the attached client (when present), every pane PTY, and the
158
- drawer PTY. Layouts are pure`LayoutManager` has no mutable state, so
159
- the renderer recomputes geometry on every tick after a resize or
160
- pane add/remove without bookkeeping.
161
-
162
- `Ctrl-a d` detaches the client but leaves the server (and its shells)
163
- running; reattaching gives you back the same panes with their full
164
- history. `Ctrl-a q` and `:quit` flash `kill session? (y/n)` in the status
165
- bar and only tear the server down on `y` — there is no "kill without
166
- confirm" keybinding by design.
295
+ A second listener at `~/.muxr/sockets/<name>.ctrl.sock` accepts
296
+ multiple concurrent NDJSON clients for the MCP control surface (see
297
+ above). The two sockets are independentprogrammatic clients never
298
+ count as "attached", so they don't lock out the human's TTY client.
299
+
300
+ The server's event loop is single-threaded `IO.select` over the
301
+ listening sockets, the attached client (when present), every pane PTY,
302
+ the drawer PTY, and every connected control client. A single
303
+ background thread polls each pane's foreground process group every
304
+ 750ms (`/proc/<pid>/stat` on Linux, `ps -o tpgid=,pgid=` on macOS) so
305
+ the cmd` annotation in the pane title can refresh without blocking
306
+ the render loop. Everything else stays on the main thread. Layouts
307
+ are pure — `LayoutManager` has no mutable state, so the renderer
308
+ recomputes geometry on every tick after a resize or pane add/remove
309
+ without bookkeeping.
310
+
311
+ `d` (normal) / `C-a d` (passthrough) detaches the client but leaves
312
+ the server (and its shells) running; reattaching gives you back the
313
+ same panes with their full history. `q` / `C-a q` / `:quit` flash
314
+ `kill session? (y/n)` in the status bar and only tear the server down
315
+ on `y` — there is no "kill without confirm" keybinding by design.
167
316
 
168
317
  The drawer's PTY is **never torn down** when the drawer is hidden — its
169
318
  shell process keeps running so the next toggle restores the previous
@@ -187,16 +336,24 @@ Sessions live in `~/.muxr/sessions/<name>.json`:
187
336
  "layout": "tall",
188
337
  "focused_index": 0,
189
338
  "master_index": 0,
190
- "panes": [{"cwd": "/home/me/code"}, {"cwd": "/tmp"}],
339
+ "panes": [
340
+ {"id": "a3f9b2", "cwd": "/home/me/code", "private": false},
341
+ {"id": "c2e810", "cwd": "/tmp", "private": true}
342
+ ],
191
343
  "drawer": {"visible": true, "cwd": "/home/me/code"}
192
344
  }
193
345
  ```
194
346
 
347
+ Pane ids and the private flag are persisted, so the same ids survive
348
+ cold-restart from the JSON snapshot and a pane that was marked private
349
+ stays private.
350
+
195
351
  The JSON file is mainly a **cold-storage fallback**. Between detaches the
196
- live session lives inside the running server process, so `Ctrl-a d` then
197
- `bin/muxr <name>` reattaches to the exact same shells with their full
198
- history. The JSON only matters once the server is gone (after `Ctrl-a q`
199
- or a reboot): re-launching `muxr <name>` rebuilds pane and drawer shells
352
+ live session lives inside the running server process, so `d` (normal) /
353
+ `C-a d` (passthrough) then `bin/muxr <name>` reattaches to the exact
354
+ same shells with their full history. The JSON only matters once the
355
+ server is gone (after `q` / `C-a q` or a reboot): re-launching
356
+ `muxr <name>` rebuilds pane and drawer shells
200
357
  using the saved working directories. Shell command history within those
201
358
  panes is **not** persisted — that's the job of your shell's own history
202
359
  file. Run `:save` from inside muxr to write the snapshot.
@@ -205,28 +362,32 @@ file. Run `:save` from inside muxr to write the snapshot.
205
362
 
206
363
  ```bash
207
364
  bundle install # only minitest and rake
208
- rake test # full suite (100+ unit tests)
365
+ rake test # full suite (200+ unit tests)
209
366
 
210
367
  # Run a single file or test
211
368
  ruby -Ilib -Itest test/test_layout_manager.rb
212
369
  ruby -Ilib -Itest test/test_terminal.rb -n test_csi_cursor_position
213
370
  ```
214
371
 
215
- Tests cover the layout algorithms, drawer state machine, window pane
216
- ordering, session JSON round-trip, the client/server framing protocol,
217
- the input-handler state machine (including scrollback and selection
218
- modes), the renderer's diff-emit, and the VT100 emulator's cursor
219
- movement, SGR (including colon-subparameter and underline-color forms),
220
- erase, scroll-region, and autowrap handling. PTY-dependent code paths
221
- are exercised via dependency injection so tests don't spawn shells.
372
+ Tests cover the layout algorithms (including spatial neighbor lookup
373
+ for `hjkl`), drawer state machine, window pane ordering, session JSON
374
+ round-trip, the client/server framing protocol, the input-handler
375
+ state machine (normal/passthrough mode transitions, scrollback,
376
+ selection), foreground-command parsing (Linux `/proc` stat format and
377
+ shell-filter rules), the renderer's diff-emit, and the VT100
378
+ emulator's cursor movement, SGR (including colon-subparameter and
379
+ underline-color forms), erase, scroll-region, and autowrap handling.
380
+ PTY-dependent code paths are exercised via dependency injection so
381
+ tests don't spawn shells.
222
382
 
223
383
  On-disk layout:
224
384
 
225
385
  ```
226
386
  ~/.muxr/
227
- ├─ sessions/<name>.json structural snapshot written by `:save`
228
- ├─ sockets/<name>.sock server's Unix listener (auto-managed)
229
- └─ logs/<name>.log server stdout/stderr
387
+ ├─ sessions/<name>.json structural snapshot written by `:save`
388
+ ├─ sockets/<name>.sock TTY client listener (auto-managed)
389
+ ├─ sockets/<name>.ctrl.sock MCP control listener (auto-managed)
390
+ └─ logs/<name>.log server stdout/stderr
230
391
  ```
231
392
 
232
393
  ## Contributing