Skip to content

Architecture

Roy Padina edited this page Jun 5, 2026 · 1 revision

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 ↔ CLI contract

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)

Session names

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).

Status detection

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.

cwd decoding

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).

Fuzzy matching

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.

ink → spawn handoff

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.

Clone this wiki locally