Metadata-Version: 2.5
Name: muxplex
Version: 0.60.2
Summary: Web-based tmux session dashboard — access all your tmux sessions from any browser
Project-URL: Repository, https://github.com/bkrabach/muxplex
Project-URL: Issues, https://github.com/bkrabach/muxplex/issues
Author-email: Brian Krabach <brian@krabach.com>
License: MIT
Keywords: dashboard,session-manager,terminal,tmux,web
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: FastAPI
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Terminals :: Terminal Emulators/X Terminals
Requires-Python: >=3.11
Requires-Dist: aiofiles>=23.0
Requires-Dist: cryptography
Requires-Dist: fastapi>=0.115.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: itsdangerous>=2.1.0
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: python-pam>=1.8.4
Requires-Dist: six>=1.16.0
Requires-Dist: tmux-kit==0.7.0
Requires-Dist: uvicorn[standard]>=0.30.0
Requires-Dist: websockets>=14.0
Provides-Extra: agent
Requires-Dist: amplifier-agent==0.12.0; (python_version >= '3.12') and extra == 'agent'
Provides-Extra: dev
Requires-Dist: beautifulsoup4>=4.12; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# muxplex

**Web-based tmux session dashboard — access, monitor, and manage all your tmux sessions from any browser on any device.**

![muxplex dashboard](https://raw.githubusercontent.com/bkrabach/muxplex/main/assets/branding/og/og-dark.png)

---

## Features

### Dashboard

- **Live session grid** — preview tiles with ANSI-colored terminal snapshots, auto-refreshed
- **Two view modes** — Auto (scrollable grid) and Fit (all sessions fill the viewport)
- **Hover preview** — full-size overlay of session content on tile hover
- **Activity indicators** — bell notification badges on tiles; amber favicon dot + `(N)` count in browser tab title when sessions have unseen activity
- **Session creation** — `+` button with device selector dropdown when multi-device is enabled; custom command template support
- **Session deletion** — `×` button with custom command template support
- **Mobile-friendly** — responsive layout, PWA-capable for home-screen install

### Terminal

- **Full interactive terminal** — powered by xterm.js + ttyd
- **Native clipboard** — Ctrl+Shift+C to copy, Cmd+V (macOS) / Ctrl+Shift+V (Linux) to paste
- **Mouse select auto-copy** — selecting text copies to system clipboard on release
- **OSC 52 tmux clipboard bridge** — tmux copy mode selections go to system clipboard
- **Search** — Ctrl+F opens a search bar to find text in terminal scrollback (xterm-addon-search)
- **Clickable URLs and Markdown labels** — Plain-click visible HTTP(S) URLs and OSC 8 Markdown-style labels to open them directly in an isolated new tab; other terminal text remains selectable.
- **Inline image rendering** — Sixel and iTerm2 graphic protocols for tools like yazi file manager (xterm-addon-image)
- **Sidebar session switcher** — quick-switch between sessions with live previews

### Settings

- **In-browser settings panel** — gear icon or `,` shortcut
- **Display** — terminal font size and bundled Nerd Font Mono choices (Fira Code Nerd Font Mono is the default), grid columns, hover delay, view mode, device badges, activity indicator. Only the effective selected face downloads; its source and license evidence is in `muxplex/frontend/fonts/`.
- **Sessions** — default session, sort order, hidden sessions, auto-open, bell sound, notifications
- **Commands** — custom create/delete session templates
- **Multi-Device** — remote instance federation
- **CLI** — `muxplex config list/get/set/reset`

### Multi-Device

- **Remote session aggregation** — federate multiple muxplex instances into a unified dashboard view
- **Device selector in new session** — `+` button shows a device dropdown when multi-device is enabled; create sessions on any connected instance directly from the dashboard
- **Remote bell-clear** — opening a session on a remote device automatically clears its activity notification via federation API (`POST /api/bell/clear`)
- **Unique session keys** — sessions identified by `remoteId:name` across devices, preventing bell-state collisions for identically-named sessions on different machines

### Service Management

- `muxplex service install/start/stop/restart/status/logs/uninstall`
- **Platform-aware** — systemd user service on Linux/WSL, launchd agent on macOS
- **Config-driven** — service reads all options from `~/.config/muxplex/settings.json` (no flags in the service file)

### Authentication

- **PAM authentication** — Linux/macOS system credentials
- **Password mode** — auto-generated or set via `MUXPLEX_PASSWORD` env var
- **Localhost bypass** — no auth needed on 127.0.0.1
- **Secure session cookies** — signed with configurable TTL

### Developer Tools

- `muxplex doctor` — dependency + config diagnostics with update check
- `muxplex upgrade` — smart version check + auto-update + service restart
- `muxplex config` — CLI settings management

### Agents & Automation

- **Public HTTP API** — the contract is discoverable at `/openapi.json` and `/docs`; headless clients authenticate with a Bearer federation key
- **Terminal input over the API** — `POST /api/sessions/{name}/input` lets an agent type into a live session (RCE by design, default-CLOSED, fenced by `input_enabled` + `input_allowed_sessions`)
- **Vendor-neutral guide** — point any agent (or a `curl` script) at [Driving muxplex from an agent](docs/AGENT_GUIDE.md)

### HTTPS / TLS

- `muxplex setup-tls` — auto-detect and set up TLS certificates
- **Tailscale** — real Let's Encrypt certs via `tailscale cert` (recommended when every client has Tailscale)
- **mkcert** — locally-trusted certs, zero browser warnings (when mkcert is installed on each client)
- **Local CA** — persistent root CA + signed leaf for browser-trusted HTTPS on plain LAN names (`spark-1`, `192.168.1.5`) without Tailscale or a public domain; install the CA once per client → see [Trusting the local CA](docs/TRUSTING_THE_LOCAL_CA.md)
- **Self-signed** — fallback for immediate HTTPS (browser shows warning)
- Required for browser clipboard API on non-localhost, and for stable PWA install (browsers refuse to keep installed PWAs in standalone mode against an untrusted origin)

---

## Prerequisites

- **Python 3.11+** — installed via `uv` or system Python
- **tmux** — terminal multiplexer
  - macOS: `brew install tmux`
  - Ubuntu/WSL: `sudo apt install tmux`
- **ttyd** — terminal sharing over HTTP (required for interactive terminal access)
  - macOS: `brew install ttyd`
  - Ubuntu/WSL: `sudo apt install ttyd` or `sudo snap install ttyd`
  - Other: https://github.com/tsl0922/ttyd#installation

> **Tip:** Run `muxplex doctor` to check all dependencies and system status.
>
> **OSC 8 Markdown links:** require tmux 3.4+. Existing users must run
> `muxplex tmux install` and reopen the terminal after upgrading; upgrading the
> package alone does not refresh the managed tmux fragments.

---

## Quick Start (uvx — no install)

Run muxplex directly without installing anything permanently:

```bash
uvx muxplex
```

Then open **http://localhost:8088** in your browser.

> **Note:** `uvx` is part of [uv](https://docs.astral.sh/uv/). Install uv with `curl -LsSf https://astral.sh/uv/install.sh | sh`.

---

## Install Permanently

```bash
uv tool install muxplex
muxplex doctor  # verify dependencies
```

Upgrade later with either:

```bash
uv tool upgrade muxplex   # standard uv workflow
muxplex upgrade           # also restarts the service if installed
```

> **Installing from git instead?** `uv tool install git+https://github.com/bkrabach/muxplex`
> tracks the default branch and gets unreleased commits. Do **not** pin a tag
> (`...@v1.2.3`) unless you mean it: `uv tool upgrade` resolves strictly within the
> recorded requirement, so a pinned rev reports "Nothing to upgrade" forever. Released
> versions on PyPI are the recommended path.

Then run it any time with:

```bash
muxplex
```

---

## Install as a Service

```bash
muxplex service install
# → prompts to set host to 0.0.0.0 for network access
```

The service starts automatically on login (macOS) or at boot (Linux) and restarts on failure.

```bash
# Open in browser
open http://localhost:8088
```

To stop and remove:

```bash
muxplex service uninstall
```

---

## CLI Reference

```
muxplex                              Start server (default)
muxplex serve [flags]                Start with CLI flag overrides
muxplex service install              Install + enable + start as OS service
muxplex service uninstall            Stop + disable + remove
muxplex service start|stop|restart   Manage running service
muxplex service status               Show service status
muxplex service logs                 Tail service logs
muxplex config                       Show all settings
muxplex config get <key>             Show one setting
muxplex config set <key> <value>     Set a setting
muxplex config reset [key]           Reset one or all to defaults
muxplex upgrade [--force]            Smart update with version check
muxplex doctor                       Check dependencies + config
muxplex show-password                Show current auth password
muxplex reset-secret                 Regenerate signing secret
muxplex setup-tls [--method auto]   Set up TLS certs (Tailscale/mkcert/self-signed)
muxplex setup-tls --status          Show current TLS configuration
muxplex env                          Print `eval`-able TMUX_TMPDIR export
muxplex tmux status                  Show whether muxplex's tmux config is active
muxplex tmux install [--dry-run]     Install muxplex's tmux config (safe, reversible)
muxplex tmux uninstall               Remove it, leaving your own config untouched
```

### Service management

```bash
muxplex service install     # Write service file + enable + start
muxplex service uninstall   # Stop + disable + remove service file
muxplex service start       # Start the service
muxplex service stop        # Stop the service
muxplex service restart     # Stop + start
muxplex service status      # Show running/stopped + PID
muxplex service logs        # Tail service logs
```

The service runs `muxplex serve` with no flags — it reads all options from `~/.config/muxplex/settings.json`. To change host/port, edit the config (or use the Settings UI in the browser) and restart:

```bash
muxplex config set host 0.0.0.0
muxplex service restart
```

### Recovering from a failed upgrade (v0.56.2 and earlier)

On v0.56.2 and earlier, `muxplex upgrade` (and its `muxplex update` alias) could leave the service down after a *successful* install. The upgrade process replaced muxplex's on-disk package files, then kept running its post-install steps (ensure the embedded agent panel is current, regenerate the service file, restart it) in that *same*, already-running process — which still had the *old* version of `muxplex.cli` / `muxplex.service` loaded in memory. A lazy cross-module import of a name that only existed in the *new* code could then fail with:

```
ImportError: cannot import name 'ensure_agent' from 'muxplex.cli'
```

leaving the service stopped even though the new version installed correctly.

muxplex v0.57.0+ fixes this at the root: `upgrade()` now hands the post-install steps off to a brand-new process of the just-installed version (an internal `muxplex _finish-upgrade` step), so those imports always resolve against the new code, never a stale cached module.

If you hit this on an affected version and the service is down after an upgrade, recover with:

```bash
muxplex service install   # regenerates the service file and restarts it
                          # from whatever version is actually installed on disk
muxplex doctor            # confirm the service is running again
```

`muxplex service install` is safe to run any time — it rebuilds the service file from scratch and doesn't depend on any in-memory state left over from a failed upgrade.

### Examples

```bash
# Start with defaults from settings.json
muxplex

# Override port for this run only
muxplex --port 9000

# Override host for this run only
muxplex serve --host 0.0.0.0
```

### HTTPS / TLS setup

```bash
# Auto-detect the best TLS method and set up certificates
muxplex setup-tls

# Use a specific TLS method
muxplex setup-tls --method tailscale
muxplex setup-tls --method mkcert
muxplex setup-tls --method selfsigned
muxplex setup-tls --method ca           # persistent local CA + signed leaf

# Show current TLS status and configuration
muxplex setup-tls --status

# Override TLS cert/key for a single run (without saving to config)
muxplex serve --tls-cert /path/cert.pem --tls-key /path/key.pem

# Check TLS configuration and dependencies
muxplex doctor
```

Auto-detection priority: **Tailscale** (if `tailscale` is installed and a cert is available) → **mkcert** (if `mkcert` is installed) → **self-signed** (always available as a fallback). Use `--method` to override.

> **Note:** Tailscale certs have a 90-day expiry. Run `muxplex setup-tls --method tailscale` to renew when needed.

#### When to use `--method ca`

The `ca` method is for the case where you want browser-trusted HTTPS on plain LAN names (e.g. `https://my-host:8088`, `https://192.168.1.5:8088`) but **can't** use Tailscale (no client install, blocked by IT policy, or the URL must be the bare LAN name) and **don't** want to buy a public domain.

It generates a persistent root CA in `~/.config/muxplex/ca/` and signs a 13-month leaf with it. The leaf's SAN automatically includes the hostname, `<hostname>.local`, `localhost`, the primary LAN IPv4 address, and the Tailscale MagicDNS name (if Tailscale is connected). Install the **CA** (not the leaf) once on each client; subsequent leaf rotations don't require re-trusting.

Not part of the `auto` cascade — must be opted into explicitly.

> **→ See [docs/TRUSTING_THE_LOCAL_CA.md](docs/TRUSTING_THE_LOCAL_CA.md)** for per-platform install instructions (Windows, macOS, Linux, iOS, Android, Firefox).

#### Fetching the CA over the network: `GET /api/ca`

Installing the CA on each client previously required `scp`-ing it off the server, which needs SSH access the client may not have — and it's easy to grab the wrong file (`muxplex.crt`, the **leaf** the server presents on the wire) instead of the CA, producing "unable to get local issuer certificate". `GET /api/ca` serves the CA's public certificate directly over HTTP(S) — no SSH, no auth (a CA public cert isn't a secret; it's the trust anchor clients are meant to install), and no ambiguity about which file it is:

```bash
curl -k https://my-host:8088/api/ca -o muxplex-ca.crt
```

`-k` is acceptable **only** for this one bootstrap fetch of a public trust anchor (there's nothing sensitive to expose by skipping verification here). For high-trust setups, confirm the fingerprint out-of-band before trusting it:

```bash
openssl x509 -in muxplex-ca.crt -noout -fingerprint -sha256
```

Returns 404 if this server isn't using `setup-tls --method ca` (e.g. it's on Tailscale, mkcert, or self-signed instead).

### tmux socket (the "invisible session" hazard)

muxplex looks for tmux sessions under a specific socket directory (the
`tmux_socket_dir` setting, mapped to tmux's `TMUX_TMPDIR` environment
variable). **Any other tool or script that creates a tmux session without
setting the same `TMUX_TMPDIR` lands on a *different* tmux server** and is
silently invisible to muxplex — `tmux list-sessions` from your interactive
shell will show it, but muxplex's dashboard, API, and Stream Deck sidecar
never will, because they're watching a different socket. This bites hardest
when `tmux_socket_dir` is left at its default (`""`): a systemd/launchd
*service* process doesn't inherit your login shell's `TMUX_TMPDIR`, so the
service quietly falls back to tmux's compiled-in default
(`/tmp/tmux-$UID`) even if your shell rc sets something else.

The one-line fix — run this before creating a session you want muxplex to see:

```bash
eval "$(muxplex env)"
tmux new-session -d -s my-session   # now lands where muxplex can see it
```

`muxplex env` prints a single `export TMUX_TMPDIR=...` line (nothing else,
so `eval` is always safe) resolved from the configured `tmux_socket_dir` —
or, if that's unset, your shell's own `TMUX_TMPDIR` — or, failing both,
tmux's own default. `GET /api/instance-info` also exposes `tmux_socket_dir`
(the exact value the *running server* resolves, since that endpoint runs
inside the server process itself) so remote tools/agents can discover it
without SSH access or tribal knowledge.

---

## Configuration

All settings are stored in `~/.config/muxplex/settings.json`.

A **†** on a key means **local-file-only** (`settings.LOCAL_ONLY_KEYS`): it can
ONLY be changed by editing `settings.json` on disk. `muxplex config set` refuses
it with an error, `PATCH /api/settings` silently ignores it, and federation never
syncs it. See [Editing local-file-only keys](#editing-local-file-only-keys) below.

| Key | Default | Description |
|---|---|---|
| `host` | `127.0.0.1` | Bind address (set to `0.0.0.0` for network access) |
| `port` | `8088` | Server port |
| `auth` | `pam` | Authentication mode: `pam` or `password` |
| `session_ttl` | `604800` | Session cookie TTL in seconds (7 days; 0 = browser session) |
| `default_session` | `null` | Session to auto-open on load |
| `sort_order` | `manual` | Session ordering: `manual`, `alphabetical`, `recent` |
| `session_filter` | `""` | fnmatch-style glob pattern (case-insensitive), matched against each session's bare name, narrowing the grid and sidebar to matching sessions only. Empty string = no filter. Does not affect the favicon badge, page title, or mobile bottom sheet -- a filtered-out session's bell still surfaces there. |
| `hidden_sessions` | `[]` | Sessions hidden from the dashboard |
| `views` | `[]` | Named session views for grouping and filtering sessions. Each view entry may carry an optional `match_names: [str]` -- fnmatch-style glob patterns matched case-insensitively against a session's bare tmux name (never a device-qualified `device_id:name` key, since the qualifier is a UUID nobody would type). A session is in the view if it is pinned in `sessions` **or** matches a pattern -- a strict union; rules are resolved fresh on every read and never written back into `sessions`, so a rule-based view cannot decay. See `docs/API_SEMANTICS.md` and `GET /api/views`. |
| `stale_key_grace_hours` | `24.0` | Hours before a session key absent from all *known* live sessions is pruned from views/hidden_sessions (syncable; per-device bookkeeping is local-only). Federation-aware: a remote device's keys are only ever evaluated for pruning while that device is currently reachable (see "Stale-key pruning" below) -- an offline device's view membership is never touched. |
| `window_size_largest` | `false` | Auto-set tmux `window-size largest` on connect |
| `auto_open_created` | `true` | Auto-open newly created sessions |
| `new_session_template` † | `tmux new-session -d -s {name}` | Command template for creating sessions |
| `delete_session_template` † | `tmux kill-session -t {name}` | Command template for deleting sessions |
| `session_commands` † | `[]` | Additional NAMED create/kill command pairs (see "Command pairs" below) |
| `tmux_theme` | `brand` | Which shipped tmux theme `muxplex tmux install` renders. `brand` is built from muxplex's own UI tokens, so a window that rings a bell turns the same amber in your terminal that its tile turns in the dashboard. Alternatives: `steel`, `catppuccin-mocha`. Not federation-syncable -- it renders to a file on this host. |
| `tmux_copy_mode` | `desktop` | Which copy-mode keybinding scheme `muxplex tmux install` (and `PATCH /api/tmux-config`) renders as `~/.config/muxplex/tmux.d/30-copy-mode.conf`. `desktop` is tmux's own default (arrow keys, Page Up/Down, Home/End, and Ctrl+C behave like every other desktop text field; no fragment is written). `vi` renders the modal `v`/`y` selection flow for vi/vim muscle memory. Not federation-syncable -- it renders to a file on this host. |
| `input_enabled` † | `false` | Global opt-in for `POST /api/sessions/{name}/input` (typing into sessions over the API). **RCE by design** — `false` makes the endpoint a hard 403. **Local-file-only**: can ONLY be set by editing `settings.json` on disk — deliberately not settable via `PATCH /api/settings` (a Bearer-key holder must not be able to self-authorize input) and not federation-syncable. |
| `input_allowed_sessions` † | `["*"]` (all sessions) | **Glob patterns** (matched case-INsensitively — both name and pattern are `.casefold()`-ed before `fnmatch.fnmatchcase`, so behavior is deterministic across platforms) naming sessions that may receive API terminal input. **The default is `["*"]` — every session** — so turning `input_enabled` on is all you need; you do not have to enumerate session names. Narrow it when you want to: `["amplifier-*"]` for a prefix family, or an exact name (matches only itself). Both the list `["*"]` and the bare string `"*"` are accepted. A session matching none of the patterns is a 403 even when `input_enabled` is true. Empty list = deny everything. **This default changed** — it used to be `[]` (deny-all), which meant flipping `input_enabled` on did nothing by itself; see the note below the table. **Local-file-only**: can ONLY be set by editing `settings.json` on disk — deliberately not settable via `PATCH /api/settings` and not federation-syncable. |
| `tmux_socket_dir` † | `""` | Override tmux's socket directory (maps to `TMUX_TMPDIR`). Set this if your tmux sessions live somewhere other than `/tmp/tmux-$UID` (e.g. a custom `TMUX_TMPDIR` in your shell rc) -- a systemd/launchd service does not inherit your login shell's environment, so without this the service can't see sessions created with a custom socket directory. |
| `focus_app` † | `""` | **macOS only.** The `.app` bundle name `POST /api/focus` runs `open -a` against to bring the muxplex PWA window to the foreground on this host. Empty = unconfigured (the endpoint returns `409` rather than silently doing nothing). **Wayland and Windows are not supported** -- Wayland has no portable activation path a headless server process can use; Windows has no muxplex port at all (see `docs/API_SEMANTICS.md`'s `POST /api/focus` section for the full platform table). **Local-file-only**: can ONLY be set by editing `settings.json` on disk -- not settable via `PATCH /api/settings` and not federation-syncable. |
| `device_name` | `""` (hostname) | Display name for this device |
| `federation_key` | `""` | Server-to-server authentication key for federation |
| `remote_instances` | `[]` | Remote muxplex instances to aggregate |
| `multi_device_enabled` | `false` | Enable multi-instance federation |
| `tls_cert` † | `""` | Path to TLS certificate file (empty = HTTP) |
| `tls_key` † | `""` | Path to TLS private key file (empty = HTTP) |
| `fontSize` | `14` | Terminal font size (px) |
| `terminalFont` | `"FiraCode"` | Live terminal face: `FiraCode` (the default) and `JetBrainsMono` select bundled Nerd Font Mono regular faces; an explicitly saved `System` preserves the established system stack and causes no font download. Existing supported saved choices remain unchanged. Unknown, absent, and wrong-type values normalize to `FiraCode`. Only the effective selected bundled face downloads. Source hashes, font-name records, licenses, and third-party notices are in [`muxplex/frontend/fonts/`](muxplex/frontend/fonts/). |
| `previewFontSize` | `11` | Tile/sidebar preview text font size (px), `8`-`24`. Independent of `fontSize`, which drives only the live terminal. |
| `previewZoom` | `100` | Tile size / grid minimum column width scale (%), `50`-`200`. `100` reproduces the default sizing. |
| `hoverPreviewDelay` | `1500` | Hover preview popup delay (ms). `0` = Off. The sole control for the popover (as of v0.47.0; folds in the retired `showHoverPreview` checkbox -- see below) |
| `gridColumns` | `"auto"` | Number of grid columns (`"auto"` or integer) |
| `bellSound` | `false` | Play audio sound on terminal bell |
| `viewMode` | `"auto"` | Grid tile sizing: `auto` or `fit` |
| `deviceLabelPlacement` | `"titlebar"` | Where a session's device label is drawn: `titlebar` (in the tile/sidebar header, the default), `corner` (inside the preview, anchored lower right), or `off` (not drawn). Presentation only — views store device-qualified keys, so session identity is unaffected. Honored by the PWA only; the soft deck and the `muxplex-deck` sidecar draw no device label at all. |
| `showDeviceBadges` | `true` | **Derived — do not edit.** Maintained by the server as `deviceLabelPlacement != "off"`. Retained so pre-v0.36 clients keep working; set `deviceLabelPlacement` instead. |
| `activityIndicator` | `"both"` | Activity style: `none`, `glow`, `dot`, `both` |
| `gridViewMode` | `"flat"` | Multi-device grid layout: `flat`, `grouped`, `filtered` |
| `sidebarOpen` | `null` | Sidebar state: `true`, `false`, or `null` (auto-detect from screen width) |
| `agentPanelOpen` | `null` | Agent chat panel state: `true`, `false`, or `null` (never toggled -- stays closed, unlike `sidebarOpen` there is no screen-width auto-detect) |
| `composeBarOpen` | `null` | Terminal compose bar (typed input into a tmux session) state: `true`, `false`, or `null` (never toggled -- resolves to visible on every device width, more discoverable than the old mobile-only default) |
| `settings_updated_at` | `0.0` | Unix timestamp of last settings write (used for federation sync) |
| `views_updated_at` | `0.0` | Unix timestamp of last change to `views`/`hidden_sessions` specifically. Metadata like `settings_updated_at`, used to arbitrate views-specific federation sync conflicts independently of unrelated field changes (e.g. a `fontSize` edit no longer bumps this). Not itself a syncable setting -- see `docs/API_SEMANTICS.md`. |
| `views_changed_at` | `{}` | Per-view / per-member stamps recording when each view and each pinned session last changed PRESENCE, shaped `{"<view>": {"at": <ts\|null>, "members": {"<session key>": <ts>}}}`. This is what lets federation sync **merge** `views` instead of replacing them wholesale, so a pin made on one device and a pin made on another both survive while a genuine deletion on either is still honoured. Metadata, like the two timestamps above: written only by the server (never accepted from a client PATCH), not itself a syncable setting. See `docs/API_SEMANTICS.md`. |

**Priority:** CLI flags > `settings.json` > defaults.

### Editing local-file-only keys

Keys marked **†** above are refused by the write path, by design:

```
$ muxplex config set focus_app "Muxplex"
error: 'focus_app' is local-file-only and cannot be set through patch_settings()
```

Edit the file directly instead. The path is the same on every platform —
`~/.config/muxplex/settings.json` — because it always derives from `$HOME`
(`XDG_CONFIG_HOME` is ignored):

```bash
$EDITOR ~/.config/muxplex/settings.json     # add "focus_app": "Muxplex"
curl -s localhost:8088/api/instance-info    # verify the SERVER sees it
```

**Most †-keys take effect on the next request — no restart.** The server
re-reads `settings.json` from disk every time it consults a setting (the same
way the federation key is read fresh per request), so a `†` edit is live as soon
as the file is saved. `tls_cert` / `tls_key` are the exception: `muxplex serve`
reads them once at startup to build its listener, so changing those needs
`muxplex service restart`.

Two things that make a good edit look like a no-op:

- **A JSON syntax error silently discards the whole file.** A stray trailing
  comma reverts *every* setting to its default — no error, nothing in the
  server log. If a change appears to do nothing, validate first:
  `python3 -m json.tool ~/.config/muxplex/settings.json`.
- **A hand edit does not bump `settings_updated_at`.** Clients that refetch only
  when that timestamp changes won't notice; reload the PWA to pick the change up.
  The server itself is unaffected — it never consults the timestamp when reading.

> **⚠️ The terminal-input allowlist default changed.** `input_allowed_sessions`
> used to default to `[]` — deny every session — so setting `input_enabled: true`
> did nothing on its own; you hit a second 403 and had to list session names by
> hand before anything worked. It now defaults to `["*"]`, every session.
>
> **What that means for you:** turning `input_enabled` on is now a *single*
> action that makes **every** session typeable over the API, including your own
> working panes. If you only want some sessions reachable, you must now say so
> explicitly, e.g. `"input_allowed_sessions": ["agent-*"]`. What did not change:
> `input_enabled` is still `false` out of the box, and both keys are still
> local-file-only — neither the API, a federation peer, nor an agent can set
> them.

> **→ Writing something that drives muxplex?** The rows above define
> `input_enabled` / `input_allowed_sessions` as *configuration*. For the
> operational side — auth, the read endpoints, session lifecycle, the terminal-input
> contract and its threat model, and copy-pasteable `curl` examples — see
> [docs/AGENT_GUIDE.md](docs/AGENT_GUIDE.md). It's vendor-neutral: point any agent
> or script at it.

---

## Keyboard Shortcuts

| Shortcut | Action |
|---|---|
| Ctrl+Shift+C | Copy terminal selection to system clipboard |
| Cmd+V / Ctrl+Shift+V | Paste from system clipboard (native browser paste) |
| Ctrl+F | Open terminal search bar |
| Enter / Shift+Enter | Next / previous search match |
| Click an HTTP(S) URL or Markdown label | Open it in a new tab |
| `,` (comma) | Open settings |
| Escape | Close settings / return to dashboard |

Mouse select in the terminal auto-copies to the system clipboard on release.

---

## Platform Support

| Platform | Service | Auth |
|---|---|---|
| Linux (Ubuntu/Debian) | systemd user service | PAM |
| macOS | launchd agent | PAM |
| WSL | systemd user service | PAM |

`POST /api/focus` (foreground-focus for the muxplex PWA window) is
**macOS-only**. Linux/X11 is unreliable (a systemd user service doesn't
reliably carry `DISPLAY`), Wayland cannot work at all (no portable
activation path a headless server process can use), WSL has a Windows
browser window to raise rather than a Linux one, and Windows has no
muxplex port at all. See `docs/API_SEMANTICS.md`'s `POST /api/focus`
section for the full per-platform table.

---

## Project Structure

```
muxplex/
├── muxplex/
│   ├── __init__.py
│   ├── __main__.py          # python -m muxplex entry
│   ├── cli.py               # CLI entry point and subcommand dispatch
│   ├── main.py              # FastAPI app, routes, WebSocket proxy
│   ├── auth.py              # PAM/password auth middleware
│   ├── sessions.py          # tmux session enumeration + snapshots
│   ├── bells.py             # Bell flag detection + clear rules
│   ├── state.py             # Persistent state (JSON)
│   ├── settings.py          # User settings management
│   ├── service.py           # Service install/start/stop (systemd + launchd)
│   ├── ttyd.py              # ttyd process lifecycle
│   ├── frontend/
│   │   ├── index.html        # Main SPA
│   │   ├── login.html        # Login page
│   │   ├── app.js            # Dashboard, sidebar, settings, previews
│   │   ├── terminal.js       # xterm.js terminal + clipboard
│   │   ├── tokens.css        # Design tokens — the ONE home for every value
│   │   ├── style.css         # All styles (dark theme)
│   │   ├── manifest.json     # PWA manifest
│   │   ├── wordmark-on-dark.svg
│   │   └── tests/            # JavaScript unit tests
│   └── tests/                # Python tests (pytest)
├── assets/branding/          # Logos, icons, design system
├── docs/plans/               # Historical design + implementation plans
├── scripts/                  # Utility scripts (asset generation)
├── pyproject.toml
└── README.md
```

---

## Development

### Setup

```bash
git clone https://github.com/bkrabach/muxplex
cd muxplex

# Install with dev dependencies
uv pip install -e ".[dev]"
```

### Run the server

```bash
muxplex
# or directly:
python -m muxplex
```

### Run tests

```bash
# Python tests (pytest)
python -m pytest muxplex/tests/ --ignore=muxplex/tests/test_integration.py

# JavaScript tests (node:test)
node --test muxplex/frontend/tests/test_terminal.mjs
node --test muxplex/frontend/tests/test_app.mjs
```

---

## Brand Assets

Brand assets — logos, icons, favicons, OG images — live in `assets/branding/`.

**Design language and colour tokens do not.** They live in
[`docs/DESIGN_LANGUAGE.md`](docs/DESIGN_LANGUAGE.md) (principles, components,
decisions) and [`muxplex/frontend/tokens.css`](muxplex/frontend/tokens.css)
(every value). That is the one place to look up a token.

`assets/branding/DESIGN-SYSTEM.md` is **superseded** and kept only as brand
provenance — its palette derivation and contrast ratios are still good, its CSS
values are not. Nine of them silently disagreed with what the app renders; see
`docs/DESIGN_LANGUAGE.md` §3.4.

To regenerate PNG/favicon assets from SVG sources:

```bash
python3 scripts/render-brand-assets.py
```

---

## Using muxplex with Amplifier (optional)

muxplex does **not** depend on [Amplifier](https://github.com/microsoft/amplifier) — Amplifier is just one optional way to drive it. A behavior bundle at [bkrabach/amplifier-bundle-muxplex](https://github.com/bkrabach/amplifier-bundle-muxplex) lets AI agents list, create, and monitor muxplex sessions with the server URL, federation key, and CA trust resolved for you instead of per-consumer.

Install it **as a behavior bundle at the app level** — added to your own Amplifier app/bundle configuration. It is not something muxplex itself pulls in.

**→ See [docs/AMPLIFIER.md](docs/AMPLIFIER.md)** for setup. Driving the raw API without Amplifier? [docs/AGENT_GUIDE.md](docs/AGENT_GUIDE.md).

---

## License

MIT
