AI agent monitor for your terminal. Like btop++, but for AI coding agents.
Supports Claude Code, Codex CLI, and OpenCode sessions.
English is mandatory for all project-facing work and communication.
- Write all source code, comments, tests, fixtures, documentation, examples, configuration text, scripts, and user-facing strings in English.
- Use English for every GitHub artifact: issue titles and bodies, issue comments, pull request titles and descriptions, review comments, commit messages, branch names, release notes, changelogs, discussions, labels, milestones, and workflow or CI messages.
- Do not use non-English text in repository content or GitHub communication unless it is an exact external identifier, a required protocol value, or a direct quote needed for context.
- When quoting or preserving non-English input, add an English explanation and keep the non-English text as short as possible.
- If a contributor opens an issue, comment, or review in another language, respond in English and continue the thread in English.
src/
├── main.rs # Entry, terminal setup, event loop, --setup flag
├── app.rs # App state, tick logic, key handling, summary generation
├── setup.rs # StatusLine hook installation (abtop --setup)
├── ui/
│ └── mod.rs # All panels in single file: header, context, quota,
│ # tokens, projects, ports, sessions, footer
├── collector/
│ ├── mod.rs # MultiCollector orchestration, orphan port detection
│ ├── claude.rs # Claude Code: session discovery, transcript parsing
│ ├── codex.rs # Codex CLI: session discovery via ps+lsof, JSONL parsing
│ ├── opencode.rs # OpenCode: session discovery via ps + SQLite DB parsing
│ ├── process.rs # Child process tree (ps) + open ports (lsof) + git stats
│ └── rate_limit.rs # Rate limit file reading (~/.claude/abtop-rate-limits.json)
└── model/
├── mod.rs # Re-exports
└── session.rs # AgentSession, SessionStatus, RateLimitInfo,
# ChildProcess, OrphanPort, SubAgent
┌─ ¹context (token rate sparkline + per-session context bars) ─────────┐
│ ▁▃▅▇█▇▅▃▁▃▅▇██ S1 abtop ████████ 82% │
│ token rate (200pt history) S2 prediction █████████91%⚠ │
│ S3 api-server ███ 22% │
└──────────────────────────────────────────────────────────────────────┘
┌─ ²quota ─────┐┌─ ³tokens ───┐┌─ projects ───┐┌─ ⁴ports ──────────┐
│ CLAUDE ││ Total 1.2M ││ abtop ││ PORT SESSION CMD │
│ 5h ████ 35% ││ Input 402k ││ main +3 ~18 ││ :3000 api-srv node│
│ resets 2h ││ Output 89k ││ ││ :8080 predict crgo│
│ 7d ██ 12% ││ Cache 710k ││ prediction ││ │
│ ││ ▁▃▅▇█▇▅▃▁▃▅││ feat/x +1~2 ││ ORPHAN PORTS │
│ CODEX ││ Turns: 48 ││ ││ :4000 old-prj node│
│ 5h █ 9% ││ Avg: 25k/t ││ api-server ││ │
│ 7d ██ 14% ││ ││ main ✓clean ││ │
└──────────────┘└─────────────┘└──────────────┘└────────────────────┘
┌─ ⁵sessions ─────────────────────────────────────────────────────────┐
│ ►*CC 7336 abtop ● Work opus 82% 1.2M 48 Edit src/pay.rs │
│ >CD 8840 pred ◌ Wait sonn 91% 340k 12 waiting │
│ ─────────────────────────────────────────────────────────────────── │
│ SESSION 7336 · /Users/graykode/abtop │
│ Stripe payment integration... │
│ └─ Edit src/pay.rs │
│ CHILDREN: 7401 cargo build │
│ SUBAGENTS: explore-data ✓12k · run-tests ●8k │
│ MEM 4f · 12/200 │ v2.1.86 · 47m │
└──────────────────────────────────────────────────────────────────────┘
Panel rendering priority (top to bottom):
- Sessions — always visible, gets priority allocation (min 5 rows, ideal = 2/session + 7)
- Mid-tier (quota, tokens, projects, ports) — split equally, shown if space allows
- Context — only renders when sessions have ideal height AND surplus >= 5 rows
- Header (1 row) + Footer (1 row) — always present
Panel descriptions:
- ¹context: Left = token rate braille sparkline (200-point history). Right = per-session context % bars with yellow/red warning.
- ²quota: Claude + Codex rate limit gauges side-by-side (5h and 7d windows with reset countdown). Quota is intentionally limited to Claude and Codex; do not add an OpenCode row unless OpenCode exposes a reliable account-level provider rate-limit source.
- ³tokens: Total token breakdown (in/out/cache) + per-turn sparkline for selected session.
- projects (always visible): Per-project git branch + added/modified file counts.
- ⁴ports: Agent-spawned open ports + orphan ports (from dead sessions). Conflict detection.
- ⁵sessions: Full-width panel below mid row. Session list table (top) + selected session detail (bottom), separated by divider.
All read-only from local filesystem + ps + lsof. No API calls, no auth.
Discovery strategy:
- Find running
claudeprocesses viaps - Map PID → open files/directories via
lsof - Infer Claude config roots from open paths that contain
sessions/andprojects/ - Read
{config-root}/sessions/{PID}.json, falling back to scanning session files for the matching embedded PID - Parse
{config-root}/projects/{encoded-path}/{sessionId}.jsonl
Fallback config roots are still scanned: ~/.claude, direct home profile roots matching ~/.claude-* when they contain both sessions/ and projects/, claude_config_dirs from ~/.config/abtop/config.toml, abtop's own CLAUDE_CONFIG_DIR, and on Linux any CLAUDE_CONFIG_DIR read from /proc/{pid}/environ.
Session file format:
{ "pid": 7336, "sessionId": "2f029acc-...", "cwd": "/Users/graykode/abtop", "startedAt": 1774715116826, "kind": "interactive", "entrypoint": "cli" }- ~170 bytes. Created on start, deleted on exit.
- Verify PID alive with shared
psdata containing aclaudebinary. - Skip sessions whose PID descends from abtop's own
claude --printsummary children without hiding user-spawned non-interactive sessions.
Path encoding: /Users/foo/bar → -Users-foo-bar
Key line types:
assistant (tokens, model, tools):
{
"type": "assistant",
"timestamp": "2026-03-28T15:25:55.123Z",
"message": {
"model": "claude-opus-4-6",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 2,
"output_tokens": 5,
"cache_read_input_tokens": 11313,
"cache_creation_input_tokens": 4350
},
"content": [
{ "type": "text", "text": "..." },
{ "type": "tool_use", "name": "Edit", "input": { "file_path": "src/main.rs", ... } }
]
}
}user (prompts, version):
{ "type": "user", "timestamp": "...", "version": "2.1.86", "gitBranch": "main", "message": { "role": "user", "content": "..." } }last-prompt (session tail marker):
{ "type": "last-prompt", "lastPrompt": "...", "sessionId": "..." }- Size: 1KB–18MB. Append-only, new line per message.
- Reading strategy: On first discovery, scan full file to build cumulative token totals. Then watch file size — on growth, read only new bytes appended since last read (track file offset). This gives both lifetime totals and real-time updates without re-reading.
- Partial line handling: new bytes may end mid-JSON-line. Buffer incomplete lines until next read.
- File rotation: if file shrinks (session restart), reset offset to 0 and re-scan.
Discovery strategy:
- Find running
codexprocesses viaps - Map PID → open
rollout-*.jsonlfile vialsof - Parse JSONL for
session_meta,token_count(includes rate_limits),agent_messageevents - Detect finished sessions: scan today's directory for JSONL < 5 min old not owned by running process
Rate limits extracted from token_count events:
{
"rate_limits": {
"limit_id": "codex",
"primary": { "used_percent": 9.0, "window_minutes": 300, "resets_at": 1774686045 },
"secondary": { "used_percent": 14.0, "window_minutes": 10080, "resets_at": 1775186466 },
"plan_type": "plus"
}
}- Discover running
opencodeprocesses via sharedpsdata. - Read recent sessions from OpenCode's SQLite DB through
sqlite3 -readonly -json. - Match live PIDs to DB sessions by process cwd. OpenCode does not expose a PID/session mapping, so when multiple DB rows share one cwd, only live PIDs should be assigned and older rows should not be shown as live duplicates.
- OpenCode contributes session/token/project/port data, but not quota data. Quota remains Claude + Codex only.
agent-{hash}.jsonl— same JSONL format as main transcriptagent-{hash}.meta.json—{ "agentType": "general-purpose", "description": "..." }
ps -eo pid,ppid,rss,%cpu,command # All processes
lsof -i -P -n -sTCP:LISTEN # Open ports- Build parent→children map from ppid
- Map listening PID → parent agent PID → session
git -C {cwd} status --porcelain # added/modified file counts- Path:
~/.claude/projects/{encoded-path}/memory/ - Count files in directory + lines in
MEMORY.md
NOT in transcript JSONL. Collected via StatusLine mechanism.
abtop --setup automates this: creates a script at ~/.claude/abtop-statusline.sh that writes rate limit JSON to ~/.claude/abtop-rate-limits.json, and registers it in ~/.claude/settings.json.
File format read by abtop:
{
"source": "claude",
"five_hour": { "used_percentage": 35.0, "resets_at": 1774715000 },
"seven_day": { "used_percentage": 12.0, "resets_at": 1775320000 },
"updated_at": 1774714400
}- Rejects stale data (> 10 minutes old).
rate_limitsonly present for Pro/Max subscribers.- Account-level metric, shared across all sessions.
- Show "—" when not configured or data unavailable.
~/.claude/stats-cache.json— daily aggregates. Only updated on/stats, NOT real-time.~/.claude/history.jsonl— prompt history with sessionId.
● Working = PID alive + transcript mtime < 30s ago
◌ Waiting = PID alive + transcript mtime > 30s ago
✗ Error = PID alive + last assistant has error content
✓ Done = PID dead (detected via kill(pid, 0) failure)
Done detection: session files are deleted on normal exit, but may linger briefly or survive crashes. When PID is dead but file exists, show as Done and clean up on next tick.
PID reuse risk: verify PID is still the expected agent process (Claude, Codex, or OpenCode) by checking ps -p {pid} -o command=. Don't trust PID alone.
Current task (2nd line under each session):
- Working → last
tool_usename + first arg (e.g.Edit src/main.rs) - Waiting → "waiting for user input"
- Error → last error message (truncated)
- Done → "finished {duration} ago"
Known limitations (all heuristic):
- Cannot distinguish model-thinking vs tool-executing vs rate-limit-waiting vs permission-prompt
- "Waiting" may be wrong if a long-running tool (cargo build, npm test) is running
- Status is best-effort, not authoritative
Each session gets a one-line summary title generated via claude --print:
- Spawned as background process with 10s timeout
- Rejects generic/empty output; falls back to sanitized first prompt (28 chars)
- Cached to
~/.cache/abtop/summaries.json(persists across runs) - Max 3 concurrent summary jobs, max 2 retries per session
Not provided in data files. Derive:
- Window size: hardcode by model name
claude-opus-4-6→ 200,000 (default)claude-opus-4-6[1m]→ 1,000,000claude-sonnet-4-6→ 200,000claude-haiku-4-5→ 200,000
- Current usage: last
assistantline'sinput_tokens + cache_read_input_tokens.cache_creation_input_tokensis intentionally excluded — on compaction turns the same tokens can be reported as bothcache_creationandcache_read, and summing all three double-counts (#54). Matches Claude Code's own statusline and the Codex collector. - Percentage: current_usage / window_size * 100
- Warning: yellow at 80%, red at 90%, ⚠ icon at 90%+
Tracks child processes that have open ports. When a parent session dies but the child process remains alive and listening:
- Added to
orphan_portslist automatically - Displayed in ports panel under "ORPHAN PORTS" section
- Can be killed via
X(Shift+X) with safety checks (fresh port scan + PID command verification before SIGKILL)
| Key | Action |
|---|---|
↑/↓ or k/j |
Select session in list |
Enter |
Jump to session terminal (cmux / tmux / iTerm2) |
x |
Kill selected session (SIGKILL) |
X |
Kill all orphan ports |
q |
Quit |
r |
Force refresh |
- Rust (2021 edition)
- ratatui + crossterm for TUI
- serde + serde_json for JSON/JSONL parsing
- chrono for timestamp formatting
- dirs for home directory resolution
- Polling intervals (staggered to avoid freezes):
- Session scan + transcript tail: every 2s
- Process tree (ps): every 2s
- Port scan (lsof) + git status + rate limits: every 10s (5 ticks)
<type>: <description>
Types: feat, fix, refactor, docs, chore
cargo build # Build
cargo run # Run TUI
cargo run -- --once # Print snapshot and exit
cargo run -- --setup # Install StatusLine hook for rate limit collection
cargo run -- --exit-on-jump # Quit after Enter-jumping to a session terminal (for popup overlays)
cargo test # Tests
cargo clippy # Lint- Pick the target semver version and update both
Cargo.tomlandCargo.lock. - Verify the package locally:
cargo test cargo clippy -- -D warnings cargo build --release cargo publish --dry-run - Commit and merge or push the version bump to
main:git add Cargo.toml Cargo.lock git commit -m "chore: bump version to X.Y.Z" git push origin main - From a clean, up-to-date
main, create and push an annotated release tag:git tag -a vX.Y.Z -m "vX.Y.Z" git push origin vX.Y.Z - Watch the tag-triggered workflows:
gh run list --workflow Release --limit 5 gh run list --workflow "Publish to crates.io" --limit 5 release.ymlbuilds platform binaries, creates the GitHub Release, and updates the Homebrew formula.publish.ymlrunscargo publishto crates.io automatically.
Do NOT run cargo publish or gh release create manually — the CI workflows handle both.
Do NOT push the tag before the version bump is on main.
Do NOT reuse a release tag after a failed publish; bump to a new patch version instead.
- Gemini/Cursor support
- Cost estimation
- Remote/SSH monitoring
- Notifications/alerts
Enter focuses the terminal running the selected session's agent process.
The logic lives in src/jump/ as a registry of TerminalJumper adapters
(one file per backend). jumpers() is the single ordered source of truth;
resolve() walks it and the first applicable adapter wins.
Each adapter returns a three-way JumpAttempt:
NotApplicable— not this backend's terminal; try the next adapter.Jumped— focused successfully; stop.Failed(msg)— this backend owns the process but the focus command errored; stop and surface"<backend>: <msg>"in the status line.
Order (most specific first), mutually exclusive by controlling tty:
- cmux (
jump/cmux.rs) — readsCMUX_WORKSPACE_ID(a UUID cmux exports into every surface, inherited by the agent) from the process environment viaps eww, thencmux select-workspace --workspace <uuid>. - tmux (
jump/tmux.rs) — only when abtop itself runs inside tmux ($TMUX). Maps PID → pane viatmux list-panes -a -F '#{pane_pid} #{session_name}:#{window_index}.#{pane_index}'- process-tree descent, then
switch-client/select-window/select-pane. PID in no pane →NotApplicable(lets another backend try).
- process-tree descent, then
- iTerm2 (
jump/iterm2.rs) — resolves the PID's controlling tty (ps -o tty=), then AppleScript selects the session whosettymatches and brings its window/app to the front. First call triggers a one-time macOS Automation permission prompt; until granted,osascriptexits non-zero →Failed.
Parsing/registry logic is unit-tested in jump/mod.rs; the thin ps/osascript/
tmux I/O wrappers are verified manually.
abtop reads transcripts, prompts, tool inputs, and memory files. These may contain secrets.
--onceoutput: redact file contents from tool_use inputs. Show tool name + file path only, not content.- TUI mode: show tool name + first arg (file path), never show file contents or prompt text in session list.
- No network: abtop never sends data anywhere. All local reads.
- Exception: summary generation calls
claude --printlocally (no network by abtop itself, but claude may use its API).
- Transcript size: 1KB–18MB. On first load, full scan for totals. After that, track file offset and read only new bytes. Buffer partial lines.
- Session file deletion: files disappear when Claude exits. Handle
NotFoundbetween scan and read. - stats-cache.json is stale: only updated on
/statscommand. Don't use for live data. - Context window not in data: must hardcode per model. Will break if Anthropic/OpenAI add new models.
- Rate limit is account-level: shared across all sessions. Don't show per-session.
- Path encoding:
/Users/foo/bar→-Users-foo-bar. Used for transcript directory names. - Path encoding collision:
-Users-foo-bar-bazcould be/Users/foo/bar-bazor/Users/foo-bar/baz. Use session JSON'scwdas source of truth. - lsof can be slow: on macOS with many open files. Cache results, poll every 10s.
- Child process tree:
pgrep -Ponly gets direct children. Build full tree fromps -eo ppid. - Port detection race: a port can close between lsof and display. Show stale data gracefully.
- Subagent directory may not exist: only created when Agent tool is used. Check existence before scanning.
- Undocumented internals: all data sources are Claude Code/Codex implementation details, not stable APIs. Schema may change without notice. Defensive parsing with
serde(default)everywhere. - Terminal size: minimum 80x24. Panels degrade gracefully when small (context panel hidden first).
- PID reuse in port cache: invalidate cached ports when the set of tracked PIDs changes.
- Rate limit staleness: reject rate limit data older than 10 minutes.
/clear+ multi-PID same cwd: after/clear, Claude Code mints a newsessionId+.jsonlwithout rewritingsessions/{PID}.json. abtop overrides the stale sid by picking the newest transcript in the project dir, but this heuristic can't disambiguate ownership when two liveclaudePIDs share a cwd — so the override is disabled in that case and both sessions keep their original sid until exit. Use separate worktrees if live tracking is needed on both simultaneously.