muxr 0.1.11 → 0.2.0

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.
data/lib/muxr/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Muxr
2
- VERSION = "0.1.11"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,121 @@
1
+ module Muxr
2
+ # Measures how the *outer* terminal actually draws glyphs whose width muxr
3
+ # would otherwise have to guess, so the emulator and Renderer can be tuned to
4
+ # match it instead of disagreeing (the disagreement is what corrupts in-place
5
+ # animations — see Terminal::AMBIGUOUS_RANGES and Renderer#contiguous_after?).
6
+ #
7
+ # The technique is the classic cursor-position probe: print a test glyph at a
8
+ # known column, ask the terminal where its cursor landed with DSR-CPR
9
+ # (`\e[6n` → `\e[<row>;<col>R`), and the column delta is the glyph's real
10
+ # display width. This only works against a live TTY in raw mode, which is why
11
+ # it runs in the client (the piece that owns the terminal) and ships its
12
+ # verdict to the server in the HELLO handshake.
13
+ module WidthProbe
14
+ # Formal East Asian Ambiguous glyphs (all > U+0300 so the class toggle can
15
+ # actually reach them). A terminal in "ambiguous = wide" mode draws all of
16
+ # these two columns wide; a narrow terminal draws them one. We sample several
17
+ # and take a majority vote — the verdict configures Terminal.ambiguous_wide,
18
+ # which covers the long tail of ambiguous glyphs we don't sample by hand.
19
+ AMBIGUOUS_SAMPLES = ["…", "●", "→", "★", "◆"].freeze
20
+
21
+ # Specific glyphs whose width no Unicode class reliably predicts because a
22
+ # font may give them emoji presentation (drawn two wide) regardless of the
23
+ # terminal's ambiguous setting. These are exactly the glyphs Claude Code's
24
+ # UI animates in place. We measure each one individually and record its
25
+ # exact width as a per-codepoint override — ground truth beats any heuristic.
26
+ GLYPH_SAMPLES = ["⏺", "✻", "❯", "✦", "✳", "◼", "▪"].freeze
27
+
28
+ # Box-drawing and block elements (Terminal::BOX_RANGES). Formally Ambiguous,
29
+ # but many terminals keep them narrow even in ambiguous-as-wide mode, so the
30
+ # AMBIGUOUS_SAMPLES verdict can't speak for them. They need their own vote
31
+ # because Renderer#contiguous_after? relies on this band being narrow to skip
32
+ # cursor repositioning — if the terminal actually draws them wide, every
33
+ # glyph after one on the same line lands a column off, which is what mangles
34
+ # a border-heavy TUI like Claude Code.
35
+ BOX_SAMPLES = ["─", "│", "█", "▌"].freeze
36
+
37
+ # Overall wall-clock budget for the whole probe. A terminal that never
38
+ # answers DSR (rare, but possible over flaky ttys / odd emulators) must not
39
+ # wedge attach — we give up and fall back to defaults.
40
+ TIMEOUT = 0.3
41
+
42
+ # Probe the terminal reachable via +out+ (writable) and +input+ (readable),
43
+ # which must already be in raw mode. Returns a capabilities hash suitable
44
+ # for Protocol.encode_caps, e.g. {ambiguous: 2, glyphs: {0x23FA => 2}}.
45
+ # Returns {} when the terminal doesn't answer (callers treat that as "use
46
+ # defaults").
47
+ def self.run(out: $stdout, input: $stdin, timeout: TIMEOUT)
48
+ deadline = now + timeout
49
+ caps = {}
50
+
51
+ amb = AMBIGUOUS_SAMPLES.filter_map { |g| measure(g, out, input, deadline) }
52
+ # No answers at all → terminal doesn't speak CPR; don't claim to know.
53
+ return caps if amb.empty?
54
+ wide = amb.count { |w| w >= 2 }
55
+ caps[:ambiguous] = wide > (amb.length - wide) ? 2 : 1
56
+
57
+ box = BOX_SAMPLES.filter_map { |g| measure(g, out, input, deadline) }
58
+ unless box.empty?
59
+ box_wide = box.count { |w| w >= 2 }
60
+ caps[:box] = box_wide > (box.length - box_wide) ? 2 : 1
61
+ end
62
+
63
+ glyphs = {}
64
+ GLYPH_SAMPLES.each do |g|
65
+ w = measure(g, out, input, deadline)
66
+ # Clamp to the 1/2 the grid understands; ignore non-answers and any
67
+ # zero-advance oddity (an unrenderable glyph the terminal swallowed).
68
+ glyphs[g.ord] = (w >= 2 ? 2 : 1) if w && w >= 1
69
+ end
70
+ caps[:glyphs] = glyphs unless glyphs.empty?
71
+ caps
72
+ ensure
73
+ # Wipe whatever the probe painted; the server's first frame repaints all.
74
+ out.write("\e[H\e[2J")
75
+ out.flush
76
+ end
77
+
78
+ # Print +glyph+ at column 1 of the home row, then read the cursor column.
79
+ # Returns the glyph's display width (col - 1) or nil if no CPR came back in
80
+ # time. Latin-1 / ASCII control bytes in the glyph would skew the result,
81
+ # so callers pass only printable test glyphs.
82
+ def self.measure(glyph, out, input, deadline)
83
+ out.write("\e[H#{glyph}\e[6n")
84
+ out.flush
85
+ col = read_cpr_col(input, deadline)
86
+ return nil unless col
87
+ col - 1
88
+ end
89
+
90
+ # Read a DSR-CPR reply (`\e[<row>;<col>R`, optionally `\e[?<row>;<col>R`)
91
+ # from +input+, honoring the shared deadline. Bytes that aren't part of the
92
+ # reply (none are expected this early in attach) are discarded. Returns the
93
+ # column, or nil on timeout / closed input.
94
+ def self.read_cpr_col(input, deadline)
95
+ buf = +"".b
96
+ loop do
97
+ remaining = deadline - now
98
+ return nil if remaining <= 0
99
+ ready, = IO.select([input], nil, nil, remaining)
100
+ return nil unless ready
101
+ begin
102
+ chunk = input.read_nonblock(64)
103
+ rescue IO::WaitReadable
104
+ next
105
+ rescue EOFError, IOError, Errno::EIO
106
+ return nil
107
+ end
108
+ buf << chunk
109
+ if (m = buf.match(/\e\[\??\d+;(\d+)R/))
110
+ return m[1].to_i
111
+ end
112
+ # Guard against a stream that never contains a terminator.
113
+ return nil if buf.bytesize > 256
114
+ end
115
+ end
116
+
117
+ def self.now
118
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
119
+ end
120
+ end
121
+ end
data/lib/muxr/window.rb CHANGED
@@ -5,8 +5,10 @@ module Muxr
5
5
  class Window
6
6
  LAYOUTS = LayoutManager::LAYOUTS
7
7
 
8
- attr_accessor :name, :layout, :master_index
9
- attr_reader :panes, :focused_index
8
+ RATIO_STEP = 0.05
9
+
10
+ attr_accessor :name, :layout, :master_index, :synchronized
11
+ attr_reader :panes, :focused_index, :master_ratio, :master_count
10
12
 
11
13
  def initialize(name: "main")
12
14
  @name = name
@@ -14,7 +16,30 @@ module Muxr
14
16
  @focused_index = 0
15
17
  @last_focused_pane = nil
16
18
  @master_index = 0
17
- @layout = :spiral
19
+ @layout = :auto
20
+ @master_ratio = LayoutManager::DEFAULT_RATIO
21
+ @master_count = 1
22
+ @synchronized = false
23
+ end
24
+
25
+ def master_ratio=(value)
26
+ @master_ratio = value.to_f.clamp(LayoutManager::RATIO_BOUNDS).round(2)
27
+ end
28
+
29
+ def master_count=(value)
30
+ @master_count = [value.to_i, 1].max
31
+ end
32
+
33
+ def adjust_master_ratio(delta)
34
+ self.master_ratio = @master_ratio + delta
35
+ end
36
+
37
+ def adjust_master_count(delta)
38
+ self.master_count = (@master_count + delta).clamp(1, [@panes.length, 1].max)
39
+ end
40
+
41
+ def layout_options
42
+ { master_index: @master_index, ratio: @master_ratio, nmaster: @master_count }
18
43
  end
19
44
 
20
45
  # Setter records the outgoing focused pane (by reference) so focus_last can
@@ -100,6 +125,7 @@ module Muxr
100
125
  end
101
126
 
102
127
  def cycle_layout
128
+ @zoom_return = nil
103
129
  i = LAYOUTS.index(@layout) || 0
104
130
  @layout = LAYOUTS[(i + 1) % LAYOUTS.length]
105
131
  end
@@ -107,9 +133,30 @@ module Muxr
107
133
  def set_layout(layout)
108
134
  layout = layout.to_sym
109
135
  raise ArgumentError, "Unknown layout: #{layout}" unless LAYOUTS.include?(layout)
136
+ @zoom_return = nil
110
137
  @layout = layout
111
138
  end
112
139
 
140
+ def zoomed?
141
+ !@zoom_return.nil?
142
+ end
143
+
144
+ attr_reader :zoom_return
145
+
146
+ def toggle_zoom
147
+ if zoomed?
148
+ @layout = @zoom_return
149
+ @zoom_return = nil
150
+ elsif @layout != :monocle
151
+ @zoom_return = @layout
152
+ @layout = :monocle
153
+ end
154
+ end
155
+
156
+ def resting_layout
157
+ @zoom_return || @layout
158
+ end
159
+
113
160
  def clamp_indices!
114
161
  if @panes.empty?
115
162
  @focused_index = 0
data/lib/muxr.rb CHANGED
@@ -1,16 +1,24 @@
1
1
  require_relative "muxr/version"
2
2
  require_relative "muxr/pty_process"
3
+ require_relative "muxr/image_store"
4
+ require_relative "muxr/mouse_report"
3
5
  require_relative "muxr/terminal"
6
+ require_relative "muxr/remote_pane"
4
7
  require_relative "muxr/pane"
8
+ require_relative "muxr/pane_transfer"
5
9
  require_relative "muxr/drawer"
6
10
  require_relative "muxr/foreground_command"
7
11
  require_relative "muxr/layout_manager"
8
12
  require_relative "muxr/window"
9
13
  require_relative "muxr/session"
14
+ require_relative "muxr/session_directory"
15
+ require_relative "muxr/pane_picker"
10
16
  require_relative "muxr/renderer"
11
17
  require_relative "muxr/input_handler"
12
18
  require_relative "muxr/command_dispatcher"
19
+ require_relative "muxr/config"
13
20
  require_relative "muxr/protocol"
21
+ require_relative "muxr/width_probe"
14
22
  require_relative "muxr/control_server"
15
23
  require_relative "muxr/application"
16
24
  require_relative "muxr/client"
data/muxr.gemspec CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.required_ruby_version = ">= 3.4"
19
19
 
20
20
  spec.metadata = {
21
+ "homepage_uri" => "https://roelbondoc.github.io/muxr/",
21
22
  "source_code_uri" => "https://github.com/roelbondoc/muxr",
22
23
  "bug_tracker_uri" => "https://github.com/roelbondoc/muxr/issues",
23
24
  "changelog_uri" => "https://github.com/roelbondoc/muxr/blob/main/CHANGELOG.md",
@@ -39,6 +40,15 @@ Gem::Specification.new do |spec|
39
40
  ]
40
41
  spec.require_paths = ["lib"]
41
42
 
43
+ spec.post_install_message = <<~MSG
44
+ muxr ships a Claude Code skill for driving sessions over MCP. To install it
45
+ into ~/.claude/skills and print the `claude mcp add` line for the bridge:
46
+
47
+ muxr --install-skill
48
+
49
+ Re-run it after each `gem update muxr` to refresh the skill contents.
50
+ MSG
51
+
42
52
  spec.add_development_dependency "minitest", "~> 5.0"
43
53
  spec.add_development_dependency "rake", "~> 13.0"
44
54
  end
@@ -3,9 +3,11 @@ name: muxr-control
3
3
  description: |
4
4
  Use when driving a muxr terminal session — running commands across panes,
5
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.
6
+ layouts, working with the muxr drawer, or driving a full-screen TUI app
7
+ (vim, a notebook, lazygit) that is already running inside a pane.
8
+ Triggers when MUXR_SESSION is set in the environment, or when the user
9
+ asks to "run X in pane Y", "what does pane N show", "drive the editor in
10
+ pane 1", "switch the muxr layout", etc.
9
11
  ---
10
12
 
11
13
  # muxr-control
@@ -21,7 +23,8 @@ settle — without taking control of the user's keyboard.
21
23
  Before doing anything else, call **`muxr_session_get`** and
22
24
  **`muxr_panes_list`**. These are cheap, idempotent reads. They tell you:
23
25
 
24
- - The session name, layout (tall / grid / monocle), and current dimensions.
26
+ - The session name, current layout, the full `available_layouts` list, and
27
+ the session dimensions.
25
28
  - Each pane's stable id (6 hex chars, e.g. `a3f9b2`), its 1-based slot
26
29
  number as shown on screen (`#1`, `#2`, …), its cwd, and whether it's the
27
30
  focused or master pane.
@@ -42,6 +45,13 @@ the user says "the second pane", look it up in `muxr_panes_list` and pass
42
45
  the id you find at slot 2 — don't pass `2` directly even though it works,
43
46
  because by the time the call lands the slots may have changed.
44
47
 
48
+ A pane the human has named with `:rename` shows the name in place of the id
49
+ (`#1 api`), and `muxr_panes_list` reports it as `name`. When the user refers
50
+ to a pane by that name ("run the tests in `api`"), you may pass the name as
51
+ `pane`. A name that is ambiguous, or that has since been changed, is refused
52
+ rather than guessed, so the id is still the safer choice for any call you
53
+ make after a list.
54
+
45
55
  ## Recipes
46
56
 
47
57
  ### Run a command and get its output
@@ -70,7 +80,7 @@ the shell has redrawn the prompt, and you'll miss the output entirely.
70
80
  - **Long builds** (npm install, cargo build): bump `timeout_ms` to
71
81
  `120000` or higher. Default is 30s.
72
82
 
73
- ### Wait without sending anything
83
+ ### Wait for something already running
74
84
 
75
85
  ```
76
86
  muxr_pane_run({ "pane": "a3f9b2", "input": "", "append_enter": false,
@@ -78,7 +88,19 @@ muxr_pane_run({ "pane": "a3f9b2", "input": "", "append_enter": false,
78
88
  ```
79
89
 
80
90
  Useful when the user has already typed a command and you want to capture
81
- its output once it finishes.
91
+ its output once it finishes — but **only if output is still coming.**
92
+
93
+ The idle timer is gated on having seen output at all: `pane.run` resolves
94
+ early only once the pane has emitted *something* and then gone quiet. On a
95
+ pane that stays silent for the whole wait, the only exit is the deadline,
96
+ so the call blocks for the full `timeout_ms` and returns
97
+ `timed_out: true` — which looks like a failure but just means "nothing
98
+ happened." A 30s timeout on an already-finished command costs you 30s.
99
+
100
+ So for a pane that may already be quiet, **poll with `muxr_pane_read`
101
+ instead** — it returns instantly, has no side effects, and re-reading a
102
+ few times is cheaper than one mis-sized wait. Reserve `pane.run` for input
103
+ you expect to produce output.
82
104
 
83
105
  ### Send multi-line input (paste mode)
84
106
 
@@ -115,6 +137,79 @@ Avoid doing this unsolicited — the human owns the layout. Only restructure
115
137
  when the user explicitly asks ("set up a dev environment", "split this
116
138
  into 3 panes").
117
139
 
140
+ ## Driving a full-screen TUI app
141
+
142
+ A pane may hold a full-screen application (vim, euporie, lazygit, htop, a
143
+ TUI notebook) rather than a shell prompt. Everything below is about those;
144
+ shells are more forgiving.
145
+
146
+ ### One key per call. Never batch repeats.
147
+
148
+ Both `pane.send_input` and `pane.run` concatenate the entire `keys` array
149
+ into one payload and hand it to the PTY in a **single write**, with no
150
+ pacing between keys. A shell's line editor handles that fine. An app that
151
+ kicks off async work per keypress often does not: sending
152
+ `["<c-r>", "<c-r>", … ]` nine times to a TUI notebook produced **three**
153
+ actions, not nine — the rest were swallowed while the app was mid-render.
154
+ Sending the same key one call at a time worked every time.
155
+
156
+ Batching is safe for a *heterogeneous* scripted sequence where each key
157
+ does something different and cheap:
158
+
159
+ ```
160
+ muxr_pane_send_input({ "pane": "a3f9b2",
161
+ "keys": ["G", "o", "hello world", "<esc>", ":w", "<enter>"] })
162
+ ```
163
+
164
+ Batching is **not** safe for "do this N times." Loop the call instead, and
165
+ confirm the app actually advanced between iterations (see below).
166
+
167
+ ### The status bar is ground truth, not the layout
168
+
169
+ Infer app state from whatever the app *prints* about itself — the status
170
+ line, a mode indicator, an execution counter — never from where borders or
171
+ highlights appear to be drawn. Box-drawing and reverse-video regions are
172
+ easy to misread, and the cell/buffer/pane you think is selected is
173
+ routinely not the one you think. If the app tells you `Cell 9`, believe
174
+ that over a box that looks like it surrounds cell 8.
175
+
176
+ This is also how you verify the previous point: read the indicator, send
177
+ one key, re-read, confirm it moved.
178
+
179
+ ### Use the `cursor` field to find focus in a dialog
180
+
181
+ `pane.read` and `pane.run` both return `cursor: {row, col}`. In a modal
182
+ dialog from prompt_toolkit, ncurses, and friends, the cursor parks on the
183
+ **focused widget** — so it is the reliable way to tell which of
184
+ `[ Yes ] [ No ] [ Cancel ]` is armed, when the rendered text gives you
185
+ nothing to go on.
186
+
187
+ The safe gesture for any dialog you did not expect:
188
+
189
+ 1. `pane_read` — note `cursor`.
190
+ 2. Send one `<tab>` or `<right>`.
191
+ 3. Re-read and confirm the cursor **moved**.
192
+ 4. Only then press `<enter>`.
193
+
194
+ Use cursor **deltas**, never absolute column arithmetic. `text` is trimmed
195
+ of trailing whitespace per row, and double-width glyphs (box-drawing,
196
+ block elements, emoji — heavily used by TUI dialogs) desync any attempt to
197
+ map a `col` onto an index into the row string. Two adjacent buttons in one
198
+ real dialog reported cols 63 and 75.
199
+
200
+ ### Scroll with the app's own keys, not muxr scrollback
201
+
202
+ `pane.read` is viewport-only, and a full-screen app *owns* its viewport: it
203
+ paints one screenful and keeps the rest in its own internal buffer. Content
204
+ below the fold is invisible to `pane.read` **and** absent from muxr's
205
+ scrollback, because it was never emitted as scrolled-off terminal output.
206
+ `Ctrl-a [` will not find it.
207
+
208
+ So to see the rest, drive the app's own scroll binding (euporie `]`/`}`,
209
+ vim `Ctrl-d`, less `space`) and re-read. Corollary: a command's output can
210
+ be sitting in the pane, already complete, and still absent from your last
211
+ read — check the app's own indicator before concluding a step didn't run.
212
+
118
213
  ## Gotchas
119
214
 
120
215
  ### Reading is cheap. Writing is destructive.
@@ -140,6 +235,9 @@ not in the response. If you need older output, ask the user to scroll
140
235
  the pane up first (they have `Ctrl-a [` for scrollback mode), or watch
141
236
  the pane via `muxr_pane_run` while the command is running.
142
237
 
238
+ If the pane holds a full-screen app, `Ctrl-a [` won't help either — see
239
+ "Scroll with the app's own keys" above.
240
+
143
241
  ### Private panes
144
242
 
145
243
  The user can mark any pane *private* with `Ctrl-a P` (status bar shows
@@ -158,6 +256,41 @@ is no `muxr_pane_unmark_private` tool.
158
256
  (they're layout ops, not content ops) — useful if the user asks to
159
257
  "bring my private pane to the front" without exposing it.
160
258
 
259
+ ### Borrowed panes belong to another session
260
+
261
+ A pane the user attached from another muxr session (`A` / `C-a A`) shows
262
+ up in `muxr_panes_list` with an `"origin"` of `"<session>:<pane id>"`,
263
+ and in the pane title as `@work:6021b5`. It behaves like any other pane
264
+ for reads and input — those reach the real shell, in the session that
265
+ owns it — with two differences worth knowing:
266
+
267
+ - `muxr_pane_kill` only detaches the mirror. The shell keeps running in
268
+ its home session. If the user asks you to kill it, say that's what
269
+ happened rather than reporting the process gone.
270
+ - It can vanish without dying: if the owning session goes away, or the
271
+ pane is moved somewhere else, it disappears from `muxr_panes_list`.
272
+ Re-read the list rather than assuming a stale id.
273
+ - Its size is negotiated with the owner, so `muxr_layout_set` may not
274
+ give it the dimensions you'd expect from the layout alone.
275
+
276
+ Anything you type there is visible to whoever is looking at the owning
277
+ session, live. Treat it the way you'd treat a shared screen.
278
+
279
+ A pane the user *moved* here (rather than shared) has no `origin` — it is
280
+ an ordinary local pane, even though the shell inside it has been running
281
+ since before it arrived. Don't assume a pane's history started in this
282
+ session.
283
+
284
+ ### You are running in one of these panes
285
+
286
+ `MUXR_PANE` in your environment is the id of the pane hosting you. The
287
+ bridge refuses `muxr_pane_read`, `muxr_pane_send_input`, `muxr_pane_run`
288
+ and `muxr_pane_kill` on that id — driving your own pty feeds your output
289
+ back to you, and reading it just returns your own UI. Check `MUXR_PANE`
290
+ before picking a target, and don't try to route around the refusal by
291
+ re-running the command in a pane you then read; ask the user to open
292
+ another pane if you need somewhere to work.
293
+
161
294
  ### The drawer might be Claude itself
162
295
 
163
296
  If the bridge sees the env var `MUXR_DRAWER_SELF=1` it refuses
@@ -166,6 +299,21 @@ muxr drawer and the call would recurse into your own pty. If you get
166
299
  that error, that's why: you can still drive the surrounding tiled panes
167
300
  normally, you just can't toggle/read the drawer that's hosting you.
168
301
 
302
+ ### The session you reach may not be the one in your env
303
+
304
+ A pane can be moved between sessions while its shell keeps running, so
305
+ `MUXR_SESSION` can name a session you have since left. The bridge follows
306
+ `MUXR_PANE` to whichever server actually owns you and re-checks every ten
307
+ seconds — so trust `muxr_session_get` over the environment if the two
308
+ disagree, and re-list panes rather than reusing ids you cached from a
309
+ session you are no longer in.
310
+
311
+ ### No tools at all means no session
312
+
313
+ If `muxr_*` tools aren't listed, this claude isn't inside muxr (or its
314
+ server isn't running). The bridge advertises nothing rather than failing
315
+ to start. Nothing to fix — just don't claim you can drive panes.
316
+
169
317
  ### Don't toggle the drawer just to peek
170
318
 
171
319
  `muxr_drawer_read` works without showing the drawer. Toggling it to
@@ -180,8 +328,10 @@ If a tool call returns `isError: true`, the text usually starts with
180
328
  - `muxr error -32602: pane: no pane with id "…"` — the pane has been
181
329
  killed, or you passed a stale id from before a kill/promote. Refetch
182
330
  `muxr_panes_list`.
183
- - `muxr error -32602: layout: unknown layout` — valid layouts are
184
- `tall`, `grid`, `monocle`.
331
+ - `muxr error -32602: layout: unknown layout` — the ten valid layouts are
332
+ `tall`, `wide`, `columns`, `rows`, `grid`, `spiral`, `centered`, `stack`,
333
+ `monocle`, `auto`. `muxr_session_get` returns the live list in
334
+ `available_layouts`; trust that over any list written down here.
185
335
 
186
336
  ## Naming muxr in conversation
187
337
 
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.11
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roel Bondoc
@@ -59,19 +59,28 @@ files:
59
59
  - lib/muxr/application.rb
60
60
  - lib/muxr/client.rb
61
61
  - lib/muxr/command_dispatcher.rb
62
+ - lib/muxr/config.rb
62
63
  - lib/muxr/control_server.rb
63
64
  - lib/muxr/drawer.rb
64
65
  - lib/muxr/foreground_command.rb
66
+ - lib/muxr/history_row.rb
67
+ - lib/muxr/image_store.rb
65
68
  - lib/muxr/input_handler.rb
66
69
  - lib/muxr/key_parser.rb
67
70
  - lib/muxr/layout_manager.rb
71
+ - lib/muxr/mouse_report.rb
68
72
  - lib/muxr/pane.rb
73
+ - lib/muxr/pane_picker.rb
74
+ - lib/muxr/pane_transfer.rb
69
75
  - lib/muxr/protocol.rb
70
76
  - lib/muxr/pty_process.rb
77
+ - lib/muxr/remote_pane.rb
71
78
  - lib/muxr/renderer.rb
72
79
  - lib/muxr/session.rb
80
+ - lib/muxr/session_directory.rb
73
81
  - lib/muxr/terminal.rb
74
82
  - lib/muxr/version.rb
83
+ - lib/muxr/width_probe.rb
75
84
  - lib/muxr/window.rb
76
85
  - muxr.gemspec
77
86
  - skills/muxr-control/SKILL.md
@@ -79,11 +88,19 @@ homepage: https://github.com/roelbondoc/muxr
79
88
  licenses:
80
89
  - MIT
81
90
  metadata:
91
+ homepage_uri: https://roelbondoc.github.io/muxr/
82
92
  source_code_uri: https://github.com/roelbondoc/muxr
83
93
  bug_tracker_uri: https://github.com/roelbondoc/muxr/issues
84
94
  changelog_uri: https://github.com/roelbondoc/muxr/blob/main/CHANGELOG.md
85
95
  rubygems_mfa_required: 'true'
86
96
  allowed_push_host: https://rubygems.org
97
+ post_install_message: |
98
+ muxr ships a Claude Code skill for driving sessions over MCP. To install it
99
+ into ~/.claude/skills and print the `claude mcp add` line for the bridge:
100
+
101
+ muxr --install-skill
102
+
103
+ Re-run it after each `gem update muxr` to refresh the skill contents.
87
104
  rdoc_options: []
88
105
  require_paths:
89
106
  - lib