-
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 agent-cli-menu 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 |
|---|---|
agent-cli-menu gui projects |
groups → dirs (branch, age), tools, defaultTool |
agent-cli-menu gui sessions |
resumable sessions (status, cwd, branch, cwd-confidence) |
agent-cli-menu gui peek --id <id> |
transcript tail as JSON turns |
agent-cli-menu gui new-dir --base <d> --name <n> |
mkdir -p, prints {path}
|
agent-cli-menu gui launch --dir <d> [--tool <t>] |
open the tool in <d> in the configured terminal |
agent-cli-menu gui resume --id <id> |
resume a session in the configured terminal |
agent-cli-menu gui terminals / set-terminal
|
terminal-picker read / write |
agent-cli-menu 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.
Agent CLI Menu · MIT © Roy Padina · Support on Ko-fi ☕