Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 32 additions & 137 deletions mux/README.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions mux/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# cmux-mux docs

`cmux-mux` is a tmux-style multiplexer that speaks Ghostty's VT engine on both ends: PTY output is parsed into Ghostty terminal state, and attach clients receive Ghostty VT replay plus live output so another frontend can reconstruct the same surface.

## Contents

- [Getting started](getting-started.md): build prerequisites, local and headless runs, sockets, detach and attach.
- [Concepts](concepts.md): session tree, focus, collapse behavior, tab naming, smart split, PTY and browser surfaces.
- [Keyboard](keyboard.md): prefix model, modeless Alt layer, default bindings, and `mux.json` key remapping.
- [Mouse](mouse.md): clickable UI, drag reorder, resize, scrollbars, menus, selection, pointer shape, and dialogs.
- [Configuration](configuration.md): full `mux.json` reference with defaults and a worked example.
- [Control socket protocol](protocol.md): JSON-lines framing, protocol v6 attach streams, events, and compatibility rules.
- [Browser panes](browser-panes.md): CDP-backed browser tabs, rendering, input, profiles, and current limitations.
54 changes: 54 additions & 0 deletions mux/docs/browser-panes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Browser Panes

Browser panes are local Chrome/Chromium targets controlled with the Chrome DevTools Protocol. They live in the same pane and tab tree as PTY tabs, but their rendering and input path are CDP-based instead of VT-based.

## Requirements

Browser panes need a local CDP endpoint or a launchable Chrome/Chromium-family binary. The TUI can reuse an external endpoint, discover one on configured local ports, or launch Chrome itself in `--headless=new` mode.

Endpoint selection order:

1. `CMUX_MUX_CDP_URL`
2. `browser.cdp_url` in `mux.json`
3. Discovery on `browser.discover_ports` when `browser.discover` is true
4. A launched Chrome using `browser.chrome_binary` or binary discovery in `mux-cdp`

Binary discovery checks configured paths, known macOS and Linux Chrome-family paths, then `PATH` names such as `google-chrome`, `chromium`, `brave-browser`, and `microsoft-edge`.

Set `CMUX_MUX_CDP_DEBUG` to print browser runtime debug messages to stderr.

## Creating Panes

Use prefix `B`, or right-click a pane and choose `New browser tab`. The prompt starts with `https://`.

Bare domains get `https://` prepended. Inputs containing `://`, or starting with `about:`, `file:`, `data:`, `chrome:`, or `devtools:`, pass through unchanged.

Browser tabs are created inside an existing pane when one is active. If the session has no workspaces, creating a browser tab creates a workspace, screen, and pane around it.

## Rendering

The browser runtime creates a target, attaches with CDP, enables the page domain, sets device metrics from the pane's cell size and detected cell pixels, and starts `Page.screencastFrame`.

The TUI draws the latest PNG frame with the kitty graphics protocol after each Ratatui frame. If a context menu or prompt overlaps the pane, the graphics placement is omitted for that frame so the terminal UI stays readable.

If the host terminal does not support kitty graphics, the pane displays `terminal has no kitty graphics support`. If the browser frame has not arrived yet, it displays a loading message.

## Input

Printable character keys and paste use CDP insert-text. Enter, Backspace, Tab, Esc, arrows, Home, End, PageUp, PageDown, and Delete use CDP key events with modifier bits for Alt, Control, Super, and Shift.

Left click, drag, release, and wheel events inside browser content are forwarded as CDP mouse input. Wheel deltas are scaled by the detected cell height.

## Profiles and Lifecycle

Browser panes share one browser runtime per mux session. Closing a browser tab closes only its target. Mux shutdown kills Chrome only when cmux launched it.

Launched Chrome uses a persistent cmux profile unless `browser.ephemeral` is true. `browser.user_data_dir` overrides the persistent profile path. When ephemeral mode is true, Chrome uses a temporary profile that is deleted on shutdown and ignores `browser.user_data_dir`.

The default launched profile is `~/Library/Application Support/cmux-mux/chrome-profile` on macOS. On non-macOS targets it is `$XDG_DATA_HOME/cmux-mux/chrome-profile` when `XDG_DATA_HOME` is set, then `~/.local/share/cmux-mux/chrome-profile`.

## Limitations

Browser panes are local-only as of protocol v6. `attach-surface` returns an error for browser surfaces, attach clients do not receive browser frame streams, and a remote TUI shows a placeholder for browser panes.

Headful external Chrome can throttle screencast frames when the window or tab is hidden or occluded. Chrome 136 and newer do not allow `--remote-debugging-port` with the default user data directory, so reusable everyday Chrome profiles may not expose CDP.
49 changes: 49 additions & 0 deletions mux/docs/concepts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Concepts

## Tree

The mux tree is:

```text
session -> workspaces -> screens -> split-tree panes -> tabs
```

A session is one mux backend and one control socket. A workspace owns one or more screens. A screen is the layout selected in the status bar. A screen layout is a binary split tree whose leaves are panes. A pane owns an ordered tab list, and each tab is a surface.

The UI uses tmux-style verbs for screens. Prefix `c` creates a screen, prefix `n` and `p` switch screens, prefix `&` closes a screen, and prefix `,` renames a screen. PTY tabs use prefix `t`, tab chips, and tab context menus.

## Active and Focus State

The session tracks the active workspace. Each workspace tracks its active screen. Each screen tracks its active pane. Each pane tracks its active tab.

Focusing a pane makes that pane's screen and workspace active. Selecting a workspace or screen changes that level's active item. Selecting a tab changes the active tab in one pane.

Pane focus tracks recent activity. When closing the active pane or the last tab in it, mux chooses the most recently active remaining pane on that screen instead of always choosing a neighbor.

## Tabs and Names

Tabs are surfaces. A PTY tab wraps a child process connected to a pseudo-terminal. A browser tab wraps a local Chrome/Chromium target.

`rename-tab` sets the surface name. Empty tab names clear the custom name and fall back to the generated tab label. The old config key `rename-pane` is still accepted as an alias for the `rename-tab` key binding, but the UI rename action targets the tab surface, not the pane object.

Pane names still exist in the control socket through `rename-pane`. They are separate from the tab labels shown in the TUI.

## Smart Split

The modeless `Alt-n` binding creates a new pane with smart split direction. The TUI first tries the focused pane. If that pane cannot split in the chosen direction, it tries the largest pane that can.

Direction follows a zellij-style rule using the terminal cell ratio. Tall enough panes split down. Wide enough panes split right. Panes below the configured size thresholds do not split.

## Collapse Behavior

Closing a tab removes one surface. If the pane still has tabs, the active tab index moves to a remaining tab.

If a pane loses its last tab, that pane is removed from the split tree and its parent split collapses to the remaining child. If that empties the screen, the screen is removed. If that empties the workspace, the workspace is removed. If every workspace is gone, mux emits an `empty` event.

Closing a pane closes all tabs in that pane. Closing a screen closes every pane and tab in that screen. Closing a workspace closes every screen, pane, and tab in that workspace.

## PTY and Browser Surfaces

A PTY surface parses child-process output with libghostty-vt. Frontends render snapshots of that terminal state. Attach clients receive a VT replay first, then a base64 stream of subsequent PTY bytes, plus ordered resize frames when the surface geometry changes.

A browser surface is a local Chrome/Chromium target controlled through the Chrome DevTools Protocol. The local TUI draws browser frames with kitty graphics and forwards keyboard, mouse, and wheel input over CDP. Browser surfaces are listed in the tree, but `attach-surface` does not stream browser pixels as of protocol v6.
158 changes: 158 additions & 0 deletions mux/docs/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Configuration

`cmux-mux` reads `~/.config/cmux/mux.json`, or `$XDG_CONFIG_HOME/cmux/mux.json` when `XDG_CONFIG_HOME` is set. Set `CMUX_MUX_CONFIG` to use another file; it takes precedence over both. Every documented key is optional. Unknown keys in the typed sections make the raw config invalid, so the TUI logs an error and falls back to defaults.

Colors accept `#rrggbb`, `#rgb`, an xterm-256 number, or a numeric string.

## Theme

Selection colors are resolved in this order: explicit `mux.json`, Ghostty config keys `selection-background` and `selection-foreground`, then built-in defaults. Ghostty configs are read from `$XDG_CONFIG_HOME/ghostty/config` (when set), `~/.config/ghostty/config`, and on macOS `~/Library/Application Support/com.mitchellh.ghostty/config`; later entries in the file win.

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `theme.selection_background` | color | `#3a3a3a`, seeded from Ghostty when present | Selection background in PTY panes |
| `theme.selection_foreground` | color or null | `null`, seeded from Ghostty when present | Selection foreground; `null` keeps each cell's foreground |
| `theme.sidebar_rail` | color | `110` | Rail color for the active workspace rows |
| `theme.sidebar_active_bg` | color | `236` | Background for the active workspace rows |
| `theme.tab_rail` | color | `110` | Rail color inside the active tab chip |
| `theme.tab_bg` | color | `236` | Background for inactive solid tab chips |
| `theme.tab_active_bg` | color or null | `null` | Overrides the focused and unfocused active-tab chip backgrounds |
| `theme.border_active` | color | `110` | Focused pane border |
| `theme.border_inactive` | color | `238` | Unfocused pane border |

## Tabs

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `tabs.min_width` | integer | `7` | Minimum tab label width, clamped to 3 through 40 |
| `tabs.solid_background` | boolean | `true` | Renders tab chips with solid backgrounds |
| `tabs.show_titles` | boolean | `false` | Shows full process titles after tab numbers |
| `tabs.agents` | string array | `["claude","codex","opencode","pi"]` | Agent names surfaced in tab labels when `show_titles` is false |

Tabs are numbered by default. A recognized agent program can appear after the number. A user-assigned tab name replaces the generated label.

## Sidebar

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `sidebar.width` | integer | `22` | Sidebar width, clamped to 10 through 60 on load |
| `sidebar.max_width` | integer | `0` | Maximum live drag width; `0` means no configured maximum |

Live sidebar dragging also leaves at least 40 columns for pane content.

## Browser

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `browser.chrome_binary` | string | `null` | Chrome/Chromium binary to launch when no external CDP endpoint is used |
| `browser.cdp_url` | string | `null` | External CDP endpoint, accepted as `http://host:port` or `ws://...` |
| `browser.discover` | boolean | `true` | Probe discovery ports before launching Chrome |
| `browser.discover_ports` | integer array | `[9222]` | Local ports to probe for `/json/version` |
| `browser.user_data_dir` | string | `null` | Persistent profile directory for launched Chrome |
| `browser.ephemeral` | boolean | `false` | Use a temporary launched Chrome profile and delete it on shutdown |

When `browser.ephemeral` is true, it takes precedence over `browser.user_data_dir`: launched Chrome uses a fresh temporary profile, and the configured directory is not deleted.

The default launched profile is `~/Library/Application Support/cmux-mux/chrome-profile` on macOS. On non-macOS targets it is `$XDG_DATA_HOME/cmux-mux/chrome-profile` when `XDG_DATA_HOME` is set, then `~/.local/share/cmux-mux/chrome-profile`.

## Scrollbar

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `scrollbar.position` | `"column"` or `"border"` | `"column"` | Dedicated scrollbar column or right-border overlay |

## Keys

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `keys.prefix` | chord string | `"ctrl+b"` | Prefix chord |
| `keys.alt_shortcuts` | boolean | `true` | Enables default modeless Alt bindings when true |
| `keys.new-tab` | chord string or array or `"none"` | `["t","alt+t"]` | New PTY tab |
| `keys.new_browser_tab` | chord string or array or `"none"` | `"B"` | Browser URL prompt |
| `keys.new-pane-smart` | chord string or array or `"none"` | `"alt+n"` | New pane using smart split direction |
| `keys.next-tab` | chord string or array or `"none"` | `"tab"` | Next tab |
| `keys.prev-tab` | chord string or array or `"none"` | `"backtab"` | Previous tab |
| `keys.split-right` | chord string or array or `"none"` | `"%"` | Split right |
| `keys.split-down` | chord string or array or `"none"` | `"\""` | Split down |
| `keys.close-tab` | chord string or array or `"none"` | `"x"` | Close active tab |
| `keys.close-pane` | chord string or array or `"none"` | `"X"` | Close active pane |
| `keys.rename-tab` | chord string or array or `"none"` | unbound | Rename active tab |
| `keys.rename-pane` | chord string or array or `"none"` | alias | Alias for `rename-tab` |
| `keys.rename-screen` | chord string or array or `"none"` | `","` | Rename active screen |
| `keys.rename-workspace` | chord string or array or `"none"` | `"$"` | Rename active workspace |
| `keys.close-screen` | chord string or array or `"none"` | `"&"` | Close active screen |
| `keys.prev-screen` | chord string or array or `"none"` | `["p","alt+["]` | Previous screen |
| `keys.next-screen` | chord string or array or `"none"` | `["n","alt+]"]` | Next screen |
| `keys.new-screen` | chord string or array or `"none"` | `"c"` | New screen |
| `keys.next-workspace` | chord string or array or `"none"` | `"w"` | Next workspace |
| `keys.new-workspace` | chord string or array or `"none"` | `"W"` | New workspace |
| `keys.toggle-sidebar` | chord string or array or `"none"` | `"s"` | Toggle sidebar |
| `keys.focus-left` | chord string or array or `"none"` | `["h","left","alt+h","alt+left"]` | Focus left |
| `keys.focus-right` | chord string or array or `"none"` | `["l","right","alt+l","alt+right"]` | Focus right |
| `keys.focus-up` | chord string or array or `"none"` | `["k","up","alt+k","alt+up"]` | Focus up |
| `keys.focus-down` | chord string or array or `"none"` | `["j","down","alt+j","alt+down"]` | Focus down |
| `keys.resize-grow` | chord string or array or `"none"` | `"alt+="` | Grow the focused split |
| `keys.resize-shrink` | chord string or array or `"none"` | `"alt+-"` | Shrink the focused split |
| `keys.scroll-up` | chord string or array or `"none"` | `"pageup"` | Scroll active PTY up 10 rows |
| `keys.scroll-down` | chord string or array or `"none"` | `"pagedown"` | Scroll active PTY down 10 rows |
| `keys.detach` | chord string or array or `"none"` | `"d"` | Quit local TUI or detach attached TUI |

Each action override replaces all default chords for that action. Values may be a string, an array of strings, or `"none"`. Non-string array entries are ignored. Set `keys.alt_shortcuts` to `false` to remove default Alt chords before applying user overrides; explicitly configured Alt chords still work. Prefix `1` through `9` stay fixed to tab selection.

Chord strings can be single characters or a key name with optional `ctrl`, `control`, `alt`, `option`, or `shift` modifiers. Examples: `"c"`, `"%"`, `"ctrl+b"`, `"alt+enter"`, `"tab"`, `"backtab"`, `"shift+tab"`, `"pageup"`, `"pagedown"`, `"esc"`, `"space"`, `"left"`, `"right"`, `"up"`, `"down"`, `"home"`, and `"end"`.

## Example

```json
{
"theme": {
"selection_background": "#355c7d",
"selection_foreground": null,
"sidebar_rail": "#87afd7",
"sidebar_active_bg": 236,
"tab_rail": "#87afd7",
"tab_bg": 236,
"tab_active_bg": null,
"border_active": "#87afd7",
"border_inactive": "#444444"
},
"tabs": {
"min_width": 9,
"solid_background": true,
"show_titles": false,
"agents": ["claude", "codex", "opencode", "pi"]
},
"sidebar": {
"width": 24,
"max_width": 40
},
"browser": {
"chrome_binary": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"cdp_url": "http://127.0.0.1:9222",
"discover": true,
"discover_ports": [9222, 9223],
"user_data_dir": "/Users/me/Library/Application Support/cmux-mux/chrome-profile",
"ephemeral": false
},
"scrollbar": {
"position": "column"
},
"keys": {
"prefix": "ctrl+a",
"alt_shortcuts": false,
"new-tab": ["t", "alt+t"],
"new_browser_tab": "B",
"new-pane-smart": "alt+n",
"next-tab": "tab",
"prev-tab": "backtab",
"next-screen": ["n", "alt+]"],
"prev-screen": ["p", "alt+["],
"rename-tab": "r",
"rename-screen": ",",
"focus-left": ["h", "left", "alt+h", "alt+left"],
"focus-right": ["l", "right", "alt+l", "alt+right"],
"close-pane": "none",
"detach": "d"
}
}
```
76 changes: 76 additions & 0 deletions mux/docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Getting started

## Prerequisites

Builds need zig 0.15.2, a Rust toolchain, and the `ghostty` submodule. `ghostty-vt-sys` compiles `libghostty-vt.a` from that submodule, so an uninitialized submodule fails before the TUI starts.

```bash
cd mux
cargo build -p mux-tui
```

## Local session

A normal run starts an in-process mux, opens the TUI, and serves the control socket.

```bash
cd mux
cargo run -p mux-tui
cargo run -p mux-tui -- --session agents
```

The default session is `main`. Quitting a local TUI shuts down that in-process session and removes its socket.

Use `--term <value>` to set `TERM` for child PTYs. Without it, children get `xterm-256color`; the surface layer also honors `CMUX_MUX_TERM` when no CLI value is supplied.

## Headless server and attach

Headless mode starts only the mux backend and control socket.

```bash
cd mux
cargo run -p mux-tui -- --headless --session agents
```

Attach a TUI to that session from another terminal.

```bash
cd mux
cargo run -p mux-tui -- attach --session agents
```

Detach from an attached TUI with prefix `d`. With default keys, that is `Ctrl-b d`. The server keeps running, and another `attach` reconnects to the same tree. PTY tabs attach with a Ghostty VT-state replay followed by a live output stream.

## Sessions and sockets

The default socket path is:

```text
$TMPDIR/cmux-mux-<uid>/<session>.sock
```

The usual default is `$XDG_RUNTIME_DIR/cmux-mux-<uid>/main.sock` when `XDG_RUNTIME_DIR` is set, then `$TMPDIR/cmux-mux-<uid>/main.sock`, then `/tmp/cmux-mux-<uid>/main.sock`. `--session <name>` changes the final file name. `--socket <path>` bypasses the session-derived path. Server-started child processes receive `CMUX_MUX_SOCKET` with the socket path.
Comment on lines +46 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the socket example so it matches the fallback chain.

The block at Lines 46-50 shows only $TMPDIR/..., but the rest of this section and mux/scripts/smoke-attach.py fall back through XDG_RUNTIME_DIR, TMPDIR, and /tmp. Please update or remove the example so it doesn't contradict the actual lookup order.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mux/docs/getting-started.md` around lines 46 - 52, The socket path example in
the getting-started docs conflicts with the actual fallback order. Update the
default socket path example near the socket-path explanation to reflect the full
lookup chain used by the socket resolution logic and smoke-attach behavior, or
remove the single-path example entirely; make sure it aligns with the
session-derived path handling in this section and the fallback order implemented
by the socket selection flow.


## Platforms and XDG

cmux-mux supports macOS and Linux; Windows support via ConPTY is planned for phase 2. The TUI config path resolves `CMUX_MUX_CONFIG`, then `$XDG_CONFIG_HOME/cmux/mux.json`, then `~/.config/cmux/mux.json`.

Launched Chrome profile paths are platform-specific. On macOS the default is `~/Library/Application Support/cmux-mux/chrome-profile`. On Linux and other non-macOS targets, `XDG_DATA_HOME` is used when set, then `~/.local/share/cmux-mux/chrome-profile`.

## Development flow

Run tests from `mux/`.

```bash
cargo test
```

Run the smoke scripts against a built binary. Set `CMUX_MUX_BIN` to test a non-default binary.

```bash
cargo build -p mux-tui
python3 scripts/smoke-tui.py
python3 scripts/smoke-attach.py
```

This checkout does not contain `scripts/mux-dev.sh`; use the cargo and smoke commands above for the TUI flow.
Loading
Loading