-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
A strict two-layer split keeps the logic reusable and testable.
| Layer | Role |
|---|---|
src/core/ |
Pure data — zero React/ink imports. Session scan & cwd-decode, streaming JSONL parse, live-PID status, git branch, the fuzzy matcher, TOML config, project scanner, launch planner. Unit-tested. |
src/cli/ |
The ink TUI (New + Resume tabs, peek, search) and the non-interactive subcommands. The only layer that touches presentation. |
gui/ |
A native SwiftUI menu-bar + window app. A thin client over agentctl gui … (JSON in, launch out) — imports none of the Node code. |
The GUI shells out to internal JSON commands and never reads ~/.claude or parses TOML itself:
| Command | Returns / does |
|---|---|
agentctl gui projects |
groups → dirs (branch, age), tools, defaultTool |
agentctl gui sessions |
resumable sessions (status, cwd, branch, cwd-confidence) |
agentctl gui peek --id <id> |
transcript tail as JSON turns |
agentctl gui new-dir --base <d> --name <n> |
mkdir -p, prints {path}
|
agentctl gui launch --dir <d> [--tool <t>] |
open the tool in <d> in the configured terminal |
agentctl gui resume --id <id> |
resume a session in the configured terminal |
agentctl gui profiles / annotate / recap
|
Claude accounts; name/note/flags/done/hide/delete/remind/due; cached recap |
agentctl gui terminals / set-terminal
|
terminal-picker read / write |
agentctl gui config-get / config-save
|
full config read / write (shared with the TUI) |
Derived from the JSONL in priority order: a /rename custom title → an auto-generated AI title → the first user prompt (with command/system tags stripped) → (no prompt yet).
A session is busy/idle only when a matching ~/.claude/sessions/<pid>.json exists, kill -0 <pid> succeeds, and ps -o comm= -p <pid> matches /claude/i. Otherwise inactive. No polling/file-watcher — r (TUI) or reopening (GUI) refreshes.
Claude Code stores sessions under ~/.claude/projects/<encoded-cwd>/, encoding the path with - as both the leading marker and the separator — so a real - in a folder name (e.g. My-App-Repo) is ambiguous. The decoder walks candidate paths with existsSync pruning. When it can't find a unique match it returns cwdDecodeConfident: false, surfaced as the ⚠ glyph; resume then refuses to guess and requires --cwd <override> (or a double-Enter confirm in the UI).
An fzf-v1-style subsequence scorer (src/core/fuzzy.ts) ranks New dirs and Resume sessions — boundary + consecutive-run bonuses, earlier-match preference, no substring pre-filter. The Swift GUI ships a byte-for-byte port so both surfaces rank identically.
The TUI never spawnSyncs inside a useInput callback. A screen sets a module-level pending launch and calls exit(); the runner awaits waitUntilExit(), drains stdin (setImmediate + pause + setRawMode(false)), then spawns — avoiding type-ahead corruption when handing the TTY to claude.
agentctl · MIT © Roy Padina · Support on Ko-fi ☕