mnml-rs 0.2.21

A NvChad-style terminal IDE in Rust — vim or standard editing, LSP, git, and an embedded HTTP client.
# mnml over SSH — what works, what degrades, what breaks

Task #1163. Ground truth as of 2026-08-23 (commit ~`a8eb98ea`).

## TL;DR

The core editor (crossterm + ratatui) runs unmodified over SSH. Every
feature that reaches OUTSIDE the terminal to the host — open a browser,
read the Keychain, control the Mac's audio output, notify the desktop
— quietly no-ops or acts on the *remote* box, not your laptop. There
are no crashes; there is one silently-wrong behavior worth
documenting (the URL-open shellouts).

## Verdict per surface

### Works verbatim over SSH

- **Editing, panes, splits, layout, palette, chords, ex-commands.**
  All keyboard + rendering routes through `crossterm` (which speaks
  the ANSI/xterm protocol crossterm ↔ your local terminal) and
  `ratatui` (renders to a byte buffer). Nothing platform-native in
  the render loop.
- **Mouse.** SGR 1006 mouse mode is negotiated over the SSH pty. All
  chip-click / drag-resize / right-click flows work.
- **File I/O, LSP, git, HTTP client, tests.** All happen on the box
  mnml is running on — the SSH scenario ("mnml on a Linux server")
  is the natural fit.
- **Pty panes** (Claude Code, Codex, htop, integrations spawned as
  `:term …`). These are just child processes on the box mnml runs
  on; the SSH client sees their output the same way it sees mnml's.
- **Ghost text via `claude -p` sub** (`src/ai/api_client.rs::nl_to_curl`
  and friends) — runs on the remote box, needs `claude` on PATH there.
- **Bufferline notifications via OSC 9 / OSC 777** (`src/tui/mod.rs:53-58`).
  Emitted to stdout, so they cross the SSH tunnel and land in whatever
  the LOCAL terminal does with them (iTerm2 badge, xterm bell, etc.).
- **Sixel / kitty / iTerm2 image protocols** (`src/image/mod.rs`) —
  auto-detected via `TERM`/`KITTY_WINDOW_ID`/`WEZTERM_EXECUTABLE`.
  Works over SSH as long as the SSH-client terminal on your end
  advertises the protocol AND your ssh config passes through the
  needed env (usually `TERM` alone is enough).
- **Clipboard via `arboard`** (`src/clipboard.rs:55`) — arboard tries
  native APIs first (OSC 52 is not one of them). On a Linux server
  arboard needs `X11`/`wayland` up; over a plain SSH session there's
  usually no display server, so arboard init returns `None` and the
  clipboard falls back to mnml's internal register (`self.sys` is
  `None`). Yank/paste inside mnml still works; system-clipboard
  bridge doesn't.

### Silently wrong

- **`gx` / right-click "Open in browser" / integration "open URL"**
  (`src/app/mod.rs::open_url_external` line 1357, plus three
  `Command::new("open" | "xdg-open" | "cmd /C start")` sites in
  `src/ui/integration_detail_view.rs:1000-1011`,
  `src/app/context_menus.rs:1974`, `src/app/mod.rs:9219`). These
  detect platform via `cfg!(target_os = …)` — a *compile-time*
  check. When mnml is compiled for Linux and running on a Linux
  server, `xdg-open` fires — which opens the URL in a browser **on
  that server**, not on your Mac. Best case: no display server,
  process errors and dies silently. Worst case: some remote-desktop
  session paints an unwanted window on the server. Either way the
  user's expectation ("show me this Bitbucket PR in my browser") is
  not met, with no error surfaced. **Recommended fix**: detect SSH
  session (env `SSH_TTY` or `SSH_CONNECTION` set) and emit OSC 8
  (hyperlinked text — modern terminals turn it into a clickable
  link the local terminal opens) OR toast the URL for manual copy
  instead of shelling out.

### Broken by environment, not by mnml

- **Anthropic Keychain resync**
  (`src/ai_usage.rs:216,366,412``security find-generic-password`).
  macOS-only shellout. On a Linux server the shellout fails; the
  function returns `None`; the token file path (`~/.config/mnml/ai_token`)
  is the only ingest point, so users on a remote server paste the
  Claude Code OAuth token there manually. No mnml-side bug — the
  code path already handles the `security` binary being missing.
- **Sonos statusline chip + AirPlay**
  (`src/sonos/coreaudio.rs`, `src/sonos/airplay.rs`, gated
  `#[cfg(target_os = "macos")]` at `src/sonos/mod.rs:27-29` and
  `src/sonos/stream.rs:93,101`). Non-macOS builds compile the
  Sonos surface out — the chip renders as inactive, transport
  actions are no-ops. Not a regression; a documented platform gate.
- **`osascript` shellouts** (music now-playing at
  `src/now_playing/macos.rs`, CDP window bounds at
  `src/app/cdp.rs:455,478,520,571`, terminal detection at
  `src/tui/mod.rs:3080`). All gated on macOS at either the module
  or the call site; degrade to safe defaults elsewhere.
- **Nerd Font glyphs.** The chrome uses Nerd Font codepoints (chips,
  file-tree icons, git glyphs). Your LOCAL SSH-client terminal has
  to have a Nerd Font configured — the remote box's fonts don't
  matter, since the remote just emits codepoints. Same story as
  running mnml locally, moved one hop.

## Case B — mnml running LOCALLY, opening remote SSH sessions in Pty panes

Different scenario, not the one this doc covers. `:term ssh
user@host` opens a Pty pane that runs SSH inside — the child
process talks to the remote box, mnml renders the child's output
into a pane. Works today because Pty panes are just `libghostty-vt`
buffers over any child process; no code change needed.

## Recommended patches (not applied here)

1. **`src/app/mod.rs::open_url_external`** — add SSH detection
   (`std::env::var("SSH_TTY").is_ok()`) and switch strategy:
   - Emit OSC 8 hyperlink into the buffer for one frame + a toast
     ("URL copied to clipboard — Cmd+Click your terminal to open"),
     OR
   - Copy to clipboard + toast the URL. Better than the current
     silent-`xdg-open`-on-the-wrong-box.
2. **Sweep the four call sites** that duplicate the same pattern
   (`integration_detail_view.rs:1000`, `context_menus.rs:1974`,
   `app/mod.rs:9219`, the `open_url_external` in `app/mod.rs:1357`)
   through `open_url_external` so the SSH fix lands in one place.
3. **Doc `~/.ssh/config` guidance**: `SendEnv COLORTERM` + `SetEnv
   TERM=xterm-256color` (or `xterm-kitty` for kitty image support)
   are the two most common gotchas — surface these in the manual
   page.

None of these are urgent. Point 1 is the only one that changes
user-visible behavior; the rest are hygiene / discoverability.