muxr 0.1.4 → 0.1.6
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 +4 -4
- data/CHANGELOG.md +111 -1
- data/README.md +210 -65
- data/bin/muxr +61 -0
- data/bin/muxr-mcp +412 -0
- data/lib/muxr/application.rb +276 -26
- data/lib/muxr/command_dispatcher.rb +2 -0
- data/lib/muxr/control_server.rb +670 -0
- data/lib/muxr/drawer.rb +9 -2
- data/lib/muxr/foreground_command.rb +86 -0
- data/lib/muxr/input_handler.rb +133 -27
- data/lib/muxr/key_parser.rb +89 -0
- data/lib/muxr/layout_manager.rb +59 -0
- data/lib/muxr/pane.rb +60 -3
- data/lib/muxr/renderer.rb +145 -33
- data/lib/muxr/session.rb +13 -2
- data/lib/muxr/terminal.rb +81 -2
- data/lib/muxr/version.rb +1 -1
- data/lib/muxr.rb +2 -0
- data/muxr.gemspec +3 -1
- data/skills/muxr-control/SKILL.md +190 -0
- metadata +7 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: muxr-control
|
|
3
|
+
description: |
|
|
4
|
+
Use when driving a muxr terminal session — running commands across panes,
|
|
5
|
+
watching long-running processes, capturing terminal output, setting up
|
|
6
|
+
layouts, or working with the muxr drawer. Triggers when MUXR_SESSION is
|
|
7
|
+
set in the environment, or when the user asks to "run X in pane Y",
|
|
8
|
+
"what does pane N show", "switch the muxr layout", etc.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# muxr-control
|
|
12
|
+
|
|
13
|
+
You're driving a [muxr](https://github.com/roelbondoc/muxr) terminal
|
|
14
|
+
multiplexer session through its MCP bridge. muxr is a tiling terminal
|
|
15
|
+
multiplexer (think tmux + xmonad). Each pane is a real shell PTY; you can
|
|
16
|
+
read its current screen contents, send keystrokes, and wait for output to
|
|
17
|
+
settle — without taking control of the user's keyboard.
|
|
18
|
+
|
|
19
|
+
## First thing: ground yourself
|
|
20
|
+
|
|
21
|
+
Before doing anything else, call **`muxr_session_get`** and
|
|
22
|
+
**`muxr_panes_list`**. These are cheap, idempotent reads. They tell you:
|
|
23
|
+
|
|
24
|
+
- The session name, layout (tall / grid / monocle), and current dimensions.
|
|
25
|
+
- Each pane's stable id (6 hex chars, e.g. `a3f9b2`), its 1-based slot
|
|
26
|
+
number as shown on screen (`#1`, `#2`, …), its cwd, and whether it's the
|
|
27
|
+
focused or master pane.
|
|
28
|
+
- The `focused_pane` field in `session.get` tells you which pane the user
|
|
29
|
+
was last looking at — if the user just said "run X" without naming a
|
|
30
|
+
pane, that's the natural target.
|
|
31
|
+
|
|
32
|
+
## Pane identity: **always use the id, never the slot**
|
|
33
|
+
|
|
34
|
+
The status bar shows panes as `#1 a3f9b2`, `#2 c2e810`, etc. The number is
|
|
35
|
+
a *slot* — purely positional and tied to where the pane sits in the array.
|
|
36
|
+
The hex string is the *id* — generated once at pane creation and stable
|
|
37
|
+
forever.
|
|
38
|
+
|
|
39
|
+
Slots shift when panes are created, killed, or promoted to master. The id
|
|
40
|
+
never moves. **Every tool call that names a pane should pass the id.** If
|
|
41
|
+
the user says "the second pane", look it up in `muxr_panes_list` and pass
|
|
42
|
+
the id you find at slot 2 — don't pass `2` directly even though it works,
|
|
43
|
+
because by the time the call lands the slots may have changed.
|
|
44
|
+
|
|
45
|
+
## Recipes
|
|
46
|
+
|
|
47
|
+
### Run a command and get its output
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
muxr_pane_run({ "pane": "a3f9b2", "input": "ls -la" })
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This sends `ls -la\r` to pane `a3f9b2`, waits for the PTY to go idle (no
|
|
54
|
+
output for 500ms by default), and returns the pane's full visible text
|
|
55
|
+
plus a `timed_out` flag.
|
|
56
|
+
|
|
57
|
+
**Always prefer `muxr_pane_run` over `muxr_pane_send_input` + a separate
|
|
58
|
+
`muxr_pane_read`.** The split version races — the read can fire before
|
|
59
|
+
the shell has redrawn the prompt, and you'll miss the output entirely.
|
|
60
|
+
|
|
61
|
+
### Tune `idle_ms` for the kind of command
|
|
62
|
+
|
|
63
|
+
- **Fast, simple commands** (`pwd`, `git status`): default 500ms is fine.
|
|
64
|
+
- **Bursty output** (test runners, builds): bump to `idle_ms: 800` or
|
|
65
|
+
`1000`. Test runners often pause briefly between phases; a too-short
|
|
66
|
+
idle window cuts off mid-run.
|
|
67
|
+
- **Interactive REPLs** that you want to type into without waiting for
|
|
68
|
+
completion: use `muxr_pane_send_input` directly with `append_enter:
|
|
69
|
+
false` — don't try to detect idleness on a REPL.
|
|
70
|
+
- **Long builds** (npm install, cargo build): bump `timeout_ms` to
|
|
71
|
+
`120000` or higher. Default is 30s.
|
|
72
|
+
|
|
73
|
+
### Wait without sending anything
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
muxr_pane_run({ "pane": "a3f9b2", "input": "", "append_enter": false,
|
|
77
|
+
"idle_ms": 1000, "timeout_ms": 30000 })
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Useful when the user has already typed a command and you want to capture
|
|
81
|
+
its output once it finishes.
|
|
82
|
+
|
|
83
|
+
### Send multi-line input (paste mode)
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
muxr_pane_send_input({
|
|
87
|
+
"pane": "a3f9b2",
|
|
88
|
+
"data": "def hello\n puts :world\nend\n",
|
|
89
|
+
"bracketed": true
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`bracketed: true` wraps the data in `\e[200~` / `\e[201~` so editors and
|
|
94
|
+
REPLs treat it as a single paste rather than N separate keystrokes (which
|
|
95
|
+
fires their auto-indent / autocomplete on every line).
|
|
96
|
+
|
|
97
|
+
### Look at the drawer without opening it
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
muxr_drawer_read({})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The drawer's shell process keeps running while hidden — its scrollback
|
|
104
|
+
survives. You can read it any time without disturbing the user's view.
|
|
105
|
+
|
|
106
|
+
### Set up a layout for a task
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
muxr_layout_set({ "layout": "tall" })
|
|
110
|
+
muxr_pane_new({}) // create a second pane
|
|
111
|
+
muxr_pane_send_input({ "pane": "<new id>", "data": "npm run dev\n" })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Avoid doing this unsolicited — the human owns the layout. Only restructure
|
|
115
|
+
when the user explicitly asks ("set up a dev environment", "split this
|
|
116
|
+
into 3 panes").
|
|
117
|
+
|
|
118
|
+
## Gotchas
|
|
119
|
+
|
|
120
|
+
### Reading is cheap. Writing is destructive.
|
|
121
|
+
|
|
122
|
+
`muxr_pane_read`, `muxr_panes_list`, `muxr_drawer_read`, and
|
|
123
|
+
`muxr_session_get` have zero side effects — call them whenever you need
|
|
124
|
+
to ground yourself. **Mutating tools** (`muxr_pane_send_input`,
|
|
125
|
+
`muxr_pane_run`, `muxr_pane_kill`, `muxr_layout_set`, …) affect the
|
|
126
|
+
user's live session. Before calling any of them:
|
|
127
|
+
|
|
128
|
+
- Confirm the user named the specific pane you're about to act on (or
|
|
129
|
+
agreed implicitly by saying "run X here").
|
|
130
|
+
- Double-check the id by reading `muxr_panes_list` if you haven't done so
|
|
131
|
+
recently.
|
|
132
|
+
- **Never `muxr_pane_kill`** without the user explicitly saying "close
|
|
133
|
+
pane X" — a pane often holds in-progress work that's not in any file.
|
|
134
|
+
|
|
135
|
+
### `pane.read` returns *visible* text only
|
|
136
|
+
|
|
137
|
+
The result is the pane's current 80×24-or-whatever grid, with trailing
|
|
138
|
+
whitespace trimmed per row. Lines that have scrolled into scrollback are
|
|
139
|
+
not in the response. If you need older output, ask the user to scroll
|
|
140
|
+
the pane up first (they have `Ctrl-a [` for scrollback mode), or watch
|
|
141
|
+
the pane via `muxr_pane_run` while the command is running.
|
|
142
|
+
|
|
143
|
+
### Private panes
|
|
144
|
+
|
|
145
|
+
The user can mark any pane *private* with `Ctrl-a P` (status bar shows
|
|
146
|
+
`[P]` after the pane id). Private panes appear in `muxr_panes_list` with
|
|
147
|
+
`"private": true` and *no* `cwd`/`rows`/`cols` — `muxr_pane_read`,
|
|
148
|
+
`muxr_pane_send_input`, `muxr_pane_run`, `muxr_pane_subscribe`, and
|
|
149
|
+
`muxr_pane_kill` all refuse with an error message that tells you the
|
|
150
|
+
human-side gesture to undo it.
|
|
151
|
+
|
|
152
|
+
When this happens: **do not retry**. Surface it to the user verbatim
|
|
153
|
+
("pane #2 a3f9b2 is private; press Ctrl-a P on it to expose it to me").
|
|
154
|
+
The privacy flag is intentionally one-way from MCP's perspective: there
|
|
155
|
+
is no `muxr_pane_unmark_private` tool.
|
|
156
|
+
|
|
157
|
+
`muxr_pane_focus` and `muxr_pane_promote` still work on private panes
|
|
158
|
+
(they're layout ops, not content ops) — useful if the user asks to
|
|
159
|
+
"bring my private pane to the front" without exposing it.
|
|
160
|
+
|
|
161
|
+
### The drawer might be Claude itself
|
|
162
|
+
|
|
163
|
+
If the bridge sees the env var `MUXR_DRAWER_SELF=1` it refuses
|
|
164
|
+
`muxr_drawer_*` methods — that means the bridge is running *inside* the
|
|
165
|
+
muxr drawer and the call would recurse into your own pty. If you get
|
|
166
|
+
that error, that's why: you can still drive the surrounding tiled panes
|
|
167
|
+
normally, you just can't toggle/read the drawer that's hosting you.
|
|
168
|
+
|
|
169
|
+
### Don't toggle the drawer just to peek
|
|
170
|
+
|
|
171
|
+
`muxr_drawer_read` works without showing the drawer. Toggling it to
|
|
172
|
+
look, then toggling back, is visible to the user as a flash of overlay
|
|
173
|
+
and is almost never what they wanted.
|
|
174
|
+
|
|
175
|
+
### Tool errors
|
|
176
|
+
|
|
177
|
+
If a tool call returns `isError: true`, the text usually starts with
|
|
178
|
+
`muxr error <code>: <message>`. Common ones:
|
|
179
|
+
|
|
180
|
+
- `muxr error -32602: pane: no pane with id "…"` — the pane has been
|
|
181
|
+
killed, or you passed a stale id from before a kill/promote. Refetch
|
|
182
|
+
`muxr_panes_list`.
|
|
183
|
+
- `muxr error -32602: layout: unknown layout` — valid layouts are
|
|
184
|
+
`tall`, `grid`, `monocle`.
|
|
185
|
+
|
|
186
|
+
## Naming muxr in conversation
|
|
187
|
+
|
|
188
|
+
When responding to the user, call panes by **slot first, id second**:
|
|
189
|
+
"pane #2 (a3f9b2) is showing the test failures." That matches what's on
|
|
190
|
+
their status bar and makes the id available for follow-up references.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: muxr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roel Bondoc
|
|
@@ -46,6 +46,7 @@ email:
|
|
|
46
46
|
- rsbondoc@gmail.com
|
|
47
47
|
executables:
|
|
48
48
|
- muxr
|
|
49
|
+
- muxr-mcp
|
|
49
50
|
extensions: []
|
|
50
51
|
extra_rdoc_files: []
|
|
51
52
|
files:
|
|
@@ -53,12 +54,16 @@ files:
|
|
|
53
54
|
- LICENSE.txt
|
|
54
55
|
- README.md
|
|
55
56
|
- bin/muxr
|
|
57
|
+
- bin/muxr-mcp
|
|
56
58
|
- lib/muxr.rb
|
|
57
59
|
- lib/muxr/application.rb
|
|
58
60
|
- lib/muxr/client.rb
|
|
59
61
|
- lib/muxr/command_dispatcher.rb
|
|
62
|
+
- lib/muxr/control_server.rb
|
|
60
63
|
- lib/muxr/drawer.rb
|
|
64
|
+
- lib/muxr/foreground_command.rb
|
|
61
65
|
- lib/muxr/input_handler.rb
|
|
66
|
+
- lib/muxr/key_parser.rb
|
|
62
67
|
- lib/muxr/layout_manager.rb
|
|
63
68
|
- lib/muxr/pane.rb
|
|
64
69
|
- lib/muxr/protocol.rb
|
|
@@ -69,6 +74,7 @@ files:
|
|
|
69
74
|
- lib/muxr/version.rb
|
|
70
75
|
- lib/muxr/window.rb
|
|
71
76
|
- muxr.gemspec
|
|
77
|
+
- skills/muxr-control/SKILL.md
|
|
72
78
|
homepage: https://github.com/roelbondoc/muxr
|
|
73
79
|
licenses:
|
|
74
80
|
- MIT
|