A Rust TUI to triage parallel Claude Code and Codex CLI sessions across tmux panes — sort by attention priority, optionally let a Sonnet auditor handle routine approvals so you don't babysit every prompt.
Reads files the agents already write:
Claude Code:
~/.claude/sessions/<pid>.json— discovery +idle/busystatus~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl— recap, prompts, tool calls, per-message usage
Codex CLI:
~/.codex/sessions/**/rollout-*.jsonl— prompts, messages, tool calls, token counts~/.codex/state_5.sqlite— native thread titles, agent labels, parent/child thread roots
Shared:
tmux list-panes -a— joined via process-ancestor walk from the agent PID
Different shape from agentop (process-centric, token/cost focused). triage is content-centric: the headline column is the recap, the detail pane shows what the agent is doing and why, and auto mode (off by default) routes safe tool approvals through an LLM auditor.
Sessions sorted by attention priority — block (needs you now) at the top, stale at the bottom — each row carrying the agent's own recap of what it's doing.
The demo runs entirely on synthetic fixtures, not real sessions. To regenerate it, see
scripts/demo/:scripts/demo/seed.shbuilds a sandboxed$HOME+ idle tmux panes, then runtriageand screenshot it (or record with asciinema for an animated version).
# Homebrew (macOS) — recommended; bundles the notification helper
brew install inkless/triage/triage
# crates.io — installs the `triage` binary (crate is named triage-tui)
cargo install triage-tui
# From source (development)
cargo install --path .Then:
triage # launch the TUI
triage --probe # print the joined session table once (no TUI)
triage agents --json # list peer agents and guarded send status
triage send --to '%42' --message "can you check this?" # message a live agent
triage launch --cwd "$PWD" --provider codex # launch a new detached agent window
triage notify "..." # one-shot ntfy push using config.toml's [ntfy] block
triage cost # daily/weekly Claude spend rollup across all transcriptsAfter reinstalling, restart any already-running triage TUI pane so it picks up the new binary and state-file behavior.
The notify subcommand lets any agent, hook, or shell script ping the user's phone without re-implementing ntfy auth:
triage notify "build green on PR #123" # positional
triage notify --title "deploy done" "all stage smoke ok" # title override
git log --oneline -3 | triage notify --title "shipped" - # stdinBlocks until curl confirms the POST (5s timeout); exit status reflects the outcome. Requires an [ntfy] block in ~/.config/triage/config.toml (see Configuration).
The agents / send subcommands are for agent-to-agent coordination through
triage's existing tmux/session snapshot. When run from inside tmux, agents
omits the caller's own pane by default; pass --include-self for a full
inventory/debug view.
triage agents --json
triage send --to '%42' --from TRI-106 --message "Can you check whether your branch touched codex.rs?"
triage send --to '%42' --from TRI-106 --file /tmp/question.md
printf '%s\n' "short message" | triage send --to '%42' --from TRI-106 -send recomputes a fresh snapshot, refuses ambiguous/no-pane/unknown targets,
and denies delivery when the target is on a visible Claude/Codex permission
prompt. Working sessions are allowed when no prompt is visible; the receiving
agent's terminal may queue the submitted line until its next input slot. The
message body is pasted through an internal tmux buffer and submitted with a
separate Enter, so agents can use either one-line text or a file/stdin body
without caring about the transport.
launch is the reusable tmux/process primitive behind the TUI's N shortcut
and future mb-work fleet launch integration:
triage launch --cwd /path/to/repo --provider claude --window-name agent-TRI-114
triage launch --cwd /path/to/repo --provider claude \
--append-system-prompt /tmp/system-prompt.md \
--after-boot "Please pick up TRI-114 and report status through triage send."
triage launch --cwd /path/to/repo --provider codex --command "codex --model gpt-5"The CLI form creates the new tmux window detached and prints the launched pane
id. --provider selects the configured default command (claude or codex);
--command overrides it and can infer the provider when omitted.
--append-system-prompt is applied only for Claude by expanding the file
contents at launch time, while Codex ignores it. --after-boot waits for the
agent shell to settle, presses Enter once, then pastes and submits the text via
the same tmux buffer path used by triage send.
cargo build auto-builds the macOS notification helper (triage-notify.app) under scripts/triage-notify/ via build.rs, then stages a copy to ~/.config/triage/triage-notify.app. The staged location is what the cargo-installed binary at ~/.cargo/bin/triage finds at runtime — without it, notifications fall back to osascript which shows a "Show" button that routes to Script Editor. Build manually if needed:
bash scripts/triage-notify/build.shRequires Xcode CLI tools (xcode-select --install) for swiftc. The .app is intentionally not committed; it's regenerated locally.
# Desktop: switch to the long-lived triage pane (preserves multi-pane layout).
bind-key -n M-t run-shell "triage --jump-to-self"
# Mobile / SSH on phone: switch to the long-lived triage pane AND zoom it.
bind-key -n M-p run-shell "triage --jump-to-self --zoom"
Desktop (M-t): jumps to the triage pane in your existing layout. Inside triage, Enter does a normal switch-client + select-pane to the target — no zoom, your multi-pane layout stays intact.
Mobile (M-p): jumps to the triage pane and tmux resize-pane -Zs it so triage fills the phone screen. Inside triage, Enter jumps to the target pane and zooms it (auto-detected — see below). Net effect: every M-p leaves you on a full-screen pane; the gesture toggles between "triage zoomed" and "current session zoomed." Ctrl-b z to un-zoom and see the multi-pane layout. (Letters pass Alt cleanly across mobile terminals; symbols like / often don't on iOS, hence M-p over M-/.)
Zoom-on-Enter is auto-detected by triage's current pane width. Tmux resizes panes to the smallest attached client, so when you're on a phone the pane is narrow (<100 cols) → Enter zooms; when on desktop it's wide → Enter doesn't zoom. No flag needed, no per-device launch dance. If you want to force zoom on a wider pane, pass --zoom-on-jump. --exit-on-jump (popup pattern, exits triage after Enter) implies zoom too.
Install the PreToolUse hook so Claude manual a/d and auto-mode verdicts route through Claude's clean approval channel instead of tmux send-keys:
triage --install-hooks # idempotent merge into ~/.claude/settings.json
triage --install-hooks --dry-run # preview
triage --uninstall-hooks # removeThe main header shows behavior-changing modes persistently, e.g.
AUTO on · phone off. Auto mode includes the in-flight auditor count as
AUTO on · N audits while auditor workers are running.
General:
↑↓ / j k move selection
gg / G jump to top / bottom
⏎ jump to selected session's tmux pane
space toggle detail panel
p toggle live preview of the selected pane — updates as you navigate
> flip preview between right (compact table) and bottom (full table)
? show all keybindings
q / Ctrl-C quit
Approve / deny / mute / watch:
a approve (selected session must be paused on a permission prompt)
d deny
A toggle autonomous mode (off → on)
P toggle ntfy phone push (on by default; Mac banners unaffected)
r reply to selected agent with a one-line user message
m mute / unmute selected session
w watch / unwatch selected session — sticky; fires a "finished" banner on every work → done transition until toggled off
N pick a known cwd and launch a new configured agent in a new tmux window
Filter & overlays:
/ start filter (matches name + cwd, case-insensitive)
in edit mode: type to filter · ↑↓ navigate · ⏎ jump to selection
Esc clear · ^W delete word · ^U clear line
R rename selected row in triage only; ^U clears the old value while editing
l open / close audit-log overlay (auto-mode decision history); H also works
$ open / close cost overlay (cross-session spend rollup)
Overlay navigation (H / $):
j k / ↑↓ scroll one line
^d / ^u half-page
gg jump to top
G jump to bottom
Esc close
Toggle preview with p. Triage captures the selected tmux pane's visible
screen and renders it beside the table on wide terminals, or below the table on
narrow terminals. The preview follows selection as you navigate. Press > to
flip between right-docked and bottom-docked preview.
Press r to compose a one-line reply to the selected agent. When preview is
open, the composer appears inside the preview panel so the target pane and the
outgoing message stay together. When preview is closed, triage shows a yellow
compose bar above the footer. Enter sends via triage send's tmux buffer
path; Esc cancels.
Default sort order, highest-attention first:
| State | Meaning |
|---|---|
error |
Last stop_hook_summary reported errors. |
block |
Paused on a permission prompt (or status=busy + no events for 90s). |
done |
Stop within last 3 min — awaiting next prompt. |
work |
status=busy and progressing. |
idle |
Stop >3 min and <30 min ago. |
long |
Stop >30 min ago. |
fresh |
No user prompts seen yet. |
stale |
No transcript activity >24h. |
? |
Indeterminate. |
Toggle with space. Three zones:
- Header —
state · pane · model (1M) · uptime. - Body — agent's latest text (Claude's reasoning, often the why before the next tool call), pending tool + full input, recap (
away_summary), last user prompt. - Stats footer — auditor decision (when auto mode is on, with cost + duration), session cost + tokens + context-window % (yellow ≥80%, red ≥95%), event timing.
Codex sessions show up beside Claude rows with the cx provider label. Filter matches cx, codex, row name, and cwd.
Triage discovers live Codex processes from ps, finds the active rollout jsonl held open by the process, and joins that process back to tmux. Titles come from Codex's native thread metadata when available. R creates a triage-local alias when the native title is too long; aliases are keyed by Codex's root thread id, so a renamed spawned-review session continues to label the parent row.
Blocked Codex detection uses two signals together: the latest unfinished tool call must request escalation, and the visible tmux pane must show Codex's approval UI. Manual a/d and auto mode answer that visible prompt through tmux. Codex does not currently have a PreToolUse hook path like Claude, so triage validates the prompt is still present before sending keys; approve requires the Yes option to be selected.
Known limits:
- The
$overlay shows live Codex token/context usage, but Codex dollar cost is unavailable from local data. - The
triage costCLI command is still Claude-dollar based. - Codex approval routing depends on the visible native prompt, so it can only answer the live tmux pane.
- Restart old triage panes after
cargo install --path .; an already-running TUI keeps its old binary and in-memory state.
Toggle with A. Off by default; persists across restart.
When on, each refresh spawns claude -p --model claude-sonnet-4-6 --tools "" --name triage-auditor for any Blocked Claude or Codex session with a captured tool request. The auditor is Claude Sonnet for both providers; triage does not spawn Codex as the reviewer. The auditor receives the session's recent recap + intent + tool + full tool input and returns APPROVE / DENY / WAIT with a one-line reason.
APPROVE/DENYroute through the same machinery as manuala/d(Claude hook decision file when available, tmux send-keys fallback; Codex visible-prompt routing).WAITsurfaces the reason in the detail pane and leaves the prompt for human review.
Decisions append to ~/.config/triage/auto-decisions.jsonl (one JSON object per line, includes cost + duration). Press l (or H) for the audit-history overlay.
Safety: the prompt explicitly approves routine repo work (Read/Glob/Grep, builds, tests, git ops, gh pr create/edit, file edits in the repo) and denies destructive actions (rm -rf, force-push to main, dropping data, sudo, shared-infrastructure writes). It WAITs when the action itself is in a middle zone — unfamiliar API, unreadable Bash flags, paths outside the repo. Customize via ~/.config/triage/auditor-prompt.md (or $TRIAGE_AUDITOR_PROMPT_FILE).
Per-call budget is --max-budget-usd 1.00. Typical Sonnet round-trip: 10–25s and $0.02–0.05 per audit.
For Claude, a/d in hook mode and auto mode both deliver decisions through a PreToolUse hook. The hook is a small bash script embedded in the binary; --install-hooks writes it to ~/.config/triage/hooks/triage-preuse.sh and merges the path into ~/.claude/settings.json. No source-repo dependency — cargo install triage users can delete their checkout and the hook keeps working.
triage --install-hooks # idempotent install (also re-installs an updated hook on triage upgrade)
triage --install-hooks --dry-run # preview both the file write and the JSON merge
triage --uninstall-hooks # remove from settings.json + delete the script fileThe hook is zero-cost when triage isn't running (single file-existence check + kill -0, ~3ms). With auto mode on, it waits up to 60s (vs the default 3s) for the auditor's verdict via a claim-file handshake. Re-running --install-hooks after a triage upgrade refreshes the on-disk script if its content changed.
Without the hook installed, h falls back to tmux mode which sends keystrokes to Claude's pane — works regardless of managed-policy settings. Codex approval routing always uses the tmux path because there is no Codex hook integration yet.
Detail pane shows approximate Claude session cost (per-message usage × per-model rates, deduplicated by message.id) and context-window occupancy as current / total (%). Codex sessions show token usage and context occupancy, but not dollars.
Context-window detection precedence:
TRIAGE_CONTEXT_WINDOWenv var (explicit override, e.g.1000000)- Session's own
modelcarries[1m] ~/.claude/settings.json"model"field has[1m](e.g."opus[1m]") — the deterministic global signal- Per-session peak input tokens >210k → 1M
- Fleet-wide peak >210k → 1M (any sibling session's evidence)
- Default 200k
Cost figures are approximate; cross-check Claude rows against /cost for the canonical per-session total. Codex local rollouts expose token counts, cached input tokens, and context windows, but not billable dollars.
Hand-editable TOML at ~/.config/triage/config.toml. All sections + fields are optional — an empty file (or no file) is valid. Loaded once at startup; restart triage to pick up changes.
# Phone push notifications via self-hosted ntfy. See
# memory-bank/projects/triage/specs/notify-self-host.md for the homelab setup.
[ntfy]
url = "https://ntfy.guangda.me/triage-alerts"
user = "triage"
token = "..."
[thresholds]
mobile_width = 140 # cols — auto-zoom-on-jump fires below this
refresh_seconds = 2 # polling fallback when fs events are quiet
[notifications]
terminal_bundle = "net.kovidgoyal.kitty" # override click-to-jump sender
[model]
context_window = 1000000 # bypass auto-detect (use the 1M window)
[approval]
mode = "hook" # "hook" or "tmux"; Claude only. Codex approvals always use tmux.
[new_agent]
provider = "claude" # "claude" or "codex"; defaults to claude
command = "claude" # optional override; provider default is "claude" / "codex"
window_name = "agent-{provider}-{cwd_basename}"Pressing N in the TUI opens a cwd picker built from current triage sessions,
then runs the same launch path in a new attached tmux window with -c <cwd>.
If there are no sessions yet, the picker offers $HOME.
Security: chmod 600 ~/.config/triage/config.toml. Triage refuses to load and warns if perms allow group/other read — the [ntfy].token field would otherwise be leakable.
The auditor system prompt lives separately at ~/.config/triage/auditor-prompt.md (markdown, easier to hand-edit than embedded TOML strings). Empty/missing falls through to the compiled-in default.
- Discovery + tmux join. Claude uses sessions JSON keyed by PID; Codex uses live process file handles into rollout jsonl files. Tmux's
pane_pidis usually the shell, so triage walks the process tree upward until an ancestor matches apane_pid. - Transcript pairing. Claude's active pane gets the jsonl with the newest qualifying user-text; remaining sessions pair greedily by mtime. Survives
/clear. Codex rollouts are discovered from the live process directly. - Mechanical extraction in the live path. Claude recap is
away_summary; Codex uses the latest rollout messages and native thread title metadata. The auditor is opt-in and runs only on Blocked sessions. - Hook is optional. Triage works without any
~/.claude/settings.jsonedits — the hook is needed only for clean Claude approve/deny + auto-mode decision delivery.
v0.2-dev — local single-machine, macOS-tested. Auto mode + per-session cost + context-window % + audit-log overlay shipped. Not yet on crates.io.
ratatui 0.30 + crossterm 0.29 + notify 8.2 + serde_json + libc. Rust edition 2024.
MIT OR Apache-2.0