Skip to content

Repository files navigation

roost

Processes are disposable. Sessions are precious.

A session-native terminal multiplexer for AI agent CLIs (pi, Claude Code, codex, gemini, opencode, shell) — no daemon, ever.

License: MIT Rust Version Platform

Screenshot of roost running in iTerm2: a single focused shell pane in ~/workspace, showing the ink-and-paper chrome — accent-red focused border, tab bar with save status, a top-right corner badge, and the bottom hint bar.

Why roost

  • Workspace resurrection. Quit roost, reboot the Mac, run roost again — every tab, split, and stacked pane comes back, each agent resumed into its exact session.
  • Session-native, not process-native. Agent CLIs persist their own conversation state and resume by id, so roost never needs a daemon — it just remembers the layout tree plus each pane's (adapter, cwd, session-id).
  • Fleet at a glance. The tab bar, corner badges, and collapsed stack rows show every agent's state — working, needs input, waiting, idle, exited — and roost rings the bell the moment one needs you.
  • A control CLI for orchestrators. roost spawn / send / read / status / wait / close — an LLM (or you) can drive a fleet of agent panes and watch the whole thing live.
  • ~1.4 MB, no daemon. One binary, zero config, nothing to keep alive in the background.

Full design rationale: DESIGN.md.

No daemon, so no detach

roost has no daemon and no detach. When you quit (Alt+q) or close the terminal, roost stops immediately — it does not reattach. What survives is the layout tree and each pane's (adapter, cwd, session-id), stored in workspace.json. Next time you run roost, every tab and split comes back, and agents resume their conversations by session id. What does not survive is an in-flight turn: an agent mid-turn when roost exits loses that turn, though its session state persists for the next resume.

This is deliberate — no daemon means nothing leaks, nothing to restart, and roost never runs invisible in the background. SSH is the one case where this matters: when an SSH session dies, roost dies with it. The fix is to run roost inside tmux or screen on the remote:

ssh user@host
screen  # or: tmux
roost

Then roost's local (layout, session-id) persistence works normally, and the outer screen/tmux session survives the SSH reconnect. When you reconnect, reattach to screen/tmux, and roost resumes from where it left off.

Quick start

Needs a Rust toolchain — grab one at rustup.rs if you don't have one.

git clone https://github.com/navbytes/roost
cd roost
cargo build --release
./target/release/roost

Or skip the build step with cargo run.

Install

  • misemise use -g github:navbytes/roost. Prebuilt binary, no Rust toolchain. See below.
  • brew install navbytes/tap/roost — from the navbytes/homebrew-tap tap. Same prebuilt binaries, checksum-verified by Homebrew.
  • Prebuilt binaries from GitHub Releases — macOS and Linux, arm64 and x86_64, with SHA256SUMS.txt to check them against.
  • cargo binstall --git https://github.com/navbytes/roost roost — fetches the release binary instead of compiling. The --git is not optional: the crates.io name roost belongs to an unrelated 2018 crate, so a bare cargo binstall roost installs something else entirely.
  • cargo install --git https://github.com/navbytes/roost — build from source onto your $PATH, no clone needed.

With mise

roost isn't in mise's registry and doesn't need to be — name the backend and mise installs it straight from this repo.

The short way — a prebuilt binary from the latest release, no Rust toolchain needed:

mise use -g github:navbytes/roost

Or build from source, if you'd rather (needs a Rust toolchain):

mise use -g "cargo:https://github.com/navbytes/roost@branch:main"

Pin a version by appending it, e.g. github:navbytes/roost@0.1.20, or swap @branch:main for @tag:v0.1.20 on the cargo backend. Drop -g to pin roost per-project in that directory's mise.toml instead of globally.

A separate workspace is one environment variable

Point ROOST_STATE somewhere else and you get an independent roost — its own workspace.json, its own config.json, and its own control socket, so roost list from that shell talks to that instance and not your main one:

$ ROOST_STATE=/tmp/roost-scratch roost

Nothing to create first — roost makes the directory. Useful for a throwaway fleet you don't want mixed into your real workspace, a per-project set of panes you keep separate, or trying a config.json remap without touching your own. Delete the directory to throw the whole thing away.

ROOST_STATE is also the root named workspaces live under, and the two compose — Workspaces covers that. For the raw-variable route, the rule stands: only one roost runs per state dir at a time — a second instance on the same dir refuses to start, since they'd race and corrupt workspace.json.

Workspaces

To run several roosts at once, give each one a name instead of a raw directory. A workspace is a named, independent fleet under the shared state root — its own layout, its own control socket, its own session claims — and exactly one roost runs in it at a time:

$ roost -w tripto          # also --workspace tripto, or ROOST_WORKSPACE=tripto
$ roost ws                 # list every workspace: running/idle, tabs, panes, adapters, last saved
$ roost ws ls --json       # the same fields as JSON
$ roost ws mv scratch tripto   # rename an idle workspace
$ roost ws rm scratch          # delete an idle workspace (its agent sessions are untouched)
  • One window per workspace. A second roost -w tripto is refused with the name of the workspace and a pointer at -w and roost ws ls. There is no in-TUI switcher: a switch is quit plus relaunch, and the quit guard still confirms while an agent is working.
  • Names are 1–32 characters of a-z, 0-9, ., _, - (lowercase only, so names never collide on case-insensitive filesystems). Creation is implicit: a new name starts fresh on first use. default is the unnamed workspace roost opens with no flag — it can be listed but never renamed or deleted.
  • ROOST_STATE stays the root override — the two compose: ROOST_STATE=/tmp/r roost -w a puts workspace a under /tmp/r, and ROOST_STATE=/tmp/r roost ws ls lists it. Plain ROOST_STATE isolation (above) keeps its exact old behaviour, including per-directory config.
  • Keybindings are shared. A named workspace reads whatever the default workspace would (below: the root config.json or the ~/.config/roost/ one, whichever is live); if the workspace directory has its own config.json, that file replaces it wholesale for that workspace (never merged).
  • Targeting from a plain shell. Out-of-pane verbs take -w anywhere on the line, before or after the verb (roost -w b list, roost close 3 -w b) — except inside send's own text, where -w is data, not a flag, past its PANE argument (roost send 3 -w b hi sends the literal text "-w b hi" to pane 3); inside a pane your own instance is targeted automatically; the flag beats ROOST_WORKSPACE, which beats the default. When nothing answers, the error names the workspace it tried and lists the ones that are running.
  • One agent session, one driver. Every running instance holds an exclusive claim per agent session it drives, so two windows can never resume or adopt the same conversation — the second one shows a placeholder naming the workspace that holds it, and resumes once that one quits.
  • Identity. Panes get $ROOST_WORKSPACE; the terminal title reads roost · <workspace> · <pane> for named workspaces (roost · <pane> for the default); roost status reports the workspace; desktop notifications carry its name. On-screen chrome is unchanged.

It isolates workspace state only. It still installs and updates the pi extension and the Claude Code hooks in your real ~/.pi and ~/.claude (below); add ROOST_NO_EXT_INSTALL=1 if you don't want that too. Deliberate: ROOST_STATE doesn't imply "don't touch my global config" because two concurrent real roost fleets, each in its own state dir, both legitimately want the one real ~/.claude wired up.

State lives in ~/.local/state/roost/workspace.json on Linux and ~/Library/Application Support/roost/workspace.json on macOS (auto-saved on every change, atomic writes) — alongside the control socket, token and audit log. Delete it to start clean.

Config is the other half and goes somewhere else, because it is the half you write by hand: config.json lives in ~/.config/roost/ (on macOS, ~/Library/Application Support/roost/). roost still reads an older one from the state dir beside workspace.json, so nothing breaks if you already have one there — but ~/.config is where a new one belongs, and roost keys prints the exact path it uses. Under ROOST_STATE the search stops in that directory, by design — you named it, so there is nothing to report. Named workspaces share the root config; a config.json inside a workspace's own directory replaces it wholesale for that workspace.

Environment

roost has no flags for any of this — the whole outside-the-TUI surface is five variables, set on the command that launches it:

Variable Effect
ROOST_STATE=DIR use DIR as the state dir: an independent workspace, config and control socket (above). Created if missing
ROOST_WORKSPACE=NAME launch directly into the named workspace instead of default (same as -w NAME; see Workspaces)
ROOST_NO_EXT_INSTALL=1 don't install or update the pi extension and Claude Code hooks in ~/.pi / ~/.claude (below)
ROOST_NO_QOS=1 don't raise input-thread scheduling priority on macOS (below)
ROOST_DEBUG=1 append control-plane diagnostics — dropped socket lines, shed connections — to <state>/roost.log. Any value enables it; the file is written only when there is something to say, and never to the TUI's own output

Inside every pane roost exports four more, so an agent can call back into the fleet or know where it's running — $ROOST_SOCK (the control socket), $ROOST_PANE (which pane it is), $ROOST_TOKEN (authenticates as that pane) and $ROOST_WORKSPACE (which workspace it's in, default for the unnamed one). You don't set these; Controlling roost covers what they're for.

On macOS roost promotes its own input threads to interactive scheduling priority so typing stays crisp while agent panes saturate the CPU (the agents themselves keep normal priority); set ROOST_NO_QOS=1 to switch that off. Either way roost keeps a tiny local latency log — one aggregate line a minute of event-loop scheduling stalls into <state>/perf.jsonl (size-capped, timings and counters only, nothing sensitive) — so the promotion's real-world effect on your machine is decidable from data.

macOS: make Option send Alt

roost's shortcuts all live on Alt. On macOS, Option sends accented characters by default, so shortcuts silently do nothing until you tell your terminal to treat Option as Meta/Alt:

  • Terminal.app: Settings → Profiles → Keyboard → check Use Option as Meta key.
  • iTerm2: Settings → Profiles → Keys → set the Left/Right Option key to Esc+.
  • Ghostty / WezTerm / kitty: send Alt by default — nothing to change.

If your first Alt+n seems to do nothing, this is almost certainly why. Once it's set, Alt+Enter opens the quick-launch picker (pi / claude / shell) and you're in.

Keys

Every shortcut lives on Alt. These twelve get you through a session:

Key Action
Alt+n new shell pane (auto split direction)
Alt+Enter quick-launch picker: pi, claude, codex, gemini, opencode, shell
Alt+arrow / Alt+hjkl move focus
Alt+w close pane (twice to confirm when the agent is busy)
Alt+t, Alt+1..9 new tab / go to tab
Alt+m next tab (wraps — the route past the ninth)
Alt+z zoom the focused pane to fill the screen
Alt+s stack the surrounding split — press again to absorb the next one out
Alt+a jump to the next pane that needs input, across tabs
Alt+c copy mode (v marks, y yanks)
Alt+? the full keymap — type to filter, runs the row
Alt+q quit — workspace saved; agents die, sessions live

Full keybinding reference — every chord roost binds, the config.json escape hatch for moving or switching any of them off (including handing Alt+arrow back to your shell), roost keys, mouse and text selection, and the Shift+Enter story.

You do not have to learn the rest from a page: Alt+? draws the whole table live, filtered as you type, and shows your bindings rather than the defaults. A shortcut hint bar along the bottom (zellij-style) shows what you can press right now and changes with context; Alt+/ hides it to reclaim the row.

Fleet features

Ten keyboard-first additions for running more agents at once, plus one CLI-only escape hatch — all Alt-only, same layer as everything above.

  • Jump to attention (Alt+a). Jumps to the next pane whose status is ◆ needs-input, across tabs, wrapping back to the first; press again for the next one. If nothing needs input, it falls back to any pane that's simply finished its turn (○ waiting) instead — the hint bar's right segment always matches what the key will actually do: ◆ N needs you · Alt+a when a real ◆ exists, ○ N your turn · Alt+a for the fallback, omitted only when there's truly nothing to jump to. Nothing needs you? A flash says so and nothing else changes.
  • Fleet roster (Alt+Shift+a). The shifted sibling of the chord above: Alt+a takes you to the next pane that needs you, Alt+Shift+a shows you all of them and lets you choose. A modal list of every pane in the workspace, grouped by tab — id, name, adapter and current state per row, the same way collapsed stack rows read — so the panes sitting in tabs you aren't looking at finally have a resting surface with names on it. It opens with its cursor already on the pane Alt+a would have jumped to, so Alt+Shift+a then Enter is exactly Alt+a. Arrows and PgUp/PgDn move, typing filters by id / name / adapter, Enter goes there (across tabs, expanding stacks, revealing the float, like every other jump), Esc or the same chord closes it. Clicking a row goes there too. Honest scope: going to a pane is the only thing it does — it's a navigator, not a control panel; roost send <id> is where scripted dispatch lives.
  • Activity feed (Alt+e). A modal overlay, not a persistent pane — there's no spare row at an 80×24 terminal — streaming the most recent 200 events: status changes, spawns, closes/reopens, exits, and control-plane calls (so roost send --all and every other control verb show up here too). Session-only, never written to disk. ↑/↓ scroll, Esc/q/Alt+e close.
  • Pane zoom (Alt+z). A pure view transform: the focused pane fills the screen, but the split/stack tree underneath is untouched. Zoom follows focus inside the tab; switching tabs, closing the zoomed pane, or any layout edit (new pane, split, stack, Alt+g) exits it first, so the layout never changes invisibly underneath you.
  • Floating scratch pane (Alt+Shift+z). One app-wide floating shell, toggled in and out of view — the process keeps running while it's hidden. Moving focus away hides it automatically; Alt+w while it's focused kills it for real. Honest scope: it's session-only, never written to workspace.json, and gone at quit like any other unsaved state — there's no persistent fourth pane type here, just an ephemeral one.
  • Raw pass-through (Alt+Shift+p). Marks the focused pane raw: every key except the toggle itself — including every other Alt chord — forwards straight through as bytes, so an agent CLI with its own Alt bindings (readline word-ops, custom editors) sees a normal terminal. The hint bar shows Alt+Shift+p exit raw the whole time you're in it, so you can't get stuck; the same chord that gets you in gets you out.
  • Keyboard copy mode. Alt+c now also takes hjkl/arrows to move a cursor, 0/$ for line start/end, v to mark an anchor, and y/Enter to yank — the mouse-drag path from before still works, and the two interleave freely (a drag moves the keyboard cursor too); Esc/q exits and clears the selection. Honest scope: visible grid only, same as the mouse path — no scrollback paging inside copy mode.
  • Canned layouts (Alt+g). Cycles the active tab through three built-in arrangements — even grid, main pane + stack, all-stack — skipping any that wouldn't fit the terminal, and always keeping focus and pane order stable. It's a snap-to-arrangement, not undoable via Alt+u.
  • Tab undo, the honest scope. Alt+u already reopened panes; it now reopens whole tabs the same way — name, layout, and pane specs restored, session ids included, so agents resume where they left off. The one limit worth knowing: a multi-pane tab is dismantled close-by-close, so undoing one that had several panes replays them as individual pane-undos — sessions intact, but re-split off the focused pane rather than at their original geometry. The undo stack holds the last 20 closes and is session-only (cleared on quit).
  • Pane notes, inside Alt+r. The edit dialog holds the pane's whole text surface — the name on its underlined first row, and under it a parking note: where this pane stands and what's next, written before you close the lid, read the next morning without a ritual. The note's first line shows in the focused pane's corner badge with an age tag (14h, 2d — a stale note confesses instead of lying), and every other pane that carries one shows a bare (¶⋮ when there's more under the first line), so walking your tabs and panes reveals each note where you're already looking. Nothing is ever shown all at once. Shift+Enter moves from name to note and adds lines (up to 8; multi-line pastes work too); Enter saves both fields — the age re-stamps only when the note actually changed, so renaming can't make a stale note look fresh — and emptying the note (Ctrl+U + Enter) clears it ("handled") without touching the name. Notes live in workspace.json next to the session id, so they survive quits and reboots, and Alt+u restores a note with its closed pane. No chord to learn: Alt+r was already the rename key, and it still is.
  • Broadcast. roost send --all TEXT [--enter] types into every running pane at once — CLI/control-plane only, deliberately no TUI key so a fat-fingered Alt chord can't blast the whole fleet. See Controlling roost below.

Status glyphs

Tab bar, corner badges, and collapsed stack rows all show the same states:

Glyph Meaning
⠋⠙⠹… working — spins (pi's own loader frames; animation always means busy, never "look at me")
needs input
waiting for you
· idle
exited

Status lives in the glyph, not the border — the focused pane's border is always accent-red, everything else stays quiet. When a non-focused pane starts waiting for you, roost rings the terminal bell (and posts a native notification on macOS).

A tab's glyph summarizes its panes (worst first), and carries a count when more than one pane is in that state — ◆3 is three agents waiting on you in that tab, ⠋2 two working. Ten or more shows +. The count cell is always reserved (blank below two), so tab widths never shift as statuses change. For the names behind the number, Alt+Shift+a lists every pane.

Status arrives two ways:

  1. Exact — agent-side integrations report over roost's unix socket ($ROOST_SOCK, pane identified by $ROOST_PANE, authenticated with a per-pane $ROOST_TOKEN):
    • pi: extensions/roost.ts — roost installs/updates it into ~/.pi/agent/extensions/ automatically at startup when pi is present (set ROOST_NO_EXT_INSTALL to manage it yourself). Uses pi's agent_start/agent_settled/session_start/project_trust/ask-tool events (pi ≥ 0.80.4), reports session ids instantly — and when an ask-tool fires, sends the question itself, so the feed line and notification say what's being asked. A turn that ends on a prose question ("Which database should I use?") is reported as needs-you too, question attached — heuristic (trailing ? on the final line), self-healing, and self-explanatory since the text rides along.
    • Claude Code: roost installs/updates three hooks into ~/.claude/settings.json automatically at startup when Claude Code is present (same ROOST_NO_EXT_INSTALL opt-out). The needs-you hook forwards Claude's own reason ("Claude needs your permission to use Bash") the same way. Details: extensions/claude-code-hooks.md.
  2. Heuristic fallback — recent PTY output ⇒ working; silence ⇒ waiting; a terminal bell (0x07) ⇒ needs-you (tmux-style); and for agents that publish state in their terminal title (Claude Code's braille spinner while working, at rest) the title itself — an exact-ish signal that fills the gap between Claude's one-shot hook connections, never overriding a live extension link or a needs-you. pi needs none of this for its own needs-you signal: its one blocking prompt (the project-trust dialog) is reported directly via the project_trust event above.

Each pane also carries a faint corner badge, top-right (iTerm2-style): name · adapter glyph — the name is its Alt+r title, or the adapter name (pi, claude, codex, gemini, opencode, or shell) when unnamed, and the glyph is the pane's live status. A cell TUI can't do true translucency, so it's rendered dim rather than see-through; the inner app's content still draws underneath it. A pane with an Alt+r note adds a here — and on the focused pane, the note's first line and age, full-strength: the one thing in the badge that isn't dim.

Appearance

roost's chrome inherits your terminal's theme; its text is exactly as legible as your shell prompt.

roost's own chrome — tabs, borders, badges, hint bar — is drawn in your colors. Every word it puts on screen uses your terminal's own foreground on your own background, the one contrast pair you have already proved readable, optionally one step quieter. The one accent is your ANSI red. Borders and separators are the only thing spending ANSI 8, so a theme that renders it faint costs you a hairline, not a word. Surfaces that need to shout (the flash, the dead-pane bar, the Alt-key warning) reverse your fg/bg rather than painting a color, and roost paints no background fill anywhere at all.

That means light themes, dark themes, tinted themes and solarized-anything all work, there is nothing to configure, and switching your terminal's theme while roost is running just works — nothing is detected, cached, or assumed. Program output inside panes is untouched: it keeps its own colors and attributes, truecolor included. Full design spec: DESIGN-ui.md.

Session resume

Adapter Launch Resume Session detection
pi pi pi --session <id> socket handshake, or newest file under ~/.pi/agent/sessions/
claude claude claude --resume <id> newest *.jsonl under ~/.claude/projects/<encoded-cwd>/
shell $SHELL relaunch in saved cwd
codex codex codex resume <SESSION_ID> newest *.jsonl under ~/.codex/sessions/YYYY/MM/DD/ (date-bucketed, not cwd-bucketed — detection cannot be scoped to a working directory)
gemini gemini gemini --resume <uuid> per-project history under ~/.gemini/tmp/<slug>/chats/, slug read from ~/.gemini/projects.json; session id extracted from file's first JSONL record
opencode opencode opencode --session <id> global SQLite database at $XDG_DATA_HOME/opencode/opencode.db (no filesystem detection — resume only by stored id)

New adapters implement the AgentAdapter trait in src/agents/ (eight methods, most with defaults).

Controlling roost (CLI / LLM)

A running roost can be driven programmatically over its control socket — the same binary in client mode:

roost list                                   # panes: id, adapter, cwd, status, …
roost spawn pi --cwd ~/api --input "run the tests, report pass/fail"
roost read 5                                  # a pane's current screen (default)
roost read 5 --tail 20                        # its last N lines (--full for the whole scrollback)
roost send 5 hello world --enter              # type into a pane (+ Enter)
roost send --all "standup: reply with status" --enter  # broadcast to every reachable pane
roost status 5                                # working | waiting | needs_input | …
roost wait 5 --until waiting --timeout 300    # block until the agent finishes
roost fork 5                                  # a sibling in the same context
roost close 5 [--force]
roost focus 5                                 # focus a pane: switch to its tab, land focus on it

(roost --help prints the same verb set, in a different order.)

Exit codes: 0 ok · 1 runtime error · 2 usage error · 3 wait timed out. The wait timeout is a distinct code so scripts can branch on it — wait && read needs to know the difference between a timeout and success.

wait is what turns "spawn then poll" into "spawn → await → read": block until a pane hits a status (or a timeout), so an orchestrator doesn't sleep-and-grep. When you spawn with initial_input, roost holds that input and delivers it after the pane's first PTY output, guaranteeing the CLI is alive and reading before a prompt lands — no silent loss like a script racing a not-yet-ready agent.

send --all is send's broadcast form, not a separate verb — it fans out to every running pane the caller may target, except the floating scratch shell: every spawned pane for the fleet token (float excluded), or just its own spawned subtree for a pane acting via its own $ROOST_TOKEN. Non-running panes are skipped, not errors; the reply reports which pane ids it reached. Audited like every control action, by shape only — broadcast len=<n> submit=<bool> -> ok count=<n> — the message text itself is never written to control.log.

This is how an LLM manages a fleet — an agent inside a pane can spawn and drive worker panes for its sub-agents, and you watch (and take over) the whole fleet live. See DESIGN-control.md.

Authorization is scoped by default, not sandboxed. A pane acting via its own $ROOST_TOKEN authenticates as that pane: its spawn/fork calls are scoped to the subtree it creates, and its actions are audited under that pane's id. That's convenience and defense-in-depth, not a hard security boundary — pi and Claude Code both ship a shell/exec tool, so any in-pane agent can cat the fleet token at <state>/control.token (0600, never placed in a pane's env, but readable by anything running as you) and drive the whole fleet with full reach. Treat every in-pane agent as capable of full control-plane access. The boundary roost does enforce is cross-UID: the socket and control.token are 0600 inside an owner-verified 0700 state dir, so no other user on the machine can drive your roost. Targeting is daemonless: an in-pane client finds its instance via $ROOST_SOCK automatically. Every control action is recorded in <state>/control.log (principal, verb, target, outcome — never the message text).

Architecture (ports & adapters)

The core never touches a PTY, socket, or the filesystem — it talks to traits in src/ports.rs, and every core behavior is unit-tested against in-memory fakes. Real I/O lives at the edges:

src/
  core/        the domain — pure, fully unit-tested
    layout.rs    split/stack/pane tree, ops, geometry
    workspace.rs tabs + (adapter, cwd, session-id) per pane
    status.rs    Working/NeedsInput/Waiting/Idle/Exited model
    app.rs       orchestration: App<B: PaneBackend>, actions, modes
    event.rs     event vocabulary (PTY output, exit, socket events)
  ports.rs     trait boundaries: PaneBackend, StateStore, Notifier
               (+ fakes for tests: FakePane, MemStore, RecordingNotifier)
  agents/      domain adapters per CLI: pi, claude, shell (AgentAdapter)
  infra/       production port implementations — all real I/O
    pty.rs       PaneBackend via portable-pty + vt100
    store.rs     StateStore via atomic workspace.json writes
    sock.rs      status socket listener (ndjson over unix socket)
    notify.rs    Notifier via terminal bell / macOS osascript
  ui/          presentation
    render.rs    ratatui drawing (generic over PaneBackend)
    input.rs     key → Action/bytes translation (pure)
    mouse.rs     hit-testing + wheel routing decisions (pure)
  main.rs      composition root: wires infra into core, runs the loop
vendor/vt100/  vendored vt100 with a scrollback-underflow fix (see below)

vendor/vt100: a fork of upstream vt100 0.15.2 carrying roost's SPEC-parity patch set — synchronized output (P1), live reflow (P5), focus reporting (P10), dim/strikethrough (P16), unicode-width parity (P17), REP (P19), stream effects (W3), OSC 9/52/777, DECSCUSR, and assorted hardening (including a saturating-subtraction fix for the visible_rows() scrollback-underflow panic upstream hits when scrolled back further than one screen height).

Roadmap status

M0 render core ✓ · M1 splits/tabs ✓ · M2 persistence + session detection ✓ · M3 status socket + badges ✓ · M4 stacks + resize ✓ · M5 picker, rename, scroll, notifications ✓ · fleet features (jump, feed, zoom, float, raw mode, keyboard copy, layouts, broadcast) ✓ · mouse, wheel routing and link-opening ✓ · native mouse selection ✓ · a drag holds the view still so you copy what you highlighted ✓ · adapters for pi, Claude Code, shell, codex, gemini and opencode ✓ · a small config.json to remap or disable individual keys ✓. What's left is deferred scope and deliberate choices, no known-broken defects — full detail: ROADMAP.md.


DESIGN-ui.md (design spec) · ROADMAP.md · LICENSE

Releases

Packages

Used by

Contributors

Languages