Files
claude-cloak/CLAUDE.md
2026-06-11 09:11:00 +02:00

4.9 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
src/term.rs    embedded claude pane: spawns `claude --session-id <uuid>` in a
               portable-pty routed through the proxy; wezterm-term models the
               screen (and answers terminal queries); renderer paints cells
               into the ratatui buffer

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-encoding is 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 session UUID in request metadata.user_id — Claude Code ≥2.1.x sends a JSON blob with "session_id":"<uuid>", older builds user_…_session_<uuid>; proxy::session_key handles both. Concurrent requests (subagents) share a session but each Tap tracks its own current entry index — entries/sessions are append-only, so indices stay stable.
  • The embedded pane's session is matched by the --session-id UUID we generate; the tap drives pane behavior: grows it for AskUserQuestion / ExitPlanMode (sized from the question's option count) before Claude Code renders the prompt, shrinks when the tool_result echoes back, and schedules a ctrl-l transcript wipe 400ms after each turn (the pane is prompt-only; the feed shows the context).
  • 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-markdown is pinned =0.3.5: later versions use ratatui-core (0.30 alpha types), incompatible with ratatui 0.29.
  • wezterm-term/wezterm-surface are not on crates.io: pinned to a git rev of the wezterm monorepo (keep both revs identical).
  • The pane's render window crops Claude Code chrome by position (term.rs: BOTTOM_CROP, start ≥ 2, PTY_PAD) — tuned to the current Claude Code UI; retune there if an update adds/removes chrome rows.
  • Keybindings avoid Alt entirely: on layouts like dk_mac_fixed, Alt composes characters (alt-c = ©) and never reaches the app as a modifier. Pane keys: F2 toggle, ctrl-↓ focus claude, ctrl-↑ focus feed. CT_DEBUG_KEYS=1 shows raw key events in the status bar.
  • ratatui needs feature unstable-rendered-line-info for Paragraph::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-thinking process holding 8484.
  • Testing: SSE parser has unit tests (cargo test). For a live pass-through check: --headless, then POST to 127.0.0.1:8484/v1/messages without 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).
  • Embedded pane: no bracketed paste or mouse forwarding yet; no scrollback view (live screen only); shift+enter needs kitty keyboard protocol pushed on the outer terminal (not done); permission prompts aren't detected for pane growth (not visible in the API stream — would need a Notification hook hitting a local control endpoint).