3.1 KiB
3.1 KiB
claude-thinking
TUI that displays Claude Code's API streams token-by-token (thinking, text, tool
calls) by acting as a pass-through proxy: Claude Code points ANTHROPIC_BASE_URL
at 127.0.0.1:8484, we forward everything verbatim to api.anthropic.com and
tee SSE responses into the UI. Never issue API requests of our own — zero
extra usage is the core constraint of this project.
Architecture
src/main.rs entry; tokio runtime for proxy task, TUI on main thread; --headless mode
src/proxy.rs axum fallback handler: buffers request body (for session metadata),
forwards via reqwest, streams response back unbuffered, tees SSE
src/sse.rs incremental SSE parser; tolerant of chunk splits mid-event/mid-UTF-8
src/app.rs Arc<Mutex<App>> shared state; Tap = one in-flight tapped request,
translates SSE events → session Entries (Drop closes it out)
src/ui.rs ratatui rendering @ ~30fps; session list + scrollable feed
Data flow: proxy task parses SSE chunks → Tap::handle() mutates shared state →
UI thread redraws on its own tick (no channel; just the mutex).
Key invariants
- Latency-neutral pass-through: response bytes are forwarded as-is, never buffered or rewritten. Auth headers pass through untouched. If the tap code panics or misparses, the proxy must still relay bytes (tee is best-effort).
accept-encodingis stripped from forwarded requests so the upstream sends identity encoding we can parse in transit. Don't "fix" that.- Hop-by-hop headers (
content-length,transfer-encoding, etc.) are stripped both directions; hyper re-frames. - Sessions are keyed by the UUID after
session_in requestmetadata.user_id. Concurrent requests (subagents) share a session but eachTaptracks its own current entry index — entries/sessions are append-only, so indices stay stable. - Tool input streams as raw JSON fragments; pretty-printed only on
content_block_stop. Text re-renders markdown every frame, so partial markdown self-heals.
Gotchas
tui-markdownis pinned=0.3.5: later versions useratatui-core(0.30 alpha types), incompatible with ratatui 0.29.- ratatui needs feature
unstable-rendered-line-infoforParagraph::line_count(used for follow/auto-scroll). - reqwest is
default-features = false+rustls-tls,stream— don't enable compression features (would re-add accept-encoding). - Bind failures (port in use) only surface in the TUI status bar; check for a
stale
claude-thinkingprocess holding 8484. - Testing: SSE parser has unit tests (
cargo test). For a live pass-through check:--headless, then POST to127.0.0.1:8484/v1/messageswithout auth — a relayed 401 from Anthropic proves the round-trip. The TUI can't run in a non-tty.
Not yet handled (known MVP limits)
- Non-streaming requests pass through untapped (e.g.
count_tokens). - Sessions are never pruned; long sessions re-render fully each frame.
- Tool results only appear once the next request fires; if the session ends right after a tool call, that result is never seen. Output is what Claude Code sends the model (i.e. post-truncation).