A phone-sized terminal UI that watches any coding agents running on a box and keeps track of what they are doing — built for people who check in from a phone (Termius, Blink, iSH, any mobile ssh client) while Claude Code, Codex, aider, opencode or another agent works on their desktop.
portagent · 1▸ 0% 08:14 ◐ 2s
● claude·… 3m12s 12% Bash: cargo test --test auth
────────────────────────────────────────────────────────
· repo 2h04m Bash: cargo test --test auth
j/k nav · ⏎ detail · q quit
One row per live agent: status dot, name+repo, how long it has been running, CPU, and what it is doing right now (the last tool call or message, from its session transcript). Finished sessions from today sit below in dim — the "keep track" view.
Press ⏎ for the detail screen: pid, cpu/mem, repo, full cwd, session age,
the current request, and the recent transcript as a scrollable log.
claude · portagent
● running · pid 50423 · 3m12s
cpu 12% · mem 1%
repo: portagent
cwd: /Users/you/codebase/portagent
req: fix the flaky auth test
────────────────────────────────
03:12 tool Bash: cargo test --test auth
03:13 msg Running 42 tests, 3 failed…
03:15 tool Edit: auth.rs
▾ scroll · esc back
| Glyph | Meaning |
|---|---|
● green |
turn in progress, or agent active in the last 90s |
○ yellow |
alive but idle (waiting for input) |
⌨ yellow |
idle and in a tmux pane — answer it with i |
✗ red |
last turn ended in error |
· dim |
a finished session from today (history) |
When an agent finishes a turn it waits for your next line of input. If it
runs inside a tmux pane, portagent can deliver your answer: the row shows a
⌨, and pressing i opens a text box at the bottom of the screen.
portagent · 2▸ 78% 11:13 ◐ 2s
⌨ pi·port… 3h10m 78% what should the plan be?
⌨ pi·revi… 2h53m 0% what should the plan be?
────────────────────────────────────────────────
· pa-ans 35s what should the plan be?
i answer · j/k nav · ⏎ detail · q quit
Type the answer and press ⏎ — it is delivered with tmux send-keys to
the pane hosting that agent (the pane is found by walking the agent's
process ancestry against tmux list-panes), so the agent reads it exactly
as if you had typed in its window. esc cancels.
Agents not running in tmux show ○ idle and say so in the detail view —
answer those in their own terminal.
- Small screen first: every row and string is width-truncated before it is drawn, so a 30×12 portrait terminal never overflows or wraps.
- Diff rendering (OpenTUI): only changed cells produce ANSI output, so each 2s refresh over a slow mobile ssh link is a few dozen bytes, not a full redraw.
- No fragile key chords: single letters (
j k q r h), Enter, Esc, and1-9to jump straight to an agent. Mouse tap/scroll also works where supported (disable with--no-mouse). - Bare-Escape handling: Esc is recognized even when the terminal sends it alone (50ms disambiguation window, so arrow keys still work when sequences arrive split over a laggy link).
- Resize-safe: rotate the phone or open the soft keyboard mid-session; it re-sizes on the next tick.
- Tidy exit: quitting restores your terminal and prints the current picture, so the last thing on screen is still useful.
cargo install --path .
# binary: ~/.cargo/bin/portagent (~900KB)Or build and copy the binary anywhere (it has no runtime deps beyond the
system's ps, and lsof on macOS for the working directory).
A terminal with truecolor (Termius, iTerm2, WezTerm, Ghostty…) shows the full palette; without it OpenTUI falls back to 256/16 colors automatically.
portagent runs on the box your agents run on — from a phone you ssh into
it, so you need a route there and an SSH client app:
- A VPN between phone and machine. Easiest is Tailscale (free for personal use) on both — no port forwarding, no public IP, works from any network. Plain port-forwarding or a WireGuard tunnel works too.
- An SSH terminal app:
- Connect:
ssh box 'portagent'— or save the box as a host in the app and tap in.portagent --onceis a nice scrollback-friendly one-shot if you don't want a live screen.
portagent # live TUI (auto-refresh every 2s)
portagent -i 5 # slower polling for a laggy link
portagent --once # scan once, print, exit — quick check
portagent --today # today's session history, exit
portagent --no-mouse # some mobile terminals lack mouse support
portagent --no-alt-screen # render in scrollback instead of alt screen
portagent --retention 7 # keep ledger entries for 7 days (default 14)
portagent --data-dir /path # override ledger locationFrom a phone:
ssh box 'portagent' # live monitor
ssh box 'portagent --once' # one snapshot, readable in scrollback| Key | Action |
|---|---|
j / ↓ / Tab |
next agent |
k / ↑ / ⇧Tab |
previous agent |
⏎ / l / → |
agent detail |
esc / h / ← |
back (in detail) / quit (in list) |
i |
answer the selected agent (when it shows ⌨) |
1-9 |
jump to agent N |
r |
rescan now |
q / ctrl-c |
quit |
A process-table scan for known agents — matched by process name or command
line, so native binaries, node …/cli.js invocations and bash-wrapper
setups are all caught:
claude · codex · aider · opencode · gemini · cursor · pi ·
generic long-running node / python / bun agent scripts.
For Claude Code, Codex and pi it additionally tails the session
transcripts (~/.claude/projects/…, ~/.codex/sessions/…,
~/.pi/agent/sessions/…) to show the last user request, the current tool
call, and whether a turn is in progress. Agents without a known transcript
format still get full process rows (cpu, mem, duration, repo).
Detection lives in src/agents.rs (detect_kind); transcript parsing per
agent goes in src/transcript.rs (Claude, Codex and pi are the reference
implementations — all three are append-only JSONL, tailed cheaply at 96KB).
Every session is remembered in ~/.portagent/ledger.json
($PORTAGENT_HOME to override): first/last seen, end time, peak CPU, last
message. Closed sessions are kept for 14 days (--retention <days>), capped
at 500 entries.
$ portagent --today
portagent — today 2026-08-15
● claude 3m12s · portagent Bash: cargo test --test auth
· codex 2h04m · ended 1h ago · website Added contact formThe plain-text --once / --today modes double as cron-friendly
check-ins.
ps (etime, macOS+Linux) ──► detection ──► transcript tail ──► ledger
│ │
└────────────── UI (OpenTUI diff render) ◄──────┘
- Process scan every interval (default 2s); transcripts read the last 96KB of the newest session file, so huge sessions stay cheap.
- Status logic: turn in progress or activity within 90s →
running; quiet with a closed turn →idle; errored turn end →error. - Subagent (
isSidechain) chatter is skipped — phone screens get the main thread's story.
The UI assumes ~30 columns × ~12 rows (hard minimum 10×3). At 44+ columns it
shows more of the message preview; below that it drops CPU from the row (it
stays in detail). History rows collapse to · name duration message.
cargo test # unit tests: ps/etime parsing, detection, transcripts, ledger, truncation
cargo clippy # zero warningsThe TUI is also exercised with a pty harness: rendering, key handling, detail view, narrow width (30×12), number-key jump, and mid-run resize all exit cleanly with no panics.