commit c99507ca1eb847cf8a3895c684ae769b6793da1a Author: kuberwastaken Date: Wed Apr 1 01:20:27 2026 +0530 hello world diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ffa95b --- /dev/null +++ b/README.md @@ -0,0 +1,478 @@ +# IMPORTANT NOTICE - UPDATE + +This repository does not hold a copy of the proprietary Claude Code typescript source code. +This is a clean-room Rust reimplementation of Claude Code's behavior. + +The process was explicitly two-phase: + +Specification [`spec/`](https://github.com/kuberwastaken/claude-code/tree/main/spec) - An AI agent analyzed the source and produced exhaustive behavioral specifications and improvements, deviated from the original: architecture, data flows, tool contracts, system designs. No source code was carried forward. + +Implementation [`src/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust)- A separate AI agent implemented from the spec alone, never referencing the original TypeScript. The output is idiomatic Rust that reproduces the behavior, not the expression. + +This mirrors the legal precedent established by Phoenix Technologies v. IBM (1984) — clean-room engineering of the BIOS — and the principle from Baker v. Selden (1879) that copyright protects expression, not ideas or behavior. + +The analysis below is commentary on publicly available software, protected under fair use (17 U.S.C. § 107). Code excerpts are quoted to illustrate technical points from a public source - no unauthorized access was involved in this process or research. + +# Claude Code's Entire Source Code Got Leaked via a Sourcemap in npm, Let's Talk About It + +## Technical Breakdown + +> **PS:** I've also published this [breakdown on my blog](https://kuber.studio/blog/AI/Claude-Code's-Entire-Source-Code-Got-Leaked-via-a-Sourcemap-in-npm,-Let's-Talk-About-it) with a better reading experience and UX :) + +Earlier today (March 31st, 2026) - Chaofan Shou on X discovered something that Anthropic probably didn't want the world to see: the **entire source code** of Claude Code, Anthropic's official AI coding CLI, was sitting in plain sight on the npm registry via a sourcemap file bundled into the published package. + +[![The tweet announcing the leak](https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/leak-tweet.png)](https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/leak-tweet.png) + +This repository is a backup of that leaked source, and this README is a full breakdown of what's in it, how the leak happened and most importantly, the things we now know that were never meant to be public. + +Let's get into it. + +## How Did This Even Happen? + +This is the part that honestly made me go "...really?" + +When you publish a JavaScript/TypeScript package to npm, the build toolchain often generates **source map files** (`.map` files). These files are a bridge between the minified/bundled production code and the original source, they exist so that when something crashes in production the stack trace can point you to the *actual* line of code in the *original* file, not some unintelligible line 1, column 48293 of a minified blob. + +But the fun part is **source maps contain the original source code**. The actual, literal, raw source code, embedded as strings inside a JSON file. + +The structure of a `.map` file looks something like this: + +```json +{ + "version": 3, + "sources": ["../src/main.tsx", "../src/tools/BashTool.ts", "..."], + "sourcesContent": ["// The ENTIRE original source code of each file", "..."], + "mappings": "AAAA,SAAS,OAAO..." +} +``` + +That `sourcesContent` array? That's everything. +Every file. Every comment. Every internal constant. Every system prompt. All of it, sitting right there in a JSON file that npm happily serves to anyone who runs `npm pack` or even just browses the package contents. + +This is not a novel attack vector. It's happened before and honestly it'll happen again. + +The mistake is almost always the same: someone forgets to add `*.map` to their `.npmignore` or doesn't configure their bundler to skip source map generation for production builds. With Bun's bundler (which Claude Code uses), source maps are generated by default unless you explicitly turn them off. + +[![Claude Code source files exposed in npm package](https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/claude-files.png)](https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/claude-files.png) + +The funniest part is, there's an entire system called ["Undercover Mode"](#undercover-mode--do-not-blow-your-cover) specifically designed to prevent Anthropic's internal information from leaking. + +They built a whole subsystem to stop their AI from accidentally revealing internal codenames in git commits... and then shipped the entire source in a `.map` file, likely by Claude. + +--- + +## What's Claude Under The Hood? + +If you've been living under a rock, Claude Code is Anthropic's official CLI tool for coding with Claude and the most popular AI coding agent. + +From the outside, it looks like a polished but relatively simple CLI. + +From the inside, It's a **785KB [`main.tsx`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/cli/src/main.rs)** entry point, a custom React terminal renderer, 40+ tools, a multi-agent orchestration system, a background memory consolidation engine called "dream," and much more + +Enough yapping, here's some parts about the source code that are genuinely cool that I found after an afternoon deep dive: + +--- + +## BUDDY - A Tamagotchi Inside Your Terminal + +I am not making this up. + +Claude Code has a full **Tamagotchi-style companion pet system** called "Buddy." A **deterministic gacha system** with species rarity, shiny variants, procedurally generated stats, and a soul description written by Claude on first hatch like OpenClaw. + +The entire thing lives in [`buddy/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates) and is gated behind the `BUDDY` compile-time feature flag. + +### The Gacha System + +Your buddy's species is determined by a **Mulberry32 PRNG**, a fast 32-bit pseudo-random number generator seeded from your `userId` hash with the salt `'friend-2026-401'`: + +```typescript +// Mulberry32 PRNG - deterministic, reproducible per-user +function mulberry32(seed: number): () => number { + return function() { + seed |= 0; seed = seed + 0x6D2B79F5 | 0; + var t = Math.imul(seed ^ seed >>> 15, 1 | seed); + t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; + return ((t ^ t >>> 14) >>> 0) / 4294967296; + } +} +``` + +Same user always gets the same buddy. + +### 18 Species (Obfuscated in Code) + +The species names are hidden via `String.fromCharCode()` arrays - Anthropic clearly didn't want these showing up in string searches. Decoded, the full species list is: + +| Rarity | Species | +|--------|---------| +| **Common** (60%) | Pebblecrab, Dustbunny, Mossfrog, Twigling, Dewdrop, Puddlefish | +| **Uncommon** (25%) | Cloudferret, Gustowl, Bramblebear, Thornfox | +| **Rare** (10%) | Crystaldrake, Deepstag, Lavapup | +| **Epic** (4%) | Stormwyrm, Voidcat, Aetherling | +| **Legendary** (1%) | Cosmoshale, Nebulynx | + +On top of that, there's a **1% shiny chance** completely independent of rarity. So a Shiny Legendary Nebulynx has a **0.01%** chance of being rolled. Dang. + +### Stats, Eyes, Hats, and Soul + +Each buddy gets procedurally generated: +- **5 stats**: `DEBUGGING`, `PATIENCE`, `CHAOS`, `WISDOM`, `SNARK` (0-100 each) +- **6 possible eye styles** and **8 hat options** (some gated by rarity) +- **A "soul"** as mentioned, the personality generated by Claude on first hatch, written in character + +The sprites are rendered as **5-line-tall, 12-character-wide ASCII art** with multiple animation frames. There are idle animations, reaction animations, and they sit next to your input prompt. + +### The Lore + +The code references April 1-7, 2026 as a **teaser window** (so probably for easter?), with a full launch gated for May 2026. The companion has a system prompt that tells Claude: + +``` +A small {species} named {name} sits beside the user's input box and +occasionally comments in a speech bubble. You're not {name} - it's a +separate watcher. +``` + +So it's not just cosmetic - the buddy has its own personality and can respond when addressed by name. I really do hope they ship it. + +--- + +## KAIROS - "Always-On Claude" + +Inside [`assistant/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates), there's an entire mode called **KAIROS** i.e. a persistent, always-running Claude assistant that doesn't wait for you to type. It watches, logs, and **proactively** acts on things it notices. + +This is gated behind the `PROACTIVE` / `KAIROS` compile-time feature flags and is completely absent from external builds. + +### How It Works + +KAIROS maintains **append-only daily log files** - it writes observations, decisions, and actions throughout the day. On a regular interval, it receives `` prompts that let it decide whether to act proactively or stay quiet. + +The system has a **15-second blocking budget**, any proactive action that would block the user's workflow for more than 15 seconds gets deferred. This is Claude trying to be helpful without being annoying. + +### Brief Mode + +When KAIROS is active, there's a special output mode called **Brief**, extremely concise responses designed for a persistent assistant that shouldn't flood your terminal. Think of it as the difference between a chatty friend and a professional assistant who only speaks when they have something valuable to say. + +### Exclusive Tools + +KAIROS gets tools that regular Claude Code doesn't have: + +| Tool | What It Does | +|------|-------------| +| **SendUserFile** | Push files directly to the user (notifications, summaries) | +| **PushNotification** | Send push notifications to the user's device | +| **SubscribePR** | Subscribe to and monitor pull request activity | + + --- + +## ULTRAPLAN - 30-Minute Remote Planning Sessions + +Here's one that's wild from an infrastructure perspective. + +**ULTRAPLAN** is a mode where Claude Code offloads a complex planning task to a **remote Cloud Container Runtime (CCR) session** running **Opus 4.6**, gives it up to **30 minutes** to think, and lets you approve the result from your browser. + +The basic flow: + +1. Claude Code identifies a task that needs deep planning +2. It spins up a remote CCR session via the `tengu_ultraplan_model` config +3. Your terminal shows a polling state - checking every **3 seconds** for the result +4. Meanwhile, a browser-based UI lets you watch the planning happen and approve/reject it +5. When approved, there's a special sentinel value `__ULTRAPLAN_TELEPORT_LOCAL__` that "teleports" the result back to your local terminal + +--- + +## The "Dream" System - Claude Literally Dreams + +Okay this is genuinely one of the coolest things in here. + +Claude Code has a system called **autoDream** ([`services/autoDream/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates)) - a background memory consolidation engine that runs as a **forked subagent**. The naming is very intentional. It's Claude... dreaming. + +This is extremely funny because [I had the same idea for LITMUS last week - OpenClaw subagents creatively having leisure time to find fun new papers](https://github.com/Kuberwastaken/litmus) + +### The Three-Gate Trigger + +The dream doesn't just run whenever it feels like it. It has a **three-gate trigger system**: + +1. **Time gate**: 24 hours since last dream +2. **Session gate**: At least 5 sessions since last dream +3. **Lock gate**: Acquires a consolidation lock (prevents concurrent dreams) + +All three must pass. This prevents both over-dreaming and under-dreaming. + +### The Four Phases + +When it runs, the dream follows four strict phases from the prompt in [`consolidationPrompt.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/query/src/compact.rs): + +**Phase 1 - Orient**: `ls` the memory directory, read `MEMORY.md`, skim existing topic files to improve. + +**Phase 2 - Gather Recent Signal**: Find new information worth persisting. Sources in priority: daily logs → drifted memories → transcript search. + +**Phase 3 - Consolidate**: Write or update memory files. Convert relative dates to absolute. Delete contradicted facts. + +**Phase 4 - Prune and Index**: Keep `MEMORY.md` under 200 lines AND ~25KB. Remove stale pointers. Resolve contradictions. + +The prompt literally says: + +> *"You are performing a dream - a reflective pass over your memory files. Synthesize what you've learned recently into durable, well-organized memories so that future sessions can orient quickly."* + +The dream subagent gets **read-only bash** - it can look at your project but not modify anything. It's purely a memory consolidation pass. + +--- + +## Undercover Mode - "Do Not Blow Your Cover" + + +This one is fascinating from a corporate strategy perspective. + +Anthropic employees (identified by `USER_TYPE === 'ant'`) use Claude Code on public/open-source repositories. **Undercover Mode** ([`utils/undercover.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/core/src/lib.rs)) prevents the AI from accidentally revealing internal information in commits and PRs. + +When active, it injects this into the system prompt: + +``` +## UNDERCOVER MODE - CRITICAL + +You are operating UNDERCOVER in a PUBLIC/OPEN-SOURCE repository. Your commit +messages, PR titles, and PR bodies MUST NOT contain ANY Anthropic-internal +information. Do not blow your cover. + +NEVER include in commit messages or PR descriptions: +- Internal model codenames (animal names like Capybara, Tengu, etc.) +- Unreleased model version numbers (e.g., opus-4-7, sonnet-4-8) +- Internal repo or project names +- Internal tooling, Slack channels, or short links (e.g., go/cc, #claude-code-…) +- The phrase "Claude Code" or any mention that you are an AI +- Co-Authored-By lines or any other attribution +``` + +The activation logic: +- `CLAUDE_CODE_UNDERCOVER=1` forces it ON (even in internal repos) +- Otherwise it's **automatic**: active UNLESS the repo remote matches an internal allowlist +- There is **NO force-OFF** - *"if we're not confident we're in an internal repo, we stay undercover."* + +So this confirms: +1. **Anthropic employees actively use Claude Code to contribute to open-source** - and the AI is told to hide that it's an AI +2. **Internal model codenames are animal names** - Capybara, Tengu, etc. +3. **"Tengu"** appears hundreds of times as a prefix for feature flags and analytics events - it's almost certainly **Claude Code's internal project codename** + +All of this is dead-code-eliminated from external builds. But source maps don't care about dead code elimination. + +Makes me wonder how much are they internally causing havoc to open source repos + +--- + +## Multi-Agent Orchestration - "Coordinator Mode" + + +Claude Code has a full **multi-agent orchestration system** in [`coordinator/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates/query/src), activated via `CLAUDE_CODE_COORDINATOR_MODE=1`. + +When enabled, Claude Code transforms from a single agent into a **coordinator** that spawns, directs, and manages multiple worker agents in parallel. The coordinator system prompt in [`coordinatorMode.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/query/src/agent_tool.rs) is a masterclass in multi-agent design: + +| Phase | Who | Purpose | +|-------|-----|---------| +| **Research** | Workers (parallel) | Investigate codebase, find files, understand problem | +| **Synthesis** | **Coordinator** | Read findings, understand the problem, craft specs | +| **Implementation** | Workers | Make targeted changes per spec, commit | +| **Verification** | Workers | Test changes work | + +The prompt **explicitly** teaches parallelism: + +> *"Parallelism is your superpower. Workers are async. Launch independent workers concurrently whenever possible - don't serialize work that can run simultaneously."* + +Workers communicate via `` XML messages. There's a shared **scratchpad directory** (gated behind `tengu_scratch`) for cross-worker durable knowledge sharing. And the prompt has this gem banning lazy delegation: + +> *Do NOT say "based on your findings" - read the actual findings and specify exactly what to do.* + +The system also includes **Agent Teams/Swarm** capabilities (`tengu_amber_flint` feature gate) with in-process teammates using `AsyncLocalStorage` for context isolation, process-based teammates using tmux/iTerm2 panes, team memory synchronization, and color assignments for visual distinction. + +--- + +## Fast Mode is Internally Called "Penguin Mode" + +Yeah, they really called it Penguin Mode. The API endpoint in [`utils/fastMode.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/core/src/lib.rs) is literally: + +```typescript +const endpoint = `${getOauthConfig().BASE_API_URL}/api/claude_code_penguin_mode` +``` + +The config key is `penguinModeOrgEnabled`. The kill-switch is `tengu_penguins_off`. The analytics event on failure is `tengu_org_penguin_mode_fetch_failed`. Penguins all the way down. + +--- + +## The System Prompt Architecture + +The system prompt isn't a single string like most apps have - it's built from **modular, cached sections** composed at runtime in [`constants/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates/core/src). + +The architecture uses a `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` marker that splits the prompt into: +- **Static sections** - cacheable across organizations (things that don't change per user) +- **Dynamic sections** - user/session-specific content that breaks cache when changed + +There's a function called `DANGEROUS_uncachedSystemPromptSection()` for volatile sections you explicitly want to break cache. The naming convention alone tells you someone learned this lesson the hard way. + +### The Cyber Risk Instruction + +One particularly interesting section is the `CYBER_RISK_INSTRUCTION` in [`constants/cyberRiskInstruction.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/core/src/lib.rs), which has a massive warning header: + +``` +IMPORTANT: DO NOT MODIFY THIS INSTRUCTION WITHOUT SAFEGUARDS TEAM REVIEW +This instruction is owned by the Safeguards team (David Forsythe, Kyla Guru) +``` + +So now we know exactly who at Anthropic owns the security boundary decisions and that it's governed by named individuals on a specific team. The instruction itself draws clear lines: authorized security testing is fine, destructive techniques and supply chain compromise are not. + +--- + +## The Full Tool Registry - 40+ Tools + +Claude Code's tool system lives in [`tools/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates/tools/src).Here's the complete list: + +| Tool | What It Does | +|------|-------------| +| **AgentTool** | Spawn child agents/subagents | +| **BashTool** / **PowerShellTool** | Shell execution (with optional sandboxing) | +| **FileReadTool** / **FileEditTool** / **FileWriteTool** | File operations | +| **GlobTool** / **GrepTool** | File search (uses native `bfs`/`ugrep` when available) | +| **WebFetchTool** / **WebSearchTool** / **WebBrowserTool** | Web access | +| **NotebookEditTool** | Jupyter notebook editing | +| **SkillTool** | Invoke user-defined skills | +| **REPLTool** | Interactive VM shell (bare mode) | +| **LSPTool** | Language Server Protocol communication | +| **AskUserQuestionTool** | Prompt user for input | +| **EnterPlanModeTool** / **ExitPlanModeV2Tool** | Plan mode control | +| **BriefTool** | Upload/summarize files to claude.ai | +| **SendMessageTool** / **TeamCreateTool** / **TeamDeleteTool** | Agent swarm management | +| **TaskCreateTool** / **TaskGetTool** / **TaskListTool** / **TaskUpdateTool** / **TaskOutputTool** / **TaskStopTool** | Background task management | +| **TodoWriteTool** | Write todos (legacy) | +| **ListMcpResourcesTool** / **ReadMcpResourceTool** | MCP resource access | +| **SleepTool** | Async delays | +| **SnipTool** | History snippet extraction | +| **ToolSearchTool** | Tool discovery | +| **ListPeersTool** | List peer agents (UDS inbox) | +| **MonitorTool** | Monitor MCP servers | +| **EnterWorktreeTool** / **ExitWorktreeTool** | Git worktree management | +| **ScheduleCronTool** | Schedule cron jobs | +| **RemoteTriggerTool** | Trigger remote agents | +| **WorkflowTool** | Execute workflow scripts | +| **ConfigTool** | Modify settings (**internal only**) | +| **TungstenTool** | Advanced features (**internal only**) | +| **SendUserFile** / **PushNotification** / **SubscribePR** | KAIROS-exclusive tools | + +Tools are registered via `getAllBaseTools()` and filtered by feature gates, user type, environment flags, and permission deny rules. There's a **tool schema cache** ([`toolSchemaCache.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/tools/src/lib.rs)) that caches JSON schemas for prompt efficiency. + +--- + +## The Permission and Security System + +Claude Code's permission system in [`tools/permissions/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates/core/src) is far more sophisticated than "allow/deny": + +**Permission Modes**: `default` (interactive prompts), `auto` (ML-based auto-approval via transcript classifier), `bypass` (skip checks), `yolo` (deny all - ironically named) + +**Risk Classification**: Every tool action is classified as **LOW**, **MEDIUM**, or **HIGH** risk. There's a **YOLO classifier** - a fast ML-based permission decision system that decides automatically. + +**Protected Files**: `.gitconfig`, `.bashrc`, `.zshrc`, `.mcp.json`, `.claude.json` and others are guarded from automatic editing. + +**Path Traversal Prevention**: URL-encoded traversals, Unicode normalization attacks, backslash injection, case-insensitive path manipulation - all handled. + +**Permission Explainer**: A separate LLM call explains tool risks to the user before they approve. When Claude says "this command will modify your git config" - that explanation is itself generated by Claude. + +--- + +## Hidden Beta Headers and Unreleased API Features + +The [`constants/betas.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/api/src/lib.rs) file reveals every beta feature Claude Code negotiates with the API: + +```typescript +'interleaved-thinking-2025-05-14' // Extended thinking +'context-1m-2025-08-07' // 1M token context window +'structured-outputs-2025-12-15' // Structured output format +'web-search-2025-03-05' // Web search +'advanced-tool-use-2025-11-20' // Advanced tool use +'effort-2025-11-24' // Effort level control +'task-budgets-2026-03-13' // Task budget management +'prompt-caching-scope-2026-01-05' // Prompt cache scoping +'fast-mode-2026-02-01' // Fast mode (Penguin) +'redact-thinking-2026-02-12' // Redacted thinking +'token-efficient-tools-2026-03-28' // Token-efficient tool schemas +'afk-mode-2026-01-31' // AFK mode +'cli-internal-2026-02-09' // Internal-only (ant) +'advisor-tool-2026-03-01' // Advisor tool +'summarize-connector-text-2026-03-13' // Connector text summarization +``` + +`redact-thinking`, `afk-mode`, and `advisor-tool` are also not released. + +--- + +## Feature Gating - Internal vs. External Builds + +This is one of the most architecturally interesting parts of the codebase. + +Claude Code uses **compile-time feature flags** via Bun's `feature()` function from `bun:bundle`. The bundler **constant-folds** these and **dead-code-eliminates** the gated branches from external builds. The complete list of known flags: + +| Flag | What It Gates | +|------|--------------| +| `PROACTIVE` / `KAIROS` | Always-on assistant mode | +| `KAIROS_BRIEF` | Brief command | +| `BRIDGE_MODE` | Remote control via claude.ai | +| `DAEMON` | Background daemon mode | +| `VOICE_MODE` | Voice input | +| `WORKFLOW_SCRIPTS` | Workflow automation | +| `COORDINATOR_MODE` | Multi-agent orchestration | +| `TRANSCRIPT_CLASSIFIER` | AFK mode (ML auto-approval) | +| `BUDDY` | Companion pet system | +| `NATIVE_CLIENT_ATTESTATION` | Client attestation | +| `HISTORY_SNIP` | History snipping | +| `EXPERIMENTAL_SKILL_SEARCH` | Skill discovery | + +Additionally, `USER_TYPE === 'ant'` gates Anthropic-internal features: staging API access (`claude-ai.staging.ant.dev`), internal beta headers, Undercover mode, the `/security-review` command, `ConfigTool`, `TungstenTool`, and debug prompt dumping to `~/.config/claude/dump-prompts/`. + +**GrowthBook** handles runtime feature gating with aggressively cached values. Feature flags prefixed with `tengu_` control everything from fast mode to memory consolidation. Many checks use `getFeatureValue_CACHED_MAY_BE_STALE()` to avoid blocking the main loop - stale data is considered acceptable for feature gates. + +--- + +## Other Notable Findings + +### The Upstream Proxy +The [`upstreamproxy/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates/bridge/src) directory contains a container-aware proxy relay that uses **`prctl(PR_SET_DUMPABLE, 0)`** to prevent same-UID ptrace of heap memory. It reads session tokens from `/run/ccr/session_token` in CCR containers, downloads CA certificates, and starts a local CONNECT→WebSocket relay. Anthropic API, GitHub, npmjs.org, and pypi.org are explicitly excluded from proxying. + +### Bridge Mode +A JWT-authenticated bridge system in [`bridge/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates/bridge/src) for integrating with claude.ai. Supports work modes: `'single-session'` | `'worktree'` | `'same-dir'`. Includes trusted device tokens for elevated security tiers. + +### Model Codenames in Migrations +The [`migrations/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust/crates/core/src) directory reveals the internal codename history: +- `migrateFennecToOpus` - **"Fennec"** (the fox) was an Opus codename +- `migrateSonnet1mToSonnet45` - Sonnet with 1M context became Sonnet 4.5 +- `migrateSonnet45ToSonnet46` - Sonnet 4.5 → Sonnet 4.6 +- `resetProToOpusDefault` - Pro users were reset to Opus at some point + +### Attribution Header +Every API request includes: +``` +x-anthropic-billing-header: cc_version={VERSION}.{FINGERPRINT}; + cc_entrypoint={ENTRYPOINT}; cch={ATTESTATION_PLACEHOLDER}; cc_workload={WORKLOAD}; +``` +The `NATIVE_CLIENT_ATTESTATION` feature lets Bun's HTTP stack overwrite the `cch=00000` placeholder with a computed hash - essentially a client authenticity check so Anthropic can verify the request came from a real Claude Code install. + +### Computer Use - "Chicago" +Claude Code includes a full Computer Use implementation, internally codenamed **"Chicago"**, built on `@ant/computer-use-mcp`. It provides screenshot capture, click/keyboard input, and coordinate transformation. Gated to Max/Pro subscriptions (with an ant bypass for internal users). + +### Pricing +For anyone wondering - all pricing in [`utils/modelCost.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/api/src/lib.rs) matches [Anthropic's public pricing](https://docs.anthropic.com/en/docs/about-claude/models) exactly. Nothing newsworthy there. + +--- + +## Final Thoughts + +This is, without exaggeration, one of the most comprehensive looks we've ever gotten at how *the* production AI coding assistant works under the hood. Through the actual source code. + +A few things stand out: + +**The engineering is genuinely impressive.** This isn't a weekend project wrapped in a CLI. The multi-agent coordination, the dream system, the three-gate trigger architecture, the compile-time feature elimination - these are deeply considered systems. + +**There's a LOT more coming.** KAIROS (always-on Claude), ULTRAPLAN (30-minute remote planning), the Buddy companion, coordinator mode, agent swarms, workflow scripts - the codebase is significantly ahead of the public release. Most of these are feature-gated and invisible in external builds. + +**The internal culture shows.** Animal codenames (Tengu, Fennec, Capybara), playful feature names (Penguin Mode, Dream System), a Tamagotchi pet system with gacha mechanics. Some people at Anthropic is having fun. + +If there's one takeaway this has, it's that security is hard. But `.npmignore` is harder, apparently :P + +--- + +A writeup by [Kuber Mehta](https://kuber.studio/) \ No newline at end of file diff --git a/public/claude-files.png b/public/claude-files.png new file mode 100644 index 0000000..bf19df7 Binary files /dev/null and b/public/claude-files.png differ diff --git a/public/leak-tweet.png b/public/leak-tweet.png new file mode 100644 index 0000000..35da39c Binary files /dev/null and b/public/leak-tweet.png differ diff --git a/spec/00_overview.md b/spec/00_overview.md new file mode 100644 index 0000000..028124b --- /dev/null +++ b/spec/00_overview.md @@ -0,0 +1,334 @@ +# Claude Code — Master Architecture Overview + +> **Repository:** `X:\Bigger-Projects\Claude-Code` +> **Primary Language:** TypeScript/TSX (~1,902 files, ~800K+ LOC) +> **Secondary Language:** Rust (~47 files, in-progress port) +> **Bundler:** Bun +> **UI Framework:** Custom Ink (React reconciler for terminal) +> **Runtime Target:** Node.js / Bun CLI + +--- + +## 1. What Is Claude Code? + +Claude Code is an AI-powered CLI tool and coding assistant. It is a full-featured interactive terminal application that: + +- Embeds a Claude AI model as an agentic coding assistant +- Runs in the terminal using a custom React-based TUI (Terminal User Interface) +- Executes tools (file read/write, bash, grep, web search, etc.) with user permission +- Supports multi-agent task delegation, background agents, and swarm mode +- Integrates with IDEs (VS Code, JetBrains) via direct-connect bridge +- Supports remote sessions via WebSocket/SSE transports +- Has a plugin/skills marketplace +- Includes voice input (speech-to-text) +- Features a companion "buddy" system (Tamagotchi-style) +- Syncs sessions to the cloud via the bridge protocol + +--- + +## 2. Repository Structure + +``` +Claude-Code/ +├── src/ # Main TypeScript/TSX source (34 MB, ~1,902 files) +│ ├── main.tsx # PRIMARY ENTRY POINT (4,683 lines) +│ ├── replLauncher.tsx # REPL mode launcher +│ ├── query.ts # Main query/turn execution engine (69KB) +│ ├── QueryEngine.ts # Query engine class (46KB) +│ ├── Tool.ts # Tool base framework (30KB) +│ ├── Task.ts # Task definitions +│ ├── commands.ts # Command registry (25KB) +│ ├── context.ts # Context management +│ ├── cost-tracker.ts # Cost tracking (11KB) +│ ├── costHook.ts # Cost hooks +│ ├── history.ts # Session history (14KB) +│ ├── dialogLaunchers.tsx # Dialog launchers (23KB) +│ ├── interactiveHelpers.tsx # Interactive UI helpers (57KB) +│ ├── projectOnboardingState.ts # Project onboarding state +│ ├── setup.ts # Initialization (21KB) +│ ├── tasks.ts # Task management +│ ├── tools.ts # Tools registry (17KB) +│ ├── ink.ts # Ink export shim +│ │ +│ ├── assistant/ # Assistant session history +│ ├── bootstrap/ # Bootstrap/state +│ ├── bridge/ # Bridge protocol (31 files) +│ ├── buddy/ # Companion pet system (6 files) +│ ├── cli/ # CLI framework & transports (19 files) +│ ├── commands/ # 87 slash commands (207 files) +│ ├── components/ # React/Ink UI components (389 files, 32 subdirs) +│ ├── constants/ # Constants & config values (21 files) +│ ├── context/ # React context providers (9 files) +│ ├── coordinator/ # Coordinator mode logic +│ ├── entrypoints/ # Multiple entry points (8 files) +│ ├── hooks/ # React hooks (104 files) +│ ├── ink/ # Custom Ink terminal framework (96 files) +│ ├── keybindings/ # Keyboard shortcut system (14 files) +│ ├── memdir/ # Memory directory system (8 files) +│ ├── migrations/ # Settings migrations (11 files) +│ ├── moreright/ # useMoreRight hook +│ ├── native-ts/ # Native TypeScript bindings (4 files) +│ ├── outputStyles/ # Output style loader +│ ├── plugins/ # Plugin system (2 files) +│ ├── query/ # Query helpers (4 files) +│ ├── remote/ # Remote session management (4 files) +│ ├── schemas/ # Zod/JSON schemas +│ ├── screens/ # Top-level screen layouts (3 files) +│ ├── server/ # Direct-connect server (3 files) +│ ├── services/ # Business logic services (130 files) +│ ├── skills/ # Claude skills/slash commands (20 files) +│ ├── tools/ # Tool implementations (40+ tools, 184 files) +│ ├── types/ # TypeScript type definitions +│ ├── utils/ # Utility functions (~564 files) +│ └── voice/ # Voice integration +│ +├── claude-code-rust/ # Rust port (in-progress, 47 files) +│ ├── Cargo.toml # Workspace manifest +│ ├── tools/ # 27 files — tool implementations +│ ├── query/ # 5 files — query system +│ ├── cli/ # 3 files — CLI framework +│ ├── api/ # 2 files — API bindings +│ ├── bridge/ # 2 files — bridge protocol +│ ├── commands/ # 2 files — command system +│ ├── core/ # 2 files — core utilities +│ ├── mcp/ # 2 files — MCP integration +│ └── tui/ # 2 files — terminal UI +│ +├── public/ # Static assets +├── README.md # Main documentation (27KB) +└── .git/ # Git metadata +``` + +--- + +## 3. High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ USER INTERFACE │ +│ Terminal (Ink TUI) ←→ React Components ←→ Hooks ←→ Context │ +└────────────────────────────┬────────────────────────────────────┘ + │ +┌────────────────────────────▼────────────────────────────────────┐ +│ MAIN APPLICATION │ +│ main.tsx → REPL.tsx → PromptInput → MessageList │ +│ Commands (87) ←→ Command Registry ←→ Plugin System │ +└────────────────────────────┬────────────────────────────────────┘ + │ +┌────────────────────────────▼────────────────────────────────────┐ +│ QUERY ENGINE │ +│ query.ts → QueryEngine.ts → Tool execution → Response handling │ +│ Token budget → Stop hooks → Compact → History │ +└────────────────────────────┬────────────────────────────────────┘ + │ +┌────────────────────────────▼────────────────────────────────────┐ +│ TOOL SYSTEM (40+ tools) │ +│ BashTool, FileReadTool, FileEditTool, FileWriteTool │ +│ GlobTool, GrepTool, WebFetchTool, WebSearchTool │ +│ AgentTool, TaskCreateTool, MCPTool, SkillTool, ... │ +└────────────────────────────┬────────────────────────────────────┘ + │ +┌────────────────────────────▼────────────────────────────────────┐ +│ SERVICES LAYER │ +│ API Client (claude.ts) → Analytics → SessionMemory │ +│ AutoDream → Compact → RateLimit → MCP servers │ +└────────────────────────────┬────────────────────────────────────┘ + │ +┌────────────────────────────▼────────────────────────────────────┐ +│ TRANSPORT LAYER │ +│ CLI (local) / Bridge (remote) / IDE direct-connect │ +│ SSETransport | WebSocketTransport | HybridTransport │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. Core Subsystems + +### 4.1 Query / Turn Execution (`query.ts`, `QueryEngine.ts`) +The core loop that: +1. Takes user input +2. Builds the API request (system prompt + history + tools) +3. Streams the response from Claude API +4. Handles tool use (executes tools, feeds results back) +5. Manages token budget and context compaction +6. Tracks cost + +### 4.2 Tool Framework (`Tool.ts`, `tools/`) +- Base `Tool` abstract class/interface +- Input schema validation (Zod) +- Permission system (each tool declares required permissions) +- 40+ tool implementations +- Sandboxing for dangerous tools + +### 4.3 Terminal UI (`ink/`, `components/`) +- Custom React reconciler that renders to terminal +- Layout engine based on Yoga (flexbox for terminal) +- Event system (keyboard, mouse, focus) +- ANSI/CSI/escape sequence processing +- Components: Messages, PromptInput, Spinner, Dialogs, etc. + +### 4.4 Commands System (`commands/`, `commands.ts`) +- 87 slash commands (e.g., `/compact`, `/diff`, `/plan`, `/mcp`) +- Plugin-contributed commands +- Command registry with fuzzy matching +- Keybinding integration + +### 4.5 Bridge Protocol (`bridge/`) +- Enables remote/cloud-synced sessions +- JWT-authenticated WebSocket/SSE connection to cloud backend servers +- REPL bridge for IDE integration +- Message polling, flush gates, session runners + +### 4.6 Multi-Agent System (`tools/AgentTool.ts`, `components/agents/`) +- Spawn sub-agents as isolated Claude instances +- Background task execution +- Coordinator mode (orchestrate multiple agents) +- Swarm mode (parallel worker agents) +- Team system for collaborative agents + +### 4.7 Memory System (`memdir/`, `services/SessionMemory/`, `services/autoDream/`) +- Short-term: session history +- Long-term: memdir (markdown files in `~/.claude/memory/`) +- Auto-consolidation: "dream" service consolidates memories during idle +- Memory scanning/relevance scoring for context injection + +### 4.8 MCP Integration (`tools/MCPTool.ts`, `components/mcp/`, `entrypoints/mcp.ts`) +- Model Context Protocol server support +- Dynamic tool registration from MCP servers +- Resource management +- Elicitation dialog support + +### 4.9 Plugin/Skills System (`plugins/`, `skills/`, `commands/plugin/`) +- Built-in plugins +- Marketplace for community plugins +- Skills: user-invocable slash command macros +- Plugin trust model with approval flow + +### 4.10 IDE Integration (`bridge/`, `hooks/useIDEIntegration.tsx`) +- VS Code / JetBrains extensions connect via direct-connect +- Live diff viewing in IDE +- File selection sync (IDE → Claude) +- Status indicator in IDE + +--- + +## 5. Data Flow: A User Turn + +``` +1. User types in PromptInput +2. Input submitted → useCommandQueue processes +3. If slash command: dispatched to command handler +4. If regular prompt: sent to query.ts runQuery() +5. QueryEngine builds API request: + - System prompt (from constants/prompts.ts + CLAUDE.md) + - Message history (from history.ts) + - Available tools (filtered by permission) + - Token budget constraints +6. Stream response from the Claude API (services/api/claude.ts) +7. For each content block: + - text → render AssistantTextMessage + - thinking → render AssistantThinkingMessage + - tool_use → execute tool, show permission dialog if needed +8. Tool results fed back into next API request +9. Loop until stop condition (no more tool use, stop hook, budget exceeded) +10. Final response rendered, history updated, cost tracked +``` + +--- + +## 6. Key Files by Importance + +| Rank | File | Size | Role | +|------|------|------|------| +| 1 | `src/main.tsx` | 4,683 lines | Primary entry point, app initialization | +| 2 | `src/query.ts` | 69KB | Main query execution loop | +| 3 | `src/QueryEngine.ts` | 46KB | Query engine class | +| 4 | `src/interactiveHelpers.tsx` | 57KB | Interactive UI helpers | +| 5 | `src/Tool.ts` | 30KB | Tool base framework | +| 6 | `src/commands.ts` | 25KB | Command registry | +| 7 | `src/dialogLaunchers.tsx` | 23KB | Dialog launch system | +| 8 | `src/setup.ts` | 21KB | Initialization | +| 9 | `src/tools.ts` | 17KB | Tools registry | +| 10 | `src/history.ts` | 14KB | Session history | + +--- + +## 7. Permission Model + +Claude Code uses a layered permission system: + +1. **Automatic** — Read-only operations, info queries +2. **Ask Once** — Prompt user, remember for session +3. **Ask Always** — Prompt user every time +4. **Deny** — Block completely + +Permission rules are stored in settings (global `~/.claude/settings.json`, project `.claude/settings.json`) and can be configured with patterns. + +Permission categories: +- `Bash` — Shell command execution +- `FileRead` — Reading files/directories +- `FileEdit` — Editing existing files +- `FileWrite` — Creating new files +- `WebFetch` — HTTP requests +- `MCP` — MCP tool calls +- `Sandbox` — Sandboxed execution + +--- + +## 8. Settings System + +Layered settings (in priority order): +1. **Managed** — Enterprise/managed settings (read-only) +2. **Local project** — `.claude/settings.local.json` (gitignored) +3. **Project** — `.claude/settings.json` (shared) +4. **Global** — `~/.claude/settings.json` + +Settings include: model selection, permission rules, API key, theme, keybindings, MCP server configurations, beta features. + +--- + +## 9. Model Support + +Based on migration files, the model evolution: +- `claude-3-sonnet` → `claude-sonnet-1m` → `claude-sonnet-4-5` → `claude-sonnet-4-6` +- `claude-3-opus` → `claude-opus-1m` → `claude-opus` → (various) +- `claude-3-5-haiku` → (current) +- `claude-haiku-4-5` (current haiku) + +Current defaults (as of source): `claude-sonnet-4-6` and `claude-opus-4-6` + +--- + +## 10. Analytics & Telemetry + +- **First-party logging** — Session events to the backend (`services/analytics/`) +- **Datadog** — Performance metrics +- **Growthbook** — Feature flags / A/B testing +- **Opt-out** — `services/api/metricsOptOut.ts` handles user opt-out + +--- + +## 11. Spec Document Index + +| File | Contents | +|------|----------| +| `00_overview.md` | This file — master architecture overview | +| `01_core_entry_query.md` | Entry points, query system, history, cost tracking | +| `02_commands.md` | All 87 slash commands | +| `03_tools.md` | All 40+ tool implementations | +| `04_components_core_messages.md` | Top-level components and message components | +| `05_components_agents_permissions_design.md` | Agents, permissions, design system, feature modules | +| `06_services_context_state.md` | Services, context providers, state, screens, server | +| `07_hooks.md` | All React hooks | +| `08_ink_terminal.md` | Ink terminal rendering framework | +| `09_bridge_cli_remote.md` | Bridge protocol, CLI framework, remote sessions | +| `10_utils.md` | All utility functions (~564 files) | +| `11_special_systems.md` | Buddy, memory, keybindings, skills, voice, plugins | +| `12_constants_types.md` | All constants, types, and configuration | +| `13_rust_codebase.md` | Rust port/rewrite | +| `INDEX.md` | Quick-reference index | + +--- + +*Generated from source analysis of the Claude Code codebase. ~1,902 TypeScript/TSX files, ~800K+ lines of code.* diff --git a/spec/01_core_entry_query.md b/spec/01_core_entry_query.md new file mode 100644 index 0000000..7d7c211 --- /dev/null +++ b/spec/01_core_entry_query.md @@ -0,0 +1,1925 @@ +# Claude Code — Core Entry Points & Query System + +## Table of Contents + +1. [entrypoints/cli.tsx — Bootstrap Dispatcher](#entrypointsclisx--bootstrap-dispatcher) +2. [main.tsx — Full CLI Entry Point](#maintsx--full-cli-entry-point) +3. [replLauncher.tsx — REPL UI Launcher](#repplaunchertsx--repl-ui-launcher) +4. [entrypoints/init.ts — Initialization & Telemetry](#entrypointsinits--initialization--telemetry) +5. [entrypoints/mcp.ts — MCP Server Entrypoint](#entrypointsmcpts--mcp-server-entrypoint) +6. [entrypoints/agentSdkTypes.ts — Agent SDK Public API](#entrypointsagentsdktypests--agent-sdk-public-api) +7. [entrypoints/sandboxTypes.ts — Sandbox Configuration Types](#entrypointssandboxtypests--sandbox-configuration-types) +8. [entrypoints/sdk/coreSchemas.ts — SDK Core Zod Schemas](#entrypointssdkcoreshematss--sdk-core-zod-schemas) +9. [entrypoints/sdk/coreTypes.ts — SDK Core TypeScript Types](#entrypointssdkcoretypests--sdk-core-typescript-types) +10. [entrypoints/sdk/controlSchemas.ts — SDK Control Protocol Schemas](#entrypointssdkcontrolschematss--sdk-control-protocol-schemas) +11. [query.ts — Core Async Query Loop](#queryts--core-async-query-loop) +12. [QueryEngine.ts — Stateful Query Engine (SDK/Headless)](#queryenginets--stateful-query-engine-sdkheadless) +13. [query/config.ts — Query Configuration Snapshot](#queryconfigts--query-configuration-snapshot) +14. [query/deps.ts — Query Dependency Injection](#querydepsts--query-dependency-injection) +15. [query/stopHooks.ts — Stop Hook Orchestration](#querystophooksts--stop-hook-orchestration) +16. [query/tokenBudget.ts — Token Budget Tracking](#querytokenbudgetts--token-budget-tracking) +17. [context.ts — System & User Context Providers](#contextts--system--user-context-providers) +18. [history.ts — Prompt History Management](#historyts--prompt-history-management) +19. [cost-tracker.ts — Session Cost Tracking](#cost-trackerts--session-cost-tracking) +20. [costHook.ts — React Cost Summary Hook](#costhookts--react-cost-summary-hook) +21. [projectOnboardingState.ts — Project Onboarding State](#projectonboardingstatets--project-onboarding-state) +22. [bootstrap/state.ts — Global Session State](#bootstrapstatets--global-session-state) +23. [assistant/sessionHistory.ts — Remote Session History Pagination](#assistantsessionhistoryts--remote-session-history-pagination) + +--- + +## entrypoints/cli.tsx — Bootstrap Dispatcher + +### Purpose + +The very first module executed when a user runs `claude`. Acts as a lightweight bootstrap dispatcher that checks process arguments for known fast-paths **before** loading any heavy modules. Each fast-path dynamically imports only what it needs. The full CLI (`main.tsx`) is only loaded when no fast-path matches. + +### Key Flow + +``` +process.argv parsing + ├── --version / -v → print MACRO.VERSION, exit (zero imports) + ├── --dump-system-prompt → dump rendered system prompt (ant-only, DUMP_SYSTEM_PROMPT feature) + ├── --claude-in-chrome-mcp → runClaudeInChromeMcpServer() + ├── --chrome-native-host → runChromeNativeHost() + ├── --computer-use-mcp → runComputerUseMcpServer() (CHICAGO_MCP feature) + ├── --daemon-worker= → runDaemonWorker() (DAEMON feature) + ├── remote-control|rc|remote|sync|bridge → bridgeMain() (BRIDGE_MODE feature) + ├── daemon → daemonMain() (DAEMON feature) + ├── ps|logs|attach|kill|--bg|--background → bg handlers (BG_SESSIONS feature) + ├── new|list|reply → templatesMain() (TEMPLATES feature) + ├── environment-runner → environmentRunnerMain() (BYOC_ENVIRONMENT_RUNNER feature) + ├── self-hosted-runner → selfHostedRunnerMain() (SELF_HOSTED_RUNNER feature) + ├── --worktree + --tmux → execIntoTmuxWorktree() + └── (default) → startCapturingEarlyInput() → import main.tsx → cliMain() +``` + +### Top-Level Side Effects (at module load time) + +| Side Effect | Purpose | +|---|---| +| `process.env.COREPACK_ENABLE_AUTO_PIN = '0'` | Prevent yarnpkg from being added to package.json | +| `process.env.NODE_OPTIONS += '--max-old-space-size=8192'` | CCR environment (16GB containers) heap size | +| `ABLATION_BASELINE` flag | Sets multiple `CLAUDE_CODE_*` env vars for harness-science L0 ablation | + +### Exports + +```typescript +// No named exports — module executes main() IIFE at bottom +async function main(): Promise // internal, not exported +``` + +### Feature Flags Checked + +- `DUMP_SYSTEM_PROMPT` — ant-only system prompt dump +- `CHICAGO_MCP` — computer-use MCP server +- `DAEMON` — daemon worker and supervisor +- `BRIDGE_MODE` — remote control bridge +- `BG_SESSIONS` — background session management +- `TEMPLATES` — template job commands +- `BYOC_ENVIRONMENT_RUNNER` — BYOC headless runner +- `SELF_HOSTED_RUNNER` — self-hosted runner +- `ABLATION_BASELINE` — harness-science baseline + +### Dependencies + +- `bun:bundle` (`feature`) +- `../utils/startupProfiler.js` +- `../utils/config.js` (enableConfigs) +- Various fast-path modules loaded dynamically + +--- + +## main.tsx — Full CLI Entry Point + +### Purpose + +The main CLI module. Loaded only after `entrypoints/cli.tsx` determines no fast-path matches. Defines the Commander.js command tree, handles all CLI flags, orchestrates startup (migrations, trust dialog, MCP config, tool loading), and launches either the interactive REPL or the headless/print (`-p`) path. + +### Exported Functions + +```typescript +export async function main(): Promise +export function startDeferredPrefetches(): void +``` + +#### `main()` + +The primary entry point for the full CLI. Responsibilities in order: + +1. **Security**: Sets `process.env.NoDefaultCurrentDirectoryInExePath = '1'` (Windows PATH attack prevention) +2. **Warning handler**: `initializeWarningHandler()` +3. **Signal handlers**: SIGINT (skip in print mode), exit cursor reset +4. **Early arg processing**: + - `cc://` / `cc+unix://` URL rewriting (DIRECT_CONNECT feature) + - `--handle-uri` deep link handling (LODESTONE feature) + - `claude assistant [sessionId]` rewriting (KAIROS feature) + - `claude ssh ` rewriting (SSH_REMOTE feature) +5. **Settings flag parsing**: `eagerLoadSettings()` runs before `init()` +6. **Commander.js setup**: Defines the complete command tree (see CLI flags below) +7. **`init()`** call: Validates configs, sets up network, telemetry loading promise +8. **Migration run**: `runMigrations()` at version `CURRENT_MIGRATION_VERSION = 11` +9. **Trust check**: Shows trust dialog if not previously accepted +10. **Telemetry init**: `initializeTelemetryAfterTrust()` +11. **Session setup**: model, permissions, MCP servers, tools, agents +12. **Launch**: Either `showSetupScreens()` + `launchRepl()`, or headless `runHeadless()` + +#### `startDeferredPrefetches()` + +Called after first REPL render to avoid blocking the initial paint. Skipped when: +- `CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER=1` +- `--bare` mode (isBareMode()) + +Prefetches (fire-and-forget): +- `initUser()` +- `getUserContext()` +- `prefetchSystemContextIfSafe()` +- `getRelevantTips()` +- AWS/GCP credentials (if Bedrock/Vertex enabled) +- `countFilesRoundedRg()` (3 second timeout) +- `initializeAnalyticsGates()` +- `prefetchOfficialMcpUrls()` +- `refreshModelCapabilities()` +- `settingsChangeDetector.initialize()` +- `skillChangeDetector.initialize()` (non-bare only) + +### Constants + +| Constant | Value | Purpose | +|---|---|---| +| `CURRENT_MIGRATION_VERSION` | `11` | Version gate for running config migrations | + +### CLI Flags (Commander.js) + +| Flag | Type | Description | +|---|---|---| +| `-p, --print ` | `string` | Non-interactive/headless mode | +| `--model ` | `string` | Model override | +| `--fallback-model ` | `string` | Fallback model for retry | +| `--permission-mode ` | enum | Permission mode for tool execution | +| `--dangerously-skip-permissions` | `boolean` | Bypass all permission checks | +| `--verbose` | `boolean` | Verbose output | +| `--debug` | `boolean` | Debug mode | +| `--mcp-config ` | `string` | MCP config file path | +| `--add-dir ` | `string[]` | Additional directories for CLAUDE.md | +| `--resume [sessionId]` | `string?` | Resume previous session | +| `--bare` | `boolean` | Simple/stripped mode (no UI extras) | +| `--settings ` | `string` | Flag-layer settings override | +| `--setting-sources ` | `string` | Allowed settings sources | +| `--output-format ` | string | Output format for headless mode | +| `--max-turns ` | `number` | Max turns in headless mode | +| `--max-budget-usd ` | `number` | Cost budget limit | +| `--task-budget ` | `number` | API task budget | +| `--no-streaming` | `boolean` | Disable streaming | +| `--worktree ` | `string` | Git worktree mode | +| `--agent ` | `string` | Main thread agent type | +| `--tmux` / `--tmux=classic` | `boolean` | Use tmux | +| `--plugin-dir ` | `string[]` | Session-only plugin directories | +| `--input-format ` | string | Input format | +| `--sdk-betas ` | `string` | Comma-separated SDK beta headers | +| `--allowedTools ` | `string` | Tool allowlist (CLI override) | +| `--disallowedTools ` | `string` | Tool denylist (CLI override) | + +### Migration Functions + +Executed in `runMigrations()` when `globalConfig.migrationVersion !== 11`: + +| Function | Purpose | +|---|---| +| `migrateAutoUpdatesToSettings()` | Move auto-update config | +| `migrateBypassPermissionsAcceptedToSettings()` | Move bypass flag | +| `migrateEnableAllProjectMcpServersToSettings()` | Move MCP enable setting | +| `resetProToOpusDefault()` | Reset Pro model to Opus | +| `migrateSonnet1mToSonnet45()` | Rename model string | +| `migrateLegacyOpusToCurrent()` | Upgrade legacy Opus | +| `migrateSonnet45ToSonnet46()` | Rename to Sonnet 4.6 | +| `migrateOpusToOpus1m()` | Rename to Opus 1m | +| `migrateReplBridgeEnabledToRemoteControlAtStartup()` | Rename bridge flag | +| `resetAutoModeOptInForDefaultOffer()` | Reset auto mode opt-in (TRANSCRIPT_CLASSIFIER) | +| `migrateFennecToOpus()` | ant-only Fennec model migration | + +### Top-Level Side Effects + +```typescript +profileCheckpoint('main_tsx_entry') // startup profiling +startMdmRawRead() // parallel MDM subprocess (plutil/reg query) +startKeychainPrefetch() // parallel macOS keychain reads +``` + +### Key Internal Functions + +```typescript +function logManagedSettings(): void +function isBeingDebugged(): boolean +function logSessionTelemetry(): void +function getCertEnvVarTelemetry(): Record +async function logStartupTelemetry(): Promise +function runMigrations(): void +function prefetchSystemContextIfSafe(): void +function loadSettingsFromFlag(settingsFile: string): void +function loadSettingSourcesFromFlag(settingSourcesArg: string): void +function eagerLoadSettings(): void +function initializeEntrypoint(isNonInteractive: boolean): void +``` + +### Pending State Types (feature-gated) + +```typescript +type PendingConnect = { + url: string | undefined + authToken: string | undefined + dangerouslySkipPermissions: boolean +} + +type PendingAssistantChat = { + sessionId?: string + discover: boolean +} + +type PendingSSH = { + host: string | undefined + cwd: string | undefined + permissionMode: string | undefined + dangerouslySkipPermissions: boolean + local: boolean + extraCliArgs: string[] +} +``` + +### Feature Flags + +`DIRECT_CONNECT`, `KAIROS`, `SSH_REMOTE`, `LODESTONE`, `COORDINATOR_MODE`, `TRANSCRIPT_CLASSIFIER`, `BREAK_CACHE_COMMAND`, `HISTORY_SNIP`, `DAEMON`, `BG_SESSIONS`, `TEMPLATES` + +--- + +## replLauncher.tsx — REPL UI Launcher + +### Purpose + +Thin async launcher that dynamically imports `App` and `REPL` components (avoiding circular dependencies) and renders them into the Ink root. Exists as a separate file so that `App` and `REPL` are loaded lazily. + +### Exports + +```typescript +export async function launchRepl( + root: Root, + appProps: AppWrapperProps, + replProps: REPLProps, + renderAndRun: (root: Root, element: React.ReactNode) => Promise, +): Promise +``` + +### Types + +```typescript +type AppWrapperProps = { + getFpsMetrics: () => FpsMetrics | undefined + stats?: StatsStore + initialState: AppState +} +``` + +### Implementation + +Dynamically imports `./components/App.js` and `./screens/REPL.js`, then calls `renderAndRun(root, )`. + +### Dependencies + +- `react` +- `./context/stats.js` (type only) +- `./ink.js` (type only) +- `./screens/REPL.js` (type only) +- `./state/AppStateStore.js` (type only) +- `./utils/fpsTracker.js` (type only) + +--- + +## entrypoints/init.ts — Initialization & Telemetry + +### Purpose + +Handles all early initialization tasks that must complete before the CLI can safely make API calls or show a UI. Memoized so it runs exactly once per process. Telemetry initialization is deferred until after trust is established. + +### Exports + +```typescript +export const init: () => Promise // memoized with lodash-es/memoize +export function initializeTelemetryAfterTrust(): void +``` + +#### `init()` — Memoized Async Initialization + +Runs once. Sequence: + +1. `enableConfigs()` — validate and activate config system +2. `applySafeConfigEnvironmentVariables()` — apply env vars that are safe before trust +3. `applyExtraCACertsFromConfig()` — inject `NODE_EXTRA_CA_CERTS` before first TLS connection +4. `setupGracefulShutdown()` — register flush/cleanup on exit +5. `initialize1PEventLogging()` + GrowthBook refresh listener (deferred) +6. `populateOAuthAccountInfoIfNeeded()` (fire-and-forget) +7. `initJetBrainsDetection()` (fire-and-forget) +8. `detectCurrentRepository()` (fire-and-forget) +9. `initializeRemoteManagedSettingsLoadingPromise()` (if eligible) +10. `initializePolicyLimitsLoadingPromise()` (if eligible) +11. `recordFirstStartTime()` +12. `configureGlobalMTLS()` +13. `configureGlobalAgents()` (proxy) +14. `preconnectAnthropicApi()` — overlap TCP+TLS with action handler work +15. Upstream proxy initialization (CLAUDE_CODE_REMOTE only) +16. `setShellIfWindows()` — configure git-bash on Windows +17. `registerCleanup(shutdownLspServerManager)` +18. `registerCleanup(cleanupSessionTeams)` (lazy import) +19. `ensureScratchpadDir()` (if scratchpad enabled) + +**Error handling**: `ConfigParseError` → shows `InvalidConfigDialog` (interactive) or `stderr` (non-interactive). Other errors re-throw. + +#### `initializeTelemetryAfterTrust()` + +Called once after trust is established. For remote-settings-eligible users: waits for settings to load, then calls `applyConfigEnvironmentVariables()` before initializing. For SDK/headless with beta tracing: initializes eagerly first. + +Internal: `doInitializeTelemetry()` → `setMeterState()` → `initializeTelemetry()` (lazy-loaded OpenTelemetry, ~400KB) + +`AttributedCounter` factory: wraps OpenTelemetry `Counter` to always merge `getTelemetryAttributes()` with any additional attributes on each `add()` call. + +### Dependencies + +- `../bootstrap/state.js` +- `../utils/config.js` +- `../services/lsp/manager.js` +- `../services/oauth/client.js` +- `../services/policyLimits/index.js` +- `../services/remoteManagedSettings/index.js` +- `../utils/apiPreconnect.js` +- `../utils/caCertsConfig.js` +- `../utils/cleanupRegistry.js` +- `../utils/gracefulShutdown.js` +- `../utils/managedEnv.js` +- `../utils/mtls.js` +- `../utils/proxy.js` +- `../utils/telemetry/betaSessionTracing.js` +- `../utils/telemetryAttributes.js` +- `../utils/windowsPaths.js` + +--- + +## entrypoints/mcp.ts — MCP Server Entrypoint + +### Purpose + +Starts Claude Code as an MCP (Model Context Protocol) server, exposing Claude's built-in tools over the `stdio` transport. Server name: `claude/tengu`. + +### Exports + +```typescript +export async function startMCPServer( + cwd: string, + debug: boolean, + verbose: boolean, +): Promise +``` + +### Implementation Details + +- **Transport**: `StdioServerTransport` (stdin/stdout) +- **Capabilities**: `{ tools: {} }` +- **File state cache**: LRU with limit 100 files / 25 MB +- **Commands exposed**: Only `review` command (via `MCP_COMMANDS`) +- **Tool exposure**: All tools from `getTools(toolPermissionContext)` with empty permission context + +#### `ListTools` Handler + +For each tool: +1. Calls `tool.prompt(...)` to get the description +2. Converts `tool.inputSchema` to JSON Schema via `zodToJsonSchema()` +3. Converts `tool.outputSchema` (if present) to JSON Schema — only included if root type is `object` (not `anyOf`/`oneOf`) + +#### `CallTool` Handler + +1. Gets tools via `getTools(emptyPermissionContext)` +2. Finds tool by name; throws if not found +3. Calls `tool.isEnabled()`, `tool.validateInput()`, then `tool.call()` +4. Builds a `ToolUseContext` with: + - `isNonInteractiveSession: true` + - `thinkingConfig: { type: 'disabled' }` + - `mcpClients: []` +5. Returns `{ content: [{ type: 'text', text: result }] }` or `{ isError: true, content: [...] }` + +### Constants + +| Constant | Value | +|---|---| +| `READ_FILE_STATE_CACHE_SIZE` | `100` | +| MCP server name | `'claude/tengu'` | +| MCP server version | `MACRO.VERSION` | + +--- + +## entrypoints/agentSdkTypes.ts — Agent SDK Public API + +### Purpose + +The main entrypoint for Claude Code Agent SDK types. Re-exports all public SDK types and declares stub functions that throw `'not implemented'` — actual implementations are provided by the real SDK runtime (the CLI process). This file is the type-only interface for SDK consumers. + +### Exports + +```typescript +// Control protocol (alpha) +export type { SDKControlRequest, SDKControlResponse } from './sdk/controlTypes.js' + +// Core types (common serializable) +export * from './sdk/coreTypes.js' + +// Runtime types (callbacks, interfaces) +export * from './sdk/runtimeTypes.js' + +// Settings types +export type { Settings } from './sdk/settingsTypes.generated.js' + +// Tool types +export * from './sdk/toolTypes.js' +``` + +### Exported Functions (stubs) + +```typescript +export function tool( + _name: string, + _description: string, + _inputSchema: Schema, + _handler: (args: InferShape, extra: unknown) => Promise, + _extras?: { annotations?: ToolAnnotations; searchHint?: string; alwaysLoad?: boolean }, +): SdkMcpToolDefinition + +export function createSdkMcpServer( + _options: CreateSdkMcpServerOptions, +): McpSdkServerConfigWithInstance + +export class AbortError extends Error {} + +// V1 API +export function query(_params: { + prompt: string | AsyncIterable + options?: InternalOptions | Options +}): InternalQuery | Query + +// V2 API (alpha/unstable) +export function unstable_v2_createSession(_options: SDKSessionOptions): SDKSession +export function unstable_v2_resumeSession(_sessionId: string, _options: SDKSessionOptions): SDKSession +export async function unstable_v2_prompt(_message: string, _options: SDKSessionOptions): Promise + +// Session management +export async function getSessionMessages(_sessionId: string, _options?: GetSessionMessagesOptions): Promise +export async function listSessions(_options?: ListSessionsOptions): Promise +export async function getSessionInfo(_sessionId: string, _options?: GetSessionInfoOptions): Promise +export async function renameSession(_sessionId: string, _title: string, _options?: SessionMutationOptions): Promise +export async function tagSession(_sessionId: string, _tag: string | null, _options?: SessionMutationOptions): Promise +``` + +### Re-exported Constants + +```typescript +export const HOOK_EVENTS = [ + 'PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'Notification', + 'UserPromptSubmit', 'SessionStart', 'SessionEnd', 'Stop', 'StopFailure', + 'SubagentStart', 'SubagentStop', 'PreCompact', 'PostCompact', + 'PermissionRequest', 'PermissionDenied', 'Setup', 'TeammateIdle', + 'TaskCreated', 'TaskCompleted', 'Elicitation', 'ElicitationResult', + 'ConfigChange', 'WorktreeCreate', 'WorktreeRemove', 'InstructionsLoaded', + 'CwdChanged', 'FileChanged', +] as const + +export const EXIT_REASONS = [ + 'clear', 'resume', 'logout', 'prompt_input_exit', 'other', + 'bypass_permissions_disabled', +] as const +``` + +--- + +## entrypoints/sandboxTypes.ts — Sandbox Configuration Types + +### Purpose + +Single source of truth for sandbox configuration types. Both the SDK and settings validation import from here. + +### Exports + +#### Schemas (Zod, via `lazySchema`) + +```typescript +export const SandboxNetworkConfigSchema // optional object +export const SandboxFilesystemConfigSchema // optional object +export const SandboxSettingsSchema // passthrough object +``` + +#### Inferred TypeScript Types + +```typescript +export type SandboxSettings // from SandboxSettingsSchema +export type SandboxNetworkConfig // NonNullable<...> +export type SandboxFilesystemConfig // NonNullable<...> +export type SandboxIgnoreViolations // NonNullable +``` + +### Schema Detail + +#### `SandboxNetworkConfigSchema` + +| Field | Type | Description | +|---|---|---| +| `allowedDomains` | `string[]?` | Allowed outbound domains | +| `allowManagedDomainsOnly` | `boolean?` | Managed-settings-only domain enforcement | +| `allowUnixSockets` | `string[]?` | macOS-only unix socket paths | +| `allowAllUnixSockets` | `boolean?` | Disable unix socket blocking | +| `allowLocalBinding` | `boolean?` | Allow local port binding | +| `httpProxyPort` | `number?` | HTTP proxy port | +| `socksProxyPort` | `number?` | SOCKS proxy port | + +#### `SandboxFilesystemConfigSchema` + +| Field | Type | Description | +|---|---|---| +| `allowWrite` | `string[]?` | Additional write-allowed paths | +| `denyWrite` | `string[]?` | Additional write-denied paths | +| `denyRead` | `string[]?` | Additional read-denied paths | +| `allowRead` | `string[]?` | Paths to re-allow within denyRead regions | +| `allowManagedReadPathsOnly` | `boolean?` | Managed-settings-only read path enforcement | + +#### `SandboxSettingsSchema` + +| Field | Type | Description | +|---|---|---| +| `enabled` | `boolean?` | Enable sandboxing | +| `failIfUnavailable` | `boolean?` | Hard-fail if sandbox unavailable | +| `autoAllowBashIfSandboxed` | `boolean?` | Auto-allow Bash when sandboxed | +| `allowUnsandboxedCommands` | `boolean?` | Allow `dangerouslyDisableSandbox` param | +| `network` | `SandboxNetworkConfig?` | Network config | +| `filesystem` | `SandboxFilesystemConfig?` | Filesystem config | +| `ignoreViolations` | `Record?` | Per-rule violation ignoring | +| `enableWeakerNestedSandbox` | `boolean?` | Weaker nested sandbox | +| `enableWeakerNetworkIsolation` | `boolean?` | macOS: allow com.apple.trustd.agent | +| `excludedCommands` | `string[]?` | Commands to exclude from sandboxing | +| `ripgrep` | `{ command: string; args?: string[] }?` | Custom ripgrep config | + +Note: Schema uses `.passthrough()` — undocumented `enabledPlatforms` field is accepted. + +--- + +## entrypoints/sdk/coreSchemas.ts — SDK Core Zod Schemas + +### Purpose + +Single source of truth for SDK data type schemas. TypeScript types are code-generated from these schemas. Uses `lazySchema()` wrapper for all schemas (deferred evaluation). + +### Schema Groups + +#### Usage & Model + +```typescript +export const ModelUsageSchema // inputTokens, outputTokens, cacheRead/Write, webSearch, costUSD, contextWindow, maxOutputTokens +``` + +#### Output Format + +```typescript +export const OutputFormatTypeSchema // z.literal('json_schema') +export const BaseOutputFormatSchema // { type: OutputFormatTypeSchema } +export const JsonSchemaOutputFormatSchema // { type: 'json_schema', schema: Record } +export const OutputFormatSchema // = JsonSchemaOutputFormatSchema +``` + +#### Config + +```typescript +export const ApiKeySourceSchema // 'user' | 'project' | 'org' | 'temporary' | 'oauth' +export const ConfigScopeSchema // 'local' | 'user' | 'project' +export const SdkBetaSchema // z.literal('context-1m-2025-08-07') +``` + +#### Thinking Config + +```typescript +export const ThinkingAdaptiveSchema // { type: 'adaptive' } — Claude decides (Opus 4.6+) +export const ThinkingEnabledSchema // { type: 'enabled', budgetTokens?: number } — fixed budget +export const ThinkingDisabledSchema // { type: 'disabled' } +export const ThinkingConfigSchema // union of above three +``` + +#### MCP Server Config + +```typescript +export const McpStdioServerConfigSchema // { type?: 'stdio', command, args?, env? } +export const McpSSEServerConfigSchema // { type: 'sse', url, headers? } +export const McpHttpServerConfigSchema // { type: 'http', url, headers? } +export const McpSdkServerConfigSchema // { type: 'sdk', name } +export const McpServerConfigForProcessTransportSchema // union of stdio|sse|http|sdk +export const McpClaudeAIProxyServerConfigSchema // { type: 'claudeai-proxy', url, id } +export const McpServerStatusConfigSchema // union of process transport + claudeai-proxy +export const McpSetServersResultSchema // { added, removed, errors } +``` + +#### MCP Server Status + +```typescript +export const McpServerStatusSchema // { name, status, serverInfo?, error?, config?, scope?, tools?, capabilities? } +// status enum: 'connected' | 'failed' | 'needs-auth' | 'pending' | 'disabled' +``` + +#### Permission Types + +```typescript +export const PermissionUpdateDestinationSchema // 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg' +export const PermissionBehaviorSchema // 'allow' | 'deny' | 'ask' +export const PermissionRuleValueSchema // { toolName, ruleContent? } +export const PermissionUpdateSchema // discriminated union: addRules | replaceRules | removeRules | setMode | addDirectories | removeDirectories +export const PermissionDecisionClassificationSchema // 'user_temporary' | 'user_permanent' | 'user_reject' +export const PermissionResultSchema // { behavior: 'allow', updatedInput?, ... } | { behavior: 'deny', message, ... } +export const PermissionModeSchema // 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' +``` + +#### Hook Types + +```typescript +export const HOOK_EVENTS // const array of 28 event names (same as coreTypes.ts) +export const HookEventSchema // z.enum(HOOK_EVENTS) +export const BaseHookInputSchema // { session_id, transcript_path, cwd, permission_mode?, agent_id? } +export const HookInputSchema // full hook input (extends base) +``` + +#### Message Types + +All `SDKMessage*` schemas define the full message taxonomy emitted by `query()`: + +| Schema | Description | +|---|---| +| `SDKMessageSchema` | Root union of all message types | +| `SDKUserMessageSchema` | User turn message | +| `SDKStreamlinedTextMessageSchema` | Optimized text-only message | +| `SDKStreamlinedToolUseSummaryMessageSchema` | Tool use summary (optimized) | +| `SDKPostTurnSummaryMessageSchema` | End-of-turn summary | + +--- + +## entrypoints/sdk/coreTypes.ts — SDK Core TypeScript Types + +### Purpose + +TypeScript type declarations for the SDK, code-generated from `coreSchemas.ts`. Not edited manually. + +### Exports + +```typescript +// Re-exports sandbox types +export type { SandboxFilesystemConfig, SandboxIgnoreViolations, SandboxNetworkConfig, SandboxSettings } + from '../sandboxTypes.js' + +// All generated types +export * from './coreTypes.generated.js' + +// Utility types not expressible as Zod schemas +export type { NonNullableUsage } from './sdkUtilityTypes.js' +``` + +### Const Arrays (runtime) + +```typescript +export const HOOK_EVENTS = [...] as const // 28 hook event names +export const EXIT_REASONS = ['clear', 'resume', 'logout', 'prompt_input_exit', 'other', 'bypass_permissions_disabled'] as const +``` + +--- + +## entrypoints/sdk/controlSchemas.ts — SDK Control Protocol Schemas + +### Purpose + +Zod schemas for the SDK control protocol — the bidirectional communication channel between SDK implementations (Python SDK, desktop apps) and the CLI process. Uses `SDKControlRequest` / `SDKControlResponse` message wrappers over stdin/stdout. + +### Control Request Schemas + +Each request has a `subtype` discriminator: + +| Schema | `subtype` | Description | +|---|---|---| +| `SDKControlInitializeRequestSchema` | `'initialize'` | Initialize SDK session (hooks, MCP, agents, jsonSchema) | +| `SDKControlInterruptRequestSchema` | `'interrupt'` | Interrupt current turn | +| `SDKControlPermissionRequestSchema` | `'can_use_tool'` | Request tool permission | +| `SDKControlSetPermissionModeRequestSchema` | `'set_permission_mode'` | Change permission mode | +| `SDKControlSetModelRequestSchema` | `'set_model'` | Change active model | +| `SDKControlSetMaxThinkingTokensRequestSchema` | `'set_max_thinking_tokens'` | Set thinking budget | +| `SDKControlMcpStatusRequestSchema` | `'mcp_status'` | Get MCP server status | +| `SDKControlGetContextUsageRequestSchema` | `'get_context_usage'` | Get context window breakdown | +| `SDKControlRewindFilesRequestSchema` | `'rewind_files'` | Rewind file changes since a user message | +| `SDKControlCancelAsyncMessageRequestSchema` | `'cancel_async_message'` | Drop queued async message | +| `SDKControlSeedReadStateRequestSchema` | `'seed_read_state'` | Seed readFileState cache | +| `SDKHookCallbackRequestSchema` | `'hook_callback'` | Deliver hook callback | +| `SDKControlMcpMessageRequestSchema` | `'mcp_message'` | Send JSON-RPC to MCP server | +| `SDKControlMcpSetServersRequestSchema` | `'mcp_set_servers'` | Replace dynamic MCP servers | +| `SDKControlReloadPluginsRequestSchema` | `'reload_plugins'` | Reload plugins from disk | +| `SDKControlMcpReconnectRequestSchema` | `'mcp_reconnect'` | Reconnect failed MCP server | +| `SDKControlMcpToggleRequestSchema` | `'mcp_toggle'` | Enable/disable MCP server | +| `SDKControlStopTaskRequestSchema` | `'stop_task'` | Stop a running task | +| `SDKControlApplyFlagSettingsRequestSchema` | `'apply_flag_settings'` | Merge flag settings layer | +| `SDKControlGetSettingsRequestSchema` | `'get_settings'` | Get effective + per-source settings | +| `SDKControlElicitationRequestSchema` | `'elicitation'` | MCP elicitation request | + +### Control Response Schemas + +```typescript +export const SDKControlInitializeResponseSchema // commands, agents, output_style, models, account, pid?, fast_mode_state? +export const SDKControlMcpStatusResponseSchema // { mcpServers: McpServerStatus[] } +export const SDKControlGetContextUsageResponseSchema // detailed context breakdown +export const SDKControlRewindFilesResponseSchema // { canRewind, error?, filesChanged?, insertions?, deletions? } +export const SDKControlCancelAsyncMessageResponseSchema // { cancelled: boolean } +export const SDKControlMcpSetServersResponseSchema // { added, removed, errors } +export const SDKControlReloadPluginsResponseSchema // { commands, agents, plugins, mcpServers, error_count } +export const SDKControlGetSettingsResponseSchema // { effective, sources, applied? } +export const SDKControlElicitationResponseSchema // { action: 'accept'|'decline'|'cancel', content? } +``` + +### Wire Message Wrappers + +```typescript +// Outer request envelope +export const SDKControlRequestSchema = z.object({ + type: z.literal('control_request'), + request_id: z.string(), + request: SDKControlRequestInnerSchema(), // union of all request types +}) + +// Response envelope +export const SDKControlResponseSchema = z.object({ + type: z.literal('control_response'), + response: z.union([ControlResponseSchema(), ControlErrorResponseSchema()]), +}) + +// Cancel (for long-running requests) +export const SDKControlCancelRequestSchema = z.object({ + type: z.literal('control_cancel_request'), + request_id: z.string(), +}) +``` + +### Aggregate Message Types + +```typescript +// Messages written to stdout by CLI +export const StdoutMessageSchema = z.union([ + SDKMessageSchema(), + SDKStreamlinedTextMessageSchema(), + SDKStreamlinedToolUseSummaryMessageSchema(), + SDKPostTurnSummaryMessageSchema(), + SDKControlResponseSchema(), + SDKControlRequestSchema(), + SDKControlCancelRequestSchema(), + SDKKeepAliveMessageSchema(), +]) + +// Messages read from stdin by CLI +export const StdinMessageSchema = z.union([ + SDKUserMessageSchema(), + SDKControlRequestSchema(), + SDKControlResponseSchema(), + SDKKeepAliveMessageSchema(), + SDKUpdateEnvironmentVariablesMessageSchema(), +]) +``` + +### Hook Callback Matcher + +```typescript +export const SDKHookCallbackMatcherSchema = z.object({ + matcher: z.string().optional(), + hookCallbackIds: z.array(z.string()), + timeout: z.number().optional(), +}) +``` + +### Context Usage Response Detail + +The `SDKControlGetContextUsageResponseSchema` response includes: + +| Field | Type | +|---|---| +| `categories` | `Array<{ name, tokens, color, isDeferred? }>` | +| `totalTokens` | `number` | +| `maxTokens` | `number` | +| `rawMaxTokens` | `number` | +| `percentage` | `number` | +| `gridRows` | `Array>` | +| `model` | `string` | +| `memoryFiles` | `Array<{ path, type, tokens }>` | +| `mcpTools` | `Array<{ name, serverName, tokens, isLoaded? }>` | +| `deferredBuiltinTools` | `Array<{ name, tokens, isLoaded }>?` | +| `systemTools` | `Array<{ name, tokens }>?` | +| `systemPromptSections` | `Array<{ name, tokens }>?` | +| `agents` | `Array<{ agentType, source, tokens }>` | +| `slashCommands` | `{ totalCommands, includedCommands, tokens }?` | +| `skills` | `{ totalSkills, includedSkills, tokens, skillFrontmatter[] }?` | +| `autoCompactThreshold` | `number?` | +| `isAutoCompactEnabled` | `boolean` | +| `messageBreakdown` | detailed message token breakdown `?` | +| `apiUsage` | `{ input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens }` or `null` | + +--- + +## query.ts — Core Async Query Loop + +### Purpose + +The core agentic query loop. Drives the back-and-forth between the user's prompt, the Claude API, and tool execution. Implemented as an `AsyncGenerator` that yields `StreamEvent | RequestStartEvent | Message | TombstoneMessage | ToolUseSummaryMessage` and returns a `Terminal` value. + +### Exports + +```typescript +export type QueryParams = { + messages: Message[] + systemPrompt: SystemPrompt + userContext: { [k: string]: string } + systemContext: { [k: string]: string } + canUseTool: CanUseToolFn + toolUseContext: ToolUseContext + fallbackModel?: string + querySource: QuerySource + maxOutputTokensOverride?: number + maxTurns?: number + skipCacheWrite?: boolean + taskBudget?: { total: number } + deps?: QueryDeps +} + +export async function* query( + params: QueryParams, +): AsyncGenerator< + StreamEvent | RequestStartEvent | Message | TombstoneMessage | ToolUseSummaryMessage, + Terminal +> +``` + +### Internal State Type + +```typescript +type State = { + messages: Message[] + toolUseContext: ToolUseContext + autoCompactTracking: AutoCompactTrackingState | undefined + maxOutputTokensRecoveryCount: number + hasAttemptedReactiveCompact: boolean + maxOutputTokensOverride: number | undefined + pendingToolUseSummary: Promise | undefined + stopHookActive: boolean | undefined + turnCount: number + transition: Continue | undefined // Why the previous iteration continued +} +``` + +### Constants + +| Constant | Value | Description | +|---|---|---| +| `MAX_OUTPUT_TOKENS_RECOVERY_LIMIT` | `3` | Max recovery retries for max_output_tokens errors | + +### Query Loop Architecture + +``` +query(params) + └── queryLoop(params, consumedCommandUuids) + ├── snapshot config (buildQueryConfig()) + ├── start memory prefetch (startRelevantMemoryPrefetch) + └── while (true): + 1. yield { type: 'stream_request_start' } + 2. build queryTracking (chainId/depth) + 3. get messages after compact boundary + 4. apply tool result budget (applyToolResultBudget) + 5. snip compact if needed (HISTORY_SNIP feature) + 6. microcompact (deps.microcompact) + 7. context collapse (CONTEXT_COLLAPSE feature) + 8. build fullSystemPrompt + 9. autocompact (deps.autocompact) → maybe yield compact boundary messages + 10. check blocking token limit (if not compacted and not reactive compact) + 11. call model (deps.callModel) → stream assistant messages + 12. execute tools (runTools or StreamingToolExecutor) + 13. yield messages, tool results + 14. handleStopHooks + 15. check for continuation conditions: + - stop hooks blocked → continue with blocking errors + - maxTurns exceeded → return 'max_turns' + - no tool use → check tokenBudget → return 'end_turn' + - tool use → continue loop +``` + +### Recovery Paths + +The query loop includes several error recovery paths: + +| Error | Recovery | +|---|---| +| `max_output_tokens` | Retry up to `MAX_OUTPUT_TOKENS_RECOVERY_LIMIT` times, incrementing budget | +| Prompt too long | Reactive compact (REACTIVE_COMPACT feature) or return `blocking_limit` | +| Streaming fallback | Tombstone orphaned messages, create fresh `StreamingToolExecutor` | +| FallbackTriggeredError | Switch to fallback model, retry | +| Context collapse overflow | Drain staged collapses via CONTEXT_COLLAPSE feature | + +### Feature Flags + +`HISTORY_SNIP`, `CONTEXT_COLLAPSE`, `REACTIVE_COMPACT`, `CACHED_MICROCOMPACT`, `TOKEN_BUDGET`, `BG_SESSIONS` + +### Tool Execution Integration + +- **Streaming tool execution** (gated on `config.gates.streamingToolExecution`): Uses `StreamingToolExecutor` class +- **Sequential tool execution**: Uses `runTools()` from `services/tools/toolOrchestration.js` +- Tool results are yielded back into the loop as `UserMessage` objects + +### Key Behaviors + +- **`persistReplacements`**: Tool result content replacement is persisted for `agent:*` and `repl_main_thread*` query sources +- **`backfillObservableInput`**: Adds observable fields to tool input before yielding (e.g., expanded file paths) — only when NEW fields are added, not overwrites +- **Tombstoning**: Orphaned messages from failed streaming fallback are tombstoned via `{ type: 'tombstone', message }` events +- **Query chain tracking**: Each iteration increments `queryTracking.depth`; first iteration creates a new `chainId` UUID + +--- + +## QueryEngine.ts — Stateful Query Engine (SDK/Headless) + +### Purpose + +Owns the complete query lifecycle and session state for a conversation. Designed for the SDK/headless (`-p`) path. One instance per conversation; each `submitMessage()` call starts a new turn while preserving all state (messages, file cache, usage, permission denials). + +### Exports + +```typescript +export type QueryEngineConfig = { + cwd: string + tools: Tools + commands: Command[] + mcpClients: MCPServerConnection[] + agents: AgentDefinition[] + canUseTool: CanUseToolFn + getAppState: () => AppState + setAppState: (f: (prev: AppState) => AppState) => void + initialMessages?: Message[] + readFileCache: FileStateCache + customSystemPrompt?: string + appendSystemPrompt?: string + userSpecifiedModel?: string + fallbackModel?: string + thinkingConfig?: ThinkingConfig + maxTurns?: number + maxBudgetUsd?: number + taskBudget?: { total: number } + jsonSchema?: Record + verbose?: boolean + replayUserMessages?: boolean + handleElicitation?: ToolUseContext['handleElicitation'] + includePartialMessages?: boolean + setSDKStatus?: (status: SDKStatus) => void + abortController?: AbortController + orphanedPermission?: OrphanedPermission + snipReplay?: (yieldedSystemMsg: Message, store: Message[]) => { messages: Message[]; executed: boolean } | undefined +} + +export class QueryEngine { + constructor(config: QueryEngineConfig) + async *submitMessage( + prompt: string | ContentBlockParam[], + options?: { uuid?: string; isMeta?: boolean }, + ): AsyncGenerator + abort(): void + getMessages(): Message[] + getTotalUsage(): NonNullableUsage + getPermissionDenials(): SDKPermissionDenial[] +} +``` + +### Internal State + +```typescript +private config: QueryEngineConfig +private mutableMessages: Message[] +private abortController: AbortController +private permissionDenials: SDKPermissionDenial[] +private totalUsage: NonNullableUsage +private hasHandledOrphanedPermission: boolean +private readFileState: FileStateCache +private discoveredSkillNames: Set // cleared each submitMessage() +private loadedNestedMemoryPaths: Set // grows across turns +``` + +### `submitMessage()` Flow + +1. Clear `discoveredSkillNames` +2. Set CWD via `setCwd(cwd)` +3. Wrap `canUseTool` to track permission denials +4. Resolve initial model and thinking config +5. `fetchSystemPromptParts()` — build system prompt, user context, system context +6. Build `systemPrompt` from `customSystemPrompt | defaultSystemPrompt` + `memoryMechanicsPrompt?` + `appendSystemPrompt?` +7. Register structured output enforcement (if `jsonSchema` + synthetic output tool) +8. Build initial `processUserInputContext` +9. Handle orphaned permission (once per lifetime) +10. Call `processUserInput()` for the user's prompt +11. Push new messages to `mutableMessages` +12. Persist to transcript (before API call — ensures `--resume` works even if killed mid-flight) +13. Replay user messages if `replayUserMessages: true` +14. Update `ToolPermissionContext.alwaysAllowRules.command` from `processUserInput` result +15. Re-build `processUserInputContext` with updated messages and model +16. Stream from `query()` generator — convert to `SDKMessage`, yield +17. Track usage via `accumulateUsage()` / `updateUsage()` +18. Handle local command output, compact boundaries, snip boundaries + +### Transcript Persistence + +In `submitMessage()`, the user's messages are written to the transcript **before** entering the API query loop. Timing variants: +- **`--bare` / `isBareMode()`**: Fire-and-forget (saves ~4ms on SSD) +- **`CLAUDE_CODE_EAGER_FLUSH` or `CLAUDE_CODE_IS_COWORK`**: Awaited + flushed +- **Default**: Awaited + +### `ProcessUserInputContext` Internals + +First build (before slash command processing): +- `setMessages`: writes back to `mutableMessages` +- `isNonInteractiveSession: true` + +Second build (after slash command processing): +- `setMessages`: no-op (slash commands already committed) + +--- + +## query/config.ts — Query Configuration Snapshot + +### Purpose + +Captures immutable configuration values at query entry. Separated from per-iteration state to make future `step()` extraction (pure reducer pattern) tractable. Intentionally excludes `feature()` gates (those are build-time tree-shaking boundaries). + +### Exports + +```typescript +export type QueryConfig = { + sessionId: SessionId + gates: { + streamingToolExecution: boolean // Statsig: 'tengu_streaming_tool_execution2' + emitToolUseSummaries: boolean // env: CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES + isAnt: boolean // env: USER_TYPE === 'ant' + fastModeEnabled: boolean // env: !CLAUDE_CODE_DISABLE_FAST_MODE + } +} + +export function buildQueryConfig(): QueryConfig +``` + +### Gate Details + +| Gate | Source | Key | +|---|---|---| +| `streamingToolExecution` | Statsig (cached, may be stale) | `tengu_streaming_tool_execution2` | +| `emitToolUseSummaries` | Environment variable | `CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES` | +| `isAnt` | Environment variable | `USER_TYPE === 'ant'` | +| `fastModeEnabled` | Environment variable | `!CLAUDE_CODE_DISABLE_FAST_MODE` | + +--- + +## query/deps.ts — Query Dependency Injection + +### Purpose + +I/O dependencies for `query()`, passed via `QueryParams.deps`. Enables test injection of fakes without `spyOn`-per-module boilerplate. + +### Exports + +```typescript +export type QueryDeps = { + callModel: typeof queryModelWithStreaming // API streaming call + microcompact: typeof microcompactMessages // microcompaction + autocompact: typeof autoCompactIfNeeded // autocompaction + uuid: () => string // UUID generation +} + +export function productionDeps(): QueryDeps +``` + +### Production Dependencies + +| Dep | Implementation | +|---|---| +| `callModel` | `queryModelWithStreaming` from `services/api/claude.js` | +| `microcompact` | `microcompactMessages` from `services/compact/microCompact.js` | +| `autocompact` | `autoCompactIfNeeded` from `services/compact/autoCompact.js` | +| `uuid` | `randomUUID` from Node.js `crypto` module | + +--- + +## query/stopHooks.ts — Stop Hook Orchestration + +### Purpose + +Orchestrates all end-of-turn hooks: `Stop`, `SubagentStop`, `TeammateIdle`, `TaskCompleted`. Also handles background side-effects (prompt suggestion, memory extraction, auto-dream, computer-use cleanup). + +### Exports + +```typescript +export async function* handleStopHooks( + messagesForQuery: Message[], + assistantMessages: AssistantMessage[], + systemPrompt: SystemPrompt, + userContext: { [k: string]: string }, + systemContext: { [k: string]: string }, + toolUseContext: ToolUseContext, + querySource: QuerySource, + stopHookActive?: boolean, +): AsyncGenerator< + StreamEvent | RequestStartEvent | Message | TombstoneMessage | ToolUseSummaryMessage, + StopHookResult +> + +type StopHookResult = { + blockingErrors: Message[] + preventContinuation: boolean +} +``` + +### Execution Order + +1. **`saveCacheSafeParams()`** — snapshot context for prompt suggestion / btw queries (main thread and SDK only) +2. **Template job classification** (TEMPLATES feature, main thread only, non-subagent): `classifyAndWriteState()` — max 60s timeout +3. **Background side-effects** (non-bare mode): + - `executePromptSuggestion()` (fire-and-forget, unless `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false`) + - `executeExtractMemories()` (EXTRACT_MEMORIES feature, main thread only) + - `executeAutoDream()` (non-subagent) +4. **Computer-use cleanup** (CHICAGO_MCP feature, main thread only): `cleanupComputerUseAfterTurn()` +5. **`executeStopHooks()`** — runs Stop/SubagentStop hooks in parallel, yields progress messages +6. **Summary message** if any hooks ran +7. **Notification** if hook errors occurred +8. **Teammate hooks** (if `isTeammate()`): + - `executeTaskCompletedHooks()` for in-progress tasks owned by this agent + - `executeTeammateIdleHooks()` + +### Hook Result Types + +| Result Field | Effect | +|---|---| +| `blockingError` | Creates `UserMessage` with `isMeta: true`, added to `blockingErrors` | +| `preventContinuation` | Sets flag, yields `hook_stopped_continuation` attachment | +| Abort signal | Yields `UserInterruptionMessage`, returns `{ blockingErrors: [], preventContinuation: true }` | + +### Telemetry Events + +- `tengu_pre_stop_hooks_cancelled` — hook aborted mid-execution +- `tengu_stop_hook_error` — exception in hook execution + +--- + +## query/tokenBudget.ts — Token Budget Tracking + +### Purpose + +Tracks token budget utilization across query loop iterations to decide whether to continue or stop based on the `+500k auto-continue` feature (distinct from the API `task_budget`). + +### Exports + +```typescript +export type BudgetTracker = { + continuationCount: number + lastDeltaTokens: number + lastGlobalTurnTokens: number + startedAt: number +} + +export function createBudgetTracker(): BudgetTracker + +export type TokenBudgetDecision = + | { action: 'continue'; nudgeMessage: string; continuationCount: number; pct: number; turnTokens: number; budget: number } + | { action: 'stop'; completionEvent: { continuationCount: number; pct: number; turnTokens: number; budget: number; diminishingReturns: boolean; durationMs: number } | null } + +export function checkTokenBudget( + tracker: BudgetTracker, + agentId: string | undefined, + budget: number | null, + globalTurnTokens: number, +): TokenBudgetDecision +``` + +### Constants + +| Constant | Value | Description | +|---|---|---| +| `COMPLETION_THRESHOLD` | `0.9` | 90% of budget consumed → stop | +| `DIMINISHING_THRESHOLD` | `500` | Tokens delta < 500 across two checks → diminishing returns | + +### Algorithm + +``` +checkTokenBudget(tracker, agentId, budget, globalTurnTokens): + if agentId or budget null or budget <= 0: + return { action: 'stop', completionEvent: null } + + pct = round(turnTokens / budget * 100) + deltaSinceLast = globalTurnTokens - tracker.lastGlobalTurnTokens + + isDiminishing = ( + continuationCount >= 3 AND + deltaSinceLast < 500 AND + lastDeltaTokens < 500 + ) + + if !isDiminishing AND turnTokens < budget * 0.9: + → return { action: 'continue', nudgeMessage: ... } + + if isDiminishing OR continuationCount > 0: + → return { action: 'stop', completionEvent: { ..., diminishingReturns: isDiminishing } } + + → return { action: 'stop', completionEvent: null } +``` + +--- + +## context.ts — System & User Context Providers + +### Purpose + +Provides memoized context functions that build the `systemContext` and `userContext` dictionaries prepended to each conversation. Both are cached for the duration of the conversation. Includes the git status, CLAUDE.md content, and current date. + +### Exports + +```typescript +export function getSystemPromptInjection(): string | null +export function setSystemPromptInjection(value: string | null): void + +export const getGitStatus: () => Promise // memoized +export const getSystemContext: () => Promise<{ [k: string]: string }> // memoized +export const getUserContext: () => Promise<{ [k: string]: string }> // memoized +``` + +### Constants + +| Constant | Value | Description | +|---|---|---| +| `MAX_STATUS_CHARS` | `2000` | Max characters for `git status --short` output | + +### `getGitStatus()` — Memoized + +Returns `null` in test environment. Otherwise: +1. Checks `getIsGit()` — returns `null` if not a git repo +2. Runs in parallel: `getBranch()`, `getDefaultBranch()`, `git status --short`, `git log --oneline -n 5`, `git config user.name` +3. Truncates status at `MAX_STATUS_CHARS` (appends truncation notice) +4. Returns formatted string with branch, main branch, git user, status, and recent commits + +**Format**: +``` +This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation. + +Current branch: + +Main branch (you will usually use this for PRs): + +Git user: + +Status: + + +Recent commits: + +``` + +### `getSystemContext()` — Memoized + +```typescript +{ + gitStatus?: string, // from getGitStatus() — skipped for CCR or when git disabled + cacheBreaker?: string, // "[CACHE_BREAKER: injection]" — BREAK_CACHE_COMMAND feature only +} +``` + +Skips git status when: +- `CLAUDE_CODE_REMOTE=true` (CCR environment) +- `shouldIncludeGitInstructions()` returns false + +### `getUserContext()` — Memoized + +```typescript +{ + claudeMd?: string, // Combined CLAUDE.md content + currentDate: string, // "Today's date is YYYY-MM-DD." +} +``` + +CLAUDE.md loading is disabled when: +- `CLAUDE_CODE_DISABLE_CLAUDE_MDS=true` +- `isBareMode()` AND no `--add-dir` directories + +Side effect: calls `setCachedClaudeMdContent()` to cache content for auto-mode classifier. + +### `setSystemPromptInjection()` Side Effect + +When injection changes, clears both `getUserContext.cache` and `getSystemContext.cache` (from lodash memoize) to force rebuild. + +--- + +## history.ts — Prompt History Management + +### Purpose + +Manages the persistent prompt history (up-arrow navigation and Ctrl+R fuzzy search). History is stored as JSONL in `~/.claude/history.jsonl`. Handles pasted content with both inline (≤ 1024 bytes) and external hash-based storage. Entries are scoped to project root. + +### Exports + +```typescript +// Pasted content helpers +export function getPastedTextRefNumLines(text: string): number +export function formatPastedTextRef(id: number, numLines: number): string +export function formatImageRef(id: number): string +export function parseReferences(input: string): Array<{ id: number; match: string; index: number }> +export function expandPastedTextRefs(input: string, pastedContents: Record): string + +// History reading +export async function* makeHistoryReader(): AsyncGenerator +export async function* getTimestampedHistory(): AsyncGenerator +export async function* getHistory(): AsyncGenerator + +// History writing +export function addToHistory(command: HistoryEntry | string): void +export function clearPendingHistoryEntries(): void +export function removeLastFromHistory(): void + +// Types +export type TimestampedHistoryEntry = { + display: string + timestamp: number + resolve: () => Promise +} +``` + +### Constants + +| Constant | Value | Description | +|---|---|---| +| `MAX_HISTORY_ITEMS` | `100` | Max entries returned from `getHistory()` | +| `MAX_PASTED_CONTENT_LENGTH` | `1024` | Threshold for inline vs hash-based storage | + +### Internal Types + +```typescript +type LogEntry = { + display: string + pastedContents: Record + timestamp: number + project: string + sessionId?: string +} + +type StoredPastedContent = { + id: number + type: 'text' | 'image' + content?: string // inline (≤ MAX_PASTED_CONTENT_LENGTH) + contentHash?: string // hash ref for large pastes + mediaType?: string + filename?: string +} +``` + +### History File + +**Path**: `join(getClaudeConfigHomeDir(), 'history.jsonl')` + +**Locking**: Uses file-based lockfile with: +- `stale: 10000` ms +- Retries: 3, min timeout 50ms + +### `addToHistory()` Behavior + +1. Skips if `CLAUDE_CODE_SKIP_PROMPT_HISTORY=true` (tmux subprocess sessions) +2. Registers cleanup hook on first call (flushes pending entries on process exit) +3. Calls `addToPromptHistory()` async (fire-and-forget) + +### `getHistory()` Ordering + +Current session entries first (newest-first), then other session entries (also newest-first). Same `MAX_HISTORY_ITEMS` window. This prevents concurrent sessions from interleaving up-arrow history. + +### `removeLastFromHistory()` — Undo Last Entry + +Fast path: if entry is still in `pendingEntries`, splices it out. +Slow path: if already flushed, adds timestamp to `skippedTimestamps` set (consulted during reads). +One-shot: clears `lastAddedEntry` after use. + +### Paste Reference Pattern + +Reference format: `\[(Pasted text|Image|\.\.\.Truncated text) #(\d+)(?: \+\d+ lines)?(\.)*\]` + +Examples: +- `[Pasted text #1]` — zero-line paste +- `[Pasted text #1 +10 lines]` — multi-line paste (counts `\n` occurrences) +- `[Image #2]` — image paste + +--- + +## cost-tracker.ts — Session Cost Tracking + +### Purpose + +Manages session cost/usage tracking, persistence to project config, and formatted display. Delegates all state to `bootstrap/state.ts`. + +### Exports + +```typescript +// Re-exports from bootstrap/state.ts +export { getTotalCostUSD as getTotalCost } +export { getTotalDuration } +export { getTotalAPIDuration } +export { getTotalAPIDurationWithoutRetries } +export { addToTotalLinesChanged } +export { getTotalLinesAdded, getTotalLinesRemoved } +export { getTotalInputTokens, getTotalOutputTokens } +export { getTotalCacheReadInputTokens, getTotalCacheCreationInputTokens } +export { getTotalWebSearchRequests } +export { hasUnknownModelCost } +export { resetStateForTests, resetCostState } +export { setHasUnknownModelCost } +export { getModelUsage, getUsageForModel } +export { formatCost } // internal function, also exported + +// New functions +export function getStoredSessionCosts(sessionId: string): StoredCostState | undefined +export function restoreCostStateForSession(sessionId: string): boolean +export function saveCurrentSessionCosts(fpsMetrics?: FpsMetrics): void +export function addToTotalSessionCost(cost: number, usage: Usage, model: string): number +export function formatTotalCost(): string +``` + +### Internal Types + +```typescript +type StoredCostState = { + totalCostUSD: number + totalAPIDuration: number + totalAPIDurationWithoutRetries: number + totalToolDuration: number + totalLinesAdded: number + totalLinesRemoved: number + lastDuration: number | undefined + modelUsage: { [modelName: string]: ModelUsage } | undefined +} +``` + +### `addToTotalSessionCost()` Detail + +1. Calls `addToTotalModelUsage()` — aggregates per-model token counts +2. Calls `addToTotalCostState()` in bootstrap/state.ts +3. Records to OpenTelemetry counters (`getCostCounter()`, `getTokenCounter()`) +4. Processes advisor usage from `getAdvisorUsage(usage)` — recursively calls itself for each advisor model +5. Returns total cost (including advisor costs) + +### `saveCurrentSessionCosts()` — Persisted Fields + +Writes to project config: +- `lastCost`, `lastAPIDuration`, `lastAPIDurationWithoutRetries`, `lastToolDuration`, `lastDuration` +- `lastLinesAdded`, `lastLinesRemoved` +- `lastTotalInputTokens`, `lastTotalOutputTokens` +- `lastTotalCacheCreationInputTokens`, `lastTotalCacheReadInputTokens` +- `lastTotalWebSearchRequests` +- `lastFpsAverage`, `lastFpsLow1Pct` (from `fpsMetrics`) +- `lastModelUsage` (per-model: input/output/cache tokens, web searches, cost) +- `lastSessionId` + +### `formatTotalCost()` — Display Format + +``` +Total cost: $X.XXXX +Total duration (API): Xs +Total duration (wall): Xs +Total code changes: N lines added, N lines removed +Usage by model: + claude-sonnet-4-6: N input, N output, N cache read, N cache write ($X.XXXX) +``` + +### `formatCost()` Helper + +```typescript +function formatCost(cost: number, maxDecimalPlaces: number = 4): string +// Returns "$X.XX" if cost > 0.5, else "$X.XXXX" (or fewer decimal places) +``` + +--- + +## costHook.ts — React Cost Summary Hook + +### Purpose + +React hook that registers a `process.exit` listener to print cost summary and save session costs when the process exits. + +### Exports + +```typescript +export function useCostSummary(getFpsMetrics?: () => FpsMetrics | undefined): void +``` + +### Behavior + +- Runs once on mount (empty dependency array) +- On `process.exit`: + 1. If `hasConsoleBillingAccess()`: writes `formatTotalCost()` to stdout + 2. Calls `saveCurrentSessionCosts(getFpsMetrics?.())` +- Cleans up the exit listener on unmount + +--- + +## projectOnboardingState.ts — Project Onboarding State + +### Purpose + +Tracks and controls the display of the project onboarding checklist (shown when a user first opens a new project). + +### Exports + +```typescript +export type Step = { + key: string + text: string + isComplete: boolean + isCompletable: boolean + isEnabled: boolean +} + +export function getSteps(): Step[] +export function isProjectOnboardingComplete(): boolean +export function maybeMarkProjectOnboardingComplete(): void +export const shouldShowProjectOnboarding: () => boolean // memoized +export function incrementProjectOnboardingSeenCount(): void +``` + +### Steps + +| Key | Condition to enable | Completion check | +|---|---|---| +| `'workspace'` | `isDirEmpty(getCwd())` | Never auto-completes (user must act) | +| `'claudemd'` | `!isDirEmpty(getCwd())` | `existsSync(join(getCwd(), 'CLAUDE.md'))` | + +### `shouldShowProjectOnboarding()` — Memoized + +Returns `false` if any of: +- `projectConfig.hasCompletedProjectOnboarding === true` +- `projectConfig.projectOnboardingSeenCount >= 4` +- `process.env.IS_DEMO` is set +- `isProjectOnboardingComplete()` returns true + +### `maybeMarkProjectOnboardingComplete()` Behavior + +Short-circuits on `hasCompletedProjectOnboarding: true` in cached config (avoids filesystem hit on every prompt submit). Saves to project config when complete. + +--- + +## bootstrap/state.ts — Global Session State + +### Purpose + +The single module-level state singleton for a Claude Code process. **DO NOT ADD MORE STATE HERE** (documented with triple comment emphasis). Contains all session-scoped values including costs, tokens, model configuration, telemetry, agent state, and session identity. + +### State Structure + +The `State` type is a large flat object. Key groupings: + +#### Session Identity + +| Field | Type | Description | +|---|---|---| +| `sessionId` | `SessionId` | UUID (randomUUID at init) | +| `parentSessionId` | `SessionId?` | Parent session for lineage tracking | +| `originalCwd` | `string` | CWD at startup (NFC-normalized) | +| `projectRoot` | `string` | Stable project root (set by --worktree; not updated mid-session) | +| `cwd` | `string` | Current working directory (updated by setCwd) | +| `sessionProjectDir` | `string | null` | Dir containing session JSONL; null = derive from originalCwd | + +#### Cost & Token Counters + +| Field | Type | +|---|---| +| `totalCostUSD` | `number` | +| `totalAPIDuration` | `number` | +| `totalAPIDurationWithoutRetries` | `number` | +| `totalToolDuration` | `number` | +| `totalLinesAdded`, `totalLinesRemoved` | `number` | +| `modelUsage` | `{ [modelName: string]: ModelUsage }` | + +#### Per-Turn Counters (reset each turn) + +| Field | Type | +|---|---| +| `turnHookDurationMs` | `number` | +| `turnToolDurationMs` | `number` | +| `turnClassifierDurationMs` | `number` | +| `turnToolCount` | `number` | +| `turnHookCount` | `number` | +| `turnClassifierCount` | `number` | + +#### Model Configuration + +| Field | Type | Description | +|---|---|---| +| `mainLoopModelOverride` | `ModelSetting?` | Set by --model flag | +| `initialMainLoopModel` | `ModelSetting` | Set at startup | +| `modelStrings` | `ModelStrings | null` | Loaded model string definitions | + +#### Telemetry / OpenTelemetry + +| Field | Type | +|---|---| +| `meter` | `Meter | null` | +| `meterProvider` | `MeterProvider | null` | +| `loggerProvider` | `LoggerProvider | null` | +| `tracerProvider` | `BasicTracerProvider | null` | +| `eventLogger` | `ReturnType | null` | +| `sessionCounter` | `AttributedCounter | null` | +| `locCounter`, `prCounter`, `commitCounter` | `AttributedCounter | null` | +| `costCounter`, `tokenCounter` | `AttributedCounter | null` | +| `codeEditToolDecisionCounter`, `activeTimeCounter` | `AttributedCounter | null` | +| `statsStore` | `{ observe(name, value): void } | null` | + +#### Session Flags + +| Field | Default | Description | +|---|---|---| +| `isInteractive` | `false` | Interactive REPL mode | +| `kairosActive` | `false` | Assistant (KAIROS) mode active | +| `strictToolResultPairing` | `false` | HFI mode — throws on mismatch | +| `sessionBypassPermissionsMode` | `false` | Not persisted | +| `sessionPersistenceDisabled` | `false` | Disable transcript write | +| `sessionTrustAccepted` | `false` | Session-only trust (home dir) | +| `hasExitedPlanMode` | `false` | For re-entry guidance | +| `scheduledTasksEnabled` | `false` | Set by cron scheduler | +| `isRemoteMode` | `false` | --remote flag | + +#### Prompt Cache Latches + +All start as `null` (not yet triggered), flip to `true` once, and stay `true`: + +| Field | Purpose | +|---|---| +| `afkModeHeaderLatched` | Sticky AFK_MODE_BETA_HEADER | +| `fastModeHeaderLatched` | Sticky FAST_MODE_BETA_HEADER | +| `cacheEditingHeaderLatched` | Sticky cache-editing beta header | +| `thinkingClearLatched` | Clear thinking after >1h idle | + +### Exported Functions (selected) + +#### Session Identity + +```typescript +export function getSessionId(): SessionId +export function regenerateSessionId(options?: { setCurrentAsParent?: boolean }): SessionId +export function getParentSessionId(): SessionId | undefined +export function switchSession(sessionId: SessionId, projectDir?: string | null): void +export function getSessionProjectDir(): string | null +export const onSessionSwitch: (listener: (id: SessionId) => void) => () => void +export function getOriginalCwd(): string +export function setOriginalCwd(cwd: string): void +export function getProjectRoot(): string +export function setProjectRoot(cwd: string): void +export function getCwdState(): string +export function setCwdState(cwd: string): void +``` + +#### Cost / Duration Tracking + +```typescript +export function addToTotalDurationState(duration: number, durationWithoutRetries: number): void +export function addToTotalCostState(cost: number, modelUsage: ModelUsage, model: string): void +export function getTotalCostUSD(): number +export function getTotalAPIDuration(): number +export function getTotalDuration(): number +export function getTotalAPIDurationWithoutRetries(): number +export function getTotalToolDuration(): number +export function addToToolDuration(duration: number): void +export function getTurnHookDurationMs(): number +export function addToTurnHookDuration(duration: number): void +export function resetTurnHookDuration(): void +export function getTurnHookCount(): number +export function getTurnToolDurationMs(): number +export function resetTurnToolDuration(): void +export function getTurnToolCount(): number +export function getTurnClassifierDurationMs(): number +export function addToTurnClassifierDuration(duration: number): void +export function resetTurnClassifierDuration(): void +export function getTurnClassifierCount(): number +``` + +#### Token Accounting + +```typescript +export function getTotalInputTokens(): number // sumBy modelUsage.inputTokens +export function getTotalOutputTokens(): number +export function getTotalCacheReadInputTokens(): number +export function getTotalCacheCreationInputTokens(): number +export function getTotalWebSearchRequests(): number +export function getModelUsage(): { [modelName: string]: ModelUsage } +export function getUsageForModel(model: string): ModelUsage | undefined +``` + +#### Turn Token Budget + +```typescript +export function getTurnOutputTokens(): number // current turn output tokens +export function getCurrentTurnTokenBudget(): number | null +export function snapshotOutputTokensForTurn(budget: number | null): void +export function getBudgetContinuationCount(): number +export function incrementBudgetContinuationCount(): void +``` + +#### Post-Compaction Tracking + +```typescript +export function markPostCompaction(): void // sets pendingPostCompaction=true +export function consumePostCompaction(): boolean // returns true once after compaction, resets +``` + +#### Cost State Persistence + +```typescript +export function resetCostState(): void +export function setCostStateForRestore({ totalCostUSD, totalAPIDuration, ... }): void +export function resetStateForTests(): void +``` + +#### Scroll Drain + +```typescript +export function markScrollActivity(): void +export function getIsScrollDraining(): boolean +export async function waitForScrollIdle(): Promise +``` + +#### Model + +```typescript +export function getMainLoopModelOverride(): ModelSetting | undefined +export function getInitialMainLoopModel(): ModelSetting +export function setMainLoopModelOverride(model: ModelSetting | undefined): void +export function setInitialMainLoopModel(model: ModelSetting): void +export function getSdkBetas(): string[] | undefined +export function setSdkBetas(betas: string[] | undefined): void +``` + +#### Telemetry Setters + +```typescript +export function setMeter(meter: Meter, createAttributedCounter: ...): void +export function getSessionCounter(): AttributedCounter | null +export function setStatsStore(store: ...): void +export function getStatsStore(): ... +export function updateLastInteractionTime(immediate?: boolean): void +export function flushInteractionTime(): void +``` + +#### Misc Session State + +```typescript +export function setIsInteractive(v: boolean): void +export function getIsNonInteractiveSession(): boolean +export function setKairosActive(v: boolean): void +export function isSessionPersistenceDisabled(): boolean +export function setSessionPersistenceDisabled(v: boolean): void +export function setMainThreadAgentType(type: string | undefined): void +export function setIsRemoteMode(v: boolean): void +export function setClientType(type: string): void +export function setSessionSource(source: string | undefined): void +export function setInlinePlugins(dirs: string[]): void +export function getAdditionalDirectoriesForClaudeMd(): string[] +export function setAdditionalDirectoriesForClaudeMd(dirs: string[]): void +export function setAllowedChannels(channels: ChannelEntry[]): void +export function setAllowedSettingSources(sources: SettingSource[]): void +export function setSdkBetas(betas: string[] | undefined): void +export function setCachedClaudeMdContent(content: string | null): void +export function getLastMainRequestId(): string | undefined +export function setLastMainRequestId(requestId: string): void +export function setTeleportedSessionInfo(info: ...): void +``` + +### Important Design Notes + +- **Singleton**: `STATE` is a module-level constant, initialized once via `getInitialState()` +- **`projectRoot` vs `originalCwd`**: `projectRoot` is set at startup (including by `--worktree`) and **never** updated by `EnterWorktreeTool`. `originalCwd` is for file operations; `projectRoot` is for session identity (history, skills) +- **`sessionProjectDir`**: Always reset on `switchSession()` and `regenerateSessionId()`. `null` means "derive from `originalCwd`" +- **Scroll drain**: Module-level (not in `STATE`) — ephemeral hot-path flag with 150ms debounce +- **Bootstrap isolation**: This module must remain a leaf in the import DAG (cannot import from `src/utils/` directly — uses path aliases) + +### `AttributedCounter` Type + +```typescript +export type AttributedCounter = { + add(value: number, additionalAttributes?: Attributes): void +} +``` + +### `ChannelEntry` Type + +```typescript +export type ChannelEntry = + | { kind: 'plugin'; name: string; marketplace: string; dev?: boolean } + | { kind: 'server'; name: string; dev?: boolean } +``` + +--- + +## assistant/sessionHistory.ts — Remote Session History Pagination + +### Purpose + +Fetches conversation event history from the Claude API for remote (CCR/BYOC) sessions. Used by the assistant/teleport feature to replay conversation history when resuming a remote session. + +### Exports + +```typescript +export const HISTORY_PAGE_SIZE = 100 + +export type HistoryPage = { + events: SDKMessage[] // chronological within page + firstId: string | null // oldest event ID → before_id cursor for next-older page + hasMore: boolean // true = older events exist +} + +export type HistoryAuthCtx = { + baseUrl: string + headers: Record +} + +export async function createHistoryAuthCtx(sessionId: string): Promise +export async function fetchLatestEvents(ctx: HistoryAuthCtx, limit?: number): Promise +export async function fetchOlderEvents(ctx: HistoryAuthCtx, beforeId: string, limit?: number): Promise +``` + +### API Endpoints + +Base URL: `${getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}/events` + +| Function | Query Params | Description | +|---|---|---| +| `fetchLatestEvents` | `{ limit, anchor_to_latest: true }` | Newest `limit` events, chronological | +| `fetchOlderEvents` | `{ limit, before_id: beforeId }` | Events before cursor | + +### Authentication + +Requires OAuth access token + organization UUID. Headers: +- Standard OAuth headers (`getOAuthHeaders(accessToken)`) +- `anthropic-beta: ccr-byoc-2025-07-29` +- `x-organization-uuid: orgUUID` + +### Request Configuration + +- **Timeout**: 15,000ms per request +- **Status validation**: `validateStatus: () => true` (manual status check) +- **Error handling**: Returns `null` on network error or non-200 status; logs HTTP status to debug + +### Response Type + +```typescript +type SessionEventsResponse = { + data: SDKMessage[] + has_more: boolean + first_id: string | null + last_id: string | null +} +``` + +--- + +## Cross-Cutting Concerns + +### Startup Profiling + +Throughout these files, `profileCheckpoint(label)` calls mark timing milestones for startup performance analysis. Key checkpoints: + +| Checkpoint | Location | +|---|---| +| `main_tsx_entry` | First line of main.tsx (before imports) | +| `main_tsx_imports_loaded` | After all imports loaded | +| `cli_entry` | cli.tsx after profile import | +| `cli_before_main_import` | Before `import('../main.js')` | +| `cli_after_main_import` | After main.ts loaded | +| `cli_after_main_complete` | After `main()` returns | +| `init_function_start` / `init_function_end` | init.ts boundaries | +| `main_function_start` | Entry to `main()` | +| `query_fn_entry` | Each query loop iteration | +| `query_api_streaming_start` | Before first API streaming call | + +### Feature Flags System + +Feature flags (`feature('FLAG_NAME')`) from `bun:bundle` are **build-time** dead-code elimination gates, not runtime toggles. The bun bundler evaluates these at build time and removes unreachable code branches from external builds. + +Runtime gates use: +- `checkStatsigFeatureGate_CACHED_MAY_BE_STALE()` — Statsig experiment gates +- `isEnvTruthy(process.env.*)` — environment variable gates +- GrowthBook feature values — for more complex multi-variant experiments + +### Query Source Values + +The `querySource: QuerySource` parameter in `query()` and `handleStopHooks()` categorizes the origin of each query: + +| Value | Description | +|---|---| +| `'repl_main_thread'` | Interactive REPL, main user turn | +| `'sdk'` | SDK/headless path | +| `'agent:*'` | Subagent (AgentTool) | +| `'compact'` | Compaction fork | +| `'session_memory'` | Session memory fork | + +Values starting with `'agent:'` or `'repl_main_thread'` enable transcript persistence of content replacements. + +### Dependency Injection Pattern + +`QueryDeps` (in `query/deps.ts`) is the primary example of DI in this codebase. Rather than `jest.spyOn` across 6-8 files, tests can pass `deps` to `QueryParams` directly. The pattern is intentionally narrow (4 deps) with the note that it can be expanded. diff --git a/spec/02_commands.md b/spec/02_commands.md new file mode 100644 index 0000000..8818c46 --- /dev/null +++ b/spec/02_commands.md @@ -0,0 +1,2065 @@ +# Claude Code — Commands Reference + +This document is an exhaustive reference for every slash command in the Claude Code CLI, derived directly from the source in `src/commands/` and the top-level registry in `src/commands.ts`. + +--- + +## Table of Contents + +1. [Command System Architecture](#1-command-system-architecture) +2. [Command Type Definitions](#2-command-type-definitions) +3. [Command Registry (`commands.ts`)](#3-command-registry-commandsts) +4. [Individual Command Reference](#4-individual-command-reference) + - [add-dir](#add-dir) + - [advisor](#advisor) + - [agents](#agents) + - [ant-trace](#ant-trace-stub) + - [autofix-pr](#autofix-pr-stub) + - [backfill-sessions](#backfill-sessions-stub) + - [branch / fork](#branch--fork) + - [break-cache](#break-cache-stub) + - [bridge-kick](#bridge-kick) + - [bridge / remote-control / rc](#bridge--remote-control--rc) + - [brief](#brief) + - [btw](#btw) + - [bughunter](#bughunter-stub) + - [chrome](#chrome) + - [clear / reset / new](#clear--reset--new) + - [color](#color) + - [commit](#commit) + - [commit-push-pr](#commit-push-pr) + - [compact](#compact) + - [config / settings](#config--settings) + - [context](#context) + - [copy](#copy) + - [cost](#cost) + - [createMovedToPluginCommand (utility)](#createmovedtoplugincommand-utility) + - [ctx_viz](#ctx_viz-stub) + - [debug-tool-call](#debug-tool-call-stub) + - [desktop / app](#desktop--app) + - [diff](#diff) + - [doctor](#doctor) + - [effort](#effort) + - [env](#env-stub) + - [exit / quit](#exit--quit) + - [export](#export) + - [extra-usage](#extra-usage) + - [fast](#fast) + - [feedback / bug](#feedback--bug) + - [files](#files) + - [good-claude](#good-claude-stub) + - [heapdump](#heapdump) + - [help](#help) + - [hooks](#hooks) + - [ide](#ide) + - [init](#init) + - [init-verifiers](#init-verifiers) + - [insights](#insights) + - [install-github-app](#install-github-app) + - [install-slack-app](#install-slack-app) + - [install (component)](#install-component) + - [issue](#issue-stub) + - [keybindings](#keybindings) + - [login](#login) + - [logout](#logout) + - [mcp](#mcp) + - [memory](#memory) + - [mobile / ios / android](#mobile--ios--android) + - [mock-limits](#mock-limits-stub) + - [model](#model) + - [oauth-refresh](#oauth-refresh-stub) + - [onboarding](#onboarding-stub) + - [output-style](#output-style) + - [passes](#passes) + - [perf-issue](#perf-issue-stub) + - [permissions / allowed-tools](#permissions--allowed-tools) + - [plan](#plan) + - [plugin / plugins / marketplace](#plugin--plugins--marketplace) + - [pr-comments](#pr-comments) + - [privacy-settings](#privacy-settings) + - [rate-limit-options](#rate-limit-options) + - [release-notes](#release-notes) + - [reload-plugins](#reload-plugins) + - [remote-env](#remote-env) + - [remote-setup / web-setup](#remote-setup--web-setup) + - [rename](#rename) + - [reset-limits](#reset-limits-stub) + - [resume / continue](#resume--continue) + - [review](#review) + - [ultrareview](#ultrareview) + - [rewind / checkpoint](#rewind--checkpoint) + - [sandbox](#sandbox) + - [security-review](#security-review) + - [session / remote](#session--remote) + - [share](#share-stub) + - [skills](#skills) + - [stats](#stats) + - [status](#status) + - [statusline](#statusline) + - [stickers](#stickers) + - [summary](#summary-stub) + - [tag](#tag) + - [tasks / bashes](#tasks--bashes) + - [teleport](#teleport-stub) + - [terminal-setup](#terminal-setup) + - [theme](#theme) + - [think-back](#think-back) + - [thinkback-play](#thinkback-play) + - [ultraplan](#ultraplan) + - [upgrade](#upgrade) + - [usage](#usage) + - [version](#version) + - [vim](#vim) + - [voice](#voice) +5. [Internal-Only Commands Summary](#5-internal-only-commands-summary) +6. [Remote-Safe Commands](#6-remote-safe-commands) +7. [Bridge-Safe Commands](#7-bridge-safe-commands) +8. [Command Availability & Feature Flags](#8-command-availability--feature-flags) + +--- + +## 1. Command System Architecture + +All slash commands in Claude Code share a unified `Command` interface defined in `src/types/command.ts` and re-exported from `src/commands.ts`. The command registry is loaded lazily (memoized) in `commands.ts` and is assembled from several sources: + +``` +Priority order in getCommands(): + bundledSkills → builtinPluginSkills → skillDirCommands → + workflowCommands → pluginCommands → pluginSkills → COMMANDS() +``` + +The three command types are: + +| Type | Description | +|------|-------------| +| `'local'` | Runs synchronously; returns `LocalCommandResult` (`{ type: 'text' \| 'compact' \| 'skip', value?: string }`) | +| `'local-jsx'` | Renders a React/Ink component in the TUI; returns `React.ReactNode` | +| `'prompt'` | Expands to a text prompt that is sent to the model via the main loop | + +### Lazy Loading + +Every command's implementation is loaded via dynamic `import()` from its `load()` method. The index file contains only metadata (name, description, type) so that startup cost is minimized. The heavy module is only loaded when the command is first invoked. + +--- + +## 2. Command Type Definitions + +```typescript +// Base for all command types +interface CommandBase { + name: string + description: string + aliases?: string[] + argumentHint?: string + isEnabled?: () => boolean + isHidden?: boolean + availability?: Array<'claude-ai' | 'console'> + source?: 'builtin' | 'plugin' | 'mcp' | 'bundled' | string + load: () => Promise<{ call: Function }> +} + +// 'local' command — synchronous, returns text +interface LocalCommand extends CommandBase { + type: 'local' + supportsNonInteractive?: boolean +} + +// 'local-jsx' command — renders Ink UI +interface LocalJSXCommand extends CommandBase { + type: 'local-jsx' + immediate?: boolean // skip the "processing" spinner +} + +// 'prompt' command — expanded and sent to the model +interface PromptCommand extends CommandBase { + type: 'prompt' + contentLength: number + progressMessage: string + allowedTools?: string[] + disableModelInvocation?: boolean + disableNonInteractive?: boolean + getPromptForCommand(args: string, context: ToolUseContext): Promise +} +``` + +--- + +## 3. Command Registry (`commands.ts`) + +**File:** `src/commands.ts` + +### Exports + +| Export | Description | +|--------|-------------| +| `getCommands(cwd)` | Async; returns all commands filtered by `meetsAvailabilityRequirement` and `isCommandEnabled`. Memoized per-cwd for expensive loading; availability/isEnabled run fresh every call. | +| `INTERNAL_ONLY_COMMANDS` | Array of commands only available when `USER_TYPE === 'ant'`. Includes backfill-sessions, break-cache, bughunter, commit, commit-push-pr, ctx_viz, good-claude, issue, init-verifiers, mock-limits, bridge-kick, version, ultraplan, subscribePr, resetLimits, onboarding, share, summary, teleport, ant-trace, perf-issue, env, oauth-refresh, debug-tool-call, agents-platform, autofix-pr. | +| `REMOTE_SAFE_COMMANDS` | `Set` of commands safe in remote (--remote) mode: session, exit, clear, help, theme, color, vim, cost, usage, copy, btw, feedback, plan, keybindings, statusline, stickers, mobile. | +| `BRIDGE_SAFE_COMMANDS` | `Set` of `'local'` commands safe over the Remote Control bridge: compact, clear, cost, summary, release-notes, files. | +| `builtInCommandNames` | Memoized `Set` of all built-in command names and aliases. | +| `meetsAvailabilityRequirement(cmd)` | Checks `cmd.availability` against current auth state. | +| `filterCommandsForRemoteMode(commands)` | Filters to only REMOTE_SAFE_COMMANDS. | +| `isBridgeSafeCommand(cmd)` | Returns `true` for `'prompt'` type or BRIDGE_SAFE_COMMANDS members; `false` for `'local-jsx'`. | +| `findCommand(name, commands)` | Lookup by name, computed name, or alias. | +| `hasCommand(name, commands)` | Boolean version of findCommand. | +| `getCommand(name, commands)` | Like findCommand but throws `ReferenceError` if not found. | +| `formatDescriptionWithSource(cmd)` | User-facing description with source annotation (plugin name, bundled, skill dir). | +| `getSkillToolCommands(cwd)` | Memoized; filters to prompt-type commands the model can invoke as tools. | +| `getSlashCommandToolSkills(cwd)` | Memoized; filters to true skills (skills/plugin/bundled). | +| `getMcpSkillCommands(mcpCommands)` | Filters MCP commands to model-invocable prompt skills. | +| `clearCommandMemoizationCaches()` | Clears loadAllCommands and skill index caches. | +| `clearCommandsCache()` | Full reset including plugin and skill caches. | + +### Feature-Gated Commands + +The following commands are conditionally registered using `feature()` bundle flags: + +| Feature Flag | Command | +|---|---| +| `BRIDGE_MODE` | `/remote-control` (rc) | +| `BRIDGE_MODE` + `DAEMON` | `remoteControlServer` | +| `KAIROS` or `KAIROS_BRIEF` | `/brief` | +| `KAIROS` | `assistant` | +| `VOICE_MODE` | `/voice` | +| `HISTORY_SNIP` | `force-snip` | +| `WORKFLOW_SCRIPTS` | `workflows` | +| `CCR_REMOTE_SETUP` | `/web-setup` | +| `ULTRAPLAN` | `/ultraplan` | +| `KAIROS_GITHUB_WEBHOOKS` | `subscribe-pr` | +| `TORCH` | `torch` | +| `UDS_INBOX` | `peers` | +| `FORK_SUBAGENT` | `fork` | +| `BUDDY` | `buddy` | +| `PROACTIVE` or `KAIROS` | `proactive` | +| `EXPERIMENTAL_SKILL_SEARCH` | clearSkillIndexCache hook | + +--- + +## 4. Individual Command Reference + +--- + +### `/add-dir` + +**Files:** `commands/add-dir/index.ts`, `commands/add-dir/add-dir.tsx`, `commands/add-dir/validation.ts` + +**Type:** `local-jsx` +**Syntax:** `/add-dir []` +**Description:** Add a new working directory to the current session's permission context. + +**Behavior:** +- If a path argument is provided, validates it and adds it immediately (with an optional "remember" step prompting to save to local settings). +- If no path is provided, opens the `AddWorkspaceDirectory` interactive picker component. +- On success, calls `applyPermissionUpdate` (session) and optionally `persistPermissionUpdate` (local settings). +- Also calls `SandboxManager.refreshConfig()` to update bash sandboxing configuration. +- Updates bootstrap state for additional CLAUDE.md directories. + +**Validation (validation.ts):** + +```typescript +export type AddDirectoryResult = + | { resultType: 'success'; absolutePath: string } + | { resultType: 'emptyPath' } + | { resultType: 'pathNotFound' | 'notADirectory'; directoryPath: string; absolutePath: string } + | { resultType: 'alreadyInWorkingDirectory'; directoryPath: string; workingDir: string } + +export async function validateDirectoryForWorkspace( + directoryPath: string, + permissionContext: ToolPermissionContext, +): Promise + +export function addDirHelpMessage(result: AddDirectoryResult): string +``` + +**Key logic:** +- Expands `~` and resolves symlinks before validation. +- Handles `ENOENT`, `ENOTDIR`, `EACCES`, `EPERM` gracefully (returns `pathNotFound` rather than throwing). +- Checks `pathInWorkingPath` to avoid adding redundant subdirectories. + +**Dependencies:** `chalk`, `bootstrap/state.js`, `components/permissions/rules/AddWorkspaceDirectory`, `utils/permissions/PermissionUpdate.js`, `utils/sandbox/sandbox-adapter.js` + +--- + +### `/advisor` + +**File:** `commands/advisor.ts` + +**Type:** `local` +**Syntax:** `/advisor [|off|unset]` +**Description:** Configure the advisor model (a secondary model that reviews/advises on the primary model's outputs). + +**Arguments:** + +| Argument | Description | +|----------|-------------| +| *(none)* | Display current advisor setting and status | +| `` | Set the advisor model (e.g. `opus`, `claude-opus-4-5`) | +| `off` / `unset` | Disable the advisor | + +**Behavior:** +- Checks `canUserConfigureAdvisor()` — command is hidden/disabled if user cannot configure an advisor. +- Validates model string via `validateModel()`. +- Checks `isValidAdvisorModel()` and `modelSupportsAdvisor()`. +- Persists to `userSettings.advisorModel` via `updateSettingsForSource`. +- If base model doesn't support advisors, returns a warning (still sets the value). + +**Gate:** `isEnabled: () => canUserConfigureAdvisor()` + +**Dependencies:** `utils/advisor.js`, `utils/model/model.js`, `utils/model/validateModel.js`, `utils/settings/settings.js` + +--- + +### `/agents` + +**Files:** `commands/agents/index.ts`, `commands/agents/agents.tsx` + +**Type:** `local-jsx` +**Syntax:** `/agents` +**Description:** Open the Agents management menu (create, edit, delete custom agent configurations). + +**Behavior:** Renders the `AgentsMenu` component passing current tools from `getTools(permissionContext)`. + +**Dependencies:** `components/agents/AgentsMenu`, `tools.js` + +--- + +### `/ant-trace` (stub) + +**File:** `commands/ant-trace/index.js` + +**Type:** Stub — `isEnabled: () => false, isHidden: true` +**Description:** Internal tracing command. Disabled in all external builds. + +--- + +### `/autofix-pr` (stub) + +**File:** `commands/autofix-pr/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/backfill-sessions` (stub) + +**File:** `commands/backfill-sessions/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/branch` / `/fork` + +**Files:** `commands/branch/index.ts`, `commands/branch/branch.ts` + +**Type:** `local-jsx` +**Syntax:** `/branch [name]` +**Description:** Create a forked copy (branch) of the current conversation at this point. The user immediately enters the fork; the original can be resumed with `claude -r `. + +**Arguments:** + +| Argument | Description | +|----------|-------------| +| `[name]` | Optional custom title for the branch (defaults to first-prompt-derived kebab title + " (Branch)") | + +**Aliases:** When the `FORK_SUBAGENT` feature flag is off, also registered as `/fork`. + +**Behavior:** +1. Reads the current transcript JSONL file. +2. Generates a new UUID for the fork session. +3. Copies all non-sidechain messages, rewriting `sessionId` and adding `forkedFrom` traceability metadata to each entry. +4. Copies `content-replacement` entries (rewriting `sessionId`) to preserve prompt-cache budgeting. +5. Saves the fork to a new JSONL file. +6. Calls `getUniqueForkName()` to avoid collisions (appends " (Branch 2)", " (Branch 3)", etc.). +7. Calls `context.resume(sessionId, forkLog, 'fork')` to switch the live session into the fork. + +**Exports:** +```typescript +export function deriveFirstPrompt(firstUserMessage): string +export async function call(onDone, context, args): Promise +``` + +**Dependencies:** `crypto`, `fs/promises`, `bootstrap/state.js`, `utils/sessionStorage.js`, `services/analytics/index.js` + +--- + +### `/break-cache` (stub) + +**File:** `commands/break-cache/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/bridge-kick` + +**File:** `commands/bridge-kick.ts` + +**Type:** `local` +**Syntax:** `/bridge-kick [args...]` +**Description:** Internal command to inject bridge (Remote Control) failure states for manual recovery path testing. Only enabled when `USER_TYPE === 'ant'`. + +**Subcommands:** + +| Subcommand | Arguments | Description | +|---|---|---| +| `close` | `` | Fire WebSocket close event with given code (e.g. 1002, 1006) | +| `poll` | `` or `transient` | Next poll throws BridgeFatalError(status) or a transient rejection | +| `poll` | ` ` | Poll error with explicit error_type | +| `register` | `fail [N]` | Next N registerBridgeEnvironment calls transient-fail (default 1) | +| `register` | `fatal` | Next register 403s (terminal failure) | +| `reconnect-session` | `fail` | Next 2 POST /bridge/reconnect calls return 404 | +| `heartbeat` | `` | Next heartbeat throws BridgeFatalError(status) | +| `reconnect` | *(none)* | Call reconnectEnvironmentWithSession() directly | +| `status` | *(none)* | Print current bridge state | + +**Gate:** `isEnabled: () => process.env.USER_TYPE === 'ant'` + +**Dependencies:** `bridge/bridgeDebug.js` + +--- + +### `/bridge` / `/remote-control` / `/rc` + +**Files:** `commands/bridge/index.ts`, `commands/bridge/bridge.tsx` + +**Type:** `local-jsx` +**Name:** `remote-control` +**Aliases:** `rc` +**Syntax:** `/remote-control [name]` +**Description:** Start or manage a Remote Control bridge session, allowing the terminal to be controlled from a mobile device or web interface. + +**Feature gate:** `BRIDGE_MODE` feature flag must be on AND `isBridgeEnabled()` must return true. + +The implementation (`bridge.tsx`) is a large file (~34KB) rendering a multi-step wizard for QR code display, session naming, connection state, and error recovery. + +**Arguments:** + +| Argument | Description | +|----------|-------------| +| `[name]` | Optional session name to display in the remote client | + +**Properties:** +- `immediate: true` — renders immediately without spinner +- `isHidden`: mirrors `isEnabled()` + +--- + +### `/brief` + +**File:** `commands/brief.ts` + +**Type:** `local-jsx` +**Syntax:** `/brief` +**Description:** Toggle "brief-only mode." When enabled, the model must use the `SendUserMessage` (Brief) tool for all user-facing output; plain text is hidden. Feature-gated via `KAIROS` or `KAIROS_BRIEF` bundle flags + GrowthBook config `tengu_kairos_brief_config.enable_slash_command`. + +**Behavior:** +1. Checks entitlement via `isBriefEntitled()` — if trying to enable and not entitled, logs analytics and returns an error. +2. Toggles `setUserMsgOptIn` and `context.setAppState({ isBriefOnly })`. +3. Injects a `` meta-message into the next turn (unless Kairos is active) so the model immediately transitions its output style. +4. Logs `tengu_brief_mode_toggled` event. + +**Properties:** `immediate: true` + +--- + +### `/btw` + +**Files:** `commands/btw/index.ts`, `commands/btw/btw.tsx` + +**Type:** `local-jsx` +**Syntax:** `/btw ` +**Description:** Ask a quick "by the way" side question without interrupting the main conversation. Opens a scrollable overlay with a spinner, sends the question as a forked side-query to the model, and returns the response in a scrollable panel. Pressing Enter/Escape/Space dismisses the result. + +**Properties:** `immediate: true` — renders without spinner. + +**Behavior:** +- Uses `runSideQuestion()` to send the question with an abbreviated context (messages after the compact boundary, with cache-safe params from the last turn). +- Renders markdown response in a `ScrollBox`. +- Keyboard: `Up`/`Down` or `Ctrl+P`/`Ctrl+N` to scroll; `Escape`/`Enter`/`Space`/`Ctrl+C`/`Ctrl+D` to dismiss. + +**Dependencies:** `utils/sideQuestion.js`, `components/Markdown`, `ink/components/ScrollBox` + +--- + +### `/bughunter` (stub) + +**File:** `commands/bughunter/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/chrome` + +**Files:** `commands/chrome/index.ts`, `commands/chrome/chrome.tsx` + +**Type:** `local-jsx` +**Syntax:** `/chrome` +**Description:** Claude in Chrome (Beta) settings. Available only to `claude-ai` subscribers and non-interactive session check. + +**Gate:** `availability: ['claude-ai']`, `isEnabled: () => !getIsNonInteractiveSession()` + +The implementation (`chrome.tsx`, ~23KB) renders a settings panel for managing the Claude Chrome extension integration. + +--- + +### `/clear` / `/reset` / `/new` + +**Files:** `commands/clear/index.ts`, `commands/clear/clear.ts`, `commands/clear/conversation.ts`, `commands/clear/caches.ts` + +**Type:** `local` +**Syntax:** `/clear` +**Aliases:** `reset`, `new` +**Description:** Clear conversation history and free up context. Starts a fresh session. + +**Core function (`clearConversation`):** +1. Executes `SessionEnd` hooks (bounded by `CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS`, default 1.5s). +2. Emits `tengu_cache_eviction_hint` analytics event. +3. Identifies background tasks to preserve (tasks with `isBackgrounded !== false`). +4. Clears messages: `setMessages(() => [])`. +5. Calls `clearSessionCaches(preservedAgentIds)`. +6. Resets cwd to original cwd, clears `readFileState`, `discoveredSkillNames`, `loadedNestedMemoryPaths`. +7. Kills/aborts foreground tasks (local shell tasks get `kill()`+`cleanup()`; agent tasks get `abortController.abort()`). +8. Clears attribution state, file history, and MCP client state in AppState. +9. Clears plan slugs and session metadata. +10. Regenerates session ID (sets old as parent for analytics). +11. Re-points task output symlinks for surviving background tasks. +12. Persists mode (coordinator/normal) and worktree state. +13. Executes `SessionStart` hooks after clearing. + +**`clearSessionCaches()` (caches.ts):** Clears user/system/git context caches, file suggestions, commands/skills cache, prompt cache break detection, system prompt injection, emitted date, post-compact cleanup, skill names, memory files cache, stored images, session ingress, pending permission callbacks, tungsten session tracking, attribution caches, repository caches, bash command prefix caches, dump prompts state, invoked skills, git dir resolution, dynamic skills, LSP diagnostics, magic docs tracking, session env vars, WebFetch URL cache, ToolSearch description cache, agent definitions cache, SkillTool prompt cache. + +**Gate:** `supportsNonInteractive: false` + +--- + +### `/color` + +**Files:** `commands/color/index.ts`, `commands/color/color.ts` + +**Type:** `local-jsx` +**Syntax:** `/color ` +**Description:** Set the prompt bar / session color. Colors are drawn from `AGENT_COLORS` (the same palette used by swarm agents). + +**Arguments:** + +| Argument | Description | +|----------|-------------| +| `` | A named color from `AGENT_COLORS` | +| `default` / `reset` / `none` / `gray` / `grey` | Reset to default (no color) | + +**Behavior:** +- Saves color to transcript via `saveAgentColor()`. +- Updates `AppState.standaloneAgentContext.color` for immediate display. +- Blocked for swarm teammates (their colors are assigned by the team leader). + +**Properties:** `immediate: true` + +--- + +### `/commit` + +**File:** `commands/commit.ts` + +**Type:** `prompt` +**Syntax:** `/commit` +**Description:** Create a git commit using the current staged and unstaged changes. + +**Allowed tools:** +``` +Bash(git add:*) +Bash(git status:*) +Bash(git commit:*) +``` + +**Prompt behavior:** +- Injects live shell output via `executeShellCommandsInPrompt` for: `git status`, `git diff HEAD`, `git branch --show-current`, `git log --oneline -10`. +- Provides a Git Safety Protocol in the prompt (never amend, never skip hooks, etc.). +- If `USER_TYPE === 'ant'` and `isUndercover()`, prepends undercover instructions. +- Includes optional commit attribution text from `getAttributionTexts()`. +- Instructs model to create a commit using HEREDOC syntax. + +**Gate:** Internal-only (`INTERNAL_ONLY_COMMANDS`). + +--- + +### `/commit-push-pr` + +**File:** `commands/commit-push-pr.ts` + +**Type:** `prompt` +**Syntax:** `/commit-push-pr [additional instructions]` +**Description:** Commit, push, and open a pull request in a single operation. + +**Allowed tools:** +``` +Bash(git checkout --branch:*) +Bash(git checkout -b:*) +Bash(git add:*) +Bash(git status:*) +Bash(git push:*) +Bash(git commit:*) +Bash(gh pr create:*) +Bash(gh pr edit:*) +Bash(gh pr view:*) +Bash(gh pr merge:*) +ToolSearch +mcp__slack__send_message +mcp__claude_ai_Slack__slack_send_message +``` + +**Prompt behavior:** +- Injects live shell output for: `git status`, `git diff HEAD`, `git branch --show-current`, `git diff ${defaultBranch}...HEAD`, `gh pr view --json number`. +- Orchestrates: create branch if on main, commit, push, create/update PR. +- Optionally posts to Slack if CLAUDE.md mentions Slack channels (uses ToolSearch). +- PR body template includes Summary, Test plan, Changelog sections. +- Includes `getEnhancedPRAttribution()` text. + +**Gate:** Internal-only (`INTERNAL_ONLY_COMMANDS`). + +--- + +### `/compact` + +**Files:** `commands/compact/index.ts`, `commands/compact/compact.ts` + +**Type:** `local` +**Syntax:** `/compact [custom summarization instructions]` +**Description:** Summarize the conversation, replacing message history with a compact summary. Preserves context while reducing token usage. + +**Arguments:** + +| Argument | Description | +|----------|-------------| +| `[instructions]` | Optional custom instructions to guide the compaction summary | + +**Behavior:** +1. Filters messages to those after the compact boundary (strips UI-only scrollback). +2. If no custom instructions, first tries `trySessionMemoryCompaction()` (session-memory-based, cheaper). +3. If `REACTIVE_COMPACT` flag is on, routes through the reactive compaction path. +4. Otherwise falls back to traditional compaction: + - Runs `microcompactMessages()` first to reduce tokens. + - Calls `compactConversation()` with cache-sharing parameters. +5. After success: clears user context cache, runs `runPostCompactCleanup()`, suppresses the compact warning. +6. Returns `{ type: 'compact', compactionResult, displayText }`. + +**Gate:** `isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT)`, `supportsNonInteractive: true` + +--- + +### `/config` / `/settings` + +**Files:** `commands/config/index.ts`, `commands/config/config.tsx` + +**Type:** `local-jsx` +**Syntax:** `/config` +**Aliases:** `settings` +**Description:** Open the interactive configuration panel (`Settings` component, defaulting to the "Config" tab). + +--- + +### `/context` + +**Files:** `commands/context/index.ts`, `commands/context/context-noninteractive.ts`, `commands/context/context.tsx` + +**Type:** `local-jsx` (interactive) or `local` (non-interactive) +**Syntax:** `/context` +**Description:** Visualize or display current context usage. + +**Two variants:** +- **Interactive** (`context`): `local-jsx`, renders a colored visualization grid. Enabled when `!getIsNonInteractiveSession()`. +- **Non-interactive** (`contextNonInteractive`): `local`, outputs a markdown table with token breakdown. Enabled when `getIsNonInteractiveSession()`. + +**Output (non-interactive markdown table includes):** +- Total tokens / max tokens / percentage +- Context strategy (collapse stats if `CONTEXT_COLLAPSE` is on) +- Estimated usage by category +- MCP Tools breakdown +- ANT-ONLY: System Tools, System Prompt Sections +- Custom Agents (by source: Project/User/Local/Flag/Policy/Plugin/Built-in) +- Memory Files (type, path, tokens) +- Skills (name, source, tokens) +- ANT-ONLY: Message Breakdown (tool calls, tool results, attachments, assistant messages, user messages; top tools by call+result tokens; top attachments) + +**Core function:** +```typescript +export async function collectContextData(context): Promise +export async function call(_args, context): Promise<{ type: 'text'; value: string }> +``` + +--- + +### `/copy` + +**Files:** `commands/copy/index.ts`, `commands/copy/copy.tsx` + +**Type:** `local-jsx` +**Syntax:** `/copy [N]` +**Description:** Copy Claude's last response to the clipboard. `/copy N` copies the Nth-latest response. + +The implementation (`copy.tsx`, ~31KB) handles clipboard access, finds the most recent assistant message(s), and copies their text content. + +--- + +### `/cost` + +**Files:** `commands/cost/index.ts`, `commands/cost/cost.ts` + +**Type:** `local` +**Syntax:** `/cost` +**Description:** Show the total cost and duration of the current session. + +**Behavior:** +- For `claude-ai` subscribers: shows subscription usage message (or overage notice). +- For API users: calls `formatTotalCost()` to show token cost. +- ANT users always see cost breakdown even if subscriber. + +**Gate:** Hidden for claude.ai subscribers (except ANT users). +**`supportsNonInteractive: true`** + +--- + +### `createMovedToPluginCommand` (utility) + +**File:** `commands/createMovedToPluginCommand.ts` + +Not a slash command itself — a factory function used to create commands that have been migrated to the plugin marketplace. + +```typescript +export function createMovedToPluginCommand(options: { + name: string + description: string + progressMessage: string + pluginName: string + pluginCommand: string + getPromptWhileMarketplaceIsPrivate(args, context): Promise +}): Command +``` + +**Behavior:** For `USER_TYPE === 'ant'`, returns a prompt instructing the model to tell the user to install the plugin. For external users, runs the fallback `getPromptWhileMarketplaceIsPrivate` function. Used by `pr-comments`, `security-review`. + +--- + +### `/ctx_viz` (stub) + +**File:** `commands/ctx_viz/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/debug-tool-call` (stub) + +**File:** `commands/debug-tool-call/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/desktop` / `/app` + +**Files:** `commands/desktop/index.ts`, `commands/desktop/desktop.tsx` + +**Type:** `local-jsx` +**Syntax:** `/desktop` +**Aliases:** `app` +**Description:** Continue the current session in Claude Desktop via handoff. + +**Gate:** +- `availability: ['claude-ai']` +- `isEnabled`: only macOS or Windows x64 + +**Behavior:** Renders `DesktopHandoff` component which generates a handoff token/URL to open in the Claude Desktop app. + +--- + +### `/diff` + +**Files:** `commands/diff/index.ts`, `commands/diff/diff.tsx` + +**Type:** `local-jsx` +**Syntax:** `/diff` +**Description:** View uncommitted git changes and per-turn diffs. Renders an interactive diff viewer. + +--- + +### `/doctor` + +**Files:** `commands/doctor/index.ts`, `commands/doctor/doctor.tsx` + +**Type:** `local-jsx` +**Syntax:** `/doctor` +**Description:** Run diagnostics on the Claude Code installation: API key validity, model access, MCP connectivity, LSP status, plugin health, etc. + +**Gate:** `isEnabled: () => !isEnvTruthy(process.env.DISABLE_DOCTOR_COMMAND)` + +--- + +### `/effort` + +**Files:** `commands/effort/index.ts`, `commands/effort/effort.tsx` + +**Type:** `local-jsx` +**Syntax:** `/effort [low|medium|high|max|auto|help]` +**Description:** Set the effort level that controls how much "thinking" tokens the model uses. + +**Arguments:** + +| Value | Description | +|---|---| +| `low` | Minimal thinking tokens | +| `medium` | Moderate thinking tokens | +| `high` | Extended thinking | +| `max` | Maximum thinking tokens | +| `auto` | Model decides | +| `help` / `-h` / `--help` | Show help | +| *(none)* | Show current level | + +**Behavior:** +- Calls `toPersistableEffort()` — some levels are session-only. +- Persists persistable values to `userSettings.effortLevel`. +- Logs `tengu_effort_command` analytics event. +- If `CLAUDE_CODE_EFFORT_LEVEL` env var is set and conflicts with the requested value, warns the user. + +**`immediate`:** Dynamically set by `shouldInferenceConfigCommandBeImmediate()`. + +--- + +### `/env` (stub) + +**File:** `commands/env/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/exit` / `/quit` + +**Files:** `commands/exit/index.ts`, `commands/exit/exit.tsx` + +**Type:** `local-jsx` +**Syntax:** `/exit` +**Aliases:** `quit` +**Description:** Exit the REPL gracefully. + +**Behavior:** +- If `BG_SESSIONS` flag is on and this is a `--bg` tmux session: detaches the tmux client instead of killing it. +- If in a worktree session: shows `ExitFlow` dialog (asks about worktree cleanup). +- Otherwise: calls `gracefulShutdown(0, 'prompt_input_exit')` with a random goodbye message from `['Goodbye!', 'See ya!', 'Bye!', 'Catch you later!']`. + +**Properties:** `immediate: true` + +--- + +### `/export` + +**Files:** `commands/export/index.ts`, `commands/export/export.tsx` + +**Type:** `local-jsx` +**Syntax:** `/export [filename]` +**Description:** Export the current conversation to a file or clipboard. + +The implementation (~31KB) handles multiple formats, clipboard copy, and file output. + +--- + +### `/extra-usage` + +**Files:** `commands/extra-usage/index.ts`, `commands/extra-usage/extra-usage-core.ts`, `commands/extra-usage/extra-usage-noninteractive.ts`, `commands/extra-usage/extra-usage.tsx` + +**Two variants:** +- `extraUsage`: `local-jsx`, for interactive sessions +- `extraUsageNonInteractive`: `local`, for `--print` / headless mode + +**Syntax:** `/extra-usage` +**Description:** Configure extra usage (overage) to keep working when subscription limits are hit. + +**Gate:** `isEnabled: () => isOverageProvisioningAllowed() && !isEnvTruthy(DISABLE_EXTRA_USAGE_COMMAND)` + +**Core logic (`extra-usage-core.ts`):** +1. Marks `hasVisitedExtraUsage` in global config. +2. Invalidates overage credit grant cache. +3. For Team/Enterprise without billing access: + - If unlimited overage already enabled → "already have unlimited." + - If admin request pending/dismissed → "already submitted." + - Creates an `admin_request` of type `limit_increase`. +4. For users with billing access or individual plans: opens browser to `claude.ai/settings/usage` or `claude.ai/admin-settings/usage`. + +--- + +### `/fast` + +**Files:** `commands/fast/index.ts`, `commands/fast/fast.tsx` + +**Type:** `local-jsx` +**Syntax:** `/fast [on|off]` +**Description:** Toggle "fast mode" — switches to a faster, cheaper model (displayed as `FAST_MODE_MODEL_DISPLAY`). + +**Gate:** +- `availability: ['claude-ai', 'console']` +- `isEnabled: () => isFastModeEnabled()` +- `isHidden: !isFastModeEnabled()` + +**Behavior:** +- Clears fast mode cooldown. +- Writes `fastMode: true/undefined` to `userSettings`. +- If enabling and current model doesn't support fast mode, also switches `mainLoopModel`. +- Prefetches fast mode status on render. +- Shows a picker dialog (`FastModePicker`) with pricing info and cooldown status. + +**`immediate`:** Set by `shouldInferenceConfigCommandBeImmediate()`. + +--- + +### `/feedback` / `/bug` + +**Files:** `commands/feedback/index.ts`, `commands/feedback/feedback.tsx` + +**Type:** `local-jsx` +**Syntax:** `/feedback [report]` +**Aliases:** `bug` +**Description:** Submit feedback or bug reports about Claude Code. + +**Gate:** Disabled when: +- `CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY` are set +- `DISABLE_FEEDBACK_COMMAND` or `DISABLE_BUG_COMMAND` set +- `isEssentialTrafficOnly()` is true +- `USER_TYPE === 'ant'` +- `!isPolicyAllowed('allow_product_feedback')` + +--- + +### `/files` + +**Files:** `commands/files/index.ts`, `commands/files/files.ts` + +**Type:** `local` +**Syntax:** `/files` +**Description:** List all files currently in the context (read state cache). + +**Gate:** `isEnabled: () => process.env.USER_TYPE === 'ant'` + +**Output:** Lists relative paths of all files in `readFileState` cache, or "No files in context." + +**`supportsNonInteractive: true`** + +--- + +### `/good-claude` (stub) + +**File:** `commands/good-claude/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/heapdump` + +**Files:** `commands/heapdump/index.ts`, `commands/heapdump/heapdump.ts` + +**Type:** `local` +**Syntax:** `/heapdump` +**Description:** Dump the JavaScript heap to `~/Desktop`. + +**`isHidden: true`** — not shown in help but accessible. +**`supportsNonInteractive: true`** + +**Output:** Prints `heapPath` and `diagPath` on success, or an error message. + +--- + +### `/help` + +**Files:** `commands/help/index.ts`, `commands/help/help.tsx` + +**Type:** `local-jsx` +**Syntax:** `/help` +**Description:** Show help screen with available commands and keyboard shortcuts. + +--- + +### `/hooks` + +**Files:** `commands/hooks/index.ts`, `commands/hooks/hooks.tsx` + +**Type:** `local-jsx` +**Syntax:** `/hooks` +**Description:** View and manage hook configurations for tool events (PreToolUse, PostToolUse, Stop, etc.). + +**Properties:** `immediate: true` + +--- + +### `/ide` + +**Files:** `commands/ide/index.ts`, `commands/ide/ide.tsx` + +**Type:** `local-jsx` +**Syntax:** `/ide [open]` +**Description:** Manage IDE integrations (VS Code, JetBrains, etc.) and show connection status. + +--- + +### `/init` + +**File:** `commands/init.ts` + +**Type:** `prompt` +**Syntax:** `/init` +**Description:** Initialize CLAUDE.md (and optionally skills/hooks) for the current repository. + +**Two prompt variants (feature-gated):** + +**Old prompt (`OLD_INIT_PROMPT`):** Analyzes the codebase and creates a minimal CLAUDE.md with commands, architecture overview. + +**New prompt (`NEW_INIT_PROMPT`, enabled by `NEW_INIT` flag or `CLAUDE_CODE_NEW_INIT=1`):** Multi-phase interactive setup: +- Phase 1: Ask what to set up (project CLAUDE.md, personal CLAUDE.local.md, skills, hooks) +- Phase 2: Explore codebase (manifest files, README, CI config, existing CLAUDE.md, linter config, git worktrees) +- Phase 3: Fill in gaps via `AskUserQuestion` +- Phase 4: Write CLAUDE.md at project root +- Phase 5: Write CLAUDE.local.md (added to .gitignore) +- Phase 6: Suggest and create skills in `.claude/skills/` +- Phase 7: Suggest additional optimizations (GitHub CLI, linting, hooks, format-on-edit) +- Phase 8: Summary and next steps (plugin suggestions) + +**Side effect:** Calls `maybeMarkProjectOnboardingComplete()` when invoked. + +--- + +### `/init-verifiers` + +**File:** `commands/init-verifiers.ts` + +**Type:** `prompt` +**Syntax:** `/init-verifiers` +**Description:** Create verifier skill(s) for automated verification of code changes. Creates skills in `.claude/skills/` that can be used by the Verify agent. + +**Multi-phase workflow:** +- Phase 1: Auto-detect project type and stack (web app → Playwright, CLI → Tmux, API → HTTP) +- Phase 2: Verification tool setup (Playwright installation, MCP configuration) +- Phase 3: Interactive Q&A (verifier names, dev server details, auth setup) +- Phase 4: Generate verifier skills with appropriate `allowed-tools` +- Phase 5: Confirm creation + +**Verifier types and allowed tools:** + +| Type | Tools | +|---|---| +| `verifier-playwright` | `Bash(npm:*)`, `Bash(yarn:*)`, `mcp__playwright__*`, Read, Glob, Grep | +| `verifier-cli` | Tmux, `Bash(asciinema:*)`, Read, Glob, Grep | +| `verifier-api` | `Bash(curl:*)`, `Bash(http:*)`, `Bash(npm:*)`, Read, Glob, Grep | + +--- + +### `/insights` + +**File:** `commands/insights.ts` (lazy-loaded from `commands.ts`) + +**Type:** `prompt` +**Syntax:** `/insights` +**Description:** Generate an AI-powered report analyzing Claude Code usage sessions. + +**Implementation note:** This is a 113KB module (~3200 lines) that includes HTML rendering and diff utilities. It is registered in `commands.ts` as a lazy shim that dynamically imports the real module only when invoked, to avoid startup overhead. + +**Key features (from the module):** +- Reads session files from `~/.claude/projects/` +- Analyzes conversations, extracts facets and patterns +- Runs summarization via Opus model +- Generates HTML and diff-line reports +- Supports remote Coder host enumeration for internal users + +--- + +### `/install-github-app` + +**Files:** `commands/install-github-app/index.ts`, plus step components + +**Type:** `local-jsx` +**Syntax:** `/install-github-app` +**Description:** Interactive wizard to set up Claude GitHub Actions for a repository. + +**Gate:** +- `availability: ['claude-ai', 'console']` +- `isEnabled: () => !isEnvTruthy(process.env.DISABLE_INSTALL_GITHUB_APP_COMMAND)` + +**Wizard steps** (each is a separate React component): + +| Component | Purpose | +|---|---| +| `OAuthFlowStep` | GitHub OAuth authentication | +| `CheckGitHubStep` | Verify GitHub CLI and remote setup | +| `ChooseRepoStep` | Select the repository | +| `CheckExistingSecretStep` | Check for existing ANTHROPIC_API_KEY secret | +| `ApiKeyStep` | Collect/create API key | +| `InstallAppStep` | Install the Claude GitHub App | +| `ExistingWorkflowStep` | Handle existing workflow files | +| `WarningsStep` | Display security warnings | +| `CreatingStep` | Create workflow file and secret | +| `SuccessStep` | Show success and next steps | +| `ErrorStep` | Display errors | + +**Setup utility (`setupGitHubActions.ts`):** Creates `.github/workflows/claude.yml` and sets `ANTHROPIC_API_KEY` repository secret via GitHub API. + +--- + +### `/install-slack-app` + +**Files:** `commands/install-slack-app/index.ts`, `commands/install-slack-app/install-slack-app.ts` + +**Type:** `local` +**Syntax:** `/install-slack-app` +**Description:** Open the Claude Slack app installation page in the browser. + +**Gate:** `availability: ['claude-ai']` + +**Behavior:** +- Logs `tengu_install_slack_app_clicked` event. +- Increments `slackAppInstallCount` in global config. +- Opens `https://slack.com/marketplace/A08SF47R6P4-claude` in browser. +- Returns URL if browser open fails. + +--- + +### `install` (component) + +**File:** `commands/install.tsx` + +This is a large (~27KB) React component used during the onboarding flow, not a standalone slash command. It handles installation steps, auth setup, and account configuration for first-time users. + +--- + +### `/issue` (stub) + +**File:** `commands/issue/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/keybindings` + +**Files:** `commands/keybindings/index.ts`, `commands/keybindings/keybindings.ts` + +**Type:** `local` +**Syntax:** `/keybindings` +**Description:** Open or create the keybindings configuration file in the user's editor. + +**Gate:** `isEnabled: () => isKeybindingCustomizationEnabled()` (feature in preview) + +**Behavior:** +1. Gets keybindings file path from `getKeybindingsPath()`. +2. Creates parent directory if needed. +3. Writes a template file (using `'wx'` exclusive flag — no-op if exists). +4. Opens the file in the user's editor via `editFileInEditor()`. + +--- + +### `/login` + +**Files:** `commands/login/index.ts`, `commands/login/login.tsx` + +**Type:** `local-jsx` +**Syntax:** `/login` +**Description:** Sign in with an account. Description dynamically shows "Switch accounts" if already authenticated. + +**Gate:** `isEnabled: () => !isEnvTruthy(process.env.DISABLE_LOGIN_COMMAND)` + +**Post-login actions:** +- `context.onChangeAPIKey()` — notifies the app of key change +- `stripSignatureBlocks` on messages (signatures are key-bound) +- `resetCostState()` +- `refreshRemoteManagedSettings()` (non-blocking) +- `refreshPolicyLimits()` (non-blocking) +- `resetUserCache()` +- `refreshGrowthBookAfterAuthChange()` +- `clearTrustedDeviceToken()` + `enrollTrustedDevice()` (non-blocking) +- Killswitch gate resets: `resetBypassPermissionsCheck()`, `resetAutoModeGateCheck()` +- `checkAndDisableBypassPermissionsIfNeeded()`, `checkAndDisableAutoModeIfNeeded()` + +--- + +### `/logout` + +**Files:** `commands/logout/index.ts`, `commands/logout/logout.tsx` + +**Type:** `local-jsx` +**Syntax:** `/logout` +**Description:** Sign out from account. + +**Gate:** `isEnabled: () => !isEnvTruthy(process.env.DISABLE_LOGOUT_COMMAND)` + +**`performLogout()` steps:** +1. Flushes telemetry (before clearing credentials to prevent org data exposure). +2. `removeApiKey()` — deletes API key from storage. +3. `secureStorage.delete()` — wipes all secure storage. +4. Clears auth-related caches: Grove settings, OAuth tokens, policy limits, remote managed settings, betas caches, tool schema cache. +5. `resetUserCache()`, `refreshGrowthBookAfterAuthChange()`. +6. Updates global config (can optionally reset onboarding state). +7. `clearTrustedDeviceTokenCache()`. +8. `gracefulShutdownSync()` to exit. + +--- + +### `/mcp` + +**Files:** `commands/mcp/index.ts`, `commands/mcp/mcp.tsx`, `commands/mcp/addCommand.ts`, `commands/mcp/xaaIdpCommand.ts` + +**Type:** `local-jsx` +**Syntax:** `/mcp [enable|disable [server-name]]` +**Description:** Manage Model Context Protocol (MCP) servers. + +**Properties:** `immediate: true` + +**CLI subcommands (`mcp add` via `addCommand.ts`):** + +``` +claude mcp add [args...] + -s, --scope Configuration scope: local, user, project (default: local) + -t, --transport Transport: stdio, sse, http (default: stdio) + -e, --env Environment variables (KEY=value) + -H, --header HTTP headers + --client-id OAuth client ID + --client-secret Prompt for OAuth client secret + --callback-port Fixed OAuth callback port + --xaa Enable XAA (SEP-990) authentication +``` + +**XAA IdP subcommands (`claude mcp xaa` via `xaaIdpCommand.ts`):** + +| Subcommand | Description | +|---|---| +| `xaa setup` | Configure IdP connection: `--issuer ` `--client-id ` `[--client-secret]` `[--callback-port]` | +| `xaa login` | Authenticate with IdP (OIDC browser flow). `[--force]` `[--id-token ]` | +| `xaa show` | Show current IdP config and login status | +| `xaa clear` | Clear IdP config and cached id_token | + +**Security notes for `mcp xaa setup`:** +- Validates issuer URL: must be `https://` (or `http://localhost` for conformance). +- Validates `callbackPort` is a positive integer. +- Clears stale keychain slots when issuer or clientId changes. +- Writes settings before clearing keychain to avoid half-cleared state. + +--- + +### `/memory` + +**Files:** `commands/memory/index.ts`, `commands/memory/memory.tsx` + +**Type:** `local-jsx` +**Syntax:** `/memory` +**Description:** Edit Claude memory files (CLAUDE.md, CLAUDE.local.md, etc.). + +--- + +### `/mobile` / `/ios` / `/android` + +**Files:** `commands/mobile/index.ts`, `commands/mobile/mobile.tsx` + +**Type:** `local-jsx` +**Syntax:** `/mobile` +**Aliases:** `ios`, `android` +**Description:** Show a QR code to download the Claude mobile app. + +--- + +### `/mock-limits` (stub) + +**File:** `commands/mock-limits/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/model` + +**Files:** `commands/model/index.ts`, `commands/model/model.tsx` + +**Type:** `local-jsx` +**Syntax:** `/model [model]` +**Description:** Set the AI model for Claude Code. Description dynamically shows the currently selected model. + +**`immediate`:** Set by `shouldInferenceConfigCommandBeImmediate()`. + +--- + +### `/oauth-refresh` (stub) + +**File:** `commands/oauth-refresh/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/onboarding` (stub) + +**File:** `commands/onboarding/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/output-style` + +**Files:** `commands/output-style/index.ts`, `commands/output-style/output-style.tsx` + +**Type:** `local-jsx` +**Syntax:** `/output-style` +**Description:** Deprecated — use `/config` to change output style. + +**`isHidden: true`** — not shown in the command palette. + +--- + +### `/passes` + +**Files:** `commands/passes/index.ts`, `commands/passes/passes.tsx` + +**Type:** `local-jsx` +**Syntax:** `/passes` +**Description:** Share a free week of Claude Code with friends (referral passes). Optionally shows "earn extra usage" if referrer rewards are available. + +**Gate:** Hidden unless `checkCachedPassesEligibility().eligible && hasCache`. + +--- + +### `/perf-issue` (stub) + +**File:** `commands/perf-issue/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/permissions` / `/allowed-tools` + +**Files:** `commands/permissions/index.ts`, `commands/permissions/permissions.tsx` + +**Type:** `local-jsx` +**Syntax:** `/permissions` +**Aliases:** `allowed-tools` +**Description:** Manage allow and deny rules for tool permissions. + +--- + +### `/plan` + +**Files:** `commands/plan/index.ts`, `commands/plan/plan.tsx` + +**Type:** `local-jsx` +**Syntax:** `/plan [open|]` +**Description:** Enable plan mode or view/edit the current session plan. + +**Arguments:** + +| Argument | Description | +|---|---| +| *(none)* | View current plan or toggle plan mode | +| `open` | Open the plan file in an external editor | +| `` | Set/update the plan description | + +**Behavior:** +- Reads the plan file via `getPlan()` / `getPlanFilePath()`. +- Calls `prepareContextForPlanMode()` and `applyPermissionUpdate()` to restrict tools when entering plan mode. +- Calls `handlePlanModeTransition()` in bootstrap state. +- If `open`, launches `editFileInEditor()` with the plan file path. +- Shows `PlanDisplay` component with plan content, path, and editor hint. + +--- + +### `/plugin` / `/plugins` / `/marketplace` + +**Files:** `commands/plugin/index.tsx`, `commands/plugin/plugin.tsx`, plus multiple sub-components and utilities + +**Type:** `local-jsx` +**Syntax:** `/plugin [subcommand] [args]` +**Aliases:** `plugins`, `marketplace` +**Description:** Manage Claude Code plugins (install, uninstall, enable, disable, browse marketplace). + +**Properties:** `immediate: true` + +**Parsed subcommands (`parseArgs.ts`):** + +| Subcommand | Syntax | Description | +|---|---|---| +| *(none)* | `/plugin` | Open interactive menu | +| `install` / `i` | `/plugin install [plugin@marketplace]` | Install a plugin | +| `manage` | `/plugin manage` | Open manage plugins UI | +| `uninstall` | `/plugin uninstall [plugin]` | Uninstall a plugin | +| `enable` | `/plugin enable [plugin]` | Enable a plugin | +| `disable` | `/plugin disable [plugin]` | Disable a plugin | +| `validate` | `/plugin validate [path]` | Validate a plugin definition | +| `marketplace` | `/plugin marketplace [add\|remove\|update\|list] [target]` | Manage marketplaces | +| `help` | `/plugin help` | Show help | + +**Plugin format:** `plugin-name@marketplace-name` or `plugin-name@https://marketplace-url` + +**UI components:** +- `ManagePlugins.tsx` — list/toggle/remove installed plugins +- `BrowseMarketplace.tsx` — browse marketplace listings +- `AddMarketplace.tsx` — add a new marketplace URL +- `ManageMarketplaces.tsx` — manage registered marketplaces +- `DiscoverPlugins.tsx` — discovery flow for new users +- `PluginErrors.tsx` — display plugin load errors +- `PluginOptionsDialog.tsx` — configure plugin options +- `PluginOptionsFlow.tsx` — step-by-step option configuration +- `PluginSettings.tsx` — plugin settings panel +- `PluginTrustWarning.tsx` — security warning before install +- `UnifiedInstalledCell.tsx` — single plugin row in list +- `ValidatePlugin.tsx` — validation result display +- `pluginDetailsHelpers.tsx` — helpers for displaying plugin info +- `usePagination.ts` — pagination hook for marketplace listings + +--- + +### `/pr-comments` + +**File:** `commands/pr_comments/index.ts` + +**Type:** `prompt` (via `createMovedToPluginCommand`) +**Syntax:** `/pr-comments [PR number or args]` +**Description:** Get comments from a GitHub pull request. Uses `gh` CLI to fetch PR-level and code review comments. + +**Plugin migration:** For `USER_TYPE === 'ant'`, directs to `pr-comments@claude-code-marketplace`. For external users, runs the embedded prompt. + +**Fallback prompt logic:** +1. `gh pr view --json number,headRepository` to get PR info. +2. `gh api /repos/{owner}/{repo}/issues/{number}/comments` for PR-level comments. +3. `gh api /repos/{owner}/{repo}/pulls/{number}/comments` for review comments. +4. Formats with author, file#line, diff_hunk, comment text. + +--- + +### `/privacy-settings` + +**Files:** `commands/privacy-settings/index.ts`, `commands/privacy-settings/privacy-settings.tsx` + +**Type:** `local-jsx` +**Syntax:** `/privacy-settings` +**Description:** View and update privacy settings (training data opt-out, etc.). + +**Gate:** `isEnabled: () => isConsumerSubscriber()` + +--- + +### `/rate-limit-options` + +**Files:** `commands/rate-limit-options/index.ts`, `commands/rate-limit-options/rate-limit-options.tsx` + +**Type:** `local-jsx` +**Syntax:** `/rate-limit-options` +**Description:** Show options when the rate limit is reached (upgrade, extra usage, etc.). Shown only to `claude-ai` subscribers. + +**`isHidden: true`** — only used internally (e.g., triggered from the rate-limit message component, not user-invoked). + +--- + +### `/release-notes` + +**Files:** `commands/release-notes/index.ts`, `commands/release-notes/release-notes.ts` + +**Type:** `local` +**Syntax:** `/release-notes` +**Description:** Display the changelog for Claude Code. + +**`supportsNonInteractive: true`** + +**Behavior:** +1. Tries to fetch fresh changelog via `fetchAndStoreChangelog()` with a 500ms timeout. +2. Falls back to cached notes via `getStoredChangelog()`. +3. If nothing available, shows link to `CHANGELOG_URL`. +4. Formats as: `Version X.Y.Z:\n· bullet1\n· bullet2` + +--- + +### `/reload-plugins` + +**Files:** `commands/reload-plugins/index.ts`, `commands/reload-plugins/reload-plugins.ts` + +**Type:** `local` +**Syntax:** `/reload-plugins` +**Description:** Activate pending plugin changes in the current session (Layer-3 refresh). + +**Gate:** `supportsNonInteractive: false` + +**Behavior:** +1. In remote/CCR mode with `DOWNLOAD_USER_SETTINGS` flag: re-downloads user settings from server and fires `settingsChangeDetector.notifyChange`. +2. Calls `refreshActivePlugins(context.setAppState)`. +3. Returns summary: "Reloaded: N plugins · N skills · N agents · N hooks · N plugin MCP servers · N plugin LSP servers". +4. If errors: "N errors during load. Run /doctor for details." + +--- + +### `/remote-env` + +**Files:** `commands/remote-env/index.ts`, `commands/remote-env/remote-env.tsx` + +**Type:** `local-jsx` +**Syntax:** `/remote-env` +**Description:** Configure the default remote environment for teleport sessions. + +**Gate:** +- `isEnabled: () => isClaudeAISubscriber() && isPolicyAllowed('allow_remote_sessions')` +- Hidden when not eligible. + +--- + +### `/remote-setup` / `/web-setup` + +**Files:** `commands/remote-setup/index.ts`, `commands/remote-setup/remote-setup.tsx`, `commands/remote-setup/api.ts` + +**Type:** `local-jsx` +**Name:** `web-setup` +**Description:** Set up Claude Code on the web (connects GitHub account). + +**Gate:** +- `availability: ['claude-ai']` +- `isEnabled: () => getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_lantern', false) && isPolicyAllowed('allow_remote_sessions')` +- Feature flag: `CCR_REMOTE_SETUP` + +--- + +### `/rename` + +**Files:** `commands/rename/index.ts`, `commands/rename/rename.ts`, `commands/rename/generateSessionName.ts` + +**Type:** `local-jsx` +**Syntax:** `/rename [name]` +**Description:** Rename the current conversation. + +**Properties:** `immediate: true` + +**Arguments:** + +| Argument | Description | +|---|---| +| `[name]` | New name for the session. If omitted, auto-generates one using Haiku. | + +**Behavior:** +1. Blocked for swarm teammates (names set by team leader). +2. If no name provided: calls `generateSessionName(messages, abortSignal)`: + - Sends conversation text to `queryHaiku` with a system prompt to generate a 2-4 word kebab-case name. + - Returns JSON `{ name: string }`. +3. Saves name via `saveCustomTitle()` and `saveAgentName()`. +4. Syncs to bridge session title via `updateBridgeSessionTitle()` if connected. +5. Updates `AppState.standaloneAgentContext.name`. + +--- + +### `/reset-limits` (stub) + +**File:** `commands/reset-limits/index.js` + +**Type:** Stub — exports both `resetLimits` and `resetLimitsNonInteractive` as disabled stubs. + +--- + +### `/resume` / `/continue` + +**Files:** `commands/resume/index.ts`, `commands/resume/resume.tsx` + +**Type:** `local-jsx` +**Syntax:** `/resume [conversation id or search term]` +**Aliases:** `continue` +**Description:** Resume a previous conversation. Opens a fuzzy-searchable list of recent sessions. + +--- + +### `/review` + +**File:** `commands/review.ts` + +**Type:** `prompt` +**Syntax:** `/review [PR number]` +**Description:** Review a GitHub pull request using `gh` CLI. + +**Prompt logic:** +1. If no PR number: runs `gh pr list` to show open PRs. +2. If PR number: runs `gh pr view ` and `gh pr diff `. +3. Returns a structured code review with Overview, Code quality, Suggestions, Issues/risks sections. + +--- + +### `/ultrareview` + +**File:** `commands/review.ts`, `commands/review/ultrareviewCommand.tsx`, `commands/review/ultrareviewEnabled.ts`, `commands/review/reviewRemote.ts`, `commands/review/UltrareviewOverageDialog.tsx` + +**Type:** `local-jsx` +**Syntax:** `/ultrareview` +**Description:** Deep automated bug-finding review running in Claude Code on the web (~10–20 min). Finds and verifies bugs in the current branch. + +**Gate:** `isEnabled: () => isUltrareviewEnabled()` — checks GrowthBook `tengu_review_bughunter_config.enabled`. + +**Behavior:** Launches a remote agent session (CCR) that runs the bughunter analysis. Shows an overage dialog if free reviews are exhausted. + +--- + +### `/rewind` / `/checkpoint` + +**Files:** `commands/rewind/index.ts`, `commands/rewind/rewind.ts` + +**Type:** `local` +**Syntax:** `/rewind` +**Aliases:** `checkpoint` +**Description:** Restore the code and/or conversation to a previous point. Opens the message selector UI. + +**Gate:** `supportsNonInteractive: false` + +**Behavior:** Calls `context.openMessageSelector()` and returns `{ type: 'skip' }` (no message appended). + +--- + +### `/sandbox` + +**Files:** `commands/sandbox-toggle/index.ts`, `commands/sandbox-toggle/sandbox-toggle.tsx` + +**Type:** `local-jsx` +**Syntax:** `/sandbox [exclude "command pattern"]` +**Description:** Configure sandbox settings for bash command execution. + +**Dynamic description** shows current sandbox status: enabled/disabled, auto-allow mode, fallback allowed, managed policy. + +**`isHidden`:** Hidden on unsupported platforms (`!SandboxManager.isSupportedPlatform()` or `!SandboxManager.isPlatformInEnabledList()`). +**`immediate: true`** + +--- + +### `/security-review` + +**File:** `commands/security-review.ts` + +**Type:** `prompt` (via `createMovedToPluginCommand`) +**Syntax:** `/security-review` +**Description:** Complete a security-focused review of pending branch changes. + +**Plugin migration:** Directs `USER_TYPE === 'ant'` users to `security-review@claude-code-marketplace`. + +**Fallback prompt:** Large SECURITY_REVIEW_MARKDOWN with: +- Allowed tools: `Bash(git diff:*)`, `Bash(git status:*)`, `Bash(git log:*)`, `Bash(git show:*)`, `Bash(git remote show:*)`, `Read`, `Glob`, `Grep`, `LS`, `Task` +- Live shell injection: `git status`, `git diff --name-only origin/HEAD...`, `git log origin/HEAD...`, `git diff origin/HEAD...` + +**Analysis methodology:** 3-phase (explore codebase, vulnerability scan, false-positive filtering with confidence scoring). Launches parallel sub-tasks for FP filtering. + +**Security categories:** Input validation (SQLi, CMDi, XXE, template injection, NoSQL injection, path traversal), auth/authz, crypto/secrets, injection/RCE, data exposure. + +**Hard exclusions:** DoS, secrets on disk, rate limiting, memory issues, test-only code, log spoofing, SSRF path-only, AI prompt injection, regex, insecure docs, missing audit logs. + +--- + +### `/session` / `/remote` + +**Files:** `commands/session/index.ts`, `commands/session/session.tsx` + +**Type:** `local-jsx` +**Syntax:** `/session` +**Aliases:** `remote` +**Description:** Show remote session URL and QR code (for connecting mobile/web clients). + +**Gate:** `isEnabled: () => getIsRemoteMode()` — only shown in `--remote` mode. + +--- + +### `/share` (stub) + +**File:** `commands/share/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/skills` + +**Files:** `commands/skills/index.ts`, `commands/skills/skills.tsx` + +**Type:** `local-jsx` +**Syntax:** `/skills` +**Description:** List all available skills (custom commands loaded from skill directories, plugins, and bundled skills). + +--- + +### `/stats` + +**Files:** `commands/stats/index.ts`, `commands/stats/stats.tsx` + +**Type:** `local-jsx` +**Syntax:** `/stats` +**Description:** Show Claude Code usage statistics and activity (sessions, token usage, costs over time). + +--- + +### `/status` + +**Files:** `commands/status/index.ts`, `commands/status/status.tsx` + +**Type:** `local-jsx` +**Syntax:** `/status` +**Description:** Show comprehensive Claude Code status: version, model, account, API connectivity, tool statuses. + +**Properties:** `immediate: true` + +--- + +### `/statusline` + +**File:** `commands/statusline.tsx` + +**Type:** `prompt` +**Syntax:** `/statusline [description]` +**Description:** Set up Claude Code's status line UI. Launches a specialized `statusline-setup` subagent. + +**Allowed tools:** `AgentTool`, `Read(~/**)`, `Edit(~/.claude/settings.json)` + +**`disableNonInteractive: true`** + +**Behavior:** Creates an AgentTool call with `subagent_type: "statusline-setup"` and the user's prompt (default: "Configure my statusLine from my shell PS1 configuration"). + +--- + +### `/stickers` + +**Files:** `commands/stickers/index.ts`, `commands/stickers/stickers.ts` + +**Type:** `local` +**Syntax:** `/stickers` +**Description:** Open the Claude Code sticker ordering page at `https://www.stickermule.com/claudecode`. + +**Gate:** `supportsNonInteractive: false` + +--- + +### `/summary` (stub) + +**File:** `commands/summary/index.js` + +**Type:** Stub — disabled in all external builds. +**Note:** This is in `BRIDGE_SAFE_COMMANDS` and `INTERNAL_ONLY_COMMANDS` in commands.ts; the non-stub implementation is internal-only. + +--- + +### `/tag` + +**Files:** `commands/tag/index.ts`, `commands/tag/tag.tsx` + +**Type:** `local-jsx` +**Syntax:** `/tag ` +**Description:** Toggle a searchable tag on the current session (for session search/filtering). + +**Gate:** `isEnabled: () => process.env.USER_TYPE === 'ant'` + +--- + +### `/tasks` / `/bashes` + +**Files:** `commands/tasks/index.ts`, `commands/tasks/tasks.tsx` + +**Type:** `local-jsx` +**Syntax:** `/tasks` +**Aliases:** `bashes` +**Description:** List and manage background tasks (running shell commands, agent tasks, etc.). + +--- + +### `/teleport` (stub) + +**File:** `commands/teleport/index.js` + +**Type:** Stub — disabled in all external builds. + +--- + +### `/terminal-setup` + +**Files:** `commands/terminalSetup/index.ts`, `commands/terminalSetup/terminalSetup.tsx` + +**Type:** `local-jsx` +**Syntax:** `/terminal-setup` +**Description:** Install terminal keyboard bindings for newline input. + +**Dynamic description:** +- Apple Terminal: "Enable Option+Enter key binding for newlines and visual bell" +- Other terminals: "Install Shift+Enter key binding for newlines" + +**`isHidden`:** True for terminals with native CSI-u support (Ghostty, Kitty, iTerm2, WezTerm). + +--- + +### `/theme` + +**Files:** `commands/theme/index.ts`, `commands/theme/theme.tsx` + +**Type:** `local-jsx` +**Syntax:** `/theme` +**Description:** Change the TUI color theme (dark/light/etc.). + +--- + +### `/think-back` + +**Files:** `commands/thinkback/index.ts`, `commands/thinkback/thinkback.tsx` + +**Type:** `local-jsx` +**Syntax:** `/think-back` +**Description:** "Your 2025 Claude Code Year in Review" — a year-in-review animation. + +**Gate:** `isEnabled: () => checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_thinkback')` + +--- + +### `/thinkback-play` + +**Files:** `commands/thinkback-play/index.ts`, `commands/thinkback-play/thinkback-play.ts` + +**Type:** `local` +**Syntax:** `/thinkback-play` +**Description:** Play the thinkback animation. Hidden command called by the thinkback skill after generation is complete. + +**Gate:** `checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_thinkback')` +**`isHidden: true`**, `supportsNonInteractive: false` + +--- + +### `/ultraplan` + +**File:** `commands/ultraplan.tsx` + +**Type:** `local-jsx` +**Syntax:** `/ultraplan [seed plan]` +**Description:** Run an extended multi-agent planning session on Claude Code on the web (CCR). Explores the codebase, creates a comprehensive implementation plan, and enters plan-approval mode. + +**Feature gate:** `ULTRAPLAN` bundle flag required. + +**Constants:** +```typescript +const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000 // 30 min +export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the-web' +``` + +**Model:** From GrowthBook `tengu_ultraplan_model` (defaults to `ALL_MODEL_CONFIGS.opus46.firstParty`). + +**Behavior:** +1. Checks `checkRemoteAgentEligibility()` — returns error if not eligible. +2. Opens a remote CCR session via `teleportToRemote()`. +3. Sends the ultraplan prompt (from `ultraplan/prompt.txt`, inlined at bundle time; ANT builds can override via `ULTRAPLAN_PROMPT_FILE` env). +4. Polls `pollForApprovedExitPlanMode()` every ~5s for up to 30 minutes. +5. On approval: archives the remote session, updates local app state, transitions to plan mode. +6. On `REMOTE_CONTROL_DISCONNECTED_MSG`: falls back gracefully. +7. Shows CCR terms URL in description for legal visibility. + +--- + +### `/upgrade` + +**Files:** `commands/upgrade/index.ts`, `commands/upgrade/upgrade.tsx` + +**Type:** `local-jsx` +**Syntax:** `/upgrade` +**Description:** Upgrade to Max plan for higher rate limits and more Opus access. + +**Gate:** +- `availability: ['claude-ai']` +- `isEnabled: () => !isEnvTruthy(DISABLE_UPGRADE_COMMAND) && getSubscriptionType() !== 'enterprise'` + +--- + +### `/usage` + +**Files:** `commands/usage/index.ts`, `commands/usage/usage.tsx` + +**Type:** `local-jsx` +**Syntax:** `/usage` +**Description:** Show plan usage limits and current consumption. + +**Gate:** `availability: ['claude-ai']` + +--- + +### `/version` + +**File:** `commands/version.ts` + +**Type:** `local` +**Syntax:** `/version` +**Description:** Print the version this session is running (not what auto-update downloaded). + +**Gate:** `isEnabled: () => process.env.USER_TYPE === 'ant'` + +**Output:** `"${VERSION} (built ${BUILD_TIME})"` or just `VERSION` if no build time. + +**`supportsNonInteractive: true`** + +**Note:** Uses `MACRO.VERSION` and `MACRO.BUILD_TIME` — build-time macros injected by the bundler. + +--- + +### `/vim` + +**Files:** `commands/vim/index.ts`, `commands/vim/vim.ts` + +**Type:** `local` +**Syntax:** `/vim` +**Description:** Toggle between Vim and Normal (readline) editing modes for the prompt input. + +**Gate:** `supportsNonInteractive: false` + +**Behavior:** +- Reads `config.editorMode` (handles legacy `'emacs'` → `'normal'`). +- Toggles between `'normal'` and `'vim'`. +- Saves to global config. +- Logs `tengu_editor_mode_changed` event with `source: 'command'`. +- Returns description: "Editor mode set to vim/normal. Use Escape key..." / "Using standard (readline) keyboard bindings." + +--- + +### `/voice` + +**Files:** `commands/voice/index.ts`, `commands/voice/voice.ts` + +**Type:** `local` +**Syntax:** `/voice` +**Description:** Toggle voice mode (speech-to-text input via push-to-talk). + +**Gate:** +- `availability: ['claude-ai']` +- `isEnabled: () => isVoiceGrowthBookEnabled()` — feature-flagged via GrowthBook +- `isHidden: !isVoiceModeEnabled()` +- Feature flag: `VOICE_MODE` bundle flag + +**Toggle ON pre-flight checks:** +1. `isVoiceModeEnabled()` — checks kill-switch + auth. +2. `checkRecordingAvailability()` — microphone hardware check. +3. `isVoiceStreamAvailable()` — checks for API key. +4. `checkVoiceDependencies()` — checks for SoX or other audio recording tool. +5. `requestMicrophonePermission()` — OS permission probe (fires permission dialog now). + +**Toggle OFF:** No checks needed, always allowed. + +**On success:** `updateSettingsForSource('userSettings', { voiceEnabled: true/false })`, fires `settingsChangeDetector.notifyChange`, logs `tengu_voice_toggled`. + +**Language hint:** Shows STT language on first 2 enables; falls back to English if language not supported. + +--- + +## 5. Internal-Only Commands Summary + +These commands are registered only when `process.env.USER_TYPE === 'ant'` and `!process.env.IS_DEMO`: + +| Command | Description | +|---|---| +| `backfill-sessions` | Stub (disabled) | +| `break-cache` | Stub (disabled) | +| `bughunter` | Stub (disabled) | +| `commit` | Git commit via model | +| `commit-push-pr` | Commit + push + PR via model | +| `ctx_viz` | Stub (disabled) | +| `good-claude` | Stub (disabled) | +| `issue` | Stub (disabled) | +| `init-verifiers` | Create verifier skills | +| `force-snip` | Force history snip (feature-gated) | +| `mock-limits` | Stub (disabled) | +| `bridge-kick` | Inject bridge failure states | +| `version` | Print version | +| `ultraplan` | Remote planning session (feature-gated) | +| `subscribe-pr` | GitHub webhook subscription (feature-gated) | +| `reset-limits` | Stub (disabled) | +| `reset-limits-noninteractive` | Stub (disabled) | +| `onboarding` | Stub (disabled) | +| `share` | Stub (disabled) | +| `summary` | Stub (disabled) | +| `teleport` | Stub (disabled) | +| `ant-trace` | Stub (disabled) | +| `perf-issue` | Stub (disabled) | +| `env` | Stub (disabled) | +| `oauth-refresh` | Stub (disabled) | +| `debug-tool-call` | Stub (disabled) | +| `agents-platform` | Internal agents platform command | +| `autofix-pr` | Stub (disabled) | + +--- + +## 6. Remote-Safe Commands + +Commands available in `--remote` mode (before CCR init arrives). These only affect local TUI state: + +``` +session, exit, clear, help, theme, color, vim, cost, usage, +copy, btw, feedback, plan, keybindings, statusline, stickers, mobile +``` + +--- + +## 7. Bridge-Safe Commands + +`'local'`-type commands safe to execute over the Remote Control bridge (from mobile/web). `'prompt'` commands are always bridge-safe. `'local-jsx'` commands are always blocked. + +``` +compact, clear, cost, summary, release-notes, files +``` + +--- + +## 8. Command Availability & Feature Flags + +### Availability requirements + +| Value | Meaning | +|---|---| +| `'claude-ai'` | Must be a claude.ai subscriber (`isClaudeAISubscriber()`) | +| `'console'` | Must be a first-party API user (not 3P, not claude.ai, not custom baseURL) | + +Commands without `availability` are shown to all users. + +### Environment variables affecting commands + +| Variable | Effect | +|---|---| +| `DISABLE_COMPACT` | Disables `/compact` | +| `DISABLE_DOCTOR_COMMAND` | Disables `/doctor` | +| `DISABLE_INSTALL_GITHUB_APP_COMMAND` | Disables `/install-github-app` | +| `DISABLE_LOGIN_COMMAND` | Disables `/login` | +| `DISABLE_LOGOUT_COMMAND` | Disables `/logout` | +| `DISABLE_FEEDBACK_COMMAND` | Disables `/feedback` | +| `DISABLE_BUG_COMMAND` | Disables `/feedback` alias | +| `DISABLE_UPGRADE_COMMAND` | Disables `/upgrade` | +| `DISABLE_EXTRA_USAGE_COMMAND` | Disables `/extra-usage` | +| `CLAUDE_CODE_USE_BEDROCK` | Disables `/feedback` | +| `CLAUDE_CODE_USE_VERTEX` | Disables `/feedback` | +| `CLAUDE_CODE_USE_FOUNDRY` | Disables `/feedback` | +| `USER_TYPE=ant` | Enables internal-only commands | +| `IS_DEMO=1` | Suppresses internal-only commands even for ant users | +| `CLAUDE_CODE_NEW_INIT=1` | Enables new multi-phase `/init` prompt | +| `CLAUDE_CODE_ENABLE_XAA=1` | Enables `--xaa` flag in `claude mcp add` | +| `MCP_CLIENT_SECRET` | OAuth client secret for `claude mcp add --client-secret` | +| `MCP_XAA_IDP_CLIENT_SECRET` | IdP client secret for `claude mcp xaa setup --client-secret` | +| `ULTRAPLAN_PROMPT_FILE` | Override ultraplan prompt (ant builds only) | + +### Bun bundle feature flags + +Feature flags evaluated at bundle time via `feature('FLAG_NAME')`: + +| Flag | Commands/Behavior | +|---|---| +| `BRIDGE_MODE` | Enables `/remote-control` | +| `BRIDGE_MODE` + `DAEMON` | Enables remoteControlServer | +| `KAIROS` / `KAIROS_BRIEF` | Enables `/brief` | +| `KAIROS` | Enables `assistant` command | +| `VOICE_MODE` | Enables `/voice` | +| `HISTORY_SNIP` | Enables `force-snip` | +| `WORKFLOW_SCRIPTS` | Enables `workflows` + `getWorkflowCommands` | +| `CCR_REMOTE_SETUP` | Enables `/web-setup` | +| `ULTRAPLAN` | Enables `/ultraplan` | +| `KAIROS_GITHUB_WEBHOOKS` | Enables `subscribe-pr` | +| `TORCH` | Enables `torch` | +| `UDS_INBOX` | Enables `peers` | +| `FORK_SUBAGENT` | Enables `fork` command; removes `fork` alias from `/branch` | +| `BUDDY` | Enables `buddy` | +| `PROACTIVE` / `KAIROS` | Enables `proactive` | +| `EXPERIMENTAL_SKILL_SEARCH` | Enables skill index cache clearing | +| `REACTIVE_COMPACT` | Routes `/compact` through reactive path | +| `CONTEXT_COLLAPSE` | Adds collapse stats to `/context` output | +| `NEW_INIT` | New multi-phase `/init` prompt | +| `BG_SESSIONS` | `/exit` detaches tmux instead of killing | +| `COORDINATOR_MODE` | `/clear` saves coordinator mode | +| `COMMIT_ATTRIBUTION` | `/clear` clears attribution caches | +| `TRANSCRIPT_CLASSIFIER` | `/login` runs auto-mode gate check | +| `DOWNLOAD_USER_SETTINGS` | `/reload-plugins` re-downloads settings in CCR mode | +| `MCP_SKILLS` | `getMcpSkillCommands` returns MCP prompt skills | diff --git a/spec/03_tools.md b/spec/03_tools.md new file mode 100644 index 0000000..25b2359 --- /dev/null +++ b/spec/03_tools.md @@ -0,0 +1,2171 @@ +# Claude Code — Tools System + +## Table of Contents + +1. [Tool Framework (Tool.ts)](#1-tool-framework) +2. [Tool Registry (tools.ts)](#2-tool-registry) +3. [Task Framework (Task.ts / tasks.ts)](#3-task-framework) +4. [Core File Tools](#4-core-file-tools) + - [FileReadTool](#41-filereadtool) + - [FileWriteTool](#42-filewritetool) + - [FileEditTool](#43-fileedittool) +5. [Shell Execution Tools](#5-shell-execution-tools) + - [BashTool](#51-bashtool) + - [PowerShellTool](#52-powershelltool) +6. [Search Tools](#6-search-tools) + - [GlobTool](#61-globtool) + - [GrepTool](#62-greptool) +7. [Agent / Multi-Agent Tools](#7-agent--multi-agent-tools) + - [AgentTool](#71-agenttool) + - [TeamCreateTool](#72-teamcreatetool) + - [TeamDeleteTool](#73-teamdeletetool) + - [SendMessageTool](#74-sendmessagetool) +8. [Task Management Tools](#8-task-management-tools) + - [TaskStopTool](#81-taskstoptool) + - [TaskOutputTool](#82-taskoutputtool) + - [TodoWriteTool (V1)](#83-todowritetool-v1) + - [TaskCreateTool (V2)](#84-taskcreatetool-v2) + - [TaskGetTool (V2)](#85-taskgettool-v2) + - [TaskUpdateTool (V2)](#86-taskunpdatetool-v2) + - [TaskListTool (V2)](#87-tasklisttool-v2) +9. [Web Tools](#9-web-tools) + - [WebFetchTool](#91-webfetchtool) + - [WebSearchTool](#92-websearchtool) +10. [MCP Integration Tools](#10-mcp-integration-tools) + - [MCPTool](#101-mcptool) + - [McpAuthTool](#102-mcpauthtool) + - [ListMcpResourcesTool](#103-listmcpresourcestool) + - [ReadMcpResourceTool](#104-readmcpresourcetool) +11. [Plan Mode Tools](#11-plan-mode-tools) + - [EnterPlanModeTool](#111-enterplanmodetool) + - [ExitPlanModeV2Tool](#112-exitplanmodev2tool) +12. [Notebook Tool](#12-notebook-tool) +13. [Worktree Tools](#13-worktree-tools) + - [EnterWorktreeTool](#131-enterworkreetool) + - [ExitWorktreeTool](#132-exitworkreetool) +14. [Scheduling Tools](#14-scheduling-tools) + - [CronCreateTool](#141-croncreatetool) + - [CronDeleteTool](#142-crondeletetool) + - [CronListTool](#143-cronlisttool) +15. [Meta / Discovery Tools](#15-meta--discovery-tools) + - [ToolSearchTool](#151-toolsearchtool) + - [AskUserQuestionTool](#152-askuserquestiontool) +16. [Kairos / Special Mode Tools](#16-kairos--special-mode-tools) + - [BriefTool (SendUserMessage)](#161-brieftool-senduserrmessage) + - [SleepTool](#162-sleeptool) + - [RemoteTriggerTool](#163-remotetriggertool) +17. [SDK / Output Tools](#17-sdk--output-tools) + - [SyntheticOutputTool (StructuredOutput)](#171-syntheticoutputtool-structuredoutput) +18. [Skill Tool](#18-skill-tool) +19. [LSP Tool](#19-lsp-tool) +20. [REPL Tool](#20-repl-tool) +21. [Config Tool](#21-config-tool) +22. [Shared Utilities](#22-shared-utilities) + - [tools/utils.ts](#221-toolsutilsts) + - [tools/shared/gitOperationTracking.ts](#222-toolssharedgitoperationtrackingts) + - [tools/shared/spawnMultiAgent.ts](#223-toolssharedspawnmultiagentts) +23. [Testing Utilities](#23-testing-utilities) + +--- + +## 1. Tool Framework + +**Source:** `src/Tool.ts` + +### 1.1 Core Interface + +```typescript +export type Tool = { + // Identity + name: string + isMcp?: boolean + mcpInfo?: { serverName: string; toolName: string } + isLsp?: boolean + alwaysLoad?: boolean + shouldDefer?: boolean + + // Schema (getter properties for lazy init) + readonly inputSchema: Input + readonly outputSchema?: ZodType + + // Metadata + description(): Promise + prompt(): Promise + userFacingName(input?: z.infer): string + maxResultSizeChars?: number + searchHint?: string + + // Capability flags (accept input for per-call decisions) + isEnabled(permissionContext?: ToolPermissionContext): boolean + isConcurrencySafe(input?: z.infer): boolean + isReadOnly(input?: z.infer): boolean + isDestructive?(input: z.infer): boolean + toAutoClassifierInput(input: z.infer): string + isSearchOrReadCommand?: (input: z.infer) => { isSearch: boolean; isRead: boolean } + + // Execution + validateInput?(input: z.infer): Promise + checkPermissions(input: z.infer, context: ToolUseContext): Promise + call(input: z.infer, context: ToolUseContext): Promise> + + // UI rendering (React / Ink) + renderToolUseMessage(input: z.infer, options: RenderOptions): ReactNode | null + renderToolUseProgressMessage?(progress: Progress, input?: z.infer): ReactNode | null + renderToolUseQueuedMessage?(input: z.infer): ReactNode | null + renderToolUseRejectedMessage?(input: z.infer, ...): ReactNode | null + renderToolResultMessage(output: Output, ...): ReactNode | null + renderToolUseErrorMessage?(error: Error, ...): ReactNode | null + + // Output mapping + mapToolResultToToolResultBlockParam( + output: Output, + toolUseID: string, + context: ToolUseContext, + ): ToolResultBlockParam + + // Path tracking (for permission rules) + getPath?(input?: z.infer): string | undefined +} +``` + +### 1.2 ToolDef — Definition Shape + +`ToolDef` is the shape passed to `buildTool()`. It has the same fields as `Tool` minus the defaults filled in by `buildTool()`. + +### 1.3 buildTool() + +```typescript +function buildTool(def: ToolDef): Tool<...> +``` + +Fills in safe defaults: +- `isEnabled` → `() => true` +- `isConcurrencySafe` → `() => false` +- `isReadOnly` → `() => false` +- `checkPermissions` → `async () => ({ behavior: 'allow', updatedInput: input })` +- `toAutoClassifierInput` → `() => ''` +- `userFacingName` → `() => def.name` + +### 1.4 ToolUseContext + +The context object passed to every `call()` and `checkPermissions()`: + +```typescript +type ToolUseContext = { + // Configuration + options: { + commands: Command[] + tools: Tool[] + mcpClients: MCPClient[] + mainLoopModel: string + thinkingConfig: ThinkingConfig + // ... additional options + } + + // Abort signal + abortController: AbortController + + // App state accessors + getAppState(): AppState + setAppState(fn: (prev: AppState) => AppState): void + + // File read tracking (for read-before-write enforcement) + readFileState: Map + + // Permission context + permissionContext: ToolPermissionContext + + // UI injection + setToolJSX: SetToolJSXFn + + // Callbacks + onPermissionRequest(request: PermissionRequest): Promise + onToolCallStart(toolName: string, input: unknown): void + onToolCallEnd(toolName: string, result: unknown): void + + // Agent/teammate context + agentId?: AgentId + isSubagent?: boolean + isCoordinator?: boolean + + // Additional fields for specific tool categories + globLimits?: { maxResults: number } +} +``` + +### 1.5 ToolPermissionContext + +```typescript +type ToolPermissionContext = { + mode: PermissionMode // 'default' | 'plan' | 'auto' | 'bypassPermissions' | 'acceptEdits' + alwaysAllow: PermissionRule[] + alwaysDeny: PermissionRule[] + alwaysAsk: PermissionRule[] + additionalWorkingDirectories: string[] + toolPermissions: Record +} +``` + +### 1.6 PermissionResult / PermissionDecision + +```typescript +type PermissionDecision = + | { behavior: 'allow'; updatedInput: Input } + | { behavior: 'ask'; message: string; decisionReason?: string } + | { behavior: 'deny'; message: string } + | { behavior: 'passthrough' } // Always asks user + +type ValidationResult = + | { result: true } + | { result: false; message: string; errorCode?: number } +``` + +### 1.7 ToolResult + +```typescript +type ToolResult = { data: T } +``` + +### 1.8 Tools Type Alias & Helpers + +```typescript +type Tools = Tool[] + +function findToolByName(tools: Tools, name: string): Tool | undefined +function toolMatchesName(tool: Tool, name: string): boolean +``` + +--- + +## 2. Tool Registry + +**Source:** `src/tools.ts` + +### 2.1 getAllBaseTools() + +Returns the full ordered list of built-in tools. Order must stay in sync with Statsig caching config. + +```typescript +function getAllBaseTools(): Tool[] +``` + +Includes (conditionally): +- Always: BashTool, GlobTool, GrepTool, FileReadTool, FileEditTool, FileWriteTool, AgentTool, WebFetchTool, WebSearchTool, NotebookEditTool, TodoWriteTool, TaskStopTool, AskUserQuestionTool, SkillTool, MCPTool, EnterPlanModeTool, ExitPlanModeV2Tool, ToolSearchTool, TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool, TeamCreateTool, TeamDeleteTool, SendMessageTool, TaskOutputTool, SyntheticOutputTool, EnterWorktreeTool, ExitWorktreeTool, BriefTool, RemoteTriggerTool +- Ant-only: ConfigTool, TungstenTool, REPLTool +- Feature-gated (`feature('KAIROS')` + `isKairosCronEnabled()`): CronCreateTool, CronDeleteTool, CronListTool +- Feature-gated (`feature('AGENT_TRIGGERS')`): RemoteTriggerTool +- Feature-gated (`feature('SLEEP_TOOL')`): SleepTool +- Feature-gated (`feature('MONITOR_TOOL')`): MonitorMcpTask +- LSP enabled: LSPTool + +### 2.2 getTools(permissionContext) + +```typescript +function getTools(permissionContext: ToolPermissionContext): Tool[] +``` + +- If `CLAUDE_CODE_SIMPLE` env var is set: returns only `[BashTool, FileReadTool, FileEditTool]` +- Calls `getAllBaseTools()`, filters with `filterToolsByDenyRules()` +- In REPL mode: hides `REPL_ONLY_TOOLS` (Bash, Read, Write, Edit, Glob, Grep, NotebookEdit, Agent) + +### 2.3 assembleToolPool() + +```typescript +function assembleToolPool( + baseTools: Tool[], + mcpTools: Tool[], +): Tool[] +``` + +- Combines built-in + MCP tools +- Sorts by name for prompt-cache stability +- Deduplicates (built-in tools win over MCP tools with same name) + +### 2.4 filterToolsByDenyRules() + +```typescript +function filterToolsByDenyRules( + tools: Tool[], + permissionContext: ToolPermissionContext, +): Tool[] +``` + +Removes tools whose name matches a blanket deny rule in permissionContext. + +### 2.5 Tool Presets + +```typescript +const TOOL_PRESETS = { + 'full': /* all tools */, + 'minimal': /* BashTool, FileReadTool, FileEditTool */, + // ... +} + +function parseToolPreset(preset: string): Tool[] | null +function getToolsForDefaultPreset(): Tool[] +function getMergedTools(base: Tool[], overrides: Tool[]): Tool[] +``` + +--- + +## 3. Task Framework + +**Sources:** `src/Task.ts`, `src/tasks.ts` + +### 3.1 TaskType + +```typescript +type TaskType = + | 'local_bash' // prefix: 'b' + | 'local_agent' // prefix: 'a' + | 'remote_agent' // prefix: 'r' + | 'in_process_teammate' // prefix: 't' + | 'local_workflow' // prefix: 'w' + | 'monitor_mcp' // prefix: 'm' + | 'dream' // prefix: 'd' +``` + +### 3.2 TaskStatus + +```typescript +type TaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'killed' + +function isTerminalTaskStatus(status: TaskStatus): boolean +// Returns true for 'completed', 'failed', 'killed' +``` + +### 3.3 TaskStateBase + +```typescript +type TaskStateBase = { + id: string // prefix + 8 random base-36 chars + type: TaskType + status: TaskStatus + description: string + toolUseId?: string + startTime: number // Date.now() + endTime?: number + totalPausedMs?: number + outputFile: string // getTaskOutputPath(id) + outputOffset: number + notified: boolean +} +``` + +### 3.4 ID Generation + +```typescript +// Alphabet: '0123456789abcdefghijklmnopqrstuvwxyz' +// 36^8 ≈ 2.8 trillion combinations +function generateTaskId(type: TaskType): string +// Returns: prefix + 8 crypto-random base-36 chars + +function createTaskStateBase( + id: string, + type: TaskType, + description: string, + toolUseId?: string, +): TaskStateBase +``` + +### 3.5 Task Interface + +```typescript +type Task = { + name: string + type: TaskType + kill(taskId: string, setAppState: SetAppState): Promise +} +``` + +### 3.6 Task Registry + +```typescript +// src/tasks.ts +function getAllTasks(): Task[] +// Returns: [LocalShellTask, LocalAgentTask, RemoteAgentTask, DreamTask, +// optionally LocalWorkflowTask, MonitorMcpTask] + +function getTaskByType(type: TaskType): Task | undefined +``` + +--- + +## 4. Core File Tools + +### 4.1 FileReadTool + +**Tool name:** `Read` +**Source:** `src/tools/FileReadTool/FileReadTool.ts` + +**Characteristics:** +- `isConcurrencySafe: true` +- `isReadOnly: true` +- `maxResultSizeChars: Infinity` (prevents circular reads through disk persistence) +- `strict: true` +- `searchHint: 'read files, images, PDFs, notebooks'` +- `isSearchOrReadCommand: { isSearch: false, isRead: true }` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file_path` | `string` | Yes | Absolute path to file | +| `offset` | `integer` | No | Line number to start reading from | +| `limit` | `integer` | No | Number of lines to read | +| `pages` | `string` | No | PDF page range (e.g. `"1-5"`, `"3"`, `"10-20"`); PDF only; max 20 pages per request | + +**Output Schema (discriminated union on `type`):** + +```typescript +// Text file +{ type: 'text'; content: string; numLines: number; startLine: number; totalLines: number } + +// Image file +{ type: 'image'; base64: string; mimeType: string; originalSize: number; dimensions: { width: number; height: number } } + +// Jupyter notebook +{ type: 'notebook'; cells: NotebookCell[] } + +// PDF (full) +{ type: 'pdf'; base64: string; originalSize: number } + +// PDF (extracted pages) +{ type: 'parts'; pages: PDFPage[] } + +// Unchanged (content not modified since last read) +{ type: 'file_unchanged' } +``` + +**Security / Validation:** +- Blocked device paths: `/dev/zero`, `/dev/random`, `/dev/urandom`, `/dev/full`, `/dev/stdin`, `/dev/tty`, and other infinite-stream devices +- Registers file in `readFileState` cache (path → `{mtime, content}`) enabling FileEditTool/FileWriteTool read-before-write enforcement +- UNC path handling skipped on Windows + +**Exports:** +```typescript +function registerFileReadListener(listener: FileReadListener): void +class MaxFileReadTokenExceededError extends Error +``` + +--- + +### 4.2 FileWriteTool + +**Tool name:** `Write` +**Source:** `src/tools/FileWriteTool/FileWriteTool.ts` + +**Characteristics:** +- `strict: true` +- `maxResultSizeChars: 100_000` +- `searchHint: 'create or overwrite files'` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file_path` | `string` | Yes | Absolute path to file | +| `content` | `string` | Yes | Full file content to write | + +**Output Schema:** + +```typescript +{ + type: 'create' | 'update' + filePath: string + content: string + structuredPatch: StructuredPatch + originalFile: string | null // null for new files + gitDiff?: string // Optional git diff of changes +} +``` + +**Validation / Safety:** +- **Read-before-write enforcement:** For existing files, file must appear in `readFileState` cache (must have been read with FileReadTool) +- **mtime staleness check:** If file mtime has changed since last read, refuses to overwrite to prevent clobbering concurrent changes +- **File size limit:** Max 1 GiB +- **UNC path security:** Skips read-check for UNC paths (`\\server\share`) on Windows +- **`.ipynb` files:** Redirected to `NotebookEditTool` +- **Team memory protection:** Blocks writes to team memory secret files +- **Deny rules:** Checks permission context deny rules +- **LF line endings:** New content uses LF regardless of platform +- Notifies LSP client on successful edit + +--- + +### 4.3 FileEditTool + +**Tool name:** `Edit` +**Source:** `src/tools/FileEditTool/FileEditTool.ts` + +**Characteristics:** +- `strict: true` +- `maxResultSizeChars: 100_000` +- `searchHint: 'modify file contents in place'` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file_path` | `string` | Yes | Absolute path to file to edit | +| `old_string` | `string` | Yes | Text to search for (must exist exactly once unless `replace_all` is true) | +| `new_string` | `string` | Yes | Replacement text (must differ from `old_string`) | +| `replace_all` | `boolean` | No (default `false`) | Replace all occurrences of `old_string` | + +**Output Schema:** + +```typescript +{ + filePath: string + oldString: string + newString: string + originalFile: string // File content before edit + structuredPatch: StructuredPatch + userModified: boolean // Whether user modified the proposed diff + replaceAll: boolean + gitDiff?: string +} +``` + +**Validation / Safety:** +- `old_string` must differ from `new_string` +- File must have been read via `readFileState` (read-before-write enforcement) +- mtime staleness check (same as FileWriteTool) +- `old_string` must be found in file content +- Unless `replace_all` is true, at most 1 occurrence of `old_string` is allowed +- File size: max 1 GiB +- UNC path security skip +- `.ipynb` files redirected to `NotebookEditTool` +- Team memory secret guard +- Permission context deny rule check +- Uses `findActualString()` for quote normalization (handles straight/curly quotes interchangeably) +- Uses `preserveQuoteStyle()` to maintain original quote style +- Notifies LSP client on successful edit + +--- + +## 5. Shell Execution Tools + +### 5.1 BashTool + +**Tool name:** `Bash` +**Source:** `src/tools/BashTool/BashTool.tsx` + +**Characteristics:** +- `maxResultSizeChars` (varies by output type) +- `searchHint` (from prompt) +- Supports background task execution +- Supports sandboxing (bwrap on Linux, sandbox-exec on macOS) + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `command` | `string` | Yes | Shell command to execute | +| `timeout` | `number` | No | Timeout in milliseconds (max varies by context) | +| `description` | `string` | No | Human-readable description of what the command does | +| `run_in_background` | `boolean` | No | Launch as background task; omitted from schema when `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=true` | +| `dangerouslyDisableSandbox` | `boolean` | No | Override sandbox mode | +| `_simulatedSedEdit` | internal | — | Never in model-facing schema; used for sed edit simulation | + +**Output Schema:** + +```typescript +{ + stdout: string + stderr: string + interrupted: boolean + isImage?: boolean // stdout contains base64 image data + backgroundTaskId?: string // Set when run_in_background=true + backgroundedByUser?: boolean // User pressed Ctrl+B + assistantAutoBackgrounded?: boolean // Auto-backgrounded after blocking budget + dangerouslyDisableSandbox?: boolean + returnCodeInterpretation?: string // Semantic meaning of exit code + noOutputExpected?: boolean + structuredContent?: unknown + persistedOutputPath?: string // Path when output too large for inline + persistedOutputSize?: number // Total bytes when persisted +} +``` + +**Timing Constants:** +- Progress threshold: `2_000ms` (show progress spinner after 2s) +- Auto-background threshold: `120_000ms` (auto-background after 2 minutes) +- `ASSISTANT_BLOCKING_BUDGET_MS`: `15_000ms` (in assistant/Kairos mode, auto-background after 15s in main agent) + +**Permission Behavior:** +- Checks permission rules against the command string +- `bypassPermissions` mode: allows all commands +- `auto`/`acceptEdits` mode: allows read-only bash commands without asking; asks for write commands +- `default` mode: always asks unless explicitly allowed + +**Sandbox:** +- Linux: `bwrap` (bubblewrap) based isolation +- macOS: `sandbox-exec` based isolation +- Windows: no sandbox (bwrap/sandbox-exec are POSIX-only) +- `dangerouslyDisableSandbox` overrides per-call +- Enterprise policy: if sandbox required and unavailable, execution is blocked + +**Blocked patterns:** +- `detectBlockedSleepPattern()`: detects bare `sleep N` commands with N>=2 as first statement; suggests using SleepTool instead + +**Git operation tracking:** +- `trackGitOperations()` called after each bash command to fire analytics for commits, pushes, PR creation +- `isSearchOrReadBashCommand()`: classifies command for UI collapsing + +**Exports:** +```typescript +function isSearchOrReadBashCommand(command: string): { isSearch: boolean; isRead: boolean } +function detectBlockedSleepPattern(command: string): string | null +type BashToolInput = { command: string; timeout?: number; description?: string; run_in_background?: boolean; dangerouslyDisableSandbox?: boolean } +``` + +--- + +### 5.2 PowerShellTool + +**Tool name:** `PowerShell` +**Source:** `src/tools/PowerShellTool/PowerShellTool.tsx` + +Windows-native PowerShell execution tool, mirroring BashTool's interface. + +**Characteristics:** +- Windows-specific; requires PowerShell (`pwsh`) to be installed +- Detects PowerShell path via `getCachedPowerShellPath()` +- Tracks git operations via shared `trackGitOperations()` +- Same sandbox policy as BashTool (POSIX sandbox applies on Linux/macOS when running `pwsh`) +- `PROGRESS_THRESHOLD_MS: 2_000ms` +- `ASSISTANT_BLOCKING_BUDGET_MS: 15_000ms` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `command` | `string` | Yes | PowerShell command to execute | +| `timeout` | `number` | No | Optional timeout in milliseconds (`max getMaxTimeoutMs()`) | +| `description` | `string` | No | Description of what the command does | +| `run_in_background` | `boolean` | No | Background execution; omitted when `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=true` | +| `dangerouslyDisableSandbox` | `boolean` | No | Override sandbox mode | + +**Output Schema:** + +```typescript +{ + stdout: string + stderr: string + interrupted: boolean + returnCodeInterpretation?: string + isImage?: boolean + persistedOutputPath?: string + persistedOutputSize?: number + backgroundTaskId?: string + backgroundedByUser?: boolean + assistantAutoBackgrounded?: boolean +} +``` + +**PowerShell-specific features:** +- `PS_SEARCH_COMMANDS`: `Select-String`, `Get-ChildItem`, `FindStr`, `where.exe` (grep/find equivalents) +- `PS_READ_COMMANDS`: `Get-Content`, `Get-Item`, `Test-Path`, `Resolve-Path`, `Get-Process`, `Get-Service`, `Get-ChildItem`, `Get-Location`, `Get-FileHash`, `Get-Acl`, `Format-Hex` +- `PS_SEMANTIC_NEUTRAL_COMMANDS`: `Write-Output`, `Write-Host` +- `detectBlockedSleepPattern()`: catches `Start-Sleep N`, `Start-Sleep -Seconds N`, `sleep N` as first statement +- `DISALLOWED_AUTO_BACKGROUND_COMMANDS`: `['start-sleep', 'sleep']` (not auto-backgrounded) +- Windows-native sandbox policy: if enterprise requires sandbox but Windows native, execution blocked + +**Exports:** +```typescript +export type PowerShellToolInput +function detectBlockedSleepPattern(command: string): string | null +``` + +--- + +## 6. Search Tools + +### 6.1 GlobTool + +**Tool name:** `Glob` +**Source:** `src/tools/GlobTool/GlobTool.ts` + +**Characteristics:** +- `isConcurrencySafe: true` +- `isReadOnly: true` +- `searchHint: 'find files by name pattern or wildcard'` +- `isSearchOrReadCommand: { isSearch: true, isRead: false }` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `pattern` | `string` | Yes | Glob pattern (e.g. `"**/*.js"`, `"src/**/*.ts"`) | +| `path` | `string` | No | Directory to search in (defaults to cwd) | + +**Output Schema:** + +```typescript +{ + filenames: string[] // Relative to cwd + durationMs: number + numFiles: number + truncated: boolean // True if results were truncated +} +``` + +**Behavior:** +- Default limit: 100 files (overridable via `context.globLimits?.maxResults`) +- Results are sorted by modification time (most recent first) +- Paths relativized to cwd for compactness + +--- + +### 6.2 GrepTool + +**Tool name:** `Grep` +**Source:** `src/tools/GrepTool/GrepTool.ts` + +**Characteristics:** +- `isConcurrencySafe: true` +- `isReadOnly: true` +- `strict: true` +- `maxResultSizeChars: 20_000` +- `searchHint: 'search file contents with regex (ripgrep)'` +- `isSearchOrReadCommand: { isSearch: true }` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `pattern` | `string` | Yes | Regular expression pattern | +| `path` | `string` | No | File or directory to search | +| `glob` | `string` | No | Glob filter (e.g. `"*.js"`, `"**/*.tsx"`) | +| `output_mode` | `'content' \| 'files_with_matches' \| 'count'` | No (default `'files_with_matches'`) | Output format | +| `-B` | `number` | No | Lines before each match (requires `output_mode: 'content'`) | +| `-A` | `number` | No | Lines after each match (requires `output_mode: 'content'`) | +| `-C` | `number` | No | Lines before and after each match | +| `context` | `number` | No | Alias for `-C` | +| `-n` | `boolean` | No | Show line numbers (requires `output_mode: 'content'`) | +| `-i` | `boolean` | No | Case-insensitive search | +| `type` | `string` | No | File type filter (e.g. `"js"`, `"py"`) | +| `head_limit` | `number` | No (default `250`) | Limit output to first N lines/entries | +| `offset` | `number` | No (default `0`) | Skip first N entries | +| `multiline` | `boolean` | No (default `false`) | Enable multiline matching (`.` matches newlines) | + +**Output Schema:** + +```typescript +{ + mode: 'content' | 'files_with_matches' | 'count' + numFiles: number + filenames: string[] + content?: string // When mode='content' + numLines?: number // When mode='content' + numMatches?: number // When mode='count' + appliedLimit?: number + appliedOffset?: number +} +``` + +**Implementation Details:** +- Backed by `ripgrep` (`rg`) binary +- Excludes VCS directories: `.git`, `.svn`, `.hg`, `.bzr`, `.jj`, `.sl` +- `max-columns: 500` to prevent oversized lines +- Default `head_limit: 250` when unspecified + +--- + +## 7. Agent / Multi-Agent Tools + +### 7.1 AgentTool + +**Tool name:** `Agent` (alias: `Task`) +**Constants:** `AGENT_TOOL_NAME`, `LEGACY_AGENT_TOOL_NAME` +**Source:** `src/tools/AgentTool/AgentTool.tsx` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `description` | `string` | Yes | 3-5 word description of the task | +| `prompt` | `string` | Yes | Full task prompt for the agent | +| `subagent_type` | `string` | No | Type of subagent to spawn | +| `model` | `'sonnet' \| 'opus' \| 'haiku'` | No | Model alias for the agent | +| `run_in_background` | `boolean` | No | Launch as background task | +| `name` | `string` | No | Named agent for messaging | +| `team_name` | `string` | No | Associate with this team | +| `mode` | `string` | No | Permission mode override | +| `isolation` | `'worktree' \| 'remote'` | No | Isolation strategy | +| `cwd` | `string` | No | Working directory (Kairos only) | + +**Output Schema:** + +Synchronous completion: +```typescript +{ + status: 'completed' + result: string +} +``` + +Asynchronous launch: +```typescript +{ + status: 'async_launched' + agentId: string + description: string + prompt: string +} +``` + +**Behavior:** +- Auto-backgrounds after `120_000ms` +- Progress shown after `2_000ms` +- Supports fork subagent (`subagent_type: 'fork'`) +- Multi-agent swarm integration: when inside a team, can spawn named teammates +- `isolation: 'worktree'` creates git worktree for isolated execution +- `isolation: 'remote'` runs in remote session + +--- + +### 7.2 TeamCreateTool + +**Tool name:** `TeamCreate` +**Source:** `src/tools/TeamCreateTool/TeamCreateTool.ts` +**Gate:** `isAgentSwarmsEnabled()` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `team_name` | `string` | Yes | Name for the team | +| `description` | `string` | No | Team description | +| `agent_type` | `string` | No | Default agent type for team members | + +**Output Schema:** + +```typescript +{ + team_name: string + team_file_path: string + lead_agent_id: string +} +``` + +**Behavior:** +- Creates team file at `~/.claude/teams/.json` +- Resets task list to team-scoped task list +- Registers team for session cleanup (auto-cleanup on exit) +- One team per leader enforced: calling again while a team exists returns an error + +--- + +### 7.3 TeamDeleteTool + +**Tool name:** `TeamDelete` +**Source:** `src/tools/TeamDeleteTool/TeamDeleteTool.ts` +**Gate:** `isAgentSwarmsEnabled()` +**Input:** `{}` (empty object) + +**Output Schema:** + +```typescript +{ + success: boolean + message: string + team_name?: string +} +``` + +**Behavior:** +- Refuses to delete if any non-lead members have `isActive !== false` (still running) +- Calls `cleanupTeamDirectories(teamName)` to remove team files and worktrees +- Unregisters team from session cleanup +- Clears teammate color assignments +- Clears leader team name (task list falls back to session ID) +- Clears team context and inbox from app state + +--- + +### 7.4 SendMessageTool + +**Tool name:** `SendMessage` +**Source:** `src/tools/SendMessageTool/SendMessageTool.ts` +**Gate:** `isAgentSwarmsEnabled()` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `to` | `string` | Yes | Recipient: agent name, `'*'` (broadcast), `'uds:'`, or `'bridge:'` | +| `summary` | `string` | No | Short summary of message for UI | +| `message` | `string \| StructuredMessage` | Yes | Message content | + +**StructuredMessage union:** + +```typescript +type StructuredMessage = + | { type: 'shutdown_request'; reason?: string } + | { type: 'shutdown_response'; status: 'ok' | 'error'; message?: string } + | { type: 'plan_approval_response'; approved: boolean; comment?: string; requestId: string } +``` + +**Routing:** +- **In-process agents:** Queues message in agent's inbox or resumes paused agent +- **Mailbox (teammates):** Writes to `~/.claude/mailboxes/.json` +- **UDS socket:** Sends via Unix domain socket (for local inter-process) +- **Bridge (cross-machine):** Routes via Remote Control API; requires user safety check (not auto-approvable) + +**Permission:** Bridge messages require user consent via `decisionReason` safety gate. + +--- + +## 8. Task Management Tools + +### 8.1 TaskStopTool + +**Tool name:** `TaskStop` (alias: `KillShell`) +**Source:** `src/tools/TaskStopTool/TaskStopTool.ts` +**Characteristics:** `shouldDefer: true`, `isConcurrencySafe: true` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `task_id` | `string` | No | Task ID to stop (from background task launch) | +| `shell_id` | `string` | No | Deprecated alias for `task_id` | + +**Output Schema:** + +```typescript +{ + message: string + task_id: string + task_type: TaskType + command?: string // For bash tasks +} +``` + +**Validation:** +- Task must exist in app state +- Task must be in a running (non-terminal) state + +--- + +### 8.2 TaskOutputTool + +**Tool name:** `TaskOutput` (`TASK_OUTPUT_TOOL_NAME`) +**Source:** `src/tools/TaskOutputTool/TaskOutputTool.tsx` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `task_id` | `string` | Yes | Task ID to read output from | +| `block` | `boolean` | No (default `true`) | Block until task completes | +| `timeout` | `number` | No (default `30_000`, range `0–600_000ms`) | Maximum wait time in ms | + +**Output Schema:** + +```typescript +{ + retrieval_status: 'success' | 'timeout' | 'not_ready' + task: TaskOutput | null +} + +type TaskOutput = { + task_id: string + task_type: TaskType + status: TaskStatus + description: string + output: string + exitCode?: number + error?: string + prompt?: string // For agent tasks + result?: string // Final result text for agent tasks +} +``` + +--- + +### 8.3 TodoWriteTool (V1) + +**Tool name:** `TodoWrite` +**Source:** `src/tools/TodoWriteTool/TodoWriteTool.ts` +**Characteristics:** `strict: true`, `shouldDefer: true`, `maxResultSizeChars: 100_000` +**Gate:** Disabled when `isTodoV2Enabled()` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `todos` | `TodoItem[]` | Yes | Full replacement list of todo items | + +`TodoItem` schema: +```typescript +{ + id: string + content: string + status: 'pending' | 'in_progress' | 'completed' + priority: 'high' | 'medium' | 'low' +} +``` + +**Output Schema:** + +```typescript +{ + oldTodos: TodoItem[] + newTodos: TodoItem[] + verificationNudgeNeeded?: boolean +} +``` + +**Behavior:** +- Replaces the entire todo list atomically +- Clears list automatically when all todos are completed +- `verificationNudgeNeeded`: signals UI to nudge model to verify completed items + +--- + +### 8.4 TaskCreateTool (V2) + +**Tool name:** `TaskCreate` +**Source:** `src/tools/TaskCreateTool/TaskCreateTool.ts` +**Gate:** `isTodoV2Enabled()` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `subject` | `string` | Yes | Short task title | +| `description` | `string` | Yes | Detailed task description | +| `activeForm` | `object` | No | Form data for active task | +| `metadata` | `object` | No | Arbitrary metadata | + +**Output Schema:** + +```typescript +{ + task: { + id: string + subject: string + } +} +``` + +**Behavior:** +- Runs `executeTaskCreatedHooks` after creation +- Auto-expands task panel in UI +- Deletes task if hook throws an error + +--- + +### 8.5 TaskGetTool (V2) + +**Tool name:** `TaskGet` +**Source:** `src/tools/TaskGetTool/TaskGetTool.ts` +**Gate:** `isTodoV2Enabled()` +**Characteristics:** `shouldDefer: true`, `isReadOnly: true` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `taskId` | `string` | Yes | Task ID to retrieve | + +**Output Schema:** + +```typescript +{ + task: { + id: string + subject: string + description: string + status: TaskStatus + blocks: string[] // Task IDs this task blocks + blockedBy: string[] // Task IDs blocking this task + } | null +} +``` + +--- + +### 8.6 TaskUpdateTool (V2) + +**Tool name:** `TaskUpdate` +**Source:** `src/tools/TaskUpdateTool/TaskUpdateTool.ts` +**Gate:** `isTodoV2Enabled()` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `taskId` | `string` | Yes | Task ID to update | +| `subject` | `string` | No | Updated title | +| `description` | `string` | No | Updated description | +| `activeForm` | `object` | No | Updated form data | +| `status` | `TaskStatus \| 'deleted'` | No | New status; `'deleted'` removes the task | +| `addBlocks` | `string[]` | No | Task IDs this task should block | +| `addBlockedBy` | `string[]` | No | Task IDs that block this task | +| `owner` | `string` | No | Assign ownership | +| `metadata` | `object` | No | Updated metadata | + +**Output Schema:** + +```typescript +{ + success: boolean + taskId: string + updatedFields: string[] + error?: string + statusChange?: { from: TaskStatus; to: TaskStatus | 'deleted' } + verificationNudgeNeeded?: boolean +} +``` + +**Behavior:** +- Runs `executeTaskCompletedHooks` when status transitions to `completed` +- Auto-sets owner to calling agent on `in_progress` status +- Writes mailbox notification on owner change +- `verificationNudgeNeeded`: set when 3+ tasks completed without a verification step + +--- + +### 8.7 TaskListTool (V2) + +**Tool name:** `TaskList` +**Source:** `src/tools/TaskListTool/TaskListTool.ts` +**Gate:** `isTodoV2Enabled()` +**Characteristics:** `shouldDefer: true`, `isReadOnly: true` +**Input:** `{}` (empty object) + +**Output Schema:** + +```typescript +{ + tasks: Array<{ + id: string + subject: string + status: TaskStatus + owner?: string + blockedBy: string[] // Only non-completed blocking tasks + }> +} +``` + +**Behavior:** +- Filters out tasks with `_internal` metadata flag +- Filters already-completed IDs from `blockedBy` lists + +--- + +## 9. Web Tools + +### 9.1 WebFetchTool + +**Tool name:** `WebFetch` +**Source:** `src/tools/WebFetchTool/WebFetchTool.ts` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `url` | `string` (URL) | Yes | URL to fetch | +| `prompt` | `string` | Yes | Instruction for summarizing the fetched content | + +**Output Schema:** + +```typescript +{ + bytes: number + code: number // HTTP status code + codeText: string + result: string // Processed/summarized content + durationMs: number + url: string +} +``` + +**Permission:** Per-hostname rules. Preapproved hosts bypass prompt. Rule format: `domain:hostname`. + +**Implementation:** +- Converts HTML to Markdown before processing +- Applies Haiku model summarization via `applyPromptToMarkdown(prompt, markdown)` when content exceeds threshold +- Respects `abortController` signal + +--- + +### 9.2 WebSearchTool + +**Tool name:** `WebSearch` +**Source:** `src/tools/WebSearchTool/WebSearchTool.ts` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `query` | `string` (min 2 chars) | Yes | Search query | +| `allowed_domains` | `string[]` | No | Restrict results to these domains | +| `blocked_domains` | `string[]` | No | Exclude results from these domains | + +**Output Schema:** + +```typescript +{ + query: string + results: SearchResult[] | string // String when no results or commentary + durationSeconds: number +} + +type SearchResult = { + title: string + url: string + snippet: string +} +``` + +**Permission:** `'passthrough'` — always prompts user + +**Implementation:** +- Uses beta tool: `web_search_20250305` +- Maximum 8 search operations per call +- Enabled only for `firstParty`, `vertex`, and `foundry` API providers + +--- + +## 10. MCP Integration Tools + +### 10.1 MCPTool + +**Tool name:** `mcp` (overridden per server to `mcp____`) +**Source:** `src/tools/MCPTool/MCPTool.ts` + +**Characteristics:** +- `isMcp: true` +- `maxResultSizeChars: 100_000` +- `permission: 'passthrough'` (always asks) +- All methods are overridden in `mcpClient.ts` when instantiated per server + +**Input Schema:** `z.object({}).passthrough()` — accepts any object +**Output:** `string` + +**Exports:** +```typescript +type MCPProgress // Re-exported from mcp progress types +``` + +--- + +### 10.2 McpAuthTool + +**Tool name:** `mcp____authenticate` +**Source:** `src/tools/McpAuthTool/McpAuthTool.ts` + +A pseudo-tool factory, not a standard `buildTool()` instance. + +```typescript +function createMcpAuthTool( + serverName: string, + config: ScopedMcpServerConfig, +): Tool +``` + +**Input:** `{}` (empty) + +**Output Schema:** + +```typescript +type McpAuthOutput = { + status: 'auth_url' | 'unsupported' | 'error' + message: string + authUrl?: string // Present when status='auth_url' +} +``` + +**Behavior:** +- Created for MCP servers that are installed but need OAuth authentication +- Starts `performMCPOAuthFlow()` with `skipBrowserOpen: true` +- Returns authorization URL for user to open in browser +- Background continuation: when OAuth completes, calls `reconnectMcpServerImpl()` and swaps real tools into app state via prefix-based replacement +- `claudeai-proxy` transport: returns `'unsupported'` and directs user to `/mcp` +- Silent auth (cached IdP token): returns success message without URL + +--- + +### 10.3 ListMcpResourcesTool + +**Tool name:** `mcp__listResources` (`LIST_MCP_RESOURCES_TOOL_NAME`) +**Source:** `src/tools/ListMcpResourcesTool/ListMcpResourcesTool.ts` +**Characteristics:** `shouldDefer: true` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `server` | `string` | No | Filter by server name | + +**Output Schema:** + +```typescript +Array<{ + uri: string + name: string + mimeType?: string + description?: string + server: string +}> +``` + +**Note:** Not included in `getTools()` directly; only added when MCP servers with resources are present. + +--- + +### 10.4 ReadMcpResourceTool + +**Tool name:** `ReadMcpResourceTool` +**Source:** `src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts` +**Characteristics:** `shouldDefer: true` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `server` | `string` | Yes | MCP server name | +| `uri` | `string` | Yes | Resource URI to read | + +**Output Schema:** + +```typescript +{ + contents: Array<{ + uri: string + mimeType?: string + text?: string + blobSavedTo?: string // Path when binary blob saved to disk + }> +} +``` + +**Implementation:** +- Binary blobs are saved to disk; `getBinaryBlobSavedMessage()` returns path reference string + +--- + +## 11. Plan Mode Tools + +### 11.1 EnterPlanModeTool + +**Tool name:** `EnterPlanMode` +**Source:** `src/tools/EnterPlanModeTool/EnterPlanModeTool.ts` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000`, `isConcurrencySafe: true`, `isReadOnly: true` + +**Input:** `{}` (empty object) + +**Output Schema:** + +```typescript +{ + message: string // Confirmation message +} +``` + +**Behavior:** +- Sets permission mode to `'plan'` (read-only planning mode) +- Disabled with `--channels` flag +- Cannot be called from agent (teammate) context; only from main agent +- Saves current permission mode as `prePlanMode` for restoration by `ExitPlanMode` + +--- + +### 11.2 ExitPlanModeV2Tool + +**Tool name:** `ExitPlanMode` (`EXIT_PLAN_MODE_V2_TOOL_NAME`) +**Source:** `src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000` + +**Input Schema (model-facing):** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `allowedPrompts` | `AllowedPrompt[]` | No | Pre-approved tool calls (passthrough schema) | + +`AllowedPrompt`: +```typescript +{ + tool: 'Bash' + prompt: string +} +``` + +**SDK Input Schema** (adds): + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `plan` | `string` | No | Plan text for SDK mode | +| `planFilePath` | `string` | No | Path to plan file for SDK mode | + +**Output Schema:** + +```typescript +type Output = { + plan: string + isAgent: boolean + filePath?: string + hasTaskTool?: boolean + planWasEdited?: boolean + awaitingLeaderApproval?: boolean + requestId?: string +} +``` + +**Behavior:** +- For teammates with `isPlanModeRequired()`: sends `plan_approval_request` message to team-lead's mailbox; returns `awaitingLeaderApproval: true` with a `requestId` +- For non-teammates: restores permission mode to `prePlanMode` (typically `default` or `auto`) +- Disabled with `--channels` flag + +**Exports:** +```typescript +type AllowedPrompt +const _sdkInputSchema // Extended schema with plan/planFilePath +type Output +``` + +--- + +## 12. Notebook Tool + +### NotebookEditTool + +**Tool name:** `NotebookEdit` +**Source:** `src/tools/NotebookEditTool/NotebookEditTool.ts` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `notebook_path` | `string` | Yes | Absolute path to `.ipynb` file | +| `cell_id` | `string` | No | Target cell ID (required for edit/delete; omit for new cell) | +| `new_source` | `string` | Yes | New source content for the cell | +| `cell_type` | `'code' \| 'markdown'` | No | Cell type (for new cells) | +| `edit_mode` | `'replace' \| 'insert' \| 'delete'` | No (default `'replace'`) | Edit operation | + +**Output Schema:** + +```typescript +{ + new_source: string + cell_id?: string + cell_type: 'code' | 'markdown' + language: string + edit_mode: 'replace' | 'insert' | 'delete' + error?: string + notebook_path: string + original_file: string // Full notebook JSON before edit + updated_file: string // Full notebook JSON after edit +} +``` + +**Validation / Safety:** +- Read-before-write required (same as FileEditTool/FileWriteTool) +- mtime staleness check +- UNC path security skip +- Clears `execution_count` and `outputs` on cell replace (avoids stale output display) + +--- + +## 13. Worktree Tools + +### 13.1 EnterWorktreeTool + +**Tool name:** `EnterWorktree` (`ENTER_WORKTREE_TOOL_NAME`) +**Source:** `src/tools/EnterWorktreeTool/EnterWorktreeTool.ts` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `name` | `string` | No | Optional name for the worktree. Each `/`-separated segment: letters, digits, dots, underscores, dashes only; max 64 chars total. Random name generated if not provided. | + +**Output Schema:** + +```typescript +{ + worktreePath: string + worktreeBranch?: string + message: string +} +``` + +**Behavior:** +- Validates not already in a worktree session created by this session +- Resolves to main repo root before creating worktree +- Calls `createWorktreeForSession(sessionId, slug)` to create git worktree (or hooks-based worktree) +- Updates `cwd`, `originalCwd` to worktree path +- Clears system prompt sections cache (so `env_info_simple` recomputes with worktree context) +- Clears memoized caches that depend on cwd +- Logs `tengu_worktree_created` analytics event + +--- + +### 13.2 ExitWorktreeTool + +**Tool name:** `ExitWorktree` (`EXIT_WORKTREE_TOOL_NAME`) +**Source:** `src/tools/ExitWorktreeTool/ExitWorktreeTool.ts` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `action` | `'keep' \| 'remove'` | Yes | `'keep'` preserves worktree and branch on disk; `'remove'` deletes both | +| `discard_changes` | `boolean` | No | Required `true` when `action='remove'` and worktree has uncommitted files or unmerged commits | + +**Output Schema:** + +```typescript +{ + action: 'keep' | 'remove' + originalCwd: string + worktreePath: string + worktreeBranch?: string + tmuxSessionName?: string + discardedFiles?: number // Set when action='remove' + discardedCommits?: number // Set when action='remove' + message: string +} +``` + +**Validation:** +- Only operates on worktrees created by `EnterWorktreeTool` in the current session (scope guard via `getCurrentWorktreeSession()`) +- When `action='remove'` without `discard_changes: true`: + - Runs `countWorktreeChanges()`: uses `git status --porcelain` + `git rev-list --count ..HEAD` + - Returns error listing uncommitted files and unmerged commits + - Returns error if git state cannot be determined (fail-closed) + +**Behavior:** +- `action='keep'`: calls `keepWorktree()`, restores session to original cwd, preserves worktree for later use +- `action='remove'`: kills tmux session if any, calls `cleanupWorktree()` (removes worktree and branch), restores session +- Both actions: restore `cwd`, `originalCwd`, optionally `projectRoot`, clear caches +- Logs `tengu_worktree_kept` or `tengu_worktree_removed` analytics events + +--- + +## 14. Scheduling Tools + +These tools are gated by `isKairosCronEnabled()` (requires `feature('KAIROS')` + GB gate). + +### 14.1 CronCreateTool + +**Tool name:** `CronCreate` (`CRON_CREATE_TOOL_NAME`) +**Source:** `src/tools/ScheduleCronTool/CronCreateTool.ts` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `cron` | `string` | Yes | Standard 5-field cron expression in local time: `"M H DoM Mon DoW"` | +| `prompt` | `string` | Yes | Prompt to enqueue at each fire time | +| `recurring` | `boolean` | No (default `true`) | `true` = fire on every cron match (auto-expires after `DEFAULT_MAX_AGE_DAYS` days); `false` = fire once then auto-delete | +| `durable` | `boolean` | No (default `false`) | `true` = persist to `.claude/scheduled_tasks.json` and survive restarts; `false` = in-memory only, dies when session ends | + +**Output Schema:** + +```typescript +{ + id: string // Job ID for reference in CronDelete/CronList + humanSchedule: string // Human-readable schedule (e.g. "Every 5 minutes") + recurring: boolean + durable?: boolean +} +``` + +**Validation:** +- Valid 5-field cron expression required +- Expression must match at least one calendar date within the next year +- Maximum 50 concurrent scheduled jobs +- Durable crons not supported for teammates (teammates don't persist across sessions) + +**Constants:** +- `MAX_JOBS: 50` +- `DEFAULT_MAX_AGE_DAYS`: defined in prompt.ts + +--- + +### 14.2 CronDeleteTool + +**Tool name:** `CronDelete` (`CRON_DELETE_TOOL_NAME`) +**Source:** `src/tools/ScheduleCronTool/CronDeleteTool.ts` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | `string` | Yes | Job ID returned by `CronCreate` | + +**Output Schema:** + +```typescript +{ + id: string // Cancelled job ID +} +``` + +**Validation:** +- Job with given ID must exist +- Teammates may only delete their own cron jobs (ownership enforced by `agentId`) + +--- + +### 14.3 CronListTool + +**Tool name:** `CronList` (`CRON_LIST_TOOL_NAME`) +**Source:** `src/tools/ScheduleCronTool/CronListTool.ts` +**Characteristics:** `shouldDefer: true`, `isConcurrencySafe: true`, `isReadOnly: true`, `maxResultSizeChars: 100_000` +**Input:** `{}` (empty object) + +**Output Schema:** + +```typescript +{ + jobs: Array<{ + id: string + cron: string + humanSchedule: string + prompt: string + recurring?: boolean + durable?: boolean + }> +} +``` + +**Behavior:** +- Teammates only see their own cron jobs (filtered by `agentId`) +- Team lead (no teammate context) sees all jobs + +--- + +## 15. Meta / Discovery Tools + +### 15.1 ToolSearchTool + +**Tool name:** `ToolSearch` (`TOOL_SEARCH_TOOL_NAME`) +**Source:** `src/tools/ToolSearchTool/ToolSearchTool.ts` +**Characteristics:** `maxResultSizeChars: 100_000`, `isConcurrencySafe: true`, `isReadOnly: true` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `query` | `string` | Yes | `select:` for exact tool lookup by name, or keywords for fuzzy search | +| `max_results` | `number` | No (default `5`) | Maximum number of tools to return | + +**Output Schema:** + +```typescript +{ + matches: string[] // Tool names that matched + query: string + total_deferred_tools: number + pending_mcp_servers?: string[] // MCP servers still loading +} +``` + +**Scoring algorithm:** + +| Match type | Built-in score | MCP score | +|------------|---------------|-----------| +| Exact name part match | 10 | 12 | +| Substring in name | 5 | 6 | +| Word boundary in `searchHint` | 4 | — | +| Match in description | 2 | — | + +**Behavior:** +- `select:` prefix: exact name lookup, fetches full schema definition +- Keywords: fuzzy scoring across all deferred tools +- `mapToolResultToToolResultBlockParam`: returns `tool_reference` blocks for matched tools (injects their schemas into context) + +**Exports:** +```typescript +function clearToolSearchDescriptionCache(): void +``` + +--- + +### 15.2 AskUserQuestionTool + +**Tool name:** `AskUserQuestion` (`ASK_USER_QUESTION_TOOL_NAME`) +**Source:** `src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000`, `requiresUserInteraction: true` + +**Input Schema (model-facing):** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `questions` | `Question[]` | Yes | 1–4 questions to ask the user | +| `answers` | `object` | No | Injected by UI after user responds (not in model-facing schema) | +| `annotations` | `object` | No | Metadata annotations | +| `metadata` | `object` | No | Arbitrary metadata | + +`Question` schema: +```typescript +{ + question: string + header?: string + options: QuestionOption[] // 2-4 options + multiSelect?: boolean +} + +type QuestionOption = { + label: string + value: string + description?: string +} +``` + +**Output Schema:** + +```typescript +{ + questions: Question[] + answers: Record // question → answer(s) + annotations?: object +} +``` + +**Behavior:** +- Disabled when `--channels` flag is active (no terminal for dialog) +- UI renders interactive question dialog; `answers` field is injected by UI layer +- Supports single-select and multi-select question types + +**Exports:** +```typescript +const _sdkInputSchema // Full input schema including answers field +const _sdkOutputSchema // Full output schema +type Question +type QuestionOption +``` + +--- + +## 16. Kairos / Special Mode Tools + +### 16.1 BriefTool (SendUserMessage) + +**Tool name:** `SendUserMessage` (`BRIEF_TOOL_NAME`, alias: `LEGACY_BRIEF_TOOL_NAME`) +**Source:** `src/tools/BriefTool/BriefTool.ts` +**Gate:** `isBriefEnabled()` — requires `feature('KAIROS')` or `feature('KAIROS_BRIEF')` + Growthbook gate + `userMsgOptIn` or `kairosActive` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `message` | `string` (markdown) | Yes | Message to send to the user | +| `attachments` | `string[]` | No | File paths to attach | +| `status` | `'normal' \| 'proactive'` | Yes | Message disposition: `'proactive'` for unsolicited updates | + +**Output Schema:** + +```typescript +type Output = { + message: string + attachments?: Array<{ + path: string + size: number + isImage: boolean + file_uuid: string + }> + sentAt?: string // ISO timestamp +} +``` + +**Exports:** +```typescript +function isBriefEntitled(): boolean // Has the entitlement +function isBriefEnabled(): boolean // Has entitlement AND feature flags on +type Output +``` + +--- + +### 16.2 SleepTool + +**Tool name:** `Sleep` +**Source:** `src/tools/SleepTool/` (only `prompt.ts` present; tool implementation loaded via `require()` when `feature('SLEEP_TOOL')`) +**Gate:** `feature('SLEEP_TOOL')` feature flag + +**Purpose:** Wait for a specified duration without holding a shell process. Can be interrupted by the user. + +**Prompt highlights:** +- Use when user says to sleep/rest, when waiting for something, or when nothing to do +- Can be called concurrently with other tools +- Prefer over `Bash(sleep ...)` — doesn't hold a shell process +- Receives periodic `` prompts during sleep; check for useful work before sleeping +- Each wake-up costs an API call; prompt cache expires after 5 minutes of inactivity + +--- + +### 16.3 RemoteTriggerTool + +**Tool name:** `RemoteTrigger` (`REMOTE_TRIGGER_TOOL_NAME`) +**Source:** `src/tools/RemoteTriggerTool/RemoteTriggerTool.ts` +**Gate:** `feature('AGENT_TRIGGERS')` + `getFeatureValue_CACHED_MAY_BE_STALE('tengu_surreal_dali', false)` + `isPolicyAllowed('allow_remote_sessions')` +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000`, `isConcurrencySafe: true` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `action` | `'list' \| 'get' \| 'create' \| 'update' \| 'run'` | Yes | CRUD operation on triggers | +| `trigger_id` | `string` | No | Required for `get`, `update`, `run` (regex: `[\w-]+`) | +| `body` | `Record` | No | JSON body for `create` and `update` | + +**Output Schema:** + +```typescript +{ + status: number // HTTP status code + json: string // Serialized response body +} +``` + +**Implementation:** +- Calls `${BASE_API_URL}/v1/code/triggers` REST API +- Uses OAuth token (`checkAndRefreshOAuthTokenIfNeeded()` + `getClaudeAIOAuthTokens()`) +- Requires org UUID (`getOrganizationUUID()`) +- API beta header: `ccr-triggers-2026-01-30` +- Timeout: `20_000ms` +- `isReadOnly`: `true` for `list` and `get` actions; `false` otherwise + +--- + +## 17. SDK / Output Tools + +### 17.1 SyntheticOutputTool (StructuredOutput) + +**Tool name:** `StructuredOutput` +**Source:** `src/tools/SyntheticOutputTool/SyntheticOutputTool.ts` +**Gate:** Enabled only in non-interactive sessions (SDK / `--output-format json` mode) + +**Input:** Passthrough — any object, validated against the provided JSON schema via AJV +**Output:** `'Structured output provided successfully'` (string) + +**Factory:** + +```typescript +function createSyntheticOutputTool(jsonSchema: object): Tool +``` + +- Creates a validated instance with `WeakMap` caching (same schema object returns same tool) +- Used in SDK/`--output-format json` mode to force the model to emit a structured final response +- AJV validation ensures output matches the caller-provided JSON schema + +--- + +## 18. Skill Tool + +**Tool name:** `Skill` (`SKILL_TOOL_NAME`) +**Source:** `src/tools/SkillTool/SkillTool.ts` +**Characteristics:** `maxResultSizeChars: 100_000` + +**Purpose:** Runs prompt commands (skills) defined in local files or MCP skill servers. + +**Exports:** +```typescript +type Progress // Re-export of SkillToolProgress +``` + +--- + +## 19. LSP Tool + +**Tool name:** `LSP` (`LSP_TOOL_NAME`) +**Source:** `src/tools/LSPTool/LSPTool.ts` +**Gate:** `ENABLE_LSP_TOOL=true` environment variable +**Characteristics:** `isLsp: true` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `operation` | `string` | Yes | One of: `goToDefinition`, `findReferences`, `hover`, `documentSymbol`, `workspaceSymbol`, `goToImplementation`, `prepareCallHierarchy`, `incomingCalls`, `outgoingCalls` | +| `filePath` | `string` | Yes | Absolute path to file | +| `line` | `number` | Yes | 1-based line number | +| `character` | `number` | Yes | 1-based character position | + +**Constraints:** +- Max file size: 10 MB + +--- + +## 20. REPL Tool + +**Tool name:** `REPL` +**Source:** `src/tools/REPLTool/` +**Gate:** Ant-only (`USER_TYPE === 'ant'`) + loaded via `require()` + +**Constants (`src/tools/REPLTool/constants.ts`):** + +```typescript +const REPL_TOOL_NAME = 'REPL' + +function isReplModeEnabled(): boolean +// true when: CLAUDE_CODE_REPL not falsy AND (CLAUDE_REPL_MODE=1 OR (USER_TYPE='ant' AND CLAUDE_CODE_ENTRYPOINT='cli')) +// SDK entrypoints default to REPL mode OFF + +const REPL_ONLY_TOOLS = new Set([ + 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'NotebookEdit', 'Agent', +]) +// Hidden from model in REPL mode; model must use REPL for batch operations +``` + +**Primitive Tools (`src/tools/REPLTool/primitiveTools.ts`):** + +```typescript +function getReplPrimitiveTools(): readonly Tool[] +// Returns: [FileReadTool, FileWriteTool, FileEditTool, GlobTool, GrepTool, BashTool, NotebookEditTool, AgentTool] +// Lazy getter to avoid TDZ circular dependency +// These tools remain accessible inside REPL VM context even when hidden from model +``` + +--- + +## 21. Config Tool + +**Tool name:** `Config` (`CONFIG_TOOL_NAME`) +**Source:** `src/tools/ConfigTool/ConfigTool.ts` +**Gate:** Ant-only +**Characteristics:** `shouldDefer: true`, `maxResultSizeChars: 100_000` + +**Input Schema:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `setting` | `string` | Yes | Configuration key (e.g. `"theme"`, `"model"`) | +| `value` | `string \| boolean \| number` | No | New value (omit for GET operation) | + +**Output Schema:** + +```typescript +{ + success: boolean + operation?: 'get' | 'set' + setting?: string + value?: unknown + previousValue?: unknown + newValue?: unknown + error?: string +} +``` + +**Permission:** +- GET operations: auto-allow +- SET operations: requires user permission prompt + +**Sources:** Settings can come from `'global'` config or `'settings'` config. + +**Exports:** +```typescript +type Input +type Output +``` + +--- + +## 22. Shared Utilities + +### 22.1 tools/utils.ts + +```typescript +/** + * Tags user messages with a sourceToolUseID so they stay transient + * until the tool resolves. Prevents "is running" message duplication in UI. + */ +function tagMessagesWithToolUseID( + messages: (UserMessage | AttachmentMessage | SystemMessage)[], + toolUseID: string | undefined, +): (UserMessage | AttachmentMessage | SystemMessage)[] + +/** + * Extracts the tool use ID from a parent message for a given tool name. + */ +function getToolUseIDFromParentMessage( + parentMessage: AssistantMessage, + toolName: string, +): string | undefined +``` + +--- + +### 22.2 tools/shared/gitOperationTracking.ts + +Shell-agnostic git operation tracking for usage metrics. Works identically for BashTool and PowerShellTool. + +**Exported Types:** + +```typescript +type CommitKind = 'committed' | 'amended' | 'cherry-picked' +type BranchAction = 'merged' | 'rebased' +type PrAction = 'created' | 'edited' | 'merged' | 'commented' | 'closed' | 'ready' +``` + +**Key Functions:** + +```typescript +/** + * Scan command + output for git operations worth surfacing in UI summary. + * Detects: git commit, git push, git merge, git rebase, gh pr *, glab mr create, curl PR APIs. + */ +function detectGitOperation( + command: string, + output: string, +): { + commit?: { sha: string; kind: CommitKind } + push?: { branch: string } + branch?: { ref: string; action: BranchAction } + pr?: { number: number; url?: string; action: PrAction } +} + +/** + * Fire analytics events and OTLP counters for git operations. + * Called after each Bash/PowerShell command completes (exit code 0 only). + */ +function trackGitOperations( + command: string, + exitCode: number, + stdout?: string, +): void + +// Exported for testing +function parseGitCommitId(stdout: string): string | undefined +``` + +**Detected operations:** +- `git commit` → `tengu_git_operation{operation: 'commit'}`, increments commit OTLP counter +- `git commit --amend` → additionally fires `tengu_git_operation{operation: 'commit_amend'}` +- `git push` → `tengu_git_operation{operation: 'push'}` +- `gh pr create/edit/merge/comment/close/ready` → `tengu_git_operation{operation: 'pr_'}`, creates fires PR OTLP counter + links session to PR URL +- `glab mr create` → `tengu_git_operation{operation: 'pr_create'}`, increments PR OTLP counter +- `curl POST` to PR endpoints → `tengu_git_operation{operation: 'pr_create'}` + +**Git command regex:** Tolerates global options between `git` and subcommand (e.g. `git -c commit.gpgsign=false commit`). + +--- + +### 22.3 tools/shared/spawnMultiAgent.ts + +Shared module for teammate/subagent creation, extracted from TeammateTool for reuse by AgentTool. + +**Key functions:** + +```typescript +// Internal helper +function getDefaultTeammateModel(leaderModel: string | null): string +// Checks globalConfig.teammateDefaultModel; null → follow leader; undefined → use hardcoded fallback +``` + +**Backend types:** +- `in-process`: Spawns teammate as in-process coroutine (no external process) +- Tmux-based pane: Spawns in new tmux pane within swarm session +- External process backends + +**Detection:** +- `detectAndGetBackend()`: Probes available backends +- `isInProcessEnabled()`: Checks if in-process spawning is available +- `isTmuxAvailable()`: Checks tmux availability for pane backend + +**Environment inheritance:** +- `buildInheritedEnvVars()`: Builds environment variable set for spawned teammate +- Key env vars propagated: `TEAMMATE_COMMAND_ENV_VAR`, model overrides, plugin paths, etc. + +--- + +## 23. Testing Utilities + +### TestingPermissionTool + +**Tool name:** `TestingPermission` +**Source:** `src/tools/testing/TestingPermissionTool.tsx` +**Gate:** Only enabled when `NODE_ENV === 'test'` (hardcoded: `"production" === 'test'` → always disabled in production) + +```typescript +export const TestingPermissionTool: Tool +``` + +**Input:** `{}` (empty object) +**Output:** `'TestingPermission executed successfully'` + +**Behavior:** +- Always returns `{ behavior: 'ask', message: 'Run test?' }` from `checkPermissions()` +- Used for end-to-end permission dialog testing +- All render functions return `null` +- `isConcurrencySafe: true`, `isReadOnly: true` +- Never appears in production tool list (disabled at build time) + +--- + +## Appendix: Tool Name Constants + +| Constant | Value | Source | +|----------|-------|--------| +| `BASH_TOOL_NAME` | `'Bash'` | `BashTool/toolName.ts` | +| `FILE_READ_TOOL_NAME` | `'Read'` | `FileReadTool/prompt.ts` | +| `FILE_WRITE_TOOL_NAME` | `'Write'` | `FileWriteTool/prompt.ts` | +| `FILE_EDIT_TOOL_NAME` | `'Edit'` | `FileEditTool/constants.ts` | +| `GLOB_TOOL_NAME` | `'Glob'` | `GlobTool/prompt.ts` | +| `GREP_TOOL_NAME` | `'Grep'` | `GrepTool/prompt.ts` | +| `AGENT_TOOL_NAME` | `'Agent'` | `AgentTool/constants.ts` | +| `LEGACY_AGENT_TOOL_NAME` | `'Task'` | `AgentTool/constants.ts` | +| `NOTEBOOK_EDIT_TOOL_NAME` | `'NotebookEdit'` | `NotebookEditTool/constants.ts` | +| `TASK_OUTPUT_TOOL_NAME` | `'TaskOutput'` | `TaskOutputTool/` | +| `ASK_USER_QUESTION_TOOL_NAME` | `'AskUserQuestion'` | `AskUserQuestionTool/` | +| `SKILL_TOOL_NAME` | `'Skill'` | `SkillTool/` | +| `TOOL_SEARCH_TOOL_NAME` | `'ToolSearch'` | `ToolSearchTool/` | +| `CONFIG_TOOL_NAME` | `'Config'` | `ConfigTool/` | +| `BRIEF_TOOL_NAME` | `'SendUserMessage'` | `BriefTool/` | +| `SLEEP_TOOL_NAME` | `'Sleep'` | `SleepTool/prompt.ts` | +| `REMOTE_TRIGGER_TOOL_NAME` | (from `prompt.ts`) | `RemoteTriggerTool/prompt.ts` | +| `ENTER_WORKTREE_TOOL_NAME` | (from `constants.ts`) | `EnterWorktreeTool/constants.ts` | +| `EXIT_WORKTREE_TOOL_NAME` | (from `constants.ts`) | `ExitWorktreeTool/constants.ts` | +| `TEAM_DELETE_TOOL_NAME` | (from `constants.ts`) | `TeamDeleteTool/constants.ts` | +| `CRON_CREATE_TOOL_NAME` | (from `prompt.ts`) | `ScheduleCronTool/prompt.ts` | +| `CRON_DELETE_TOOL_NAME` | (from `prompt.ts`) | `ScheduleCronTool/prompt.ts` | +| `CRON_LIST_TOOL_NAME` | (from `prompt.ts`) | `ScheduleCronTool/prompt.ts` | +| `EXIT_PLAN_MODE_V2_TOOL_NAME` | `'ExitPlanMode'` | `ExitPlanModeTool/` | +| `LIST_MCP_RESOURCES_TOOL_NAME` | `'mcp__listResources'` | `ListMcpResourcesTool/` | +| `POWERSHELL_TOOL_NAME` | `'PowerShell'` | `PowerShellTool/toolName.ts` | +| `REPL_TOOL_NAME` | `'REPL'` | `REPLTool/constants.ts` | +| `LSP_TOOL_NAME` | `'LSP'` | `LSPTool/` | + +--- + +## Appendix: Tool Feature Gates Summary + +| Tool | Gate / Condition | +|------|-----------------| +| `ConfigTool`, `REPLTool` | `USER_TYPE === 'ant'` | +| `CronCreate/Delete/List` | `feature('KAIROS')` + `isKairosCronEnabled()` GB gate | +| `SleepTool` | `feature('SLEEP_TOOL')` | +| `RemoteTriggerTool` | `feature('AGENT_TRIGGERS')` + `tengu_surreal_dali` GB flag + `allow_remote_sessions` policy | +| `BriefTool` | `feature('KAIROS')` or `feature('KAIROS_BRIEF')` + GB gate + userMsgOptIn or kairosActive | +| `TeamCreate/Delete`, `SendMessage` | `isAgentSwarmsEnabled()` | +| `TaskCreate/Get/Update/List` | `isTodoV2Enabled()` | +| `TodoWriteTool` | `!isTodoV2Enabled()` | +| `LSPTool` | `ENABLE_LSP_TOOL=true` env var | +| `TestingPermissionTool` | `NODE_ENV === 'test'` (always disabled in production) | +| `MonitorMcpTask` | `feature('MONITOR_TOOL')` | +| `LocalWorkflowTask` | `feature('WORKFLOW_SCRIPTS')` | + +--- + +## Appendix: deferred vs. alwaysLoad + +Tools with `shouldDefer: true` are hidden from the initial prompt context to save tokens. The model discovers them via `ToolSearchTool` using keyword search or `select:` queries. `ToolSearchTool` injects `tool_reference` blocks for matched tools, which causes the tool schemas to be loaded into context. + +Tools with `alwaysLoad: true` are always included even in contexts where deferral is the default. + +Tools without either flag are included in the initial prompt by default. diff --git a/spec/04_components_core_messages.md b/spec/04_components_core_messages.md new file mode 100644 index 0000000..9db4870 --- /dev/null +++ b/spec/04_components_core_messages.md @@ -0,0 +1,2586 @@ +# Claude Code — Components: Core & Messages + +This document covers every component in `src/components/` (top-level files) and `src/components/messages/` (including `UserToolResultMessage/` sub-directory). + +--- + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [Top-Level Components](#top-level-components) +3. [messages/ Subdirectory](#messages-subdirectory) +4. [messages/UserToolResultMessage/ Subdirectory](#messagesusertooltoolresultmessage-subdirectory) + +--- + +## Architecture Overview + +All components are compiled with the **React Compiler** (`react/compiler-runtime`). The `_c(N)` cache allocator and `Symbol.for("react.memo_cache_sentinel")` guard pattern appear in virtually every component — this is automatic memoization, not hand-written. + +The UI framework is **Ink** (terminal React renderer). Common Ink primitives used across all components: + +- `Box`, `Text` — layout and text +- `useInput`, `useTheme`, `useTerminalFocus`, `useAnimationFrame`, `useTheme` — Ink hooks +- `Ansi`, `RawAnsi`, `NoSelect`, `Link` — special rendering nodes +- `ScrollBox`, `ScrollBoxHandle` — scrollable region + +Feature flags are evaluated at compile time via `feature('FLAG_NAME')` from `bun:bundle`. Dead code is eliminated in external builds. + +Global state is accessed via `useAppState`, `useSetAppState`, `useAppStateStore` from `src/state/AppState.js`. + +--- + +## Top-Level Components + +--- + +### App.tsx + +**Purpose:** Top-level React provider wrapper that nests all global context providers. + +**Exports:** `App` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `getFpsMetrics` | `() => FpsMetrics \| undefined` | yes | FPS metrics supplier for FpsMetricsProvider | +| `stats` | `StatsStore` | no | Stats store for StatsProvider | +| `initialState` | `AppState` | yes | Initial AppState passed to AppStateProvider | +| `children` | `React.ReactNode` | yes | Content rendered inside providers | + +**Provider nesting:** `FpsMetricsProvider > StatsProvider > AppStateProvider` + +--- + +### AgentProgressLine.tsx + +**Purpose:** Renders a single line in the coordinator agent progress tree, showing type/name label, status text, tool use count, and token count. + +**Exports:** `AgentProgressLine` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `agentType` | `string` | yes | Type identifier of the sub-agent | +| `description` | `string` | no | Human-readable description | +| `name` | `string` | no | Agent name | +| `descriptionColor` | `string` | no | Color for description text | +| `taskDescription` | `string` | no | Current task description | +| `toolUseCount` | `number` | yes | Number of tool uses | +| `tokens` | `number \| null` | yes | Token count (null = not yet known) | +| `color` | `string` | no | Color for type/name label | +| `isLast` | `boolean` | yes | Whether this is the last item (controls `└─` vs `├─`) | +| `isResolved` | `boolean` | yes | Whether task is complete | +| `isError` | `boolean` | yes | Whether task errored | +| `isAsync` | `boolean` | no | Whether agent runs asynchronously | +| `shouldAnimate` | `boolean` | yes | Whether to animate the spinner | +| `lastToolInfo` | `object` | no | Info about most recent tool use | +| `hideType` | `boolean` | no | Suppress type label rendering | + +**Key behavior:** Renders tree connector characters `└─` (last) or `├─` (non-last). Shows tool use count suffix and token count. + +--- + +### ApproveApiKey.tsx + +**Purpose:** Dialog asking user to approve or reject a custom (user-supplied) API key found in the environment. + +**Exports:** `ApproveApiKey` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `customApiKeyTruncated` | `string` | yes | Truncated API key for display | +| `onDone` | `(approved: boolean) => void` | yes | Called with approval result | + +**Key behavior:** Saves to `globalConfig.customApiKeyResponses.approved` or `.rejected` depending on selection. + +--- + +### AutoModeOptInDialog.tsx + +**Purpose:** Dialog presented to users to opt in or out of auto mode (full agentic mode). Contains legally-reviewed description text. + +**Exports:** `AUTO_MODE_DESCRIPTION` (const string), `AutoModeOptInDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onAccept` | `() => void` | yes | Called when user accepts | +| `onDecline` | `() => void` | yes | Called when user declines | +| `declineExits` | `boolean` | no | Whether declining exits the app | + +**Key behavior:** Three options: accept-default (sets `defaultMode:'auto'`), accept, decline. Logs `tengu_auto_mode_opt_in_dialog_shown`, `tengu_auto_mode_opt_in_dialog_accept`, `tengu_auto_mode_opt_in_dialog_accept_default`, `tengu_auto_mode_opt_in_dialog_decline`. + +--- + +### AutoUpdater.tsx + +**Purpose:** npm-based auto-updater. Polls GCS every 30 minutes for newer versions and installs them. + +**Exports:** `AutoUpdater` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `isUpdating` | `boolean` | yes | Whether an update is in progress | +| `onChangeIsUpdating` | `(v: boolean) => void` | yes | Toggle update in-progress state | +| `onAutoUpdaterResult` | `(r: AutoUpdaterResult) => void` | yes | Called when update completes/fails | +| `autoUpdaterResult` | `AutoUpdaterResult \| null` | yes | Current result state | +| `showSuccessMessage` | `boolean` | yes | Whether to display success notification | +| `verbose` | `boolean` | yes | Verbose output mode | + +**Key state:** `versions: { global?, latest? }`, `hasLocalInstall: boolean` + +**Key behavior:** Checks `maxVersion` kill switch, reads `installationType` to decide whether to run, polls on 30-minute interval. + +--- + +### AutoUpdaterWrapper.tsx + +**Purpose:** Routes auto-update logic to the appropriate updater (package manager, native installer, or npm). + +**Exports:** `AutoUpdaterWrapper` + +**Props:** Same as `AutoUpdater`. + +**Key state:** `useNativeInstaller: boolean | null`, `isPackageManager: boolean | null` + +**Key behavior:** Renders `PackageManagerAutoUpdater`, `NativeAutoUpdater`, or `AutoUpdater` depending on how Claude Code was installed. + +--- + +### AwsAuthStatusBox.tsx + +**Purpose:** Displays a bordered box with "Cloud Authentication" status when AWS authentication is in progress or has errored. + +**Exports:** `AwsAuthStatusBox` + +**Props:** None (reads from `AwsAuthStatusManager` singleton). + +**Key state:** `status: AwsAuthStatus` (subscribed via `useEffect`) + +**Key behavior:** Returns `null` unless `isAuthenticating` or there is an error. Renders a styled bordered box. + +--- + +### BaseTextInput.tsx + +**Purpose:** Low-level text input component shared by `TextInput` and `VimTextInput`. Handles cursor, paste, and highlight rendering. + +**Exports:** `BaseTextInput` + +**Props:** `BaseTextInputProps & { inputState: BaseInputState; children?: ReactNode; terminalFocus: boolean; highlights?: TextHighlight[]; invert?: (text: string) => string; hidePlaceholderText?: boolean }` + +**Key behavior:** Uses `useDeclaredCursor` (for terminal cursor placement), `usePasteHandler` (clipboard paste), `renderPlaceholder` utility. + +--- + +### BashModeProgress.tsx + +**Purpose:** Renders the in-progress bash command UI: the input message and streaming progress output. + +**Exports:** `BashModeProgress` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `input` | `string` | yes | The bash command being run | +| `progress` | `ShellProgress \| null` | yes | Current shell progress | +| `verbose` | `boolean` | yes | Verbose mode | + +**Key behavior:** Renders `UserBashInputMessage` + `ShellProgressMessage` (or falls back to `BashTool.renderToolUseProgressMessage`). + +--- + +### BridgeDialog.tsx + +**Purpose:** Dialog for setting up the remote bridge (REPL bridge). Shows a QR code and branch name for mobile/remote access. + +**Exports:** `BridgeDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `() => void` | yes | Called when dialog closes | + +**Key state:** `showQR: boolean`, `qrText: string`, `branchName: string` + +**Key behavior:** Uses `qrcode` library to render QR code. Reads bridge state from AppState. + +--- + +### BypassPermissionsModeDialog.tsx + +**Purpose:** Confirmation dialog shown when the `--dangerously-skip-permissions` flag is used. Requires explicit acceptance. + +**Exports:** `BypassPermissionsModeDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onAccept` | `() => void` | yes | Called when user accepts danger mode | + +**Key behavior:** Escape handler calls `gracefulShutdownSync(0)`. Decline calls `gracefulShutdownSync(1)`. Logs `tengu_bypass_permissions_mode_dialog_shown` and `tengu_bypass_permissions_mode_dialog_accept`. + +--- + +### ChannelDowngradeDialog.tsx + +**Purpose:** Dialog shown when the installed version would be a downgrade relative to the user's current version on a different release channel. + +**Exports:** `ChannelDowngradeChoice` (type: `'downgrade' | 'stay' | 'cancel'`), `ChannelDowngradeDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `currentVersion` | `string` | yes | Current installed version | +| `onChoice` | `(choice: ChannelDowngradeChoice) => void` | yes | Called with user's choice | + +--- + +### ClickableImageRef.tsx + +**Purpose:** Renders an image reference (by `imageId`) as a clickable hyperlink in terminals that support OSC 8. Falls back to styled text in non-supporting terminals. + +**Exports:** `ClickableImageRef` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `imageId` | `number` | yes | Internal image ID | +| `backgroundColor` | `keyof Theme` | no | Background color key | +| `isSelected` | `boolean` | no | Whether this ref is currently selected | + +**Key behavior:** Uses `pathToFileURL` + `supportsHyperlinks()` to produce OSC 8 links. + +--- + +### ClaudeInChromeOnboarding.tsx + +**Purpose:** Onboarding flow for the Claude in Chrome browser extension. Shows installation status and saves acceptance to global config. + +**Exports:** `ClaudeInChromeOnboarding` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `() => void` | yes | Called when onboarding completes | + +**Key state:** `isExtensionInstalled: boolean` + +**Key behavior:** Logs `tengu_claude_in_chrome_onboarding_shown`, saves config flag. + +--- + +### ClaudeMdExternalIncludesDialog.tsx + +**Purpose:** Dialog warning the user about external files included in CLAUDE.md (`@path` directives). User must approve to allow them. + +**Exports:** `ClaudeMdExternalIncludesDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `() => void` | yes | Called when dialog closes | +| `isStandaloneDialog` | `boolean` | no | Whether shown standalone vs embedded | +| `externalIncludes` | `ExternalClaudeMdInclude[]` | no | List of detected external includes | + +**Key behavior:** Saves `hasClaudeMdExternalIncludesApproved` and `hasClaudeMdExternalIncludesWarningShown` to project config. + +--- + +### CompactSummary.tsx + +**Purpose:** Renders a visual separator/summary card marking a compacted conversation boundary. + +**Exports:** `CompactSummary` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `NormalizedUserMessage` | yes | The compact summary attachment message | +| `screen` | `Screen` | yes | Current screen context | + +**Key behavior:** Shows metadata: `messagesSummarized`, direction, `userContext`. + +--- + +### ConfigurableShortcutHint.tsx + +**Purpose:** Renders a keyboard shortcut hint using the user-configured binding for an action, falling back to a literal string if no binding is set. + +**Exports:** `ConfigurableShortcutHint` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `action` | `KeybindingAction` | yes | Keybinding action name | +| `context` | `KeybindingContextName` | yes | Keybinding context name | +| `fallback` | `string` | yes | Literal fallback if no binding | +| `description` | `string` | yes | Human description of action | +| `parens` | `boolean` | no | Whether to wrap in parentheses | +| `bold` | `boolean` | no | Whether to bold the hint | + +**Key behavior:** Calls `useShortcutDisplay` to resolve binding, delegates to `KeyboardShortcutHint`. + +--- + +### ConsoleOAuthFlow.tsx + +**Purpose:** Full OAuth login flow for claude.ai/console authentication. Manages a state machine for the multi-step auth process. + +**Exports:** `ConsoleOAuthFlow` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `() => void` | yes | Called when auth completes or is cancelled | +| `startingMessage` | `string` | no | Introductory message text | +| `mode` | `'login' \| 'setup-token'` | no | Whether doing login or token setup | +| `forceLoginMethod` | `'claudeai' \| 'console'` | no | Override default login provider | + +**Key state:** `OAuthStatus` — union of states: `idle`, `platform_setup`, `ready_to_start`, `waiting_for_login`, `creating_api_key`, `about_to_retry`, `success`, `error`. + +--- + +### ContextSuggestions.tsx + +**Purpose:** Renders a list of context-saving suggestions (e.g., "remove X tokens by adding a .gitignore pattern"). + +**Exports:** `ContextSuggestions` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `suggestions` | `ContextSuggestion[]` | yes | List of suggestions to display | + +**Key behavior:** Each suggestion shows a title, severity icon, and token savings. + +--- + +### ContextVisualization.tsx + +**Purpose:** Visualizes context window usage with a collapse-status indicator (when `CONTEXT_COLLAPSE` feature flag is active). + +**Exports:** `ContextVisualization` + +**Key behavior:** Contains internal `CollapseStatus` sub-component gated by `feature('CONTEXT_COLLAPSE')`. + +--- + +### CoordinatorAgentStatus.tsx + +**Purpose:** Shows the coordinator/swarm agent progress panel in the sidebar. Reads agent task state from AppState. + +**Exports:** `getVisibleAgentTasks(tasks): Task[]`, `CoordinatorTaskPanel` + +**Props:** None (reads from AppState). + +--- + +### CostThresholdDialog.tsx + +**Purpose:** Dialog warning the user that they have spent $5 on API calls. + +**Exports:** `CostThresholdDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `() => void` | yes | Called when user acknowledges | + +--- + +### CtrlOToExpand.tsx + +**Purpose:** Renders a dim hint "ctrl+o to expand" for collapsible content. Provides a React context to prevent nested hints. + +**Exports:** `SubAgentProvider`, `CtrlOToExpand`, `ctrlOToExpand(): string` + +**Key behavior:** `SubAgentContext` is `React.createContext(false)` — prevents double-rendering the hint inside nested sub-agent output. `ctrlOToExpand()` returns a `chalk.dim` string for use in non-React contexts. + +--- + +### DesktopHandoff.tsx + +**Purpose:** Manages the "open in Claude Desktop" handoff flow. Checks if Desktop is installed, downloads it if not, then opens it. + +**Exports:** `getDownloadUrl(): string`, `DesktopHandoff` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `(result?: string, options?: { display?: CommandResultDisplay }) => void` | yes | Called when handoff completes | + +**Key state:** `DesktopHandoffState = 'checking' | 'prompt-download' | 'flushing' | 'opening' | 'success' | 'error'` + +--- + +### DevBar.tsx + +**Purpose:** Internal developer bar showing slow operations (for dev/ant builds only). + +**Exports:** `DevBar` + +**Props:** None. + +**Key state:** `slowOps` — polled every 500ms; displays last 3 slow operations. + +**Key behavior:** Only renders for dev or ant builds. + +--- + +### DiagnosticsDisplay.tsx + +**Purpose:** Renders diagnostic issues found in files (TypeScript errors, lint warnings, etc.). + +**Exports:** `DiagnosticsDisplay` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `attachment` | `DiagnosticsAttachment` | yes | Attachment containing file diagnostic data | +| `verbose` | `boolean` | yes | Show per-file detail vs summary | + +**Key behavior:** In non-verbose mode shows "Found **N** new diagnostic issue(s) in N file(s)" + `CtrlOToExpand`. In verbose mode shows per-file breakdown. + +--- + +### EffortCallout.tsx + +**Purpose:** Renders a callout when a non-default effort level is active (e.g., "max thinking" mode). + +**Exports:** `EffortCallout` + +**Key behavior:** Feature-gated. Shows the effort symbol and description. + +--- + +### EffortIndicator.ts + +**Purpose:** Utility module (non-component) providing effort-related display helpers. + +**Exports:** + +| Export | Signature | Description | +|---|---|---| +| `getEffortNotificationText` | `(effortValue: any, model: string): string \| undefined` | Returns notification text for effort level changes | +| `effortLevelToSymbol` | `(level: EffortLevel): string` | Maps effort level to display symbol | + +--- + +### ExitFlow.tsx + +**Purpose:** Orchestrates the exit flow — showing worktree cleanup dialog if in a worktree. + +**Exports:** `ExitFlow` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `(message?: string) => void` | yes | Called when exit flow completes | +| `onCancel` | `() => void` | no | Called when user cancels exit | +| `showWorktree` | `boolean` | yes | Whether to show worktree cleanup UI | + +**Key behavior:** Shows `WorktreeExitDialog` when `showWorktree` is true; otherwise null. + +--- + +### ExportDialog.tsx + +**Purpose:** Dialog for exporting conversation content to clipboard or a file. + +**Exports:** `ExportDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `content` | `string` | yes | Content to export | +| `defaultFilename` | `string` | yes | Suggested filename | +| `onDone` | `(result: { success: boolean; message: string }) => void` | yes | Called with result | + +**Key state:** `ExportOption = 'clipboard' | 'file'` + +**Key behavior:** Shows filename `TextInput` when "file" is selected. + +--- + +### FallbackToolUseErrorMessage.tsx + +**Purpose:** Renders an error message for tool use results when no tool-specific renderer is available. + +**Exports:** `FallbackToolUseErrorMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `result` | `ToolResultBlockParam['content']` | yes | The error result content | +| `verbose` | `boolean` | yes | Verbose mode | + +**Key behavior:** `MAX_RENDERED_LINES = 10`. Strips underline ANSI, removes sandbox violation/error XML tags. Shows "+N lines (ctrl+o to see all)" hint when truncated. + +--- + +### FallbackToolUseRejectedMessage.tsx + +**Purpose:** Renders the "Interrupted · What should Claude do instead?" message for rejected tool uses when no tool-specific renderer exists. + +**Exports:** `FallbackToolUseRejectedMessage` + +**Props:** None. + +**Key behavior:** Wraps `InterruptedByUser` in `MessageResponse` with height=1. + +--- + +### FastIcon.tsx + +**Purpose:** Renders the lightning bolt icon (⚡) for fast mode, with appropriate color and dim state for cooldown. + +**Exports:** `FastIcon`, `getFastIconString(applyColor?: boolean, cooldown?: boolean): string` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `cooldown` | `boolean` | no | Whether fast mode is in cooldown (dims icon) | + +--- + +### Feedback.tsx + +**Purpose:** Full feedback submission form. Collects a description, optional transcript, optionally queries Haiku for AI-assisted categorization, then opens a GitHub issue. + +**Exports:** `redactSensitiveInfo(text: string): string`, `Feedback` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `abortSignal` | `AbortSignal` | yes | For cancellable async operations | +| `messages` | `Message[]` | yes | Conversation messages | +| `initialDescription` | `string` | no | Pre-filled feedback text | +| `onDone` | `(result: string, options?: { display?: CommandResultDisplay }) => void` | yes | Called when done | +| `backgroundTasks` | `{ [taskId: string]: { type: string; identity?: { agentId: string }; messages?: Message[] } }` | no | Background task messages | + +**Key state:** `Step = 'userInput' | 'consent' | 'submitting' | 'done'` + +**Constants:** `GITHUB_URL_LIMIT = 7250`, `GITHUB_ISSUES_REPO_URL` (build-target specific) + +**Key behavior:** `redactSensitiveInfo` strips API keys (`sk-ant-...`) before submission using regex. + +--- + +### FileEditToolDiff.tsx + +**Purpose:** Renders a diff for a file edit (via `FileEditTool`). Loads diff data asynchronously using React `Suspense`. + +**Exports:** `FileEditToolDiff` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `file_path` | `string` | yes | Absolute path to the file being edited | +| `edits` | `FileEdit[]` | yes | List of edits to apply | + +**Key behavior:** Uses `useState` to initialize a promise-based data loader, wraps `DiffBody` in ``. + +--- + +### FileEditToolUpdatedMessage.tsx + +**Purpose:** Displays a summary of a file edit (lines added/removed) with a structured diff. + +**Exports:** `FileEditToolUpdatedMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `filePath` | `string` | yes | Path to edited file | +| `structuredPatch` | `StructuredPatchHunk[]` | yes | Diff hunks | +| `firstLine` | `string \| null` | yes | First line of file (for shebang detection) | +| `fileContent` | `string` | no | Full file content for syntax context | +| `style` | `'condensed'` | no | Compact display mode | +| `verbose` | `boolean` | yes | Verbose mode | +| `previewHint` | `string` | no | Optional hint text below diff | + +--- + +### FileEditToolUseRejectedMessage.tsx + +**Purpose:** Shows a "User rejected write/update to file" message with a preview of the rejected diff or content. + +**Exports:** `FileEditToolUseRejectedMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `file_path` | `string` | yes | Target file path | +| `operation` | `'write' \| 'update'` | yes | Operation type | +| `patch` | `StructuredPatchHunk[]` | no | Diff for update operations | +| `firstLine` | `string \| null` | yes | First line of file | +| `fileContent` | `string` | no | Full file content | +| `content` | `string` | no | New file content for write operations | +| `style` | `'condensed'` | no | Compact display mode | +| `verbose` | `boolean` | yes | Verbose mode | + +**Constants:** `MAX_LINES_TO_RENDER = 10` + +--- + +### FilePathLink.tsx + +**Purpose:** Renders an absolute file path as an OSC 8 hyperlink for terminal emulators (e.g., iTerm2) that support them. + +**Exports:** `FilePathLink` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `filePath` | `string` | yes | Absolute file path | +| `children` | `React.ReactNode` | no | Display text (defaults to filePath) | + +**Key behavior:** Uses `pathToFileURL` to convert the path to a `file://` URL, wraps in Ink `Link`. + +--- + +### FullscreenLayout.tsx + +**Purpose:** The main layout container for fullscreen mode. Manages a scrollable region, fixed bottom slot, overlays, modal pane, and floating content. + +**Exports:** `ScrollChromeContext` (React context), `FullscreenLayout` + +**`ScrollChromeContext`:** `{ setStickyPrompt: (p: StickyPrompt | null) => void }` — allows VirtualMessageList's `StickyTracker` to update sticky prompt state without prop-drilling. + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `scrollable` | `ReactNode` | yes | Content that scrolls (messages, tool output) | +| `bottom` | `ReactNode` | yes | Content pinned to bottom (spinner, prompt, permissions) | +| `overlay` | `ReactNode` | no | Content rendered inside ScrollBox after messages (permission requests) | +| `bottomFloat` | `ReactNode` | no | Absolute-positioned floating content over scrollback (companion speech bubble) | +| `modal` | `ReactNode` | no | Slash-command dialog in absolute bottom-anchored pane. Provides ModalContext. Fullscreen only. | +| `modalScrollRef` | `React.RefObject` | no | Ref for Tabs to attach scroll-owning ScrollBox | +| `scrollRef` | `RefObject` | no | Ref for keyboard scrolling | +| `dividerYRef` | `RefObject` | no | Y-position of unseen-divider (for scroll pill) | +| `hidePill` | `boolean` | no | Force-hide the scroll pill | + +**Constants:** `MODAL_TRANSCRIPT_PEEK = 2` rows. + +--- + +### GlobalSearchDialog.tsx + +**Purpose:** Full-text ripgrep search dialog (ctrl+shift+f). Debounced search with a file preview pane. + +**Exports:** `GlobalSearchDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `() => void` | yes | Close callback | +| `onInsert` | `(text: string) => void` | yes | Called to insert a result into the prompt | + +**Key state:** `matches: Match[]`, `truncated: boolean`, `isSearching: boolean` + +**Constants:** `VISIBLE_RESULTS = 12`, `DEBOUNCE_MS = 100`, `PREVIEW_CONTEXT_LINES = 4`, `MAX_MATCHES_PER_FILE = 10`, `MAX_TOTAL_MATCHES = 500` + +**Key behavior:** Uses `useRegisterOverlay("global-search")`. Previews on right column when `columns >= 140`. Calls `ripGrepStream` for search. + +--- + +### HighlightedCode.tsx + +**Purpose:** Renders syntax-highlighted source code using the native Rust `ColorFile` module. Falls back to `HighlightedCodeFallback` when unavailable. + +**Exports:** `HighlightedCode` (memo-wrapped) + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `code` | `string` | yes | Source code string | +| `filePath` | `string` | yes | File path for language detection | +| `width` | `number` | no | Render width (defaults to 80) | +| `dim` | `boolean` | no | Whether to dim the output | + +**Key state:** `measuredWidth: number` (from `measureElement`) + +**Constants:** `DEFAULT_WIDTH = 80` + +**Key behavior:** Respects `settings.syntaxHighlightingDisabled`. Uses `expectColorFile()` from the Rust `colorDiff` module. + +--- + +### HistorySearchDialog.tsx + +**Purpose:** Fuzzy-searchable history browser (ctrl+r). Loads all timestamped history entries async, allows fuzzy matching and preview. + +**Exports:** `HistorySearchDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `initialQuery` | `string` | no | Pre-filled search query | +| `onSelect` | `(entry: HistoryEntry) => void` | yes | Called when user selects an entry | +| `onCancel` | `() => void` | yes | Called when user cancels | + +**Key state:** `items: Item[] | null`, `query: string` + +**Constants:** `PREVIEW_ROWS = 6`, `AGE_WIDTH = 8` + +**Key behavior:** Uses `useRegisterOverlay('history-search')`. Loads from `getTimestampedHistory()` async generator. Uses `FuzzyPicker` for display. + +--- + +### IdeAutoConnectDialog.tsx + +**Purpose:** First-run dialog asking whether to auto-connect to the IDE on startup. + +**Exports:** `IdeAutoConnectDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onComplete` | `() => void` | yes | Called when user makes a selection | + +**Key behavior:** Saves `globalConfig.autoConnectIde` and `globalConfig.hasIdeAutoConnectDialogBeenShown = true`. + +--- + +### IdeOnboardingDialog.tsx + +**Purpose:** Onboarding dialog for IDE integration (VS Code, JetBrains etc). Shows installation status and instructions. + +**Exports:** `IdeOnboardingDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `() => void` | yes | Called when dialog closes | +| `installationStatus` | `IDEExtensionInstallationStatus \| null` | yes | Current extension install status | + +**Key behavior:** Marks dialog as shown immediately on mount via `markDialogAsShown()`. Responds to `confirm:yes` and `confirm:no` keybindings. + +--- + +### IdeStatusIndicator.tsx + +**Purpose:** Shows current IDE selection state in the status line — active file or selected lines count. + +**Exports:** `IdeStatusIndicator` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `ideSelection` | `IDESelection \| undefined` | yes | Current IDE selection state | +| `mcpClients` | `MCPServerConnection[]` | no | MCP clients for connection status | + +**Key behavior:** Uses `useIdeConnectionStatus`. Returns null unless connected with a selection. Shows `⧉ N lines selected` or `⧉ In filename.ts`. + +--- + +### IdleReturnDialog.tsx + +**Purpose:** Dialog shown when user returns after an idle period, offering options to continue, clear, dismiss, or never show again. + +**Exports:** `IdleReturnDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `idleMinutes` | `number` | yes | Minutes the user was away | +| `totalInputTokens` | `number` | yes | Current context size in tokens | +| `onDone` | `(action: IdleReturnAction) => void` | yes | Called with user's choice | + +**Types:** `IdleReturnAction = 'continue' | 'clear' | 'dismiss' | 'never'` + +--- + +### InterruptedByUser.tsx + +**Purpose:** Renders the "Interrupted · What should Claude do instead?" text. In ant builds shows a different message. + +**Exports:** `InterruptedByUser` + +**Props:** None. + +--- + +### InvalidConfigDialog.tsx + +**Purpose:** Dialog shown when the Claude config file contains invalid JSON. User can exit or reset the config. + +**Exports:** `InvalidConfigDialog` (named, not default), `InvalidConfigHandlerProps`, `InvalidConfigDialogProps` + +**`InvalidConfigDialogProps`:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `filePath` | `string` | yes | Path to invalid config file | +| `errorDescription` | `string` | yes | Error description to display | +| `onExit` | `() => void` | yes | Exit without fixing | +| `onReset` | `() => void` | yes | Reset config to defaults | + +**Key behavior:** Also exports standalone renderer via `render()` for use outside the React tree. + +--- + +### InvalidSettingsDialog.tsx + +**Purpose:** Dialog shown when settings files have validation errors. User can continue (skipping invalid files) or exit. + +**Exports:** `InvalidSettingsDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `settingsErrors` | `ValidationError[]` | yes | List of validation errors | +| `onContinue` | `() => void` | yes | Continue despite errors | +| `onExit` | `() => void` | yes | Exit to fix errors | + +--- + +### KeybindingWarnings.tsx + +**Purpose:** Displays keybinding validation warnings/errors. Only shown when keybinding customization is enabled (ant + feature gate). + +**Exports:** `KeybindingWarnings` + +**Props:** None. + +**Key behavior:** Calls `isKeybindingCustomizationEnabled()`. Groups warnings by severity (error/warning). Shows file path via `getKeybindingsPath()`. + +--- + +### LanguagePicker.tsx + +**Purpose:** Text input for selecting the preferred response language. + +**Exports:** `LanguagePicker` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `initialLanguage` | `string \| undefined` | yes | Current language setting | +| `onComplete` | `(language: string \| undefined) => void` | yes | Called with selected language | +| `onCancel` | `() => void` | yes | Called when cancelled | + +**Key state:** `language: string | undefined`, `cursorOffset: number` + +--- + +### LogSelector.tsx + +**Purpose:** Full-featured session log browser with fuzzy search, tag filtering, agentic search, and session preview. + +**Exports:** `LogSelectorProps`, `LogSelector` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `logs` | `LogOption[]` | yes | All available log entries | +| `maxHeight` | `number` | no | Max display height | +| `forceWidth` | `number` | no | Override terminal width | +| `onCancel` | `() => void` | no | Cancel callback | +| `onSelect` | `(log: LogOption) => void` | yes | Selection callback | +| `onLogsChanged` | `() => void` | no | Called when log list changes | +| `onLoadMore` | `(count: number) => void` | no | Load more entries | +| `initialSearchQuery` | `string` | no | Pre-filled query | +| `showAllProjects` | `boolean` | no | Show logs from all projects | +| `onToggleAllProjects` | `() => void` | no | Toggle all-projects mode | +| `onAgenticSearch` | `(query: string, logs: LogOption[], signal?: AbortSignal) => Promise` | no | AI-powered search callback | + +**Internal types:** `AgenticSearchState`, `LogTreeNode` + +--- + +### MarkdownTable.tsx + +**Purpose:** Renders a Markdown table token with ANSI-aware column width calculation and wrapping. Switches to vertical (key-value) format for wide content. + +**Exports:** `MarkdownTable` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `token` | `Tokens.Table` | yes | Parsed marked.js table token | +| `highlight` | `CliHighlight \| null` | yes | Syntax highlight context | +| `forceWidth` | `number` | no | Override terminal width (for testing) | + +**Constants:** `SAFETY_MARGIN = 4`, `MIN_COLUMN_WIDTH = 3`, `MAX_ROW_LINES = 4` + +--- + +### Markdown.tsx + +**Purpose:** Renders Markdown text with `marked` (GFM mode). Includes an LRU token cache and a fast path for plain text. + +**Exports:** `Markdown` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `children` | `string` | yes | Markdown source text | +| `dimColor` | `boolean` | no | Whether to dim the output | + +**Key behavior:** Module-level LRU token cache with max 500 entries. Fast path skips `marked.lexer` for plain text (no Markdown syntax). Uses marked GFM dialect. + +--- + +### MemoryUsageIndicator.tsx + +**Purpose:** Shows high/critical heap memory warning with `/heapdump` link. Ant-internal build only — returns `null` in external builds. + +**Exports:** `MemoryUsageIndicator` + +**Props:** None. + +**Key behavior:** Returns null in external builds (build-time constant). Uses `useMemoryUsage()` hook (10s polling). Shows warning/error color based on status. + +--- + +### Message.tsx + +**Purpose:** Central message dispatcher. Routes each message/content block to its appropriate rendering component. + +**Exports:** `hasThinkingContent(message): boolean`, `Message` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `NormalizedMessage` | yes | Normalized message to render | +| `lookups` | `ReturnType` | yes | Precomputed message lookups | +| `containerWidth` | `number` | no | Available width | +| `addMargin` | `boolean` | yes | Whether to add top margin | +| `tools` | `Tools` | yes | Available tools | +| `commands` | `Command[]` | yes | Available commands | +| `verbose` | `boolean` | yes | Verbose mode | +| `inProgressToolUseIDs` | `Set` | yes | IDs of in-flight tool uses | +| `progressMessagesForMessage` | `Message[]` | yes | Progress messages for this message | +| `shouldAnimate` | `boolean` | yes | Whether to animate spinners | +| `shouldShowDot` | `boolean` | yes | Whether to show dot prefix | +| `style` | `object` | no | Style overrides | +| `width` | `number` | no | Explicit width | +| `isTranscriptMode` | `boolean` | yes | Transcript/history mode | +| `isStatic` | `boolean` | yes | Prevent re-renders | +| `onOpenRateLimitOptions` | `() => void` | no | Opens rate limit options | +| `isActiveCollapsedGroup` | `boolean` | no | Whether this is the active collapsed group | +| `isUserContinuation` | `boolean` | no | Whether prev message is also user | +| `lastThinkingBlockId` | `string \| null` | yes | ID of last thinking block | +| `latestBashOutputUUID` | `string \| null` | yes | UUID of latest bash output | + +--- + +### MessageModel.tsx + +**Purpose:** Displays the model identifier for an assistant message in transcript mode. + +**Exports:** `MessageModel` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `NormalizedMessage` | yes | Message to check | +| `isTranscriptMode` | `boolean` | yes | Only shows in transcript mode | + +**Key behavior:** Only renders for assistant messages with a `model` field and text content blocks. + +--- + +### MessageResponse.tsx + +**Purpose:** Wraps assistant response content with the `⎿` prefix character. Uses `MessageResponseContext` to prevent double-prefix nesting. + +**Exports:** `MessageResponse` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `children` | `ReactNode` | yes | Response content | +| `height` | `number` | no | Explicit height (skips Ratchet wrapper) | + +**Key behavior:** Renders `⎿` via `NoSelect`. Uses `MessageResponseContext` to avoid nested prefixes. Wraps in `Ratchet` unless `height` is specified. + +--- + +### MessageRow.tsx + +**Purpose:** Renders a single message row with optional OffscreenFreeze, model indicator, and timestamp. + +**Exports:** `hasContentAfterIndex(messages, index, tools, streamingToolUseIDs): boolean`, `Props`, `MessageRow` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `RenderableMessage` | yes | Message to render | +| `isUserContinuation` | `boolean` | yes | Whether prev message is user | +| `hasContentAfter` | `boolean` | yes | Whether non-skippable content follows | +| `tools` | `Tools` | yes | Available tools | +| `commands` | `Command[]` | yes | Available commands | +| `verbose` | `boolean` | yes | Verbose mode | +| `inProgressToolUseIDs` | `Set` | yes | In-flight tool IDs | +| `streamingToolUseIDs` | `Set` | yes | Streaming tool IDs | +| `screen` | `Screen` | yes | Current screen context | +| `canAnimate` | `boolean` | yes | Allow animation | +| `onOpenRateLimitOptions` | `() => void` | no | Rate limit options callback | +| `lastThinkingBlockId` | `string \| null` | yes | ID of last thinking block | +| `latestBashOutputUUID` | `string \| null` | yes | UUID of latest bash output | +| `columns` | `number` | yes | Terminal column count | +| `isLoading` | `boolean` | yes | Whether a query is in flight | +| `lookups` | `ReturnType` | yes | Precomputed lookups | + +--- + +### MessageSelector.tsx + +**Purpose:** Allows the user to select a historical message to rewind/restore to. + +**Exports:** `MessageSelector` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `messages` | `Message[]` | yes | All conversation messages | +| `onPreRestore` | `() => void` | yes | Pre-restore hook | +| `onRestoreMessage` | `(message: UserMessage) => Promise` | yes | Restore conversation to message | +| `onRestoreCode` | `(message: UserMessage) => Promise` | yes | Restore code to message | +| `onSummarize` | `(message: UserMessage, feedback?: string, direction?: PartialCompactDirection) => Promise` | yes | Summarize up to message | +| `onClose` | `() => void` | yes | Close callback | +| `preselectedMessage` | `UserMessage` | no | Skip pick-list, go direct to confirm | + +**Types:** `RestoreOption = 'both' | 'conversation' | 'code' | 'summarize' | 'summarize_up_to' | 'nevermind'` + +**Constants:** `MAX_VISIBLE_MESSAGES = 7` + +--- + +### MessageTimestamp.tsx + +**Purpose:** Shows the formatted timestamp for an assistant message in transcript mode. + +**Exports:** `MessageTimestamp` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `NormalizedMessage` | yes | Message with timestamp | +| `isTranscriptMode` | `boolean` | yes | Only shows in transcript mode | + +**Key behavior:** Only renders for assistant messages with text content. Formats as `HH:MM AM/PM`. + +--- + +### Messages.tsx + +**Purpose:** Top-level conversation view component. Normalizes messages, collapses read/search groups, builds lookups, and drives the virtual scroll list or static list. + +**Exports:** `shouldRenderStatically(screen: Screen): boolean`, `Messages` + +**Key behavior:** +- Contains `LogoHeader = React.memo(...)` with blit-optimization note (must render before messages for correct scroll behavior). +- Filters out null-rendering attachments before the 200-message render cap. +- Builds `buildMessageLookups` for efficient tool use result matching. +- Uses `VirtualMessageList` with `JumpHandle` for transcript mode, static list for REPL mode. + +--- + +### ModelPicker.tsx + +**Purpose:** Model selection picker with effort level toggle. Supports fast mode awareness and session-scoped vs global settings. + +**Exports:** `Props`, `ModelPicker` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `initial` | `string \| null` | yes | Initial model (null = no preference) | +| `sessionModel` | `ModelSetting` | no | Current session model | +| `onSelect` | `(model: string \| null, effort: EffortLevel \| undefined) => void` | yes | Selection callback | +| `onCancel` | `() => void` | no | Cancel callback | +| `isStandaloneCommand` | `boolean` | no | In standalone command context | +| `showFastModeNotice` | `boolean` | no | Show fast mode info | +| `headerText` | `string` | no | Override dim header line | +| `skipSettingsWrite` | `boolean` | no | Skip writing effort to userSettings (for project-scoped installs) | + +**Constants:** `NO_PREFERENCE = '__NO_PREFERENCE__'` + +--- + +### NativeAutoUpdater.tsx + +**Purpose:** Auto-updater for native installer builds. Calls `installLatest()` from `nativeInstaller` when a newer version is available. + +**Exports:** `NativeAutoUpdater` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `isUpdating` | `boolean` | yes | In-progress flag | +| `onChangeIsUpdating` | `(v: boolean) => void` | yes | Toggle in-progress | +| `onAutoUpdaterResult` | `(r: AutoUpdaterResult) => void` | yes | Result callback | +| `autoUpdaterResult` | `AutoUpdaterResult \| null` | yes | Current result | +| `showSuccessMessage` | `boolean` | yes | Show success notification | +| `verbose` | `boolean` | yes | Verbose mode | + +**Key state:** `versions: { current?: string | null; latest?: string | null }` (polled by interval) + +--- + +### NotebookEditToolUseRejectedMessage.tsx + +**Purpose:** Shows "User rejected replace/insert/delete cell in notebook_path" with code preview. + +**Exports:** `NotebookEditToolUseRejectedMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `notebook_path` | `string` | yes | Path to the notebook | +| `cell_id` | `string \| undefined` | yes | Target cell ID | +| `new_source` | `string` | yes | New cell source | +| `cell_type` | `'code' \| 'markdown'` | no | Cell type | +| `edit_mode` | `'replace' \| 'insert' \| 'delete'` | no | Edit mode (default: 'replace') | +| `verbose` | `boolean` | yes | Verbose mode | + +--- + +### OffscreenFreeze.tsx + +**Purpose:** Performance optimization that freezes children when they scroll above the terminal viewport into scrollback. + +**Exports:** `OffscreenFreeze` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `children` | `React.ReactNode` | yes | Content to potentially freeze | + +**Key behavior:** Uses `'use no memo'` directive to opt out of React Compiler. Uses `useTerminalViewport` to detect visibility. Caches the last visible render in `useRef`. When `inVirtualList` is true, freeze is bypassed (virtual list clips inside viewport). + +--- + +### Onboarding.tsx + +**Purpose:** Multi-step first-run onboarding flow: preflight checks, theme selection, OAuth, API key approval, security review, terminal setup. + +**Exports:** `Onboarding` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `() => void` | yes | Called when all steps complete | + +**Key state:** `currentStepIndex: number`, `skipOAuth: boolean`, `oauthEnabled: boolean`, `theme` + +**Steps:** `StepId = 'preflight' | 'theme' | 'oauth' | 'api-key' | 'security' | 'terminal-setup'` + +--- + +### OutputStylePicker.tsx + +**Purpose:** Picker for selecting the active output style (default, concise, detailed, custom styles from `.claude/output-styles/`). + +**Exports:** `OutputStylePickerProps`, `OutputStylePicker` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `initialStyle` | `OutputStyle` | yes | Current style | +| `onComplete` | `(style: OutputStyle) => void` | yes | Selection callback | +| `onCancel` | `() => void` | yes | Cancel callback | +| `isStandaloneCommand` | `boolean` | no | In standalone command context | + +**Key state:** `styleOptions: OptionWithDescription[]`, `isLoading: boolean` + +--- + +### PackageManagerAutoUpdater.tsx + +**Purpose:** Notifies users about available updates when Claude Code was installed via a package manager (brew, pip, etc.). + +**Exports:** `PackageManagerAutoUpdater` + +**Props:** Same as `NativeAutoUpdater`. + +**Key state:** `updateAvailable: boolean`, `packageManager: PackageManager | "unknown"` + +**Key behavior:** Only shows notification text — does not auto-install. Uses `MACRO.VERSION` (build-time constant). + +--- + +### PrBadge.tsx + +**Purpose:** Renders a PR number badge with review state coloring. + +**Exports:** `PrBadge` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `number` | `number` | yes | PR number | +| `url` | `string` | yes | PR URL | +| `reviewState` | `PrReviewState` | no | Review status | +| `bold` | `boolean` | no | Bold style | + +--- + +### PressEnterToContinue.tsx + +**Purpose:** Simple "Press **Enter** to continue…" prompt with permission color styling. + +**Exports:** `PressEnterToContinue` + +**Props:** None. + +--- + +### QuickOpenDialog.tsx + +**Purpose:** Quick-open fuzzy file finder (ctrl+shift+p). Shows file results with a syntax-highlighted preview. + +**Exports:** `QuickOpenDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `() => void` | yes | Close callback | +| `onInsert` | `(text: string) => void` | yes | Insert path into prompt | + +**Key state:** `results`, `query`, `focusedPath`, `preview` + +**Constants:** `VISIBLE_RESULTS = 8`, `PREVIEW_LINES = 20` + +--- + +### RemoteCallout.tsx + +**Purpose:** One-time callout dialog prompting users to enable Remote Control (bridge). Saves `remoteDialogSeen = true` on mount. + +**Exports:** `RemoteCallout` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `(selection: RemoteCalloutSelection) => void` | yes | Called with 'enable' or 'dismiss' | + +**Types:** `RemoteCalloutSelection = 'enable' | 'dismiss'` + +--- + +### RemoteEnvironmentDialog.tsx + +**Purpose:** Picker for selecting a remote Teleport environment (claude.ai/code environments). + +**Exports:** `RemoteEnvironmentDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `(message?: string) => void` | yes | Close callback | + +**Key state:** `loadingState: 'loading' | 'updating' | null`, `environments: EnvironmentResource[]`, `selectedEnvironment`, `selectedEnvironmentSource`, `error` + +--- + +### ResumeTask.tsx + +**Purpose:** Lists remote Claude Code sessions (from Sessions API) for resuming. Filters by current git repository. + +**Exports:** `ResumeTask` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onSelect` | `(session: CodeSession) => void` | yes | Session selection callback | +| `onCancel` | `() => void` | yes | Cancel callback | +| `isEmbedded` | `boolean` | no | Whether shown inside another component | + +**Key state:** `sessions: CodeSession[]`, `currentRepo: string | null`, `loading: boolean`, `loadErrorType: LoadErrorType | null`, `retrying: boolean`, `focusedIndex: number` + +--- + +### SandboxViolationExpandedView.tsx + +**Purpose:** Shows recent sandboxing violations (last 10). Subscribed to `SandboxManager.getSandboxViolationStore()`. + +**Exports:** `SandboxViolationExpandedView` + +**Props:** None. + +**Key state:** `violations: SandboxViolationEvent[]`, `totalCount: number` + +**Key behavior:** Returns null when sandboxing is disabled or on Linux. + +--- + +### ScrollKeybindingHandler.tsx + +**Purpose:** Keyboard handler for scrolling — j/k/arrows, page up/down, g/G, ctrl+u/d/b/f. Includes scroll acceleration for smooth wheel events. + +**Exports:** `ScrollKeybindingHandler` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `scrollRef` | `RefObject` | yes | The scroll box to control | +| `isActive` | `boolean` | yes | Whether key bindings are active | +| `onScroll` | `(sticky: boolean, handle: ScrollBoxHandle) => void` | no | Post-scroll callback | +| `isModal` | `boolean` | no | Enable modal pager keys (g/G, ctrl+u/d/b/f) | + +**Constants:** `WHEEL_ACCEL_WINDOW_MS = 40`, `WHEEL_ACCEL_STEP = 0.3`, `WHEEL_ACCEL_MAX = 6` + +--- + +### SearchBox.tsx + +**Purpose:** Renders a styled search input box with cursor display and placeholder. + +**Exports:** `SearchBox` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `query` | `string` | yes | Current query text | +| `placeholder` | `string` | no | Placeholder text (default: "Search…") | +| `isFocused` | `boolean` | yes | Whether input is focused | +| `isTerminalFocused` | `boolean` | yes | Whether the terminal has focus | +| `prefix` | `string` | no | Icon prefix (default: "⌕") | +| `width` | `number \| string` | no | Width override | +| `cursorOffset` | `number` | no | Cursor position (defaults to query.length) | +| `borderless` | `boolean` | no | Remove border | + +--- + +### SentryErrorBoundary.ts + +**Purpose:** React error boundary that silently swallows rendering errors (renders `null`). + +**Exports:** `SentryErrorBoundary` + +**Props:** `{ children: React.ReactNode }` + +**Key behavior:** Class component; catches render errors via `getDerivedStateFromError` and renders null. + +--- + +### SessionBackgroundHint.tsx + +**Purpose:** Shows a hint and handles the Ctrl+B double-press pattern for backgrounding the current session. + +**Exports:** `SessionBackgroundHint` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onBackgroundSession` | `() => void` | yes | Called to background the session | +| `isLoading` | `boolean` | yes | Whether a query is in progress | + +**Key state:** `showSessionHint: boolean` + +**Key behavior:** Only activates when `isLoading` and no foreground bash/agent tasks. Uses `useDoublePress` — first press shows hint, second within 800ms executes. + +--- + +### SessionPreview.tsx + +**Purpose:** Renders a read-only preview of a historical session log using the full `Messages` component. + +**Exports:** `SessionPreview` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `log` | `LogOption` | yes | Log entry to preview | +| `onExit` | `() => void` | yes | Close callback | +| `onSelect` | `(log: LogOption) => void` | yes | Open this session fully | + +**Key state:** `fullLog: LogOption | null` (loaded async from `loadFullLog` for lite logs) + +--- + +### ShowInIDEPrompt.tsx + +**Purpose:** Shows an "Opened changes in {IDE}" confirmation pane with Yes/No options when a diff was opened in the IDE. + +**Exports:** `ShowInIDEPrompt` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `filePath` | `string` | yes | File being reviewed | +| `input` | `A` (generic) | yes | Tool input arguments | +| `onChange` | `(option: PermissionOption, args: A, feedback?: string) => void` | yes | Selection callback | +| `options` | `PermissionOptionWithLabel[]` | yes | Accept/reject options | +| `ideName` | `string` | yes | IDE display name (e.g., "VS Code") | +| `symlinkTarget` | `string \| null` | no | Symlink target if applicable | +| `rejectFeedback` | `string` | yes | Text for reject confirmation | +| `acceptFeedback` | `string` | yes | Text for accept confirmation | +| `setFocusedOption` | `(value: string) => void` | yes | Focus control callback | +| `onInputModeToggle` | `(value: string) => void` | yes | Toggle input mode | +| `focusedOption` | `string` | yes | Currently focused option | +| `yesInputMode` | `boolean` | yes | Whether in yes-input mode | +| `noInputMode` | `boolean` | yes | Whether in no-input mode | + +--- + +### SkillImprovementSurvey.tsx + +**Purpose:** Renders a post-skill-execution survey asking if the skill improvement was helpful. + +**Exports:** `SkillImprovementSurvey` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `isOpen` | `boolean` | yes | Whether survey is visible | +| `skillName` | `string` | yes | Name of the skill | +| `updates` | `SkillUpdate[]` | yes | Skill update descriptions | +| `handleSelect` | `(selected: FeedbackSurveyResponse) => void` | yes | Response callback | +| `inputValue` | `string` | yes | Current text input value | +| `setInputValue` | `(value: string) => void` | yes | Text input setter | + +--- + +### Spinner.tsx + +**Purpose:** Re-exports the main animated Spinner component and its `SpinnerMode` type. + +**Exports:** `SpinnerMode` (re-export from `./Spinner/index.js`), `Spinner` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `mode` | `SpinnerMode` | yes | Spinner animation mode | +| `loadingStartTimeRef` | `RefObject` | yes | When loading started | +| `totalPausedMsRef` | `RefObject` | yes | Accumulated paused milliseconds | +| `pauseStartTimeRef` | `RefObject` | yes | When last pause began | +| `spinnerTip` | `string` | no | Tip text below spinner | +| `responseLengthRef` | `RefObject` | yes | Streaming response length | +| `overrideColor` | `string` | no | Override spinner color | +| `overrideShimmerColor` | `string` | no | Override shimmer color | + +--- + +### Stats.tsx + +**Purpose:** Full-screen usage statistics viewer with date range tabs, token breakdown, model heatmap, and ASCII chart. + +**Exports:** `Stats` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onClose` | `(result?: string, options?: { display?: CommandResultDisplay }) => void` | yes | Close callback | + +**Types:** `StatsResult = { type: 'success'; data: ClaudeCodeStats } | { type: 'error'; message: string } | { type: 'empty' }` + +**Constants:** `DATE_RANGE_LABELS: Record` (`7d`, `30d`, `90d`) + +--- + +### StatusLine.tsx + +**Purpose:** Bottom status line showing model, permission mode, context usage, worktree, and session info. + +**Exports:** `statusLineShouldDisplay(settings): boolean`, `StatusLine` + +**Key behavior:** Reads model, permission mode, context usage, worktree status, and session info from AppState/settings. + +--- + +### StatusNotices.tsx + +**Purpose:** Renders active startup notices (e.g., deprecation warnings, MCP errors). Uses React `use()` for async memory file loading. + +**Exports:** `StatusNotices` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `agentDefinitions` | `AgentDefinitionsResult` | no | Agent definitions for notice context | + +**Key behavior:** Calls `getActiveNotices(context)` where context includes `config`, `agentDefinitions`, `memoryFiles`. Returns null if no active notices. + +--- + +### StructuredDiff.tsx + +**Purpose:** Renders a single diff hunk with syntax highlighting via the Rust `ColorDiff` NAPI module. Caches rendered output at module level (WeakMap) to survive remounts. + +**Exports:** `StructuredDiff` (memo-wrapped) + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `patch` | `StructuredPatchHunk` | yes | Single diff hunk | +| `dim` | `boolean` | yes | Whether to dim the output | +| `filePath` | `string` | yes | File path for language detection | +| `firstLine` | `string \| null` | yes | First line for shebang detection | +| `fileContent` | `string` | no | Full file content for syntax context | +| `width` | `number` | yes | Render width | +| `skipHighlighting` | `boolean` | no | Skip syntax highlighting | + +**Key behavior:** Module-level `RENDER_CACHE = new WeakMap>()`. Two `RawAnsi` columns (gutter + content) replace N `DiffLine` rows. Gutter width = marker(1) + space + max_digits + space. + +--- + +### StructuredDiffList.tsx + +**Purpose:** Renders a list of diff hunks (from `StructuredDiff`) separated by ellipsis markers. + +**Exports:** `StructuredDiffList` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `hunks` | `StructuredPatchHunk[]` | yes | Diff hunks to render | +| `dim` | `boolean` | yes | Dim mode | +| `width` | `number` | yes | Render width | +| `filePath` | `string` | yes | For language detection | +| `firstLine` | `string \| null` | yes | For shebang detection | +| `fileContent` | `string` | no | Full file for syntax context | + +**Key behavior:** Uses `intersperse` utility to inject `...` separators between hunks. + +--- + +### TagTabs.tsx + +**Purpose:** Renders a horizontal tab bar for session log tag filtering with overflow handling and "← N" / "→ (tab to cycle)" hints. + +**Exports:** `TagTabs` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `tabs` | `string[]` | yes | Tab labels | +| `selectedIndex` | `number` | yes | Currently selected tab index | +| `availableWidth` | `number` | yes | Display width constraint | +| `showAllProjects` | `boolean` | no | Whether all-projects mode is on | + +**Constants:** `ALL_TAB_LABEL = 'All'`, `TAB_PADDING = 2`, `MAX_OVERFLOW_DIGITS = 2` + +--- + +### TextInput.tsx + +**Purpose:** Full-featured text input with voice recording waveform cursor animation, clipboard paste hint, and vim/normal mode routing. + +**Exports:** `Props` (= `BaseTextInputProps & { highlights?: TextHighlight[] }`), `TextInput` (default export) + +**Key behavior:** +- Uses `feature('VOICE_MODE')` for conditional voice recording waveform cursor. +- Smoothed waveform uses exponential moving average (`SMOOTH = 0.7`). +- `LEVEL_BOOST = 1.8` to amplify audio levels to full bar range. +- Delegates to `BaseTextInput` with `useTextInput` hook for state. + +**Constants:** `BARS = ' ▁▂▃▄▅▆▇█'`, `CURSOR_WAVEFORM_WIDTH = 1`, `SMOOTH = 0.7`, `LEVEL_BOOST = 1.8`, `SILENCE_THRESHOLD = 0.15` + +--- + +### ThemePicker.tsx + +**Purpose:** Theme selection UI with live preview of a syntax-highlighted diff snippet. + +**Exports:** `ThemePickerProps`, `ThemePicker` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onThemeSelect` | `(setting: ThemeSetting) => void` | yes | Called when theme is selected | +| `showIntroText` | `boolean` | no | Show introductory text (default: false) | +| `helpText` | `string` | no | Help text below selector | +| `showHelpTextBelow` | `boolean` | no | Position of help text | +| `hideEscToCancel` | `boolean` | no | Hide Esc hint | +| `skipExitHandling` | `boolean` | no | Skip exit handling (for onboarding) | +| `onCancel` | `() => void` | no | Custom cancel handler | + +--- + +### ThinkingToggle.tsx + +**Purpose:** Picker to enable/disable extended thinking. Shows a confirmation prompt when toggling mid-conversation. + +**Exports:** `Props`, `ThinkingToggle` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `currentValue` | `boolean` | yes | Current thinking enabled state | +| `onSelect` | `(enabled: boolean) => void` | yes | Selection callback | +| `onCancel` | `() => void` | no | Cancel callback | +| `isMidConversation` | `boolean` | no | Whether mid-conversation (affects confirmation) | + +**Key state:** `confirmationPending: boolean | null` + +--- + +### TokenWarning.tsx + +**Purpose:** Displays context window usage warning. When `CONTEXT_COLLAPSE` is enabled, shows live collapse progress. + +**Exports:** `TokenWarning` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `tokenUsage` | `number` | yes | Current token count | +| `model` | `string` | yes | Active model (for context window size) | + +**Key behavior:** Contains internal `CollapseLabel` sub-component that subscribes to the collapse stats store via `useSyncExternalStore`. + +--- + +### ToolUseLoader.tsx + +**Purpose:** Animated tool use status dot (●). Blinks while unresolved, shows green for success, red for error. + +**Exports:** `ToolUseLoader` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `isError` | `boolean` | yes | Whether the tool use errored | +| `isUnresolved` | `boolean` | yes | Whether still in progress | +| `shouldAnimate` | `boolean` | yes | Whether to animate blink | + +**Key behavior:** Uses `useBlink`. Color: undefined (dim) when unresolved, 'error' on error, 'success' on success. Note: sensitive to dim+bold ANSI reset interaction (see chalk issue #290). + +--- + +### ValidationErrorsList.tsx + +**Purpose:** Renders a tree-formatted list of settings validation errors using dot-notation paths. + +**Exports:** `ValidationErrorsList` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `errors` | `ValidationError[]` | yes | Validation errors to display | + +**Key behavior:** Uses `lodash-es/setWith` to build nested tree from dot-notation paths, then `treeify` util to render. Formats array indices with their values for readability. + +--- + +### VimTextInput.tsx + +**Purpose:** Vim-mode text input. Wraps `BaseTextInput` with vim state management from `useVimInput`. + +**Exports:** `Props` (= `VimTextInputProps & { highlights?: TextHighlight[] }`), `VimTextInput` (default export) + +**Key behavior:** Passes full vim input props through `useVimInput`. Invert function applies `chalk.inverse` when terminal has focus. + +--- + +### VirtualMessageList.tsx + +**Purpose:** Virtualized scrollable message list with search highlighting, jump navigation, and sticky prompt support. + +**Exports:** `StickyPrompt` (type), `JumpHandle` (imperative handle type), `VirtualMessageList` + +**`JumpHandle` interface:** + +| Method | Description | +|---|---| +| `jumpToIndex(index: number)` | Scroll to specific message index | +| `setSearchQuery(query: string)` | Set text search query | +| `nextMatch()` | Jump to next search match | +| `prevMatch()` | Jump to previous search match | +| `setAnchor(index: number)` | Set scroll anchor | +| `warmSearchIndex()` | Pre-warm search index | +| `disarmSearch()` | Clear search state | + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `messages` | `RenderableMessage[]` | yes | Messages to render | +| `scrollRef` | `RefObject` | yes | Scroll box ref | +| `columns` | `number` | yes | Terminal width | +| `itemKey` | `(msg: RenderableMessage) => string` | yes | Unique key function | +| `renderItem` | `(msg: RenderableMessage, index: number) => ReactNode` | yes | Item renderer | +| `onItemClick` | `(index: number) => void` | no | Click handler | + +--- + +### WorkflowMultiselectDialog.tsx + +**Purpose:** Multi-select dialog for choosing GitHub Actions workflows to install with the GitHub App integration. + +**Exports:** `WorkflowMultiselectDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onSubmit` | `(selectedWorkflows: Workflow[]) => void` | yes | Called with selected workflows | +| `defaultSelections` | `Workflow[]` | yes | Initially selected workflows | + +**Workflows:** `claude` (@Claude Code tag), `claude-review` (automated PR review) + +--- + +### WorktreeExitDialog.tsx + +**Purpose:** Dialog shown when exiting from a git worktree session. Asks whether to keep the worktree, clean it up, or eject commits. + +**Exports:** `WorktreeExitDialog` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `onDone` | `(result?: string, options?: { display?: CommandResultDisplay }) => void` | yes | Close with result | +| `onCancel` | `() => void` | no | Cancel callback | + +**Key state:** `status: 'loading' | 'asking' | 'keeping' | 'removing' | 'done'`, `changes: string[]`, `commitCount: number`, `resultMessage: string | undefined` + +**Key behavior:** Reads git status and worktree commit count. Uses `cleanupWorktree`/`keepWorktree`/`killTmuxSession`. Lazy-requires `sessionStorage` to avoid circular import. + +--- + +### messageActions.tsx + +**Purpose:** Provides contexts and types for keyboard-driven message navigation in the virtual list. + +**Exports:** + +| Export | Type/Signature | Description | +|---|---|---| +| `NavigableType` | type | Union of navigable message types | +| `NavigableOf` | generic type | Navigable message wrapper | +| `NavigableMessage` | type | Union of all navigable message variants | +| `isNavigableMessage(msg)` | `(msg: NormalizedMessage) => boolean` | Type guard | +| `PRIMARY_INPUT` | `Map` | Maps type to primary keyboard action | +| `InVirtualListContext` | `React.Context` | Whether inside virtual list | +| `MessageActionsSelectedContext` | `React.Context` | Whether this message is selected | +| `MessageActionsNav` | type | Navigation state type | +| `MessageActionsState` | type | Full message actions state type | +| `useSelectedMessageBg` | hook | Returns background color for selected message | + +--- + +## messages/ Subdirectory + +--- + +### messages/AdvisorMessage.tsx + +**Purpose:** Renders advisor (internal assistant) server tool use blocks with status indicator and optional JSON input. + +**Exports:** `AdvisorMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `block` | `AdvisorBlock` | yes | The advisor content block | +| `addMargin` | `boolean` | yes | Top margin | +| `resolvedToolUseIDs` | `Set` | yes | Completed tool IDs | +| `erroredToolUseIDs` | `Set` | yes | Errored tool IDs | +| `shouldAnimate` | `boolean` | yes | Animate loader | +| `verbose` | `boolean` | yes | Verbose mode | +| `advisorModel` | `string` | no | Advisor model name | + +--- + +### messages/AssistantRedactedThinkingMessage.tsx + +**Purpose:** Renders a placeholder for redacted thinking blocks ("✻ Thinking…"). + +**Exports:** `AssistantRedactedThinkingMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | no | Top margin (default: false) | + +--- + +### messages/AssistantTextMessage.tsx + +**Purpose:** Renders assistant text response blocks. Handles many special API error string constants (rate limit messages, overload strings, etc.). + +**Exports:** `AssistantTextMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `param` | `TextBlockParam` | yes | The text content block | +| `addMargin` | `boolean` | yes | Top margin | +| `shouldShowDot` | `boolean` | yes | Show response dot prefix | +| `verbose` | `boolean` | yes | Verbose mode | +| `width` | `number` | no | Explicit width | +| `onOpenRateLimitOptions` | `() => void` | no | Opens rate limit upgrade options | + +--- + +### messages/AssistantThinkingMessage.tsx + +**Purpose:** Renders a thinking block — either as "∴ Thinking (ctrl+o to expand)" summary or full content in transcript/verbose mode. + +**Exports:** `AssistantThinkingMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `param` | `ThinkingBlock \| ThinkingBlockParam \| { type: 'thinking'; thinking: string }` | yes | Thinking content | +| `addMargin` | `boolean` | no | Top margin (default: false) | +| `isTranscriptMode` | `boolean` | yes | Show full thinking in transcript mode | +| `verbose` | `boolean` | yes | Show full thinking in verbose mode | +| `hideInTranscript` | `boolean` | no | Hide this block in transcript mode | + +--- + +### messages/AssistantToolUseMessage.tsx + +**Purpose:** Renders a tool use invocation block. Routes to the tool's own `renderToolUse` method or fallback. + +**Exports:** `AssistantToolUseMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `param` | `ToolUseBlockParam` | yes | Tool use content block | +| `addMargin` | `boolean` | yes | Top margin | +| `tools` | `Tools` | yes | Available tools | +| `commands` | `Command[]` | yes | Available commands | +| `verbose` | `boolean` | yes | Verbose mode | +| `inProgressToolUseIDs` | `Set` | yes | In-flight tool IDs | +| `progressMessagesForMessage` | `Message[]` | yes | Progress messages | +| `shouldAnimate` | `boolean` | yes | Animate spinner | +| `shouldShowDot` | `boolean` | yes | Show dot prefix | +| `inProgressToolCallCount` | `number` | no | Count of in-flight calls | +| `lookups` | `ReturnType` | yes | Message lookups | +| `isTranscriptMode` | `boolean` | no | Transcript mode | + +--- + +### messages/AttachmentMessage.tsx + +**Purpose:** Routes attachment messages to their specific renderers based on `attachment.type`. The `switch` default branch asserts `NullRenderingAttachmentType` via TypeScript to enforce exhaustiveness. + +**Exports:** `AttachmentMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `attachment` | `Attachment` | yes | Attachment to render | +| `verbose` | `boolean` | yes | Verbose mode | +| `isTranscriptMode` | `boolean` | no | Transcript mode | + +**Key behavior:** Feature-gated `EXPERIMENTAL_SKILL_SEARCH` for demo env detection. Handles `teammate_mailbox` attachment type specially (filters idle notifications before count). Uses `tryRenderPlanApprovalMessage` for plan-related attachments. + +--- + +### messages/CollapsedReadSearchContent.tsx + +**Purpose:** Renders the collapsed "Read N files, searched M patterns" summary line for collapsed read/search tool groups. + +**Exports:** `CollapsedReadSearchContent` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `CollapsedReadSearchGroup` | yes | The collapsed group | +| `inProgressToolUseIDs` | `Set` | yes | In-flight tool IDs | +| `shouldAnimate` | `boolean` | yes | Animate loader | +| `verbose` | `boolean` | yes | Verbose mode | +| `tools` | `Tools` | yes | Available tools | +| `lookups` | `ReturnType` | yes | Message lookups | +| `isActiveGroup` | `boolean` | no | Whether this is the currently active group | + +**Constants:** `MIN_HINT_DISPLAY_MS = 700` + +**Key behavior:** Feature-gated team memory (`TEAMMEM`) counts. Internal `VerboseToolUse` sub-component for verbose mode. + +--- + +### messages/CompactBoundaryMessage.tsx + +**Purpose:** Renders the "✻ Conversation compacted (ctrl+o for history)" marker at compact boundaries. + +**Exports:** `CompactBoundaryMessage` + +**Props:** None. + +--- + +### messages/GroupedToolUseContent.tsx + +**Purpose:** Renders a grouped tool use by delegating to `tool.renderGroupedToolUse`. + +**Exports:** `GroupedToolUseContent` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `GroupedToolUseMessage` | yes | The grouped tool use message | +| `tools` | `Tools` | yes | Available tools | +| `lookups` | `ReturnType` | yes | Message lookups | +| `inProgressToolUseIDs` | `Set` | yes | In-flight tool IDs | +| `shouldAnimate` | `boolean` | yes | Animate spinner | + +--- + +### messages/HighlightedThinkingText.tsx + +**Purpose:** Renders thinking/prompt text with optional KAIROS brief layout mode. Supports "You {timestamp}" header in brief layout. Applies rainbow color to "ultrathink" trigger sequences. + +**Exports:** `HighlightedThinkingText` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `text` | `string` | yes | The text to render | +| `useBriefLayout` | `boolean` | no | Use brief (compact) layout | +| `timestamp` | `string` | no | ISO timestamp for brief header | + +**Key behavior:** Uses `findThinkingTriggerPositions` and `getRainbowColor` for ultrathink highlighting. Uses `QueuedMessageContext` for queued state styling. + +--- + +### messages/HookProgressMessage.tsx + +**Purpose:** Renders hook execution progress for PreToolUse/PostToolUse hooks. + +**Exports:** `HookProgressMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `hookEvent` | `HookEvent` | yes | The hook event | +| `lookups` | `ReturnType` | yes | Message lookups | +| `toolUseID` | `string` | yes | Associated tool use ID | +| `verbose` | `boolean` | yes | Verbose mode | +| `isTranscriptMode` | `boolean` | no | Transcript mode | + +--- + +### messages/nullRenderingAttachments.ts + +**Purpose:** Defines which attachment types render as `null` (no visible output) and should be filtered before the 200-message render budget. + +**Exports:** + +| Export | Type | Description | +|---|---|---| +| `NullRenderingAttachmentType` | type | Union of 29 null-rendering attachment type strings | +| `isNullRenderingAttachment(msg)` | `(msg: Message \| NormalizedMessage) => boolean` | Returns true if message is a null-rendering attachment | + +**Null-rendering types (29 total):** `hook_success`, `hook_additional_context`, `hook_cancelled`, `command_permissions`, `agent_mention`, `budget_usd`, `critical_system_reminder`, `edited_image_file`, `edited_text_file`, `opened_file_in_ide`, `output_style`, `plan_mode`, `plan_mode_exit`, `plan_mode_reentry`, `structured_output`, `team_context`, `todo_reminder`, `context_efficiency`, `deferred_tools_delta`, `mcp_instructions_delta`, `companion_intro`, `token_usage`, `ultrathink_effort`, `max_turns_reached`, `task_reminder`, `auto_mode`, `auto_mode_exit`, `output_token_usage`, `pen_mode_enter`, `pen_mode_exit`, `verify_plan_reminder`, `current_session_memory`, `compaction_reminder`, `date_change` + +**Note:** TypeScript enforces sync: `AttachmentMessage`'s `switch` default branch asserts `attachment.type satisfies NullRenderingAttachmentType`. Tracked as CC-724. + +--- + +### messages/PlanApprovalMessage.tsx + +**Purpose:** Renders plan approval request/response display and handles plan-related attachments. + +**Exports:** `PlanApprovalRequestDisplay`, `tryRenderPlanApprovalMessage`, plus other plan approval message components. + +--- + +### messages/RateLimitMessage.tsx + +**Purpose:** Renders rate limit error messages with optional upsell text. + +**Exports:** `getUpsellMessage(params: UpsellParams): string | null`, `RateLimitMessage` + +**Key behavior:** `getUpsellMessage` returns upgrade-prompt text based on subscription type and rate limit reason. + +--- + +### messages/ShutdownMessage.tsx + +**Purpose:** Renders swarm agent shutdown-related messages. + +**Exports:** `ShutdownRequestDisplay`, `ShutdownRejectedDisplay` + +**`ShutdownRequestDisplay` Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `request` | `ShutdownRequestMessage` | yes | Shutdown request data | + +**Key behavior:** Shows warning-colored bordered box with `from` and `reason`. `ShutdownRejectedDisplay` shows subtle (grey) bordered box. + +--- + +### messages/SystemAPIErrorMessage.tsx + +**Purpose:** Displays API error messages with a countdown timer before retry. Hidden for first 3 retries. + +**Exports:** `SystemAPIErrorMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `SystemAPIErrorMessage` | yes | The error message | +| `verbose` | `boolean` | yes | Verbose mode | + +**Key state:** `countdownMs: number` + +**Constants:** `MAX_API_ERROR_CHARS = 1000` + +**Key behavior:** `hidden = retryAttempt < 4`. Uses `useInterval` to tick countdown toward `retryInMs`. Shows `retryAttempt / maxRetries` progress. + +--- + +### messages/SystemTextMessage.tsx + +**Purpose:** Renders all system message subtypes: turn duration, stop hook summary, bridge status, thinking, memory saved, and generic text. + +**Exports:** `SystemTextMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `SystemMessage` | yes | System message to render | +| `addMargin` | `boolean` | yes | Top margin | +| `verbose` | `boolean` | yes | Verbose mode | +| `isTranscriptMode` | `boolean` | no | Transcript mode | + +**Key behavior:** Routes to `TurnDurationMessage`, `StopHookSummaryMessage`, etc. based on `message.subtype`. Feature-gated `TEAMMEM` for memory saved display. Uses `TURN_COMPLETION_VERBS` for random completion verb selection. + +--- + +### messages/TaskAssignmentMessage.tsx + +**Purpose:** Renders a task assignment from the coordinator to a sub-agent. + +**Exports:** `TaskAssignmentDisplay` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `assignment` | `TaskAssignmentMessage` | yes | Task assignment data | + +**Key behavior:** Renders cyan-bordered box with task ID, assigned-by, subject, and optional description. + +--- + +### messages/UserAgentNotificationMessage.tsx + +**Purpose:** Renders agent task-completion notifications extracted from XML `` tag. Shows colored bullet with summary and optional detail line. + +**Exports:** `UserAgentNotificationMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `param` | `TextBlockParam` | yes | Text block containing `` XML | + +**Key behavior:** Extracts `` and `` inner tags. `getStatusColor(status)` maps: `'completed'` → `'success'`, `'failed'` → `'error'`, `'killed'` → `'warning'`, default → `'text'`. Renders `BLACK_CIRCLE` in status color followed by summary text. Returns null if no summary. + +--- + +### messages/UserBashInputMessage.tsx + +**Purpose:** Renders the bash command input (extracted from `` XML tag) with bash border styling. + +**Exports:** `UserBashInputMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `param` | `TextBlockParam` | yes | Text block containing bash input tag | + +**Key behavior:** Extracts `` tag. Returns null if no input. Renders `! {command}` with `bashMessageBackgroundColor`. + +--- + +### messages/UserBashOutputMessage.tsx + +**Purpose:** Renders bash tool output (stdout + stderr) by delegating to `BashToolResultMessage`. + +**Exports:** `UserBashOutputMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `content` | `string` | yes | Raw bash output XML | +| `verbose` | `boolean` | yes | Verbose mode | + +**Key behavior:** Extracts `` and `` tags (handles `` within stdout). Delegates to `BashToolResultMessage`. + +--- + +### messages/UserChannelMessage.tsx + +**Purpose:** Renders messages received over the bridge/channel connection (e.g., from Slack plugin). Parses `content` XML format. + +**Exports:** `UserChannelMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `param` | `TextBlockParam` | yes | Text block containing `` XML | + +**Constants:** `TRUNCATE_AT = 60` — body is whitespace-collapsed and truncated to 60 characters for display. + +**Key behavior:** Regex parses `source`, optional `user` attribute, and body content. Plugin-provided server names (e.g., `plugin:slack-channel:slack`) are stripped to the leaf after the last `:`. Shows `CHANNEL_ARROW` prefix, server name, optional user attribution, and truncated message body. Returns null if regex doesn't match. + +--- + +### messages/UserCommandMessage.tsx + +**Purpose:** Renders a user slash command (e.g., `/help`, `/compact`) extracted from XML `` tag. Supports both regular command display and `skill-format` display. + +**Exports:** `UserCommandMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `param` | `TextBlockParam` | yes | Text block containing `` XML | + +**Key behavior:** Extracts `` and `` tags. If `true` is present, renders as `Skill(commandName)` with subtle pointer icon. Otherwise renders the command name with `figures.pointer`. Returns null if no command message found. + +--- + +### messages/UserImageMessage.tsx + +**Purpose:** Renders an image attachment in user messages. Shows as a clickable OSC 8 hyperlink if image is stored and terminal supports hyperlinks. + +**Exports:** `UserImageMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `imageId` | `number` | no | ID for stored image lookup via `getStoredImagePath` | +| `addMargin` | `boolean` | no | Top margin (for images starting a new turn without preceding text) | + +**Key behavior:** Label is `[Image #N]` or `[Image]`. If `imageId` is provided and `getStoredImagePath(imageId)` returns a path and `supportsHyperlinks()` is true, wraps content in an Ink `Link` with a `file://` URL. When `addMargin` is true, wraps in `Box marginTop={1}`; otherwise uses `MessageResponse` styling to appear connected to the message above. + +--- + +### messages/UserLocalCommandOutputMessage.tsx + +**Purpose:** Renders local command output from `` and `` tags. Used for displaying output from locally-executed hooks/commands. + +**Exports:** `UserLocalCommandOutputMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `content` | `string` | yes | Raw XML containing stdout/stderr tags | + +**Key behavior:** Extracts `` and `` tags. If neither is present, renders `NO_CONTENT_MESSAGE` dimmed in a `MessageResponse`. Each non-empty trimmed stream is rendered via internal `IndentedContent` component. `IndentedContent` checks if content already starts with `DIAMOND_OPEN` or `DIAMOND_FILLED` prefix (avoids double-prefixing); otherwise renders indented with `Markdown` or plain text depending on content type. + +--- + +### messages/UserMemoryInputMessage.tsx + +**Purpose:** Renders a user-triggered memory save notification. Displays the memory content with a distinctive `#` prefix and a randomly-chosen acknowledgment phrase. + +**Exports:** `UserMemoryInputMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `text` | `string` | yes | Raw text containing `` XML tag | + +**Key behavior:** Extracts `` tag content. Returns null if not found. Renders the memory content with `#` prefix in `remember` color on `memoryBackgroundColor` background. Shows a randomized acknowledgment (`'Got it.'`, `'Good to know.'`, `'Noted.'`) from `lodash-es/sample` in a `MessageResponse` with `height={1}`. + +--- + +### messages/UserPlanMessage.tsx + +**Purpose:** Renders a plan content block with structured formatting. + +**Exports:** `UserPlanMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `planContent` | `string` | yes | Plan content text | + +--- + +### messages/UserPromptMessage.tsx + +**Purpose:** Renders a user text prompt with truncation for long inputs. Feature-gated brief layout for KAIROS. + +**Exports:** `UserPromptMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `param` | `TextBlockParam` | yes | Text content block | +| `isTranscriptMode` | `boolean` | no | Transcript mode | +| `timestamp` | `string` | no | ISO timestamp | + +**Constants:** `MAX_DISPLAY_CHARS = 10000`, `TRUNCATE_HEAD_CHARS = 2500`, `TRUNCATE_TAIL_CHARS = 2500` + +**Key behavior:** Feature-gated `KAIROS` / `KAIROS_BRIEF` for `isBriefOnly` layout. Truncates to `HEAD...TAIL` with "+N chars omitted" notice. + +--- + +### messages/UserResourceUpdateMessage.tsx + +**Purpose:** Renders MCP resource and polling update notifications, showing what resources changed and why. + +**Exports:** `UserResourceUpdateMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `param` | `TextBlockParam` | yes | Text block containing `` / `` XML | + +**Internal types:** + +```ts +type ParsedUpdate = { + kind: 'resource' | 'polling'; + server: string; + target: string; // URI for resource updates, tool name for polling + reason?: string; +} +``` + +**Key behavior:** `parseUpdates(text)` uses two regexes: `` and ``. `formatUri(uri)` strips `file://` prefix and shows filename only; truncates other URIs to 39 chars + ellipsis if over 40. Shows `REFRESH_ARROW` icon, server name, target, and optional reason per update. Returns null if no updates found. + +--- + +### messages/UserTeammateMessage.tsx + +**Purpose:** Renders messages from teammate (sub-agent/coordinator) delivered via the mailbox XML protocol. Dispatches to specialized renderers for plans, shutdowns, and task assignments. + +**Exports:** `TeammateMessageContent`, `UserTeammateMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `param` | `TextBlockParam` | yes | Text block containing `` XML tags | +| `isTranscriptMode` | `boolean` | no | Transcript display mode | + +**Internal type:** + +```ts +type ParsedMessage = { + teammateId: string; + content: string; + color?: string; + summary?: string; +} +``` + +**Key behavior:** `TEAMMATE_MSG_REGEX` matches `content`. Pre-filters messages: removes `isShutdownApproved` lifecycle messages and `teammate_terminated` JSON payloads (avoids blank line artifacts). For each remaining message, tries in order: `tryRenderPlanApprovalMessage`, `tryRenderShutdownMessage`, `tryRenderTaskAssignmentMessage`. Falls back to generic rendering with teammate ID as colored label (using `toInkColor(color)`) and content. Special `'leader'` teammateId displays as `'leader'`. + +--- + +### messages/UserTextMessage.tsx + +**Purpose:** Master router for user text messages. Extracts XML tags and dispatches to specific sub-renderers. + +**Exports:** `UserTextMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `addMargin` | `boolean` | yes | Top margin | +| `param` | `TextBlockParam` | yes | Text content block | +| `verbose` | `boolean` | yes | Verbose mode | +| `planContent` | `string` | no | Plan content extracted separately | +| `isTranscriptMode` | `boolean` | no | Transcript mode | +| `timestamp` | `string` | no | ISO timestamp | + +**Key behavior:** Dispatches to: `UserPlanMessage` (planContent), `UserBashInputMessage` (``), `UserBashOutputMessage` (``/``), `UserCommandMessage` (``), `UserLocalCommandOutputMessage` (``), `UserMemoryInputMessage` (``), `UserTeammateMessage` (``), `UserAgentNotificationMessage` (``), `InterruptedByUser` (INTERRUPT_MESSAGE), `UserResourceUpdateMessage`, `UserPromptMessage` (fallthrough). + +--- + +### messages/teamMemCollapsed.tsx + +**Purpose:** Renders team memory operation counts (reads/searches/writes) for the collapsed read/search summary. Loaded lazily when `feature('TEAMMEM')` is true. + +**Exports:** `checkHasTeamMemOps(message: CollapsedReadSearchGroup): boolean`, `TeamMemCountParts` + +**`TeamMemCountParts` Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `CollapsedReadSearchGroup` | yes | The collapsed group | +| `isActiveGroup` | `boolean` | yes | Whether currently active | +| `hasPrecedingParts` | `boolean` | yes | Whether other counts precede this | + +**Key behavior:** Plain function (not React component) to prevent React Compiler from memoizing property accesses. Renders comma-separated "Recalled N", "Searched N", "Saved N" parts. + +--- + +### messages/teamMemSaved.ts + +**Purpose:** Utility for extracting team memory save info from a system memory-saved message. + +**Exports:** `teamMemSavedPart(message: SystemMemorySavedMessage): { segment: string; count: number } | null` + +--- + +## messages/UserToolResultMessage/ Subdirectory + +--- + +### UserToolResultMessage/UserToolResultMessage.tsx + +**Purpose:** Routes tool result blocks to specific sub-renderers based on result status. + +**Exports:** `UserToolResultMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `param` | `ToolResultBlockParam` | yes | Tool result content | +| `message` | `NormalizedUserMessage` | yes | Parent message | +| `lookups` | `ReturnType` | yes | Message lookups | +| `progressMessagesForMessage` | `Message[]` | yes | Progress messages | +| `style` | `'condensed'` | no | Condensed display mode | +| `tools` | `Tools` | yes | Available tools | +| `verbose` | `boolean` | yes | Verbose mode | +| `width` | `number` | yes | Render width | +| `isTranscriptMode` | `boolean` | no | Transcript mode | + +**Key behavior:** Routes to: +- `UserToolCanceledMessage` — when result text matches `CANCEL_MESSAGE` +- `UserToolRejectMessage` — when result text matches `REJECT_MESSAGE` or `INTERRUPT` +- `UserToolErrorMessage` — when `is_error: true` +- `UserToolSuccessMessage` — otherwise + +--- + +### UserToolResultMessage/UserToolCanceledMessage.tsx + +**Purpose:** No-props component. Renders the "Interrupted · What should Claude do instead?" message for explicitly canceled tool uses. + +**Exports:** `UserToolCanceledMessage` + +**Props:** None. + +**Key behavior:** Wraps `InterruptedByUser` in a `MessageResponse` with `height={1}`. Fully static — memoized as a module-level constant by React Compiler. + +--- + +### UserToolResultMessage/UserToolErrorMessage.tsx + +**Purpose:** Renders error results for tool uses. Detects interrupt, plan rejection, classifier denial, and custom reject-with-reason prefix before falling back to the tool's own error renderer. + +**Exports:** `UserToolErrorMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `progressMessagesForMessage` | `ProgressMessage[]` | yes | Progress messages for this tool use | +| `tool` | `Tool` | no | Tool definition (undefined for old conversations) | +| `tools` | `Tools` | yes | All available tools | +| `param` | `ToolResultBlockParam` | yes | Tool result block | +| `verbose` | `boolean` | yes | Verbose mode | +| `isTranscriptMode` | `boolean` | no | Transcript mode | + +**Key behavior:** Routes in priority order: +1. `INTERRUPT_MESSAGE_FOR_TOOL_USE` → `InterruptedByUser` in `MessageResponse` +2. `PLAN_REJECTION_PREFIX` → `RejectedPlanMessage` with plan content extracted +3. `isClassifierDenial(param.content)` or `REJECT_MESSAGE_WITH_REASON_PREFIX` → `RejectedToolUseMessage` +4. If tool defines `renderToolUseErrorMessage` → delegates to tool renderer +5. Fallback → `FallbackToolUseErrorMessage` + +--- + +### UserToolResultMessage/UserToolRejectMessage.tsx + +**Purpose:** Renders rejection messages for tool uses that were explicitly rejected by user permission rules. Routes to tool-specific renderer or generic fallback. + +**Exports:** `UserToolRejectMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `input` | `Record` | yes | Tool input parameters | +| `progressMessagesForMessage` | `ProgressMessage[]` | yes | Progress messages | +| `style` | `'condensed'` | no | Condensed display mode | +| `tool` | `Tool` | no | Tool definition | +| `tools` | `Tools` | yes | All available tools | +| `lookups` | `ReturnType` | yes | Message lookups | +| `verbose` | `boolean` | yes | Verbose mode | +| `isTranscriptMode` | `boolean` | no | Transcript mode | + +**Key behavior:** If tool has no `renderToolUseRejectedMessage`, falls back to `FallbackToolUseRejectedMessage`. Otherwise calls `tool.renderToolUseRejectedMessage(input, columns, theme, verbose, isTranscriptMode, filterToolProgressMessages(...))`. + +--- + +### UserToolResultMessage/UserToolSuccessMessage.tsx + +**Purpose:** Renders successful tool result output. Validates against the tool's output schema before rendering, and supports classifier approval banners and KAIROS brief mode. + +**Exports:** `UserToolSuccessMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `message` | `NormalizedUserMessage` | yes | Parent message | +| `lookups` | `ReturnType` | yes | Message lookups | +| `toolUseID` | `string` | yes | Tool use ID for result lookup | +| `progressMessagesForMessage` | `ProgressMessage[]` | yes | Progress messages | +| `style` | `'condensed'` | no | Condensed display mode | +| `tool` | `Tool` | no | Tool definition | +| `tools` | `Tools` | yes | All available tools | +| `verbose` | `boolean` | yes | Verbose mode | +| `width` | `number \| string` | yes | Render width | +| `isTranscriptMode` | `boolean` | no | Transcript mode | + +**Key behavior:** +- Feature-gated `KAIROS` / `KAIROS_BRIEF` reads `isBriefOnly` from `useAppState` (same pattern as `UserPromptMessage` to avoid subscriptions in non-KAIROS builds). +- Captures `classifierRule` and `yoloReason` via `useState` lazy initializer from `getClassifierApproval(toolUseID)` / `getYoloClassifierApproval(toolUseID)`. Deletes from map on effect to prevent linear memory growth. +- Validates `message.toolUseResult` against `tool.outputSchema?.safeParse(...)` — returns null if parse fails (handles corrupt/old-format resumed transcripts; issue anthropics/claude-code#39817). +- Wraps tool's `renderToolResultMessage` in `SentryErrorBoundary`. +- Renders `HookProgressMessage` items from filtered progress messages. + +--- + +### UserToolResultMessage/RejectedPlanMessage.tsx + +**Purpose:** Renders a user-rejected plan approval. Shows the plan content in a styled box. + +**Exports:** `RejectedPlanMessage` + +**Props:** + +| Prop | Type | Required | Description | +|---|---|---|---| +| `plan` | `string` | yes | Plan content text | + +**Key behavior:** Renders "User rejected Claude's plan:" label in subtle color above a round-bordered `planMode`-colored box containing the plan rendered via `Markdown`. Uses `overflow="hidden"` for correct Windows Terminal rendering. + +--- + +### UserToolResultMessage/RejectedToolUseMessage.tsx + +**Purpose:** No-props component. Renders the generic "Tool use rejected" dimmed message. + +**Exports:** `RejectedToolUseMessage` + +**Props:** None. + +**Key behavior:** Fully static — renders dimmed "Tool use rejected" text in a `MessageResponse` with `height={1}`. Memoized as a module-level constant by React Compiler. + +--- + +### UserToolResultMessage/utils.tsx + +**Purpose:** Shared React hook for resolving a `Tool` definition from a tool use ID via message lookups. + +**Exports:** `useGetToolFromMessages(toolUseID: string, tools: Tools, lookups: ReturnType): { tool: Tool; toolUse: ToolUseBlockParam } | null` + +**Key behavior:** Memoized hook. Looks up `toolUseByToolUseID.get(toolUseID)`, then resolves via `findToolByName(tools, toolUse.name)`. Returns null if either lookup fails. diff --git a/spec/05_components_agents_permissions_design.md b/spec/05_components_agents_permissions_design.md new file mode 100644 index 0000000..c6e57d1 --- /dev/null +++ b/spec/05_components_agents_permissions_design.md @@ -0,0 +1,1952 @@ +# Claude Code — Components: Agents, Permissions, Design System & Feature Modules + +This document provides an exhaustive spec for all components in the `src/components/` subdirectories covering agents management, permission UIs, the design system primitives, and the many feature-specific modules (MCP, memory, tasks, teams, diff, grove, hooks, HelpV2, TrustDialog, ManagedSettingsSecurityDialog, ClaudeCodeHint, HighlightedCode, LogoV2, DesktopUpsell, FeedbackSurvey, LspRecommendation, Passes, Spinner, PromptInput, CustomSelect, Settings, sandbox, shell, skills, ui, wizard). + +--- + +## Table of Contents + +1. [agents/](#1-agents) +2. [permissions/](#2-permissions) +3. [design-system/](#3-design-system) +4. [wizard/](#4-wizard) +5. [mcp/](#5-mcp) +6. [memory/](#6-memory) +7. [tasks/](#7-tasks) +8. [teams/](#8-teams) +9. [diff/](#9-diff) +10. [grove/](#10-grove) +11. [hooks/](#11-hooks) +12. [HelpV2/](#12-helpv2) +13. [TrustDialog/](#13-trustdialog) +14. [ManagedSettingsSecurityDialog/](#14-managedsettingssecuritydialog) +15. [ClaudeCodeHint/](#15-claudecodehint) +16. [HighlightedCode/](#16-highlightedcode) +17. [LogoV2/](#17-logov2) +18. [DesktopUpsell/](#18-desktopupsell) +19. [FeedbackSurvey/](#19-feedbacksurvey) +20. [LspRecommendation/](#20-lsprecommendation) +21. [Passes/](#21-passes) +22. [Spinner/](#22-spinner) +23. [PromptInput/](#23-promptinput) +24. [CustomSelect/](#24-customselect) +25. [Settings/](#25-settings) +26. [sandbox/](#26-sandbox) +27. [shell/](#27-shell) +28. [skills/](#28-skills) +29. [ui/](#29-ui) + +--- + +## 1. agents/ + +The agents subsystem provides a full UI for creating, viewing, editing, deleting, and listing custom Claude sub-agents (stored as Markdown files with YAML front matter in `.claude/agents/`). + +### 1.1 AgentDetail + +**File:** `agents/AgentDetail.tsx` + +**Purpose:** Renders a read-only detail view of a single `AgentDefinition`, showing all agent metadata (file path, description, tools, model, permission mode, memory, hooks, skills, color swatch, and system prompt for non-built-in agents). + +**Props Interface:** +```typescript +type Props = { + agent: AgentDefinition; // The agent to display + tools: Tools; // All available tools (for resolving tool names) + allAgents?: AgentDefinition[]; // All agents (unused in current render, passed for context) + onBack: () => void; // Called when user presses Esc or Enter +} +``` + +**Key Behaviors:** +- Resolves tool list via `resolveAgentTools(agent, tools, false)` — shows "All tools", named valid tools, and warns about unrecognized tool names with a warning symbol. +- Computes `backgroundColor` from `getAgentColor(agent.agentType)` for the color swatch preview. +- Binds `confirm:no` keybinding in Confirmation context to call `onBack`. +- Binds `return` keypress to `onBack`. +- Renders system prompt via `` only for non-built-in agents (`!isBuiltInAgent(agent)`). +- Displays model via `getAgentModelDisplay(agent.model)`. +- Displays memory via `getMemoryScopeDisplay(agent.memory)`. +- Displays skills count inline when > 10 entries. + +**Exports:** `AgentDetail` + +--- + +### 1.2 AgentEditor + +**File:** `agents/AgentEditor.tsx` + +**Purpose:** Full-screen editor for modifying an existing `AgentDefinition`. Supports editing the agent type (name), system prompt, description (whenToUse), tools, model, color, memory, and effort. Persists changes by calling `updateAgentFile`. Provides a step-by-step editing flow using a sub-state machine. + +**Props Interface:** (large component; reconstructed from source) +```typescript +type Props = { + agent: AgentDefinition; // The agent to edit + tools: Tools; // All available tools + existingAgents: AgentDefinition[]; // For duplicate-name validation + onComplete: (message: string) => void; // Called after successful save + onCancel: () => void; // Called when user cancels +} +``` + +**Key Behaviors:** +- Multi-step flow: main edit menu → individual field editors (each with their own step). +- Edit menu shows: Edit system prompt, Edit description, Select tools, Select model, Choose color, (conditional) Choose memory, Back. +- Each field editor reuses the same sub-components used in wizard steps (ToolSelector, ModelSelector, ColorPicker, etc.). +- On save calls `updateAgentFile(agent, ...)` and reloads agent state in app state. +- Validates agent type via `validateAgentType()` before allowing name changes. +- Uses React compiler memoization throughout. + +**Exports:** `AgentEditor` + +--- + +### 1.3 AgentNavigationFooter + +**File:** `agents/AgentNavigationFooter.tsx` + +**Purpose:** Renders a dimmed footer line with keyboard navigation instructions. Integrates with Ctrl+C/D exit state to show "Press X again to exit" during double-tap detection. + +**Props Interface:** +```typescript +type Props = { + instructions?: string; + // Default: "Press ↑↓ to navigate · Enter to select · Esc to go back" +} +``` + +**Key Behaviors:** +- Calls `useExitOnCtrlCDWithKeybindings()` to detect pending exit. +- When `exitState.pending` is true, overrides instruction text with `"Press ${exitState.keyName} again to exit"`. +- Renders with `marginLeft={2}` and `dimColor`. + +**Exports:** `AgentNavigationFooter` + +--- + +### 1.4 AgentsList + +**File:** `agents/AgentsList.tsx` + +**Purpose:** Displays a list of agents for a given source (all, built-in, userSettings, projectSettings, etc.), grouped by source category. Supports keyboard navigation, optional "Create new agent" entry, and shows override warnings when an agent is shadowed by another scope. + +**Props Interface:** +```typescript +type Props = { + source: SettingSource | 'all' | 'built-in' | 'plugin'; + agents: ResolvedAgent[]; // Agents resolved with override info + onBack: () => void; + onSelect: (agent: AgentDefinition) => void; + onCreateNew?: () => void; // If provided, shows "Create new agent" option at top + changes?: string[]; // Recent change messages to display +} +``` + +**Key Behaviors:** +- State: `selectedAgent` (currently highlighted agent), `isCreateNewSelected` (boolean, focuses "Create new agent"). +- Auto-selects first agent or "Create new" if nothing selected. +- `handleKeyDown`: Up/Down arrows navigate the list (wraps around). Enter activates selected item. +- Groups agents by source using `AGENT_SOURCE_GROUPS`. +- Built-in agents are rendered separately with `renderBuiltInAgentsSection()` (dimmed, no pointer). +- Per-agent display: name, optional model display (middot-separated), memory label, override shadow warning. +- Shadowed agents shown dimmed with a warning symbol (`figures.warning`) and "shadowed by X" text. +- Changes list shown at top with success color. + +**Exports:** `AgentsList` + +--- + +### 1.5 AgentsMenu + +**File:** `agents/AgentsMenu.tsx` + +**Purpose:** Top-level orchestrator for the agents management UI. Implements a state machine with modes: `list-agents`, `create-agent`, `agent-menu`, `view-agent`, `edit-agent`, `delete-confirm`. Reads/writes app state for agent definitions. + +**Props Interface:** +```typescript +type Props = { + tools: Tools; + onExit: (result?: string, options?: { display?: CommandResultDisplay }) => void; +} +``` + +**Key Behaviors:** +- `modeState` union type discriminated by `mode` field. +- Reads `agentDefinitions`, `mcpTools`, `toolPermissionContext` from app state. +- Merges tools via `useMergedTools()`. +- Groups agents into 8 buckets by source (built-in, userSettings, projectSettings, policySettings, localSettings, flagSettings, plugin, all). +- Resolves display-time overrides via `resolveAgentOverrides()`. +- `handleAgentCreated`: adds change message, returns to list-agents/all view. +- `handleAgentDeleted`: calls `deleteAgentFromFile()`, updates app state, adds change message. +- In `agent-menu` mode: renders a `` component for choosing an agent model from the standard list (`getAgentModelOptions()`). Injects the current model as a custom option if it is a full model ID not in the standard alias list. + +**Props Interface:** +```typescript +interface ModelSelectorProps { + initialModel?: string; // Current model value + onComplete: (model?: string) => void; // Called with selected model + onCancel?: () => void; // If absent, cancel calls onComplete(undefined) +} +``` + +**Key Behaviors:** +- If `initialModel` is not in the standard options list, prepends `{ value: initialModel, label: initialModel, description: "Current model (custom ID)" }`. +- Default value: `initialModel ?? 'sonnet'`. +- Cancel: calls `onCancel()` if provided, otherwise `onComplete(undefined)`. +- Renders a description line: "Model determines the agent's reasoning capabilities and speed." + +**Exports:** `ModelSelector` + +--- + +### 1.8 ToolSelector + +**File:** `agents/ToolSelector.tsx` + +**Purpose:** Multi-select UI for choosing which tools an agent can access. Groups tools into buckets: Read-only, Edit, Execution, MCP (per-server), and Other. Supports "All tools" wildcard selection. + +**Props Interface:** +```typescript +type Props = { + tools: Tools; // All available tools + initialTools: string[] | undefined; // Pre-selected tool names (undefined = all) + onComplete: (selectedTools: string[] | undefined) => void; // undefined = all tools + onCancel?: () => void; +} + +type ToolBucket = { + name: string; + toolNames: Set; + isMcp?: boolean; +}; + +type ToolBuckets = { + READ_ONLY: ToolBucket; // GlobTool, GrepTool, FileReadTool, WebFetchTool, etc. + EDIT: ToolBucket; // FileEditTool, FileWriteTool, NotebookEditTool + EXECUTION: ToolBucket; // BashTool, (TungstenTool for internal builds) + MCP: ToolBucket; // Dynamic, populated per MCP server + OTHER: ToolBucket; // Uncategorized catch-all +}; +``` + +**Key Behaviors:** +- MCP tools grouped dynamically by server name via `getMcpServerBuckets()`. +- Tools not matching any bucket go into OTHER. +- AGENT_TOOL_NAME excluded from selectable tools. +- Toggle selection per individual tool; "Select all" / "Deselect all" shortcuts within buckets. +- Up/Down navigation; Space/Enter toggle; Tab/Shift+Tab move between buckets. +- On submit: if no tools selected and wildcard not set, passes empty array. Pressing a confirm shortcut with no selection can also pass `undefined` (all tools). + +**Exports:** `ToolSelector` + +--- + +### 1.9 agentFileUtils.ts + +**File:** `agents/agentFileUtils.ts` + +**Purpose:** File system utility functions for reading and writing agent definition Markdown files. + +**Exports:** + +```typescript +// Formats agent fields into a Markdown file with YAML front matter +function formatAgentAsMarkdown( + agentType: string, + whenToUse: string, + tools: string[] | undefined, + systemPrompt: string, + color?: string, + model?: string, + memory?: AgentMemoryScope, + effort?: EffortValue, +): string + +// Returns the absolute directory for an agent based on source +function getAgentDirectoryPath(location: SettingSource): string // (private) + +// Returns relative directory path for display +function getRelativeAgentDirectoryPath(location: SettingSource): string // (private) + +// Path for a NEW agent file (uses agentType as filename) +function getNewAgentFilePath(agent: { source: SettingSource; agentType: string }): string + +// Path for an EXISTING agent (uses actual filename if different from agentType) +function getActualAgentFilePath(agent: AgentDefinition): string + +// Relative path for new agent display +function getNewRelativeAgentFilePath(agent: { source: SettingSource | 'built-in'; agentType: string }): string + +// Relative path for existing agent display (handles built-in/plugin/flagSettings) +function getActualRelativeAgentFilePath(agent: AgentDefinition): string + +// Ensures directory exists then writes agent file +async function saveAgentToFile( + source: SettingSource | 'built-in', + agentType: string, + whenToUse: string, + tools: string[] | undefined, + systemPrompt: string, + checkExists?: boolean, // default true — throws EEXIST if file already exists + color?: string, + model?: string, + memory?: AgentMemoryScope, + effort?: EffortValue, +): Promise + +// Overwrites an existing agent's file +async function updateAgentFile( + agent: AgentDefinition, + newWhenToUse: string, + newTools: string[] | undefined, + newSystemPrompt: string, + newColor?: string, + newModel?: string, + newMemory?: AgentMemoryScope, + newEffort?: EffortValue, +): Promise + +// Removes the agent's file (ignores ENOENT) +async function deleteAgentFromFile(agent: AgentDefinition): Promise +``` + +**Key Behaviors:** +- All writes use `writeFileAndFlush` which calls `handle.datasync()` after writing for durability. +- `formatAgentAsMarkdown` escapes backslashes, double quotes, and newlines in `whenToUse` for YAML double-quoted strings. +- Tools field omitted entirely when `undefined` or `['*']` (means all tools allowed). + +--- + +### 1.10 generateAgent.ts + +**File:** `agents/generateAgent.ts` + +**Purpose:** Uses an LLM call (`queryModelWithoutStreaming`) to auto-generate an agent configuration from a natural language user prompt. Returns `identifier`, `whenToUse`, and `systemPrompt` as structured JSON. + +**Exports:** +```typescript +type GeneratedAgent = { + identifier: string + whenToUse: string + systemPrompt: string +} + +async function generateAgent( + userPrompt: string, + model: ModelName, + existingIdentifiers: string[], // Blocked identifiers included in prompt + abortSignal: AbortSignal, +): Promise +``` + +**Key Behaviors:** +- Uses a detailed system prompt (`AGENT_CREATION_SYSTEM_PROMPT`) instructing Claude on how to design agent personas, write system prompts, create identifiers (lowercase, 2-4 words, hyphens only, no "helper"/"assistant"). +- When `isAutoMemoryEnabled()`, appends `AGENT_MEMORY_INSTRUCTIONS` to the system prompt. +- Prepends user context via `prependUserContext()`. +- Parses JSON from response — falls back to regex extraction if direct parse fails. +- Fires `tengu_agent_definition_generated` analytics event. +- Throws on invalid/missing fields. + +--- + +### 1.11 types.ts + +**File:** `agents/types.ts` + +**Purpose:** Shared type definitions for the agents UI state machine. + +**Exports:** +```typescript +const AGENT_PATHS = { + FOLDER_NAME: '.claude', + AGENTS_DIR: 'agents', +} as const + +type ModeState = + | { mode: 'main-menu' } + | { mode: 'list-agents'; source: SettingSource | 'all' | 'built-in' } + | { mode: 'agent-menu'; agent: AgentDefinition; previousMode: ModeState } + | { mode: 'view-agent'; agent: AgentDefinition; previousMode: ModeState } + | { mode: 'create-agent' } + | { mode: 'edit-agent'; agent: AgentDefinition; previousMode: ModeState } + | { mode: 'delete-confirm'; agent: AgentDefinition; previousMode: ModeState } + +type AgentValidationResult = { + isValid: boolean + warnings: string[] + errors: string[] +} +``` + +--- + +### 1.12 utils.ts + +**File:** `agents/utils.ts` + +**Exports:** +```typescript +function getAgentSourceDisplayName( + source: SettingSource | 'all' | 'built-in' | 'plugin' +): string +// Returns: 'Agents' | 'Built-in agents' | 'Plugin agents' | capitalize(getSettingSourceName(source)) +``` + +--- + +### 1.13 validateAgent.ts + +**File:** `agents/validateAgent.ts` + +**Exports:** +```typescript +type AgentValidationResult = { + isValid: boolean + errors: string[] + warnings: string[] +} + +function validateAgentType(agentType: string): string | null +// Returns error message or null if valid. +// Rules: required, /^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$/, length 3-50 chars. + +function validateAgent( + agent: Omit, + availableTools: Tools, + existingAgents: AgentDefinition[], +): AgentValidationResult +// Validates: agentType, whenToUse (min 10 chars / max 5000), tools array, +// system prompt (min 20 chars / max 10000 chars warning threshold). +// Checks for duplicate agentType across sources. +// Uses resolveAgentTools to check for invalid tool names. +``` + +--- + +### 1.14 new-agent-creation/CreateAgentWizard + +**File:** `agents/new-agent-creation/CreateAgentWizard.tsx` + +**Purpose:** Assembles the multi-step new-agent creation wizard by composing step components inside ``. Conditionally includes `MemoryStep` based on feature flag. + +**Props Interface:** +```typescript +type Props = { + tools: Tools; + existingAgents: AgentDefinition[]; + onComplete: (message: string) => void; + onCancel: () => void; +} +``` + +**Wizard Step Order (0-indexed):** +0. `LocationStep` — project vs personal scope +1. `MethodStep` — generate with Claude vs manual +2. `GenerateStep` — natural language prompt → LLM generation (skipped for manual) +3. `TypeStep` — agent identifier / name +4. `PromptStep` — system prompt text +5. `DescriptionStep` — whenToUse description +6. `ToolsStep` — tool selection +7. `ModelStep` — model selection +8. `ColorStep` — color selection +9. `MemoryStep` — memory scope (conditional on `isAutoMemoryEnabled()`) +10. `ConfirmStepWrapper` — review and save + +**Key Behaviors:** +- WizardProvider title: "Create new agent", `showStepCounter: false`. +- `onComplete` of WizardProvider is a no-op (actual completion handled by `ConfirmStepWrapper`). +- Passes `onCancel` through to WizardProvider. + +--- + +### 1.15 Wizard Steps + +#### LocationStep +Offers two options: "Project (.claude/agents/)" (`projectSettings`) or "Personal (~/.claude/agents/)" (`userSettings`). Updates `wizardData.location` and calls `goNext()`. + +#### MethodStep +Offers "Generate with Claude (recommended)" or "Manual configuration". For generate: sets `wizardData.method = 'generate'`, `wasGenerated: true`, `goNext()`. For manual: sets `method: 'manual'`, `wasGenerated: false`, skips to step 3 via `goToStep(3)`. + +#### GenerateStep +- Text input for natural language description of the agent. +- On submit: calls `generateAgent(prompt, model, existingIdentifiers, abortSignal)`. +- Shows animated `` during generation. +- On success: populates `wizardData.agentType`, `systemPrompt`, `whenToUse`, `generatedAgent`. +- Navigates directly to step 6 (ToolsStep) after generation (skips manual name/prompt/description steps). +- Esc during generation cancels via abort signal. +- Supports external editor (`chat:externalEditor` keybinding). + +#### TypeStep +Props: `{ existingAgents: AgentDefinition[] }`. Text input for agent identifier. Validates via `validateAgentType()`. Updates `wizardData.agentType`. + +#### PromptStep +Large text input for system prompt. Min 20 chars enforced. Supports external editor. Updates `wizardData.systemPrompt`. + +#### DescriptionStep +Text input for `whenToUse`. Required, min 1 char. Supports external editor. Updates `wizardData.whenToUse`. + +#### ToolsStep +Props: `{ tools: Tools }`. Wraps `` in `WizardDialogLayout`. Updates `wizardData.selectedTools`. + +#### ModelStep +Wraps `` in `WizardDialogLayout`. Updates `wizardData.selectedModel`. + +#### ColorStep +Wraps `` in `WizardDialogLayout`. On confirm: builds `wizardData.finalAgent` object with all accumulated data, updates `wizardData.selectedColor`. + +#### MemoryStep +Conditional step (only shown when `isAutoMemoryEnabled()`). Offers memory scope options ordered by recommended scope (project-first for project agents, user-first for personal agents). Includes "None" option. Updates `wizardData.selectedMemory`. + +#### ConfirmStep / ConfirmStepWrapper +Props: `{ tools: Tools; existingAgents: AgentDefinition[]; onComplete: (message: string) => void }`. Shows a summary of all selected options, validates via `validateAgent()`, calls `saveAgentToFile()`, then reloads agents in app state and calls `onComplete(message)`. + +--- + +## 2. permissions/ + +The permissions subsystem renders interactive dialogs asking users to approve, reject, or configure rules for tool uses. Each tool has its own permission request component. + +### 2.1 Core Types & PermissionRequest + +**File:** `permissions/PermissionRequest.tsx` + +**Purpose:** Top-level dispatcher that picks the correct permission request component for each tool type and renders it. Also handles notifications and idle detection. + +**Key Types:** +```typescript +type PermissionRequestProps = { + toolUseConfirm: ToolUseConfirm; + toolUseContext: ToolUseContext; + onDone(): void; + onReject(): void; + verbose: boolean; + workerBadge: WorkerBadgeProps | undefined; + setStickyFooter?: (jsx: React.ReactNode | null) => void; + // Registers JSX in a sticky footer below scrollable area (fullscreen only). +} + +type ToolUseConfirm = { + assistantMessage: AssistantMessage; + tool: Tool; + description: string; + input: z.infer; + toolUseContext: ToolUseContext; + toolUseID: string; + permissionResult: PermissionDecision; + permissionPromptStartTimeMs: number; + classifierCheckInProgress?: boolean; + classifierAutoApproved?: boolean; + classifierMatchedRule?: string; + workerBadge?: WorkerBadgeProps; + onUserInteraction(): void; + onAbort(): void; + onDismissCheckmark?(): void; + onAllow(updatedInput, permissionUpdates: PermissionUpdate[], feedback?, contentBlocks?): void; + onReject(feedback?, contentBlocks?): void; + recheckPermission(): Promise; +} +``` + +**Tool→Component Mapping:** +| Tool | Component | +|------|-----------| +| FileEditTool | FileEditPermissionRequest | +| FileWriteTool | FileWritePermissionRequest | +| BashTool | BashPermissionRequest | +| PowerShellTool | PowerShellPermissionRequest | +| WebFetchTool | WebFetchPermissionRequest | +| NotebookEditTool | NotebookEditPermissionRequest | +| ExitPlanModeV2Tool | ExitPlanModePermissionRequest | +| EnterPlanModeTool | EnterPlanModePermissionRequest | +| SkillTool | SkillPermissionRequest | +| AskUserQuestionTool | AskUserQuestionPermissionRequest | +| GlobTool / GrepTool / FileReadTool | FilesystemPermissionRequest | +| ReviewArtifactTool (feature flag) | ReviewArtifactPermissionRequest | +| WorkflowTool (feature flag) | WorkflowPermissionRequest | +| MonitorTool (feature flag) | MonitorPermissionRequest | +| default | FallbackPermissionRequest | + +**Exports:** `PermissionRequest`, `PermissionRequestProps`, `ToolUseConfirm` + +--- + +### 2.2 PermissionDialog + +**File:** `permissions/PermissionDialog.tsx` + +**Purpose:** The shared visual container for all tool permission requests. Renders a title bar (with optional worker badge), subtitle, and children, wrapped in a styled border box. + +**Props Interface:** +```typescript +type Props = { + title: string; + subtitle?: React.ReactNode; + color?: keyof Theme; // default: 'permission' + titleColor?: keyof Theme; + innerPaddingX?: number; // default: 1 + workerBadge?: WorkerBadgeProps; + titleRight?: React.ReactNode; // Right-aligned content in title row + children: React.ReactNode; +} +``` + +**Key Behaviors:** +- Renders `` with the title, subtitle, optional color override, and workerBadge. +- Title row uses `justifyContent="space-between"` to place `titleRight` on the right. +- Children rendered in inner `Box` with `paddingX={innerPaddingX}`. + +**Exports:** `PermissionDialog` + +--- + +### 2.3 BashPermissionRequest + +**File:** `permissions/BashPermissionRequest/BashPermissionRequest.tsx` + +**Purpose:** Permission dialog for `BashTool`. Handles classifier-based auto-approval animation, sed edit detection (redirects to `SedEditPermissionRequest`), sandbox detection, and a rich set of options. + +**Key Behaviors:** +- `ClassifierCheckingSubtitle`: separate sub-component that renders an animated shimmer "Attempting to auto-approve…" text at 20fps, isolated to prevent full-dialog re-renders. +- Checks `classifierCheckInProgress` prop — shows shimmer subtitle while classifier runs. +- If command matches a `sed` edit pattern, delegates to `SedEditPermissionRequest`. +- If `shouldUseSandbox()`, shows sandbox-specific options. +- Options computed by `bashToolUseOptions()`. +- Logs permission decision via `usePermissionRequestLogging()`. +- Supports destructive command warning display. + +**Exports:** `BashPermissionRequest` + +--- + +### 2.4 FilePermissionDialog + +**File:** `permissions/FilePermissionDialog/FilePermissionDialog.tsx` + +**Purpose:** Generic reusable dialog for file-operation permissions (used by FileEdit, FileWrite, NotebookEdit permission requests). Handles path display, symlink detection, IDE diff integration, and option rendering. + +**Props Interface:** +```typescript +type FilePermissionDialogProps = { + toolUseConfirm: ToolUseConfirm; + toolUseContext: ToolUseContext; + onDone: () => void; + onReject: () => void; + title: string; + subtitle?: React.ReactNode; + question?: string | React.ReactNode; // default: 'Do you want to proceed?' + content?: React.ReactNode; + completionType?: CompletionType; // default: 'tool_use_single' + languageName?: string; // Overrides path-derived language name + path: string | null; + parseInput: (input: unknown) => T; + operationType?: FileOperationType; // default: 'write' + ideDiffSupport?: IDEDiffSupport; + workerBadge: WorkerBadgeProps | undefined; +} +``` + +**Key Behaviors:** +- Language name derived async from `getLanguageName(path)` if not overridden. +- Checks for symlink target when `operationType !== 'read'`. +- Shows `` when IDE diff is available. +- Logs via `usePermissionRequestLogging()`. + +**Exports:** `FilePermissionDialog`, `FilePermissionDialogProps` + +--- + +### 2.5 WorkerBadge + +**File:** `permissions/WorkerBadge.tsx` + +**Purpose:** Colored badge showing which swarm worker is requesting a permission. + +**Props Interface:** +```typescript +type WorkerBadgeProps = { + name: string; // Worker name (shown as @name) + color: string; // Raw color string (converted to Ink color via toInkColor) +} +``` + +**Rendering:** `● @{name}` with the bullet in the worker's color. + +**Exports:** `WorkerBadge`, `WorkerBadgeProps` + +--- + +### 2.6 Other Permission Request Components + +Each follows the `PermissionRequestProps` interface and renders inside `` or ``. + +#### FileEditPermissionRequest +Renders a diff of the proposed file edit. Uses `FilePermissionDialog` with diff content component. `operationType: 'write'`. + +#### FileWritePermissionRequest +Renders the full proposed file content. Uses `FilePermissionDialog`. `operationType: 'write'`. + +#### FilesystemPermissionRequest +For GlobTool / GrepTool / FileReadTool. Shows a read-only access dialog. `operationType: 'read'`. + +#### NotebookEditPermissionRequest +For Jupyter notebook edits. Renders a notebook cell diff. Uses `FilePermissionDialog` with `languageName` derived from cell type. + +#### WebFetchPermissionRequest +Shows the URL being fetched and fetch options. + +#### PowerShellPermissionRequest +Similar to BashPermissionRequest but for PowerShell commands. Uses `powershellToolUseOptions()`. + +#### SedEditPermissionRequest +Rendered by BashPermissionRequest when sed-edit pattern is detected. Shows the file diff that the sed command would produce. + +#### EnterPlanModePermissionRequest +Simple confirmation to enter plan mode. + +#### ExitPlanModePermissionRequest +Full plan review with sticky footer for response options. Uses `setStickyFooter` for keeping response visible while scrolling. + +#### SkillPermissionRequest +For executing a skill/command. + +#### AskUserQuestionPermissionRequest +Renders a multi-question form with navigation. Subcomponents: +- `PreviewBox` — shows rendered preview of a question +- `PreviewQuestionView` — full question preview +- `QuestionNavigationBar` — prev/next navigation +- `QuestionView` — single question input +- `SubmitQuestionsView` — confirmation before submitting +- `use-multiple-choice-state.ts` — hook managing checkbox/radio state + +#### ComputerUseApproval +For computer-use actions. + +#### FallbackPermissionRequest +Generic fallback for unrecognized tool types. Shows tool name and description. + +#### SandboxPermissionRequest +Shown when a command is being run in sandbox mode. + +--- + +### 2.7 PermissionDecisionDebugInfo + +**File:** `permissions/PermissionDecisionDebugInfo.tsx` + +**Purpose:** Renders debug information about the permission decision (decision reason, rule details, classifier results) when verbose mode is enabled. + +--- + +### 2.8 PermissionExplanation + +**File:** `permissions/PermissionExplanation.tsx` + +**Purpose:** Renders an explanation of why a permission rule is being requested. `usePermissionExplainerUI` hook manages the expand/collapse state of the explainer section. + +**Exports:** `PermissionExplainerContent`, `usePermissionExplainerUI` + +--- + +### 2.9 PermissionPrompt + +**File:** `permissions/PermissionPrompt.tsx` + +**Purpose:** Wraps permission request UI with the fullscreen layout management and sticky footer support for plan mode responses. + +--- + +### 2.10 PermissionRequestTitle + +**File:** `permissions/PermissionRequestTitle.tsx` + +**Purpose:** Renders the colored title bar for permission dialogs. Shows title text in permission color with optional worker badge below. + +**Props Interface:** +```typescript +type Props = { + title: string; + subtitle?: React.ReactNode; + color?: keyof Theme; + workerBadge?: WorkerBadgeProps; +} +``` + +--- + +### 2.11 PermissionRuleExplanation + +**File:** `permissions/PermissionRuleExplanation.tsx` + +**Purpose:** Shows the permission rule suggestions (e.g., "Allow bash: git *", "Deny bash: rm -rf") that would be created if the user clicks "Always allow" or "Always deny". + +--- + +### 2.12 WorkerPendingPermission + +**File:** `permissions/WorkerPendingPermission.tsx` + +**Purpose:** Rendered in the swarm coordinator view to show a pending permission from a worker agent. + +--- + +### 2.13 hooks.ts + +**File:** `permissions/hooks.ts` + +**Exports:** +```typescript +type UnaryEvent = { + completion_type: CompletionType; + language_name: string | Promise; +} + +// Logs permission request start/end/result analytics. Called once per dialog. +function usePermissionRequestLogging( + toolUseConfirm: ToolUseConfirm, + unaryEvent: UnaryEvent, +): void +``` + +**Key Behaviors:** +- Fires `tengu_permission_request_start` on mount. +- Fires `tengu_permission_request_end` with result info on unmount. +- Converts `permissionResult` to a structured log string. + +--- + +### 2.14 shellPermissionHelpers.tsx + +**File:** `permissions/shellPermissionHelpers.tsx` + +**Purpose:** Shared JSX helpers and option builders used by `BashPermissionRequest` and `PowerShellPermissionRequest` (e.g., building the approve/deny/always-allow option list). + +--- + +### 2.15 useShellPermissionFeedback.ts + +**File:** `permissions/useShellPermissionFeedback.ts` + +**Purpose:** Hook managing the feedback text input that appears when user selects "No (feedback)" in shell permission dialogs. + +--- + +### 2.16 utils.ts + +**File:** `permissions/utils.ts` + +**Exports:** +```typescript +function logUnaryPermissionEvent( + toolUseConfirm: ToolUseConfirm, + unaryEvent: UnaryEvent, + result: string, +): void +``` + +--- + +### 2.17 rules/ subdirectory + +Permission rule management UI for the `/permissions` command screen. + +| File | Purpose | +|------|---------| +| `AddPermissionRules.tsx` | Form to add new allow/deny rules | +| `AddWorkspaceDirectory.tsx` | Form to add trusted workspace directories | +| `PermissionRuleDescription.tsx` | Renders a human-readable description of a rule | +| `PermissionRuleInput.tsx` | Text input + validation for rule patterns | +| `PermissionRuleList.tsx` | Shows existing rules with delete option | +| `RecentDenialsTab.tsx` | Tab showing recently denied tool uses (can convert to rules) | +| `RemoveWorkspaceDirectory.tsx` | Confirmation dialog for removing workspace directory | +| `WorkspaceTab.tsx` | Tab showing trusted workspace directories | + +--- + +## 3. design-system/ + +Reusable primitive components that form the visual foundation of all terminal UI. + +### 3.1 Byline + +**File:** `design-system/Byline.tsx` + +**Purpose:** Joins children with a middot separator (` · `) for inline metadata display. Automatically filters null/undefined/false children. + +**Props Interface:** +```typescript +type Props = { + children: React.ReactNode; +} +``` + +**Key Behaviors:** +- Uses `Children.toArray()` which filters falsy nodes. +- Returns `null` if no valid children. +- Renders separators only between adjacent valid elements. + +**Exports:** `Byline` + +--- + +### 3.2 Dialog + +**File:** `design-system/Dialog.tsx` + +**Purpose:** Confirm/cancel dialog container. Registers `confirm:no` and Ctrl+C/D keybindings. Shows title, optional subtitle, children, and a keyboard hint footer. + +**Props Interface:** +```typescript +type DialogProps = { + title: React.ReactNode; + subtitle?: React.ReactNode; + children: React.ReactNode; + onCancel: () => void; + color?: keyof Theme; // default: 'permission' + hideInputGuide?: boolean; + hideBorder?: boolean; + inputGuide?: (exitState: ExitState) => React.ReactNode; // Custom footer + isCancelActive?: boolean; // default: true — controls keybinding activation +} +``` + +**Key Behaviors:** +- `isCancelActive=false`: disables the `confirm:no` and exit keybindings (useful when embedded TextInput needs Esc). +- Default input guide: "Enter to confirm · Esc to cancel" (or "Press X again to exit" when exit pending). +- Wraps content in `` unless `hideBorder`. +- Title rendered in bold with `color`. + +**Exports:** `Dialog` + +--- + +### 3.3 Divider + +**File:** `design-system/Divider.tsx` + +**Props Interface:** +```typescript +type DividerProps = { + width?: number; // Defaults to terminal width + color?: keyof Theme; // If absent, uses dimColor + char?: string; // default: '─' (U+2500) + padding?: number; // Subtracted from width, default: 0 + title?: string; // Centered title (may contain ANSI codes) +} +``` + +**Key Behaviors:** +- Without title: `char.repeat(effectiveWidth)` in a ``. +- With title: left fill + space + title + space + right fill. Left/right fill split evenly (left gets floor half). +- Terminal width queried via `useTerminalSize()`. + +**Exports:** `Divider` + +--- + +### 3.4 FuzzyPicker + +**File:** `design-system/FuzzyPicker.tsx` + +**Purpose:** Full-featured fuzzy search picker with optional preview panel. Supports up/down/down-to-top list directions, Tab/Shift+Tab secondary actions, preview on right or bottom, match count label. + +**Props Interface:** +```typescript +type PickerAction = { + action: string; // Label for byline hint + handler: (item: T) => void; +} + +type Props = { + title: string; + placeholder?: string; // default: 'Type to search…' + initialQuery?: string; + items: readonly T[]; + getKey: (item: T) => string; + renderItem: (item: T, isFocused: boolean) => React.ReactNode; + renderPreview?: (item: T) => React.ReactNode; + previewPosition?: 'bottom' | 'right'; // default: 'bottom' + visibleCount?: number; // default: 8 + direction?: 'down' | 'up'; // default: 'down' + onQueryChange: (query: string) => void; + onSelect: (item: T) => void; + onTab?: PickerAction; + onShiftTab?: PickerAction; + onFocus?: (item: T | undefined) => void; + onCancel: () => void; + emptyMessage?: string | ((query: string) => string); + matchLabel?: string; + selectAction?: string; + extraHints?: React.ReactNode; +} +``` + +**Key Behaviors:** +- Constants: `DEFAULT_VISIBLE=8`, `CHROME_ROWS=10`, `MIN_VISIBLE=2`. +- Auto-adjusts visible count based on terminal height. +- Fires `onFocus` when focused item changes. +- `direction='up'`: items[0] at bottom (atuin-style); arrows match screen direction. + +**Exports:** `FuzzyPicker` + +--- + +### 3.5 KeyboardShortcutHint + +**File:** `design-system/KeyboardShortcutHint.tsx` + +**Purpose:** Renders a keyboard shortcut hint like "ctrl+o to expand" or "(tab to toggle)". + +**Props Interface:** +```typescript +type Props = { + shortcut: string; // e.g., "ctrl+o", "Enter", "↑↓" + action: string; // e.g., "expand", "select", "navigate" + parens?: boolean; // default: false — wraps in parentheses + bold?: boolean; // default: false — renders shortcut in bold +} +``` + +**Exports:** `KeyboardShortcutHint` + +--- + +### 3.6 ListItem + +**File:** `design-system/ListItem.tsx` + +**Purpose:** Standard list item for selection UIs with pointer (❯), checkmark (✓), scroll hint arrows, focus/selection colors, and disabled state. + +**Props Interface:** +```typescript +type ListItemProps = { + isFocused: boolean; + isSelected?: boolean; // default: false — shows ✓ + children: ReactNode; + description?: string; // Shown below main content + showScrollDown?: boolean; // Shows ↓ instead of pointer (scroll hint) + showScrollUp?: boolean; // Shows ↑ instead of pointer (scroll hint) + styled?: boolean; // default: true — auto-colors children by state + disabled?: boolean; // default: false — dimmed, no indicators + declareCursor?: boolean; // default: true — declares terminal cursor position +} +``` + +**Key Behaviors:** +- When focused and not selected: pointer (❯) in suggestion color. +- When selected and not focused: checkmark (✓) in suggestion color. +- When disabled: no indicator, dimmed text. +- When `styled=false`: children rendered as-is for custom styling. + +**Exports:** `ListItem` + +--- + +### 3.7 LoadingState + +**File:** `design-system/LoadingState.tsx` + +**Purpose:** Spinner + message for async loading states. + +**Props Interface:** +```typescript +type LoadingStateProps = { + message: string; + bold?: boolean; // default: false + dimColor?: boolean; // default: false + subtitle?: string; // Optional secondary line below +} +``` + +**Exports:** `LoadingState` + +--- + +### 3.8 Pane + +**File:** `design-system/Pane.tsx` + +**Purpose:** A terminal region bounded by a colored top divider line, used by all slash-command screens. + +**Props Interface:** +```typescript +type PaneProps = { + children: React.ReactNode; + color?: keyof Theme; // Theme color for top divider +} +``` + +**Key Behaviors:** +- When rendered inside a modal (`useIsInsideModal()`): skips the Divider (the modal frame serves as border), renders with `paddingX={1}` and `flexShrink={0}`. +- Normal rendering: `paddingTop={1}` + `` + `children`. + +**Exports:** `Pane` + +--- + +### 3.9 ProgressBar + +**File:** `design-system/ProgressBar.tsx` + +**Purpose:** Horizontal text-art progress bar using Unicode block characters. + +**Props Interface:** +```typescript +type Props = { + ratio: number; // [0, 1] + width: number; // Number of character columns + fillColor?: keyof Theme; + emptyColor?: keyof Theme; +} +``` + +**Key Behaviors:** +- Uses 9 block characters: `[' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']`. +- Clamps ratio to [0, 1]. +- Whole filled cells: `Math.floor(ratio * width)`. +- Partial cell: `Math.floor(remainder * BLOCKS.length)`. +- `fillColor` applied as text color; `emptyColor` as background. + +**Exports:** `ProgressBar` + +--- + +### 3.10 Ratchet + +**File:** `design-system/Ratchet.tsx` + +**Purpose:** Prevents layout bounce/shrink by maintaining minimum height equal to the maximum seen height. Used for content that can grow but should not shrink when re-rendered. + +**Props Interface:** +```typescript +type Props = { + children: React.ReactNode; + lock?: 'always' | 'offscreen'; // default: 'always' +} +``` + +**Key Behaviors:** +- `lock='always'`: always enforces minHeight (inner Box max seen height, capped at terminal rows). +- `lock='offscreen'`: only enforces minHeight when the element is not visible in terminal viewport. +- Uses `useTerminalViewport()` for visibility detection. +- `useLayoutEffect` measures inner content height after every render. + +**Exports:** `Ratchet` + +--- + +### 3.11 StatusIcon + +**File:** `design-system/StatusIcon.tsx` + +**Purpose:** Renders a colored status indicator icon. + +**Props Interface:** +```typescript +type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading' +type Props = { + status: Status; + withSpace?: boolean; // default: false — adds trailing space +} +``` + +**Status Config:** +| Status | Icon | Color | +|--------|------|-------| +| success | ✓ (figures.tick) | success (green) | +| error | ✗ (figures.cross) | error (red) | +| warning | ⚠ (figures.warning) | warning (yellow) | +| info | ℹ (figures.info) | suggestion (blue) | +| pending | ○ (figures.circle) | dimColor | +| loading | … | dimColor | + +**Exports:** `StatusIcon` + +--- + +### 3.12 Tabs + +**File:** `design-system/Tabs.tsx` + +**Purpose:** Tab container with keyboard navigation. Supports controlled and uncontrolled modes, fixed content height, optional banner, and content-initiated tab switching. + +**Props Interface:** +```typescript +type TabsProps = { + children: Array>; + title?: string; + color?: keyof Theme; + defaultTab?: string; + hidden?: boolean; + useFullWidth?: boolean; + selectedTab?: string; // Controlled mode + onTabChange?: (tabId: string) => void; // Controlled mode + banner?: React.ReactNode; + disableNavigation?: boolean; + initialHeaderFocused?: boolean; // default: true + contentHeight?: number; // Fixed height for all tabs + navFromContent?: boolean; // Allow Tab/←/→ from content +} + +type TabsContextValue = { + selectedTab: string | undefined; + width: number | undefined; + headerFocused: boolean; + focusHeader: () => void; + blurHeader: () => void; + registerOptIn: () => () => void; +} +``` + +**Key Behaviors:** +- Tab component reads `TabsContext` to know if it is the selected tab. +- Left/Right arrows or Tab key switches tabs when header focused. +- `navFromContent=true` allows content area to trigger tab switches. + +**Exports:** `Tabs`, `TabsContext`, `TabsContextValue` + +--- + +### 3.13 ThemeProvider + +**File:** `design-system/ThemeProvider.tsx` + +**Purpose:** Provides theme state (dark/light/auto) to the component tree. Resolves `'auto'` by detecting system theme via OSC 11 terminal queries. + +**Props Interface:** +```typescript +type Props = { + children: React.ReactNode; + initialState?: ThemeSetting; + onThemeSave?: (setting: ThemeSetting) => void; +} + +type ThemeContextValue = { + themeSetting: ThemeSetting; // Saved preference (may be 'auto') + setThemeSetting: (s: ThemeSetting) => void; + setPreviewTheme: (s: ThemeSetting) => void; + savePreview: () => void; + cancelPreview: () => void; + currentTheme: ThemeName; // Resolved theme, never 'auto' +} +``` + +**Key Behaviors:** +- `previewTheme` takes priority over `themeSetting` during theme picker interactions. +- Seeds system theme from `$COLORFGBG` env var; OSC 11 watcher corrects it. +- Default theme (outside provider, for tests): `'dark'`. + +**Exports:** `ThemeProvider`, `ThemeContext`, `useTheme` (hook) + +--- + +### 3.14 ThemedBox + +**File:** `design-system/ThemedBox.tsx` + +**Purpose:** Theme-aware Box component that resolves theme key colors (`keyof Theme`) in all border/background color props to raw terminal colors before passing to the underlying Ink `Box`. + +**Props Type:** `BaseStylesWithoutColors & ThemedColorProps & EventHandlerProps` + +```typescript +type ThemedColorProps = { + borderColor?: keyof Theme | Color; + borderTopColor?: keyof Theme | Color; + borderBottomColor?: keyof Theme | Color; + borderLeftColor?: keyof Theme | Color; + borderRightColor?: keyof Theme | Color; + backgroundColor?: keyof Theme | Color; +} +``` + +**Key Behaviors:** +- `resolveColor()`: passes through raw colors (`#`, `rgb(`, `ansi256(`, `ansi:`), looks up theme keys. + +**Exports:** `ThemedBox` (default export), `Props` + +--- + +### 3.15 ThemedText + +**File:** `design-system/ThemedText.tsx` + +**Purpose:** Theme-aware Text component that resolves `keyof Theme` color values and supports `TextHoverColorContext` for cascade coloring. + +**Props Interface:** +```typescript +type Props = { + color?: keyof Theme | Color; + backgroundColor?: keyof Theme; + dimColor?: boolean; // Uses theme's inactive color (compatible with bold) + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + inverse?: boolean; + wrap?: Styles['textWrap']; + children?: ReactNode; +} +``` + +**Exports:** `ThemedText`, `Props`, `TextHoverColorContext` + +--- + +### 3.16 color.ts + +**File:** `design-system/color.ts` + +**Purpose:** Curried theme-aware colorization function. + +**Exports:** +```typescript +function color( + c: keyof Theme | Color | undefined, + theme: ThemeName, + type: ColorType = 'foreground', +): (text: string) => string +``` + +**Key Behaviors:** +- Raw color values bypass theme lookup. +- Theme key values are looked up in `getTheme(theme)` and passed to `colorize()`. + +--- + +## 4. wizard/ + +A generic multi-step wizard framework used by agent creation and potentially other flows. + +### 4.1 WizardProvider + +**File:** `wizard/WizardProvider.tsx` + +**Purpose:** Context provider managing wizard state: current step, data accumulation, navigation history, and completion. + +**Props Interface:** +```typescript +type WizardProviderProps = { + steps: WizardStepComponent[]; + initialData?: Partial; + onComplete: (data: T) => void; + onCancel: () => void; + children?: React.ReactNode; + title?: string; + showStepCounter?: boolean; // default: true +} +``` + +**WizardContextValue:** +```typescript +type WizardContextValue = { + currentStepIndex: number; + totalSteps: number; + wizardData: Partial; + updateWizardData: (partial: Partial) => void; + goNext: () => void; + goBack: () => void; + goToStep: (index: number) => void; + cancel: () => void; + title: string | undefined; + showStepCounter: boolean; +} +``` + +**Key Behaviors:** +- Maintains `navigationHistory` stack for `goBack()`. +- `goNext()` at last step sets `isCompleted=true`, triggering `onComplete(wizardData)` in an effect. +- Calls `useExitOnCtrlCDWithKeybindings()` to register Ctrl+C/D at wizard level. + +**Exports:** `WizardProvider`, `WizardContext` + +--- + +### 4.2 WizardDialogLayout + +**File:** `wizard/WizardDialogLayout.tsx` + +**Purpose:** Standard layout for wizard steps — wraps content in a `` with step counter in title, navigation footer. + +**Props Interface:** +```typescript +type Props = { + title?: string; // Overrides provider title + color?: keyof Theme; // default: 'suggestion' + children: ReactNode; + subtitle?: string; + footerText?: ReactNode; +} +``` + +**Key Behaviors:** +- Title format: `"${title} (${currentStep + 1}/${totalSteps})"` when `showStepCounter` is true. +- `isCancelActive={false}` on the inner Dialog (wizard handles its own cancel). +- Renders `` below Dialog. + +**Exports:** `WizardDialogLayout` + +--- + +### 4.3 WizardNavigationFooter + +**File:** `wizard/WizardNavigationFooter.tsx` + +**Purpose:** Renders keyboard hints at the bottom of wizard dialogs. + +**Props Interface:** +```typescript +type Props = { + instructions?: ReactNode; +} +``` + +--- + +### 4.4 useWizard + +**File:** `wizard/useWizard.ts` + +**Purpose:** Hook to access `WizardContext` from within a step component. Throws if used outside a `WizardProvider`. + +**Exports:** +```typescript +function useWizard = Record>(): WizardContextValue +``` + +--- + +### 4.5 index.ts + +Re-exports `WizardProvider`, `useWizard`, and wizard-related types. + +--- + +## 5. mcp/ + +Model Context Protocol server management UI. + +### Files Overview + +| File | Purpose | +|------|---------| +| `CapabilitiesSection.tsx` | Renders server capabilities list (tools, resources, prompts) | +| `ElicitationDialog.tsx` | Dialog for MCP elicitation (server requesting structured user input) | +| `MCPAgentServerMenu.tsx` | Menu for managing MCP agent-type servers | +| `MCPListPanel.tsx` | Panel showing all connected MCP servers with status | +| `MCPReconnect.tsx` | UI for reconnecting to a disconnected server | +| `MCPRemoteServerMenu.tsx` | Menu for remote MCP server configuration | +| `MCPSettings.tsx` | Top-level MCP settings screen | +| `MCPStdioServerMenu.tsx` | Menu for stdio-type MCP servers | +| `MCPToolDetailView.tsx` | Detail view of a single MCP tool | +| `MCPToolListView.tsx` | List of tools provided by an MCP server | +| `McpParsingWarnings.tsx` | Shows YAML/JSON parsing warnings from MCP config | +| `index.ts` | Re-exports | +| `utils/reconnectHelpers.tsx` | Helper functions for server reconnect logic | + +**Key Types (from MCPSettings):** +```typescript +// Server status display combines name, transport type, connection state, tool count +``` + +--- + +## 6. memory/ + +Agent memory file management UI. + +### Files + +| File | Purpose | +|------|---------| +| `MemoryFileSelector.tsx` | File picker for selecting which memory file to view/edit | +| `MemoryUpdateNotification.tsx` | Toast-style notification shown when agent updates its memory | + +**MemoryFileSelector Props:** +```typescript +type Props = { + onSelect: (filePath: string) => void; + onCancel: () => void; +} +``` + +--- + +## 7. tasks/ + +Background task and remote session monitoring UI. + +### Files Overview + +| File | Purpose | +|------|---------| +| `AsyncAgentDetailDialog.tsx` | Detail dialog for an async/queued agent task | +| `BackgroundTask.tsx` | Single background task row in the task list | +| `BackgroundTaskStatus.tsx` | Status badge for a background task | +| `BackgroundTasksDialog.tsx` | Full dialog listing all background tasks | +| `DreamDetailDialog.tsx` | Detail for a "dream" (autoDream background consolidation) task | +| `InProcessTeammateDetailDialog.tsx` | Detail for an in-process swarm teammate | +| `RemoteSessionDetailDialog.tsx` | Detail for a remote Claude Code session | +| `RemoteSessionProgress.tsx` | Progress display for remote session activity | +| `ShellDetailDialog.tsx` | Detail for a shell background task | +| `ShellProgress.tsx` | Progress line for shell commands | +| `renderToolActivity.tsx` | Renders current tool activity for a running task | +| `taskStatusUtils.tsx` | Utility functions for task status display | + +--- + +## 8. teams/ + +Swarm/multi-agent team status UI. + +### Files + +| File | Purpose | +|------|---------| +| `TeamStatus.tsx` | Shows the status of all active team members (swarm workers) | +| `TeamsDialog.tsx` | Dialog for managing team composition and viewing worker details | + +--- + +## 9. diff/ + +File diff viewing UI. + +### Files + +| File | Purpose | +|------|---------| +| `DiffDetailView.tsx` | Full-screen diff detail with scroll support | +| `DiffDialog.tsx` | Dialog wrapping `DiffDetailView` | +| `DiffFileList.tsx` | List of changed files with summary stats | + +--- + +## 10. grove/ + +Grove (shared project workspace) integration. + +### Files + +| File | Purpose | +|------|---------| +| `Grove.tsx` | Main Grove integration UI component | + +--- + +## 11. hooks/ (components/hooks/) + +These are **component-level** hook subdirectory hooks, not the top-level `src/hooks/`. They are part of the hooks command settings UI. + +### Files + +| File | Purpose | +|------|---------| +| `HooksConfigMenu.tsx` | Configuration menu for Claude hooks (pre/post tool hooks) | +| `PromptDialog.tsx` | Dialog for entering a hook prompt/command | +| `SelectEventMode.tsx` | Select hook event type (PreToolUse, PostToolUse, etc.) | +| `SelectHookMode.tsx` | Select hook execution mode (allow, block, prompt) | +| `SelectMatcherMode.tsx` | Select how to match tools (all, specific, pattern) | +| `ViewHookMode.tsx` | Read-only view of an existing hook configuration | + +--- + +## 12. HelpV2/ + +Second-generation help UI, shown by `/help`. + +### Files + +| File | Purpose | +|------|---------| +| `Commands.tsx` | Renders the commands reference tab | +| `General.tsx` | Renders the general help/tips tab | +| `HelpV2.tsx` | Top-level help screen with Tabs (General, Commands) | + +--- + +## 13. TrustDialog/ + +Trust confirmation dialogs for files and workspace directories. + +### Files + +| File | Purpose | +|------|---------| +| `TrustDialog.tsx` | Dialog asking user to confirm trust for a directory or file | +| `utils.ts` | Trust decision helpers | + +--- + +## 14. ManagedSettingsSecurityDialog/ + +Security warning for managed/policy settings. + +### Files + +| File | Purpose | +|------|---------| +| `ManagedSettingsSecurityDialog.tsx` | Warning dialog when policy settings override user preferences | +| `utils.ts` | Utilities for detecting managed setting conflicts | + +--- + +## 15. ClaudeCodeHint/ + +Plugin/command hint menu. + +### Files + +| File | Purpose | +|------|---------| +| `PluginHintMenu.tsx` | Shows plugin-provided hints in a menu format | + +--- + +## 16. HighlightedCode/ + +Syntax-highlighted code rendering. + +### Files + +| File | Purpose | +|------|---------| +| `Fallback.tsx` | Non-highlighted code block fallback | + +The main `HighlightedCode.tsx` is in the parent `components/` directory (not in this subdirectory); this subdirectory provides only the fallback. + +--- + +## 17. LogoV2/ + +Animated logo/welcome screen and feed system. + +### Files + +| File | Purpose | +|------|---------| +| `AnimatedAsterisk.tsx` | Spinning asterisk logo animation | +| `AnimatedClawd.tsx` | Animated "Clawd" mascot version | +| `ChannelsNotice.tsx` | Notice about available channels | +| `Clawd.tsx` | Static Clawd mascot | +| `CondensedLogo.tsx` | Compact logo for limited space contexts | +| `EmergencyTip.tsx` | Urgent tip overlay | +| `Feed.tsx` | Scrollable feed of announcements/tips | +| `FeedColumn.tsx` | Column layout for feed items | +| `GuestPassesUpsell.tsx` | Upsell for guest passes feature | +| `LogoV2.tsx` | Main logo component (animated asterisk + welcome) | +| `Opus1mMergeNotice.tsx` | Notice about Opus 1M model merge | +| `OverageCreditUpsell.tsx` | Upsell for overage credit purchase | +| `VoiceModeNotice.tsx` | Notice about voice mode availability | +| `WelcomeV2.tsx` | Full welcome screen with logo and feed | +| `feedConfigs.tsx` | Configuration data for feed content | + +--- + +## 18. DesktopUpsell/ + +Desktop app upsell during startup. + +### Files + +| File | Purpose | +|------|---------| +| `DesktopUpsellStartup.tsx` | Full-screen upsell shown at startup for desktop app | + +--- + +## 19. FeedbackSurvey/ + +In-app feedback and survey system. + +### Files + +| File | Purpose | +|------|---------| +| `FeedbackSurvey.tsx` | Main survey container | +| `FeedbackSurveyView.tsx` | Renders a single survey question | +| `TranscriptSharePrompt.tsx` | Asks user whether to share conversation transcript | +| `submitTranscriptShare.ts` | API call to submit transcript share | +| `useDebouncedDigitInput.ts` | Hook for numeric rating inputs (debounced) | +| `useFeedbackSurvey.tsx` | Main hook managing survey display lifecycle | +| `useMemorySurvey.tsx` | Hook for memory-specific survey prompts | +| `usePostCompactSurvey.tsx` | Hook for post-compact operation survey | +| `useSurveyState.tsx` | Core survey state management hook | + +--- + +## 20. LspRecommendation/ + +LSP/IDE integration recommendation UI. + +### Files + +| File | Purpose | +|------|---------| +| `LspRecommendationMenu.tsx` | Menu suggesting LSP/IDE plugin installation | + +--- + +## 21. Passes/ + +Guest passes system UI. + +### Files + +| File | Purpose | +|------|---------| +| `Passes.tsx` | Displays and manages guest passes for Claude | + +--- + +## 22. Spinner/ + +Animated spinner components. + +### Files + +| File | Purpose | +|------|---------| +| `FlashingChar.tsx` | Character that flashes on/off | +| `GlimmerMessage.tsx` | Full shimmer-animated message text | +| `ShimmerChar.tsx` | Individual character with shimmer animation | +| `SpinnerAnimationRow.tsx` | Single row of spinner animation | +| `SpinnerGlyph.tsx` | The animated spinner glyph itself | +| `TeammateSpinnerLine.tsx` | Spinner line for teammate/worker status | +| `TeammateSpinnerTree.tsx` | Tree of spinner lines for all teammates | +| `index.ts` | Re-exports `Spinner` as the main export | +| `teammateSelectHint.ts` | Returns keyboard hint for teammate selection | +| `useShimmerAnimation.ts` | Hook generating shimmer animation indices | +| `useStalledAnimation.ts` | Hook detecting when animation has stalled | +| `utils.ts` | Spinner utility functions | + +**SpinnerGlyph:** Animated terminal spinner using Unicode braille or other characters, driven by a clock tick interval. + +**useShimmerAnimation:** Returns `[ref, glimmerIndex]`. The `glimmerIndex` represents the leading edge of a shimmer animation sweep across the text. + +--- + +## 23. PromptInput/ + +The main prompt input area at the bottom of the REPL screen. + +### Files + +| File | Purpose | +|------|---------| +| `HistorySearchInput.tsx` | Ctrl+R history search input overlay | +| `IssueFlagBanner.tsx` | Banner for flagged issues | +| `Notifications.tsx` | In-prompt notification bubbles | +| `PromptInput.tsx` | Top-level prompt input orchestrator | +| `PromptInputFooter.tsx` | Footer area (model name, token count, etc.) | +| `PromptInputFooterLeftSide.tsx` | Left side of footer (model indicator) | +| `PromptInputFooterSuggestions.tsx` | File/command suggestions dropdown | +| `PromptInputHelpMenu.tsx` | Quick help menu (shown on ?) | +| `PromptInputModeIndicator.tsx` | Shows current input mode (normal, plan, etc.) | +| `PromptInputQueuedCommands.tsx` | Shows queued commands waiting to run | +| `PromptInputStashNotice.tsx` | Shows stashed input notification | +| `SandboxPromptFooterHint.tsx` | Hint for sandbox mode in footer | +| `ShimmeredInput.tsx` | Text input with shimmer loading state | +| `VoiceIndicator.tsx` | Voice mode activity indicator | +| `inputModes.ts` | Input mode type definitions and transitions | +| `inputPaste.ts` | Paste handling logic | +| `useMaybeTruncateInput.ts` | Hook for truncating long inputs in display | +| `usePromptInputPlaceholder.ts` | Hook generating placeholder text | +| `useShowFastIconHint.ts` | Hook controlling fast-mode icon hint visibility | +| `useSwarmBanner.ts` | Hook for swarm status banner in prompt | +| `utils.ts` | Input utility functions | + +--- + +## 24. CustomSelect/ + +Accessible terminal select/dropdown components. + +### Files + +| File | Purpose | +|------|---------| +| `SelectMulti.tsx` | Multi-select dropdown | +| `index.ts` | Re-exports | +| `option-map.ts` | Option data structure and utilities | +| `select-input-option.tsx` | Option rendering in input mode | +| `select-option.tsx` | Single option rendering | +| `select.tsx` | Main single-select component | +| `use-multi-select-state.ts` | State hook for multi-select | +| `use-select-input.ts` | Input handling hook for select | +| `use-select-navigation.ts` | Keyboard navigation hook | +| `use-select-state.ts` | State hook for single select | + +**Select Props (core):** +```typescript +type SelectProps = { + options: Array<{ value: T; label: string; description?: string; disabled?: boolean }>; + defaultValue?: T; + onChange: (value: T) => void; + onCancel?: () => void; + isDisabled?: boolean; +} +``` + +**SelectMulti Props:** +```typescript +type SelectMultiProps = { + options: Array<{ value: T; label: string }>; + defaultValues?: T[]; + onChange: (values: T[]) => void; + onCancel?: () => void; +} +``` + +--- + +## 25. Settings/ + +Settings slash-command screen (`/config`). + +### Files + +| File | Purpose | +|------|---------| +| `Config.tsx` | Configuration settings tab (API key, model, etc.) | +| `Settings.tsx` | Top-level settings screen with tabs | +| `Status.tsx` | Status tab showing connection and auth state | +| `Usage.tsx` | Usage statistics tab | + +--- + +## 26. sandbox/ + +Sandbox configuration UI (shown by `/sandbox`). + +### Files + +| File | Purpose | +|------|---------| +| `SandboxConfigTab.tsx` | Sandbox enable/disable and mode selection | +| `SandboxDependenciesTab.tsx` | Shows dependencies inside sandbox | +| `SandboxDoctorSection.tsx` | Diagnostic section for sandbox health | +| `SandboxOverridesTab.tsx` | Per-project sandbox overrides | +| `SandboxSettings.tsx` | Top-level sandbox settings screen with tabs | + +--- + +## 27. shell/ + +Shell output display components. + +### Files + +| File | Purpose | +|------|---------| +| `ExpandShellOutputContext.tsx` | Context provider for controlling shell output expansion state | +| `OutputLine.tsx` | Single line of shell output with ANSI support | +| `ShellProgressMessage.tsx` | In-progress shell command display with live output | +| `ShellTimeDisplay.tsx` | Shows elapsed/total time for a shell command | + +--- + +## 28. skills/ + +Skills (slash commands from plugins) UI. + +### Files + +| File | Purpose | +|------|---------| +| `SkillsMenu.tsx` | Menu listing available skills with search | + +--- + +## 29. ui/ + +General-purpose UI primitives not in design-system. + +### Files + +| File | Purpose | +|------|---------| +| `OrderedList.tsx` | Numbered list container | +| `OrderedListItem.tsx` | Single item in an ordered list | +| `TreeSelect.tsx` | Tree/hierarchical select component | + +**TreeSelect** supports nested option trees where nodes can be expanded/collapsed. Used for hierarchical tool or directory selection. + +--- + +## Cross-Cutting Patterns + +### React Compiler Memoization +All components use `import { c as _c } from "react/compiler-runtime"` and the `$` cache array pattern for React Compiler auto-memoization. This is a build-time transformation — the source TypeScript uses standard React patterns. + +### Theme Integration +- All color props accept `keyof Theme` (semantic tokens like `'permission'`, `'suggestion'`, `'success'`, `'error'`, `'warning'`, `'inactive'`) or raw CSS color strings. +- `ThemedBox` and `ThemedText` resolve theme tokens to raw colors at render time via `useTheme()`. + +### Keybinding System +- Components use `useKeybinding(action, handler, { context, isActive })` from `../../keybindings/useKeybinding.js`. +- Contexts: `'Confirmation'`, `'Settings'`, `'Chat'`, `'Application'`. +- Standard actions: `'confirm:no'` (Esc/N), `'app:exit'` (Ctrl+C), `'app:interrupt'` (Ctrl+D). + +### Permission Flow +1. Tool use triggers permission check → `PermissionResult` with behavior `'ask'` / `'allow'` / `'deny'` / `'passthrough'`. +2. `'ask'` behavior → `ToolUseConfirm` object created → `PermissionRequest` component rendered. +3. User selects option → `onAllow(updatedInput, permissionUpdates)` or `onReject(feedback)` called. +4. Permission updates stored in settings for future auto-approval. + +### Wizard Pattern +1. `WizardProvider` holds step array and accumulated data. +2. Each step calls `useWizard()` to get navigation functions. +3. Steps call `updateWizardData(partial)` then `goNext()` / `goToStep(n)`. +4. Final step calls external `onComplete(message)` directly (not via wizard's `onComplete`). + +### Agent File Format +```markdown +--- +name: agent-identifier +description: "When to use this agent..." +tools: BashTool, FileEditTool +model: sonnet +effort: 3 +color: blue +memory: project +--- + +System prompt content here... +``` diff --git a/spec/06_services_context_state.md b/spec/06_services_context_state.md new file mode 100644 index 0000000..43ee648 --- /dev/null +++ b/spec/06_services_context_state.md @@ -0,0 +1,2631 @@ +# Claude Code — Services, Context, State & Screens + +This document exhaustively covers all files in `src/services/`, `src/context/`, `src/bootstrap/state.ts`, `src/coordinator/`, `src/server/`, and `src/screens/`. Every exported symbol is listed with its full signature, key logic, configuration, and dependencies. + +--- + +## Table of Contents + +1. [bootstrap/state.ts](#bootstrapstatets) +2. [coordinator/coordinatorMode.ts](#coordinatorcoordinatormodetse) +3. [server/types.ts](#servertypests) +4. [server/createDirectConnectSession.ts](#servercreatedirectconnectsessionts) +5. [server/directConnectManager.ts](#serverdirectconnectmanagerts) +6. [services/analytics/config.ts](#servicesanalyticsconfigts) +7. [services/analytics/growthbook.ts](#servicesanalyticsgrowthbookts) +8. [services/analytics/metadata.ts](#servicesanalyticsmetadatats) +9. [services/analytics/index.ts](#servicesanalyticsindexts) +10. [services/analytics/sink.ts](#servicesanalyticssinkt) +11. [services/analytics/sinkKillswitch.ts](#servicesanalyticssinkKillswitchts) +12. [services/analytics/datadog.ts](#servicesanalyticsdatadogts) +13. [services/analytics/firstPartyEventLogger.ts](#servicesanalyticsfirstpartyeventloggerts) +14. [services/analytics/firstPartyEventLoggingExporter.ts](#servicesanalyticsfirstpartyeventloggingexporterts) +15. [services/api/bootstrap.ts](#servicesapibootstrapts) +16. [services/api/client.ts](#servicesapiclientts) +17. [services/api/claude.ts](#servicesapicludets) +18. [services/api/dumpPrompts.ts](#servicesapidumppromptsts) +19. [services/api/emptyUsage.ts](#servicesapiemptyusagets) +20. [services/api/errorUtils.ts](#servicesapierrorutilsts) +21. [services/api/errors.ts](#servicesapierrorsts) +22. [services/api/filesApi.ts](#servicesapifilesapits) +23. [services/api/firstTokenDate.ts](#servicesapifirsttokendatets) +24. [services/api/grove.ts](#servicesapigrovet) +25. [services/api/logging.ts](#servicesapiloggingts) +26. [services/api/metricsOptOut.ts](#servicesapimetricsoptoutts) +27. [services/api/overageCreditGrant.ts](#servicesapiovaragecreditgrantts) +28. [services/api/promptCacheBreakDetection.ts](#servicesapipromptcachebreakdetectionts) +29. [services/api/referral.ts](#servicesapireferralts) +30. [services/api/sessionIngress.ts](#servicesapisessioningressts) +31. [services/api/ultrareviewQuota.ts](#servicesapiultrareviewquotats) +32. [services/api/usage.ts](#servicesapiusagets) +33. [services/api/withRetry.ts](#servicesapiwithretryts) +34. [services/AgentSummary/agentSummary.ts](#servicesagentsummaryagentsummaryts) +35. [services/autoDream/autoDream.ts](#servicesautodreamautodreamts) +36. [services/autoDream/config.ts](#servicesautodreamconfigts) +37. [services/autoDream/consolidationLock.ts](#servicesautodreamconsolidationlockts) +38. [services/autoDream/consolidationPrompt.ts](#servicesautodreamconsolidationpromptt) +39. [services/awaySummary.ts](#servicesawaysummaryts) +40. [services/claudeAiLimits.ts](#servicesclaudeailimitsts) +41. [services/claudeAiLimitsHook.ts](#servicesclaudeailimitshookts) +42. [services/compact/apiMicrocompact.ts](#servicescompactapimicrocompactts) +43. [services/compact/autoCompact.ts](#servicescompactautocompactts) +44. [services/compact/compact.ts](#servicescompactcompactts) +45. [services/compact/compactWarningHook.ts](#servicescompactcompactwarninghookts) +46. [services/compact/compactWarningState.ts](#servicescompactcompactwarningstatets) +47. [services/compact/grouping.ts](#servicescompactgroupingts) +48. [services/compact/microCompact.ts](#servicescompactmicrocompactts) +49. [services/compact/postCompactCleanup.ts](#servicescompactpostcompactcleanuptes) +50. [services/compact/prompt.ts](#servicescompactpromptts) +51. [services/compact/sessionMemoryCompact.ts](#servicescompactsessionmemorycompactts) +52. [services/compact/timeBasedMCConfig.ts](#servicescompacttimebasedmcconfigts) +53. [services/diagnosticTracking.ts](#servicesdiagnostictrackingtss) +54. [services/internalLogging.ts](#servicesinternalloggingts) +55. [services/MagicDocs/magicDocs.ts](#servicesmagicdocsmagicdocsts) +56. [services/MagicDocs/prompts.ts](#servicesmagicdocspromptsts) +57. [services/mcpServerApproval.tsx](#servicesmcpserverapprovala) +58. [services/mockRateLimits.ts](#servicesmockratelimitsts) +59. [services/MCP (mcp/)](#services-mcp) +60. [services/notifier.ts](#servicesnotifierts) +61. [services/preventSleep.ts](#servicespreventsleepts) +62. [services/PromptSuggestion/promptSuggestion.ts](#servicespromptsuggestionpromptsuggestsionts) +63. [services/PromptSuggestion/speculation.ts](#servicespromptsuggestionspeculationts) +64. [services/rateLimitMocking.ts](#servicesratelimitmockingts) +65. [services/rateLimitMessages.ts](#servicesratelimitmessagests) +66. [services/SessionMemory/prompts.ts](#servicessessionmemorypromptsts) +67. [services/SessionMemory/sessionMemory.ts](#servicessessionmemorysessionmemoryts) +68. [services/SessionMemory/sessionMemoryUtils.ts](#servicessessionmemorysessionmemoryutilsts) +69. [services/tokenEstimation.ts](#servicestokenestimationts) +70. [services/vcr.ts](#servicesvcrts) +71. [services/voice.ts](#servicesvoicets) +72. [services/voiceKeyterms.ts](#servicesvoicekeytermsss) +73. [services/voiceStreamSTT.ts](#servicesvoicestreamsttts) +74. [context/QueuedMessageContext.tsx](#contextqueuedmessagecontexttsx) +75. [context/fpsMetrics.tsx](#contextfpsmetricstsx) +76. [context/mailbox.tsx](#contextmailboxtsx) +77. [context/modalContext.tsx](#contextmodalcontexttsx) +78. [context/notifications.tsx](#contextnotificationstsx) +79. [context/overlayContext.tsx](#contextoverlaycontexttsx) +80. [context/promptOverlayContext.tsx](#contextpromptoverlaycontexttsx) +81. [context/stats.tsx](#contextstatstsx) +82. [context/voice.tsx](#contextvoicetsx) +83. [screens/Doctor.tsx](#screensdoctortsx) +84. [screens/REPL.tsx](#screensrepltsx) +85. [screens/ResumeConversation.tsx](#screensresumeconversationtsx) + +--- + +## bootstrap/state.ts + +**Path:** `src/bootstrap/state.ts` + +**Purpose:** The single global session state singleton for the entire Claude Code process. Acts as the authoritative source of truth for all per-session metrics, model configuration, telemetry handles, and feature flags. Designed as a strict leaf in the import DAG — imports nothing from `src/utils/` except via explicit safe indirection. + +**Key Types Exported:** + +```typescript +export type ChannelEntry = + | { kind: 'plugin'; name: string; marketplace: string; dev?: boolean } + | { kind: 'server'; name: string; dev?: boolean } + +export type AttributedCounter = { + add(value: number, additionalAttributes?: Attributes): void +} +``` + +**`State` Type (internal, not exported directly):** Contains ~80 fields including: +- `originalCwd: string` — resolved cwd at process start (NFC-normalized, symlinks resolved) +- `projectRoot: string` — stable identity root (set at startup, never changed by mid-session EnterWorktreeTool) +- `totalCostUSD: number`, `totalAPIDuration: number`, `totalAPIDurationWithoutRetries: number` +- `totalToolDuration: number`, `turnHookDurationMs: number`, `turnToolDurationMs: number` +- `totalLinesAdded: number`, `totalLinesRemoved: number` +- `cwd: string` — current working directory (mutable, changes with shell.ts setCwd) +- `modelUsage: { [modelName: string]: ModelUsage }` — per-model usage tracking +- `mainLoopModelOverride: ModelSetting | undefined`, `initialMainLoopModel: ModelSetting` +- `sessionId: SessionId` — UUID regenerated on `clearConversation` +- `parentSessionId: SessionId | undefined` — previous session (for lineage tracking) +- `isInteractive: boolean`, `kairosActive: boolean`, `strictToolResultPairing: boolean` +- `sdkAgentProgressSummariesEnabled: boolean`, `userMsgOptIn: boolean` +- `clientType: string` (default `'cli'`), `sessionSource: string | undefined` +- `meter: Meter | null`, `sessionCounter`, `locCounter`, `prCounter`, `commitCounter`, `costCounter`, `tokenCounter`, `codeEditToolDecisionCounter`, `activeTimeCounter` — OTel metrics +- `sessionId: SessionId` (randomUUID at init), `parentSessionId: SessionId | undefined` +- `loggerProvider: LoggerProvider | null`, `eventLogger: ReturnType | null` +- `meterProvider: MeterProvider | null`, `tracerProvider: BasicTracerProvider | null` +- `agentColorMap: Map`, `agentColorIndex: number` +- `lastAPIRequest`, `lastAPIRequestMessages`, `lastClassifierRequests`, `cachedClaudeMdContent` +- `inMemoryErrorLog: Array<{ error: string; timestamp: string }>` +- `inlinePlugins: string[]`, `chromeFlagOverride: boolean | undefined` +- `sessionBypassPermissionsMode: boolean`, `scheduledTasksEnabled: boolean` +- `sessionCronTasks: SessionCronTask[]`, `sessionCreatedTeams: Set` +- `sessionTrustAccepted: boolean`, `sessionPersistenceDisabled: boolean` +- `hasExitedPlanMode: boolean`, `needsPlanModeExitAttachment: boolean`, `needsAutoModeExitAttachment: boolean` +- `initJsonSchema: Record | null`, `registeredHooks: Partial> | null` +- `planSlugCache: Map` — sessionId → wordSlug +- `teleportedSessionInfo: { isTeleported, hasLoggedFirstMessage, sessionId } | null` +- `invokedSkills: Map` — keyed by `"${agentId ?? ''}:${skillName}"` +- `slowOperations: Array<{ operation, durationMs, timestamp }>` — ant-only dev bar +- `sdkBetas: string[] | undefined`, `mainThreadAgentType: string | undefined` +- `isRemoteMode: boolean`, `directConnectServerUrl: string | undefined` +- `systemPromptSectionCache: Map`, `lastEmittedDate: string | null` +- `additionalDirectoriesForClaudeMd: string[]`, `allowedChannels: ChannelEntry[]`, `hasDevChannels: boolean` +- `sessionProjectDir: string | null` — transcript directory override +- `promptCache1hAllowlist: string[] | null`, `promptCache1hEligible: boolean | null` +- `afkModeHeaderLatched: boolean | null`, `fastModeHeaderLatched: boolean | null` +- `cacheEditingHeaderLatched: boolean | null`, `thinkingClearLatched: boolean | null` +- `promptId: string | null`, `lastMainRequestId: string | undefined` +- `lastApiCompletionTimestamp: number | null`, `pendingPostCompaction: boolean` + +**Exported Functions (getters/setters/mutators):** + +```typescript +export function getSessionId(): SessionId +export function regenerateSessionId(options?: { setCurrentAsParent?: boolean }): SessionId +export function getParentSessionId(): SessionId | undefined +export function switchSession(sessionId: SessionId, projectDir?: string | null): void +export const onSessionSwitch: Signal<[id: SessionId]>['subscribe'] +export function getSessionProjectDir(): string | null +export function getOriginalCwd(): string +export function getProjectRoot(): string +export function setOriginalCwd(cwd: string): void +export function setProjectRoot(cwd: string): void // --worktree startup only +export function getCwdState(): string +export function setCwdState(cwd: string): void +export function getDirectConnectServerUrl(): string | undefined +export function setDirectConnectServerUrl(url: string): void +export function addToTotalDurationState(duration: number, durationWithoutRetries: number): void +export function resetTotalDurationStateAndCost_FOR_TESTS_ONLY(): void +export function addToTotalCostState(cost: number, modelUsage: ModelUsage, model: string): void +export function getTotalCostUSD(): number +export function getTotalAPIDuration(): number +export function getTotalDuration(): number +export function getTotalAPIDurationWithoutRetries(): number +export function getTotalToolDuration(): number +export function addToToolDuration(duration: number): void +export function getTurnHookDurationMs(): number +export function addToTurnHookDuration(duration: number): void +export function resetTurnHookDuration(): void +export function getTurnHookCount(): number +export function getTurnToolDurationMs(): number +export function resetTurnToolDuration(): void +export function getTurnToolCount(): number +export function getTurnClassifierDurationMs(): number +export function addToTurnClassifierDuration(duration: number): void +export function resetTurnClassifierDuration(): void +export function getTurnClassifierCount(): number +export function getStatsStore(): { observe(name: string, value: number): void } | null +export function setStatsStore(store: { observe(name: string, value: number): void } | null): void +export function updateLastInteractionTime(immediate?: boolean): void +export function flushInteractionTime(): void +export function addToTotalLinesChanged(added: number, removed: number): void +export function getTotalLinesAdded(): number +export function getTotalLinesRemoved(): number +export function getTotalInputTokens(): number +export function getTotalOutputTokens(): number +export function getTotalCacheReadInputTokens(): number +export function getTotalCacheCreationInputTokens(): number +export function getTotalWebSearchRequests(): number +export function getTurnOutputTokens(): number +export function getCurrentTurnTokenBudget(): number | null +export function snapshotOutputTokensForTurn(budget: number | null): void +export function getBudgetContinuationCount(): number +export function incrementBudgetContinuationCount(): void +export function setHasUnknownModelCost(): void +export function hasUnknownModelCost(): boolean +export function getLastMainRequestId(): string | undefined +export function setLastMainRequestId(requestId: string): void +export function getLastApiCompletionTimestamp(): number | null +export function setLastApiCompletionTimestamp(timestamp: number): void +export function markPostCompaction(): void +export function consumePostCompaction(): boolean +export function getLastInteractionTime(): number +export function markScrollActivity(): void +export function getIsScrollDraining(): boolean +export function waitForScrollIdle(): Promise +export function getModelUsage(): { [modelName: string]: ModelUsage } +export function getUsageForModel(model: string): ModelUsage | undefined +export function getMainLoopModelOverride(): ModelSetting | undefined +export function getInitialMainLoopModel(): ModelSetting +export function setMainLoopModelOverride(model: ModelSetting | undefined): void +// ... and many more setters for isInteractive, clientType, sessionSource, telemetry counters, etc. +``` + +**Key Logic:** +- `STATE` is a module-level singleton initialized via `getInitialState()` on import +- `updateLastInteractionTime(immediate?)`: deferred by default (batches keypresses into single Date.now() per Ink render); pass `immediate=true` for post-render useEffect callbacks +- `flushInteractionTime()`: called by Ink before each render cycle +- Scroll drain: `markScrollActivity()` sets a debounce flag (`scrollDraining`) for 150ms; background intervals call `getIsScrollDraining()` to yield; `waitForScrollIdle()` polls with 150ms intervals +- `switchSession()` atomically updates `sessionId + sessionProjectDir`; emits `sessionSwitched` signal +- `regenerateSessionId()` can optionally set current as parent (used for plan mode → implementation lineage) +- `markPostCompaction()` / `consumePostCompaction()`: one-shot latch, auto-resets after first consumption + +**Configuration:** +- `SCROLL_DRAIN_IDLE_MS = 150` +- `RESERVOIR_SIZE` (histogram sampling) = 1024 (in stats.tsx) + +**Dependencies:** `@anthropic-ai/sdk`, `@opentelemetry/api`, `@opentelemetry/sdk-*`, `src/utils/crypto.js`, `src/utils/signal.js`, `src/utils/settings/settingsCache.js`, `src/types/ids.js` + +--- + +## coordinator/coordinatorMode.ts + +**Path:** `src/coordinator/coordinatorMode.ts` + +**Purpose:** Implements multi-worker "coordinator mode" where Claude Code orchestrates multiple parallel subagents. Provides the system prompt, user context injection, mode detection, and session-resume alignment logic. + +**Exports:** + +```typescript +export function isCoordinatorMode(): boolean +export function matchSessionMode( + sessionMode: 'coordinator' | 'normal' | undefined +): string | undefined +export function getCoordinatorUserContext( + mcpClients: ReadonlyArray<{ name: string }>, + scratchpadDir?: string +): { [k: string]: string } +export function getCoordinatorSystemPrompt(): string +``` + +**Key Logic:** +- `isCoordinatorMode()`: reads `CLAUDE_CODE_COORDINATOR_MODE` env var; only active when `feature('COORDINATOR_MODE')` bundle flag is set +- `matchSessionMode()`: when resuming a session, aligns the current coordinator mode with the stored session mode. Flips `process.env.CLAUDE_CODE_COORDINATOR_MODE` in-place (since `isCoordinatorMode()` reads it live). Returns a user-visible warning message if mode was switched, `undefined` if no change needed. Logs `tengu_coordinator_mode_switched` analytics event +- `getCoordinatorUserContext()`: returns `{ workerToolsContext: string }` with worker tool list, MCP server names, and scratchpad directory (if gate `tengu_scratch` enabled). In `CLAUDE_CODE_SIMPLE` mode, limits worker tools to Bash/Read/Edit +- `getCoordinatorSystemPrompt()`: returns a multi-section system prompt (1500+ chars) describing coordinator role, available tools (Agent, SendMessage, TaskStop), task workflow phases (Research → Synthesis → Implementation → Verification), concurrency strategy, worker prompt writing guidelines, and full example session + +**Internal Constants:** +```typescript +const INTERNAL_WORKER_TOOLS = new Set([ + TEAM_CREATE_TOOL_NAME, + TEAM_DELETE_TOOL_NAME, + SEND_MESSAGE_TOOL_NAME, + SYNTHETIC_OUTPUT_TOOL_NAME, +]) +``` + +**Configuration:** +- `COORDINATOR_MODE` bundle feature flag +- `CLAUDE_CODE_COORDINATOR_MODE` env var +- `CLAUDE_CODE_SIMPLE` env var — restricts worker tool set to Bash/Read/Edit +- GrowthBook gate `tengu_scratch` — enables scratchpad directory context + +**Dependencies:** `bun:bundle`, `constants/tools.js`, `services/analytics/growthbook.js`, `services/analytics/index.js`, various tool name constants, `utils/envUtils.js` + +--- + +## server/types.ts + +**Path:** `src/server/types.ts` + +**Purpose:** Shared type definitions for the Claude Code server (direct-connect mode). Provides the Zod validation schema for session creation responses. + +**Exports:** + +```typescript +export const connectResponseSchema: () => ZodObject<{ + session_id: ZodString + ws_url: ZodString + work_dir: ZodString.optional() +}> + +export type ServerConfig = { + port: number + host?: string + authToken?: string +} + +export type SessionState = 'starting' | 'running' | 'detached' | 'stopping' | 'stopped' + +export type SessionInfo = { + sessionId: string + state: SessionState + wsUrl: string + workDir?: string + createdAt: number + lastActivity: number +} + +export type SessionIndexEntry = { + sessionId: string + createdAt: number + workDir?: string +} + +export type SessionIndex = Record +``` + +**Key Logic:** `connectResponseSchema()` is a factory function (not a cached value) to allow Zod to be lazy-loaded. Used by `createDirectConnectSession.ts` to validate the `POST /sessions` response body. + +**Dependencies:** `zod` + +--- + +## server/createDirectConnectSession.ts + +**Path:** `src/server/createDirectConnectSession.ts` + +**Purpose:** Creates a session on a remote direct-connect Claude Code server. Posts to `/sessions`, validates response, returns a `DirectConnectConfig` ready for use by the REPL or headless runner. + +**Exports:** + +```typescript +export class DirectConnectError extends Error { + constructor(message: string) + name: 'DirectConnectError' +} + +export async function createDirectConnectSession(opts: { + serverUrl: string + authToken?: string + cwd: string + dangerouslySkipPermissions?: boolean +}): Promise<{ + config: DirectConnectConfig + workDir?: string +}> +``` + +**Key Logic:** +- POSTs `{ cwd, dangerously_skip_permissions? }` as JSON to `${serverUrl}/sessions` +- Sends `Authorization: Bearer ${authToken}` if provided +- Validates response JSON via `connectResponseSchema().safeParse()` +- Returns `{ config: { serverUrl, sessionId, wsUrl, authToken }, workDir }` +- Throws `DirectConnectError` on fetch failure, non-OK HTTP status, or response parse failure + +**Dependencies:** `server/types.js`, `server/directConnectManager.js`, `utils/errors.js`, `utils/slowOperations.js` + +--- + +## server/directConnectManager.ts + +**Path:** `src/server/directConnectManager.ts` + +**Purpose:** WebSocket client for communicating with a remote direct-connect Claude Code server. Handles message routing, permission request/response, interrupt signals, and connection lifecycle. + +**Exports:** + +```typescript +export type DirectConnectConfig = { + serverUrl: string + sessionId: string + wsUrl: string + authToken?: string +} + +export type DirectConnectCallbacks = { + onMessage: (message: SDKMessage) => void + onPermissionRequest: (request: SDKControlPermissionRequest, requestId: string) => void + onConnected?: () => void + onDisconnected?: () => void + onError?: (error: Error) => void +} + +export class DirectConnectSessionManager { + constructor(config: DirectConnectConfig, callbacks: DirectConnectCallbacks) + connect(): void + sendMessage(content: RemoteMessageContent): boolean + respondToPermissionRequest(requestId: string, result: RemotePermissionResponse): void + sendInterrupt(): void + disconnect(): void + isConnected(): boolean +} +``` + +**Key Logic:** +- `connect()`: opens WebSocket with `Authorization: Bearer` header (Bun WebSocket headers override); sets up `open`, `message`, `close`, `error` listeners +- Message parsing: splits NDJSON lines, parses each line, dispatches: + - `control_request` with subtype `can_use_tool` → `onPermissionRequest()` + - unrecognized control subtypes → auto-sends error response (prevents server hang) + - Filtered out: `control_response`, `keep_alive`, `control_cancel_request`, `streamlined_text`, `streamlined_tool_use_summary`, system messages with subtype `post_turn_summary` + - All others → `onMessage()` +- `sendMessage()`: formats as `SDKUserMessage` (`{ type: 'user', message: { role: 'user', content }, parent_tool_use_id: null, session_id: '' }`) +- `respondToPermissionRequest()`: formats as `SDKControlResponse` with `behavior` and either `updatedInput` (allow) or `message` (deny) +- `sendInterrupt()`: sends `{ type: 'control_request', request_id: crypto.randomUUID(), request: { subtype: 'interrupt' } }` + +**Dependencies:** `entrypoints/agentSdkTypes.js`, `entrypoints/sdk/controlTypes.js`, `remote/RemoteSessionManager.js`, `utils/debug.js`, `utils/slowOperations.js`, `utils/teleport/api.js` + +--- + +## services/analytics/config.ts + +**Path:** `src/services/analytics/config.ts` + +**Purpose:** Shared analytics configuration — common logic for disabling analytics across all backends. + +**Exports:** + +```typescript +export function isAnalyticsDisabled(): boolean +export function isFeedbackSurveyDisabled(): boolean +``` + +**Key Logic:** +- `isAnalyticsDisabled()`: returns `true` when `NODE_ENV === 'test'`, `CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY` truthy, or `isTelemetryDisabled()` is true +- `isFeedbackSurveyDisabled()`: returns `true` when `NODE_ENV === 'test'` or `isTelemetryDisabled()` — does NOT gate on 3P providers (Bedrock/Vertex/Foundry) since the survey is local UI with no transcript data; enterprise captures via OTEL + +**Dependencies:** `utils/envUtils.js`, `utils/privacyLevel.js` + +--- + +## services/analytics/growthbook.ts + +**Path:** `src/services/analytics/growthbook.ts` + +**Purpose:** GrowthBook feature flag and dynamic config client. Provides cached and blocking access to feature gates, handles remote eval with disk persistence, manages refresh lifecycle, and exposes override APIs for development/testing. + +**Key Types:** + +```typescript +export type GrowthBookUserAttributes = { + user_id?: string + org_id?: string + user_type?: string + // ... other Statsig-compatible attributes +} +``` + +**Exports:** + +```typescript +export function onGrowthBookRefresh(listener: () => void): () => void +export function hasGrowthBookEnvOverride(feature: string): boolean +export function getAllGrowthBookFeatures(): Record +export function getGrowthBookConfigOverrides(): Record +export function setGrowthBookConfigOverride(feature: string, value: unknown): void +export function clearGrowthBookConfigOverrides(): void +export function getApiBaseUrlHost(): string | undefined +export function initializeGrowthBook(): Promise +export function getFeatureValue_DEPRECATED(feature: string, defaultValue: T): Promise +export function getFeatureValue_CACHED_MAY_BE_STALE(feature: string, defaultValue: T): T +export function getFeatureValue_CACHED_WITH_REFRESH(feature: string, defaultValue: T): T // deprecated +export function checkStatsigFeatureGate_CACHED_MAY_BE_STALE(gate: string): boolean +export function checkSecurityRestrictionGate(gate: string): Promise +export function checkGate_CACHED_OR_BLOCKING(gate: string): Promise +export function getDynamicConfig_CACHED_MAY_BE_STALE(config: string, defaultValue: T): T +export function refreshGrowthBookAfterAuthChange(): void +``` + +**Key Logic:** +- **Initialization (`initializeGrowthBook()`):** Memoized singleton. Loads disk-cached features from `~/.claude/cachedGrowthBookFeatures`. Applies `CLAUDE_INTERNAL_FC_OVERRIDES` env var overrides (JSON). Connects to GrowthBook remote with 5000ms timeout, then sets up periodic refresh. Returns `null` when analytics disabled or in API key mode without user_id +- **Remote eval workaround:** GrowthBook's remote eval returns `{ value }` but client expects `{ defaultValue }`. The code transforms `{ value: V }` → `{ defaultValue: V }` before storing in `remoteEvalFeatureValues` Map. Synced to disk via `syncRemoteEvalToDisk()` +- **Caching tiers:** + - `_DEPRECATED` functions: block on `initializeGrowthBook()` Promise + - `_CACHED_MAY_BE_STALE`: returns synchronously from in-memory cache (may be stale after refresh) + - `_CACHED_OR_BLOCKING`: awaits init, then returns cached value; used for security gates only +- **Security gates (`checkSecurityRestrictionGate()`):** awaits init, checks gate value, blocks if not initialized. Used for enterprise policy enforcement +- **Refresh listeners:** `onGrowthBookRefresh()` registers a listener called after each GrowthBook refresh cycle; returns unsubscribe function +- **Overrides:** `setGrowthBookConfigOverride()` / `clearGrowthBookConfigOverrides()` in-process override map; `CLAUDE_INTERNAL_FC_OVERRIDES` JSON env var for process-level overrides + +**Configuration:** +- Disk cache: `~/.claude/cachedGrowthBookFeatures` +- Init timeout: 5000ms +- `CLAUDE_INTERNAL_FC_OVERRIDES` env var: JSON override map + +**Dependencies:** `growthbook` SDK, `services/analytics/config.js`, `utils/auth.js`, `utils/config.js` + +--- + +## services/analytics/metadata.ts + +**Path:** `src/services/analytics/metadata.ts` + +**Purpose:** Event metadata enrichment for analytics. Provides types and utilities for building structured `EventMetadata` objects with environment context, process metrics, and safe telemetry extraction from tool inputs. + +**Constants:** +- `TOOL_INPUT_STRING_TRUNCATE_AT = 512` — strings longer than this get truncated +- `TOOL_INPUT_STRING_TRUNCATE_TO = 128` — truncated target length +- `TOOL_INPUT_MAX_JSON_CHARS = 4096` — JSON input cap before discarding +- `MAX_FILE_EXTENSION_LENGTH = 10` — max chars for file extensions + +**Exports:** + +```typescript +export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never // marker type + +export function sanitizeToolNameForAnalytics(toolName: string): never // returns marker type + +export function isToolDetailsLoggingEnabled(): boolean // gated on OTEL_LOG_TOOL_DETAILS env + +export function isAnalyticsToolDetailsLoggingEnabled( + mcpServerType: string | undefined, + mcpServerBaseUrl: string | undefined +): boolean + +export function mcpToolDetailsForAnalytics( + toolName: string, + mcpServerType: string | undefined, + mcpServerBaseUrl: string | undefined +): { mcpServerName?: never; mcpToolName?: never } + +export function extractMcpToolDetails( + toolName: string +): { serverName: string; mcpToolName: string } | undefined + +export function extractSkillName( + toolName: string, + input: unknown +): never | undefined // returns marker type or undefined + +export function extractToolInputForTelemetry(input: unknown): string | undefined + +export function getFileExtensionForAnalytics(filePath: string): never | undefined + +export function getFileExtensionsFromBashCommand( + command: string, + simulatedSedEditFilePath?: string +): never | undefined + +export type EnvContext = { + userType: string + isCI: boolean + platform: string + // ... other context fields +} + +export type ProcessMetrics = { + heapUsedMB: number + heapTotalMB: number + rssMB: number + externalMB: number +} + +export type EventMetadata = { + // enriched event payload type +} + +export type EnrichMetadataOptions = { + includeProcessMetrics?: boolean + // ... +} + +export async function getEventMetadata(options?: EnrichMetadataOptions): Promise +export async function buildEnvContext(): Promise // memoized +``` + +**Key Logic:** +- `BUILTIN_MCP_SERVER_NAMES` set is gated behind `CHICAGO_MCP` feature flag — determines which MCP servers are considered "builtin" +- `extractToolInputForTelemetry()`: JSON-serializes input, truncates strings over `TOOL_INPUT_STRING_TRUNCATE_AT` to `TOOL_INPUT_STRING_TRUNCATE_TO`, caps total at `TOOL_INPUT_MAX_JSON_CHARS` +- `getFileExtensionsFromBashCommand()`: parses bash command to extract file extensions using regex patterns; handles `sed -i` specially via `simulatedSedEditFilePath` +- `buildEnvContext()` is memoized — called once per process and cached +- Agent identification classifies turns as: teammate (subagent of another), subagent (spawned by coordinator), standalone + +**Dependencies:** `services/analytics/growthbook.js`, `utils/envUtils.js`, `utils/platform.js` + +--- + +## services/analytics/index.ts + +**Path:** `src/services/analytics/index.ts` + +**Purpose:** The main analytics entry point — a no-dependency module that provides a queuing facade for all event logging. Events are queued until a sink is attached, preventing startup ordering issues. + +**Design:** Explicitly has NO dependencies to avoid import cycles. Events are queued in `eventQueue` until `attachAnalyticsSink()` drains them via `queueMicrotask`. + +**Exports:** + +```typescript +export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never // marker type +export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never // PII-tagged marker type + +export function stripProtoFields( + metadata: Record +): Record + +export type AnalyticsSink = { + logEvent: (eventName: string, metadata: LogEventMetadata) => void + logEventAsync: (eventName: string, metadata: LogEventMetadata) => Promise +} + +export function attachAnalyticsSink(newSink: AnalyticsSink): void +export function logEvent(eventName: string, metadata: LogEventMetadata): void +export async function logEventAsync(eventName: string, metadata: LogEventMetadata): Promise +export function _resetForTesting(): void +``` + +**Types (internal):** +```typescript +type LogEventMetadata = { [key: string]: boolean | number | undefined } +type QueuedEvent = { eventName: string; metadata: LogEventMetadata; async: boolean } +``` + +**Key Logic:** +- `attachAnalyticsSink()`: idempotent (no-op if sink already set). Drains queue via `queueMicrotask` to avoid blocking startup. For ant users (`USER_TYPE === 'ant'`), logs `analytics_sink_attached` with `queued_event_count` +- `stripProtoFields()`: removes keys starting with `_PROTO_` from event metadata (for non-1P destinations). Returns same reference if no `_PROTO_` keys present +- `_PROTO_*` keys route to PII-tagged BigQuery columns — stripped before Datadog but preserved for firstPartyEventLoggingExporter +- Metadata type is intentionally restricted to `boolean | number | undefined` — no strings unless explicitly cast with the marker type + +--- + +## services/analytics/sink.ts + +**Path:** `src/services/analytics/sink.ts` + +**Purpose:** The analytics sink implementation that routes events to Datadog and first-party event logging backends. Handles sampling, metadata enrichment, and per-sink kill switches. + +**Exports:** + +```typescript +export function createAnalyticsSink(options?: { + isInteractive?: boolean +}): AnalyticsSink +``` + +**Key Logic:** +- Routes events to two sinks: Datadog (via `logDatadogEvent`) and first-party event logging (via `log1PEvent`) +- Checks `isSinkKilled('datadog')` and `isSinkKilled('firstParty')` before dispatching to each sink +- Applies sampling based on `tengu_event_sampling_config` dynamic config — adds `sample_rate` to metadata when sampled +- Strips `_PROTO_*` keys before Datadog fanout (`stripProtoFields`) +- Metadata is enriched with `getEventMetadata()` before dispatch + +--- + +## services/analytics/sinkKillswitch.ts + +**Path:** `src/services/analytics/sinkKillswitch.ts` + +**Purpose:** Per-sink analytics kill switch, controlled by a GrowthBook JSON config. + +**Exports:** + +```typescript +export type SinkName = 'datadog' | 'firstParty' + +export function isSinkKilled(sink: SinkName): boolean +``` + +**Key Logic:** +- Config name: `tengu_frond_boric` (mangled/obfuscated name) +- Shape: `{ datadog?: boolean, firstParty?: boolean }` — `true` stops dispatch to that sink +- Default `{}` (nothing killed). Fail-open: missing/malformed config = sink stays on +- Must NOT be called from `isGrowthBookEnabled()` — would cause recursion; call at per-event dispatch sites instead + +**Dependencies:** `services/analytics/growthbook.js` + +--- + +## services/analytics/datadog.ts + +**Path:** `src/services/analytics/datadog.ts` + +**Purpose:** Datadog metrics and event logging integration for Claude Code analytics. + +**Key Logic:** +- Sends events via `@datadog/datadog-ci` or direct HTTP to Datadog API +- Disabled when `isAnalyticsDisabled()` returns true +- Event namespace: `tengu_*` prefix for all Claude Code events +- Tags include version, platform, user type, session metadata + +--- + +## services/analytics/firstPartyEventLogger.ts + +**Path:** `src/services/analytics/firstPartyEventLogger.ts` + +**Purpose:** First-party event logging integration — routes events to the internal event logging system. + +**Key Logic:** +- Gated by `is1PEventLoggingEnabled()` which checks GrowthBook feature `tengu_fpel` and `isAnalyticsDisabled()` +- Enriches events with `EventMetadata` from `getEventMetadata()` +- Handles proto field hoisting from `_PROTO_*` keys + +--- + +## services/analytics/firstPartyEventLoggingExporter.ts + +**Path:** `src/services/analytics/firstPartyEventLoggingExporter.ts` + +**Purpose:** OpenTelemetry log exporter that routes logs to the first-party event logging pipeline. + +**Key Logic:** +- Implements OTel `LogRecordExporter` interface +- Hoists `_PROTO_*` keys to top-level proto fields in the BQ destination +- Calls `stripProtoFields()` after hoisting as defensive cleanup +- Only sends to first-party pipeline (not Datadog) + +--- + +## services/api/bootstrap.ts + +**Path:** `src/services/api/bootstrap.ts` + +**Purpose:** Bootstraps the API client with session-specific configuration at startup. Wires together auth, proxy, and telemetry settings. + +**Key Logic:** +- Configures `ANTHROPIC_API_URL` base URL, auth headers, proxy settings +- Calls `initializeGrowthBook()` early in startup +- Sets up OTel span management +- Handles `CLAUDE_CODE_SKIP_BEDROCK_TLS` for Bedrock TLS verification skip + +--- + +## services/api/client.ts + +**Path:** `src/services/api/client.ts` + +**Purpose:** Provides the configured SDK client instance and helper utilities for making API calls. + +**Exports:** + +```typescript +export function getClient(): Anthropic +export function getClientForModel(model: string): Anthropic +``` + +**Key Logic:** +- Client is created lazily and cached +- Applies base URL overrides from `ANTHROPIC_BASE_URL` env +- Configures mTLS via `getMtlsConfig()` +- Routes through proxy if `HTTPS_PROXY` / `HTTP_PROXY` set + +--- + +## services/api/claude.ts + +**Path:** `src/services/api/claude.ts` + +**Purpose:** Main API call layer for Claude completions. Handles prompt caching, extra body parameters, task budget configuration, metadata, and user message formatting. + +**Exports:** + +```typescript +export function getExtraBodyParams(betaHeaders?: string[]): JsonObject +export function getPromptCachingEnabled(model: string): boolean +export function getCacheControl(opts: { + scope?: string + querySource?: QuerySource +}): { type: 'ephemeral' | 'persistent'; ttl?: number; scope?: string } +export function configureTaskBudgetParams( + taskBudget: number, + outputConfig: OutputConfig, + betas: string[] +): void +export function getAPIMetadata(): { user_id: string } +export async function verifyApiKey( + apiKey: string, + isNonInteractiveSession: boolean +): Promise +export function userMessageToMessageParam( + message: UserMessage, + addCache: boolean, + enablePromptCaching: boolean, + querySource?: QuerySource +): MessageParam +``` + +**Internal functions (not exported):** +- `configureEffortParams()`: sets thinking budget based on effort level +- `should1hCacheTTL()`: checks if 1h TTL is applicable for current user/model + +**Key Logic:** +- **Prompt caching:** `getPromptCachingEnabled()` checks model allowlist. `getCacheControl()` returns `{ type: 'ephemeral' }` normally, `{ type: 'ephemeral', ttl: 3600 }` when 1h TTL gate passes +- **1h TTL gate:** `should1hCacheTTL()` checks `tengu_prompt_cache_1h_config` GrowthBook allowlist (session-stable, latched in `STATE.promptCache1hAllowlist`) and `STATE.promptCache1hEligible` (also latched to prevent mid-session overage flips) +- **Anti-distillation:** `tengu_anti_distill_fake_tool_injection` GrowthBook gate — injects fake tools into API calls as a training data quality signal +- **Extra body params:** `getExtraBodyParams()` assembles beta headers, model-specific params, and context management config + +**Dependencies:** `@anthropic-ai/sdk`, `bootstrap/state.js`, `services/analytics/growthbook.js`, `services/compact/apiMicrocompact.js` + +--- + +## services/api/dumpPrompts.ts + +**Path:** `src/services/api/dumpPrompts.ts` + +**Purpose:** Debug utility for ant users — dumps API request/response JSONL logs to disk for inspection. Used for prompt debugging and sharing bug reports. + +**Exports:** + +```typescript +export function getLastApiRequests(): Array<{ timestamp: string; request: unknown }> +export function clearApiRequestCache(): void +export function clearDumpState(agentIdOrSessionId: string): void +export function clearAllDumpState(): void +export function addApiRequestToCache(requestData: unknown): void +export function getDumpPromptsPath(agentIdOrSessionId?: string): string +export function createDumpPromptsFetch( + agentIdOrSessionId: string +): ClientOptions['fetch'] +``` + +**Key Logic:** +- Ant-only (no-op for non-ant users) +- `MAX_CACHED_REQUESTS = 5` — in-memory ring buffer of recent requests +- Deferred writes via `setImmediate` to avoid blocking the request path +- JSONL format with records of types: `init`, `system_update`, `message`, `response` +- Per-session state tracking with fingerprint-based change detection (avoids re-writing unchanged system prompts) +- Path: `~/.claude/dump-prompts/.jsonl` + +--- + +## services/api/emptyUsage.ts + +**Path:** `src/services/api/emptyUsage.ts` + +**Purpose:** Provides a zero-value `Usage` object for cases where usage data is unavailable. + +**Exports:** + +```typescript +export const EMPTY_USAGE: Usage +export type NonNullableUsage = { + input_tokens: number + output_tokens: number + cache_read_input_tokens: number + cache_creation_input_tokens: number +} +``` + +--- + +## services/api/errorUtils.ts + +**Path:** `src/services/api/errorUtils.ts` + +**Purpose:** Utilities for classifying and handling API errors. + +**Exports:** + +```typescript +export function isRateLimitError(error: unknown): boolean +export function isOverloadedError(error: unknown): boolean +export function isAuthError(error: unknown): boolean +export function isConnectionError(error: unknown): boolean +export function getRetryAfterMs(error: unknown): number | undefined +``` + +--- + +## services/api/errors.ts + +**Path:** `src/services/api/errors.ts` + +**Purpose:** Defines all API error message constants and classification functions for user-facing error handling. + +**Exports:** + +```typescript +export const API_ERROR_MESSAGE_PREFIX = 'API Error' +export function startsWithApiErrorPrefix(text: string): boolean + +export const PROMPT_TOO_LONG_ERROR_MESSAGE: string +export function isPromptTooLongMessage(msg: string): boolean +export function parsePromptTooLongTokenCounts( + rawMessage: string +): { actualTokens: number; limitTokens: number } | null +export function getPromptTooLongTokenGap(msg: string): number | undefined + +export function isMediaSizeError(raw: unknown): boolean +export function isMediaSizeErrorMessage(msg: string): boolean + +export const CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE: string +export const INVALID_API_KEY_ERROR_MESSAGE: string +export const INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL: string +export const TOKEN_REVOKED_ERROR_MESSAGE: string +export const CCR_AUTH_ERROR_MESSAGE: string +export const REPEATED_529_ERROR_MESSAGE: string +export const CUSTOM_OFF_SWITCH_MESSAGE: string +export const API_TIMEOUT_ERROR_MESSAGE: string +export const OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE: string + +export function getPdfTooLargeErrorMessage(): string +export function getPdfPasswordProtectedErrorMessage(): string +export function getPdfInvalidErrorMessage(): string +export function getImageTooLargeErrorMessage(): string +export function getRequestTooLargeErrorMessage(): string +export function getTokenRevokedErrorMessage(): string +export function getOauthOrgNotAllowedErrorMessage(): string +``` + +--- + +## services/api/filesApi.ts + +**Path:** `src/services/api/filesApi.ts` + +**Purpose:** Files API client — downloads, uploads, lists, and manages files in Files API (beta). + +**Constants:** +- `FILES_API_BETA_HEADER = 'files-api-2025-04-14,oauth-2025-04-20'` +- `MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024` (500MB) +- `DEFAULT_CONCURRENCY = 5` +- `MAX_RETRIES = 3` +- `BASE_DELAY_MS = 500` + +**Exports:** + +```typescript +export type File = { + fileId: string + relativePath: string + mimeType?: string +} + +export type FilesApiConfig = { + apiKey?: string + baseUrl?: string + sessionId?: string +} + +export type DownloadResult = { + fileId: string + relativePath: string + success: boolean + error?: string + savedPath?: string +} + +export type UploadResult = { + fileId: string + relativePath: string + success: boolean + error?: string + remoteFileId?: string +} + +export type FileMetadata = { + id: string + filename: string + created_at: number + purpose: string + size: number +} + +export class UploadNonRetriableError extends Error {} + +export function parseFileSpecs(fileSpecs: string[]): File[] +export async function downloadFile(fileId: string, config: FilesApiConfig): Promise +export function buildDownloadPath( + basePath: string, + sessionId: string, + relativePath: string +): string | null +export async function downloadAndSaveFile( + attachment: File, + config: FilesApiConfig +): Promise +export async function downloadSessionFiles( + files: File[], + config: FilesApiConfig, + concurrency?: number +): Promise +export async function uploadFile( + filePath: string, + relativePath: string, + config: FilesApiConfig, + opts?: { retries?: number } +): Promise +export async function uploadSessionFiles( + files: File[], + config: FilesApiConfig, + concurrency?: number +): Promise +export async function listFilesCreatedAfter( + afterCreatedAt: number, + config: FilesApiConfig +): Promise +``` + +**Key Logic:** +- `buildDownloadPath()`: path traversal guard — if `relativePath` contains `..` components or resolves outside `basePath/sessionId`, returns `null` +- Download/upload use exponential backoff: `BASE_DELAY_MS * 2^attempt` with jitter +- `uploadSessionFiles()` / `downloadSessionFiles()`: parallel with configurable concurrency (default 5) +- `listFilesCreatedAfter()`: paginated using cursor, collects all pages + +--- + +## services/api/firstTokenDate.ts + +**Path:** `src/services/api/firstTokenDate.ts` + +**Purpose:** Fetches and stores the date when the user first made a Claude Code API call. + +**Exports:** + +```typescript +export async function fetchAndStoreClaudeCodeFirstTokenDate(): Promise +``` + +**Key Logic:** +- Fetches `/api/organization/claude_code_first_token_date` +- Stores in `claudeCodeFirstTokenDate` config field +- Idempotent — no-ops if already stored + +--- + +## services/api/grove.ts + +**Path:** `src/services/api/grove.ts` + +**Purpose:** Grove is a consumer Terms/Privacy Policy notification feature. Manages fetching, caching, and determining whether to show the Grove notice to users. + +**Constants:** +- `GROVE_CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000` (24 hours) + +**Exports:** + +```typescript +export type AccountSettings = { + groveEnabled: boolean + groveNoticeViewed: boolean + // ... +} + +export type GroveConfig = { + enabled: boolean + forceShow: boolean + // ... +} + +export type ApiResult = { success: true; data: T } | { success: false; error: string } + +export async function getGroveSettings(): Promise> // memoized 24h +export async function markGroveNoticeViewed(): Promise +export async function updateGroveSettings(groveEnabled: boolean): Promise +export async function isQualifiedForGrove(): Promise +export async function getGroveNoticeConfig(): Promise> // memoized 24h +export function calculateShouldShowGrove( + settingsResult: ApiResult, + configResult: ApiResult, + showIfAlreadyViewed: boolean +): boolean +export async function checkGroveForNonInteractive(): Promise +``` + +**Key Logic:** +- Cache-first with background refresh: returns cached data immediately, refreshes in background after expiry +- `calculateShouldShowGrove()`: checks config enabled, settings not viewed, and user qualification +- `checkGroveForNonInteractive()`: called in non-interactive mode to log grove status without showing UI + +--- + +## services/api/logging.ts + +**Path:** `src/services/api/logging.ts` + +**Purpose:** API query/success/error logging with gateway detection and OTel span management. + +**Exports:** + +```typescript +export type GlobalCacheStrategy = 'tool_based' | 'system_prompt' | 'none' + +export function logAPIQuery(opts: { + model: string + querySource: QuerySource + // ... other fields +}): void + +export function logAPIError(opts: { + error: unknown + model: string + querySource: QuerySource + // ... other fields +}): void + +export function logAPISuccessAndDuration(opts: { + model: string + usage: Usage + querySource: QuerySource + durationMs: number + // ... other fields +}): void +``` + +**Key Logic:** +- Gateway detection: identifies litellm, helicone, portkey, cloudflare-ai-gateway, kong, braintrust, databricks from `ANTHROPIC_BASE_URL` +- Events: `tengu_api_query`, `tengu_api_error`, `tengu_api_success` +- OTel spans created/ended around API calls +- Teleport session tracking: logs extra fields when session is teleported +- Re-exports `EMPTY_USAGE` and `NonNullableUsage` + +--- + +## services/api/metricsOptOut.ts + +**Path:** `src/services/api/metricsOptOut.ts` + +**Purpose:** Checks whether metrics collection is enabled for the current organization. Implements two-tier caching. + +**Constants:** +- `CACHE_TTL_MS = 60 * 60 * 1000` (1 hour in-memory) +- `DISK_CACHE_TTL_MS = 24 * 60 * 60 * 1000` (24 hours on disk) +- Endpoint: `api/claude_code/organizations/metrics_enabled` + +**Exports:** + +```typescript +export async function checkMetricsEnabled(): Promise +export function _clearMetricsEnabledCacheForTesting(): void +``` + +**Key Logic:** +- Two-tier cache: in-memory (1h TTL) → disk (24h TTL) → network +- Requires `profile` OAuth scope; returns `enabled: true` if unauthenticated or scope missing +- `MetricsStatus`: `{ enabled: boolean; source: 'cache' | 'network' | 'default' }` + +--- + +## services/api/overageCreditGrant.ts + +**Path:** `src/services/api/overageCreditGrant.ts` + +**Purpose:** Manages overage credit grant information for subscribed users who exceed their plan limits. + +**Constants:** +- `CACHE_TTL_MS = 60 * 60 * 1000` (1 hour) + +**Exports:** + +```typescript +export type OverageCreditGrantInfo = { + hasGrant: boolean + amount?: number + currency?: string + expiresAt?: string +} + +export type OverageCreditGrantCacheEntry = { + data: OverageCreditGrantInfo + fetchedAt: number + orgId: string +} + +export function getCachedOverageCreditGrant(): OverageCreditGrantInfo | null +export function invalidateOverageCreditGrantCache(): void +export async function refreshOverageCreditGrantCache(): Promise +export function formatGrantAmount(info: OverageCreditGrantInfo): string | null +``` + +**Key Logic:** +- Per-org cache in `overageCreditGrantCache` Map (keyed by org ID) +- `formatGrantAmount()`: formats amount as currency string (e.g., "$5.00") or `null` if no grant + +--- + +## services/api/promptCacheBreakDetection.ts + +**Path:** `src/services/api/promptCacheBreakDetection.ts` + +**Purpose:** Detects unexpected prompt cache breaks that indicate server-side cache eviction. Writes diff files to disk for debugging. + +**Constants:** +- `CACHE_TTL_1HOUR_MS = 3_600_000` (1 hour) +- `MIN_CACHE_MISS_TOKENS = 2_000` — minimum to consider a break significant +- 95% threshold — cache reads must drop to ≤5% of expected to count as a break +- `MAX_TRACKED_SOURCES = 10` + +**Exports:** + +```typescript +export type PromptStateSnapshot = { + messages: MessageParam[] + systemPrompt: string + tools: unknown[] + timestamp: number + querySource: QuerySource +} + +export const CACHE_TTL_1HOUR_MS: number + +export function recordPromptState(snapshot: PromptStateSnapshot): void +export async function checkResponseForCacheBreak( + querySource: QuerySource, + cacheReadTokens: number, + cacheCreationTokens: number, + messages: MessageParam[], + agentId?: string, + requestId?: string +): Promise +export function notifyCacheDeletion(querySource: QuerySource, agentId?: string): void +export function notifyCompaction(querySource: QuerySource, agentId?: string): void +export function cleanupAgentTracking(agentId: string): void +export function resetPromptCacheBreakDetection(): void +``` + +**Key Logic:** +- 2-phase detection: `recordPromptState()` before call, `checkResponseForCacheBreak()` after +- Per-source tracking Map: keyed by `querySource` (or `agent:${agentId}`) +- Tracked source prefixes: `repl_main_thread`, `sdk`, `agent:custom`, `agent:default`, `agent:builtin` +- Writes diff files to `~/.claude/tmp/cache-break-*.diff` when a break is detected +- Events: `tengu_prompt_cache_break` +- `notifyCacheDeletion()` / `notifyCompaction()`: suppress false positives after intentional cache clearing + +--- + +## services/api/referral.ts + +**Path:** `src/services/api/referral.ts` + +**Purpose:** Manages referral program eligibility, redemptions, and guest passes for Claude Code subscribers. + +**Constants:** +- `CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000` (24 hours) + +**Exports:** + +```typescript +export async function fetchReferralEligibility( + campaign?: string +): Promise +export async function fetchReferralRedemptions( + campaign?: string +): Promise +export function checkCachedPassesEligibility(): { + eligible: boolean + needsRefresh: boolean + hasCache: boolean +} +export function formatCreditAmount(reward: ReferrerReward): string +export function getCachedReferrerReward(): ReferrerRewardInfo | null +export function getCachedRemainingPasses(): number | null +export async function fetchAndStorePassesEligibility(): Promise +export async function getCachedOrFetchPassesEligibility(): Promise +export async function prefetchPassesEligibility(): Promise +``` + +**Key Logic:** +- Max-subscription only — returns `null` / ineligible for non-max subscribers +- In-flight deduplication: multiple calls to `getCachedOrFetchPassesEligibility()` share one pending Promise +- 24h cache TTL + +--- + +## services/api/sessionIngress.ts + +**Path:** `src/services/api/sessionIngress.ts` + +**Purpose:** Manages session log ingress — append-log with optimistic concurrency for multi-writer scenarios (e.g., continued sessions from different machines). + +**Constants:** +- `MAX_RETRIES = 10` +- `BASE_DELAY_MS = 500` — exponential backoff base + +**Exports:** + +```typescript +export async function appendSessionLog( + sessionId: string, + entry: SessionLogEntry, + url: string +): Promise +export async function getSessionLogs( + sessionId: string, + url: string +): Promise +export async function getSessionLogsViaOAuth( + sessionId: string, + accessToken: string, + orgUUID: string +): Promise +export async function getTeleportEvents( + sessionId: string, + accessToken: string, + orgUUID: string +): Promise +export function clearSession(sessionId: string): void +export function clearAllSessions(): void +``` + +**Key Logic:** +- Optimistic concurrency: `Last-Uuid` header on append; 409 response adopts server's last UUID +- Sequential wrappers per session prevent out-of-order appends +- `getTeleportEvents()`: paginated (max 100 pages, 1000 events/page) +- `clearSession()` / `clearAllSessions()`: clears in-memory sequential wrapper state + +--- + +## services/api/ultrareviewQuota.ts + +**Path:** `src/services/api/ultrareviewQuota.ts` + +**Purpose:** Fetches ultrareview (deep code review) quota information for subscribed users. + +**Exports:** + +```typescript +export type UltrareviewQuotaResponse = { + used: number + limit: number + resetsAt: string +} + +export async function fetchUltrareviewQuota(): Promise +``` + +**Key Logic:** +- Endpoint: `/v1/ultrareview/quota` +- Subscriber-only; returns `null` for non-subscribers or on error +- 5 second timeout + +--- + +## services/api/usage.ts + +**Path:** `src/services/api/usage.ts` + +**Purpose:** Fetches usage statistics for the current user/organization from the Claude Code API. + +**Exports:** + +```typescript +export async function fetchUsage(): Promise +export type UsageStats = { + // usage breakdown fields +} +``` + +--- + +## services/api/withRetry.ts + +**Path:** `src/services/api/withRetry.ts` + +**Purpose:** Generic retry wrapper for API calls with exponential backoff. + +**Exports:** + +```typescript +export async function withRetry( + fn: () => Promise, + opts?: { + maxRetries?: number + baseDelayMs?: number + shouldRetry?: (error: unknown) => boolean + } +): Promise +``` + +--- + +## services/AgentSummary/agentSummary.ts + +**Path:** `src/services/AgentSummary/agentSummary.ts` + +**Purpose:** Periodic background summarization of agent conversations to compress context while preserving key information. + +**Key Logic:** +- Runs on a 30-second background timer +- Generates summaries using the main Claude model +- Compressed summaries are injected back as system messages +- Used by subagents and teammates to manage long-running conversations + +--- + +## services/autoDream/autoDream.ts + +**Path:** `src/services/autoDream/autoDream.ts` + +**Purpose:** Background memory consolidation system. Periodically scans session transcripts and uses a forked agent to consolidate learnings into persistent memory files. + +**Constants:** +- `SESSION_SCAN_INTERVAL_MS = 10 * 60 * 1000` (10 minutes) +- `DEFAULTS = { minHours: 24, minSessions: 5 }` — minimum time and sessions before consolidation + +**Exports:** + +```typescript +export function initAutoDream(): () => void // returns stop/cleanup function +``` + +**Key Logic:** +- Gate order: time check (minHours) → session count check (minSessions) → consolidation lock +- GrowthBook config `tengu_onyx_plover` controls `{ minHours, minSessions, enabled }` +- `initAutoDream()` registers as a post-sampling hook; returns cleanup function +- Uses `SESSION_SCAN_INTERVAL_MS` for polling +- Spawns a forked agent using `buildConsolidationPrompt()` + +--- + +## services/autoDream/config.ts + +**Path:** `src/services/autoDream/config.ts` + +**Purpose:** Configuration gate for the autoDream memory consolidation feature. + +**Exports:** + +```typescript +export function isAutoDreamEnabled(): boolean +``` + +**Key Logic:** User setting takes precedence over GrowthBook gate `tengu_onyx_plover`. Checks `userSettings.autoDream` first, then GrowthBook. + +--- + +## services/autoDream/consolidationLock.ts + +**Path:** `src/services/autoDream/consolidationLock.ts` + +**Purpose:** File-based mutex lock for the memory consolidation process to prevent concurrent consolidations across sessions/processes. + +**Constants:** +- `LOCK_FILE = '.consolidate-lock'` — in memory directory +- `HOLDER_STALE_MS = 60 * 60 * 1000` (1 hour) — stale lock threshold + +**Exports:** + +```typescript +export async function readLastConsolidatedAt(): Promise +export async function tryAcquireConsolidationLock(): Promise +export async function rollbackConsolidationLock(priorMtime: number): Promise +export async function listSessionsTouchedSince(sinceMs: number): Promise +export async function recordConsolidation(): Promise +``` + +**Key Logic:** +- Lock file mtime = `lastConsolidatedAt` timestamp (dual-purpose: both locking and timestamp) +- PID-based ownership — stale locks (PID dead or >1h old) are overwritten +- `tryAcquireConsolidationLock()`: returns prior mtime on success, `null` if already held +- `rollbackConsolidationLock()`: restores mtime to `priorMtime` on consolidation failure + +--- + +## services/autoDream/consolidationPrompt.ts + +**Path:** `src/services/autoDream/consolidationPrompt.ts` + +**Purpose:** Builds the system prompt for the memory consolidation agent. + +**Exports:** + +```typescript +export function buildConsolidationPrompt( + memoryRoot: string, + transcriptDir: string, + extra: string +): string +``` + +**Key Logic:** Returns a 4-phase prompt: +1. Orient — read existing memory files to understand current state +2. Gather recent signal — read session transcripts since last consolidation +3. Consolidate — merge new learnings into memory files +4. Prune and index — remove stale entries, update index file + +--- + +## services/awaySummary.ts + +**Path:** `src/services/awaySummary.ts` + +**Purpose:** Generates "away summaries" — brief catch-up summaries shown when the user returns to a long-running session after being away. + +**Key Logic:** +- Triggered when `lastInteractionTime` gap exceeds threshold +- Uses Claude to generate a brief (1-3 sentence) summary of what happened while away +- Displayed as a system message above the prompt + +--- + +## services/claudeAiLimits.ts + +**Path:** `src/services/claudeAiLimits.ts` + +**Purpose:** Fetches and manages rate limit information for Claude.ai-authenticated users. + +**Exports:** + +```typescript +export async function fetchClaudeAiLimits(): Promise +export type ClaudeAiLimits = { + // rate limit fields +} +``` + +--- + +## services/claudeAiLimitsHook.ts + +**Path:** `src/services/claudeAiLimitsHook.ts` + +**Purpose:** React hook for accessing Claude.ai rate limit data with automatic refresh. + +**Exports:** + +```typescript +export function useClaudeAiLimits(): ClaudeAiLimits | null +``` + +--- + +## services/compact/apiMicrocompact.ts + +**Path:** `src/services/compact/apiMicrocompact.ts` + +**Purpose:** API-native context management strategies using server-side `cache_edits` feature. Configures context window editing without full client-side rewriting. + +**Constants:** +- `DEFAULT_MAX_INPUT_TOKENS = 180_000` +- `DEFAULT_TARGET_INPUT_TOKENS = 40_000` + +**Type: `ContextEditStrategy`:** +```typescript +export type ContextEditStrategy = + | { + type: 'clear_tool_uses_20250919' + trigger?: { type: 'input_tokens'; value: number } + keep?: { type: 'tool_uses'; value: number } + clear_tool_inputs?: boolean | string[] + exclude_tools?: string[] + clear_at_least?: { type: 'input_tokens'; value: number } + } + | { + type: 'clear_thinking_20251015' + keep: { type: 'thinking_turns'; value: number } | 'all' + } + +export type ContextManagementConfig = { + edits: ContextEditStrategy[] +} +``` + +**Exports:** + +```typescript +export function getAPIContextManagement(options?: { + hasThinking?: boolean + isRedactThinkingActive?: boolean + clearAllThinking?: boolean +}): ContextManagementConfig | undefined +``` + +**Key Logic:** +- Tool clearing strategies are ant-only, gated by `USE_API_CLEAR_TOOL_RESULTS` and `USE_API_CLEAR_TOOL_USES` env vars +- `TOOLS_CLEARABLE_RESULTS`: shell tools, Glob, Grep, FileRead, WebFetch, WebSearch +- `TOOLS_CLEARABLE_USES`: FileEdit, FileWrite, NotebookEdit +- Thinking clearing: when `hasThinking && !isRedactThinkingActive`, adds `clear_thinking_20251015` +- When `clearAllThinking` (>1h idle = confirmed cache miss): keeps only last 1 thinking turn + +--- + +## services/compact/autoCompact.ts + +**Path:** `src/services/compact/autoCompact.ts` + +**Purpose:** Automatic context compaction — triggers full conversation summarization when context window usage exceeds threshold. + +**Key Logic:** +- Monitors token usage against configurable threshold (default 90% of context window) +- When triggered, calls `compact()` to summarize the conversation +- Posts a `CompactBoundaryMessage` in the conversation to mark compaction point +- Resets token tracking after compaction + +--- + +## services/compact/compact.ts + +**Path:** `src/services/compact/compact.ts` + +**Purpose:** Full conversation compaction — replaces conversation history with an LLM-generated summary. + +**Key Logic:** +- Uses the detailed analysis instruction prompts from `prompt.ts` +- Optionally performs partial compaction (keeps recent messages) +- The `NO_TOOLS_PREAMBLE` constant is a critical instruction preventing the compaction model from calling tools during summarization +- Writes compact summary as a synthetic `` tagged message + +--- + +## services/compact/compactWarningHook.ts + +**Path:** `src/services/compact/compactWarningHook.ts` + +**Purpose:** React hook for accessing the compact warning suppression state. + +**Exports:** + +```typescript +export function useCompactWarningSuppression(): boolean +``` + +--- + +## services/compact/compactWarningState.ts + +**Path:** `src/services/compact/compactWarningState.ts` + +**Purpose:** Store and actions for suppressing the "compact recommended" warning after microcompact runs. + +**Exports:** + +```typescript +export const compactWarningStore: Store +export function suppressCompactWarning(): void +export function clearCompactWarningSuppression(): void +``` + +--- + +## services/compact/grouping.ts + +**Path:** `src/services/compact/grouping.ts` + +**Purpose:** Groups conversation messages by API round (each assistant message with its preceding user message). + +**Exports:** + +```typescript +export function groupMessagesByApiRound(messages: Message[]): Message[][] +``` + +**Key Logic:** Groups by assistant `message.id` boundary — each group contains one API round (user + assistant + tool results). + +--- + +## services/compact/microCompact.ts + +**Path:** `src/services/compact/microCompact.ts` + +**Purpose:** Microcompaction — lightweight context reduction by clearing tool result content without full conversation summarization. Two paths: cached microcompact (via API `cache_edits`) and time-based microcompact (direct content mutation when cache is cold). + +**Constants (exported):** +```typescript +export const TIME_BASED_MC_CLEARED_MESSAGE = '[Old tool result content cleared]' +``` + +**Compactable tool sets:** +```typescript +const COMPACTABLE_TOOLS = new Set([ + FILE_READ_TOOL_NAME, SHELL_TOOL_NAMES..., GREP_TOOL_NAME, GLOB_TOOL_NAME, + WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME, FILE_EDIT_TOOL_NAME, FILE_WRITE_TOOL_NAME +]) +``` + +**Exports:** + +```typescript +export function consumePendingCacheEdits(): + import('./cachedMicrocompact.js').CacheEditsBlock | null + +export function getPinnedCacheEdits(): + import('./cachedMicrocompact.js').PinnedCacheEdits[] + +export function pinCacheEdits( + userMessageIndex: number, + block: import('./cachedMicrocompact.js').CacheEditsBlock +): void + +export function markToolsSentToAPIState(): void + +export function resetMicrocompactState(): void + +export function estimateMessageTokens(messages: Message[]): number + +export type PendingCacheEdits = { + trigger: 'auto' + deletedToolIds: string[] + baselineCacheDeletedTokens: number +} + +export type MicrocompactResult = { + messages: Message[] + compactionInfo?: { + pendingCacheEdits?: PendingCacheEdits + } +} + +export function evaluateTimeBasedTrigger( + messages: Message[], + querySource: QuerySource | undefined +): { gapMinutes: number; config: TimeBasedMCConfig } | null + +export async function microcompactMessages( + messages: Message[], + toolUseContext?: ToolUseContext, + querySource?: QuerySource +): Promise +``` + +**Key Logic:** +- `microcompactMessages()` dispatch order: + 1. Time-based trigger check: if gap since last assistant > threshold → `maybeTimeBasedMicrocompact()` (short-circuits) + 2. Cached MC path: if `CACHED_MICROCOMPACT` feature enabled, model supported, and main thread source → `cachedMicrocompactPath()` + 3. Otherwise: return messages unchanged (legacy path removed) +- **Cached MC path:** registers tool results grouped by user message; calls `getToolResultsToDelete()`; queues `CacheEditsBlock` as `pendingCacheEdits`; does NOT mutate message content +- **Time-based MC path:** directly mutates tool result `content` to `TIME_BASED_MC_CLEARED_MESSAGE`; resets cached MC state; notifies cache break detection +- `estimateMessageTokens()`: rough estimation with 4/3 padding factor; images/documents = 2000 tokens +- `isMainThreadSource()`: prefix-matches `repl_main_thread` (handles output style variants like `repl_main_thread:outputStyle:custom`) +- Events: `tengu_cached_microcompact`, `tengu_time_based_microcompact` + +--- + +## services/compact/postCompactCleanup.ts + +**Path:** `src/services/compact/postCompactCleanup.ts` + +**Purpose:** Runs cleanup tasks after any compaction (auto or manual `/compact`). + +**Exports:** + +```typescript +export function runPostCompactCleanup(querySource?: QuerySource): void +``` + +**Key Logic:** Clears: microcompact state, context collapse state, system prompt sections, classifier approvals, speculative checks, beta tracing state, session messages cache. + +--- + +## services/compact/prompt.ts + +**Path:** `src/services/compact/prompt.ts` + +**Purpose:** Prompt constants used for compact summarization. + +**Key Exports:** +- `NO_TOOLS_PREAMBLE`: Critical instruction string prepended to compact calls — prevents the model from invoking any tools during summarization +- `DETAILED_ANALYSIS_INSTRUCTION_BASE`: Instruction for full compaction +- `DETAILED_ANALYSIS_INSTRUCTION_PARTIAL`: Instruction for partial compaction (keeps recent messages) + +--- + +## services/compact/sessionMemoryCompact.ts + +**Path:** `src/services/compact/sessionMemoryCompact.ts` + +**Purpose:** Session memory compaction — a lighter alternative to full compact that summarizes older session memory segments while preserving recent tool context. + +**Constants:** +```typescript +export const DEFAULT_SM_COMPACT_CONFIG: SessionMemoryCompactConfig = { + minTokens: 10000, + minTextBlockMessages: 5, + maxTokens: 40000 +} +``` + +**Exports:** + +```typescript +export type SessionMemoryCompactConfig = { + minTokens: number + minTextBlockMessages: number + maxTokens: number +} + +export function setSessionMemoryCompactConfig(config: SessionMemoryCompactConfig): void +export function getSessionMemoryCompactConfig(): SessionMemoryCompactConfig +export function resetSessionMemoryCompactConfig(): void +export function hasTextBlocks(message: Message): boolean +export function adjustIndexToPreserveAPIInvariants( + messages: Message[], + startIndex: number +): number +export function calculateMessagesToKeepIndex( + messages: Message[], + lastSummarizedIndex: number +): number +export function shouldUseSessionMemoryCompaction(): boolean +export async function trySessionMemoryCompaction( + messages: Message[], + agentId?: string, + autoCompactThreshold?: number +): Promise +``` + +**Key Logic:** +- GrowthBook config `tengu_sm_compact_config` overrides defaults +- Guarded by `ENABLE_CLAUDE_CODE_SM_COMPACT` / `DISABLE_CLAUDE_CODE_SM_COMPACT` env vars +- AND requires both GrowthBook gates `tengu_session_memory` AND `tengu_sm_compact` to be enabled +- `adjustIndexToPreserveAPIInvariants()`: ensures cut point doesn't leave orphaned tool_use without tool_result +- `calculateMessagesToKeepIndex()`: binary search-based index calculation respecting min/max token bounds + +--- + +## services/compact/timeBasedMCConfig.ts + +**Path:** `src/services/compact/timeBasedMCConfig.ts` + +**Purpose:** Configuration for time-based microcompact (triggers on idle gap). + +**Exports:** + +```typescript +export type TimeBasedMCConfig = { + enabled: boolean + gapThresholdMinutes: number + keepRecent: number +} + +export function getTimeBasedMCConfig(): TimeBasedMCConfig +``` + +**Key Logic:** +- GrowthBook config: `tengu_slate_heron` +- Defaults: `{ enabled: false, gapThresholdMinutes: 60, keepRecent: 5 }` + +--- + +## services/diagnosticTracking.ts + +**Path:** `src/services/diagnosticTracking.ts` + +**Purpose:** Tracks file diagnostics (lint errors, type errors) before and after file edits to detect regressions introduced by Claude Code. + +**Constants:** +- `MAX_DIAGNOSTICS_SUMMARY_CHARS = 4000` + +**Exports:** + +```typescript +export interface Diagnostic { + severity: 'error' | 'warning' | 'information' | 'hint' + message: string + source?: string + range: { start: { line: number; character: number }; end: { line: number; character: number } } +} + +export interface DiagnosticFile { + uri: string + diagnostics: Diagnostic[] +} + +export class DiagnosticTrackingService { + static getInstance(): DiagnosticTrackingService + initialize(mcpClient: unknown): void + shutdown(): Promise + reset(): void + ensureFileOpened(fileUri: string): Promise + beforeFileEdited(filePath: string): Promise +} +``` + +**Key Logic:** +- Singleton via `getInstance()` +- `beforeFileEdited()`: captures current diagnostics for a file before any edits, so post-edit diagnostics can be compared +- `initialize()`: connects to LSP MCP client for diagnostic data +- `shutdown()`: flushes any pending diagnostic comparisons + +--- + +## services/internalLogging.ts + +**Path:** `src/services/internalLogging.ts` + +**Purpose:** Internal logging utilities for internal (ant) users — logs K8s namespace, container ID, and tool permission context for debugging. + +**Exports:** + +```typescript +export async function getContainerId(): Promise // memoized +export async function logPermissionContextForAnts( + toolPermissionContext: unknown, + moment: string +): Promise +``` + +**Key Logic:** +- K8s namespace: reads from `/var/run/secrets/kubernetes.io/serviceaccount/namespace` +- Container ID: extracted from `/proc/self/mountinfo` (first overlay/device entry) +- Both are memoized — container identity doesn't change mid-session +- Only logs when `USER_TYPE === 'ant'` + +--- + +## services/MagicDocs/magicDocs.ts + +**Path:** `src/services/MagicDocs/magicDocs.ts` + +**Purpose:** Auto-maintained markdown documentation files that stay in sync with code changes made by Claude Code. + +**Key Logic:** +- Monitors file edits and regenerates associated markdown docs +- Uses Claude to understand the semantic meaning of changes +- Prompts defined in `prompts.ts` + +--- + +## services/MagicDocs/prompts.ts + +**Path:** `src/services/MagicDocs/prompts.ts` + +**Purpose:** System prompt and instruction templates for magic docs generation. + +--- + +## services/mcpServerApproval.tsx + +**Path:** `src/services/mcpServerApproval.tsx` + +**Purpose:** Shows MCP server approval dialogs for pending project servers at startup, reusing the existing Ink root instance. + +**Exports:** + +```typescript +export async function handleMcpjsonServerApprovals(root: Root): Promise +``` + +**Key Logic:** +- Queries `getMcpConfigsByScope('project')` for project-scoped MCP servers +- Filters to servers with status `'pending'` via `getProjectMcpServerStatus()` +- Single pending server: renders `MCPServerApprovalDialog` +- Multiple pending servers: renders `MCPServerMultiselectDialog` +- Awaits user decision via Promise/resolve pattern before returning + +--- + +## services/mockRateLimits.ts + +**Path:** `src/services/mockRateLimits.ts` + +**Purpose:** Development/testing utility for simulating various rate limit scenarios without hitting actual API limits. Ant-only. + +**Exports:** + +```typescript +export type MockHeaderKey = + | 'x-ratelimit-requests-remaining' + | 'x-ratelimit-tokens-remaining' + // ... other rate limit header names + +export type MockScenario = + | 'primary_hard_limit' + | 'secondary_hard_limit' + | 'approaching_limit' + | 'fast_mode_rate_limit' + | 'burst_limit' + // ... 20+ scenarios total + +export function setMockHeader(key: MockHeaderKey, value?: string): void +export function addExceededLimit(type: string, hoursFromNow: number): void +export function setMockEarlyWarning( + claimAbbrev: string, + utilization: number, + hoursFromNow?: number +): void +export function clearMockEarlyWarning(): void +export function setMockRateLimitScenario(scenario: MockScenario): void +export function getMockHeaderless429Message(): string | null +export function getMockHeaders(): MockHeaders | null +export function getMockStatus(): string +export function clearMockHeaders(): void +export function applyMockHeaders(headers: Headers): Headers +export function shouldProcessMockLimits(): boolean +export function getCurrentMockScenario(): MockScenario | null +export function getScenarioDescription(scenario: MockScenario): string +export function setMockSubscriptionType(type: SubscriptionType | null): void +export function getMockSubscriptionType(): SubscriptionType | null +export function shouldUseMockSubscription(): boolean +export function setMockBillingAccess(hasAccess: boolean | null): void +export function isMockFastModeRateLimitScenario(): boolean +export function checkMockFastModeRateLimit(isFastModeActive?: boolean): MockHeaders | null +``` + +--- + +## services/notifier.ts + +**Path:** `src/services/notifier.ts` + +**Purpose:** System-level desktop notifications for long-running operations. + +**Key Logic:** +- Sends macOS/Linux notifications when task completes (user is away) +- Uses `node-notifier` or native OS notification APIs +- Gated on user preference and focus state + +--- + +## services/preventSleep.ts + +**Path:** `src/services/preventSleep.ts` + +**Purpose:** Prevents system sleep while Claude Code tasks are running. + +**Key Logic:** +- Uses platform-specific APIs (caffeinate on macOS, systemd-inhibit on Linux) +- Returns a cleanup function to re-enable sleep +- Only active during tool execution phases + +--- + +## services/PromptSuggestion/promptSuggestion.ts + +**Path:** `src/services/PromptSuggestion/promptSuggestion.ts` + +**Purpose:** Generates prompt suggestions based on current codebase context for the prompt input autocomplete. + +**Key Logic:** +- Analyzes recent git changes, open files, and task patterns +- Returns ranked suggestion list +- Caches suggestions per-context hash to avoid redundant generation + +--- + +## services/PromptSuggestion/speculation.ts + +**Path:** `src/services/PromptSuggestion/speculation.ts` + +**Purpose:** Speculative pre-execution — starts running likely next commands before user confirms, then either applies or discards the result. + +**Key Logic:** +- Monitors user typing patterns to predict next action +- Pre-warms common tool executions (file reads, searches) +- Cancels speculative execution if prediction was wrong + +--- + +## services/rateLimitMocking.ts + +**Path:** `src/services/rateLimitMocking.ts` + +**Purpose:** Facade layer for rate limit mock application and error checking. + +**Exports:** + +```typescript +export function processRateLimitHeaders(headers: Headers): Headers +export function shouldProcessRateLimits(isSubscriber: boolean): boolean +export function checkMockRateLimitError( + currentModel: string, + isFastModeActive?: boolean +): APIError | null +export function isMockRateLimitError(error: unknown): boolean +export { shouldProcessMockLimits } // re-exported from mockRateLimits.ts +``` + +--- + +## services/rateLimitMessages.ts + +**Path:** `src/services/rateLimitMessages.ts` + +**Purpose:** Human-readable rate limit message formatting and display logic. + +**Key Logic:** +- Formats rate limit headers into user-friendly messages +- Handles early warning, hard limit, and reset time display +- Localizes timestamps to user's timezone + +--- + +## services/SessionMemory/prompts.ts + +**Path:** `src/services/SessionMemory/prompts.ts` + +**Purpose:** Prompt templates for session memory operations (summarization, retrieval, consolidation). + +--- + +## services/SessionMemory/sessionMemory.ts + +**Path:** `src/services/SessionMemory/sessionMemory.ts` + +**Purpose:** Manages persistent session memory — stores and retrieves relevant context snippets across sessions. + +**Key Logic:** +- GrowthBook gate: `tengu_session_memory` +- Stores memory in `~/.claude/sessions//memory.jsonl` +- Retrieves relevant memories using semantic similarity +- Integrates with conversation context as system prompt additions + +--- + +## services/SessionMemory/sessionMemoryUtils.ts + +**Path:** `src/services/SessionMemory/sessionMemoryUtils.ts` + +**Purpose:** Utility functions for session memory operations (formatting, filtering, path resolution). + +--- + +## services/tokenEstimation.ts + +**Path:** `src/services/tokenEstimation.ts` + +**Purpose:** Rough token count estimation without calling the API tokenizer. + +**Exports:** + +```typescript +export function roughTokenCountEstimation(text: string): number +``` + +**Key Logic:** Approximates token count as `text.length / 4` (roughly 4 chars per token for English/code). Used for pre-flight estimates in compaction decisions. + +--- + +## services/vcr.ts + +**Path:** `src/services/vcr.ts` + +**Purpose:** VCR (Video Cassette Recorder) test fixture system — records and replays API interactions for deterministic testing. + +**Exports:** + +```typescript +export async function withVCR( + messages: unknown[], + f: () => Promise +): Promise + +export async function withFixture( + input: unknown, + fixtureName: string, + f: () => Promise +): Promise + +export async function withStreamingVCR( + messages: unknown[], + f: () => Promise +): Promise +``` + +**Key Logic:** +- SHA1-hashes input to create fixture filenames (deterministic, content-addressed) +- `FORCE_VCR` env var: ants can force VCR mode outside of test environment +- CI guard: fails if fixture is missing and `VCR_RECORD` is not set (prevents silent misses) +- Fixture storage: `src/test-fixtures/vcr/` directory + +--- + +## services/voice.ts + +**Path:** `src/services/voice.ts` + +**Purpose:** Audio recording service for push-to-talk voice input. Supports native audio (cpal via NAPI) on macOS/Linux/Windows with SoX and arecord fallbacks on Linux. + +**Constants:** +- `RECORDING_SAMPLE_RATE = 16000` +- `RECORDING_CHANNELS = 1` +- `SILENCE_DURATION_SECS = '2.0'` — SoX silence detection +- `SILENCE_THRESHOLD = '3%'` + +**Exports:** + +```typescript +export type RecordingAvailability = { + available: boolean + reason: string | null +} + +export async function checkVoiceDependencies(): Promise<{ + available: boolean + missing: string[] + installCommand: string | null +}> + +export async function requestMicrophonePermission(): Promise + +export async function checkRecordingAvailability(): Promise + +export async function startRecording( + onData: (chunk: Buffer) => void, + onEnd: () => void, + options?: { silenceDetection?: boolean } +): Promise + +export function stopRecording(): void + +export function _resetArecordProbeForTesting(): void +export function _resetAlsaCardsForTesting(): void +``` + +**Key Logic:** +- **Backend selection priority:** native (cpal via NAPI) → arecord (ALSA, Linux only) → SoX rec +- **Native module:** `audio-capture-napi` is lazy-loaded on first voice keypress (dlopen blocks event loop ~1s warm, ~8s cold) +- **arecord probe:** memoized async probe that verifies device open succeeds (not just binary existence); 150ms race timer +- **Linux ALSA guard:** checks `/proc/asound/cards` before using native cpal to avoid spurious stderr +- **WSL handling:** distinguishes WSL1 (no audio), Win10 WSL2 (no audio), Win11 WSLg (PulseAudio works) +- SoX arguments: raw PCM 16kHz/16-bit/mono with `--buffer 1024` for small chunk flushing and silence detection +- Push-to-talk mode: `silenceDetection: false` ignores native module's silence-triggered `onEnd` + +--- + +## services/voiceKeyterms.ts + +**Path:** `src/services/voiceKeyterms.ts` + +**Purpose:** Generates domain-specific vocabulary hints (Deepgram "keywords") for improved STT accuracy in the voice_stream endpoint. + +**Constants:** +- `MAX_KEYTERMS = 50` +- `GLOBAL_KEYTERMS`: hardcoded list including `'MCP'`, `'symlink'`, `'grep'`, `'regex'`, `'localhost'`, `'codebase'`, `'TypeScript'`, `'JSON'`, `'OAuth'`, `'webhook'`, `'gRPC'`, `'dotfiles'`, `'subagent'`, `'worktree'` + +**Exports:** + +```typescript +export function splitIdentifier(name: string): string[] + +export async function getVoiceKeyterms( + recentFiles?: ReadonlySet +): Promise +``` + +**Key Logic:** +- `splitIdentifier()`: splits camelCase/PascalCase/kebab-case/snake_case/path identifiers into words; discards fragments ≤2 chars +- `getVoiceKeyterms()`: combines global terms + project root basename + git branch words + recent file name words +- Project root basename kept whole (not split) to match full project name phrases +- Git branch words split via `splitIdentifier()`; recent files split by filename stem +- Capped at `MAX_KEYTERMS = 50` + +--- + +## services/voiceStreamSTT.ts + +**Path:** `src/services/voiceStreamSTT.ts` + +**Purpose:** voice_stream WebSocket STT client. Connects to `wss://api.anthropic.com/api/ws/speech_to_text/voice_stream` using OAuth credentials for push-to-talk transcription. + +**Constants:** +- `VOICE_STREAM_PATH = '/api/ws/speech_to_text/voice_stream'` +- `KEEPALIVE_INTERVAL_MS = 8_000` +- `FINALIZE_TIMEOUTS_MS = { safety: 5_000, noData: 1_500 }` (exported for tests) +- Wire messages: `KEEPALIVE_MSG = '{"type":"KeepAlive"}'`, `CLOSE_STREAM_MSG = '{"type":"CloseStream"}'` + +**Exports:** + +```typescript +export const FINALIZE_TIMEOUTS_MS: { safety: number; noData: number } + +export type VoiceStreamCallbacks = { + onTranscript: (text: string, isFinal: boolean) => void + onError: (error: string, opts?: { fatal?: boolean }) => void + onClose: () => void + onReady: (connection: VoiceStreamConnection) => void +} + +export type FinalizeSource = + | 'post_closestream_endpoint' + | 'no_data_timeout' + | 'safety_timeout' + | 'ws_close' + | 'ws_already_closed' + +export type VoiceStreamConnection = { + send: (audioChunk: Buffer) => void + finalize: () => Promise + close: () => void + isConnected: () => boolean +} + +export function isVoiceStreamAvailable(): boolean + +export async function connectVoiceStream( + callbacks: VoiceStreamCallbacks, + options?: { language?: string; keyterms?: string[] } +): Promise +``` + +**Key Logic:** +- Only available for OAuth-authenticated users (OAuth tokens); gates on `isOAuthAuthEnabled()` and valid access token +- Routes to `api.anthropic.com` (not `claude.ai`) to avoid Cloudflare TLS fingerprinting challenges +- `VOICE_STREAM_BASE_URL` env var allows override for testing +- URL params: `encoding=linear16`, `sample_rate=16000`, `channels=1`, `endpointing_ms=300`, `utterance_end_ms=1000` +- GrowthBook gate `tengu_cobalt_frost`: enables Nova 3 STT provider via `use_conversation_engine=true&stt_provider=deepgram-nova3` +- Keyterms appended as repeated `keyterms=` query params +- keepalive interval: 8s +- `finalize()` sends `CloseStream`, races `noData` (1.5s) vs `safety` (5s) timers vs `TranscriptEndpoint` message +- Wire message types: `TranscriptText`, `TranscriptEndpoint`, `TranscriptError`, `error` +- Ant-only build (behind `feature('VOICE_MODE')` gate) + +--- + +## context/QueuedMessageContext.tsx + +**Path:** `src/context/QueuedMessageContext.tsx` + +**Purpose:** React context for the queued message system — tracks messages waiting to be sent to Claude when the current turn completes. + +**Exports:** +```typescript +export const QueuedMessageContext: React.Context +export function useQueuedMessages(): QueuedMessage[] +export function QueuedMessageProvider(props: { children: React.ReactNode }): JSX.Element +``` + +--- + +## context/fpsMetrics.tsx + +**Path:** `src/context/fpsMetrics.tsx` + +**Purpose:** FPS (frames per second) measurement context for Ink terminal rendering performance monitoring. + +**Exports:** +```typescript +export function FpsMetricsProvider(props: { children: React.ReactNode }): JSX.Element +export function useFpsMetrics(): { fps: number; frameCount: number } +``` + +--- + +## context/mailbox.tsx + +**Path:** `src/context/mailbox.tsx` + +**Purpose:** Provides inter-agent messaging context — the "mailbox" for receiving messages from other agents/workers. + +**Exports:** +```typescript +export type MailboxMessage = { from: string; content: string; timestamp: number } +export const MailboxContext: React.Context +export function MailboxProvider(props: { children: React.ReactNode }): JSX.Element +export function useMailbox(): MailboxMessage[] +``` + +--- + +## context/modalContext.tsx + +**Path:** `src/context/modalContext.tsx` + +**Purpose:** Context for managing modal dialog state — tracks which modal is currently open and provides open/close actions. + +**Exports:** +```typescript +export type ModalState = { isOpen: boolean; content: React.ReactNode | null } +export const ModalContext: React.Context +export function ModalProvider(props: { children: React.ReactNode }): JSX.Element +export function useModal(): { open: (content: React.ReactNode) => void; close: () => void } +``` + +--- + +## context/notifications.tsx + +**Path:** `src/context/notifications.tsx` + +**Purpose:** Notification queue management — displays toast-style notifications in the status line with priority ordering, deduplication, fold/merge, and timeout handling. + +**Types:** + +```typescript +type Priority = 'low' | 'medium' | 'high' | 'immediate' + +type BaseNotification = { + key: string + invalidates?: string[] + priority: Priority + timeoutMs?: number + fold?: (accumulator: Notification, incoming: Notification) => Notification +} + +type TextNotification = BaseNotification & { text: string; color?: keyof Theme } +type JSXNotification = BaseNotification & { jsx: React.ReactNode } +export type Notification = TextNotification | JSXNotification +``` + +**Exports:** + +```typescript +const DEFAULT_TIMEOUT_MS = 8000 + +export function useNotifications(): { + addNotification: (content: Notification) => void + removeNotification: (key: string) => void +} +``` + +**Key Logic:** +- Notification state lives in `AppState` (`notifications.current` + `notifications.queue`) +- `immediate` priority: bypasses queue, shows immediately, re-queues current (non-immediate) notification +- `fold` function: merges notifications with the same key (accumulator pattern) +- Deduplication: only one notification per key in queue + current +- `invalidates[]`: removes named notifications from queue and clears current if matching +- `DEFAULT_TIMEOUT_MS = 8000` (8 seconds); auto-advance to next queued after timeout +- Module-level `currentTimeoutId` tracks the active auto-dismiss timer + +--- + +## context/overlayContext.tsx + +**Path:** `src/context/overlayContext.tsx` + +**Purpose:** Overlay tracking for Escape key coordination. Tracks which overlays (dialogs, selects) are currently open so the cancel handler doesn't misinterpret Escape presses. + +**Constants:** +```typescript +const NON_MODAL_OVERLAYS = new Set(['autocomplete']) +``` + +**Exports:** + +```typescript +export function useRegisterOverlay(id: string, enabled?: boolean): void +export function useIsOverlayActive(): boolean +export function useIsModalOverlayActive(): boolean +``` + +**Key Logic:** +- State stored in `AppState.activeOverlays: Set` +- `useRegisterOverlay()`: registers on mount (useEffect), unregisters on unmount via cleanup +- On overlay close: triggers `instances.get(process.stdout)?.invalidatePrevFrame()` (via useLayoutEffect) to force full-damage diff — prevents ghost cells from tall overlays (e.g. FuzzyPicker) +- `useIsOverlayActive()`: `activeOverlays.size > 0` +- `useIsModalOverlayActive()`: any overlay in set that is NOT in `NON_MODAL_OVERLAYS` + +--- + +## context/promptOverlayContext.tsx + +**Path:** `src/context/promptOverlayContext.tsx` + +**Purpose:** Context for prompt-level overlay management — tracks overlays that affect prompt input focus and behavior. + +**Exports:** +```typescript +export function useRegisterPromptOverlay(id: string, enabled?: boolean): void +export function useIsPromptOverlayActive(): boolean +``` + +--- + +## context/stats.tsx + +**Path:** `src/context/stats.tsx` + +**Purpose:** In-process performance metrics store with reservoir sampling for histograms. Persists metrics to project config on process exit. + +**Constants:** +- `RESERVOIR_SIZE = 1024` — reservoir sampling capacity for histograms + +**Types:** + +```typescript +export type StatsStore = { + increment(name: string, value?: number): void + set(name: string, value: number): void + observe(name: string, value: number): void + add(name: string, value: string): void + getAll(): Record +} +``` + +**Exports:** + +```typescript +export function createStatsStore(): StatsStore +export const StatsContext: React.Context +export function StatsProvider(props: { store?: StatsStore; children: React.ReactNode }): JSX.Element +export function useStats(): StatsStore +export function useCounter(name: string): (value?: number) => void +export function useGauge(name: string): (value: number) => void +export function useTimer(name: string): (value: number) => void +// ... additional hook exports +``` + +**Key Logic:** +- `createStatsStore()`: creates a stats store with three internal data structures: + - `metrics: Map` — counters and gauges + - `histograms: Map` — reservoir-sampled distributions + - `sets: Map>` — unique value sets (reported as `.size`) +- `observe()`: histogram using reservoir sampling (Algorithm R) with `RESERVOIR_SIZE = 1024` +- `getAll()`: returns flat `Record` with histogram percentiles (`_p50`, `_p95`, `_p99`), min, max, avg, count +- `StatsProvider`: flushes metrics to `lastSessionMetrics` in project config on process `'exit'` event +- `useCounter()` / `useGauge()` / `useTimer()`: memoized hooks returning bound store methods + +--- + +## context/voice.tsx + +**Path:** `src/context/voice.tsx` + +**Purpose:** Voice mode context — provides voice recording state, transcript, and connection status to the UI. + +**Exports:** + +```typescript +export type VoiceContextValue = { + isRecording: boolean + transcript: string + isConnecting: boolean + error: string | null + startRecording: () => void + stopRecording: () => void +} + +export const VoiceContext: React.Context +export function VoiceProvider(props: { children: React.ReactNode }): JSX.Element +export function useVoice(): VoiceContextValue +``` + +--- + +## screens/Doctor.tsx + +**Path:** `src/screens/Doctor.tsx` + +**Purpose:** The `/doctor` command UI screen — displays system health diagnostics including version info, environment checks, MCP server status, sandbox status, keybinding warnings, and available updates. + +**Props:** +```typescript +type Props = { + onDone: () => void +} +``` + +**Internal Types:** +```typescript +type AgentInfo = { + name: string + version: string + // ... +} + +type VersionLockInfo = { + locked: boolean + version?: string + // ... +} +``` + +**Key Logic:** +- Uses `getDoctorDiagnostic()` for environment checks +- Calls `checkContextWarnings()` for context-related issues +- Loads dist tags with `getNpmDistTags()` and `getGcsDistTags()` (wrapped in Suspense) +- Renders sub-sections: `SandboxDoctorSection`, `ValidationErrorsList`, `KeybindingWarnings`, `McpParsingWarnings` +- Displays version comparison: current vs latest npm/GCS versions +- Shows agent info, version lock status, and update channels + +--- + +## screens/REPL.tsx + +**Path:** `src/screens/REPL.tsx` + +**Purpose:** The main interactive REPL screen — the primary conversational UI that orchestrates the entire Claude Code interactive session. + +**Key Logic:** +- Manages the full conversation lifecycle: user input → API query → tool execution → response display +- Handles all interactive features: conversation history, compaction, clear, file editing, permissions +- Routes slash commands to command handlers +- Manages modal dialogs (permissions, MCP approvals, etc.) +- Integrates with all context providers: notifications, overlays, voice, stats +- The largest component in the codebase, responsible for the overall user interaction loop + +--- + +## screens/ResumeConversation.tsx + +**Path:** `src/screens/ResumeConversation.tsx` + +**Purpose:** UI screen for the session resume flow — shows a list of recent sessions with previews and allows the user to select one to resume. + +**Key Logic:** +- Loads session index from disk +- Renders `SessionPreview` components for each session +- Handles keyboard navigation (arrow keys, Enter to select) +- Filters sessions by project root or shows all +- Passes selected session ID back to caller via callback + +--- + +## Cross-Cutting Architecture Notes + +### State Management Architecture + +The codebase uses three distinct state layers: + +1. **`bootstrap/state.ts`** — Global mutable singleton for session-scoped state (costs, model, telemetry). Intentionally a DAG leaf — imports nothing from services. + +2. **React `AppState`** — UI state via Zustand-like store (`state/AppState.ts`). Accessed via `useAppState()` selectors and `useSetAppState()`. + +3. **React Contexts** — Domain-specific contexts (notifications, overlays, stats, voice) for scoped subtree state. + +### Analytics Architecture + +Events flow through a multi-layer pipeline: + +``` +logEvent() → index.ts queue → sink.ts dispatch + → [sampling check] → [isSinkKilled check] + → datadog.ts (strips _PROTO_ keys) + → firstPartyEventLogger.ts (hoists _PROTO_ keys to proto fields) +``` + +All analytics is disabled when: `NODE_ENV === test`, 3P providers (Bedrock/Vertex/Foundry), or `isTelemetryDisabled()`. + +### Compact/Microcompact Architecture + +Context compression has multiple layers: + +1. **`apiMicrocompact.ts`** — API-native server-side editing (cache_edits) — no client-side mutation +2. **`microCompact.ts`** — Client-side tool result clearing (cached path + time-based path) +3. **`autoCompact.ts`** — Full conversation summarization trigger (threshold-based) +4. **`compact.ts`** — Full summarization implementation (LLM call) +5. **`sessionMemoryCompact.ts`** — Lighter session memory segment summarization + +### GrowthBook Feature Flag Naming Convention + +All feature flags use obfuscated/mangled names with `tengu_` prefix to prevent scraping: +- Feature flags: `tengu_prompt_cache_1h_config`, `tengu_session_memory`, `tengu_sm_compact`, etc. +- Kill switches: `tengu_frond_boric` (analytics sink killswitch) +- Voice: `tengu_cobalt_frost` (Nova 3 STT) +- AutoDream: `tengu_onyx_plover` +- Coordinator mode: checked via `feature('COORDINATOR_MODE')` bundle flag diff --git a/spec/07_hooks.md b/spec/07_hooks.md new file mode 100644 index 0000000..6beb33c --- /dev/null +++ b/spec/07_hooks.md @@ -0,0 +1,2136 @@ +# Claude Code — React Hooks + +This document covers every hook in `src/hooks/`, `src/hooks/toolPermission/`, and `src/hooks/notifs/`. For each hook the entry covers: purpose, parameters/props, return value, key logic and side effects, and dependencies. + +--- + +## Table of Contents + +1. [Core / Utility Hooks](#core--utility-hooks) +2. [Input & Text Editing Hooks](#input--text-editing-hooks) +3. [Permission & Tool-Use Hooks](#permission--tool-use-hooks) +4. [Swarm / Teammate Hooks](#swarm--teammate-hooks) +5. [IDE Integration Hooks](#ide-integration-hooks) +6. [Remote & Session Hooks](#remote--session-hooks) +7. [Plugin & Suggestion Hooks](#plugin--suggestion-hooks) +8. [Notification Hooks (`notifs/`)](#notification-hooks-notifs) +9. [Tool Permission Subsystem (`toolPermission/`)](#tool-permission-subsystem-toolpermission) +10. [Non-Hook Utilities in `hooks/`](#non-hook-utilities-in-hooks) + +--- + +## Core / Utility Hooks + +### `useAfterFirstRender` + +**File:** `hooks/useAfterFirstRender.ts` + +**Purpose:** ANT-internal startup-time measurement hook. After the first render it writes startup time to stderr and calls `process.exit(0)` if the `CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER` environment variable is set. + +**Parameters:** none + +**Return Value:** `void` + +**Key Logic:** +- Reads env var `CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER`. +- Uses a `useEffect` on `[]` to fire after first commit. +- Computes elapsed ms from `MACRO.STARTUP_TIMESTAMP`, writes to `process.stderr`, then exits. + +**Dependencies:** `useEffect` (React) + +--- + +### `useApiKeyVerification` + +**File:** `hooks/useApiKeyVerification.ts` + +**Purpose:** Manages the full lifecycle of API key verification — loading, valid, invalid, missing, or error — and exposes a `reverify` callback. Guards against running `apiKeyHelper` scripts before the trust dialog is dismissed to prevent RCE. + +**Parameters:** none + +**Return Value:** `ApiKeyVerificationResult` — `{ status: 'loading'|'valid'|'invalid'|'missing'|'error', reverify: () => void, errorMessage?: string }` + +**Key Logic:** +- Subscribes to `AppState` for `trustDialogAccepted` and `apiKeyVerificationStatus`. +- Uses `useEffect` to run the verification on mount and whenever `reverify` is called. +- Skips the `apiKeyHelper` process before the trust dialog is shown. +- Returns a stable `reverify` callback via `useCallback`. + +**Dependencies:** `useAppState`, `useSetAppState`, `useCallback`, `useEffect` + +--- + +### `useBlink` + +**File:** `hooks/useBlink.ts` + +**Purpose:** Returns a blinking boolean flag synchronized with an animation-frame clock. Pauses when the terminal is blurred or the component is offscreen (OffscreenFreeze). + +**Parameters:** +- `enabled: boolean` — when false, always returns `true` (cursor always visible). +- `intervalMs?: number` — blink period in milliseconds (default: 530). + +**Return Value:** `[ref: RefObject, isVisible: boolean]` + +**Key Logic:** +- Uses `useAnimationFrame` (Ink hook) to read the shared clock counter. +- Divides counter by `intervalMs / frameMs` and toggles on even/odd. +- Returns same ref from Ink's `useOffscreenFreeze` to pause when not in viewport. + +**Dependencies:** `useAnimationFrame` (ink), `useTerminalFocus` (ink), `useRef`, `useMemo` + +--- + +### `useCommandQueue` + +**File:** `hooks/useCommandQueue.ts` + +**Purpose:** Exposes the current unified command queue as a reactive array. Any component can subscribe to observe queued commands without managing external store subscriptions manually. + +**Parameters:** none + +**Return Value:** `readonly QueuedCommand[]` + +**Key Logic:** +- Wraps `useSyncExternalStore` over `messageQueueManager`'s subscribe/getSnapshot pair. +- Re-renders only when the queue reference changes (not on every push that doesn't change length). + +**Dependencies:** `useSyncExternalStore` (React), `messageQueueManager` + +--- + +### `useCopyOnSelect` + +**File:** `hooks/useCopyOnSelect.ts` + +**Purpose:** Automatically copies selected text to the clipboard when the user releases the mouse (mouseup) or double/triple-clicks. Also exports `useSelectionBgColor` for theming selected text. + +**Parameters:** +- `selection: SelectionState` — current ink selection state. +- `isActive: boolean` — only active when true. +- `onCopied?: () => void` — callback fired after clipboard write. + +**Return Value:** `void` + +**Key Logic:** +- Subscribes to ink mouse events via `useEffect`. +- On mouseup: if selection is non-empty and `isActive`, calls `navigator.clipboard.writeText`. +- `useSelectionBgColor()` reads AppState theme to return the correct highlight color. + +**Dependencies:** `useEffect`, `useAppState`, `useCallback` + +--- + +### `useDoublePress` + +**File:** `hooks/useDoublePress.ts` + +**Purpose:** Returns a callback that implements double-press detection within an 800 ms window. Used for Ctrl+C/D to exit and double-Escape to clear input. + +**Parameters:** +- `setPending: (show: boolean) => void` — called with `true` after first press, `false` after timeout. +- `onDoublePress: () => void` — called when the second press occurs within the window. +- `onFirstPress?: () => void` — optional side effect on first press. + +**Return Value:** `() => void` — the wrapped press handler + +**Key Logic:** +- Tracks `lastPressTime` in a ref. +- On call: if elapsed < 800 ms, calls `onDoublePress` and resets; otherwise calls `setPending(true)`, sets a 800 ms timer to call `setPending(false)`, and optionally calls `onFirstPress`. + +**Dependencies:** `useRef`, `useCallback` + +--- + +### `useElapsedTime` + +**File:** `hooks/useElapsedTime.ts` + +**Purpose:** Computes a human-readable elapsed time string (e.g. `"1m 23s"`) that updates while a task is running and freezes once it ends. + +**Parameters:** +- `startTime: number` — Unix timestamp (ms) when timing started. +- `isRunning: boolean` — when false, elapsed is frozen. +- `ms?: number` — update interval in ms (default: 1000). +- `pausedMs?: number` — accumulated paused time to subtract. +- `endTime?: number` — if provided, freezes at this timestamp. + +**Return Value:** `string` — formatted elapsed time like `"5s"`, `"1m 23s"`, `"2h 5m"`. + +**Key Logic:** +- Uses `useSyncExternalStore` over a timer-based external clock. +- Clock updates every `ms` via `setInterval`; each subscriber gets a stable snapshot until it ticks. +- Formats the delta using `formatDuration`. + +**Dependencies:** `useSyncExternalStore`, `useRef` + +--- + +### `useExitOnCtrlCD` + +**File:** `hooks/useExitOnCtrlCD.ts` + +**Purpose:** Implements double-press Ctrl+C / Ctrl+D to exit. Returns pending state so callers can show a "Press again to exit" hint. + +**Parameters:** +- `useKeybindingsHook: (bindings: ...) => void` — injectable hook for binding. +- `onInterrupt?: () => void` — called on first Ctrl+C press. +- `onExit?: () => void` — called on second press. +- `isActive?: boolean` — enables/disables the handler. + +**Return Value:** `ExitState` — `{ pending: boolean, keyName: string | null }` + +**Key Logic:** +- Uses `useDoublePress` internally for both Ctrl+C and Ctrl+D. +- Sets `pending = true` after first press; false after timeout or second press. +- `keyName` tracks which key was pressed ('Ctrl-C' or 'Ctrl-D'). + +**Dependencies:** `useDoublePress`, `useState`, `useCallback` + +--- + +### `useExitOnCtrlCDWithKeybindings` + +**File:** `hooks/useExitOnCtrlCDWithKeybindings.ts` + +**Purpose:** Convenience wrapper that wires `useExitOnCtrlCD` to the keybinding system. + +**Parameters:** +- `onExit?: () => void` +- `onInterrupt?: () => void` +- `isActive?: boolean` + +**Return Value:** `ExitState` + +**Key Logic:** Passes `useKeybindings` as the hook parameter to `useExitOnCtrlCD`. + +**Dependencies:** `useExitOnCtrlCD`, `useKeybindings` + +--- + +### `useMemoryUsage` + +**File:** `hooks/useMemoryUsage.ts` + +**Purpose:** Polls Node.js `process.memoryUsage().heapUsed` every 10 seconds and returns a status when memory usage is high or critical. + +**Parameters:** none + +**Return Value:** `MemoryUsageInfo | null` — `null` for normal; `{ heapUsed: number, status: 'high' | 'critical' }` when heap > 1.5 GB (high) or > 2.5 GB (critical). + +**Key Logic:** +- Uses `useInterval` (usehooks-ts) with 10 000 ms period. +- Thresholds: `HIGH_HEAP_MB = 1536`, `CRITICAL_HEAP_MB = 2560`. +- Returns `null` when below thresholds. + +**Dependencies:** `useInterval`, `useState`, `useEffect` + +--- + +### `useMinDisplayTime` + +**File:** `hooks/useMinDisplayTime.ts` + +**Purpose:** Prevents UI flicker by guaranteeing each distinct value stays visible for at least `minMs` milliseconds before switching. + +**Parameters:** +- `value: T` — the value to display. +- `minMs: number` — minimum display duration. + +**Return Value:** `T` — the "stable" displayed value, may lag behind `value`. + +**Key Logic:** +- Uses `useRef` to track the current stable value and the timestamp it was set. +- On `value` change: if `Date.now() - lastChanged >= minMs`, updates immediately; otherwise schedules a `setTimeout` to update after the remainder. + +**Dependencies:** `useState`, `useRef`, `useEffect` + +--- + +### `useNotifyAfterTimeout` + +**File:** `hooks/useNotifyAfterTimeout.ts` + +**Purpose:** Sends a desktop (OS-level) notification after 6 seconds of user inactivity — used to alert the user when Claude has been working unattended. + +**Parameters:** +- `message: string` — the notification body text. +- `notificationType: string` — identifies the event type for analytics. + +**Return Value:** `void` + +**Key Logic:** +- Waits 6 000 ms after mount using `setTimeout`. +- Checks terminal focus state; only fires if the terminal is not focused. +- Calls `sendDesktopNotification` from a native module. + +**Dependencies:** `useEffect`, `useRef` + +--- + +### `useTimeout` + +**File:** `hooks/useTimeout.ts` + +**Purpose:** Returns a boolean that becomes `true` after `delay` ms. Resets when `resetTrigger` changes. + +**Parameters:** +- `delay: number` — ms to wait. +- `resetTrigger?: number` — changing this value resets the timer. + +**Return Value:** `boolean` — `false` until the delay elapses, then `true`. + +**Key Logic:** Simple `useState` + `useEffect` with `setTimeout`. Cleanup clears the timeout on re-run or unmount. + +**Dependencies:** `useState`, `useEffect` + +--- + +### `useSettings` + +**File:** `hooks/useSettings.ts` + +**Purpose:** Reads the current settings from global AppState. Reactive — re-renders when settings change (e.g. file-watcher triggers). + +**Parameters:** none + +**Return Value:** `ReadonlySettings` + +**Key Logic:** Returns `useAppState(s => s.settings)`. + +**Dependencies:** `useAppState` + +--- + +### `useSettingsChange` + +**File:** `hooks/useSettingsChange.ts` + +**Purpose:** Subscribes to the settings change detector and calls `onChange` with the new settings and the change source whenever the settings file is modified on disk. + +**Parameters:** +- `onChange: (source: string, settings: Settings) => void` + +**Return Value:** `void` + +**Key Logic:** +- Uses `useEffect` to subscribe to `settingsChangeDetector.subscribe(onChange)`. +- Returns the unsubscribe function as the cleanup. + +**Dependencies:** `useEffect`, `settingsChangeDetector` + +--- + +### `useDeferredHookMessages` + +**File:** `hooks/useDeferredHookMessages.ts` + +**Purpose:** Injects `SessionStart` hook messages into the message list asynchronously on mount, avoiding blocking the first render. + +**Parameters:** +- `pendingHookMessages: Message[]` — messages generated by session-start hooks. +- `setMessages: SetMessages` — the message list updater. + +**Return Value:** `() => Promise` — a stable async callback to trigger injection. + +**Key Logic:** +- Defers via `setTimeout(0)` to let the first render complete before injecting hook messages. +- Uses `useRef` to avoid stale closure issues. + +**Dependencies:** `useRef`, `useCallback` + +--- + +### `useDiffData` + +**File:** `hooks/useDiffData.ts` + +**Purpose:** Fetches current git diff statistics and hunks on mount (used by the `/diff` command view). + +**Parameters:** none + +**Return Value:** `DiffData` — `{ stats: DiffStats, files: string[], hunks: DiffHunk[], loading: boolean }` + +**Key Logic:** +- On mount, calls `getGitDiff()` which runs `git diff` in the cwd. +- Sets `loading: true` until the async fetch completes. + +**Dependencies:** `useState`, `useEffect` + +--- + +### `useFileHistorySnapshotInit` + +**File:** `hooks/useFileHistorySnapshotInit.ts` + +**Purpose:** One-time initialization of the file history state from snapshot data stored in the conversation log, restoring file timestamps across `/resume`. + +**Parameters:** +- `initialFileHistorySnapshots: FileHistorySnapshot[]` +- `fileHistoryState: FileHistoryState` +- `onUpdateState: (state: FileHistoryState) => void` + +**Return Value:** `void` + +**Key Logic:** +- Uses `useEffect` with `[]` dep to run only once. +- Merges `initialFileHistorySnapshots` into `fileHistoryState` without overwriting newer entries. + +**Dependencies:** `useEffect` + +--- + +### `useInputBuffer` + +**File:** `hooks/useInputBuffer.ts` + +**Purpose:** Provides a debounced undo buffer for text input, enabling "undo last paste" or "undo last edit" functionality. + +**Parameters:** +- `maxBufferSize: number` — maximum number of entries to keep. +- `debounceMs: number` — how long to wait before committing current value. + +**Return Value:** `UseInputBufferResult` — `{ pushToBuffer, undo, canUndo, clearBuffer }` + +**Key Logic:** +- Maintains a `string[]` undo stack in a ref. +- `pushToBuffer` is debounced: multiple rapid changes collapse into one buffer entry. +- `undo` pops the stack and calls `onChange` with the previous value. + +**Dependencies:** `useRef`, `useCallback`, `useEffect` + +--- + +### `useLogMessages` + +**File:** `hooks/useLogMessages.ts` + +**Purpose:** Incrementally records messages to the conversation transcript file (`.jsonl`) after each render. Avoids re-writing the full transcript on every update. + +**Parameters:** +- `messages: readonly Message[]` — the current message list. +- `ignore?: boolean` — when true, skips recording. + +**Return Value:** `void` + +**Key Logic:** +- Tracks `lastProcessedIndex` in a ref to process only new messages. +- Handles edge cases: compaction (transcript size shrinks), first render, head-pointer rewind. +- Calls `recordTranscript(messages, from, to)` for new messages only. +- Deduplicates compact-summary boundaries. + +**Dependencies:** `useEffect`, `useRef` + +--- + +### `useMainLoopModel` + +**File:** `hooks/useMainLoopModel.ts` + +**Purpose:** Returns the resolved model name for the current session. Re-evaluates when GrowthBook flags are refreshed so model alias resolution stays current mid-session. + +**Parameters:** none + +**Return Value:** `ModelName` + +**Key Logic:** +- Reads `settings.model` from AppState. +- Subscribes to `onGrowthBookRefresh` via `useEffect`; on each refresh, forces a re-render by incrementing a counter state. +- Calls `resolveModelAlias(model)` to translate user-facing aliases (e.g. `opus`) to concrete model IDs. + +**Dependencies:** `useAppState`, `useEffect`, `useState` + +--- + +### `useManagePlugins` + +**File:** `hooks/useManagePlugins.ts` + +**Purpose:** Loads the plugin list on mount and wires up plugin lifecycle management: delisting enforcement, MCP/LSP plugin counting, and refresh-needed notifications. + +**Parameters:** +- `{ enabled?: boolean }` + +**Return Value:** `void` + +**Key Logic:** +- On mount (if enabled): calls `loadPlugins()` and writes results to AppState. +- Enforces delisted plugin removal by reading `delistedPlugins` from settings. +- Counts active MCP and LSP plugins and writes totals to AppState for /doctor diagnostics. +- Does NOT auto-refresh; refresh is triggered explicitly via `/reload-plugins`. + +**Dependencies:** `useEffect`, `useSetAppState`, `useAppState` + +--- + +### `useMergedClients` + +**File:** `hooks/useMergedClients.ts` + +**Purpose:** Deduplicates two MCP client lists (initial from settings + dynamically loaded) by server name. + +**Parameters:** +- `initialClients: MCPServerConnection[]` +- `mcpClients: MCPServerConnection[]` + +**Return Value:** `MCPServerConnection[]` + +**Key Logic:** Uses `lodash.uniqBy([...initialClients, ...mcpClients], 'name')`. The `useMemo` dependency is the combined list length and name set. + +**Dependencies:** `useMemo`, `lodash.uniqBy` + +--- + +### `useMergedCommands` + +**File:** `hooks/useMergedCommands.ts` + +**Purpose:** Deduplicates command lists from initial load and MCP-sourced commands by command name. + +**Parameters:** +- `initialCommands: Command[]` +- `mcpCommands: Command[]` + +**Return Value:** `Command[]` + +**Key Logic:** `useMemo` over `uniqBy([...initialCommands, ...mcpCommands], getCommandName)`. + +**Dependencies:** `useMemo` + +--- + +### `useMergedTools` + +**File:** `hooks/useMergedTools.ts` + +**Purpose:** Assembles the full tool pool for a session by combining built-in tools, MCP tools, and applying permission-context filtering. + +**Parameters:** +- `initialTools: Tool[]` +- `mcpTools: Tool[]` +- `toolPermissionContext: ToolPermissionContext` + +**Return Value:** `Tools` (the assembled tool set) + +**Key Logic:** +- Calls `assembleToolPool(initialTools, mcpTools)` to build the combined list. +- Then calls `mergeAndFilterTools(pool, toolPermissionContext)` to remove disabled tools. + +**Dependencies:** `useMemo` + +--- + +### `useSkillsChange` + +**File:** `hooks/useSkillsChange.ts` + +**Purpose:** Keeps the command list fresh when skill files change on disk or when GrowthBook flags are refreshed. + +**Parameters:** +- `cwd: string | undefined` — the current working directory for scanning skills. +- `onCommandsChange: (commands: Command[]) => void` — callback to update the command list. + +**Return Value:** `void` + +**Key Logic:** +- Subscribes to `skillChangeDetector.subscribe(handleChange)` — fires on skill file writes. +- On file change: calls `clearCommandsCache()` + `getCommands(cwd)` and calls `onCommandsChange`. +- Subscribes to `onGrowthBookRefresh(handleGrowthBookRefresh)` — on GB flag refresh, calls `clearCommandMemoizationCaches()` + `getCommands(cwd)` to re-evaluate feature-gated commands. + +**Dependencies:** `useEffect`, `useCallback` + +--- + +### `useUpdateNotification` + +**File:** `hooks/useUpdateNotification.ts` + +**Purpose:** Returns the new semantic version string when an auto-update has been downloaded, for display in the status bar. Returns `null` if no new version or the version hasn't changed since last notification. + +**Parameters:** +- `updatedVersion: string | null | undefined` — the downloaded version (from auto-updater). +- `initialVersion?: string` — baseline version (default: `MACRO.VERSION`). + +**Return Value:** `string | null` + +**Key Logic:** +- Parses both versions with `semver` to extract `major.minor.patch`. +- Uses `useState` to track the last-notified semver. +- If the new semver differs from `lastNotifiedSemver`, sets state and returns the new value (triggers notification display). Otherwise returns `null`. + +**Dependencies:** `useState`, `semver` + +--- + +## Input & Text Editing Hooks + +### `useTextInput` + +**File:** `hooks/useTextInput.ts` + +**Purpose:** Full readline-style text input handler. Manages cursor position, multiline editing, kill ring (Ctrl+K/U/W), yank (Ctrl+Y / Meta+Y), history navigation (Up/Down arrows), and ghost text rendering. + +**Parameters:** `UseTextInputProps` including: +- `value: string` — current text value (controlled). +- `onChange: (value: string) => void` +- `onSubmit?: (value: string) => void` +- `onExit?: () => void` +- `onHistoryUp / onHistoryDown / onHistoryReset / onClearInput` +- `focus?: boolean` +- `mask?: string` — masks all chars with this string. +- `multiline?: boolean` +- `cursorChar: string` +- `columns: number` — terminal width for wrapping. +- `externalOffset: number` — cursor offset controlled externally. +- `onOffsetChange: (offset: number) => void` +- `inputFilter?: (input: string, key: Key) => string` +- `inlineGhostText?: InlineGhostText` +- `disableCursorMovementForUpDownKeys?: boolean` +- `disableEscapeDoublePress?: boolean` +- `maxVisibleLines?: number` + +**Return Value:** `TextInputState` — `{ onInput, renderedValue, offset, setOffset, cursorLine, cursorColumn, viewportCharOffset, viewportCharEnd }` + +**Key Logic:** +- `Cursor` class from `utils/Cursor.js` manages the text buffer and position arithmetic. +- Maps keypresses to cursor mutations: Ctrl+A (home), Ctrl+E (end), Ctrl+F/B (forward/back), Ctrl+N/P (next/prev line), Meta+F/B (word navigation). +- Kill ring: Ctrl+K (kill to end), Ctrl+U (kill to start), Ctrl+W (kill word). Successive kills append to ring. +- Yank: Ctrl+Y inserts last kill; Meta+Y cycles through the ring. +- Double-press Ctrl+C clears or exits (via `useDoublePress`). +- Double-press Escape clears input with "Esc again to clear" hint. +- SSH-coalesced Enter detection: `text\r` form triggers submit. +- Handles raw `\x7f` DEL characters for SSH/tmux compatibility. +- Inline ghost text rendered at cursor position when `inlineGhostText.insertPosition === offset`. + +**Dependencies:** `useDoublePress`, `useNotifications`, `Cursor` class, `useCallback` + +--- + +### `useVimInput` + +**File:** `hooks/useVimInput.ts` + +**Purpose:** Extends `useTextInput` with a full Vim normal/insert mode state machine, including operators (d, c, y), motions, dot-repeat, find (f/F/t/T), text objects (iw, aw, etc.), and yank register. + +**Parameters:** `UseVimInputProps` — same as `UseTextInputProps` plus: +- `onModeChange?: (mode: VimMode) => void` +- `onUndo?: () => void` + +**Return Value:** `VimInputState` — extends `TextInputState` with `{ mode: VimMode, setMode }` + +**Key Logic:** +- Delegates INSERT mode keypresses to `useTextInput` after running `inputFilter`. +- In NORMAL mode, dispatches keypresses through `transition(state.command, input, ctx)` from `vim/transitions.ts`. +- Manages `vimStateRef` (current mode + pending command accumulator) and `persistentRef` (register, lastFind, lastChange for dot-repeat). +- Escape in INSERT: `switchToNormalMode()` moves cursor left by one. +- Arrow keys in NORMAL: mapped to h/j/k/l motions. +- `?` in NORMAL idle: enters `/` search by writing `?` to the input. +- `setModeExternal` allows callers to programmatically switch modes (used by `/vim` command). + +**Dependencies:** `useTextInput`, `useState`, `useRef`, `useCallback`, vim operators/transitions + +--- + +### `useSearchInput` + +**File:** `hooks/useSearchInput.ts` + +**Purpose:** Full readline-style text input for search boxes (history search, global search). Includes kill ring, yank, and word navigation. + +**Parameters:** `UseSearchInputOptions` — `{ initialValue?, onKeyDown?, placeholder? }` + +**Return Value:** `{ query: string, setQuery, cursorOffset: number, handleKeyDown: (key: Key, input: string) => void }` + +**Key Logic:** +- Implements the same Ctrl key mapping as `useTextInput` but as a standalone reducer without React state for the cursor offset. +- Used in `HistorySearchInput` and `GlobalSearchDialog`. + +**Dependencies:** `useState`, `useCallback`, `useRef`, kill ring utilities + +--- + +### `useArrowKeyHistory` + +**File:** `hooks/useArrowKeyHistory.tsx` + +**Purpose:** Arrow-key navigation through input history with lazy chunked loading, mode-based filtering, and draft preservation. + +**Parameters:** +- `onSetInput: (value: string) => void` +- `currentInput: string` +- `pastedContents: string[]` — paste-detected content to exclude from history matching. +- `setCursorOffset?: (offset: number) => void` +- `currentMode?: PromptInputMode` + +**Return Value:** `{ handleHistoryUp, handleHistoryDown, handleHistoryReset }` + +**Key Logic:** +- Loads history lazily in chunks of 50 from `getHistory()`. +- Navigates using an index pointer; on first Up saves the current draft. +- Filters entries by mode (e.g., bash mode only returns bash history). +- Shows a "Search history: Ctrl+R" hint notification on first use. +- Resets index when `currentInput` changes externally. + +**Dependencies:** `useState`, `useRef`, `useCallback`, `useNotifications` + +--- + +### `useHistorySearch` + +**File:** `hooks/useHistorySearch.ts` + +**Purpose:** Implements `Ctrl+R` backward incremental history search with query matching and keyboard navigation. + +**Parameters:** +- `onSetInput: (value: string) => void` +- `currentInput: string` +- plus keybinding options. + +**Return Value:** `{ historyQuery, setHistoryQuery, historyMatch, historyFailedMatch, handleKeyDown }` + +**Key Logic:** +- Registers `history:search` keybinding (Ctrl+R) to activate search mode. +- In search mode, registers `historySearch:*` bindings (Enter to confirm, Escape to cancel, up/down to cycle matches). +- Filters history entries by substring match against `historyQuery`. +- `historyFailedMatch: boolean` — true when query has text but no match. + +**Dependencies:** `useKeybinding`, `useKeybindings`, `useState`, `useCallback`, `useEffect` + +--- + +### `useTypeahead` + +**File:** `hooks/useTypeahead.tsx` + +**Purpose:** The primary typeahead/autocomplete engine for the prompt input. Handles `@file`, `/command`, `#channel`, and directory suggestions using debounced fuzzy matching, shell completion, and MCP resources. + +**Parameters:** Large props object including: +- `inputValue: string`, `cursorOffset: number` +- `commands: Command[]`, `agents: AgentDefinition[]` +- `mcpResources: MCPResource[]` +- `isLoading: boolean` +- `onSelect: (value: string) => void` +- `onToggleVisible: (show: boolean) => void` + +**Return Value:** `{ suggestions, selectedIndex, handleKeyDown, isSuggesting, suggestionType, ... }` + +**Key Logic:** +- Detects suggestion context from input: `@token` triggers file/resource/agent suggestions; `/` triggers command suggestions; `#channel` triggers Slack channel suggestions (if Slack MCP present). +- File suggestions use `generateUnifiedSuggestions` (nucleo + Fuse.js ranked). +- Command suggestions use `generateCommandSuggestions` with argument hint generation. +- Shell completions use `getShellCompletions` for bash/zsh completions. +- Path completions use `getPathCompletions` / `getDirectoryCompletions`. +- Registers as an overlay via `useRegisterOverlay` so escape/enter/arrow keys are captured. +- Uses `useDebounceCallback` (usehooks-ts) to rate-limit file lookups. +- Tracks keyboard navigation state (`selectedIndex`) internally. +- Session resume suggestions for `/resume` queries via `searchSessionsByCustomTitle`. + +**Dependencies:** `useInput` (ink, backward-compat bridge), `useRegisterOverlay`, `useKeybindings`, `useDebounceCallback`, `useState`, `useRef`, `useMemo`, `useCallback`, `useEffect`, `generateUnifiedSuggestions`, `generateCommandSuggestions`, `getShellCompletions` + +--- + +### `usePasteHandler` + +**File:** `hooks/usePasteHandler.ts` + +**Purpose:** Handles bracketed paste mode detection, large paste chunking, image file path detection, and macOS clipboard image fallback. + +**Parameters:** +- `{ onPaste: (text: string) => void, onInput: (text: string, key: Key) => void, onImagePaste?: (base64: string, ...) => void }` + +**Return Value:** `{ wrappedOnInput, pasteState, isPasting }` + +**Key Logic:** +- Detects bracketed paste via `\x1b[?2004h` / `\x1b[200~` / `\x1b[201~` escape sequences. +- Splits large pastes (>1000 chars) into multiple `onPaste` calls to avoid blocking the event loop. +- Detects image file paths in paste content (extensions `.png`, `.jpg`, `.gif`, etc.) and triggers `onImagePaste`. +- On macOS: falls back to `pbpaste` for image clipboard content. + +**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect` + +--- + +### `useVoice` + +**File:** `hooks/useVoice.ts` + +**Purpose:** Hold-to-talk voice recording using the `voice_stream` STT endpoint. Auto-repeat key events extend the recording; releasing the key after `RELEASE_TIMEOUT_MS` stops it. + +**Parameters:** `{ onTranscript: (text: string) => void, enabled: boolean }` + +**Return Value:** `{ state: 'idle'|'recording'|'processing', handleKeyEvent: (fallbackMs?: number) => void }` + +**Key Logic:** +- Calls `connectVoiceStream()` to open a WebSocket to `voice_stream` STT. +- Maps user locale to BCP-47 language codes for Deepgram (20+ languages mapped). +- Auto-repeat detection: key events arriving within 120 ms are considered "held". +- Modifier combos (Ctrl+Space etc.) use 2 000 ms `FIRST_PRESS_FALLBACK_MS`. +- Requires 5 rapid keydowns (HOLD_THRESHOLD) for bare-char bindings to activate, 2 for warmup feedback. +- Uses `useTerminalFocus` to pause recording when terminal loses focus. +- Fetches voice keyterms for improved domain recognition. + +**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect`, `useTerminalFocus`, `connectVoiceStream`, `getVoiceKeyterms` + +--- + +### `useVoiceEnabled` + +**File:** `hooks/useVoiceEnabled.ts` + +**Purpose:** Combines user intent (`settings.voiceEnabled`), OAuth auth check, and GrowthBook kill-switch into a single boolean indicating whether voice mode is available. + +**Parameters:** none + +**Return Value:** `boolean` + +**Key Logic:** +- `userIntent` from `useAppState(s => s.settings.voiceEnabled === true)`. +- `authed` memoized on `authVersion` — avoids expensive `hasVoiceAuth()` call on every render. +- `isVoiceGrowthBookEnabled()` not memoized (cheap cached lookup, so mid-session kill-switch takes effect). + +**Dependencies:** `useAppState`, `useMemo` + +--- + +### `useVoiceIntegration` + +**File:** `hooks/useVoiceIntegration.tsx` + +**Purpose:** Orchestrates the full voice-mode integration: reading keybindings, detecting held keys, activating `useVoice`, suppressing full-width space input during recording, and showing status notifications. + +**Parameters:** +- `{ onTranscript: (text: string) => void, isModalOverlayActive: boolean }` + +**Return Value:** `{ voiceState: VoiceState, handleVoiceKeyEvent }` + +**Key Logic:** +- Reads `voice:activate` keybinding from keybinding context (default: spacebar). +- Detects held key by counting rapid key events (HOLD_THRESHOLD=5 for bare chars, 1 for modifier combos). +- Shows warmup notification after WARMUP_THRESHOLD=2 events. +- Uses `useInput` (ink) as a backward-compat bridge until REPL wires `handleKeyDown` to ``. +- Guards on `useIsModalOverlayActive()` — does not activate voice while a modal is open. +- Dead-code elimination: conditionally requires `useVoice` only if `feature('VOICE_MODE')` is set; otherwise uses a no-op stub. +- Calls `normalizeFullWidthSpace` to handle full-width space (Japanese IME) by passing it to the transcript instead of activating voice. + +**Dependencies:** `useVoice`, `useVoiceEnabled`, `useInput` (ink), `useOptionalKeybindingContext`, `useIsModalOverlayActive`, `useNotifications`, `useState`, `useRef`, `useMemo`, `useCallback`, `useEffect` + +--- + +### `useVirtualScroll` + +**File:** `hooks/useVirtualScroll.ts` + +**Purpose:** React-level virtualization for `MessageRow` items inside a `ScrollBox`. Mounts only items in the viewport plus overscan, using spacer boxes to maintain scroll height. + +**Parameters:** +- `scrollRef: RefObject` — reference to the ScrollBox. +- `itemKeys: readonly string[]` — stable keys for each item. +- `columns: number` — terminal width; triggers height cache rescaling on change. + +**Return Value:** `VirtualScrollResult`: +- `range: [startIndex, endIndex)` — half-open slice to render. +- `topSpacer: number` — rows before the first rendered item. +- `bottomSpacer: number` — rows after last rendered item. +- `measureRef: (key) => ref` — attach to each item root `Box` for height measurement. +- `spacerRef: RefObject` — attach to top spacer for drift-free origin tracking. +- `offsets: ArrayLike` — cumulative y-offsets per item. +- `getItemTop: (index) => number` — reads live Yoga `computedTop`. +- `getItemElement: (index) => DOMElement | null` +- `getItemHeight: (index) => number | undefined` +- `scrollToIndex: (i) => void` + +**Key Logic:** +- `DEFAULT_ESTIMATE = 3` rows for unmeasured items; `OVERSCAN_ROWS = 80`; `COLD_START_COUNT = 30`. +- `SCROLL_QUANTUM = 40` rows — scrollTop is quantized so React re-renders only when the mounted range needs to shift, not on every wheel tick. +- `SLIDE_STEP = 25` — caps new mounts per commit to bound reconcile time. +- `PESSIMISTIC_HEIGHT = 1` for coverage back-walk (guarantees viewport coverage). +- `MAX_MOUNTED_ITEMS = 300` cap. +- On column change: scales all cached heights by `oldCols/newCols` instead of clearing. +- Uses `useSyncExternalStore` over the ScrollBox's scroll-top external store. +- Uses `useLayoutEffect` to measure Yoga heights after each commit. +- Sticky-scroll: when pinned to the bottom, always renders the last N items. + +**Dependencies:** `useRef`, `useMemo`, `useDeferredValue`, `useLayoutEffect`, `useSyncExternalStore`, `ScrollBox` handle + +--- + +## Permission & Tool-Use Hooks + +### `useCanUseTool` + +**File:** `hooks/useCanUseTool.tsx` + +**Purpose:** Core permission gate for tool execution. Called for every tool use attempt; routes through the appropriate handler (coordinator, interactive, swarm-worker) and resolves to a `PermissionDecision`. + +**Parameters:** +- `setToolUseConfirmQueue: SetState` +- `setToolPermissionContext: (ctx: ToolPermissionContext) => void` + +**Return Value:** `CanUseToolFn` — `async (tool, input, toolUseContext, assistantMessage, toolUseID) => PermissionDecision` + +**Key Logic:** +1. Calls `hasPermissionsToUseTool` to get the initial decision (`allow`, `deny`, or `ask`). +2. If `allow` or `deny`: logs and returns immediately. +3. If `ask`: + a. If swarm worker: delegates to `handleSwarmWorkerPermission` (forwards to leader via mailbox). + b. If coordinator worker: delegates to `handleCoordinatorPermission` (awaits hooks + classifier, then falls through). + c. Otherwise: delegates to `handleInteractivePermission` (shows dialog, races hooks/classifier/bridge/channel). +4. Creates a `PermissionContext` object with the full set of callbacks (logDecision, persistPermissions, tryClassifier, runHooks, etc.). + +**Dependencies:** `useCallback`, `useAppState`, `useSetAppState`, `createPermissionContext`, `createPermissionQueueOps`, `handleCoordinatorPermission`, `handleInteractivePermission`, `handleSwarmWorkerPermission` + +--- + +### `CancelRequestHandler` (exported as `useCancelRequest` module) + +**File:** `hooks/useCancelRequest.ts` + +**Purpose:** React component (renders `null`) that registers three keybinding handlers for cancellation: +1. `chat:cancel` (Escape) — cancels running task or pops queued command. +2. `app:interrupt` (Ctrl+C) — cancels running task; in teammate view, also kills all agents and exits. +3. `chat:killAgents` (Ctrl+X Ctrl+K) — two-press pattern to stop all background agents. + +**Parameters:** `CancelRequestHandlerProps`: +- `setToolUseConfirmQueue`, `onCancel`, `onAgentsKilled` +- `isMessageSelectorVisible`, `screen` +- `abortSignal?: AbortSignal` +- `popCommandFromQueue?`, `vimMode`, `isLocalJSXCommand`, `isSearchingHistory`, `isHelpOpen` +- `inputMode?, inputValue?, streamMode?` + +**Return Value:** `null` + +**Key Logic:** +- `handleCancel`: Priority 1 — abort signal if task running. Priority 2 — pop command if queue non-empty. Fallback — call `onCancel`. +- `handleInterrupt`: if in teammate view, kills all agents + exits. Then calls `handleCancel`. +- `handleKillAgents`: first press shows "Press again" hint; second press within 3 000 ms (`KILL_AGENTS_CONFIRM_WINDOW_MS`) kills all `local_agent` tasks, emits SDK events, enqueues aggregate notification. +- `isEscapeActive` / `isCtrlCActive` guards: skip if overlay, vim INSERT, transcript, history-search, help, etc. +- `chat:killAgents` always registered to prevent Ctrl+K (chord prefix) passing to readline. + +**Dependencies:** `useKeybinding`, `useAppState`, `useSetAppState`, `useCommandQueue`, `useNotifications`, `useIsOverlayActive`, `useCallback`, `useRef`, `killAllRunningAgentTasks`, `emitTaskTerminatedSdk` + +--- + +### `useSwarmPermissionPoller` + +**File:** `hooks/useSwarmPermissionPoller.ts` + +**Purpose:** Polls every 500 ms for permission responses from the swarm leader when running as a worker agent. When a response arrives, it invokes the registered callback (`onAllow` or `onReject`). + +**Parameters:** none + +**Return Value:** `void` + +**Key Logic:** +- Only active when `isSwarmWorker()` returns `true`. +- Uses `useInterval` (usehooks-ts) with `POLL_INTERVAL_MS = 500`. +- For each `requestId` in `pendingCallbacks`, calls `pollForResponse(requestId, agentName, teamName)`. +- On response: calls `processResponse(response)` which invokes the callback, then calls `removeWorkerResponse`. +- Module-level `pendingCallbacks: Map` and `pendingSandboxCallbacks: Map<...>`. +- Exported helper functions: `registerPermissionCallback`, `unregisterPermissionCallback`, `hasPermissionCallback`, `clearAllPendingCallbacks`, `processMailboxPermissionResponse`, `registerSandboxPermissionCallback`, `hasSandboxPermissionCallback`, `processSandboxPermissionResponse`. + +**Dependencies:** `useCallback`, `useEffect`, `useRef`, `useInterval`, `permissionSync` + +--- + +## Swarm / Teammate Hooks + +### `useSwarmInitialization` + +**File:** `hooks/useSwarmInitialization.ts` + +**Purpose:** Initializes swarm features (teammate context and hooks) on mount. Handles both resumed sessions (teamName/agentName in transcript) and fresh spawns (environment variables). + +**Parameters:** +- `setAppState: SetAppState` +- `initialMessages: Message[] | undefined` +- `{ enabled?: boolean }` + +**Return Value:** `void` + +**Key Logic:** +- Checks `isAgentSwarmsEnabled()` before doing anything. +- Resumed session path: reads `teamName`/`agentName` from `initialMessages[0]`, calls `initializeTeammateContextFromSession`, reads team file to get `agentId`, calls `initializeTeammateHooks`. +- Fresh spawn path: calls `getDynamicTeamContext()` to read env vars, then calls `initializeTeammateHooks`. + +**Dependencies:** `useEffect` + +--- + +### `useTeammateViewAutoExit` + +**File:** `hooks/useTeammateViewAutoExit.ts` + +**Purpose:** Auto-exits teammate viewing mode when the viewed teammate is killed, fails, encounters an error, or is evicted from the task map. + +**Parameters:** none + +**Return Value:** `void` + +**Key Logic:** +- Selects only `viewingAgentTaskId` and the viewed task from AppState (avoids re-rendering on unrelated streaming updates). +- Narrows the task to `InProcessTeammateTask` type. +- Exits if task evicted, status is `killed`, `failed`, or error is present. +- Does NOT exit if status is `running`, `completed`, or `pending`. + +**Dependencies:** `useEffect`, `useAppState`, `useSetAppState`, `exitTeammateView` + +--- + +### `useBackgroundTaskNavigation` + +**File:** `hooks/useBackgroundTaskNavigation.ts` + +**Purpose:** Manages keyboard navigation of the background task list. Shift+Up/Down moves selection; Enter enters the view; `f` opens the full transcript; `k` kills the task; Escape exits. + +**Parameters:** `options?: { isActive?: boolean }` + +**Return Value:** `{ handleKeyDown: (key: Key, input: string) => void }` + +**Key Logic:** +- Reads the list of background tasks from AppState. +- `selectedIndex` is clamped to `[0, tasks.length - 1]` whenever the list changes. +- Enter: sets `viewingAgentTaskId` in AppState to the selected task's ID. +- `f`: sets `showFullTranscript = true` in AppState. +- `k`: calls `killTask(task.id)`. +- Escape: calls `exitTeammateView(setAppState)`. + +**Dependencies:** `useAppState`, `useSetAppState`, `useState`, `useEffect`, `useCallback` + +--- + +### `useInboxPoller` + +**File:** `hooks/useInboxPoller.ts` + +**Purpose:** Polls the team lead's inbox every 1 second (or on demand when idle) and routes messages. Handles permission requests/responses, sandbox permissions, plan approvals, shutdown handling, team permission updates, mode-set requests, and regular messages. + +**Parameters:** +- `{ enabled: boolean, isLoading: boolean, focusedInputDialog: string | null, onSubmitMessage: (msg) => void }` + +**Return Value:** `void` + +**Key Logic:** +- Uses `useInterval` with 1 000 ms period; no-ops if `!enabled`. +- Reads messages from the team lead's mailbox directory. +- Dispatches by message type: + - `permission_request` → adds to `toolUseConfirmQueue`. + - `permission_response` → calls `processMailboxPermissionResponse`. + - `sandbox_permission_request` → calls sandbox permission handler. + - `sandbox_permission_response` → calls `processSandboxPermissionResponse`. + - `plan_approval` → routes to plan approval handler. + - `shutdown` → gracefully exits. + - `team_permission_update` → updates AppState permission context. + - `mode_set` → changes permission mode. + - Regular messages → calls `onSubmitMessage` when idle. +- Delivers pending messages only when `!isLoading` and no focused input dialog. + +**Dependencies:** `useInterval`, `useEffect`, `useRef`, `useAppState`, `useSetAppState`, `processMailboxPermissionResponse`, `processSandboxPermissionResponse` + +--- + +### `useTaskListWatcher` + +**File:** `hooks/useTaskListWatcher.ts` + +**Purpose:** Watches a task list directory and automatically picks up open, unowned tasks to work on (tasks mode). Claims tasks atomically to prevent race conditions. + +**Parameters:** +- `{ taskListId?: string, isLoading: boolean, onSubmitTask: (prompt: string) => boolean }` + +**Return Value:** `void` + +**Key Logic:** +- Calls `ensureTasksDir` on mount, then `watch(tasksDir, debouncedCheck)` with DEBOUNCE_MS=1000. +- Uses stable refs for `isLoading` and `onSubmitTask` to avoid Bun PathWatcherManager deadlock (oven-sh/bun#27469) by not recreating the watcher on every turn. +- `checkForTasks`: lists tasks, finds `status=pending`, `owner=undefined`, all `blockedBy` completed; calls `claimTask(taskListId, task.id, agentId)`. +- Formats task as `"Complete all open tasks. Start with task #N: ...\n\nDescription"`. +- Additional `useEffect` on `isLoading` to trigger check when going idle. + +**Dependencies:** `fs.watch`, `useEffect`, `useRef` + +--- + +### `useTasksV2` + +**File:** `hooks/useTasksV2.ts` + +**Purpose:** Exposes the current task list for the persistent TodoV2 UI. All consumers share a single `TasksV2Store` (singleton file-watcher) to avoid watcher churn. + +**Parameters:** none + +**Return Value:** `Task[] | undefined` — `undefined` when hidden (all completed for >5 s, or empty). + +**Key Logic:** +- `TasksV2Store` class: manages `fs.watch`, `onTasksUpdated` subscription, debounced fetch (DEBOUNCE_MS=50), hide timer (HIDE_DELAY_MS=5000), fallback poll (FALLBACK_POLL_MS=5000). +- `getSnapshot` returns `undefined` when `#hidden = true`. +- `useSyncExternalStore` subscription; store starts on first subscriber, stops on last unsubscribe. +- Only active when `isTodoV2Enabled()` and (no team context, or is team lead). +- `useTasksV2WithCollapseEffect`: same as `useTasksV2` plus collapses the expanded task view in AppState when the list becomes hidden. + +**Dependencies:** `useSyncExternalStore`, `useEffect`, `useAppState`, `useSetAppState`, `fs.watch` + +--- + +### `useSessionBackgrounding` + +**File:** `hooks/useSessionBackgrounding.ts` + +**Purpose:** Manages Ctrl+B backgrounding and foregrounding of the current session. When a task is foregrounded, it syncs that task's messages to the main message list. + +**Parameters:** (large props including setMessages, setIsLoading, tools, etc.) + +**Return Value:** `{ handleBackgroundSession: () => void }` + +**Key Logic:** +- On `handleBackgroundSession`: if a task is running, backgrounds it (writes to AppState background tasks); otherwise foregrounds the first background task. +- Foreground: injects the backgrounded task's messages into the main list via `setMessages`, resumes the task's streams. + +**Dependencies:** `useCallback`, `useAppState`, `useSetAppState` + +--- + +### `useScheduledTasks` + +**File:** `hooks/useScheduledTasks.ts` + +**Purpose:** Mounts the cron scheduler in the REPL. Fired tasks are enqueued via `enqueuePendingNotification` at `later` priority; teammate-scoped crons are injected directly into that teammate's message stream. + +**Parameters:** +- `{ isLoading: boolean, assistantMode?: boolean, setMessages: Dispatch> }` + +**Return Value:** `void` + +**Key Logic:** +- Gated on `isKairosCronEnabled()` at effect time. +- Uses `isLoadingRef` to avoid stale closures on `isLoading`. +- `onFireTask` callback: if task has `agentId`, finds the teammate and calls `injectUserMessageToTeammate`; otherwise creates a `ScheduledTaskFireMessage` and enqueues the prompt. +- `createCronScheduler` is the shared scheduler core (used also by `print.ts` for headless mode). +- `isKilled` callback polls `isKairosCronEnabled()` each tick as a mid-session killswitch. + +**Dependencies:** `useEffect`, `useRef`, `useAppStateStore`, `useSetAppState`, `createCronScheduler` + +--- + +## IDE Integration Hooks + +### `useIDEIntegration` + +**File:** `hooks/useIDEIntegration.tsx` + +**Purpose:** Manages IDE auto-connection on startup. Detects running IDEs, sets up dynamic MCP config for the found IDE server, and shows the IDE onboarding dialog if needed. + +**Parameters:** +- `{ autoConnectIdeFlag, ideToInstallExtension, setDynamicMcpConfig, setShowIdeOnboarding, setIDEInstallationState }` + +**Return Value:** `void` + +**Key Logic:** +- Calls `detectIDEs()` on mount to find running IDE extension servers. +- If found: calls `setDynamicMcpConfig` to add the IDE's MCP server config. +- Checks `settings.ideHintShownCount` to decide whether to show onboarding. +- Handles the `ideToInstallExtension` CLI flag for direct IDE extension install flows. + +**Dependencies:** `useEffect`, `useRef`, `useAppState` + +--- + +### `useIdeAtMentioned` + +**File:** `hooks/useIdeAtMentioned.ts` + +**Purpose:** Listens for `at_mentioned` MCP notifications from the IDE extension and calls a callback with file/line context so Claude can reference the file. + +**Parameters:** +- `mcpClients: MCPServerConnection[]` +- `onAtMentioned: (filePath: string, lineNumber?: number) => void` + +**Return Value:** `void` + +**Key Logic:** +- Uses `getConnectedIdeClient(mcpClients)` to find the IDE client. +- Registers a notification handler for the `at_mentioned` method via `ideClient.client.setNotificationHandler`. +- Passes parsed `filePath` and `lineNumber` to `onAtMentioned`. + +**Dependencies:** `useEffect` + +--- + +### `useIdeConnectionStatus` + +**File:** `hooks/useIdeConnectionStatus.ts` + +**Purpose:** Returns the current IDE connection status (`connected`, `disconnected`, `pending`, or `null`) and the IDE name. + +**Parameters:** +- `mcpClients?: MCPServerConnection[]` + +**Return Value:** `{ status: IDEConnectionStatus | null, ideName: string | null }` + +**Key Logic:** +- Uses `useMemo` over `mcpClients` to find the IDE client by checking `isIdeClient(client)`. +- Maps client connection state to the status enum. + +**Dependencies:** `useMemo` + +--- + +### `useIdeLogging` + +**File:** `hooks/useIdeLogging.ts` + +**Purpose:** Registers a `log_event` MCP notification handler on the IDE client to forward IDE telemetry events to the analytics system. + +**Parameters:** +- `mcpClients: MCPServerConnection[]` + +**Return Value:** `void` + +**Key Logic:** +- Calls `getConnectedIdeClient` to find the IDE client. +- Registers a Zod-validated handler: `{ method: 'log_event', params: { eventName, eventData } }`. +- Calls `logEvent('tengu_ide_${eventName}', eventData)`. + +**Dependencies:** `useEffect`, `zod` + +--- + +### `useIdeSelection` + +**File:** `hooks/useIdeSelection.ts` + +**Purpose:** Listens for `selection_changed` MCP notifications from the IDE and delivers them as `IDESelection` objects to the REPL. + +**Parameters:** +- `mcpClients: MCPServerConnection[]` +- `onSelect: (selection: IDESelection) => void` + +**Return Value:** `void` + +**Key Logic:** +- Finds IDE client, registers notification handler for `selection_changed`. +- Converts the raw notification payload to `IDESelection` format `{ filePath, text, lineStart, lineEnd, lineCount }`. + +**Dependencies:** `useEffect` + +--- + +### `useDiffInIDE` + +**File:** `hooks/useDiffInIDE.ts` + +**Purpose:** Opens a file diff in the connected IDE via MCP RPC. Handles user save/close/reject responses to finalize or revert the edit. + +**Parameters:** +- `{ onChange, toolUseContext, filePath, edits, editMode }` + +**Return Value:** `{ closeTabInIDE, showingDiffInIDE, ideName, hasError }` + +**Key Logic:** +- Calls `ideClient.client.request('show_diff', { filePath, edits, ... })` to open the diff in the IDE. +- Waits for the IDE to respond with `saved`, `closed`, or `rejected`. +- On `saved`: calls `onChange` to apply the edit. +- On `rejected`: calls the abort controller. +- Returns `closeTabInIDE()` so the permission dialog can close the tab programmatically. + +**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect` + +--- + +## Remote & Session Hooks + +### `useDirectConnect` + +**File:** `hooks/useDirectConnect.ts` + +**Purpose:** Manages a WebSocket connection to a DirectConnect server (local server mode). Routes inbound messages and permission requests between the server and the REPL. + +**Parameters:** +- `{ config, setMessages, setIsLoading, setToolUseConfirmQueue, tools }` + +**Return Value:** `UseDirectConnectResult` — `{ isConnected, send }` + +**Key Logic:** +- Uses `directConnectManager` to manage the WebSocket lifecycle. +- Translates inbound SDK messages to `Message[]` format. +- Handles tool permission requests by pushing to `setToolUseConfirmQueue`. +- Reconnects on disconnect with exponential backoff. + +**Dependencies:** `useEffect`, `useRef`, `useState` + +--- + +### `useSSHSession` + +**File:** `hooks/useSSHSession.ts` + +**Purpose:** Wires an SSH session manager to the REPL. Handles reconnection, graceful shutdown on disconnect, and transcript message injection. + +**Parameters:** +- `{ session, setMessages, setIsLoading, setToolUseConfirmQueue, tools }` + +**Return Value:** `UseSSHSessionResult` — `{ isConnected, disconnect }` + +**Key Logic:** +- Subscribes to SSH session events: `message`, `connect`, `disconnect`, `error`. +- On disconnect: injects a system message explaining the disconnect and showing reconnect options. +- On reconnect: injects a system message confirming reconnection. +- Handles graceful shutdown: drains pending messages before closing. + +**Dependencies:** `useEffect`, `useRef`, `useState` + +--- + +### `useRemoteSession` + +**File:** `hooks/useRemoteSession.ts` + +**Purpose:** Full CCR (Claude Code Remote) WebSocket session management. Handles bidirectional message conversion, streaming tool uses, permission request/response flow, response timeout detection, session title updates, and subagent task counting. + +**Parameters:** Large props object including: +- `config: AppConfig` +- `setMessages: SetMessages` +- `setIsLoading`, `setToolUseConfirmQueue` +- `tools: Tool[]` +- `onSessionTitleUpdate?: (title: string) => void` + +**Return Value:** `UseRemoteSessionResult` — `{ isConnected, sendMessage, sessionId, ... }` + +**Key Logic:** +- Connects to CCR WebSocket on mount, reconnects on disconnect. +- Converts `SDKMessage` types to internal `Message` format on inbound. +- Converts outbound messages to SDK format for CCR consumption. +- Manages streaming tool uses: accumulates `input_json_delta` chunks, fires permission dialog on `tool_use` completion. +- Response timeout: sets a flag after 30 s of no response from the model. +- Session title: subscribes to `session_title_update` events and calls `onSessionTitleUpdate`. +- Subagent task counting: tracks `subagent_start` / `subagent_end` events to count running agents. + +**Dependencies:** `useEffect`, `useState`, `useRef`, `useCallback`, `SessionsWebSocket` + +--- + +### `useAssistantHistory` + +**File:** `hooks/useAssistantHistory.ts` + +**Purpose:** Lazy-loads older messages from a remote session's history as the user scrolls up in viewer-only mode. + +**Parameters:** +- `{ config, setMessages, scrollRef, onPrepend }` + +**Return Value:** `{ maybeLoadOlder: () => Promise }` + +**Key Logic:** +- On scroll-up event, calls `loadSessionHistory(sessionId, pageToken)`. +- Prepends loaded messages to the message list. +- Chains viewport fill: if the loaded messages don't fill the viewport, immediately loads another page. +- Scroll anchoring: saves the current scroll position before prepending and restores it after. +- Shows a sentinel message at the top ("Beginning of conversation") once all pages are exhausted. + +**Dependencies:** `useCallback`, `useRef`, `useEffect` + +--- + +### `useMailboxBridge` + +**File:** `hooks/useMailboxBridge.ts` + +**Purpose:** Bridges the mailbox message context to the REPL's submit function. Polls the mailbox on revision change when idle. + +**Parameters:** +- `{ isLoading: boolean, onSubmitMessage: (msg) => void }` + +**Return Value:** `void` + +**Key Logic:** +- Subscribes to `mailboxRevision` changes in AppState. +- When revision bumps and `!isLoading`: calls `pollMailbox()` and submits any pending messages via `onSubmitMessage`. + +**Dependencies:** `useEffect`, `useRef`, `useAppState` + +--- + +### `useReplBridge` + +**File:** `hooks/useReplBridge.tsx` + +**Purpose:** Full REPL bridge session management — the main hook wiring the REPL to the Claude API. Manages the full query execution loop, permission flow, streaming message assembly, compact operations, and bridge connectivity. + +**Parameters:** Large props including tools, messages, setMessages, config, and many callbacks. + +**Return Value:** Large result including `{ onQuery, isLoading, abortController, toolUseConfirmQueue, ... }` + +**Key Logic:** (file is >75k tokens; key points from reading the first 80 lines and the summary) +- Manages `abortController` lifecycle — creates a new one per query, aborts on cancel. +- Calls the streaming Claude API via `query.ts`. +- Assembles streaming `AssistantMessage` from delta events. +- Routes `tool_use` to `canUseTool` for permission gating. +- Handles compact boundary detection and auto-compact triggers. +- Integrates bridge callbacks for CCR permission relaying. +- Manages local session history recording. + +**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect`, `useMemo`, `useCanUseTool`, `useLogMessages`, `query`, `compact` + +--- + +### `useTeleportResume` + +**File:** `hooks/useTeleportResume.tsx` + +**Purpose:** Manages the async lifecycle of teleporting into a remote Code Session: loading state, error state, selected session tracking, and the `resumeSession` callback. + +**Parameters:** +- `source: TeleportSource` — `'cliArg' | 'localCommand'` (for analytics). + +**Return Value:** `{ resumeSession, isResuming, error, selectedSession, clearError }` + +**Key Logic:** +- `resumeSession(session)`: sets `isResuming = true`, logs `tengu_teleport_resume_session`, calls `teleportResumeCodeSession(session.id)`, sets `teleportedSessionInfo` for reliability logging. +- On error: wraps in `TeleportResumeError` with `isOperationError` flag for UI differentiation. +- Uses React Compiler (`_c`) memoization. + +**Dependencies:** `useState`, `useCallback`, `teleportResumeCodeSession`, `setTeleportedSessionInfo` + +--- + +## Plugin & Suggestion Hooks + +### `usePromptSuggestion` + +**File:** `hooks/usePromptSuggestion.ts` + +**Purpose:** Manages AI prompt completion suggestions: fetches a suggestion for the current input, tracks accept/ignore/submit outcomes, and logs telemetry. + +**Parameters:** +- `{ inputValue: string, isAssistantResponding: boolean }` + +**Return Value:** `{ suggestion: string | null, markAccepted, markShown, logOutcomeAtSubmission }` + +**Key Logic:** +- Calls `generatePromptSuggestion(inputValue)` debounced after 300 ms of no typing. +- `markAccepted()`: records that the user pressed Tab to accept. +- `markShown()`: records when the ghost-text suggestion becomes visible. +- `logOutcomeAtSubmission()`: called on submit; logs `accept` (Tab was pressed), `ignore` (shown but not accepted), or `no_suggestion` outcome. + +**Dependencies:** `useState`, `useRef`, `useCallback`, `useEffect`, `generatePromptSuggestion` + +--- + +### `usePromptsFromClaudeInChrome` + +**File:** `hooks/usePromptsFromClaudeInChrome.tsx` + +**Purpose:** Listens for prompts sent from the Claude in Chrome extension via MCP notifications. Also syncs the current permission mode to the extension. + +**Parameters:** +- `mcpClients: MCPServerConnection[]` +- `toolPermissionMode: PermissionMode` + +**Return Value:** `void` + +**Key Logic:** +- Finds the Chrome extension MCP client. +- Registers a notification handler for `prompt_from_chrome`. +- Submits received prompts to the command queue. +- On `toolPermissionMode` change: sends a `mode_changed` notification back to the extension. + +**Dependencies:** `useEffect`, `useRef` + +--- + +### `usePrStatus` + +**File:** `hooks/usePrStatus.ts` + +**Purpose:** Polls `gh pr status` every 60 seconds to detect review state changes on the current branch's PR. + +**Parameters:** +- `isLoading: boolean` +- `enabled?: boolean` + +**Return Value:** `PrStatusState` — `{ reviewStatus: 'approved'|'changes_requested'|'pending'|null, prUrl: string | null }` + +**Key Logic:** +- Uses `useInterval` with 60 000 ms period; skips poll when `isLoading`. +- Stops polling after 60 minutes of idle time (no new turns). +- Permanently disables if a fetch takes >4 seconds (likely no `gh` binary or no PR). +- Runs `gh pr status --json reviewDecision,url` in a subprocess. + +**Dependencies:** `useInterval`, `useState`, `useRef` + +--- + +### `useClaudeCodeHintRecommendation` + +**File:** `hooks/useClaudeCodeHintRecommendation.tsx` + +**Purpose:** Surfaces plugin install prompts from `` tags parsed from Claude's responses. Show-once semantics per plugin per session. + +**Parameters:** none + +**Return Value:** `{ recommendation: PluginRecommendation | null, handleResponse: (accepted: boolean) => void }` + +**Key Logic:** +- Monitors `messages` for assistant messages containing `` XML. +- Uses `usePluginRecommendationBase` state machine to gate display. +- `handleResponse(true)`: installs the plugin; `false`: dismisses. + +**Dependencies:** `usePluginRecommendationBase`, `useAppState`, `useEffect` + +--- + +### `useLspPluginRecommendation` + +**File:** `hooks/useLspPluginRecommendation.tsx` + +**Purpose:** Recommends an LSP plugin when the user edits a file whose extension matches a supported language and the LSP binary is present. + +**Parameters:** none + +**Return Value:** `{ recommendation: PluginRecommendation | null, handleResponse: (accepted: boolean) => void }` + +**Key Logic:** +- Watches `messages` for `FileEdit` / `FileWrite` tool result messages. +- Extracts the file extension, checks if there's a matching LSP plugin. +- Uses `usePluginRecommendationBase` to avoid showing while another recommendation is active. +- Show-once per session (tracked in AppState). + +**Dependencies:** `usePluginRecommendationBase`, `useAppState`, `useEffect` + +--- + +### `usePluginRecommendationBase` + +**File:** `hooks/usePluginRecommendationBase.tsx` + +**Purpose:** Shared state machine for plugin recommendations. Guards against showing a recommendation while in remote mode, while another is already showing, or while a check is in-flight. + +**Parameters:** generic `T` + +**Return Value:** `{ recommendation: T | null, clearRecommendation, tryResolve: (candidate: T | null) => void }` + +**Key Logic:** +- `tryResolve(candidate)`: if remote mode or already showing, no-op; otherwise sets `recommendation`. +- `clearRecommendation()`: clears the current recommendation. +- Guards `inFlight` ref to prevent concurrent checks. + +**Dependencies:** `useState`, `useRef`, `useCallback` + +--- + +### `useOfficialMarketplaceNotification` + +**File:** `hooks/useOfficialMarketplaceNotification.tsx` + +**Purpose:** Handles official marketplace auto-install on first launch and shows success/failure startup notifications. + +**Parameters:** none + +**Return Value:** `void` + +**Key Logic:** +- On mount: checks `settings.autoInstallOfficialMarketplace` flag. +- If set and not yet installed: calls `installOfficialMarketplace()`. +- Shows a `startup` priority notification on success or failure. + +**Dependencies:** `useEffect`, `useRef`, `useNotifications` + +--- + +### `useChromeExtensionNotification` + +**File:** `hooks/useChromeExtensionNotification.tsx` + +**Purpose:** Shows startup notifications about the Chrome extension status: requires subscription, not installed, or default-enabled. + +**Parameters:** none + +**Return Value:** `void` + +**Key Logic:** +- Uses `useStartupNotification` (notifs/) internally. +- Checks `chrome_extension_status` from settings/config. +- Returns appropriate notification text for each status. + +**Dependencies:** `useStartupNotification` + +--- + +### `useClipboardImageHint` + +**File:** `hooks/useClipboardImageHint.ts` + +**Purpose:** Shows a notification when the terminal gains focus and the clipboard contains an image. Debounced with 1 000 ms delay and a 30-second cooldown. + +**Parameters:** +- `isFocused: boolean` +- `enabled: boolean` + +**Return Value:** `void` + +**Key Logic:** +- Watches `isFocused` changes. +- On focus gain: checks clipboard via `navigator.clipboard.read()` for `image/*` MIME. +- If image found: calls `addNotification({ key: 'clipboard-image', text: 'Image in clipboard · Ctrl+V to attach' })`. +- Cooldown: stores last-shown timestamp to avoid notification spam. + +**Dependencies:** `useEffect`, `useRef`, `useNotifications` + +--- + +### `useManagePlugins` + +**File:** `hooks/useManagePlugins.ts` — (same as above; see Core section for full details) + +--- + +### `useIssueFlagBanner` + +**File:** `hooks/useIssueFlagBanner.ts` + +**Purpose:** ANT-internal: shows an issue-flag banner after a session has friction signals (e.g. repeated tool retries) and has been active for at least 30 minutes with 3+ submits. + +**Parameters:** +- `messages: readonly Message[]` +- `submitCount: number` + +**Return Value:** `boolean` — whether to show the banner. + +**Key Logic:** +- Counts tool errors, retries, and refusals in recent messages. +- Only enabled for ANT internal users (checks `isAntInternal()`). +- 30-minute cooldown stored in localStorage. + +**Dependencies:** `useMemo`, `useRef` + +--- + +### `useQueueProcessor` + +**File:** `hooks/useQueueProcessor.ts` + +**Purpose:** Processes queued commands from the unified command queue when no active query is running and no blocking UI is shown. + +**Parameters:** +- `{ executeQueuedInput: (cmd: QueuedCommand) => void, hasActiveLocalJsxUI: boolean, queryGuard: () => boolean }` + +**Return Value:** `void` + +**Key Logic:** +- Uses two `useSyncExternalStore` subscriptions: one for the command queue, one for the "is loading" state. +- When the queue is non-empty, `!isLoading`, `!hasActiveLocalJsxUI`, and `queryGuard()` returns true: dequeues and executes the next command. +- Uses `useEffect` to trigger processing on state changes. + +**Dependencies:** `useSyncExternalStore`, `useEffect`, `messageQueueManager` + +--- + +### `useTurnDiffs` + +**File:** `hooks/useTurnDiffs.ts` + +**Purpose:** Extracts per-turn file diffs from the message list for display in the `/diff` view. Uses incremental processing — only new messages are scanned on each render. + +**Parameters:** +- `messages: Message[]` + +**Return Value:** `TurnDiff[]` — reverse-chronological list of turns that modified files. + +**Key Logic:** +- `TurnDiff`: `{ turnIndex, userPromptPreview, timestamp, files: Map, stats }`. +- Detects turn boundaries from user messages that are not tool results and not `isMeta`. +- Collects `FileEdit`/`FileWrite` tool results within each turn. +- New-file hunks: generates synthetic `+line` hunks from `content`. +- Accumulated across edits to the same file in a turn. +- Cache ref holds `completedTurns` + `currentTurn` + `lastProcessedIndex` for O(n_new) processing. + +**Dependencies:** `useMemo`, `useRef` + +--- + +### `useSkillImprovementSurvey` + +**File:** `hooks/useSkillImprovementSurvey.ts` + +**Purpose:** Manages the skill improvement survey dialog. Triggered by `AppState.skillImprovementSurvey`, applies improvements to the skills file on accept. + +**Parameters:** +- `setMessages: SetMessages` + +**Return Value:** `{ isOpen: boolean, suggestion: SkillSuggestion | null, handleSelect: (selection) => void }` + +**Key Logic:** +- Reads `AppState.skillImprovementSurvey` to get the pending survey. +- `handleSelect('apply')`: calls `applySkillImprovement(suggestion)` and injects a system message. +- `handleSelect('dismiss')`: clears the survey from AppState. +- On any selection: clears `AppState.skillImprovementSurvey`. + +**Dependencies:** `useAppState`, `useSetAppState`, `useCallback` + +--- + +### `useGlobalKeybindings` + +**File:** `hooks/useGlobalKeybindings.tsx` + +**Purpose:** React component (renders `null`) that registers global keybinding handlers for Ctrl+T (toggle todos), Ctrl+O (toggle transcript/messages), Ctrl+E (toggle show-all), and Escape/Ctrl+C (exit transcript). + +**Parameters:** Props including `screen`, `isLoading`, `isSearchingHistory`, `isHelpOpen`, etc. + +**Return Value:** `null` + +**Key Logic:** +- Registers `view:toggleTasks`, `view:toggleTranscript`, `view:toggleShowAll` bindings. +- KAIROS feature flag gates: `view:toggleTasks` only active when `isTodoV2Enabled()`. +- `view:toggleTranscript` sets `expandedView = 'messages'` or `'none'` in AppState. +- `view:toggleShowAll` sets `showAllMessages` in AppState. + +**Dependencies:** `useKeybinding`, `useAppState`, `useSetAppState` + +--- + +### `CommandKeybindingHandlers` + +**File:** `hooks/useCommandKeybindings.tsx` + +**Purpose:** Registers all `command:*` keybinding actions as slash command submitters — e.g., `command:compact`, `command:memory`, `command:config`, etc. + +**Parameters:** `{ onSubmit: (cmd: string) => void, isActive?: boolean }` + +**Return Value:** `null` + +**Key Logic:** +- Calls `useKeybindings` with a map of `command:X` → `() => onSubmit('/X')` entries. +- Derives the slash command name from the keybinding action name. + +**Dependencies:** `useKeybindings` + +--- + +### `useAwaySummary` + +**File:** `hooks/useAwaySummary.ts` + +**Purpose:** Appends a "while you were away" summary message after the terminal has been blurred for 5 minutes, when no turn is in progress. + +**Parameters:** +- `messages: readonly Message[]` +- `setMessages: SetMessages` +- `isLoading: boolean` + +**Return Value:** `void` + +**Key Logic:** +- Gated on `feature('AWAY_SUMMARY')` bundle flag and `tengu_sedge_lantern` GrowthBook flag. +- Subscribes to `subscribeTerminalFocus` for blur/focus events. +- On blur: starts a `BLUR_DELAY_MS = 5 * 60_000` timer. +- Timer fire: if still loading, sets `pendingRef = true` (deferred); otherwise calls `generate()`. +- `generate()`: calls `generateAwaySummary(messages, signal)` → appends `createAwaySummaryMessage(text)`. +- On focus: clears timer, aborts in-flight generation, clears `pendingRef`. +- Second `useEffect` on `isLoading`: if `!isLoading` and `pendingRef` and still blurred, fires `generate()`. +- `hasSummarySinceLastUserTurn()`: walks backward to prevent duplicate summaries. + +**Dependencies:** `useEffect`, `useRef`, `useCallback`, `subscribeTerminalFocus`, `generateAwaySummary` + +--- + +### `useAfterFirstRender` / `renderPlaceholder` + +See entry under "Core / Utility Hooks" for `useAfterFirstRender`. + +--- + +## Notification Hooks (`notifs/`) + +All hooks in `notifs/` use `useNotifications()` from `context/notifications.js` to push entries to the status bar notification queue. Most are gated on `!getIsRemoteMode()`. + +--- + +### `useStartupNotification` + +**File:** `hooks/notifs/useStartupNotification.ts` + +**Purpose:** Base primitive for fire-once-on-mount notifications. Encapsulates the remote-mode gate and once-per-session ref guard used by most other `notifs/` hooks. + +**Parameters:** +- `compute: () => Result | Promise` — returns `null` to skip, `Notification` for one, `Notification[]` for many. + +**Return Value:** `void` + +**Key Logic:** +- `hasRunRef` prevents re-firing on re-render. +- Runs `compute` inside `Promise.resolve().then(...)` to allow async. +- Catches errors via `logError`. +- Skips entirely in remote mode. + +**Dependencies:** `useEffect`, `useRef`, `useNotifications` + +--- + +### `useAutoModeUnavailableNotification` + +**File:** `hooks/notifs/useAutoModeUnavailableNotification.ts` + +**Purpose:** Shows a one-shot warning when the Shift+Tab mode carousel wraps past where "auto mode" would have been, explaining why auto mode is unavailable. + +**Parameters:** none + +**Key Logic:** +- Detects the wrap: `mode === 'default' && prevMode !== 'default' && prevMode !== 'auto' && !isAutoModeAvailable && hasAutoModeOptIn()`. +- Calls `getAutoModeUnavailableReason()` to get the specific reason (circuit-breaker, org-allowlist, settings). +- `shownRef` prevents showing more than once per session. +- Gated on `feature('TRANSCRIPT_CLASSIFIER')`. + +--- + +### `useCanSwitchToExistingSubscription` + +**File:** `hooks/notifs/useCanSwitchToExistingSubscription.tsx` + +**Purpose:** Shows up to 3 times (MAX_SHOW_COUNT=3) across sessions a notification prompting users who have a Claude Pro/Max subscription but are logged in via API key to run `/login`. + +**Key Logic:** +- Reads `globalConfig.subscriptionNoticeCount`; returns null if >= 3. +- Calls `getOauthProfileFromApiKey()` to check for Pro/Max subscription. +- Increments `subscriptionNoticeCount` in global config on each show. +- Renders a JSX notification with `color="suggestion"`. + +--- + +### `useDeprecationWarningNotification` + +**File:** `hooks/notifs/useDeprecationWarningNotification.tsx` + +**Purpose:** Shows a `color="warning"` notification when the active model is deprecated. + +**Parameters:** `model: string` + +**Key Logic:** +- Calls `getModelDeprecationWarning(model)` on each `model` change. +- Uses `lastWarningRef` to avoid re-adding the same notification on re-render. +- Resets tracking if model changes to non-deprecated. + +--- + +### `useFastModeNotification` + +**File:** `hooks/notifs/useFastModeNotification.tsx` + +**Purpose:** Shows real-time notifications for fast mode state changes: cooldown started/expired, org-level enable/disable, and overage rejection. + +**Key Logic:** +- Subscribes to `onCooldownTriggered`, `onCooldownExpired`, `onFastModeOverageRejection`, `onOrgFastModeChanged` event emitters. +- On org-disabled while fast mode is active: disables fast mode in AppState. +- Shows immediate-priority notifications with `color="fastMode"` or `color="warning"`. + +--- + +### `useIDEStatusIndicator` + +**File:** `hooks/notifs/useIDEStatusIndicator.tsx` + +**Purpose:** Shows IDE connection status in the notification area: hint to install extension, JetBrains info, install error, or current selection preview. + +**Parameters:** `{ ideInstallationStatus, ideSelection, mcpClients }` + +**Key Logic:** +- Uses `useIdeConnectionStatus(mcpClients)` to get current status. +- Shows "install extension" hint up to MAX_IDE_HINT_SHOW_COUNT=5 times (tracked in globalConfig). +- Shows JetBrains info notification (different flow than VS Code). +- Shows selection preview as a persistent notification when file/text is selected. + +--- + +### `useInstallMessages` + +**File:** `hooks/notifs/useInstallMessages.tsx` + +**Purpose:** Shows startup notifications for native installer issues (PATH not configured, alias not set, install errors). + +**Key Logic:** +- Calls `checkInstall()` to get installation messages. +- Maps message types to priorities: `error`/`userActionRequired` → `high`; `path`/`alias` → `medium`; others → `low`. +- Colors: `error` → `color="error"`; others → `color="warning"`. + +--- + +### `useLspInitializationNotification` + +**File:** `hooks/notifs/useLspInitializationNotification.tsx` + +**Purpose:** Polls LSP server status every 5 000 ms and shows notifications when the LSP manager or individual servers fail to initialize. + +**Key Logic:** +- Gated on `ENABLE_LSP_TOOL` env var. +- Polls `getInitializationStatus()` and `getLspServerManager()`. +- De-duplicates errors using `notifiedErrorsRef` set. +- Adds errors to `appState.plugins.errors` for `/doctor` display. + +--- + +### `useMcpConnectivityStatus` + +**File:** `hooks/notifs/useMcpConnectivityStatus.tsx` + +**Purpose:** Shows notifications when MCP servers fail to connect or need authentication. + +**Parameters:** `{ mcpClients?: MCPServerConnection[] }` + +**Key Logic:** +- Filters `mcpClients` by connection state: `failed`, `needs_auth`. +- Separately tracks `claudeai` clients (connector) vs local clients (server). +- Shows JSX notifications with counts and `· /mcp` navigation hint. + +--- + +### `useModelMigrationNotifications` + +**File:** `hooks/notifs/useModelMigrationNotifications.tsx` + +**Purpose:** Shows one-time notifications immediately after automatic model migrations (e.g. Sonnet 4.5 → 4.6, Opus Pro → Opus 4.6). + +**Key Logic:** +- Uses `useStartupNotification` with a `MIGRATIONS` array of check functions. +- Each check reads a timestamp field from `globalConfig` and returns a notification if the timestamp is within the last 3 seconds (i.e., this is the launch that triggered the migration). + +--- + +### `useNpmDeprecationNotification` + +**File:** `hooks/notifs/useNpmDeprecationNotification.tsx` + +**Purpose:** Shows a 15-second warning notification when Claude Code is running via an npm install (deprecated) rather than the native installer. + +**Key Logic:** +- Skips if `isInBundledMode()` or `DISABLE_INSTALLATION_CHECKS` env var is set. +- Calls `getCurrentInstallationType()`; skips for `'development'` installs. + +--- + +### `usePluginAutoupdateNotification` + +**File:** `hooks/notifs/usePluginAutoupdateNotification.tsx` + +**Purpose:** Subscribes to `onPluginsAutoUpdated` and shows a notification prompting the user to run `/reload-plugins` when plugins have been auto-updated in the background. + +**Key Logic:** +- `useState([])` for `updatedPlugins` list. +- `onPluginsAutoUpdated` subscription fires with updated plugin IDs. +- Extracts plugin names (strips `@marketplace` suffix) and shows JSX notification with `color="success"`. +- 10 000 ms timeout. + +--- + +### `usePluginInstallationStatus` + +**File:** `hooks/notifs/usePluginInstallationStatus.tsx` + +**Purpose:** Shows a notification when one or more plugins fail to install (from AppState `plugins.installationStatus`). + +**Key Logic:** +- Reads `installationStatus.marketplaces` and `installationStatus.plugins` from AppState. +- Filters by `status === 'failed'`, memoizes counts. +- Shows `"N plugins failed to install · /plugin for details"` with `priority: 'medium'`. + +--- + +### `useRateLimitWarningNotification` + +**File:** `hooks/notifs/useRateLimitWarningNotification.tsx` + +**Purpose:** Shows rate limit warnings: (1) immediate notification when entering overage mode; (2) warning notification when approaching usage limits. + +**Parameters:** `model: string` + +**Key Logic:** +- `useClaudeAiLimits()` for reactive limit data. +- `getRateLimitWarning(limits, model)` — string describing approaching limit. +- `getUsingOverageText(limits)` — string for overage mode. +- Overage notification shown once per overage entry (tracked via `hasShownOverageNotification` state). +- Team/enterprise: skips overage notification unless user has billing access. +- Warning notification shown only when text changes (deduped via `shownWarningRef`). + +--- + +### `useSettingsErrors` + +**File:** `hooks/notifs/useSettingsErrors.tsx` + +**Purpose:** Watches for settings validation errors (from `getSettingsWithAllErrors`) and shows/removes a warning notification in the status bar. + +**Return Value:** `ValidationError[]` — the current list of errors (also used for /doctor display). + +**Key Logic:** +- Initial state populated synchronously from `getSettingsWithAllErrors()`. +- `useSettingsChange` subscription: re-reads errors on file change. +- Shows `"Found N settings issues · /doctor for details"` with 60 000 ms timeout. +- Removes notification when errors clear. + +--- + +### `useTeammateShutdownNotification` / `useTeammateLifecycleNotification` + +**File:** `hooks/notifs/useTeammateShutdownNotification.ts` + +**Purpose:** Fires batched spawn/shutdown notifications when in-process teammates start or complete. Uses fold() to combine `"1 agent spawned"` + `"1 agent spawned"` into `"2 agents spawned"`. + +**Key Logic:** +- Reads `tasks` from AppState. +- Tracks seen running/completed IDs in `seenRunningRef` / `seenCompletedRef`. +- `makeSpawnNotif(count)` / `makeShutdownNotif(count)` with 5 000 ms timeout and fold function. +- Exported as `useTeammateLifecycleNotification`. + +--- + +## Tool Permission Subsystem (`toolPermission/`) + +### `PermissionContext.ts` + +**File:** `hooks/toolPermission/PermissionContext.ts` + +**Purpose:** Factory for creating a `PermissionContext` object — the shared context passed to all three permission handlers. Contains all callbacks needed to approve, deny, log, queue, and persist permission decisions. + +**Exported Functions:** +- `createPermissionContext(tool, input, toolUseContext, assistantMessage, toolUseID, setToolPermissionContext, queueOps?) → PermissionContext` +- `createPermissionQueueOps(setToolUseConfirmQueue) → PermissionQueueOps` — bridges React state setter to the generic queue interface. +- `createResolveOnce(resolve) → ResolveOnce` — atomic check-and-mark-as-resolved guard for races. + +**PermissionContext methods:** +- `logDecision(args, opts?)` — delegates to `logPermissionDecision`. +- `logCancelled()` — logs `tengu_tool_use_cancelled` event. +- `persistPermissions(updates)` — calls `persistPermissionUpdates` and updates AppState. +- `resolveIfAborted(resolve)` — short-circuits if abort signal is fired. +- `cancelAndAbort(feedback?, isAbort?, contentBlocks?)` — builds a deny decision and aborts the controller if appropriate. +- `tryClassifier(pendingCheck, updatedInput)` — awaits classifier auto-approval (bash only; `BASH_CLASSIFIER` feature flag). +- `runHooks(permissionMode, suggestions, updatedInput?, startTimeMs?)` — executes PermissionRequest hooks sequentially. +- `buildAllow(updatedInput, opts?)` → `PermissionAllowDecision` +- `buildDeny(message, reason)` → `PermissionDenyDecision` +- `handleUserAllow(updatedInput, permissionUpdates, feedback?, startTimeMs?, contentBlocks?, decisionReason?)` — persists updates, logs, returns allow decision. +- `handleHookAllow(finalInput, permissionUpdates, startTimeMs?)` — same for hook-sourced allows. +- `pushToQueue(item)`, `removeFromQueue()`, `updateQueueItem(patch)` — queue management via `queueOps`. + +--- + +### `permissionLogging.ts` + +**File:** `hooks/toolPermission/permissionLogging.ts` + +**Purpose:** Centralized analytics and telemetry logging for all tool permission decisions. Fans out to Statsig (logEvent), OTel telemetry, code-edit metrics, and the `toolUseContext.toolDecisions` map. + +**Exported Functions:** +- `logPermissionDecision(ctx, args, startTimeMs?)` — main entry point. +- `isCodeEditingTool(toolName)` — checks if tool is Edit/Write/NotebookEdit. +- `buildCodeEditToolAttributes(tool, input, decision, source)` — builds OTel attributes including language from file path. + +**Analytics Events:** +- `tengu_tool_use_granted_in_config` — auto-approved by settings allowlist. +- `tengu_tool_use_granted_in_prompt_permanent` / `_temporary` — user approved. +- `tengu_tool_use_granted_by_permission_hook` — hook approved. +- `tengu_tool_use_granted_by_classifier` — classifier approved. +- `tengu_tool_use_rejected_in_prompt` — any rejection. +- `tengu_tool_use_denied_in_config` — denied by settings denylist. + +--- + +### `handlers/coordinatorHandler.ts` + +**File:** `hooks/toolPermission/handlers/coordinatorHandler.ts` + +**Purpose:** Handles the coordinator-worker permission flow: runs hooks then classifier (both awaited sequentially) before falling through to the interactive dialog. + +**Exported:** `handleCoordinatorPermission(params) → Promise` + +**Parameters:** `CoordinatorPermissionParams` — `{ ctx, pendingClassifierCheck?, updatedInput, suggestions, permissionMode }` + +**Logic:** +1. `await ctx.runHooks(...)` — if hooks return a decision, return it. +2. If `BASH_CLASSIFIER` flag: `await ctx.tryClassifier?.(...)` — if classifier returns, return it. +3. Return `null` → caller falls through to `handleInteractivePermission`. +4. On unexpected error: logs and returns null (graceful fallback to dialog). + +--- + +### `handlers/interactiveHandler.ts` + +**File:** `hooks/toolPermission/handlers/interactiveHandler.ts` + +**Purpose:** Handles the interactive (main-agent) permission flow. Sets up the `ToolUseConfirm` queue entry with all callbacks and races user interaction against background automated checks (hooks, classifier, bridge, channel). + +**Exported:** `handleInteractivePermission(params, resolve) → void` (synchronous setup) + +**Key Logic:** +- Creates a `PermissionConfirm` queue entry with callbacks: `onAbort`, `onAllow`, `onReject`, `recheckPermission`, `onUserInteraction`, `onDismissCheckmark`. +- `createResolveOnce` guard ensures only the first resolution wins. +- `userInteracted` flag: prevents classifier from auto-approving after user interaction (200 ms grace period). +- **Race 1 — User**: `onAllow`/`onReject`/`onAbort` callbacks. +- **Race 2 — Hooks**: async `ctx.runHooks(...)` — if win, removes from queue, resolves. +- **Race 3 — Classifier**: `executeAsyncClassifierCheck(...)` — on allow, shows checkmark UI for 3 s (focused) or 1 s (blurred), then removes from queue. +- **Race 4 — Bridge (CCR)**: sends `permission_request` to CCR; subscribes to CCR response; on win, logs, resolves. +- **Race 5 — Channel**: sends structured `permission_request` to all active channel MCP servers (Telegram, iMessage); subscribes to response; on win, resolves. +- Checkmark dismissal: `onDismissCheckmark` allows user to press Escape during the checkmark window. +- Abort: if abort signal fires mid-dialog, `claim()` races to resolve with cancel. + +--- + +### `handlers/swarmWorkerHandler.ts` + +**File:** `hooks/toolPermission/handlers/swarmWorkerHandler.ts` + +**Purpose:** Handles the swarm-worker permission flow: tries classifier auto-approval, then forwards the request to the team leader via mailbox. Awaits the leader's response. + +**Exported:** `handleSwarmWorkerPermission(params) → Promise` + +**Logic:** +1. Returns `null` if not `isAgentSwarmsEnabled()` or not `isSwarmWorker()`. +2. If `BASH_CLASSIFIER`: tries `ctx.tryClassifier?.(...)`. +3. Creates a `Promise` that resolves when the leader responds. +4. Registers `onAllow`/`onReject` callbacks via `registerPermissionCallback`. +5. Calls `sendPermissionRequestViaMailbox(request)` to notify the leader. +6. Sets `AppState.pendingWorkerRequest` for visual indicator. +7. On abort: resolves with `cancelAndAbort`. +8. On error: returns `null` (fallback to local UI handling). + +--- + +## Non-Hook Utilities in `hooks/` + +These files live in `hooks/` but are not React hooks. + +### `fileSuggestions.ts` + +**File:** `hooks/fileSuggestions.ts` + +**Exports:** +- `generateFileSuggestions(query, cwd, ...) → Promise` — main entry point. Manages a `FileIndex` singleton (native Rust/nucleo), fetches tracked files via `git ls-files`, falls back to `ripgrep`. Returns up to 15 scored matches. +- `startBackgroundCacheRefresh(cwd)` — queues a background refresh of untracked files. +- `clearFileSuggestionCaches()` — called on `/clear` to reset the index. +- `applyFileSuggestion(suggestion, inputValue, cursorOffset) → string` — replaces the `@token` in the input with the chosen file path. +- `findLongestCommonPrefix(suggestions) → string` — used for Tab-autocomplete of common prefix. +- `onIndexBuildComplete(callback)` — notifies when background index is built. + +**Key Design:** +- Path signature (mtime + size) invalidates cache without full rebuild. +- `.ignore` / `.rgignore` file support. +- Directory name extraction for `@dir/` completions. + +--- + +### `unifiedSuggestions.ts` + +**File:** `hooks/unifiedSuggestions.ts` + +**Exports:** +- `generateUnifiedSuggestions(query, mcpResources, agents, showOnEmpty) → Promise` — merges file suggestions (nucleo), MCP resource suggestions (Fuse.js), and agent suggestions into a ranked list of up to 15 items. + +**Key Design:** +- File suggestions use nucleo score (0–1 float). +- MCP resource suggestions use Fuse.js score (lower = better, inverted for sorting). +- Agent suggestions always appended at lower priority. + +--- + +### `renderPlaceholder.ts` + +**File:** `hooks/renderPlaceholder.ts` + +**Exports:** +- `renderPlaceholder(placeholder, hidePlaceholderText?, cursorChar?) → string` — pure function that renders placeholder text with a cursor character appended. When `hidePlaceholderText = true` (voice recording mode), returns only the cursor. diff --git a/spec/08_ink_terminal.md b/spec/08_ink_terminal.md new file mode 100644 index 0000000..083005a --- /dev/null +++ b/spec/08_ink_terminal.md @@ -0,0 +1,1608 @@ +# Claude Code — Ink Terminal Rendering System + +## Overview + +The ink directory contains a complete, custom terminal UI framework built on top of React. It is a heavily modified and extended fork of the open-source Ink library, tuned for Claude Code's requirements: fullscreen alternate-screen rendering, hardware-accelerated scroll, text selection, search highlighting, mouse tracking, bidirectional text, and fine-grained performance instrumentation. + +The system can be summarized as a pipeline: + +``` +React tree + → React Reconciler (reconciler.ts) + → Virtual DOM (dom.ts) + → Yoga layout engine (layout/) + → Output buffer (output.ts) + → Screen cell buffer (screen.ts) + → Diff engine (log-update.ts) + → Patch optimizer (optimizer.ts) + → Terminal write (terminal.ts / termio/) +``` + +The top-level entry point for consumers is `/x/Bigger-Projects/Claude-Code/src/ink.ts`, which wraps the internal `root.ts` render and `createRoot` APIs with a mandatory `ThemeProvider`. + +--- + +## File-by-File Reference + +### `/x/Bigger-Projects/Claude-Code/src/ink.ts` — Public API Module + +**Purpose:** The package-level façade that re-exports all public APIs from the ink subsystem. Wraps every `render()` and `createRoot()` call in a `ThemeProvider` so that `ThemedBox`/`ThemedText` components work at every call site without requiring consumers to mount the provider manually. + +**Exports:** +- `render(node, options?)` — async, mounts a React tree wrapped in `ThemeProvider`; returns `Instance` +- `createRoot(options?)` — async; returns a `Root` whose `.render()` method also wraps in `ThemeProvider` +- `RenderOptions`, `Instance`, `Root` — type re-exports from `root.ts` +- `color` — from the design-system color module +- `Box`, `BoxProps` — themed box (design-system ThemedBox) +- `Text`, `TextProps` — themed text (design-system ThemedText) +- `ThemeProvider`, `usePreviewTheme`, `useTheme`, `useThemeSetting` +- `Ansi` — ANSI-string rendering component +- `BaseBox`, `BaseBoxProps` — raw ink Box without theme +- `BaseText`, `BaseTextProps` — raw ink Text without theme +- `Button`, `ButtonProps`, `ButtonState` +- `Link`, `LinkProps` +- `Newline`, `NewlineProps` +- `NoSelect` +- `RawAnsi` +- `Spacer` +- `DOMElement` — the virtual DOM element type +- `ClickEvent`, `EventEmitter`, `Event`, `Key`, `InputEvent` +- `TerminalFocusEvent`, `TerminalFocusEventType` +- `FocusManager` +- `FlickerReason` +- `useAnimationFrame`, `useApp`, `useInput`, `useAnimationTimer`, `useInterval` +- `useSelection`, `useStdin`, `useTabStatus`, `useTerminalFocus` +- `useTerminalTitle`, `useTerminalViewport` +- `measureElement` +- `supportsTabStatus` +- `wrapText` + +--- + +### `/x/Bigger-Projects/Claude-Code/src/ink/constants.ts` + +**Purpose:** Shared timing constant. + +**Exports:** +- `FRAME_INTERVAL_MS = 16` — target frame interval (~60 fps), used by the throttled `scheduleRender`. + +--- + +### `/x/Bigger-Projects/Claude-Code/src/ink/ink.tsx` — Core Ink Class + +**Purpose:** The central orchestrator. `class Ink` owns the React fiber root, the yoga layout tree, the double-buffered screen, the focus manager, stdin/stdout event handlers, selection state, and the main render loop. + +**Class `Ink`** + +Constructor receives `Options`: +```typescript +type Options = { + stdout: NodeJS.WriteStream + stdin: NodeJS.ReadStream + stderr: NodeJS.WriteStream + exitOnCtrlC: boolean + patchConsole: boolean + waitUntilExit?: () => Promise + onFrame?: (event: FrameEvent) => void +} +``` + +Key private state: +| Field | Type | Description | +|---|---|---| +| `log` | `LogUpdate` | Diff engine that writes ANSI to stdout | +| `terminal` | `Terminal` | `{ stdout, stderr }` write streams | +| `scheduleRender` | throttled fn | Throttled at 16 ms, leading+trailing, deferred via `queueMicrotask` | +| `container` | `FiberRoot` | React-reconciler fiber root (ConcurrentRoot mode) | +| `rootNode` | `DOMElement` | The `ink-root` DOM node | +| `focusManager` | `FocusManager` | DOM-style focus state machine | +| `renderer` | `Renderer` | `createRenderer()` closure | +| `stylePool` | `StylePool` | Session-lived ANSI style interning pool | +| `charPool` | `CharPool` | Session-lived character interning pool | +| `hyperlinkPool` | `HyperlinkPool` | Session-lived hyperlink URL interning pool | +| `frontFrame` / `backFrame` | `Frame` | Double-buffered screen frames | +| `selection` | `SelectionState` | Text selection for alt-screen mode | +| `searchHighlightQuery` | `string` | Current /search term | +| `searchPositions` | object or null | Pre-scanned match positions for current-match highlight | +| `altScreenActive` | `boolean` | Set by `` | +| `prevFrameContaminated` | `boolean` | Forces full repaint next frame | + +**Render loop (`onRender`):** +1. Run `createRenderer()` — walks DOM, runs yoga, fills back-buffer screen +2. Apply selection overlay (invert styled cells in `selection`) +3. Apply search highlight (invert cells matching `searchHighlightQuery`) +4. Apply positioned highlight (yellow/bold current-match via `searchPositions`) +5. Diff back-frame vs. front-frame via `log.render()` → `Patch[]` +6. Run `optimize(patches)` to merge/deduplicate +7. Call `writeDiffToTerminal()` to serialize patches and write ANSI to stdout +8. Emit `onFrame` event if wired +9. Swap front/back frames + +**Alt-screen handling:** +- `setAltScreenActive(active, mouseTracking)` — called by `` during insertion effects; enables BSU/ESU synchronized output, DECSTBM hardware scroll hints, and selection-aware repaints +- `resetFramesForAltScreen()` — replaces both frames with blank screens, sets `prevFrameContaminated = true` +- `reenterAltScreen()` — re-asserts alt-screen state on SIGCONT + +**Resize handling (`handleResize`):** +- Synchronous (no debounce) to keep `terminalColumns`/`terminalRows` and yoga in sync +- For alt-screen: resets frame buffers and sets `needsEraseBeforePaint = true` so the erase happens atomically inside the next BSU/ESU block + +**Console patching:** +- `patchConsole()` — intercepts `console.log/warn/error` so they write to a separate file descriptor (not stdout), preventing output mixing +- `patchStderr()` — same for stderr + +**Key public methods:** +- `render(node)` — calls `reconciler.updateContainer()` +- `unmount()` — graceful teardown: restore console, disable mouse tracking, exit alt screen, write final frame, free yoga nodes +- `waitUntilExit()` — returns a promise resolved on `unmount()` +- `clearTextSelection()` — clear selection state and force repaint +- `setSearchHighlight(query)` — set live search term +- `setSearchPositions(positions, rowOffset, currentIdx)` — set positioned highlights for search navigation + +--- + +### `/x/Bigger-Projects/Claude-Code/src/ink/root.ts` — Public Entry Points + +**Purpose:** Wraps `Ink` in the public `render()` and `createRoot()` APIs; manages the `instances` map so repeated calls to `render()` reuse the same `Ink` instance for the same stdout stream. + +**Exports:** +```typescript +type RenderOptions = { + stdout?: NodeJS.WriteStream + stdin?: NodeJS.ReadStream + stderr?: NodeJS.WriteStream + exitOnCtrlC?: boolean + patchConsole?: boolean + onFrame?: (event: FrameEvent) => void +} + +type Instance = { + rerender: Ink['render'] + unmount: Ink['unmount'] + waitUntilExit: Ink['waitUntilExit'] + cleanup: () => void +} + +type Root = { + render: (node: ReactNode) => void + unmount: () => void + waitUntilExit: () => Promise +} + +export const renderSync(node, options?): Instance // synchronous mount +export default async function render(node, options?): Promise +export async function createRoot(options?): Promise +``` + +`renderSync` is used internally; the public-facing `render` and `createRoot` are the async versions exported through `ink.ts`. + +--- + +### `/x/Bigger-Projects/Claude-Code/src/ink/instances.ts` + +**Purpose:** Module-level singleton map keyed by `NodeJS.WriteStream`. Ensures one `Ink` instance per stdout stream. + +```typescript +const instances = new Map() +export default instances +``` + +--- + +## DOM Layer + +### `/x/Bigger-Projects/Claude-Code/src/ink/dom.ts` — Virtual DOM + +**Purpose:** Defines the virtual DOM node types and all mutation operations. Mirrors a minimal browser DOM API adapted for a terminal. Every mutation marks the affected node and all ancestors `dirty` so the render pass can skip clean subtrees. + +**Element name types:** +```typescript +type ElementNames = + | 'ink-root' // Document root; owns FocusManager + | 'ink-box' // Flex container (yoga node) + | 'ink-text' // Leaf text container (yoga measure func) + | 'ink-virtual-text' // Inline text, no yoga node + | 'ink-link' // OSC 8 hyperlink wrapper, no yoga node + | 'ink-progress' // Terminal progress indicator, no yoga node + | 'ink-raw-ansi' // Pre-rendered ANSI, yoga node with rawWidth/rawHeight + +type TextName = '#text' +type NodeNames = ElementNames | TextName +``` + +**`DOMElement` structure:** +```typescript +type DOMElement = { + nodeName: ElementNames + attributes: Record + childNodes: DOMNode[] + textStyles?: TextStyles + onComputeLayout?: () => void // Called by reconciler.resetAfterCommit + onRender?: () => void // Points to throttled scheduleRender + onImmediateRender?: () => void // Synchronous, for tests + hasRenderedContent?: boolean // React 19 test-mode guard + dirty: boolean + isHidden?: boolean + _eventHandlers?: Record // Event handlers (separated from attrs) + // Scroll state (overflow: scroll boxes): + scrollTop?: number + pendingScrollDelta?: number + scrollClampMin?: number + scrollClampMax?: number + scrollHeight?: number + scrollViewportHeight?: number + scrollViewportTop?: number + stickyScroll?: boolean + scrollAnchor?: { el: DOMElement; offset: number } + focusManager?: FocusManager // Only on ink-root + debugOwnerChain?: string[] // CLAUDE_CODE_DEBUG_REPAINTS mode + yogaNode?: LayoutNode + style: Styles + parentNode: DOMElement | undefined +} +``` + +**Exported functions:** +- `createNode(nodeName)` — allocates a `DOMElement`; attaches a yoga measure function for `ink-text` and `ink-raw-ansi` +- `createTextNode(text)` — allocates a `TextNode` +- `appendChildNode(node, childNode)` — appends child, syncs yoga tree, marks dirty +- `insertBeforeNode(node, newChild, beforeChild)` — inserts before; yoga index computed separately from DOM index because some nodes lack yoga nodes +- `removeChildNode(node, removeNode)` — removes child, collects `pendingClears`, marks dirty +- `setAttribute(node, key, value)` — sets attribute only if changed; skips `children` +- `setStyle(node, style)` — shallow-compares style objects; skips if unchanged +- `setTextStyles(node, textStyles)` — shallow-compares; skips if unchanged +- `setTextNodeValue(node, text)` — updates text, marks dirty +- `markDirty(node?)` — walks the ancestor chain setting `dirty = true`; marks yoga dirty on `ink-text`/`ink-raw-ansi` leaf nodes +- `scheduleRenderFrom(node?)` — walks to root and calls `onRender()` +- `clearYogaNodeReferences(node)` — recursively clears `yogaNode` pointers (call before `freeRecursive()`) +- `findOwnerChainAtRow(root, y)` — DFS to find the React component stack covering screen row `y` (debug repaints mode) + +**Dirty-checking optimization:** `stylesEqual` and `shallowEqual` prevent marking dirty when React creates a new style object with identical values on every render. + +**Yoga index vs DOM index:** Nodes like `ink-virtual-text`, `ink-link`, and `ink-progress` have no yoga node. `insertBeforeNode` counts only yoga-equipped children to compute the correct yoga insertion index. + +--- + +## Reconciler + +### `/x/Bigger-Projects/Claude-Code/src/ink/reconciler.ts` + +**Purpose:** Configures `react-reconciler` to use the ink virtual DOM as the host environment. This is the bridge between React's fiber tree and the ink DOM tree. + +**Reconciler type parameters:** +```typescript +createReconciler< + ElementNames, // Type + Props, // Props + DOMElement, // Container + DOMElement, // Instance + TextNode, // TextInstance + DOMElement, // SuspenseInstance + unknown, // HydratableInstance + unknown, // PublicInstance + DOMElement, // HostContext (root) + HostContext, // ChildSet + null, // UpdatePayload (unused in React 19) + NodeJS.Timeout, // TimeoutHandle + -1, // NoTimeout + null // TransitionStatus +> +``` + +**Key reconciler methods:** + +| Method | Behavior | +|---|---| +| `createInstance(type, props, rootContainer, context, fiber)` | Calls `createNode(type)`; applies all props via `applyProp`; optionally captures `debugOwnerChain` from fiber | +| `createTextInstance(text)` | Calls `createTextNode(text)` | +| `appendInitialChild` / `appendChild` | Calls `appendChildNode` | +| `insertBefore` | Calls `insertBeforeNode` | +| `removeChild` | Calls `removeChildNode`; notifies `focusManager.handleNodeRemoved` | +| `commitUpdate(instance, updatePayload, type, oldProps, newProps)` | Diffs old/new props and applies changes; uses `diff()` to find changed keys | +| `commitTextUpdate(textInstance, oldText, newText)` | Calls `setTextNodeValue` | +| `hideInstance(instance)` / `unhideInstance(instance)` | Sets `isHidden` and `LayoutDisplay.None` / restores display | +| `prepareForCommit` | Records timing start | +| `resetAfterCommit(rootNode)` | Records commit duration; calls `onComputeLayout()` (yoga layout); triggers `onImmediateRender` in test mode; calls `onRender()` in production | +| `commitMount(instance, type, props)` | Calls `focusManager.focus()` if `autoFocus` prop set | + +**`applyProp(node, key, value)`:** Routes to `setStyle` (key=`style`), `setTextStyles` (key=`textStyles`), `setEventHandler` (key in `EVENT_HANDLER_PROPS`), or `setAttribute`. + +**Event handler separation:** Event handler props are stored in `node._eventHandlers` rather than `node.attributes`. This prevents handler identity changes from marking the node dirty and defeating the blit optimization. + +**`getOwnerChain(fiber)`:** Walks the React fiber's `_debugOwner` / `return` chain to collect component names. Used for `CLAUDE_CODE_DEBUG_REPAINTS` mode to attribute flicker to source components. + +**Profiling exports:** +- `recordYogaMs(ms)` / `getLastYogaMs()` — yoga layout timing +- `markCommitStart()` / `getLastCommitMs()` — React commit timing +- `resetProfileCounters()` +- `dispatcher` — the `Dispatcher` instance (event dispatch) +- `isDebugRepaintsEnabled()` — reads `CLAUDE_CODE_DEBUG_REPAINTS` env var once + +--- + +## Layout Engine + +### `/x/Bigger-Projects/Claude-Code/src/ink/layout/node.ts` — Layout Node Interface + +**Purpose:** Abstract interface for a layout node. Decouples the ink DOM from the concrete Yoga WASM implementation. + +**Constants:** +```typescript +LayoutEdge: { All, Horizontal, Vertical, Left, Right, Top, Bottom, Start, End } +LayoutGutter: { All, Column, Row } +LayoutDisplay: { Flex, None } +LayoutFlexDirection: { Row, RowReverse, Column, ColumnReverse } +LayoutAlign: { Auto, Stretch, FlexStart, Center, FlexEnd } +LayoutJustify: { FlexStart, Center, FlexEnd, SpaceBetween, SpaceAround, SpaceEvenly } +LayoutWrap: { NoWrap, Wrap, WrapReverse } +LayoutPositionType: { Relative, Absolute } +LayoutOverflow: { Visible, Hidden, Scroll } +LayoutMeasureMode: { Undefined, Exactly, AtMost } +``` + +**`LayoutNode` interface** (abbreviated): +```typescript +interface LayoutNode { + // Tree operations + insertChild(child, index): void + removeChild(child): void + getChildCount(): number + getParent(): LayoutNode | null + + // Layout computation + calculateLayout(width?, height?): void + setMeasureFunc(fn: LayoutMeasureFunc): void + unsetMeasureFunc(): void + markDirty(): void + + // Layout reading (post-layout) + getComputedLeft(): number + getComputedTop(): number + getComputedWidth(): number + getComputedHeight(): number + getComputedBorder(edge): number + getComputedPadding(edge): number + + // Style setters (width, height, min/max, flex*, align*, justify, display, + // position, overflow, margin, padding, border, gap) + + // Lifecycle + free(): void + freeRecursive(): void +} +``` + +### `/x/Bigger-Projects/Claude-Code/src/ink/layout/yoga.ts` — Yoga Adapter + +**Purpose:** Implements `LayoutNode` by wrapping the native Yoga WASM node (`src/native-ts/yoga-layout`). Maps `LayoutEdge`/`LayoutGutter`/etc. string enums to Yoga enum values. + +**Class `YogaLayoutNode`:** +- Holds a `YogaNode` (`this.yoga`) +- `setMeasureFunc`: wraps the `LayoutMeasureFunc` to translate `MeasureMode` enum values +- `calculateLayout(width)`: calls `this.yoga.calculateLayout(width, undefined, Direction.LTR)` — height is always undefined (intrinsic) + +Edge/gutter enum maps are static `EDGE_MAP` and `GUTTER_MAP` objects. + +### `/x/Bigger-Projects/Claude-Code/src/ink/layout/engine.ts` + +**Purpose:** Factory for layout nodes. A one-line indirection: `createLayoutNode()` calls `createYogaLayoutNode()`. Allows swapping the layout backend without changing the DOM layer. + +### `/x/Bigger-Projects/Claude-Code/src/ink/layout/geometry.ts` + +**Purpose:** 2D geometry primitives used throughout the rendering pipeline. + +**Exports:** +```typescript +type Point = { x: number; y: number } +type Size = { width: number; height: number } +type Rectangle = Point & Size +type Edges = { top: number; right: number; bottom: number; left: number } + +edges(all): Edges +edges(v, h): Edges +edges(t, r, b, l): Edges +addEdges(a, b): Edges +resolveEdges(partial?): Edges +ZERO_EDGES: Edges +unionRect(a, b): Rectangle // bounding union +clampRect(rect, size): Rectangle +withinBounds(size, point): boolean +clamp(value, min?, max?): number +``` + +--- + +## Styles + +### `/x/Bigger-Projects/Claude-Code/src/ink/styles.ts` + +**Purpose:** TypeScript types for box/text styles plus `applyStyles()` which translates style props onto a `LayoutNode`. + +**Color types:** +```typescript +type RGBColor = `rgb(${number},${number},${number})` +type HexColor = `#${string}` +type Ansi256Color = `ansi256(${number})` +type AnsiColor = 'ansi:black' | 'ansi:red' | ... // 16 named colors +type Color = RGBColor | HexColor | Ansi256Color | AnsiColor +``` + +**`TextStyles`:** `{ color?, backgroundColor?, dim?, bold?, italic?, underline?, strikethrough?, inverse? }` — applied during rendering via chalk/colorize; not yoga properties. + +**`Styles`:** The complete set of layout and text style props including: +- `textWrap`: `'wrap' | 'wrap-trim' | 'end' | 'middle' | 'truncate-end' | 'truncate' | 'truncate-middle' | 'truncate-start'` +- `position`: `'absolute' | 'relative'` +- `top | bottom | left | right`: `number | '${number}%'` +- `columnGap | rowGap | gap`: number +- `margin | marginX | marginY | marginTop | marginBottom | marginLeft | marginRight`: number +- `padding | paddingX | paddingY | paddingTop | paddingBottom | paddingLeft | paddingRight`: number +- `flexGrow | flexShrink | flexBasis`: number +- `flexDirection`: `'row' | 'row-reverse' | 'column' | 'column-reverse'` +- `flexWrap`: `'wrap' | 'nowrap' | 'wrap-reverse'` +- `alignItems | alignSelf | justifyContent` +- `width | height | minWidth | minHeight | maxWidth | maxHeight`: number or `'${number}%'` or `'100%'` +- `display`: `'flex' | 'none'` +- `overflow | overflowX | overflowY`: `'visible' | 'hidden' | 'scroll'` +- `borderStyle`: `BorderStyle` +- `borderColor | borderTopColor | borderRightColor | borderBottomColor | borderLeftColor`: Color +- `borderDimColor | ...`: boolean +- `borderTop | borderRight | borderBottom | borderLeft`: boolean +- `color | backgroundColor | dimColor | bold | italic | underline | strikethrough | inverse` + +**`applyStyles(yogaNode, styles)`:** Translates each Styles property to `yogaNode.setXxx()` calls. Percentage values use `setWidthPercent`, etc. Position/overflow/display use enum mapping. + +--- + +## Screen Buffer + +### `/x/Bigger-Projects/Claude-Code/src/ink/screen.ts` + +**Purpose:** The core cell-based screen buffer. Stores the rendered content as a 2D grid of cells. Each cell is packed into two 32-bit integers for memory efficiency. + +**Cell encoding (packed as two `Int32Array` elements per cell):** +- Word 0 (low 32 bits): `charId` (high 22 bits) | `styleId` (low 10 bits) +- Word 1 (high 32 bits): `hyperlinkId` (high 16 bits) | `width` (2 bits) | flags + +`CellWidth` enum: `Single = 0`, `Wide = 1`, `SpacerTail = 2`, `SpacerHead = 3` + +**Pools (shared across all screens for zero-allocation diffing):** + +`CharPool`: +- `intern(char)`: returns a stable integer ID; ASCII chars use a fast `Int32Array` lookup; others use a `Map` +- `get(id)`: retrieves the string +- Pool index 0 = space, index 1 = empty (spacer cell) + +`HyperlinkPool`: +- `intern(hyperlink?)`: returns 0 for no hyperlink +- `get(id)`: returns the URL string or `undefined` + +`StylePool`: +- `intern(styles: AnsiCode[])`: returns a tagged integer ID; bit 0 = `VISIBLE_ON_SPACE` flag (background/inverse/underline affect spaces) +- `get(id)`: strips bit-0 flag and returns `AnsiCode[]` +- `transition(fromId, toId)`: returns the cached ANSI transition string (pre-serialized diff) +- `withInverse(baseId)`: returns ID of style with SGR 7 (inverse) added +- `withCurrentMatch(baseId)`: returns ID of style with inverse + bold + yellow-fg + underline (current search match) +- `withSelectionBg(baseId)`: returns ID of style with selection background color applied +- `setSelectionBg(bg)`: sets the selection background `AnsiCode` (clears cache) + +**`Screen` type:** +```typescript +type Screen = { + cells: Int32Array // packed cell data, width * height * 2 words per cell + width: number + height: number + charPool: CharPool + stylePool: StylePool + hyperlinkPool: HyperlinkPool + softWrap: Uint8Array // 1 bit per row; 1 = soft-wrapped continuation + noSelect: Uint8Array // 1 bit per cell; set on gutter/non-selectable regions +} +``` + +**Key functions:** +- `createScreen(width, height, stylePool, charPool, hyperlinkPool)` — allocates a new screen +- `resetScreen(screen)` — zeroes all cells +- `setCellAt(screen, x, y, char, styleId, width, hyperlink?)` — writes a cell +- `cellAt(screen, x, y)` — reads a cell +- `cellAtIndex(screen, idx)` — reads cell at raw index +- `isEmptyCellAt(screen, x, y)` — both packed words = 0 +- `blitRegion(dst, src, srcRect, dstX, dstY)` — bulk-copy a rectangle from one screen to another (pure `Int32Array` copy, no decoding) +- `shiftRows(screen, top, bottom, delta)` — hardware scroll simulation: move rows up/down within bounds +- `markNoSelectRegion(screen, x, y, w, h)` — set the noSelect bit on a rectangular region +- `diffEach(prev, next, callback)` — iterate only changed cells between two screens +- `migrateScreenPools(screen, charPool, hyperlinkPool)` — re-intern all cells when pools are replaced (generational reset) + +--- + +## Rendering Pipeline + +### `/x/Bigger-Projects/Claude-Code/src/ink/renderer.ts` + +**Purpose:** Creates a `Renderer` function (a closure over the root DOM node and `StylePool`) that converts the virtual DOM into a `Frame` object on each call. + +**`Renderer` type:** `(options: RenderOptions) => Frame` + +**`RenderOptions`:** +```typescript +{ + frontFrame: Frame + backFrame: Frame + isTTY: boolean + terminalWidth: number + terminalRows: number + altScreen: boolean + prevFrameContaminated: boolean +} +``` + +**Algorithm:** +1. Check yoga computed dimensions; return empty frame if invalid +2. For alt-screen: clamp `height` to `terminalRows` (enforces the invariant) +3. Reuse or create `Output` with the back-buffer screen +4. Reset `layoutShifted`, `scrollHint`, `scrollDrainNode` +5. If `prevFrameContaminated` or an absolute node was removed: disable blit (pass `prevScreen = undefined`) +6. Call `renderNodeToOutput(node, output, { prevScreen })` +7. After render: if a scroll-drain node remains, call `markDirty(drainNode)` to schedule the next drain frame +8. Return `Frame` with the rendered screen, viewport, cursor position, and scroll hint + +**Cursor position:** +- Alt-screen: `y = min(screen.height, terminalRows) - 1` — keeps cursor inside viewport to prevent LF-induced scroll +- Main-screen: `y = screen.height` +- `visible = !isTTY || screen.height === 0` — cursor is hidden during active rendering + +The `Output` instance is reused across frames so its `charCache` (per-line grapheme cluster cache) persists between renders, making steady-state spinner/clock renders near zero-allocation. + +### `/x/Bigger-Projects/Claude-Code/src/ink/render-node-to-output.ts` + +**Purpose:** Recursively walks the DOM tree and emits write/blit/clear/clip/shift operations onto the `Output` buffer. This is the layout-to-pixels bridge. + +**Key exported state:** +- `didLayoutShift()` / `resetLayoutShifted()` — set when any node moves; gates the full-damage path in `ink.tsx` +- `getScrollHint()` / `resetScrollHint()` — DECSTBM hardware scroll hint for `LogUpdate` +- `getScrollDrainNode()` / `resetScrollDrainNode()` — identifies a ScrollBox with remaining `pendingScrollDelta` +- `consumeFollowScroll()` — consumes the at-bottom follow-scroll event for selection adjustment + +**Scroll drain parameters:** +``` +SCROLL_MIN_PER_FRAME = 4 // minimum rows applied per frame +SCROLL_INSTANT_THRESHOLD = 5 // ≤ this: drain all at once (xterm.js wheel click) +SCROLL_HIGH_PENDING = 12 // threshold for high-speed drain +SCROLL_STEP_MED = 2 // medium pending drain step +SCROLL_STEP_HIGH = 3 // high pending drain step +``` + +**ScrollBox rendering (three-pass algorithm):** +1. **First pass:** compute scrollHeight from yoga, apply `pendingScrollDelta` (proportional drain), handle `stickyScroll`, detect `scrollAnchor` +2. **Second pass (blit path):** when content is unchanged and layout hasn't shifted, blit the scrollbox from `prevScreen` and emit a hardware `shiftRows` hint +3. **Third pass (full render):** render children with `clip` and `y-offset = -scrollTop`; record `absoluteRectsCur` for position:absolute descendants + +**Blit optimization:** When a node's bounding box matches `nodeCache` and the node is not dirty, the renderer blits from `prevScreen` instead of re-rendering. The condition is: `!dirty && prevScreen && !hasRemovedChild && !layoutShifted`. This makes steady-state frames O(changed cells). + +**`nodeCache` updates:** After rendering each node, `nodeCache.set(node, { x, y, width, height })` records the screen-space bounding box for hit-testing and blit reuse. + +### `/x/Bigger-Projects/Claude-Code/src/ink/output.ts` + +**Purpose:** Collects rendering operations (write, blit, clip, clear, no-select, shift) and applies them to a `Screen` buffer in `get()`. + +**`Operation` union type:** +```typescript +type Operation = + | WriteOperation // { type: 'write', x, y, text, softWrap? } + | ClipOperation // { type: 'clip', clip: Clip } + | UnclipOperation // { type: 'unclip' } + | BlitOperation // { type: 'blit', srcScreen, srcRect, dstX, dstY } + | ClearOperation // { type: 'clear', x, y, width, height } + | NoSelectOperation // { type: 'noSelect', x, y, width, height } + | ShiftOperation // { type: 'shift', top, bottom, delta } +``` + +**`Clip` type:** `{ x1?, x2?, y1?, y2? }` — undefined on an axis means unbounded. Clips are intersected when nested. + +**Write operation processing:** +The `WriteOperation` handler is the hot path. For each character: +1. Tokenize the text (ANSI codes) via `@alcalzone/ansi-tokenize` +2. Build a `ClusteredChar[]` array (grapheme + width + styleId + hyperlink), cached per unique line via `charCache: Map` +3. Apply bidirectional reordering (`reorderBidi`) on Windows/xterm.js +4. Call `setCellAt` for each grapheme + +**`charCache`:** Keyed by the raw ANSI string line. Cache miss: tokenize + cluster + intern styles. Cache hit: reuse the `ClusteredChar[]` array directly. The cache persists across frames (it lives on the `Output` instance). This is the dominant performance optimization for text-heavy content. + +**Tab expansion:** Tabs in text are expanded to spaces at write time using screen x-position (not at measurement time), so the rendered width matches the measured width. + +### `/x/Bigger-Projects/Claude-Code/src/ink/render-to-screen.ts` + +**Purpose:** Off-screen renderer used for search scanning. Renders a React element to an isolated `Screen` buffer at a given width without writing to the terminal. Used to pre-scan message content for search match positions. + +**`renderToScreen(el, width)`:** Returns `{ screen: Screen; height: number }`. Uses a shared persistent root/container/pools (LegacyRoot mode for synchronous rendering) so repeated calls reuse Yoga nodes. Unmounts between calls to free resources. + +**`scanPositions(screen, query)`:** Scans a rendered screen for all occurrences of `query`, returning `MatchPosition[]` with `{ row, col, len }` in message-relative coordinates. + +**`applyPositionedHighlight(screen, positions, rowOffset, currentIdx, stylePool)`:** Applies the current-match yellow/bold/underline style to the cell range at `positions[currentIdx]` and inverse style to all other positions. + +### `/x/Bigger-Projects/Claude-Code/src/ink/frame.ts` + +**Purpose:** Defines the `Frame` type and the diff/patch type hierarchy. + +**`Frame` type:** +```typescript +type Frame = { + readonly screen: Screen + readonly viewport: Size + readonly cursor: Cursor + readonly scrollHint?: ScrollHint | null + readonly scrollDrainPending?: boolean +} +``` + +**`Patch` union type:** +```typescript +type Patch = + | { type: 'stdout'; content: string } + | { type: 'clear'; count: number } + | { type: 'clearTerminal'; reason: FlickerReason; debug?: {...} } + | { type: 'cursorHide' } + | { type: 'cursorShow' } + | { type: 'cursorMove'; x: number; y: number } + | { type: 'cursorTo'; col: number } + | { type: 'carriageReturn' } + | { type: 'hyperlink'; uri: string } + | { type: 'styleStr'; str: string } + +type Diff = Patch[] +``` + +**`shouldClearScreen(prevFrame, frame)`:** Returns `'resize' | 'offscreen' | undefined`: +- `'resize'` — viewport dimensions changed +- `'offscreen'` — current or previous screen height exceeds viewport rows + +**`FlickerReason`:** `'resize' | 'offscreen' | 'clear'` + +**`FrameEvent`:** Timing breakdown emitted to `onFrame`: +```typescript +type FrameEvent = { + durationMs: number + phases?: { + renderer: number; diff: number; optimize: number; write: number + patches: number; yoga: number; commit: number + yogaVisited: number; yogaMeasured: number; yogaCacheHits: number; yogaLive: number + } + flickers: Array<{ desiredHeight, availableHeight, reason }> +} +``` + +### `/x/Bigger-Projects/Claude-Code/src/ink/log-update.ts` — Diff Engine + +**Purpose:** Computes a `Diff` (list of `Patch` objects) by comparing the new `Frame` to the previous frame, or handles full-screen clears when needed. + +**`LogUpdate` class:** + +Constructor takes `{ isTTY, stylePool }`. Maintains `previousOutput: string` (deprecated legacy string tracking). + +Key methods: +- `render(prevFrame, frame)` — main diff entry point. If `shouldClearScreen()` triggers, prepends a `clearTerminal` patch. Otherwise runs the incremental diff algorithm +- `renderPreviousOutput_DEPRECATED(prevFrame)` — used for final output on exit (writes the last frame to terminal) +- `reset()` — clears `previousOutput` (called after SIGCONT) + +**Incremental diff algorithm (`renderDiff`):** +1. Walk rows top-down, comparing `prevScreen` and `screen` cell-by-cell via `diffEach` +2. For unchanged rows: emit cursor moves to skip them +3. For changed rows: emit `styleStr` transitions + `stdout` content patches + `hyperlink` patches +4. Handle wide chars: skip `SpacerTail` cells; emit a space for `SpacerHead` (end-of-line wrap guard) +5. Track hyperlink state across rows (emit `LINK_END` when hyperlink changes) +6. After last row: position cursor per `frame.cursor` + +**DECSTBM hardware scroll (`scrollHint`):** +When the back frame has a `scrollHint` and no layout shift occurred and the viewport can accommodate the shift: +- Emit `setScrollRegion(top, bottom)` (DECSTBM) +- Emit `scrollUp(n)` (CSI S) or `scrollDown(n)` (CSI T) +- Emit `RESET_SCROLL_REGION` +- Only repaint the rows that changed due to the scroll (a narrow repair band) + +This replaces O(viewport) cell writes with O(scrolled region) writes for smooth scroll in fullscreen mode. + +### `/x/Bigger-Projects/Claude-Code/src/ink/optimizer.ts` + +**Purpose:** Single-pass patch list optimizer. + +**`optimize(diff)`:** Rules applied: +- Remove empty `stdout` patches +- Remove no-op `cursorMove(0,0)` patches +- Remove `clear` patches with count 0 +- **Merge** consecutive `cursorMove` patches (add x/y) +- **Collapse** consecutive `cursorTo` patches (keep last) +- **Concat** adjacent `styleStr` patches +- **Deduplicate** consecutive `hyperlink` patches with same URI +- **Cancel** adjacent `cursorHide`/`cursorShow` pairs + +### `/x/Bigger-Projects/Claude-Code/src/ink/node-cache.ts` + +**Purpose:** Stores the rendered bounding box for each `DOMElement`, used for blit optimization and hit-testing. + +**Exports:** +```typescript +type CachedLayout = { x: number; y: number; width: number; height: number; top?: number } +const nodeCache = new WeakMap() +const pendingClears = new WeakMap() + +addPendingClear(parent, rect, isAbsolute): void +consumeAbsoluteRemovedFlag(): boolean +``` + +`pendingClears` holds rectangles of removed children that must be painted over on the next render. `absoluteNodeRemoved` gates the global blit disable path for absolute-positioned removals. + +--- + +## Terminal I/O + +### `/x/Bigger-Projects/Claude-Code/src/ink/terminal.ts` + +**Purpose:** Terminal capability detection and the `writeDiffToTerminal` serializer. + +**`Terminal` type:** `{ stdout: NodeJS.WriteStream; stderr: NodeJS.WriteStream }` + +**Capability detection:** +- `isSynchronizedOutputSupported()` — returns `true` for iTerm2, WezTerm, Warp, ghostty, kitty, VS Code, alacritty, foot, etc. Returns `false` for tmux (parses but doesn't implement DEC 2026 properly) +- `isProgressReportingAvailable()` — returns `true` for ConEmu, Ghostty 1.2.0+, iTerm2 3.6.6+; excludes Windows Terminal (interprets OSC 9;4 as notifications) +- `supportsExtendedKeys()` — detects Kitty keyboard protocol or modifyOtherKeys support +- `isXtermJs()` — set by XTVERSION probe (survives SSH, unlike TERM_PROGRAM) +- `setXtversionName(name)` — called by `App.tsx` after terminal query response + +**`writeDiffToTerminal(terminal, diff, syncOutput)`:** +Serializes a `Diff` into ANSI sequences and writes to `terminal.stdout`. If `syncOutput === SYNC_OUTPUT_SUPPORTED`, wraps in BSU/ESU (DEC 2026 synchronized output). The serialization is a tight loop over the `Patch` array: +- `stdout`: write content directly +- `clear`: `eraseLines(count)` — moves cursor up and erases +- `clearTerminal`: `getClearTerminalSequence()` — erase screen + scrollback +- `cursorHide` / `cursorShow`: emit HIDE/SHOW_CURSOR sequences +- `cursorMove`: emit `cursorMove(x, y)` relative move +- `cursorTo`: emit `cursorTo(col)` absolute column +- `carriageReturn`: emit `\r` +- `hyperlink`: emit `link(uri)` or `LINK_END` +- `styleStr`: write pre-serialized ANSI transition string directly + +`SYNC_OUTPUT_SUPPORTED` constant is computed once at module load. + +--- + +## Termio Layer + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio.ts` — Termio Public API + +Re-exports: +- `Parser` from `termio/parser.ts` +- All types: `Action`, `Color`, `CursorAction`, `CursorDirection`, `EraseAction`, `Grapheme`, `LinkAction`, `ModeAction`, `NamedColor`, `ScrollAction`, `TextSegment`, `TextStyle`, `TitleAction`, `UnderlineStyle` +- `colorsEqual`, `defaultStyle`, `stylesEqual` + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio/types.ts` + +**Purpose:** Semantic type definitions for all ANSI parser output. + +**Key types:** +```typescript +// 16-color palette +type NamedColor = 'black' | 'red' | ... | 'brightWhite' + +// 3-way color union +type Color = + | { type: 'named'; name: NamedColor } + | { type: 'indexed'; index: number } // 0-255 + | { type: 'rgb'; r: number; g: number; b: number } + | { type: 'default' } + +type UnderlineStyle = 'none' | 'single' | 'double' | 'curly' | 'dotted' | 'dashed' + +type TextStyle = { + bold: boolean; dim: boolean; italic: boolean; underline: UnderlineStyle + blink: boolean; inverse: boolean; hidden: boolean; strikethrough: boolean + overline: boolean; fg: Color; bg: Color; underlineColor: Color +} + +// All parsed actions +type Action = + | { type: 'text'; graphemes: Grapheme[]; style: TextStyle } + | { type: 'cursor'; action: CursorAction } + | { type: 'erase'; action: EraseAction } + | { type: 'scroll'; action: ScrollAction } + | { type: 'mode'; action: ModeAction } + | { type: 'link'; action: LinkAction } + | { type: 'title'; action: TitleAction } + | { type: 'tabStatus'; action: TabStatusAction } + | { type: 'sgr'; params: string } + | { type: 'bell' } + | { type: 'reset' } + | { type: 'unknown'; sequence: string } +``` + +**`TabStatusAction`:** `{ indicator?: Color | null; status?: string | null; statusColor?: Color | null }` — for OSC 21337 tab chrome metadata. + +Utility functions: `defaultStyle()`, `stylesEqual(a, b)`, `colorsEqual(a, b)`. + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio/ansi.ts` + +**Purpose:** Base ANSI constants and C0 control character codes. + +**Exports:** +- `C0` object — complete C0 control character table (NUL through DEL) +- `ESC = '\x1b'`, `BEL = '\x07'`, `SEP = ';'` +- `ESC_TYPE` — escape sequence introducers: `CSI=0x5b, OSC=0x5d, DCS=0x50, APC=0x5f, PM=0x5e, SOS=0x58, ST=0x5c` +- `isC0(byte)`, `isEscFinal(byte)` + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio/csi.ts` + +**Purpose:** CSI (Control Sequence Introducer) sequence generation and constants. + +**Exports:** +- `CSI_PREFIX = ESC + '['` +- `CSI_RANGE` — parameter/intermediate/final byte ranges +- `isCSIParam`, `isCSIIntermediate`, `isCSIFinal` +- `csi(...args)` — sequence generator: `ESC [ params... final` +- `CSI` enum — final byte codes: `CUU=0x41(A), CUD=0x42(B), CUF=0x43(C), CUB=0x44(D), CNL=0x45(E), CPL=0x46(F), CHA=0x47(G), CUP=0x48(H), ED=0x4a(J), EL=0x4b(K), SU=0x53(S), SD=0x54(T), SGR=0x6d(m), DECSTBM=0x72(r), ...` +- Pre-generated sequences: `CURSOR_HOME`, `ERASE_SCREEN`, `ERASE_SCROLLBACK`, `RESET_SCROLL_REGION`, `PASTE_START`, `PASTE_END`, `FOCUS_IN`, `FOCUS_OUT`, `ENABLE_KITTY_KEYBOARD`, `DISABLE_KITTY_KEYBOARD`, `ENABLE_MODIFY_OTHER_KEYS`, `DISABLE_MODIFY_OTHER_KEYS` +- Parameterized: `cursorMove(x,y)`, `cursorTo(col)`, `cursorPosition(row,col)`, `eraseLines(count)`, `setScrollRegion(top,bottom)`, `scrollUp(n)`, `scrollDown(n)` + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio/dec.ts` + +**Purpose:** DEC private mode sequence generation. + +**Exports:** +- `DEC` enum — mode numbers: `CURSOR_VISIBLE=25, ALT_SCREEN=47, ALT_SCREEN_CLEAR=1049, MOUSE_NORMAL=1000, MOUSE_BUTTON=1002, MOUSE_ANY=1003, MOUSE_SGR=1006, FOCUS_EVENTS=1004, BRACKETED_PASTE=2004, SYNCHRONIZED_UPDATE=2026` +- `decset(mode)` / `decreset(mode)` — `CSI ? N h` / `CSI ? N l` +- Pre-generated: `BSU` (begin synchronized update), `ESU`, `EBP/DBP` (bracketed paste), `EFE/DFE` (focus events), `SHOW_CURSOR/HIDE_CURSOR`, `ENTER_ALT_SCREEN/EXIT_ALT_SCREEN` +- `ENABLE_MOUSE_TRACKING` — combination of 1000+1002+1003+1006 set +- `DISABLE_MOUSE_TRACKING` — reverse order reset + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio/osc.ts` + +**Purpose:** OSC (Operating System Command) sequence generation and clipboard/tab-status support. + +**Exports:** +- `OSC_PREFIX = ESC + ']'`, `ST = ESC + '\\'` +- `osc(...parts)` — generates `ESC ] parts BEL` (or ST for Kitty) +- `wrapForMultiplexer(sequence)` — wraps in tmux DCS passthrough (`ESC P tmux ; ... ESC \`) or GNU screen DCS if `TMUX`/`STY` env vars set +- `link(url)` / `LINK_END` — OSC 8 hyperlink start/end +- `setClipboard(text)` — OSC 52 + optional pbcopy/tmux load-buffer; returns `ClipboardPath` +- `getClipboardPath()` — `'native' | 'tmux-buffer' | 'osc52'` without side effects +- `tmuxLoadBuffer(text)` — async: runs `tmux load-buffer [-w] -` +- `tabStatus({indicator, status, statusColor})` / `CLEAR_TAB_STATUS` — OSC 21337 tab chrome +- `supportsTabStatus()` — detects iTerm2 / Claude Code terminal from env +- `CLEAR_ITERM2_PROGRESS` — clears iTerm2 progress bar + +**Clipboard path decision:** +- `native`: macOS + no SSH_CONNECTION → use `pbcopy` +- `tmux-buffer`: inside tmux → use `tmux load-buffer [-w]` +- `osc52`: fallback → write OSC 52 raw sequence + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio/sgr.ts` + +**Purpose:** SGR (Select Graphic Rendition) parameter parser. + +**`applySGR(paramStr, style)`:** Parses semicolon/colon separated SGR params and mutates a `TextStyle`. Handles: +- SGR 0: reset +- SGR 1/2/3/4/5/7/8/9/53: bold/dim/italic/underline/blink/inverse/hidden/strikethrough/overline +- SGR 21/22/23/24/25/27/28/29/55: attribute reset +- SGR 30-37/90-97: named fg colors +- SGR 40-47/100-107: named bg colors +- SGR 38/48: extended fg/bg (256-indexed via `5` or truecolor via `2`) +- SGR 39/49: default fg/bg +- SGR 58/59: underline color +- Kitty extended underline styles (SGR 4:1-4:5) + +Colon-separated subparams take priority over semicolon-separated for extended colors. + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio/esc.ts` + +**Purpose:** ESC (non-CSI, non-OSC) sequence parser. Handles `ESC c` (full reset), `ESC 7`/`ESC 8` (save/restore cursor), and other two-byte sequences. + +**`parseEsc(sequence)`:** Returns an `Action | null`. + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio/tokenize.ts` + +**Purpose:** Streaming tokenizer for terminal input — splits raw bytes into text chunks and escape sequences without interpreting semantics. + +**States:** `ground`, `escape`, `escapeIntermediate`, `csi`, `ss3`, `osc`, `dcs`, `apc` + +**`Token` type:** `{ type: 'text'; value: string } | { type: 'sequence'; value: string }` + +**`Tokenizer` interface:** +```typescript +{ + feed(input: string): Token[] + flush(): Token[] + reset(): void + buffer(): string +} +``` + +**`createTokenizer(options?)`:** +- `options.x10Mouse` — enables X10 legacy mouse event parsing (consume 3 extra bytes after `CSI M`) +- Maintains incremental state across `feed()` calls for streaming input + +**Algorithm:** Character-by-character state machine. `ground` state: text passes through until `ESC` or C0 control chars. `csi` state: accumulates until CSI final byte (0x40–0x7e). `osc`/`dcs`/`apc` states: accumulate until BEL or ST. + +### `/x/Bigger-Projects/Claude-Code/src/ink/termio/parser.ts` + +**Purpose:** Semantic parser that wraps the tokenizer and interprets each sequence into a structured `Action`. + +**`Parser` class:** +```typescript +class Parser { + feed(input: string): Action[] + flush(): Action[] + reset(): void + getStyle(): TextStyle +} +``` + +**Internal structure:** +- Holds a `Tokenizer` with `x10Mouse: true` +- Maintains current `TextStyle` (updated by SGR actions) +- Calls `parseCSI`, `parseEsc`, `parseOSC` for sequence tokens +- Calls `segmentGraphemes` for text tokens + +**Grapheme width detection:** +- `isEmoji(codePoint)` — ranges: 0x2600-0x26ff, 0x2700-0x27bf, 0x1F300-0x1F9FF, 0x1FA00-0x1FAFF, 0x1F1E0-0x1F1FF +- `isEastAsianWide(codePoint)` — standard CJK/Hangul ranges +- `graphemeWidth(grapheme)` — returns 1 or 2 +- `segmentGraphemes(str)` — uses `Intl.Segmenter` to split by grapheme cluster + +**`parseCSI(rawSequence)`:** Dispatches by final byte: +- `m` (SGR): `{ type: 'sgr', params }` +- Cursor movement (A-H, d, f): `{ type: 'cursor', action }` +- Erase (J, K, X): `{ type: 'erase', action }` +- Scroll (S, T): `{ type: 'scroll', action }` +- DEC private modes (h/l with `?` prefix): `{ type: 'mode', action }` +- Mouse events (M/m with `<` prefix): decoded SGR mouse + +--- + +## Event System + +### `/x/Bigger-Projects/Claude-Code/src/ink/events/event.ts` + +**Purpose:** Base `Event` class providing `stopImmediatePropagation()`. + +```typescript +class Event { + stopImmediatePropagation(): void + didStopImmediatePropagation(): boolean +} +``` + +### `/x/Bigger-Projects/Claude-Code/src/ink/events/terminal-event.ts` + +**Purpose:** `TerminalEvent` extends `Event` with DOM-style propagation properties. + +**`EventTarget` type:** `{ parentNode: EventTarget | undefined; _eventHandlers?: Record }` + +**`TerminalEvent` properties:** +- `type: string`, `timeStamp: number`, `bubbles: boolean`, `cancelable: boolean` +- `target: EventTarget | null`, `currentTarget: EventTarget | null` +- `eventPhase: 'none' | 'capturing' | 'at_target' | 'bubbling'` +- `defaultPrevented: boolean` + +**Methods:** `stopPropagation()`, `stopImmediatePropagation()` (overrides base), `preventDefault()` + +Internal setters: `_setTarget`, `_setCurrentTarget`, `_setEventPhase`, `_isPropagationStopped()`, `_isImmediatePropagationStopped()`, `_prepareForTarget(target)` (hook for subclasses) + +### `/x/Bigger-Projects/Claude-Code/src/ink/events/dispatcher.ts` + +**Purpose:** Two-phase (capture + bubble) event dispatcher modeled after the browser DOM event model. + +**`Dispatcher` class:** +- `dispatch(target, event)` — full capture+bubble cycle via `collectListeners` + `processDispatchQueue`; runs asynchronously (via `unstable_batchedUpdates` or React's scheduler for `discrete` vs `continuous` events) +- `dispatchDiscrete(target, event)` — discrete priority (keyboard, focus); triggers React's synchronous flush +- Internal `collectListeners(target, event)` — walks from target to root, prepending capture handlers (root-first) and appending bubble handlers (target-first); result: `[root-cap, ..., target-cap, target-bub, ..., root-bub]` +- `processDispatchQueue(listeners, event)` — calls each handler, checking `_isPropagationStopped()` and `_isImmediatePropagationStopped()` before each + +**React event priority mapping:** +- Keyboard/focus → `DiscreteEventPriority` +- Mouse motion → `ContinuousEventPriority` +- Other → `DefaultEventPriority` + +### `/x/Bigger-Projects/Claude-Code/src/ink/events/event-handlers.ts` + +**Purpose:** Defines the complete set of event handler props and the reverse lookup table. + +**`EventHandlerProps`:** +```typescript +{ + onKeyDown?, onKeyDownCapture?: KeyboardEventHandler + onFocus?, onFocusCapture?, onBlur?, onBlurCapture?: FocusEventHandler + onPaste?, onPasteCapture?: PasteEventHandler + onResize?: ResizeEventHandler + onClick?: ClickEventHandler + onMouseEnter?, onMouseLeave?: HoverEventHandler +} +``` + +**`HANDLER_FOR_EVENT`:** Maps event type strings to `{ bubble?, capture? }` prop name pairs. Used by `Dispatcher` for O(1) handler lookup. + +**`EVENT_HANDLER_PROPS`:** `Set` of all handler prop names; used by the reconciler to route event props to `_eventHandlers` instead of `attributes`. + +### `/x/Bigger-Projects/Claude-Code/src/ink/events/keyboard-event.ts` + +**`KeyboardEvent extends TerminalEvent`:** +- Constructor takes a `ParsedKey`; type = `'keydown'`; `bubbles = true`; `cancelable = true` +- `key: string` — printable char for printable keys; multi-char name for specials (`'down'`, `'return'`, `'escape'`, `'f1'`, etc.) +- `ctrl, shift, meta, superKey, fn: boolean` + +Key extraction: ctrl keys use the name (letter); single printable ASCII chars use the literal char; special keys use the parsed name. + +### `/x/Bigger-Projects/Claude-Code/src/ink/events/click-event.ts` + +**`ClickEvent extends Event`:** +- `col: number`, `row: number` — 0-indexed screen coordinates +- `localCol: number`, `localRow: number` — coordinates relative to the current handler's Box (updated by `dispatchClick` before each handler fires) +- `cellIsBlank: boolean` — true if the cell had no content (allows handlers to ignore clicks on empty space) + +### Other event types: + +**`InputEvent`** (events/input-event.ts): Legacy input event emitted on stdin data; carries `input: string` and `Key` object. `Key` type: `{ upArrow, downArrow, leftArrow, rightArrow, pageUp, pageDown, return, escape, ctrl, shift, tab, backspace, delete, meta, fn }`. + +**`FocusEvent`** (events/focus-event.ts): `type = 'focus' | 'blur'`; `relatedTarget: DOMElement | null` + +**`TerminalFocusEvent`** (events/terminal-focus-event.ts): `type: TerminalFocusEventType = 'terminal-focus-in' | 'terminal-focus-out'`; fired when DECSET 1004 focus events arrive. + +**`EventEmitter`** (events/emitter.ts): Simple typed event emitter. `on(event, handler)`, `off(event, handler)`, `emit(event, ...args)`. Used by `App.tsx` for stdin data events. + +--- + +## Focus Management + +### `/x/Bigger-Projects/Claude-Code/src/ink/focus.ts` + +**`FocusManager` class:** + +Stored on `ink-root` node so any node can reach it by walking `parentNode`. + +State: +- `activeElement: DOMElement | null` +- `focusStack: DOMElement[]` — history for focus restoration (max 32 entries) +- `enabled: boolean` + +Methods: +- `focus(node)` — blur previous, push to stack, focus new node; dispatches `focus`/`blur` events +- `blur()` — blur `activeElement`, dispatches `blur` event +- `handleNodeRemoved(node, root)` — removes node from stack, restores focus from stack if `activeElement` was in the removed subtree +- `handleClickFocus(node)` — called by `dispatchClick`; focuses the nearest focusable ancestor +- `getNextFocusable(root, direction)` — Tab/Shift+Tab cycling; collects all nodes with `tabIndex >= 0`, sorts by order, returns next/previous +- `enable()` / `disable()` — gates all focus operations + +**`getFocusManager(node)`** / **`getRootNode(node)`** — utility functions exported for the reconciler. + +--- + +## Input Parsing + +### `/x/Bigger-Projects/Claude-Code/src/ink/parse-keypress.ts` + +**Purpose:** Parses raw terminal stdin bytes into structured `ParsedKey` objects. Handles standard ANSI sequences, Kitty keyboard protocol (CSI u), xterm modifyOtherKeys, SGR mouse events, and terminal response sequences. + +**`ParsedKey` type:** +```typescript +{ + kind: 'key' | 'mouse' | 'terminalResponse' + name: string // key name: 'up', 'down', 'return', 'escape', 'f1', 'a', ... + fn: boolean + ctrl: boolean; meta: boolean; shift: boolean; option: boolean; super: boolean + sequence: string // raw escape sequence + raw: string // original input bytes + isPasted?: boolean +} +``` + +**`ParsedMouse` type:** +```typescript +{ + kind: 'mouse' + button: number // SGR button code + col: number; row: number // 1-indexed + press: boolean // true = press, false = release + isWheel: boolean + isDrag: boolean + modifiers: { ctrl, shift, meta, alt } +} +``` + +**`ParsedInput = ParsedKey | ParsedMouse | TerminalResponse`** + +**Regex patterns:** +- `META_KEY_CODE_RE`: `ESC + [a-zA-Z0-9]` — meta key combos +- `FN_KEY_RE`: SS3/CSI function key sequences +- `CSI_U_RE`: Kitty protocol `ESC [ codepoint [;modifier] u` +- `MODIFY_OTHER_KEYS_RE`: xterm `ESC [ 27 ; modifier ; keycode ~` +- `DECRPM_RE`, `DA1_RE`, `DA2_RE`, `KITTY_FLAGS_RE`, `CURSOR_POSITION_RE` — terminal response patterns +- `OSC_RESPONSE_RE`: OSC sequence responses +- `XTVERSION_RE`: DCS `>|` terminal name/version +- `SGR_MOUSE_RE`: `ESC [ < btn ; col ; row M/m` + +**`parseMultipleKeypresses(buffer, state)`:** Main entry point. Uses the tokenizer to split the input, then dispatches each token to the appropriate parser. Handles bracketed paste (accumulates until `PASTE_END`). + +**`INITIAL_STATE`:** Initial parser state for `parseMultipleKeypresses`. + +--- + +## Hit Testing + +### `/x/Bigger-Projects/Claude-Code/src/ink/hit-test.ts` + +**Purpose:** Mouse click hit-testing against the DOM tree. + +**`hitTest(node, col, row)`:** DFS in reverse child order (last child = top paint layer wins). Uses `nodeCache` for bounding-box lookups. Returns the deepest `DOMElement` whose rendered rect contains `(col, row)`, or `null`. + +**`dispatchClick(root, col, row, cellIsBlank?)`:** +1. Runs `hitTest` to find the deepest hit node +2. Calls `focusManager.handleClickFocus()` to click-to-focus the nearest focusable ancestor +3. Creates a `ClickEvent(col, row, cellIsBlank)` +4. Bubbles up via `parentNode` chain, calling `onClick` handlers +5. Before each handler: sets `event.localCol/localRow` relative to the handler's bounding rect +6. Stops on `stopImmediatePropagation()` +7. Returns `true` if any handler fired + +**`dispatchHover(root, col, row, hoveredNodes)`:** Diff-based hover dispatch. Finds the set of nodes hit at `(col, row)`, fires `onMouseEnter` for newly-entered nodes and `onMouseLeave` for exited nodes. Mutates `hoveredNodes` in place (owned by the `Ink` instance). + +--- + +## Text Selection + +### `/x/Bigger-Projects/Claude-Code/src/ink/selection.ts` + +**Purpose:** Linear text selection in screen-buffer coordinates for fullscreen mode. + +**`SelectionState` type:** +```typescript +{ + anchor: { col, row } | null + focus: { col, row } | null + isDragging: boolean + anchorSpan: { lo, hi, kind: 'word' | 'line' } | null // word/line mode + scrolledOffAbove: string[] // rows that scrolled off the top + scrolledOffBelow: string[] + scrolledOffAboveSW: boolean[] // soft-wrap flags parallel to scrolledOffAbove + scrolledOffBelowSW: boolean[] + virtualAnchorRow?: number // pre-clamp anchor row (for scroll restore) + virtualFocusRow?: number + lastPressHadAlt: boolean +} +``` + +**Exported functions:** +- `createSelectionState()` — returns zeroed state +- `startSelection(s, col, row, alt?)` — initializes anchor and focus +- `updateSelection(s, col, row)` — updates focus during drag +- `finishSelection(s)` — clears `isDragging` +- `clearSelection(s)` — resets to empty +- `hasSelection(s)` — returns true if `anchor !== null && focus !== null` +- `getSelectedText(s, screen)` — extracts the selected text; handles soft-wrap (joins wrapped lines), wide chars (skips `SpacerTail`), `noSelect` regions (excluded), and `scrolledOffAbove`/`scrolledOffBelow` accumulators +- `applySelectionOverlay(s, screen, stylePool)` — inverts cell styles in the selected region +- `selectWordAt(s, col, row, screen)` — double-click: select the word under the cursor +- `selectLineAt(s, row, screen)` — triple-click: select the entire line +- `extendSelection(s, col, row, screen)` — word/line-mode drag extension: extends to word/line boundaries +- `shiftSelection(s, dRow, min, max, screenWidth)` — keyboard scroll: shifts both anchor and focus +- `shiftAnchor(s, dRow, min, max)` — shift anchor only (keyboard selection extension) +- `moveFocus(s, move, screen)` — keyboard character/line focus extension +- `captureScrolledRows(s, firstRow, lastRow, side, screen)` — saves rows about to scroll off into `scrolledOffAbove`/`scrolledOffBelow` +- `shiftSelectionForFollow(s, delta, screen)` — called after follow-scroll to keep selection anchored to text + +--- + +## Search Highlighting + +### `/x/Bigger-Projects/Claude-Code/src/ink/searchHighlight.ts` + +**`applySearchHighlight(screen, query, stylePool)`:** +- Case-insensitive scan of the screen buffer +- Builds per-row char text + `codeUnitToCell` index map (handles wide chars) +- For each match: calls `setCellStyleId(screen, ..., stylePool.withInverse(cellStyleId))` to invert the cell style +- Skips `noSelect` regions (gutters, line numbers) +- Returns `true` if any match was applied (triggers full-damage flag in caller) + +The `applyPositionedHighlight` (in `render-to-screen.ts`) handles the "current match" in yellow, on top of `applySearchHighlight`'s inverse. + +--- + +## Component Library + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/App.tsx` + +**Purpose:** The root React component. Wires together stdin input, terminal size context, focus, clock, error boundaries, and all context providers. + +**Props:** `children, stdin, stdout, stderr, exitOnCtrlC, onExit, terminalColumns, terminalRows, selection, onSelectionChange, onClickAt, onHoverAt, getHyperlinkAt, onOpenHyperlink, onMultiClick, onSelectionDrag, onStdinResume?, onCursorDeclaration` + +**Responsibilities:** +- Provides `AppContext`, `StdinContext`, `TerminalSizeContext`, `ClockContext`, `TerminalFocusProvider`, `CursorDeclarationContext`, `TerminalWriteProvider` +- Listens to stdin data; calls `parseMultipleKeypresses` for each chunk +- Dispatches `KeyboardEvent` through the DOM via `dispatcher.dispatchDiscrete(rootNode, event)` +- Handles mouse events: left-button selection start/update/finish; wheel → `pendingScrollDelta`; hover → `onHoverAt`; click → `onClickAt` +- Handles pasted content via bracketed paste markers +- Detects terminal focus via DECSET 1004 `FOCUS_IN`/`FOCUS_OUT` sequences +- Runs `TerminalQuerier` on mount to probe XTVERSION and extended key support +- Re-asserts terminal modes after `STDIN_RESUME_GAP_MS = 5000` ms of stdin silence +- Handles `Ctrl+C` → `onExit` when `exitOnCtrlC = true` +- Handles `Ctrl+Z` (SIGTSTP) on non-Windows platforms + +**Class component** (`PureComponent`) for stable reference identity. All mouse/keyboard state is imperative (refs), not React state, to avoid re-renders. + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/Box.tsx` + +**Purpose:** The primary layout container. Analogous to `
`. + +**`Props`:** All `Styles` properties (except `textWrap`) plus: +- `ref?: Ref` +- `tabIndex?: number` — focus order (>= 0 participates in Tab cycling, -1 = programmatic only) +- `autoFocus?: boolean` — focus on mount +- `onClick?: (event: ClickEvent) => void` +- `onFocus?, onFocusCapture?, onBlur?, onBlurCapture?` +- `onKeyDown?, onKeyDownCapture?` +- `onMouseEnter?, onMouseLeave?` + +Renders to `` host element. Compiled with React Compiler (memo cache `_c(42)`). + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/Text.tsx` + +**Purpose:** Renders styled text. Wraps children in `ink-text` and `ink-virtual-text` elements. + +**`Props`:** +- `color?, backgroundColor?` +- `bold?, dim?` (mutually exclusive via TypeScript union) +- `italic?, underline?, strikethrough?, inverse?` +- `wrap?: Styles['textWrap']` +- `children?: ReactNode` + +Text styling props are mapped to `TextStyles` and passed as the `textStyles` prop on the host element. Layout props (flexDirection, flexGrow, etc.) are mapped to `Styles` on the `ink-text` node. + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/ScrollBox.tsx` + +**Purpose:** A Box with imperative scroll API and viewport culling. + +**`ScrollBoxHandle`:** +```typescript +{ + scrollTo(y): void + scrollBy(dy): void + scrollToElement(el, offset?): void // defers position read to render time + scrollToBottom(): void + getScrollTop(): number + getPendingDelta(): number + getScrollHeight(): number + getFreshScrollHeight(): number // reads Yoga directly + getViewportHeight(): number + getViewportTop(): number + isSticky(): boolean + subscribe(listener): () => void + setClampBounds(min, max): void +} +``` + +**`ScrollBoxProps`:** All `Styles` except `overflow`/`overflowX`/`overflowY`, plus `stickyScroll?: boolean`. + +Implementation: Sets `overflow: 'scroll'` on the underlying Box. Scroll mutations call `markDirty` + `scheduleRenderFrom` to trigger an Ink frame without going through React's reconciler. `scrollToElement` stores a `scrollAnchor` on the DOM node; `render-node-to-output` reads it at paint time (after Yoga has computed the element's position) and clears it. + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/AlternateScreen.tsx` + +**Purpose:** Enters the terminal's alternate screen buffer for fullscreen rendering. + +**Props:** `children, mouseTracking?: boolean` (default `true`) + +Uses `useInsertionEffect` (not `useLayoutEffect`) to send `ENTER_ALT_SCREEN` before the first Ink render frame. On cleanup (unmount), sends `EXIT_ALT_SCREEN`. Calls `instances.get(process.stdout).setAltScreenActive(true/false)` to coordinate with the Ink instance. Renders children inside a `Box` constrained to `terminalRows` height. + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/Link.tsx` + +**Purpose:** Renders OSC 8 terminal hyperlinks. + +Wraps children in an `ink-link` host element with `href` attribute. During rendering, `squashTextNodesToSegments` propagates the hyperlink URL to contained text segments. + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/RawAnsi.tsx` + +**Purpose:** Renders pre-formatted ANSI strings with known dimensions. + +Props: `children: string, width: number, height: number` + +Renders to `ink-raw-ansi` element with `rawWidth`/`rawHeight` attributes. The yoga measure function reads these dimensions directly (no string width measurement, no wrapping). + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/NoSelect.tsx` + +**Purpose:** Marks a rectangular region as non-selectable (e.g., gutters, line numbers). + +Sets the `noSelect` flag on cells in the screen buffer via `markNoSelectRegion`. Text in these cells is excluded from selection copy and search highlighting. + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/Newline.tsx` + +**Purpose:** Renders `\n` characters (count configurable via `count` prop). + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/Spacer.tsx` + +**Purpose:** Flexible spacer. Renders a `Box` with `flexGrow: 1`. + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/Button.tsx` + +**Purpose:** Focusable, clickable button with keyboard activation. + +**`ButtonState`:** `'idle' | 'active' | 'focus'` + +**Props:** `children, onPress?, disabled?` plus styling. + +Uses `useApp` for exit integration. Handles Return/Space keydown when focused. + +### `/x/Bigger-Projects/Claude-Code/src/ink/components/ErrorOverview.tsx` + +**Purpose:** Error boundary overlay shown when a React component throws. Renders the stack trace and error message with formatting. + +### Context components: + +**`AppContext.ts`** — provides `{ exit(error?): void }`. Consumed by `useApp`. + +**`StdinContext.ts`** — provides `{ stdin, setRawMode, isRawModeSupported, internal_exitOnCtrlC, internal_eventEmitter }`. Consumed by `useStdin`, `useInput`. + +**`TerminalSizeContext.tsx`** — provides `{ columns: number; rows: number } | null`. Consumed by `useTerminalViewport`, `AlternateScreen`. + +**`ClockContext.tsx`** — provides a shared animation clock with `subscribe(cb, keepAlive?)` and `now()`. All `useAnimationFrame` instances share one clock; idle clock (no `keepAlive` subscribers) suspends to avoid waking the process. + +**`CursorDeclarationContext.ts`** — provides a setter for declaring native cursor position. Consumed by `useDeclaredCursor`. The setter signature: `(decl: CursorDeclaration | null, node?: DOMElement | null) => void`. + +**`TerminalFocusContext.tsx`** — provides `useTerminalFocus()` which subscribes to DECSET 1004 focus events via `useSyncExternalStore` on `terminal-focus-state.ts`. + +--- + +## Hooks + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-input.ts` + +**`useInput(handler, options?)`:** Subscribes to stdin input events. Calls `setRawMode(true)` via `useLayoutEffect` (synchronous, before render returns). Subscribes to `internal_eventEmitter` `'input'` events. Options: `{ isActive?: boolean }`. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-app.ts` + +**`default useApp()`:** Returns `{ exit(error?): void }` from `AppContext`. Throws if used outside the App tree. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-stdin.ts` + +**`default useStdin()`:** Returns the full `StdinContext` value. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-animation-frame.ts` + +**`useAnimationFrame(intervalMs?)`:** Returns `[ref, time]`. Subscribes to the shared clock and updates `time` every `intervalMs`. Pauses (unsubscribes) when `intervalMs = null` or the element is off-screen (via `useTerminalViewport`). + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-interval.ts` + +**`useInterval(callback, delay?)`:** Calls `callback` every `delay` ms. Uses the shared clock. + +**`useAnimationTimer(intervalMs)`:** Returns `time` (elapsed ms). Similar to `useAnimationFrame` but without the viewport visibility check. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-terminal-viewport.ts` + +**`useTerminalViewport()`:** Returns `[ref, { isVisible }]`. Computes visibility by walking the DOM ancestor chain (including `scrollTop` offsets) during `useLayoutEffect`. Does NOT cause re-renders on visibility change — callers read the current value naturally. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-terminal-focus.ts` + +**`useTerminalFocus()`:** Returns `boolean` — whether the terminal window is focused. Uses `useSyncExternalStore` on `terminal-focus-state.ts` module-level signal. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-terminal-title.ts` + +**`useTerminalTitle(title)`:** Sets the terminal window title via OSC 0/2 on mount and clears on unmount. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-selection.ts` + +**`useSelection()`:** Returns an API object for text selection operations. Falls back to no-ops when not in fullscreen mode. The `Ink` instance is located via `instances.get(process.stdout)`. + +Methods: `copySelection()`, `copySelectionNoClear()`, `clearSelection()`, `hasSelection()`, `getState()`, `subscribe(cb)`, `shiftAnchor(dRow, min, max)`, `shiftSelection(dRow, min, max)`, `moveFocus(move)`, `captureScrolledRows(first, last, side)`, `setSelectionBgColor(color)`. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-tab-status.ts` + +**`useTabStatus(kind: TabStatusKind | null)`:** Emits OSC 21337 tab status sequences. `kind = 'idle' | 'busy' | 'waiting'`. Transitions to `null` emit `CLEAR_TAB_STATUS`. Wrapped for tmux passthrough. Uses `TerminalWriteContext`. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-declared-cursor.ts` + +**`useDeclaredCursor({ line, column, active })`:** Returns a ref callback. When active, declares the cursor position to the `Ink` instance so the native cursor parks at the text caret. Uses `useLayoutEffect` (no dep array — re-declares every commit) for correct sibling handoff. Clears on unmount via a separate `useLayoutEffect` with empty deps. + +### `/x/Bigger-Projects/Claude-Code/src/ink/hooks/use-search-highlight.ts` + +Internal hook for wiring search query to the Ink instance's `setSearchHighlight` / `setSearchPositions`. + +--- + +## Utility Modules + +### `/x/Bigger-Projects/Claude-Code/src/ink/Ansi.tsx` + +**`Ansi` component:** Parses ANSI escape codes in a string and renders them using `Text` and `Link` components. Memoized. Accepts `children: string` and optional `dimColor: boolean`. Uses `termio.Parser` to extract spans and maps them to `Text` props + `Link` wrappers for hyperlinks. + +### `/x/Bigger-Projects/Claude-Code/src/ink/bidi.ts` + +**`reorderBidi(characters: ClusteredChar[])`:** Applies the Unicode Bidi Algorithm to a `ClusteredChar` array when running on Windows Terminal, WSL, or xterm.js (all lack native bidi). Uses `bidi-js` library. Detects need via `WT_SESSION` env var or `TERM_PROGRAM=vscode`. No-op on platforms with native bidi support. + +### `/x/Bigger-Projects/Claude-Code/src/ink/clearTerminal.ts` + +**`getClearTerminalSequence()`:** Returns `ERASE_SCREEN + ERASE_SCROLLBACK + CURSOR_HOME` on modern terminals. Windows: uses HVP (`ESC [ 0 f`) for cursor home on legacy console; includes scrollback clear for Windows Terminal, VS Code, and mintty. + +**`clearTerminal`:** Pre-computed clear sequence (module load time). + +### `/x/Bigger-Projects/Claude-Code/src/ink/colorize.ts` + +**`colorize(text, styles)`:** Applies chalk-based color/style transforms to a text string. Detects the chalk level and adjusts for xterm.js and tmux environments. + +**`applyTextStyles(text, textStyles)`:** Converts `TextStyles` to chalk method chain calls. + +**Color level management:** +- `boostChalkLevelForXtermJs()` — upgrades chalk to level 3 (truecolor) when `TERM_PROGRAM=vscode` and chalk detected level 2 +- `clampChalkLevelForTmux()` — downgrades to level 2 (256-color) inside tmux to avoid truecolor passthrough bugs; skipped when `CLAUDE_CODE_TMUX_TRUECOLOR=1` + +### `/x/Bigger-Projects/Claude-Code/src/ink/get-max-width.ts` + +**`getMaxWidth(node, offsetWidth?)`:** Computes the available render width for a node accounting for padding and border. Reads from `yogaNode.getComputedPadding`/`getComputedBorder` for each relevant edge. + +### `/x/Bigger-Projects/Claude-Code/src/ink/line-width-cache.ts` + +**`lineWidth(line: string)`:** Memoized string width for individual lines (no newlines). Cache is a `Map`. Used by `measureText`. + +### `/x/Bigger-Projects/Claude-Code/src/ink/measure-element.ts` + +**`measureElement(node: DOMElement)`:** Returns `{ width, height }` by reading `yogaNode.getComputedWidth()/getComputedHeight()`. Throws if the node has no yoga node. + +### `/x/Bigger-Projects/Claude-Code/src/ink/measure-text.ts` + +**`measureText(text, maxWidth)`:** Single-pass computation of `{ width, height }` for a text string. Uses `lineWidth` per line. Height = sum of `ceil(lineWidth / maxWidth)` per line (or 1 when `noWrap`). + +### `/x/Bigger-Projects/Claude-Code/src/ink/squash-text-nodes.ts` + +**`squashTextNodes(node)`:** Concatenates all text content of a node tree into a plain string (no styles). Used by `measureTextNode` in `dom.ts`. + +**`squashTextNodesToSegments(node, inheritedStyles?, inheritedHyperlink?, out?)`:** Walks the text node tree and produces `StyledSegment[]` — text with inherited styles and hyperlink URLs. Used by `output.ts` for structured rendering. + +**`StyledSegment` type:** `{ text: string; styles: TextStyles; hyperlink?: string }` + +### `/x/Bigger-Projects/Claude-Code/src/ink/stringWidth.ts` + +**`stringWidth(str)`:** Terminal display width of a string (counts wide chars as 2, strips ANSI codes). Uses `@alcalzone/ansi-tokenize` + grapheme segmentation. + +### `/x/Bigger-Projects/Claude-Code/src/ink/widest-line.ts` + +**`widestLine(text)`:** Returns the display width of the widest line in a multi-line string. + +### `/x/Bigger-Projects/Claude-Code/src/ink/wrap-text.ts` + +**`wrapText(text, maxWidth, wrapType)`:** Applies the appropriate text wrap strategy: +- `'wrap'`: `wrapAnsi(text, maxWidth, { trim: false, hard: true })` +- `'wrap-trim'`: `wrapAnsi(text, maxWidth, { trim: true, hard: true })` +- `'truncate-end'` / `'truncate'`: append `…` +- `'truncate-middle'`: insert `…` in middle +- `'truncate-start'`: prepend `…` +- `'end'` / `'middle'`: same as corresponding truncate + +Uses `sliceFit` to avoid wide-char boundary errors in slice operations. + +### `/x/Bigger-Projects/Claude-Code/src/ink/wrapAnsi.ts` + +Custom ANSI-aware word-wrap implementation. Handles wide chars, hyperlinks (OSC 8), and soft-wrap tracking. Returns wrapped text plus `softWrap[]` flags. + +### `/x/Bigger-Projects/Claude-Code/src/ink/supports-hyperlinks.ts` + +**`supportsHyperlinks()`:** Detects terminal support for OSC 8 hyperlinks. Returns `true` for iTerm2, VS Code, kitty, Ghostty, WezTerm, Warp, and others. + +### `/x/Bigger-Projects/Claude-Code/src/ink/tabstops.ts` + +**`expandTabs(text, startColumn?)`:** Expands tab characters to spaces based on 8-column tab stops. Used during text measurement (worst-case width). Actual tab expansion at render time uses the screen x-position. + +### `/x/Bigger-Projects/Claude-Code/src/ink/render-border.ts` + +**`renderBorder(node, output, x, y, width, height)`:** Draws box borders using `cli-boxes` glyphs (single, double, round, bold, classic, dashed). Supports `borderText` option to embed text into top/bottom border lines with start/end/center alignment. Colors applied via `chalk`. + +### `/x/Bigger-Projects/Claude-Code/src/ink/terminal-focus-state.ts` + +Module-level singleton signal for terminal focus state. + +**`TerminalFocusState`:** `'focused' | 'blurred' | 'unknown'` + +**Exports:** +- `setTerminalFocused(v)` — updates state, notifies `useSyncExternalStore` subscribers +- `getTerminalFocused()` — returns `focusState !== 'blurred'` (unknown treated as focused) +- `getTerminalFocusState()` — returns the tristate value +- `subscribeTerminalFocus(cb)` — subscribe function for `useSyncExternalStore` +- `resetTerminalFocusState()` — resets to `'unknown'` + +### `/x/Bigger-Projects/Claude-Code/src/ink/terminal-querier.ts` + +**Purpose:** Queries the terminal for capability information using DA1/DECRQM/XTVERSION sentinel protocol (no timeouts — DA1 is the universal sentinel). + +**`TerminalQuery` type:** `{ request: string; match: (r) => r is T }` + +**Query builders:** +- `decrqm(mode)` — DECRQM query; response: `DecrpmResponse` +- `da1()` — Primary Device Attributes +- `da2()` — Secondary Device Attributes +- `kittyKeyboard()` — Kitty keyboard flags query +- `cursorPosition()` — DECXCPR +- `oscColor(code, index?)` — OSC 10/11/12 color queries +- `xtversion()` — DCS `>|` terminal name/version + +**`TerminalQuerier` class:** +- `send(query)` — returns a `Promise` +- `flush()` — sends a DA1 sentinel; all pending queries resolve when DA1 response arrives (terminals answer in order) +- Internal: holds a queue of `{ query, resolve }` entries + +**`xtversion`:** Stores the XTVERSION name (set by App.tsx after the query resolves; read by `isXtermJs()`). + +### `/x/Bigger-Projects/Claude-Code/src/ink/useTerminalNotification.ts` + +**`TerminalWriteContext`:** React context providing a `(data: string) => void` write function that bypasses the normal Ink render pipeline (direct stdout write). + +**`TerminalWriteProvider`:** `= TerminalWriteContext.Provider` + +**`useTerminalNotification()`:** Returns notification methods: +- `notifyITerm2({ message, title? })` — OSC 9 iTerm2 notification +- `notifyKitty({ message, title, id })` — Kitty notification via OSC 99 +- `notifyGhostty({ message, title })` — Ghostty notification via OSC 99 variant +- `notifyBell()` — raw BEL character +- `progress(state, percentage?)` — OSC 9;4 progress bar (Ghostty 1.2+, iTerm2 3.6.6+, ConEmu) + +### `/x/Bigger-Projects/Claude-Code/src/ink/warn.ts` + +Centralized warning emitter (wraps `console.warn` with deduplication). Used by `Box.tsx` for invalid prop combinations. + +--- + +## Architecture: The Complete Pipeline + +``` +[stdin bytes] + → App.tsx handleInput() + → parseMultipleKeypresses() (termio tokenizer + key parser) + → KeyboardEvent / ParsedMouse / TerminalResponse + → dispatcher.dispatchDiscrete(rootNode, event) // keyboard + → React setState / component handlers + +[React state change] + → React reconciler commit + → reconciler.resetAfterCommit(rootNode) + → rootNode.onComputeLayout() // yoga calculateLayout() + → rootNode.onRender() → queueMicrotask(ink.onRender) + +[ink.onRender()] + → createRenderer(rootNode, stylePool)(options) + → renderNodeToOutput(rootNode, output, { prevScreen }) + → DOM walk with blit/clip/write/scroll ops + → output.get() → Screen (cell buffer) + → return Frame { screen, viewport, cursor, scrollHint } + → applySelectionOverlay(selection, frame.screen, stylePool) + → applySearchHighlight(frame.screen, query, stylePool) + → applyPositionedHighlight(frame.screen, positions, ...) + → log.render(prevFrame, frame) → Diff (Patch[]) + → optimize(diff) → compressed Patch[] + → writeDiffToTerminal(terminal, diff, syncOutput) + → BSU (if sync supported) + → for each Patch: write ANSI to stdout + → ESU (if sync supported) + → emit onFrame(FrameEvent) + → swap frontFrame ↔ backFrame +``` + +### Double Buffering + +The `Ink` class maintains `frontFrame` (the last displayed frame) and `backFrame` (the rendering target). After each render: +- `backFrame.screen` contains the newly rendered content +- It becomes the new `frontFrame` +- Old `frontFrame` becomes the new `backFrame` for the next render + +The renderer always reads `prevScreen = frontFrame.screen` for blit operations and writes into `backFrame.screen`. + +### Blit Optimization + +The blit path is the dominant fast path for steady-state rendering: +1. If a node's `dirty = false` AND `nodeCache` has a valid rect AND `prevScreen` is available AND no layout shift occurred AND no removed children: call `blitRegion(backScreen, prevScreen, cachedRect)` — pure `Int32Array.copyWithin`, O(cells). +2. This means spinner ticks and clock updates only re-render the changed cell ranges; the rest of the screen is copied in bulk. + +### Synchronized Output (BSU/ESU) + +When `SYNC_OUTPUT_SUPPORTED = true`, each frame is wrapped in DEC mode 2026 begin/end synchronized update sequences (`BSU` / `ESU`). This prevents terminals from rendering intermediate states during the diff write. Supported terminals: iTerm2, WezTerm, Warp, ghostty, kitty, VS Code, alacritty, foot, kitty. + +### DECSTBM Hardware Scroll + +When a `ScrollBox`'s `scrollTop` changes and layout is otherwise stable, `render-node-to-output` sets a `ScrollHint`. `LogUpdate.render()` checks for this hint and emits: +1. `setScrollRegion(top, bottom)` — DECSTBM restricts scroll to the box's viewport +2. `SU(n)` or `SD(n)` — hardware scroll by n rows +3. `RESET_SCROLL_REGION` — restore full-screen scroll +4. Then only re-renders the narrow band of newly exposed cells + +This replaces O(viewport × width) cell writes with O(exposed_rows × width) for smooth scrolling. + +### Pool Generational Reset + +The `StylePool` and `CharPool` live for the entire session (never reset) to ensure stable IDs for the blit optimization (IDs are comparable as integers across frames). The `HyperlinkPool` is reset every 5 minutes (hyperlinks are ephemeral) via `migrateScreenPools()`, which re-interns all active cells into fresh pool instances. diff --git a/spec/09_bridge_cli_remote.md b/spec/09_bridge_cli_remote.md new file mode 100644 index 0000000..adc0736 --- /dev/null +++ b/spec/09_bridge_cli_remote.md @@ -0,0 +1,2233 @@ +# Claude Code — Bridge Protocol, CLI Framework & Remote Systems + +## Table of Contents + +1. [Bridge System Overview](#1-bridge-system-overview) +2. [Bridge Types & Core Data Structures](#2-bridge-types--core-data-structures) +3. [Bridge API Client](#3-bridge-api-client) +4. [Bridge Configuration & Auth](#4-bridge-configuration--auth) +5. [Bridge Entitlement & Feature Gating](#5-bridge-entitlement--feature-gating) +6. [Session Lifecycle: Standalone Bridge (bridgeMain.ts)](#6-session-lifecycle-standalone-bridge-bridgemaints) +7. [REPL Bridge (replBridge.ts / initReplBridge.ts)](#7-repl-bridge-replbridgets--initreplbridgets) +8. [Env-Less Bridge Core (remoteBridgeCore.ts)](#8-env-less-bridge-core-remotebridgecorts) +9. [Transport Layer](#9-transport-layer) +10. [Message Protocol (bridgeMessaging.ts)](#10-message-protocol-bridgemessagingts) +11. [JWT Authentication (jwtUtils.ts)](#11-jwt-authentication-jwtutilsts) +12. [Session ID Compatibility (sessionIdCompat.ts)](#12-session-id-compatibility-sessionidcompatts) +13. [Work Secrets & CCR v2 Registration (workSecret.ts)](#13-work-secrets--ccr-v2-registration-worksecretsts) +14. [Bridge Pointer: Crash Recovery (bridgePointer.ts)](#14-bridge-pointer-crash-recovery-bridgepointerts) +15. [Permission Callbacks (bridgePermissionCallbacks.ts)](#15-permission-callbacks-bridgepermissioncallbacksts) +16. [Inbound Messages & Attachments](#16-inbound-messages--attachments) +17. [Session Runner (sessionRunner.ts)](#17-session-runner-sessionrunnerts) +18. [Bridge Debug & Fault Injection (bridgeDebug.ts)](#18-bridge-debug--fault-injection-bridgedebugets) +19. [Bridge Utilities](#19-bridge-utilities) +20. [CLI Framework](#20-cli-framework) +21. [CLI Transports](#21-cli-transports) +22. [Remote Session System](#22-remote-session-system) +23. [replLauncher.tsx](#23-repplaunchertsx) +24. [Configuration Defaults & GrowthBook Flags](#24-configuration-defaults--growthbook-flags) +25. [Complete API Endpoint Reference](#25-complete-api-endpoint-reference) +26. [WebSocket & SSE Protocol Reference](#26-websocket--sse-protocol-reference) + +--- + +## 1. Bridge System Overview + +The "Bridge" (Remote Control) system allows a local Claude Code CLI session to be driven from the claude.ai web application. It creates a bidirectional communication channel between the running CLI process and the cloud backend (CCR — Cloud Code Runner). + +### Architecture: Two Bridge Variants + +**V1 — Environment-Based Bridge (env-based)** +- Uses the Environments API (`/v1/environments/bridge`). +- Bridge registers as an "environment", polls for "work" (session dispatches). +- Session transport: WebSocket (v1) or SSE+CCRClient (CCR v2) via `HybridTransport` or `SSETransport`. +- `initBridgeCore()` in `replBridge.ts` handles the REPL side. +- `runBridgeLoop()` in `bridgeMain.ts` handles the standalone `claude remote-control` side. + +**V2 — Environment-Less Bridge (env-less)** +- No Environments API layer whatsoever. +- Direct flow: POST `/v1/code/sessions` → POST `/v1/code/sessions/{id}/bridge` → `createV2ReplTransport()`. +- Only for REPL sessions; daemon/print stay on env-based. +- Gated by `tengu_bridge_repl_v2` GrowthBook flag. + +### Two Deployment Modes + +**REPL Bridge (always-on / `/remote-control`)** +- Initialized by `initReplBridge()`, called from `useReplBridge` hook or `print.ts`. +- Runs inside the existing REPL process; messages from claude.ai are injected as user input. +- Lives in `bridge/replBridge.ts`, `bridge/initReplBridge.ts`, `bridge/remoteBridgeCore.ts`. + +**Standalone Bridge (`claude remote-control`)** +- Spawns child claude processes per session. +- Main loop in `bridge/bridgeMain.ts` → `runBridgeLoop()`. +- Supports multi-session spawn modes: `single-session`, `worktree`, `same-dir`. + +--- + +## 2. Bridge Types & Core Data Structures + +**File:** `bridge/types.ts` + +### Constants + +```typescript +DEFAULT_SESSION_TIMEOUT_MS = 24 * 60 * 60 * 1000 // 24 hours +BRIDGE_LOGIN_INSTRUCTION: string // "Remote Control is only available with claude.ai subscriptions..." +BRIDGE_LOGIN_ERROR: string // Full error printed when not authenticated +REMOTE_CONTROL_DISCONNECTED_MSG = 'Remote Control disconnected.' +``` + +### WorkData + +```typescript +type WorkData = { + type: 'session' | 'healthcheck' + id: string // session ID +} +``` + +### WorkResponse + +The response from polling for work (`GET .../work/poll`): + +```typescript +type WorkResponse = { + id: string // work item ID + type: 'work' + environment_id: string + state: string + data: WorkData + secret: string // base64url-encoded JSON WorkSecret + created_at: string +} +``` + +### WorkSecret + +Decoded from `WorkResponse.secret` (base64url JSON): + +```typescript +type WorkSecret = { + version: number // Must be 1 + session_ingress_token: string // JWT for session-ingress API calls + api_base_url: string + sources: Array<{ + type: string + git_info?: { type: string; repo: string; ref?: string; token?: string } + }> + auth: Array<{ type: string; token: string }> + claude_code_args?: Record | null + mcp_config?: unknown | null + environment_variables?: Record | null + use_code_sessions?: boolean // Server-driven CCR v2 selector +} +``` + +### BridgeConfig + +Configuration object passed to the main loop and API client: + +```typescript +type BridgeConfig = { + dir: string // Working directory + machineName: string // Hostname + branch: string // Git branch + gitRepoUrl: string | null + maxSessions: number // Capacity for multi-session mode + spawnMode: SpawnMode // 'single-session' | 'worktree' | 'same-dir' + verbose: boolean + sandbox: boolean + bridgeId: string // Client-generated UUID identifying this bridge instance + workerType: string // Sent as metadata.worker_type (e.g. 'claude_code') + environmentId: string // Client-generated UUID for idempotent registration + reuseEnvironmentId?: string // Backend-issued ID to reuse on re-register + apiBaseUrl: string + sessionIngressUrl: string // May differ from apiBaseUrl in local dev + debugFile?: string + sessionTimeoutMs?: number +} +``` + +### SpawnMode + +```typescript +type SpawnMode = 'single-session' | 'worktree' | 'same-dir' +``` + +- `single-session`: One session, bridge tears down when it ends. +- `worktree`: Persistent server, each session gets an isolated git worktree. +- `same-dir`: Persistent server, sessions share cwd (may conflict). + +### BridgeWorkerType + +```typescript +type BridgeWorkerType = 'claude_code' | 'claude_code_assistant' +``` + +### SessionActivity + +```typescript +type SessionActivityType = 'tool_start' | 'text' | 'result' | 'error' + +type SessionActivity = { + type: SessionActivityType + summary: string // e.g. "Editing src/foo.ts", "Reading package.json" + timestamp: number +} +``` + +### SessionHandle + +Interface returned by `SessionSpawner.spawn()`: + +```typescript +type SessionHandle = { + sessionId: string + done: Promise // 'completed' | 'failed' | 'interrupted' + kill(): void + forceKill(): void + activities: SessionActivity[] // Ring buffer of last ~10 activities + currentActivity: SessionActivity | null + accessToken: string // session_ingress_token + lastStderr: string[] // Ring buffer of last stderr lines + writeStdin(data: string): void + updateAccessToken(token: string): void +} +``` + +### SessionSpawnOpts + +```typescript +type SessionSpawnOpts = { + sessionId: string + sdkUrl: string + accessToken: string + useCcrV2?: boolean // Spawn child with CCR v2 env vars + workerEpoch?: number // Required when useCcrV2=true + onFirstUserMessage?: (text: string) => void +} +``` + +### BridgeApiClient Interface + +```typescript +type BridgeApiClient = { + registerBridgeEnvironment(config: BridgeConfig): Promise<{ + environment_id: string + environment_secret: string + }> + pollForWork( + environmentId: string, + environmentSecret: string, + signal?: AbortSignal, + reclaimOlderThanMs?: number, + ): Promise + acknowledgeWork(environmentId, workId, sessionToken): Promise + stopWork(environmentId, workId, force): Promise + deregisterEnvironment(environmentId): Promise + sendPermissionResponseEvent(sessionId, event, sessionToken): Promise + archiveSession(sessionId): Promise + reconnectSession(environmentId, sessionId): Promise + heartbeatWork(environmentId, workId, sessionToken): Promise<{ + lease_extended: boolean + state: string + }> +} +``` + +### PermissionResponseEvent + +```typescript +type PermissionResponseEvent = { + type: 'control_response' + response: { + subtype: 'success' + request_id: string + response: Record + } +} +``` + +### BridgeLogger Interface + +Full interface for the bridge UI/logging system (defined in `types.ts`): + +```typescript +type BridgeLogger = { + printBanner(config, environmentId): void + logSessionStart(sessionId, prompt): void + logSessionComplete(sessionId, durationMs): void + logSessionFailed(sessionId, error): void + logStatus(message): void + logVerbose(message): void + logError(message): void + logReconnected(disconnectedMs): void + updateIdleStatus(): void + updateReconnectingStatus(delayStr, elapsedStr): void + updateSessionStatus(sessionId, elapsed, activity, trail): void + clearStatus(): void + setRepoInfo(repoName, branch): void + setDebugLogPath(path): void + setAttached(sessionId): void + updateFailedStatus(error): void + toggleQr(): void + updateSessionCount(active, max, mode): void + setSpawnModeDisplay(mode): void + addSession(sessionId, url): void + updateSessionActivity(sessionId, activity): void + setSessionTitle(sessionId, title): void + removeSession(sessionId): void + refreshDisplay(): void +} +``` + +--- + +## 3. Bridge API Client + +**File:** `bridge/bridgeApi.ts` + +### Exports + +#### `validateBridgeId(id: string, label: string): string` + +Validates that a server-provided ID is safe for URL path interpolation. Uses pattern `/^[a-zA-Z0-9_-]+$/`. Throws on unsafe characters to prevent path traversal attacks. + +#### `class BridgeFatalError extends Error` + +Non-retryable bridge errors. Carries: +- `status: number` — HTTP status code +- `errorType: string | undefined` — server-provided error type (e.g. `"environment_expired"`) + +#### `createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient` + +Factory for the HTTP client. Dependencies: + +```typescript +type BridgeApiDeps = { + baseUrl: string + getAccessToken: () => string | undefined + runnerVersion: string + onDebug?: (msg: string) => void + onAuth401?: (staleAccessToken: string) => Promise + getTrustedDeviceToken?: () => string | undefined +} +``` + +**Request Headers** (on all API calls): +``` +Authorization: Bearer +Content-Type: application/json +anthropic-version: 2023-06-01 +anthropic-beta: environments-2025-11-01 +x-environment-runner-version: +X-Trusted-Device-Token: (optional, when tengu_sessions_elevated_auth_enforcement) +``` + +**OAuth 401 Retry:** On 401, calls `onAuth401(staleToken)`. If token refresh succeeds, retries the request once. If the retry also returns 401, throws `BridgeFatalError`. + +**Poll endpoint** (`pollForWork`): Uses `environmentSecret` (not OAuth token) as Bearer auth. Logs empty polls every 1st time and then every 100th consecutive empty poll. + +#### `isExpiredErrorType(errorType: string | undefined): boolean` + +Returns true if the error type string contains `'expired'` or `'lifetime'`. + +#### `isSuppressible403(err: BridgeFatalError): boolean` + +Returns true for 403 errors involving `external_poll_sessions` or `environments:manage` scope — these are permission errors for non-critical operations that should not be surfaced to users. + +### Error Status Handling + +| HTTP Status | Behavior | +|-------------|----------| +| 200, 204 | Success | +| 401 | `BridgeFatalError` with login instruction | +| 403 (expired errorType) | `BridgeFatalError`: "session has expired" | +| 403 (other) | `BridgeFatalError`: access denied / org permissions | +| 404 | `BridgeFatalError`: not found | +| 410 | `BridgeFatalError` with `errorType='environment_expired'` | +| 429 | Plain `Error`: rate limited | +| Other | Plain `Error` with status code | + +--- + +## 4. Bridge Configuration & Auth + +**File:** `bridge/bridgeConfig.ts` + +Consolidates auth/URL resolution. Two layers: dev overrides (ant-only) and production OAuth. + +### Exports + +#### `getBridgeTokenOverride(): string | undefined` + +Returns `process.env.CLAUDE_BRIDGE_OAUTH_TOKEN` if `process.env.USER_TYPE === 'ant'`, else `undefined`. + +#### `getBridgeBaseUrlOverride(): string | undefined` + +Returns `process.env.CLAUDE_BRIDGE_BASE_URL` if `process.env.USER_TYPE === 'ant'`, else `undefined`. + +#### `getBridgeAccessToken(): string | undefined` + +Dev override first, then `getClaudeAIOAuthTokens()?.accessToken`. + +#### `getBridgeBaseUrl(): string` + +Dev override first, then `getOauthConfig().BASE_API_URL`. + +--- + +## 5. Bridge Entitlement & Feature Gating + +**File:** `bridge/bridgeEnabled.ts` + +### Exports + +#### `isBridgeEnabled(): boolean` + +Synchronous. Requires: +1. Build flag `feature('BRIDGE_MODE')` must be true +2. `isClaudeAISubscriber()` — excludes Bedrock/Vertex/API key users +3. GrowthBook flag `tengu_ccr_bridge` (cached, may be stale) + +#### `isBridgeEnabledBlocking(): Promise` + +Like `isBridgeEnabled()` but awaits GrowthBook server fetch if disk cache says false. Use at entitlement gates to avoid unfair denials from stale cache. + +#### `getBridgeDisabledReason(): Promise` + +Returns a user-facing reason string if bridge is unavailable, or `null` if enabled. Checks: +1. Not a claude.ai subscriber +2. Missing `user:profile` scope (setup-token / env-var OAuth tokens) +3. Missing `organizationUuid` in OAuth account info +4. `tengu_ccr_bridge` gate off + +#### `isEnvLessBridgeEnabled(): boolean` + +Gates `tengu_bridge_repl_v2` — the V2 (env-less) REPL bridge path. Cached, may be stale. + +#### `isCseShimEnabled(): boolean` + +Kill-switch for `cse_*` → `session_*` retag shim. Reads `tengu_bridge_repl_v2_cse_shim_enabled` (default `true`). When false, `toCompatSessionId()` is a no-op. + +#### `checkBridgeMinVersion(): string | null` + +Returns error string if CLI version is below `tengu_bridge_min_version` config (default `'0.0.0'`). + +#### `getCcrAutoConnectDefault(): boolean` + +Returns `true` when `feature('CCR_AUTO_CONNECT')` and `tengu_cobalt_harbor` gate are both enabled. Used as default for `remoteControlAtStartup` config. + +#### `isCcrMirrorEnabled(): boolean` + +Returns `true` when `feature('CCR_MIRROR')` and either `CLAUDE_CODE_CCR_MIRROR` env var is truthy or `tengu_ccr_mirror` gate is enabled. + +--- + +## 6. Session Lifecycle: Standalone Bridge (bridgeMain.ts) + +**File:** `bridge/bridgeMain.ts` + +This is the main loop for `claude remote-control` (standalone mode). + +### BackoffConfig + +```typescript +type BackoffConfig = { + connInitialMs: number // Default: 2,000ms + connCapMs: number // Default: 120,000ms (2 min) + connGiveUpMs: number // Default: 600,000ms (10 min) + generalInitialMs: number // Default: 500ms + generalCapMs: number // Default: 30,000ms + generalGiveUpMs: number // Default: 600,000ms (10 min) + shutdownGraceMs?: number // SIGTERM→SIGKILL grace. Default: 30s + stopWorkBaseDelayMs?: number // stopWork retry base. Default: 1000ms +} +``` + +### Constants + +```typescript +STATUS_UPDATE_INTERVAL_MS = 1_000 // Live display refresh rate +SPAWN_SESSIONS_DEFAULT = 32 // Default max sessions +``` + +### `runBridgeLoop(config, environmentId, environmentSecret, api, spawner, logger, signal, backoffConfig?, initialSessionId?, getAccessToken?): Promise` + +Main exported function. Manages: +- `activeSessions: Map` — currently running sessions +- `sessionStartTimes: Map` — for elapsed time display +- `sessionWorkIds: Map` — work ID per session +- `sessionCompatIds: Map` — `session_*` ID per session (stable per-session) +- `sessionIngressTokens: Map` — JWT per session for heartbeat auth +- `sessionTimers: Map>` — per-session timeout timers +- `completedWorkIds: Set` — prevents double-stop +- `sessionWorktrees: Map` — worktree cleanup state +- `timedOutSessions: Set` — sessions killed by timeout watchdog +- `titledSessions: Set` — sessions already titled (suppresses auto-title) +- `capacityWake: CapacityWake` — signals early wake from at-capacity sleep + +**Heartbeat Logic:** + +`heartbeatActiveWorkItems()` iterates all `activeSessions`, calls `api.heartbeatWork()` with each session's ingress token. On `BridgeFatalError` 401/403 (JWT expired), calls `api.reconnectSession()` to trigger server re-dispatch. Returns: +- `'ok'` — at least one heartbeat succeeded +- `'auth_failed'` — one or more sessions had expired JWTs (re-queued via reconnect) +- `'fatal'` — 404/410 errors (environment expired) +- `'failed'` — all heartbeats failed for other reasons + +**Token Refresh (Proactive):** + +`createTokenRefreshScheduler()` fires 5 minutes before each session's JWT expires. +- **V1 sessions**: calls `handle.updateAccessToken(oauthToken)` to inject the new OAuth token directly to the child process stdin. +- **V2 sessions** (`v2Sessions` set): calls `api.reconnectSession()` to trigger server re-dispatch with a fresh JWT (V2 children validate the JWT's `session_id` claim, so OAuth tokens cannot be used directly). + +**Session Done Handler:** + +`onSessionDone()` cleans up all maps, fires `capacityWake.wake()`, then: +- `status = 'completed'`: logs completion +- `status = 'failed'`: logs failure with stderr (unless it was shutdown) +- `status = 'interrupted'`: logs verbose only + +For non-interrupted sessions with a `workId`, calls `stopWorkWithRetry()` (3 retries with 1s/2s/4s backoff). For worktree sessions, calls `removeAgentWorktree()`. + +**Status Display:** + +Tick every 1s via `setInterval`. Calls `logger.updateSessionStatus()` for each active session showing: elapsed time, current activity, last 5 tool activities as a trail. + +### Session Spawn: V1 vs V2 + +When work arrives: +1. Decodes `WorkSecret` from `work.secret`. +2. If `workSecret.use_code_sessions === true`: uses CCR v2 path (`buildCCRv2SdkUrl`, `registerWorker()`, `useCcrV2=true`). +3. Otherwise: uses V1 path (`buildSdkUrl()`, `useCcrV2=false`). + +### SpawnMode: Worktree + +When `config.spawnMode === 'worktree'`: +- Calls `createAgentWorktree()` to create an isolated git worktree. +- Passes the worktree path as the session's working directory. +- On session completion, calls `removeAgentWorktree()`. + +### Graceful Shutdown Sequence + +1. Abort `loopSignal`. +2. Stop all status update timers. +3. For each active session: call `handle.kill()`, await `handle.done`. +4. Await all pending cleanups (stopWork + worktree removal). +5. Call `api.deregisterEnvironment()`. +6. If session(s) were not fatal-exited, show resume hint. + +--- + +## 7. REPL Bridge (replBridge.ts / initReplBridge.ts) + +### ReplBridgeHandle (`bridge/replBridge.ts`) + +```typescript +type ReplBridgeHandle = { + bridgeSessionId: string + environmentId: string + sessionIngressUrl: string + writeMessages(messages: Message[]): void + writeSdkMessages(messages: SDKMessage[]): void + sendControlRequest(request: SDKControlRequest): void + sendControlResponse(response: SDKControlResponse): void + sendControlCancelRequest(requestId: string): void + sendResult(): void + teardown(): Promise +} +``` + +### BridgeState + +```typescript +type BridgeState = 'ready' | 'connected' | 'reconnecting' | 'failed' +``` + +### BridgeCoreParams + +The explicit-parameter interface to `initBridgeCore()` (enabling daemon/non-REPL callers): + +```typescript +type BridgeCoreParams = { + dir: string + machineName: string + branch: string + gitRepoUrl: string | null + title: string + baseUrl: string + sessionIngressUrl: string + workerType: string + sessionId: string // REPL's own session ID + getAccessToken: () => string | undefined + onAuth401?: (staleAccessToken: string) => Promise + toSDKMessages: (messages: Message[]) => SDKMessage[] + initialHistoryCap: number + pollConfig?: PollIntervalConfig + // ... plus InitBridgeOptions callbacks +} +``` + +### InitBridgeOptions (`bridge/initReplBridge.ts`) + +```typescript +type InitBridgeOptions = { + onInboundMessage?: (msg: SDKMessage) => void | Promise + onPermissionResponse?: (response: SDKControlResponse) => void + onInterrupt?: () => void + onSetModel?: (model: string | undefined) => void + onSetMaxThinkingTokens?: (maxTokens: number | null) => void + onSetPermissionMode?: (mode: PermissionMode) => { ok: true } | { ok: false; error: string } + onStateChange?: (state: BridgeState, detail?: string) => void + initialMessages?: Message[] + initialName?: string + getMessages?: () => Message[] + previouslyFlushedUUIDs?: Set + perpetual?: boolean +} +``` + +### replBridgeHandle.ts + +Global pointer to the active REPL bridge handle: + +```typescript +setReplBridgeHandle(h: ReplBridgeHandle | null): void +getReplBridgeHandle(): ReplBridgeHandle | null +getSelfBridgeCompatId(): string | undefined // Returns session_* compat ID +``` + +--- + +## 8. Env-Less Bridge Core (remoteBridgeCore.ts) + +**File:** `bridge/remoteBridgeCore.ts` + +Direct-connect bridge that bypasses the Environments API entirely. + +### Connection Flow + +1. `POST /v1/code/sessions` with `{ title, bridge: {} }` → `session.id` (`cse_*`) +2. `POST /v1/code/sessions/{id}/bridge` → `{ worker_jwt, expires_in, api_base_url, worker_epoch }` +3. `createV2ReplTransport(worker_jwt, worker_epoch)` — SSE + CCRClient +4. `createTokenRefreshScheduler(scheduleFromExpiresIn)` — proactive `/bridge` re-call +5. On 401 SSE: rebuild transport with fresh `/bridge` credentials (same seq-num) + +### EnvLessBridgeParams + +```typescript +type EnvLessBridgeParams = { + baseUrl: string + orgUUID: string + title: string + getAccessToken: () => string | undefined + onAuth401?: (staleAccessToken: string) => Promise + toSDKMessages: (messages: Message[]) => SDKMessage[] + initialHistoryCap: number + initialMessages?: Message[] + onInboundMessage?: (msg: SDKMessage) => void | Promise + onUserMessage?: (text: string, sessionId: string) => boolean + onPermissionResponse?: (response: SDKControlResponse) => void + onInterrupt?: () => void + onSetModel?: (model: string | undefined) => void + onSetMaxThinkingTokens?: (maxTokens: number | null) => void + onSetPermissionMode?: (mode: PermissionMode) => { ok: true } | { ok: false; error: string } + onStateChange?: (state: BridgeState, detail?: string) => void + perpetual?: boolean +} +``` + +### Connect Cause Telemetry + +```typescript +type ConnectCause = 'initial' | 'proactive_refresh' | 'auth_401_recovery' +``` + +Sent with `tengu_bridge_repl_v2_ws_connected` analytics event. + +--- + +## 9. Transport Layer + +### ReplBridgeTransport Interface (`bridge/replBridgeTransport.ts`) + +Abstracts over V1 (HybridTransport) and V2 (SSETransport+CCRClient): + +```typescript +type ReplBridgeTransport = { + write(message: StdoutMessage): Promise + writeBatch(messages: StdoutMessage[]): Promise + close(): void + isConnectedStatus(): boolean + getStateLabel(): string + setOnData(callback: (data: string) => void): void + setOnClose(callback: (closeCode?: number) => void): void + setOnConnect(callback: () => void): void + connect(): void + getLastSequenceNum(): number // V1 always returns 0; V2 returns SSE seq + readonly droppedBatchCount: number // V1 only; V2 always 0 + reportState(state: SessionState): void // V2 only; V1 no-op + reportMetadata(metadata: Record): void // V2 only + reportDelivery(eventId: string, status: 'processing' | 'processed'): void // V2 only + flush(): Promise // V2 only; V1 resolves immediately +} +``` + +### `createV1ReplTransport(hybrid: HybridTransport): ReplBridgeTransport` + +Thin no-op wrapper that delegates to `HybridTransport`. V1-specific behaviors: +- `getLastSequenceNum()` always returns 0 +- `reportState()`, `reportMetadata()`, `reportDelivery()`, `flush()` are all no-ops + +### `createV2ReplTransport(opts): Promise` + +Options: +```typescript +{ + sessionUrl: string // /v1/code/sessions/{id} + ingressToken: string + sessionId: string + initialSequenceNum?: number // SSE resume cursor + epoch?: number // If from /bridge, server already bumped epoch + heartbeatIntervalMs?: number // Default: 20s + heartbeatJitterFraction?: number + outboundOnly?: boolean // Skip SSE read stream (mirror mode) + getAuthToken?: () => string | undefined // Per-instance auth (multi-session safe) +} +``` + +**Close Codes used internally:** +- `4090` — epoch superseded (epoch mismatch from CCRClient) +- `4091` — CCR initialize() failure +- `4092` — SSE reconnect-budget exhaustion (mapped from `undefined`) + +**Delivery ACK behavior:** Both `'received'` and `'processed'` are fired immediately on SSE event receipt to prevent phantom prompt flooding on restarts. This is a fix for the issue where `reconnectSession` re-queues prompts that haven't been ACK'd as `'processed'`. + +### HybridTransport (`cli/transports/HybridTransport.ts`) + +Extends `WebSocketTransport`. WebSocket for reads, HTTP POST for writes. + +**Configuration:** +```typescript +BATCH_FLUSH_INTERVAL_MS = 100 // Accumulates stream_events before POST +POST_TIMEOUT_MS = 15_000 // Per-attempt timeout +CLOSE_GRACE_MS = 3000 // Grace period for queued writes on close +``` + +**Write flow:** +``` +write(stream_event) ─┐ + │ (100ms timer) +write(other) ──────► SerialBatchEventUploader.enqueue() +writeBatch() ───────┘ │ + ▼ serial, batched, retries indefinitely + postOnce() (single HTTP POST) +``` + +- `maxBatchSize`: 500 +- `maxQueueSize`: 100,000 +- `baseDelayMs`: 500, `maxDelayMs`: 8000, `jitterMs`: 1000 + +**Post URL:** Converts WebSocket URL to HTTP(S) POST endpoint (`convertWsUrlToPostUrl()`). + +### WebSocketTransport (`cli/transports/WebSocketTransport.ts`) + +Base WebSocket transport. + +**Configuration:** +```typescript +DEFAULT_MAX_BUFFER_SIZE = 1000 +DEFAULT_BASE_RECONNECT_DELAY = 1000 +DEFAULT_MAX_RECONNECT_DELAY = 30_000 +DEFAULT_RECONNECT_GIVE_UP_MS = 600_000 // 10 minutes +DEFAULT_PING_INTERVAL = 10_000 +DEFAULT_KEEPALIVE_INTERVAL = 300_000 // 5 minutes +SLEEP_DETECTION_THRESHOLD_MS = 60_000 // 2× max reconnect delay +KEEP_ALIVE_FRAME = '{"type":"keep_alive"}\n' +``` + +**Permanent Close Codes** (no retry): +- `1002` — protocol error (session reaped) +- `4001` — session expired/not found +- `4003` — unauthorized + +**Sleep detection:** If gap between reconnection attempts exceeds `60s`, resets the reconnection budget and retries (machine likely slept). + +**States:** `'idle' | 'connected' | 'reconnecting' | 'closing' | 'closed'` + +### SSETransport (`cli/transports/SSETransport.ts`) + +Server-Sent Events transport. + +**Configuration:** +```typescript +RECONNECT_BASE_DELAY_MS = 1000 +RECONNECT_MAX_DELAY_MS = 30_000 +RECONNECT_GIVE_UP_MS = 600_000 // 10 minutes +LIVENESS_TIMEOUT_MS = 45_000 // Server keepalives every 15s +PERMANENT_HTTP_CODES = {401, 403, 404} +POST_MAX_RETRIES = 10 +POST_BASE_DELAY_MS = 500 +POST_MAX_DELAY_MS = 8000 +``` + +**SSE Frame Parsing:** + +```typescript +type SSEFrame = { + event?: string + id?: string // Used as sequence number for Last-Event-ID + data?: string +} +``` + +Frames are double-newline delimited. Leading space after `:` is stripped per SSE spec. Comments (`:keepalive`) are ignored. Sequence numbers are tracked via `id` field. + +**Exported for testing:** `parseSSEFrames(buffer: string): { frames: SSEFrame[]; remaining: string }` + +**Sequence number carryover:** On reconnect, sends `Last-Event-ID` or `from_sequence_num` query param so the server resumes from where the old stream left off. + +### SerialBatchEventUploader (`cli/transports/SerialBatchEventUploader.ts`) + +```typescript +type SerialBatchEventUploaderConfig = { + maxBatchSize: number // Max items per POST + maxBatchBytes?: number // Max serialized bytes per POST + maxQueueSize: number // Max pending items before enqueue() blocks + send: (batch: T[]) => Promise // The actual HTTP call + baseDelayMs: number + maxDelayMs: number + jitterMs: number + maxConsecutiveFailures?: number // After N failures, drop batch and advance + onBatchDropped?: (batchSize, failures) => void +} +``` + +**`class RetryableError extends Error`**: Throw from `config.send()` to override exponential backoff with server-supplied `retryAfterMs` (e.g., for 429 responses). + +**Backpressure:** `enqueue()` blocks when `maxQueueSize` is reached. + +### WorkerStateUploader (`cli/transports/WorkerStateUploader.ts`) + +Coalescing uploader for `PUT /worker` (session state + metadata). + +- At most 1 in-flight PUT + 1 pending patch (never grows beyond 2 slots). +- Coalescing rules: + - Top-level keys: last value wins. + - `external_metadata` / `internal_metadata`: RFC 7396 merge (null values preserved for server-side delete). + +### CCRClient (`cli/transports/ccrClient.ts`) + +The CCR v2 write-side client. + +**Configuration:** +```typescript +DEFAULT_HEARTBEAT_INTERVAL_MS = 20_000 // Server TTL: 60s +STREAM_EVENT_FLUSH_INTERVAL_MS = 100 // text_delta coalescing window +MAX_CONSECUTIVE_AUTH_FAILURES = 10 // ~200s at 20s heartbeat +``` + +**`class CCRInitError extends Error`**: Carries `reason: CCRInitFailReason`: +- `'no_auth_headers'` +- `'missing_epoch'` +- `'worker_register_failed'` + +**Epoch mismatch (409 response)**: Triggers `onEpochMismatch()` callback, which closes CCRClient and SSETransport and fires `onCloseCb(4090)`. + +**text_delta coalescing:** `stream_event` messages with `content_block_delta` / `text_delta` are accumulated in a per-message-ID buffer for `100ms`. Each emitted event is a complete self-contained snapshot of the text so far. + +### Transport Selection (`cli/transports/transportUtils.ts`) + +```typescript +getTransportForUrl(url, headers, sessionId, refreshHeaders): Transport +``` + +Priority: +1. `SSETransport` — when `CLAUDE_CODE_USE_CCR_V2` env is truthy +2. `HybridTransport` — when URL is `ws(s)://` AND `CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2` env is truthy +3. `WebSocketTransport` — default for `ws(s)://` +4. Throws for unsupported protocols + +--- + +## 10. Message Protocol (bridgeMessaging.ts) + +**File:** `bridge/bridgeMessaging.ts` + +Pure functions shared by both V1 (`initBridgeCore`) and V2 (`initEnvLessBridgeCore`). + +### Type Guards + +#### `isSDKMessage(value: unknown): value is SDKMessage` +Checks for non-null object with string `type` field. + +#### `isSDKControlResponse(value: unknown): value is SDKControlResponse` +Checks `type === 'control_response'` and has `'response'` field. + +#### `isSDKControlRequest(value: unknown): value is SDKControlRequest` +Checks `type === 'control_request'`, has `'request_id'` and `'request'` fields. + +#### `isEligibleBridgeMessage(m: Message): boolean` + +Returns true for messages that should be forwarded to the bridge transport: +- `type === 'user'` (non-virtual) +- `type === 'assistant'` (non-virtual) +- `type === 'system'` with `subtype === 'local_command'` + +#### `extractTitleText(m: Message): string | undefined` + +Extracts title-worthy text from a user message. Filters out: +- Non-user messages +- `isMeta` messages +- Tool result messages +- Compact summary messages +- Non-human origins (`origin.kind !== 'human'`) +- Pure display-tag content (stripped via `stripDisplayTagsAllowEmpty`) + +### `handleIngressMessage(data, recentPostedUUIDs, recentInboundUUIDs, onInboundMessage, onPermissionResponse?, onControlRequest?): void` + +Parses and routes an ingress WebSocket message: +1. Parses JSON, normalizes control message keys. +2. Checks for `control_response` → calls `onPermissionResponse`. +3. Checks for `control_request` → calls `onControlRequest`. +4. Validates it's an `SDKMessage`. +5. Echo dedup: skips if UUID in `recentPostedUUIDs`. +6. Re-delivery dedup: skips if UUID in `recentInboundUUIDs`. +7. Only forwards `type === 'user'` messages to `onInboundMessage`. +8. All other message types are logged and ignored. + +### Server Control Request Handling + +`handleServerControlRequest(request, handlers): void` + +Processes server-sent `control_request` messages. Must respond promptly (server kills WS after ~10-14s timeout). + +**Supported subtypes:** + +| Subtype | Behavior | +|---------|----------| +| `initialize` | Responds with `{ commands: [], output_style: 'normal', available_output_styles: ['normal'], models: [], account: {}, pid: process.pid }` | +| `set_model` | Calls `onSetModel(request.request.model)`, responds success | +| `set_max_thinking_tokens` | Calls `onSetMaxThinkingTokens(maxTokens)`, responds success | +| `set_permission_mode` | Calls `onSetPermissionMode(mode)`, responds success or error | +| `interrupt` | Calls `onInterrupt()`, responds success | +| unknown | Responds with error: "REPL bridge does not handle control_request subtype: ..." | + +**Outbound-only mode**: All mutable requests respond with error `'This session is outbound-only...'`. `initialize` still responds success. + +**Response envelope:** +```json +{ + "type": "control_response", + "response": { + "subtype": "success" | "error", + "request_id": "", + ... + }, + "session_id": "" +} +``` + +### `makeResultMessage(sessionId: string): SDKResultSuccess` + +Builds a minimal result message for session archival: +```json +{ + "type": "result", + "subtype": "success", + "duration_ms": 0, + "duration_api_ms": 0, + "is_error": false, + "num_turns": 0, + "result": "", + "stop_reason": null, + "total_cost_usd": 0, + "usage": {...}, + "modelUsage": {}, + "permission_denials": [], + "session_id": "", + "uuid": "" +} +``` + +### `class BoundedUUIDSet` + +FIFO-bounded ring buffer for UUID deduplication. O(capacity) memory. + +```typescript +class BoundedUUIDSet { + constructor(capacity: number) + add(uuid: string): void // Evicts oldest when at capacity + has(uuid: string): boolean + clear(): void +} +``` + +Used for: +- `recentPostedUUIDs` — echo suppression (messages we sent reflected back) +- `recentInboundUUIDs` — re-delivery dedup (server replays history after transport swap) + +--- + +## 11. JWT Authentication (jwtUtils.ts) + +**File:** `bridge/jwtUtils.ts` + +### `decodeJwtPayload(token: string): unknown | null` + +Decodes JWT payload segment without signature verification. Strips `sk-ant-si-` prefix if present. Returns parsed JSON or `null` on malformed input. + +### `decodeJwtExpiry(token: string): number | null` + +Extracts the `exp` Unix seconds claim from a JWT without verifying the signature. Returns `null` if unparseable. + +### `createTokenRefreshScheduler(opts): { schedule, scheduleFromExpiresIn, cancel, cancelAll }` + +**Options:** +```typescript +{ + getAccessToken: () => string | undefined | Promise + onRefresh: (sessionId: string, oauthToken: string) => void + label: string + refreshBufferMs?: number // Default: TOKEN_REFRESH_BUFFER_MS = 5 min +} +``` + +**Constants:** +```typescript +TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000 // 5 minutes before expiry +FALLBACK_REFRESH_INTERVAL_MS = 30 * 60 * 1000 // 30 minutes (fallback) +MAX_REFRESH_FAILURES = 3 +REFRESH_RETRY_DELAY_MS = 60_000 // 1 minute +``` + +**Methods:** + +`schedule(sessionId, token)`: Decodes `exp` from JWT, schedules refresh `(exp × 1000 - now - refreshBufferMs)` ms from now. If token has no decodable `exp` (e.g., OAuth token), preserves existing timer. + +`scheduleFromExpiresIn(sessionId, expiresInSeconds)`: Schedules refresh using explicit TTL. Clamp to 30s floor: `max(expiresInSeconds × 1000 - refreshBufferMs, 30_000)`. + +`cancel(sessionId)`: Clears timer, bumps generation to invalidate in-flight refreshes. + +`cancelAll()`: Clears all timers and failure counters. + +**Generation tracking:** Each session has a monotonic generation counter. `doRefresh()` checks that the generation hasn't changed before scheduling follow-up timers. This prevents orphaned timers when a session is cancelled while a refresh is in flight. + +**Follow-up refresh:** After each successful refresh, schedules `FALLBACK_REFRESH_INTERVAL_MS` (30 min) follow-up to handle long-running sessions that outlast the first refresh window. + +--- + +## 12. Session ID Compatibility (sessionIdCompat.ts) + +**File:** `bridge/sessionIdCompat.ts` + +Handles the V2 compat layer's `cse_*` ↔ `session_*` ID translation. + +**Problem:** CCR V2 infra uses `cse_*` prefix internally; the compat gateway and client-facing API (`/v1/sessions`) expect `session_*`. Same UUID, different prefix. + +### Exports + +#### `setCseShimGate(gate: () => boolean): void` + +Registers the GrowthBook gate `isCseShimEnabled`. Called from bridge init code that already imports `bridgeEnabled.ts`. The SDK bundle never calls this, so the shim defaults to active. + +#### `toCompatSessionId(id: string): string` + +Re-tags `cse_*` → `session_*` for compat API calls (`/v1/sessions/{id}`, `/archive`, `/events`). No-op for IDs that aren't `cse_*`. No-op when shim gate is off. + +``` +"cse_abc123" → "session_abc123" +"session_abc123" → "session_abc123" (no-op) +``` + +#### `toInfraSessionId(id: string): string` + +Inverse: re-tags `session_*` → `cse_*` for infrastructure calls (`/bridge/reconnect`). No-op for IDs that aren't `session_*`. + +``` +"session_abc123" → "cse_abc123" +"cse_abc123" → "cse_abc123" (no-op) +``` + +--- + +## 13. Work Secrets & CCR v2 Registration (workSecret.ts) + +**File:** `bridge/workSecret.ts` + +### `decodeWorkSecret(secret: string): WorkSecret` + +Decodes base64url-encoded work secret JSON. Validates: +- Must be a version-1 secret. +- `session_ingress_token` must be a non-empty string. +- `api_base_url` must be a string. + +### `buildSdkUrl(apiBaseUrl: string, sessionId: string): string` + +Builds V1 WebSocket URL: +- Localhost: `ws://host/v2/session_ingress/ws/{sessionId}` (direct to session-ingress) +- Production: `wss://host/v1/session_ingress/ws/{sessionId}` (Envoy rewrites `/v1/` → `/v2/`) + +### `sameSessionId(a: string, b: string): boolean` + +Compares two session IDs regardless of prefix (`cse_` vs `session_`). Compares the UUID body (everything after the last `_`). Requires body length ≥ 4 to avoid false matches on malformed IDs. + +### `buildCCRv2SdkUrl(apiBaseUrl: string, sessionId: string): string` + +Builds V2 HTTP(S) session URL: `{apiBaseUrl}/v1/code/sessions/{sessionId}` + +### `registerWorker(sessionUrl: string, accessToken: string): Promise` + +`POST {sessionUrl}/worker/register` to register as the CCR worker. Returns `worker_epoch`. The epoch is serialized as `int64` which may be returned as a string by protojson — handles both `string` and `number` forms. + +--- + +## 14. Bridge Pointer: Crash Recovery (bridgePointer.ts) + +**File:** `bridge/bridgePointer.ts` + +### Purpose + +Crash-recovery pointer written after session creation, refreshed periodically, cleared on clean shutdown. On next startup, `claude remote-control` detects stale pointers and offers to resume. + +### Constants + +```typescript +BRIDGE_POINTER_TTL_MS = 4 * 60 * 60 * 1000 // 4 hours (matches Redis BRIDGE_LAST_POLL_TTL) +MAX_WORKTREE_FANOUT = 50 +``` + +### BridgePointer Schema + +```typescript +type BridgePointer = { + sessionId: string + environmentId: string + source: 'standalone' | 'repl' +} +``` + +**File location:** `{projectsDir}/{sanitizedDir}/bridge-pointer.json` + +### Exports + +#### `getBridgePointerPath(dir: string): string` + +Returns the absolute path to the bridge pointer file for a given working directory. + +#### `writeBridgePointer(dir, pointer): Promise` + +Writes pointer atomically. Also used to refresh mtime (same-content writes). Best-effort — logs and swallows errors. + +#### `readBridgePointer(dir): Promise<(BridgePointer & { ageMs: number }) | null>` + +Reads the pointer. Returns `null` if: +- File does not exist +- JSON is malformed +- Schema validation fails +- `mtime` is more than 4 hours ago (stale) + +Stale/invalid pointers are automatically deleted. + +#### `readBridgePointerAcrossWorktrees(dir): Promise<{ pointer, dir } | null>` + +Worktree-aware read for `--continue`. Fast-path checks the given dir first. If not found, fans out to git worktree siblings (via `getWorktreePathsPortable()`) in parallel (capped at 50). Returns the freshest pointer and its directory. + +#### `clearBridgePointer(dir): Promise` + +Deletes the pointer file. Idempotent (ENOENT is expected on clean shutdown). + +--- + +## 15. Permission Callbacks (bridgePermissionCallbacks.ts) + +**File:** `bridge/bridgePermissionCallbacks.ts` + +### Types + +```typescript +type BridgePermissionResponse = { + behavior: 'allow' | 'deny' + updatedInput?: Record + updatedPermissions?: PermissionUpdate[] + message?: string +} + +type BridgePermissionCallbacks = { + sendRequest( + requestId, toolName, input, toolUseId, description, + permissionSuggestions?, blockedPath? + ): void + sendResponse(requestId, response: BridgePermissionResponse): void + cancelRequest(requestId): void + onResponse( + requestId, + handler: (response: BridgePermissionResponse) => void + ): () => void // returns unsubscribe function +} +``` + +### `isBridgePermissionResponse(value: unknown): value is BridgePermissionResponse` + +Type predicate. Checks that `value.behavior === 'allow' || value.behavior === 'deny'`. + +--- + +## 16. Inbound Messages & Attachments + +### inboundAttachments.ts + +Resolves `file_uuid` attachments from inbound bridge user messages. + +**Flow:** +1. Web composer uploads via `/api/{org}/upload` (cookie-auth). +2. Bridge receives `file_attachments: [{ file_uuid, file_name }]` on the user message. +3. `resolveInboundAttachments()` fetches each file via `GET /api/oauth/files/{uuid}/content`. +4. Downloads to `~/.claude/uploads/{sessionId}/{uuid-prefix}-{sanitizedName}`. +5. Returns `@"path"` prefix string to prepend to message content. + +**`DOWNLOAD_TIMEOUT_MS = 30_000`** + +**File path sanitization:** `sanitizeFileName()` strips path components and replaces non-alphanumeric chars except `._-` with `_`. + +**Prefix format:** 8 chars from `file_uuid` (or random UUID), e.g. `@"abc12345-filename.pdf" `. + +**Exports:** +```typescript +extractInboundAttachments(msg: unknown): InboundAttachment[] +resolveInboundAttachments(attachments: InboundAttachment[]): Promise +prependPathRefs( + content: string | Array, + prefix: string, +): string | Array +resolveAndPrepend( + msg: unknown, + content: string | Array, +): Promise> +``` + +`prependPathRefs()` targets the **last** text block in a content array (because `processUserInputBase` reads the last block). + +### inboundMessages.ts + +**`extractInboundMessageFields(msg: SDKMessage): { content, uuid } | undefined`** + +Extracts content and UUID from a user message. Normalizes image blocks: +- Converts camelCase `mediaType` → `media_type` (mobile app compatibility fix for `mobile-apps#5825`) +- Detects missing `media_type` via `detectImageFormatFromBase64()` + +**`normalizeImageBlocks(blocks: ContentBlockParam[]): ContentBlockParam[]`** + +Fast-path: returns original reference if no malformed blocks. Only allocates on the fix-needed path. + +--- + +## 17. Session Runner (sessionRunner.ts) + +**File:** `bridge/sessionRunner.ts` + +Spawns child Claude CLI processes for bridge sessions. + +### Constants + +```typescript +MAX_ACTIVITIES = 10 // Ring buffer size for activity history +MAX_STDERR_LINES = 10 // Ring buffer size for stderr +``` + +### `safeFilenameId(id: string): string` + +Sanitizes session IDs for use in file names. Replaces non-alphanumeric chars (except `_-`) with underscores. + +### PermissionRequest + +Message emitted by child CLI on stdout when it needs permission: + +```typescript +type PermissionRequest = { + type: 'control_request' + request_id: string + request: { + subtype: 'can_use_tool' + tool_name: string + input: Record + tool_use_id: string + } +} +``` + +### SessionSpawnerDeps + +```typescript +type SessionSpawnerDeps = { + execPath: string + scriptArgs: string[] // Empty for compiled binaries; [process.argv[1]] for npm + env: NodeJS.ProcessEnv + verbose: boolean + sandbox: boolean + debugFile?: string + permissionMode?: string + onDebug: (msg: string) => void + onActivity?: (sessionId, activity) => void + onPermissionRequest?: (sessionId, request, accessToken) => void +} +``` + +### Tool Activity Verbs + +```typescript +const TOOL_VERBS = { + Read: 'Reading', Write: 'Writing', Edit: 'Editing', MultiEdit: 'Editing', + Bash: 'Running', Glob: 'Searching', Grep: 'Searching', + WebFetch: 'Fetching', WebSearch: 'Searching', Task: 'Running task', + FileReadTool: 'Reading', FileWriteTool: 'Writing', FileEditTool: 'Editing', + GlobTool: 'Searching', GrepTool: 'Searching', BashTool: 'Running', + NotebookEditTool: 'Editing notebook', LSP: 'LSP', +} +``` + +--- + +## 18. Bridge Debug & Fault Injection (bridgeDebug.ts) + +**File:** `bridge/bridgeDebug.ts` + +Ant-only fault injection for testing bridge recovery paths. Zero overhead in external builds. + +### BridgeFault + +```typescript +type BridgeFault = { + method: 'pollForWork' | 'registerBridgeEnvironment' | 'reconnectSession' | 'heartbeatWork' + kind: 'fatal' | 'transient' + status: number + errorType?: string + count: number // Decremented on consume; removed at 0 +} +``` + +- **fatal**: Throws `BridgeFatalError` — triggers environment teardown. +- **transient**: Throws a plain `Error` (mimics 5xx/network) — triggers retry/backoff. + +### BridgeDebugHandle + +```typescript +type BridgeDebugHandle = { + fireClose: (code: number) => void // Invoke transport permanent-close handler + forceReconnect: () => void // Call reconnectEnvironmentWithSession() + injectFault: (fault: BridgeFault) => void + wakePollLoop: () => void // Abort at-capacity sleep immediately + describe: () => string // "envId=... sessionId=..." +} +``` + +### Exports + +```typescript +registerBridgeDebugHandle(h: BridgeDebugHandle): void +clearBridgeDebugHandle(): void +getBridgeDebugHandle(): BridgeDebugHandle | null +injectBridgeFault(fault: BridgeFault): void +wrapApiForFaultInjection(api: BridgeApiClient): BridgeApiClient +``` + +`wrapApiForFaultInjection()` wraps `pollForWork`, `registerBridgeEnvironment`, `reconnectSession`, and `heartbeatWork` with fault queue checks. All other methods pass through unchanged. + +--- + +## 19. Bridge Utilities + +### debugUtils.ts + +```typescript +redactSecrets(s: string): string +``` + +Redacts sensitive field values matching: `session_ingress_token`, `environment_secret`, `access_token`, `secret`, `token`. Values shorter than 16 chars → `[REDACTED]`. Longer: `first8chars...last4chars`. + +```typescript +debugTruncate(s: string): string // 2000 char limit, collapses newlines +debugBody(data: unknown): string // Serialize + redact + truncate +describeAxiosError(err: unknown): string // Extracts server message from axios errors +extractHttpStatus(err: unknown): number | undefined +extractErrorDetail(data: unknown): string | undefined // Checks data.message, data.error.message +logBridgeSkip(reason, debugMsg?, v2?): void // Logs analytics + debug for bridge skip +``` + +### bridgeStatusUtil.ts + +```typescript +type StatusState = 'idle' | 'attached' | 'titled' | 'reconnecting' | 'failed' +TOOL_DISPLAY_EXPIRY_MS = 30_000 // How long a tool activity stays visible +SHIMMER_INTERVAL_MS = 150 // Shimmer animation tick + +timestamp(): string // "HH:MM:SS" +formatDuration(ms): string // re-exported from utils/format.ts +truncatePrompt(s, width): string // re-exported +abbreviateActivity(summary): string // Truncates to 30 chars + +buildBridgeConnectUrl(environmentId, ingressUrl?): string + // → "{baseUrl}/code?bridge={environmentId}" + +buildBridgeSessionUrl(sessionId, environmentId, ingressUrl?): string + // → "{remoteSessionUrl}?bridge={environmentId}" + +computeGlimmerIndex(tick, messageWidth): number +computeShimmerSegments(text, glimmerIndex): { before, shimmer, after } + +getBridgeStatus({ error, connected, sessionActive, reconnecting }): BridgeStatusInfo + // Returns { label, color } for UI rendering + +buildIdleFooterText(url): string +buildActiveFooterText(url): string +FAILED_FOOTER_TEXT = 'Something went wrong, please try again' + +wrapWithOsc8Link(text, url): string // OSC 8 terminal hyperlink +``` + +### capacityWake.ts + +```typescript +type CapacitySignal = { signal: AbortSignal; cleanup: () => void } +type CapacityWake = { + signal(): CapacitySignal // Merged abort: outer loop OR capacity wake + wake(): void // Abort current sleep, arm fresh controller +} + +createCapacityWake(outerSignal: AbortSignal): CapacityWake +``` + +Shared primitive for both `replBridge.ts` and `bridgeMain.ts` to sleep while at-capacity and wake early when a session ends or the outer signal aborts. + +### flushGate.ts + +```typescript +class FlushGate { + get active(): boolean + get pendingCount(): number + start(): void // Mark flush in-progress; enqueue() queues items + end(): T[] // End flush, return queued items + enqueue(...items: T[]): boolean // Queue if active, else returns false + drop(): number // Discard all (permanent close); returns count dropped + deactivate(): void // Clear active without dropping (transport replacement) +} +``` + +Used during initial history flush to queue new messages that arrive concurrently, preventing server-side interleaving. + +### pollConfig.ts / pollConfigDefaults.ts + +`getPollIntervalConfig(): PollIntervalConfig` — reads from GrowthBook `tengu_bridge_poll_interval_config` with 5-minute refresh. Falls back to defaults on schema violation. + +```typescript +type PollIntervalConfig = { + poll_interval_ms_not_at_capacity: number // Default: 2000ms + poll_interval_ms_at_capacity: number // Default: 600,000ms (10 min) + non_exclusive_heartbeat_interval_ms: number // Default: 0 (disabled) + multisession_poll_interval_ms_not_at_capacity: number // Default: 2000ms + multisession_poll_interval_ms_partial_capacity: number // Default: 2000ms + multisession_poll_interval_ms_at_capacity: number // Default: 600,000ms + reclaim_older_than_ms: number // Default: 5000ms + session_keepalive_interval_v2_ms: number // Default: 120,000ms +} +``` + +**Validation rules:** +- `poll_interval_ms_*` and `multisession_*` fields: min 100ms. +- `non_exclusive_heartbeat_interval_ms`: min 0 (0 = disabled). +- At-capacity intervals: 0 (disabled) or ≥100ms (1-99 rejected to prevent confusion with seconds). +- Object-level: at least one at-capacity liveness mechanism must be enabled (heartbeat > 0 OR at-capacity poll > 0). + +### envLessBridgeConfig.ts + +`getEnvLessBridgeConfig(): Promise` — reads from GrowthBook `tengu_bridge_repl_v2_config`. Defaults: + +```typescript +DEFAULT_ENV_LESS_BRIDGE_CONFIG = { + init_retry_max_attempts: 3, + init_retry_base_delay_ms: 500, + init_retry_jitter_fraction: 0.25, + init_retry_max_delay_ms: 4000, + http_timeout_ms: 10_000, + uuid_dedup_buffer_size: 2000, + heartbeat_interval_ms: 20_000, + heartbeat_jitter_fraction: 0.1, + token_refresh_buffer_ms: 300_000, + teardown_archive_timeout_ms: 1500, + connect_timeout_ms: 15_000, + min_version: '0.0.0', + should_show_app_upgrade_message: false, +} +``` + +Validation ranges: +- `init_retry_max_attempts`: 1–10 +- `http_timeout_ms`: min 2000ms +- `heartbeat_interval_ms`: 5000–30,000ms +- `heartbeat_jitter_fraction`: 0–0.5 +- `token_refresh_buffer_ms`: 30,000–1,800,000ms +- `teardown_archive_timeout_ms`: 500–2000ms +- `connect_timeout_ms`: 5,000–60,000ms + +### trustedDevice.ts + +Manages the trusted device token for ELEVATED security tier bridge sessions. + +```typescript +getTrustedDeviceToken(): string | undefined +clearTrustedDeviceTokenCache(): void +clearTrustedDeviceToken(): void +enrollTrustedDevice(): Promise +``` + +**Gate:** `tengu_sessions_elevated_auth_enforcement`. When gate is off, `getTrustedDeviceToken()` returns `undefined` unconditionally. + +**Storage:** macOS keychain via `getSecureStorage()` (memoized — keychain spawn is ~40ms). + +**Token precedence:** `CLAUDE_TRUSTED_DEVICE_TOKEN` env var > keychain. + +**Enrollment:** `POST /api/auth/trusted_devices` with display name `"Claude Code on {hostname()} · {platform}"`. Must be called within 10 minutes of login. Best-effort — never throws. On success, persists to keychain and clears memo cache. + +### codeSessionApi.ts + +Thin HTTP wrappers for CCR V2 code-session API. + +```typescript +createCodeSession( + baseUrl, accessToken, title, timeoutMs, tags? +): Promise +// POST /v1/code/sessions → session.id (cse_*) +// Body: { title, bridge: {}, tags?: [...] } + +type RemoteCredentials = { + worker_jwt: string + api_base_url: string + expires_in: number // Seconds + worker_epoch: number +} + +fetchRemoteCredentials( + sessionId, baseUrl, accessToken, timeoutMs, trustedDeviceToken? +): Promise +// POST /v1/code/sessions/{id}/bridge +``` + +`worker_epoch` is parsed defensively (protojson may return int64 as string). + +### sessionRunner.ts (full createSessionSpawner) + +The `createSessionSpawner()` function (referenced from `bridgeMain.ts`) creates a `SessionSpawner` that: +1. Calls `spawn(child_process)` with the Claude binary and appropriate flags. +2. Parses child stdout as NDJSON, routing `control_request` messages to permission callbacks. +3. Tracks `SessionActivity` in a ring buffer of size 10. +4. Tracks last 10 stderr lines. +5. Watches child exit to resolve `handle.done` with `'completed'` (exit 0) or `'failed'` (exit != 0) or `'interrupted'` (SIGTERM/SIGKILL). + +--- + +## 20. CLI Framework + +### cli/exit.ts + +```typescript +cliError(msg?: string): never // stderr + process.exit(1) +cliOk(msg?: string): never // stdout + process.exit(0) +``` + +Centralized CLI exit helpers. `cliError` uses `console.error`; `cliOk` uses `process.stdout.write`. The `never` return type allows TypeScript to narrow control flow at call sites. + +### cli/ndjsonSafeStringify.ts + +```typescript +ndjsonSafeStringify(value: unknown): string +``` + +JSON serializer that escapes `U+2028` (LINE SEPARATOR) and `U+2029` (PARAGRAPH SEPARATOR) as `\u2028`/`\u2029`. These are valid line terminators in JavaScript (ECMA-262 §11.3) and would break line-splitting NDJSON receivers. The escaped form is still valid JSON. + +### cli/handlers/auth.ts + +```typescript +installOAuthTokens(tokens: OAuthTokens): Promise +authLogin({ email?, sso?, console?, claudeai? }): Promise +authStatus({ json?, text? }): Promise +authLogout(): Promise +``` + +**`installOAuthTokens()`** performs post-token acquisition: +1. `performLogout({ clearOnboarding: false })` — clears old state +2. Fetches OAuth profile or falls back to `tokenAccount` +3. Calls `storeOAuthAccountInfo()` +4. `saveOAuthTokensIfNeeded()` + `clearOAuthTokenCache()` +5. `fetchAndStoreUserRoles()` (best-effort) +6. For claude.ai auth: `fetchAndStoreClaudeCodeFirstTokenDate()` (best-effort) +7. For Console auth: `createAndStoreApiKey()` (required — throws if fails) +8. `clearAuthRelatedCaches()` + +**`authLogin()`** fast path: When `CLAUDE_CODE_OAUTH_REFRESH_TOKEN` env var is set, exchanges directly via `refreshOAuthToken()`, skipping browser OAuth flow. Requires `CLAUDE_CODE_OAUTH_SCOPES` env var (space-separated scopes). + +**`authStatus()`** JSON output fields: +```json +{ + "loggedIn": boolean, + "authMethod": "none" | "claude.ai" | "api_key_helper" | "oauth_token" | "api_key" | "third_party", + "apiProvider": string, + "apiKeySource": string, // Present when apiKey + "email": string | null, // Present when claude.ai + "orgId": string | null, + "orgName": string | null, + "subscriptionType": string | null +} +``` + +### cli/handlers/agents.ts + +```typescript +agentsHandler(): Promise +``` + +Lists configured agents grouped by source. Output format: `agentType · model · memory` per agent. Shows shadowed agents (overridden by a higher-priority source) with `(shadowed by {source})` prefix. + +### cli/handlers/autoMode.ts + +```typescript +autoModeDefaultsHandler(): void // Dumps default auto mode rules as JSON +autoModeConfigHandler(): void // Dumps effective config (user settings OR defaults) +autoModeCritiqueHandler({ model? }): Promise // AI critique of user rules +``` + +**Auto mode rule categories:** `allow`, `soft_deny`, `environment`. + +**Critique uses `sideQuery()`** with a dedicated system prompt that asks Claude to evaluate rules for clarity, completeness, conflicts, and actionability. + +### cli/handlers/mcp.tsx (partial) + +```typescript +mcpServeHandler({ debug?, verbose? }): Promise +mcpRemoveHandler(name, { scope? }): Promise +// ... plus add, get, list, reset, import, desktop-import handlers +``` + +### cli/handlers/plugins.ts (partial) + +Handlers for `claude plugin *` and marketplace commands: +- `installPlugin`, `uninstallPlugin`, `enablePlugin`, `disablePlugin` +- `listPlugins`, `marketplaceSearch`, `addMarketplace`, `removeMarketplace` + +### cli/print.ts + +The main SDK `-p` (print mode) handler. Orchestrates: +- `StructuredIO` / `RemoteIO` for I/O +- Tool pool assembly +- Message queue management +- Session state notifications +- Optional bridge enablement via `enableRemoteControl` + +### cli/update.ts + +```typescript +update(): Promise +``` + +Handles `claude update`. Checks current version, detects install type, selects updater (npm global, native binary, local). Shows warnings for multiple installations. + +--- + +## 21. CLI Transports + +See [Section 9 — Transport Layer](#9-transport-layer) for the transport hierarchy. + +### cli/remoteIO.ts — RemoteIO class + +```typescript +class RemoteIO extends StructuredIO { + constructor(streamUrl: string, initialPrompt?, replayUserMessages?) +} +``` + +Bidirectional streaming for SDK mode. Extends `StructuredIO`. + +**Constructor behavior:** +1. Creates `PassThrough` input stream. +2. Reads `CLAUDE_CODE_SESSION_ACCESS_TOKEN` for initial auth headers. +3. Reads `CLAUDE_CODE_ENVIRONMENT_RUNNER_VERSION` for `x-environment-runner-version` header. +4. Creates `refreshHeaders` closure that re-reads the token dynamically on reconnects. +5. Calls `getTransportForUrl()` to get transport (WS, Hybrid, or SSE). + +**Keep-alive:** When `session_keepalive_interval_v2_ms > 0` and transport is SSE/v2, sends silent `{type:'keep_alive'}` frames at that interval to prevent upstream proxy idle timeouts. + +**State/metadata listeners:** Wires `setSessionStateChangedListener` and `setSessionMetadataChangedListener` to propagate `SessionState` changes to the CCRClient via `reportState()` and `reportMetadata()`. + +**Command lifecycle:** Wires `setCommandLifecycleListener` to fire `reportDelivery('processing')` on command start and `reportDelivery('processed')` on command end. + +### cli/structuredIO.ts — StructuredIO class + +```typescript +class StructuredIO { + constructor(inputStream: Readable, replayUserMessages?) + // Handles SDK control message parsing (control_request/control_response) + // Permission handling via can_use_tool protocol + // Elicitation dialog support + // Hook system integration +} + +const SANDBOX_NETWORK_ACCESS_TOOL_NAME = 'SandboxNetworkAccess' +``` + +`StructuredIO` is the base class providing: +- NDJSON message deserialization from stdin +- `can_use_tool` permission request handling +- `control_response` dispatching +- Elicitation dialog flow (`SDKControlElicitationResponseSchema`) +- Hook execution before permission decisions + +--- + +## 22. Remote Session System + +### remote/SessionsWebSocket.ts — SessionsWebSocket + +WebSocket client for viewing sessions via `/v1/sessions/ws/{id}/subscribe`. + +**Connection Protocol:** +1. Connect to `wss://api.anthropic.com/v1/sessions/ws/{sessionId}/subscribe?organization_uuid={orgUuid}` +2. Send auth message: + ```json + { "type": "auth", "credential": { "type": "oauth", "token": "" } } + ``` +3. Receive `SessionsMessage` stream. + +**Configuration:** +```typescript +RECONNECT_DELAY_MS = 2000 +MAX_RECONNECT_ATTEMPTS = 5 +PING_INTERVAL_MS = 30000 +MAX_SESSION_NOT_FOUND_RETRIES = 3 // 4001 can be transient during compaction +PERMANENT_CLOSE_CODES = { 4003 } // unauthorized — stops reconnecting +``` + +**Note:** `4001` (session not found) is handled separately with up to 3 limited retries because compaction can briefly cause false "not found" responses. + +**Callbacks:** +```typescript +type SessionsWebSocketCallbacks = { + onMessage: (message: SessionsMessage) => void + onClose?: () => void // Permanent close only + onError?: (error: Error) => void + onConnected?: () => void + onReconnecting?: () => void // Transient drop with reconnect scheduled +} +``` + +**Message types accepted:** Any object with a string `type` field (open-ended to avoid dropping new server message types). + +### remote/RemoteSessionManager.ts — RemoteSessionManager + +Coordinates WebSocket subscription + HTTP POST + permission flow. + +```typescript +type RemoteSessionConfig = { + sessionId: string + getAccessToken: () => string + orgUuid: string + hasInitialPrompt?: boolean + viewerOnly?: boolean // Pure viewer: no interrupt, no title update, no 60s reconnect timeout +} + +type RemotePermissionResponse = + | { behavior: 'allow'; updatedInput: Record } + | { behavior: 'deny'; message: string } +``` + +**Callbacks:** +```typescript +type RemoteSessionCallbacks = { + onMessage: (message: SDKMessage) => void + onPermissionRequest: (request, requestId) => void + onPermissionCancelled?: (requestId, toolUseId) => void + onConnected?: () => void + onDisconnected?: () => void + onReconnecting?: () => void + onError?: (error: Error) => void +} +``` + +### remote/remotePermissionBridge.ts + +```typescript +createSyntheticAssistantMessage( + request: SDKControlPermissionRequest, + requestId: string, +): AssistantMessage +``` + +Creates a synthetic `AssistantMessage` wrapping a remote `tool_use` for the permission dialog. Uses a fake message ID `remote-{requestId}` and empty usage stats. + +```typescript +createToolStub(toolName: string): Tool +``` + +Creates a minimal `Tool` stub for tools unknown to the local CLI (e.g., MCP tools running on CCR). Routes to `FallbackPermissionRequest`. The stub's `renderToolUseMessage()` shows up to 3 input key-value pairs. + +### remote/sdkMessageAdapter.ts + +Converts `SDKMessage` from CCR to REPL `Message` types. + +```typescript +type ConvertedMessage = + | { type: 'message'; message: Message } + | { type: 'stream_event'; event: StreamEvent } + | { type: 'ignored' } + +type ConvertOptions = { + convertToolResults?: boolean // For direct-connect mode + convertUserTextMessages?: boolean // For historical event conversion +} + +convertSDKMessage(msg: SDKMessage, opts?: ConvertOptions): ConvertedMessage +isSessionEndMessage(msg: SDKMessage): boolean // msg.type === 'result' +isSuccessResult(msg: SDKResultMessage): boolean // msg.subtype === 'success' +getResultText(msg: SDKResultMessage): string | null +``` + +**Conversion rules:** + +| SDKMessage type | Converted to | +|-----------------|-------------| +| `assistant` | `AssistantMessage` | +| `user` (tool_result, convertToolResults=true) | `UserMessage` | +| `user` (text, convertUserTextMessages=true) | `UserMessage` | +| `user` (other) | `ignored` | +| `stream_event` | `StreamEvent` | +| `result` (success) | `ignored` | +| `result` (error) | `SystemMessage` (warning) | +| `system` (init) | `SystemMessage` (info): "Remote session initialized (model: ...)" | +| `system` (status: compacting) | `SystemMessage` (info): "Compacting conversation…" | +| `system` (compact_boundary) | `SystemMessage` (compact_boundary) | +| `tool_progress` | `SystemMessage` (info): "Tool {name} running for {n}s…" | +| `auth_status`, `tool_use_summary`, `rate_limit_event` | `ignored` | +| unknown | `ignored` (logged) | + +--- + +## 23. replLauncher.tsx + +**File:** `src/replLauncher.tsx` + +```typescript +type AppWrapperProps = { + getFpsMetrics: () => FpsMetrics | undefined + stats?: StatsStore + initialState: AppState +} + +launchRepl( + root: Root, + appProps: AppWrapperProps, + replProps: REPLProps, + renderAndRun: (root: Root, element: React.ReactNode) => Promise, +): Promise +``` + +Lazy-loads `App` and `REPL` components and renders them wrapped in the React tree. Used by `main.tsx` to defer loading of the heavy UI component tree (App.js + REPL.js) until it's actually needed for interactive mode. + +--- + +## 24. Configuration Defaults & GrowthBook Flags + +### GrowthBook Feature Flags (Bridge) + +| Flag | Type | Default | Purpose | +|------|------|---------|---------| +| `tengu_ccr_bridge` | boolean | `false` | Master gate: enables Remote Control | +| `tengu_bridge_repl_v2` | boolean | `false` | Enables env-less (V2) REPL bridge | +| `tengu_bridge_repl_v2_cse_shim_enabled` | boolean | `true` | `cse_*` → `session_*` retag shim | +| `tengu_bridge_min_version` | DynamicConfig `{minVersion}` | `'0.0.0'` | Min CLI version for V1 bridge | +| `tengu_bridge_repl_v2_config` | DynamicConfig (EnvLessBridgeConfig) | See defaults | V2 bridge timing config | +| `tengu_bridge_poll_interval_config` | DynamicConfig (PollIntervalConfig) | See defaults | Poll intervals | +| `tengu_ccr_bridge_multi_session` | boolean | N/A | Enables multi-session spawn modes | +| `tengu_sessions_elevated_auth_enforcement` | boolean | `false` | Enables trusted device requirement | +| `tengu_cobalt_harbor` | boolean | `false` | Auto-connect CCR on startup | +| `tengu_ccr_mirror` | boolean | `false` | CCR mirror mode | + +### Build Flags (bun:bundle features) + +| Flag | Purpose | +|------|---------| +| `BRIDGE_MODE` | Enables bridge-related code paths | +| `CCR_AUTO_CONNECT` | Enables `getCcrAutoConnectDefault()` | +| `CCR_MIRROR` | Enables `isCcrMirrorEnabled()` | +| `BASH_CLASSIFIER` | Enables bash classifier reason serialization | +| `TRANSCRIPT_CLASSIFIER` | Enables transcript classifier reason serialization | + +### Environment Variables (Bridge) + +| Variable | Purpose | +|----------|---------| +| `CLAUDE_BRIDGE_OAUTH_TOKEN` | Ant-only: override OAuth token for bridge | +| `CLAUDE_BRIDGE_BASE_URL` | Ant-only: override API base URL | +| `CLAUDE_TRUSTED_DEVICE_TOKEN` | Override trusted device token from env | +| `CLAUDE_CODE_USE_CCR_V2` | Use SSETransport + CCRClient for all sessions | +| `CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2` | Use HybridTransport (WS reads + POST writes) | +| `CLAUDE_CODE_CCR_MIRROR` | Enable CCR mirror mode | +| `CLAUDE_CODE_SESSION_ACCESS_TOKEN` | Session ingress auth token | +| `CLAUDE_CODE_ENVIRONMENT_RUNNER_VERSION` | Sent as `x-environment-runner-version` header | +| `CLAUDE_CODE_OAUTH_REFRESH_TOKEN` | Fast-path login via token exchange | +| `CLAUDE_CODE_OAUTH_SCOPES` | Required when `CLAUDE_CODE_OAUTH_REFRESH_TOKEN` is set | + +--- + +## 25. Complete API Endpoint Reference + +### Environments API (`/v1/environments/bridge`) + +All environment API calls require: +- `anthropic-version: 2023-06-01` +- `anthropic-beta: environments-2025-11-01` +- `x-environment-runner-version: ` +- `Authorization: Bearer ` +- Optional: `X-Trusted-Device-Token: ` + +#### POST `/v1/environments/bridge` + +Register a bridge environment. + +**Request:** +```json +{ + "machine_name": "hostname", + "directory": "/path/to/dir", + "branch": "main", + "git_repo_url": "https://github.com/owner/repo", + "max_sessions": 4, + "metadata": { "worker_type": "claude_code" }, + "environment_id": "" // Optional: for re-registration +} +``` + +**Response:** +```json +{ + "environment_id": "env_abc123", + "environment_secret": "secret_xyz" +} +``` + +**Timeout:** 15s + +#### GET `/v1/environments/{environmentId}/work/poll` + +Poll for new work. Auth: `environmentSecret`. + +**Query params:** +- `reclaim_older_than_ms` (optional): Reclaim unacknowledged work older than this + +**Response:** `WorkResponse | null` (null = no work available) + +**Timeout:** 10s + +#### POST `/v1/environments/{environmentId}/work/{workId}/ack` + +Acknowledge a work item. Auth: `sessionToken` (session ingress JWT). + +**Timeout:** 10s + +#### POST `/v1/environments/{environmentId}/work/{workId}/heartbeat` + +Send heartbeat. Auth: `sessionToken` (session ingress JWT). + +**Response:** +```json +{ + "lease_extended": true, + "state": "running", + "last_heartbeat": "2025-03-31T...", + "ttl_seconds": 300 +} +``` + +**Timeout:** 10s + +#### POST `/v1/environments/{environmentId}/work/{workId}/stop` + +Stop a work item. Auth: OAuth token. + +**Request:** `{ "force": true|false }` + +**Timeout:** 10s + +#### DELETE `/v1/environments/bridge/{environmentId}` + +Deregister environment. Auth: OAuth token. + +**Timeout:** 10s + +#### POST `/v1/environments/{environmentId}/bridge/reconnect` + +Force re-dispatch a session. Auth: OAuth token. + +**Request:** `{ "session_id": "cse_abc123" }` + +**Timeout:** 10s + +### Sessions API (`/v1/sessions`) + +All calls require `anthropic-beta: ccr-byoc-2025-07-29` and `x-organization-uuid: `. + +#### POST `/v1/sessions` + +Create a session. + +**Request:** +```json +{ + "title": "My Session", + "events": [{ "type": "event", "data": }], + "session_context": { + "sources": [{ "type": "git_repository", "url": "...", "revision": "main" }], + "outcomes": [...], + "model": "claude-opus-4" + }, + "environment_id": "env_abc123", + "source": "remote-control", + "permission_mode": "auto" +} +``` + +**Response:** `{ "id": "session_abc123" }` + +#### GET `/v1/sessions/{sessionId}` + +Fetch session metadata. Returns `{ environment_id?, title? }`. + +#### PATCH `/v1/sessions/{sessionId}` + +Update session title. **Request:** `{ "title": "New Title" }` + +#### POST `/v1/sessions/{sessionId}/archive` + +Archive a session. Returns `409` if already archived (idempotent). + +#### POST `/v1/sessions/{sessionId}/events` + +Send events to a session (used for permission responses). Auth: session ingress token. + +**Request:** +```json +{ + "events": [ + { + "type": "control_response", + "response": { + "subtype": "success", + "request_id": "req_abc", + "response": { "behavior": "allow" } + } + } + ] +} +``` + +### CCR V2 Code Sessions API + +#### POST `/v1/code/sessions` + +**Request:** +```json +{ + "title": "Bridge Session", + "bridge": {}, + "tags": ["optional", "tags"] +} +``` + +**Response:** `{ "session": { "id": "cse_abc123" } }` + +#### POST `/v1/code/sessions/{sessionId}/bridge` + +Register as bridge worker and get JWT. + +**Optional header:** `X-Trusted-Device-Token` + +**Response:** +```json +{ + "worker_jwt": "sk-ant-si-...", + "api_base_url": "https://...", + "expires_in": 18000, + "worker_epoch": "42" +} +``` + +**Note:** Each call bumps `worker_epoch` — this call IS the registration. + +#### POST `/v1/code/sessions/{sessionId}/worker/register` + +Register as CCR worker (V1 CCR v2 path). Returns `{ "worker_epoch": "42" }`. + +#### GET `/v1/code/sessions/{sessionId}/worker/events/stream` + +SSE stream for receiving inbound events. Sends `Last-Event-ID` or `from_sequence_num` for resumption. + +**SSE frame format:** +``` +event: sdk_event +id: 42 +data: {"event_id":"evt_abc","payload":{...}} + +:keepalive + +``` + +#### POST `/v1/code/sessions/{sessionId}/worker/events` + +Post events to the session. Auth: worker JWT. + +#### PUT `/v1/code/sessions/{sessionId}/worker` + +Update worker state. + +**Request:** +```json +{ + "worker_status": "running" | "requires_action" | "completed", + "external_metadata": { "key": "value" }, + "internal_metadata": { "key": "value" } +} +``` + +**Metadata merge:** RFC 7396 — keys added/overwritten, `null` values = server-side delete. + +#### POST `/v1/code/sessions/{sessionId}/worker/events/{eventId}/delivery` + +Report event delivery status. **Request:** `{ "status": "received" | "processing" | "processed" }` + +### Auth / Trusted Device + +#### POST `/api/auth/trusted_devices` + +Enroll device for elevated security sessions. + +**Request:** +```json +{ "display_name": "Claude Code on hostname · darwin" } +``` + +**Response:** +```json +{ "device_token": "...", "device_id": "..." } +``` + +**Gate:** Must be called within 10 minutes of login. + +### File Attachments + +#### GET `/api/oauth/files/{fileUuid}/content` + +Download attachment content. Auth: OAuth token. Returns binary file data. + +### Sessions WebSocket (Remote Viewer) + +#### WS `/v1/sessions/ws/{sessionId}/subscribe?organization_uuid={orgUuid}` + +Connect as viewer. After connect, send: +```json +{ "type": "auth", "credential": { "type": "oauth", "token": "" } } +``` + +--- + +## 26. WebSocket & SSE Protocol Reference + +### Session-Ingress WebSocket Protocol + +**URL:** `wss://api.anthropic.com/v1/session_ingress/ws/{sessionId}` + +**V1 read/write:** Messages in both directions are NDJSON (`StdoutMessage` / `StdinMessage`). + +**Permanent close codes (client stops retrying):** +- `1002` — protocol error +- `4001` — session expired/not found +- `4003` — unauthorized + +**Keep-alive frame:** `{"type":"keep_alive"}` — sent by client at `DEFAULT_KEEPALIVE_INTERVAL` (5 min). + +**Ping/pong:** Client sends ping every 10s, expects pong within 10s. Connection recycled on pong timeout. + +### SSE Event Format + +``` +event: sdk_event +id: +data: + +:keepalive + +``` + +- `id` field: monotonic sequence number used for resume (`Last-Event-ID`) +- `event` field: event type (`sdk_event`, `keep_alive`, etc.) +- `data` field: JSON object with `event_id` and `payload` + +### Control Message Protocol + +**SDKControlRequest** (server → client): +```json +{ + "type": "control_request", + "request_id": "req_abc", + "request": { + "subtype": "initialize" | "set_model" | "set_max_thinking_tokens" | "set_permission_mode" | "interrupt" | "can_use_tool", + ...subtype-specific fields... + } +} +``` + +**SDKControlResponse** (client → server): +```json +{ + "type": "control_response", + "session_id": "session_abc", + "response": { + "subtype": "success" | "error", + "request_id": "req_abc", + ...response payload... + } +} +``` + +**SDKControlCancelRequest** (server → client): +```json +{ + "type": "control_cancel_request", + "request_id": "req_abc" +} +``` + +### Permission Request Flow + +1. Child CLI emits `control_request` with `subtype: 'can_use_tool'` on stdout. +2. Bridge receives it via `onPermissionRequest` callback. +3. Bridge forwards to server via `POST /v1/sessions/{id}/events` (permission response event). +4. Claude.ai displays approval dialog. +5. User approves/denies. +6. Server sends `control_response` back through the WebSocket. +7. Bridge calls `onPermissionResponse` callback. +8. Child CLI receives the decision on stdin and proceeds. + +**Permission response payload:** +```json +{ + "behavior": "allow" | "deny", + "updatedInput": {...}, // Optional: modified tool input + "updatedPermissions": [...], // Optional: new permission rules + "message": "..." // Optional: deny message +} +``` + +### NDJSON Safety + +All NDJSON-format messages (used in child process stdio) must escape `U+2028` and `U+2029` as `\u2028`/`\u2029` (see `ndjsonSafeStringify`). These are valid JSON but are JavaScript line terminators that can break line-splitting receivers. + +--- + +*Document generated from source analysis of Claude Code codebase, 2026-03-31.* diff --git a/spec/10_utils.md b/spec/10_utils.md new file mode 100644 index 0000000..5f9f1ae --- /dev/null +++ b/spec/10_utils.md @@ -0,0 +1,2182 @@ +# Claude Code — Utilities + +This document covers the entire `src/utils/` directory (564 files across 36 subdirectories) plus `src/interactiveHelpers.tsx` and `src/ink.ts`. + +## Table of Contents + +1. [Core Data Structures](#1-core-data-structures) +2. [Shell & Process Execution](#2-shell--process-execution) +3. [Bash Parsing & Analysis](#3-bash-parsing--analysis) +4. [Configuration & Settings](#4-configuration--settings) +5. [Authentication & Auth Management](#5-authentication--auth-management) +6. [Permissions System](#6-permissions-system) +7. [File & Filesystem Utilities](#7-file--filesystem-utilities) +8. [Git Utilities](#8-git-utilities) +9. [Session Management](#9-session-management) +10. [Message & Content Utilities](#10-message--content-utilities) +11. [Model Utilities](#11-model-utilities) +12. [MCP Utilities](#12-mcp-utilities) +13. [Swarm / Multi-Agent System](#13-swarm--multi-agent-system) +14. [Task & Output Management](#14-task--output-management) +15. [Process User Input](#15-process-user-input) +16. [Hooks System](#16-hooks-system) +17. [Analytics & Telemetry](#17-analytics--telemetry) +18. [Background & Cron Systems](#18-background--cron-systems) +19. [Computer Use](#19-computer-use) +20. [Claude-in-Chrome](#20-claude-in-chrome) +21. [Deep Link System](#21-deep-link-system) +22. [Plugin Utilities](#22-plugin-utilities) +23. [Swarm Backends](#23-swarm-backends) +24. [Skills & Suggestions](#24-skills--suggestions) +25. [Todo System](#25-todo-system) +26. [Teleport System](#26-teleport-system) +27. [Memory Utilities](#27-memory-utilities) +28. [Sandbox Utilities](#28-sandbox-utilities) +29. [Security & Secure Storage](#29-security--secure-storage) +30. [Native Installer](#30-native-installer) +31. [General Utilities (Top-Level)](#31-general-utilities-top-level) +32. [interactiveHelpers.tsx](#32-interactivehelperstsx) +33. [ink.ts (Top-Level Module)](#33-inkts-top-level-module) +34. [utils/shell/ — Shell Provider Abstraction](#34-utilsshell--shell-provider-abstraction) +35. [utils/ultraplan/ — Ultraplan Remote Session Polling](#35-utilsultraplan--ultraplan-remote-session-polling) + +--- + +## 1. Core Data Structures + +### `CircularBuffer.ts` +Fixed-size circular buffer that auto-evicts oldest items when full. + +```typescript +export class CircularBuffer { + constructor(private capacity: number) + add(item: T): void // Adds item, evicts oldest if full + addAll(items: T[]): void // Bulk add + getRecent(count: number): T[] // N most recent items + toArray(): T[] // All items oldest-to-newest + clear(): void + length(): number +} +``` + +### `QueryGuard.ts` +Synchronous state machine for query lifecycle (`idle` / `dispatching` / `running`). Compatible with React's `useSyncExternalStore`. + +```typescript +export class QueryGuard { + reserve(): boolean // idle → dispatching; returns false if not idle + cancelReservation(): void // dispatching → idle + tryStart(): number | null // → running; returns generation or null if running + end(generation: number): boolean // running → idle; returns false if stale + forceEnd(): void // kills any running query + get isActive(): boolean // true for dispatching OR running + get generation(): number + subscribe: (listener) => () => void // useSyncExternalStore subscribe + getSnapshot: () => boolean // useSyncExternalStore snapshot +} +``` + +### `signal.ts` +Lightweight event signal primitive (no state storage, just notifications). + +```typescript +export type Signal = { + subscribe: (listener: (...args: Args) => void) => () => void + emit: (...args: Args) => void + clear: () => void +} + +export function createSignal(): Signal +``` +Used ~15× across the codebase for pub/sub of state-change events. + +### `memoize.ts` +Advanced memoization utilities beyond lodash. + +```typescript +// Write-through cache with TTL and background refresh +export function memoizeWithTTL( + f: (...args: Args) => Result, + cacheLifetimeMs?: number // default 5 minutes +): MemoizedFunction + +// Async version with in-flight dedup +export function memoizeWithTTLAsync( + f: (...args: Args) => Promise, + cacheLifetimeMs?: number +): ((...args) => Promise) & { cache: { clear() } } + +// LRU-eviction memoize (prevents unbounded memory growth) +export function memoizeWithLRU( + f: (...args: Args) => Result, + cacheFn: (...args: Args) => string, // key generator + maxCacheSize?: number // default 100 +): LRUMemoizedFunction +``` + +### `generators.ts` +Async generator utilities. + +```typescript +export async function lastX(as: AsyncGenerator): Promise +export async function returnValue(as: AsyncGenerator): Promise +export async function* all( + generators: AsyncGenerator[], + concurrencyCap?: number // default Infinity +): AsyncGenerator +export async function toArray(generator: AsyncGenerator): Promise +export async function* fromArray(values: T[]): AsyncGenerator +``` + +### `array.ts` +Array utility helpers (deduplicate, group, etc.). + +### `set.ts` +Set utility helpers. + +### `sequential.ts` +Sequential execution helpers. + +### `objectGroupBy.ts` +`Object.groupBy` polyfill/wrapper. + +--- + +## 2. Shell & Process Execution + +### `Shell.ts` +Core shell execution engine. The most important utility file. + +**Exports:** +```typescript +export type ShellConfig = { provider: ShellProvider } +export type ExecOptions = { + timeout?: number + onProgress?: (lastLines, allLines, totalLines, totalBytes, isIncomplete) => void + preventCwdChanges?: boolean + shouldUseSandbox?: boolean + shouldAutoBackground?: boolean + onStdout?: (data: string) => void // pipe mode callback +} + +export async function findSuitableShell(): Promise +// Priority: CLAUDE_CODE_SHELL env > $SHELL > zsh > bash + +export const getShellConfig: typeof getShellConfigImpl // memoized +export const getPsProvider: () => Promise // memoized + +export async function exec( + command: string, + abortSignal: AbortSignal, + shellType: ShellType, + options?: ExecOptions +): Promise + +export function setCwd(path: string, relativeTo?: string): void +export type { ExecResult } from './ShellCommand.js' +``` + +**Key Implementation Details:** +- DEFAULT_TIMEOUT = 30 minutes +- File mode (bash): stdout+stderr both go to a single file fd (atomic O_APPEND) +- Pipe mode (hooks, `onStdout`): data flows through StreamWrapper → TaskOutput in-memory +- CWD tracking: captures `pwd -P` output to temp file after each command +- Sandbox wrapping: calls `SandboxManager.wrapWithSandbox()` when `shouldUseSandbox` +- Sandboxed PowerShell uses `/bin/sh` as outer shell with base64-encoded inner command +- After command: reads cwd file, updates state, notifies hooks, cleans up +- Windows path handling via `posixPathToWindowsPath()` + +### `ShellCommand.ts` +Wraps a child process into a `ShellCommand` interface. + +**Exports:** +```typescript +export type ExecResult = { + stdout: string + stderr: string + code: number + interrupted: boolean + backgroundTaskId?: string + backgroundedByUser?: boolean + assistantAutoBackgrounded?: boolean + outputFilePath?: string // when stdout was too large for inline + outputFileSize?: number + outputTaskId?: string + preSpawnError?: string +} + +export type ShellCommand = { + background(backgroundTaskId: string): boolean // runningbg + result: Promise + kill(): void + status: 'running' | 'backgrounded' | 'completed' | 'killed' + cleanup(): void // removes event listeners + onTimeout?: (callback: (backgroundFn) => void) => void + taskOutput: TaskOutput +} + +export function wrapSpawn( + childProcess: ChildProcess, + abortSignal: AbortSignal, + timeout: number, + taskOutput: TaskOutput, + shouldAutoBackground?: boolean, + maxOutputBytes?: number +): ShellCommand + +export function createAbortedCommand( + backgroundTaskId?: string, + opts?: { stderr?: string; code?: number } +): ShellCommand + +export function createFailedCommand(preSpawnError: string): ShellCommand +``` + +**Key Details:** +- SIGKILL = 137, SIGTERM = 143 +- SIZE_WATCHDOG_INTERVAL_MS = 5,000ms — kills backgrounded processes over disk limit +- Uses `treeKill` to kill entire process trees +- Abort reason `'interrupt'` allows backgrounding instead of killing +- StreamWrapper: thin pipe from ChildProcess stdout/stderr → TaskOutput (pipe mode only) + +### `abortController.ts` +Memory-safe AbortController utilities using WeakRef. + +```typescript +export function createAbortController(maxListeners?: number): AbortController + +export function createChildAbortController( + parent: AbortController, + maxListeners?: number +): AbortController +// Child aborts when parent aborts (not vice versa) +// Uses WeakRef — dropped children can be GC'd +// Auto-removes parent listener when child aborts +``` + +### `combinedAbortSignal.ts` +Creates an AbortSignal that fires when any of multiple signals fires. + +### `execFileNoThrow.ts` / `execFileNoThrowPortable.ts` +```typescript +export async function execFileNoThrow( + file: string, args?: string[], options?: ExecFileOptions +): Promise<{ stdout: string; stderr: string; code: number }> + +export function execSyncWithDefaults_DEPRECATED( + command: string +): string | null +``` + +### `execSyncWrapper.ts` +Synchronous shell execution wrapper with error handling. + +### `genericProcessUtils.ts` +Generic child process utilities (spawn, kill, etc.). + +### `gracefulShutdown.ts` +```typescript +export function gracefulShutdown(exitCode?: number): Promise +export function gracefulShutdownSync(exitCode?: number): never +export function registerShutdownHandler(fn: () => void | Promise): void +``` + +### `process.ts` +Process-level utilities. + +### `subprocessEnv.ts` +```typescript +export function subprocessEnv(): NodeJS.ProcessEnv +// Returns filtered process.env suitable for subprocess spawning +``` + +--- + +## 3. Bash Parsing & Analysis + +### `bash/ParsedCommand.ts` +```typescript +export type ParsedCommand = { + command: string + args: string[] + redirects: Redirect[] + // ... more fields +} +export function parseBashCommand(input: string): ParsedCommand | null +``` + +### `bash/ShellSnapshot.ts` +Captures shell environment state (env vars, aliases) for reproduction. + +### `bash/ast.ts` +Bash AST node types for the parser. + +### `bash/bashParser.ts` +Full bash command parser. Handles pipes, redirections, compound commands. + +### `bash/bashPipeCommand.ts` +Represents a pipe-connected command chain. + +### `bash/commands.ts` +```typescript +export function extractOutputRedirections(command: string): string[] +export function getCommandName(command: string): string +export function isDestructiveCommand(command: string): boolean +``` + +### `bash/heredoc.ts` +Heredoc detection and parsing. + +### `bash/parser.ts` +Low-level token parser for bash syntax. + +### `bash/prefix.ts` +Handles shell variable prefix syntax (`VAR=value cmd`). + +### `bash/registry.ts` +Registry of known bash commands and their semantics. + +### `bash/shellCompletion.ts` +Shell completion suggestions for partial commands. + +### `bash/shellPrefix.ts` +```typescript +export function formatShellPrefixCommand(command: string, envFile: string): string +``` + +### `bash/shellQuote.ts` / `bash/shellQuoting.ts` +Shell quoting utilities to safely embed strings in shell commands. + +### `bash/specs/` +Command-specific completion specs: `alias.ts`, `nohup.ts`, `pyright.ts`, `sleep.ts`, `srun.ts`, `time.ts`, `timeout.ts`. + +### `bash/treeSitterAnalysis.ts` +Tree-sitter based deep bash AST analysis for command classification. + +--- + +## 4. Configuration & Settings + +### `config.ts` +The central configuration file. Manages global config (per-user), project config (per-project), and session config. + +**Key Types:** +```typescript +export type PastedContent = { + id: number; type: 'text' | 'image'; content: string + mediaType?: string; filename?: string; dimensions?: ImageDimensions + sourcePath?: string +} + +export interface HistoryEntry { + display: string + pastedContents: Record +} + +export type ReleaseChannel = 'stable' | 'latest' + +export type ProjectConfig = { + allowedTools: string[] + mcpContextUris: string[] + mcpServers?: Record + // + many more fields +} + +export type GlobalConfig = { + // Account info, oauth tokens, model preferences, theme, etc. +} + +export type AccountInfo = { + emailAddress: string + // + subscription/billing fields +} +``` + +**Key Exports:** +```typescript +export function getGlobalConfig(): GlobalConfig +export function saveGlobalConfig(update: (current: GlobalConfig) => GlobalConfig): void +export function getProjectConfig(cwd?: string): ProjectConfig +export function saveProjectConfig(update: (current: ProjectConfig) => ProjectConfig, cwd?: string): void +export function checkHasTrustDialogAccepted(): boolean +export function getCustomApiKeyStatus(): 'user' | 'project' | 'env' | 'none' +export function normalizePathForConfigKey(p: string): string +``` + +### `configConstants.ts` +Config-related constants (file names, paths, defaults). + +### `settings/settings.ts` +User settings with multi-source merge (user file, project file, MDM, managed settings). + +**Key Exports:** +```typescript +export function getSettings_DEPRECATED(): SettingsJson +export function getSettingsForSource(source: SettingSource): SettingsJson +export function saveSettings(update: (current: SettingsJson) => SettingsJson, source?: EditableSettingSource): void +export function hasAutoModeOptIn(): boolean +export function hasSkipDangerousModePermissionPrompt(): boolean +``` + +### `settings/types.ts` +`SettingsJson` and `SettingsSchema` (Zod schema) defining all user-configurable settings. + +### `settings/constants.ts` +```typescript +export type SettingSource = 'userSettings' | 'projectSettings' | 'sessionSettings' | 'managed' | 'mdm' | 'remoteManagedSettings' +export type EditableSettingSource = 'userSettings' | 'projectSettings' | 'sessionSettings' +export const SETTING_SOURCES: SettingSource[] +``` + +### `settings/validation.ts` +Settings validation (Zod-based), permission rule filtering, validation tips. + +### `settings/settingsCache.ts` +Multi-layer settings cache keyed by source. + +### `settings/changeDetector.ts` +Detects when settings change and fires notifications. + +### `settings/applySettingsChange.ts` +Applies incremental settings updates. + +### `settings/mdm/` +MDM (Mobile Device Management) policy settings: +- `settings.ts`: reads from Windows HKCU registry and macOS plist +- `rawRead.ts`: low-level MDM config reading +- `constants.ts`: MDM-specific constants + +### `settings/managedPath.ts` +```typescript +export function getManagedFilePath(): string +export function getManagedSettingsDropInDir(): string +``` + +### `settings/allErrors.ts` +```typescript +export function getSettingsWithAllErrors(): SettingsWithErrors +``` + +### `settings/permissionValidation.ts` +Validates permission rule configurations in settings. + +### `settings/toolValidationConfig.ts` +Tool-level validation configuration. + +### `settings/validateEditTool.ts` +Validates EditTool-specific settings. + +### `settings/schemaOutput.ts` +Generates JSON Schema from the settings Zod schema. + +### `settings/pluginOnlyPolicy.ts` +Enforces that certain settings can only be configured via plugins. + +--- + +## 5. Authentication & Auth Management + +### `auth.ts` +Main authentication orchestration. Handles API key, OAuth, and AWS auth. + +**Key Exports:** +```typescript +export function getSubscriptionType(): SubscriptionType | null +export function isClaudeAISubscriber(): boolean +export function isProSubscriber(): boolean +export function isMaxSubscriber(): boolean +export function isTeamPremiumSubscriber(): boolean +export async function getAuthHeaders(): Promise> +export async function logout(): Promise +export async function refreshAuth(): Promise +``` + +### `authPortable.ts` +Cross-platform auth utilities. + +```typescript +export function normalizeApiKeyForConfig(key: string): string +export async function maybeRemoveApiKeyFromMacOSKeychainThrows(): Promise +``` + +### `authFileDescriptor.ts` +Reads API key / OAuth token from file descriptor (for CI/headless use). + +```typescript +export function getApiKeyFromFileDescriptor(): string | null +export function getOAuthTokenFromFileDescriptor(): OAuthTokens | null +``` + +### `aws.ts` +AWS credential management and STS caller identity checking. + +```typescript +export async function checkStsCallerIdentity(profile?: string): Promise +export function isValidAwsStsOutput(output: string): boolean +export function clearAwsIniCache(): void +``` + +### `awsAuthStatusManager.ts` +```typescript +export class AwsAuthStatusManager { + getStatus(): AwsAuthStatus + // Manages AWS auth lifecycle with refresh +} +``` + +### `betas.ts` +Beta feature flag management tied to account subscription. + +```typescript +export function clearBetasCaches(): void +export async function getBetaFeatures(): Promise +``` + +### `billing.ts` +Billing-related utilities (subscription type detection, extra-usage checks). + +--- + +## 6. Permissions System + +### `permissions/permissions.ts` +Core permission checking engine. + +**Key Logic:** +- Integrates `TRANSCRIPT_CLASSIFIER` feature flag for AI-based permission decisions +- Checks permission rules from settings, project config, session config +- Handles `bypassPermissionsKillswitch` +- Manages permission modes (normal, bypass, plan, auto) +- Returns `PermissionDecision` with reason for UI display + +**Key Exports:** +```typescript +export async function getPermissionDecision( + tool: Tool, + input: AnyObject, + context: ToolPermissionContext +): Promise + +export async function applyPermissionUpdate( + update: PermissionUpdate, + destination: PermissionUpdateDestination +): Promise + +export async function applyPermissionUpdates(updates: PermissionUpdate[]): Promise +export async function persistPermissionUpdates(updates: PermissionUpdate[]): Promise +export function deletePermissionRuleFromSettings(rule: PermissionRuleFromEditableSettings): void +``` + +### `permissions/PermissionMode.ts` +```typescript +export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' +export function permissionModeTitle(mode: PermissionMode): string +``` + +### `permissions/PermissionResult.ts` +```typescript +export type PermissionDecision = + | PermissionAskDecision // { type: 'ask', message, ... } + | PermissionDenyDecision // { type: 'deny', reason, ... } + | { type: 'allow' } + | { type: 'allowWithSandbox' } +``` + +### `permissions/PermissionRule.ts` +```typescript +export type PermissionBehavior = 'allow' | 'deny' +export type PermissionRuleValue = string // e.g., "Bash(rm:*)" +export type PermissionRule = { + ruleValue: PermissionRuleValue + behavior: PermissionBehavior + source: PermissionRuleSource +} +``` + +### `permissions/PermissionUpdate.ts` / `PermissionUpdateSchema.ts` +Types and logic for updating permission rules in settings. + +### `permissions/bashClassifier.ts` +Classifies bash commands for permission decisions (uses patterns and optionally AI). + +### `permissions/yoloClassifier.ts` +Classifier for "bypass permissions" mode — auto-approves most commands. + +### `permissions/classifierDecision.ts` +AI-based transcript classifier for permission decisions (feature-gated by `TRANSCRIPT_CLASSIFIER`). + +### `permissions/classifierShared.ts` +Shared types and utilities for classifiers. + +### `permissions/dangerousPatterns.ts` +List of dangerous bash patterns that always require explicit approval. + +```typescript +export const DANGEROUS_PATTERNS: RegExp[] +export function hasDangerousPattern(command: string): boolean +``` + +### `permissions/denialTracking.ts` +Tracks recent permission denials for UI display in `/permissions recent-denials`. + +### `permissions/filesystem.ts` +```typescript +export function getClaudeTempDirName(): string +export function validateFilesystemAccess(path: string, mode: 'read' | 'write'): boolean +``` + +### `permissions/getNextPermissionMode.ts` +State machine transitions for permission mode cycling. + +### `permissions/pathValidation.ts` +Validates file paths against workspace directories. + +### `permissions/permissionExplainer.ts` +Generates human-readable explanations for permission decisions. + +### `permissions/permissionRuleParser.ts` +```typescript +export function permissionRuleValueFromString(s: string): PermissionRuleValue +export function permissionRuleValueToString(v: PermissionRuleValue): string +``` + +### `permissions/permissionSetup.ts` +Initial permission setup at session start. + +### `permissions/permissionsLoader.ts` +Loads and merges permission rules from all sources. + +### `permissions/shadowedRuleDetection.ts` +Detects when a permission rule is shadowed/overridden by another rule. + +### `permissions/shellRuleMatching.ts` +Matches shell commands against permission rule patterns (glob-style). + +### `permissions/autoModeState.ts` +State management for auto-approve mode. + +### `permissions/bypassPermissionsKillswitch.ts` +Emergency killswitch for bypass-permissions mode. + +--- + +## 7. File & Filesystem Utilities + +### `file.ts` +Core file utilities. + +```typescript +export async function pathExists(p: string): Promise +export function writeFileSyncAndFlush_DEPRECATED(path: string, content: string): void +export async function safeReadFile(path: string): Promise +``` + +### `fileRead.ts` +```typescript +export function readFileSync(path: string, options?: { encoding?: 'utf8' }): string +// NFC-normalizes content on read (macOS APFS compat) +``` + +### `fileReadCache.ts` +Content-addressed file read cache with invalidation. + +### `fileStateCache.ts` +Caches file stat information to avoid repeated fs.stat calls. + +### `fsOperations.ts` +```typescript +export function getFsImplementation(): FsImplementation +// Returns real fs or test mock + +export async function readFileRange( + path: string, start: number, end: number +): Promise + +export async function tailFile( + path: string, maxBytes?: number +): Promise +``` + +### `fileHistory.ts` +File edit history tracking for `/rewind` and undo operations. + +### `fileOperationAnalytics.ts` +Tracks file operation analytics (reads, writes, edits per turn). + +### `filePersistence/filePersistence.ts` +Persistence of file state across sessions. + +### `filePersistence/outputsScanner.ts` +Scans output files for persistence. + +### `readFileInRange.ts` +Reads a specific byte range from a file. + +### `readEditContext.ts` +Builds context for file read/edit operations (surrounding lines, etc.). + +### `generatedFiles.ts` +Detection of generated/auto-generated files (to skip in certain operations). + +### `path.ts` +Path manipulation utilities. + +```typescript +export function normalizePathForConfigKey(p: string): string +export function toRelativePath(absolute: string, base?: string): string +export function expandHome(p: string): string +``` + +### `xdg.ts` +XDG base directory spec utilities. + +### `systemDirectories.ts` +Platform-specific system directory paths. + +### `cachePaths.ts` +Cache directory paths for various cached data. + +### `tempfile.ts` +```typescript +export async function createTempFile(suffix?: string): Promise +export function cleanupTempFiles(): void +``` + +### `lockfile.ts` +File-based distributed locking. + +```typescript +export async function lock(path: string, options?: LockOptions): Promise<() => void> +export function tryLock(path: string): boolean +``` + +### `cleanupRegistry.ts` +```typescript +export function registerCleanup(fn: () => void | Promise): void +export async function runCleanup(): Promise +``` + +### `cleanup.ts` +Session cleanup utilities. + +### `glob.ts` +Glob pattern matching utilities (wraps glob library). + +### `ripgrep.ts` +Ripgrep integration for file content searching. + +--- + +## 8. Git Utilities + +### `git.ts` +Primary git utility module. + +**Key Exports:** +```typescript +export function findGitRoot(startPath?: string): string | null +export function findCanonicalGitRoot(startPath?: string): string | null +export async function getGitBranch(cwd?: string): Promise +export async function getGitStatus(cwd?: string): Promise +export async function getGitDiff(args?: string[]): Promise +export async function isGitRepo(cwd?: string): Promise +``` + +Uses LRU memoization (`memoizeWithLRU`) for root detection. +NFC-normalizes paths for macOS APFS compatibility. + +### `git/gitFilesystem.ts` +Low-level git filesystem operations. + +```typescript +export function getCachedBranch(cwd: string): string | null +export function getCachedHead(cwd: string): string | null +export function getCachedDefaultBranch(cwd: string): string | null +export function getCachedRemoteUrl(cwd: string): string | null +export function getWorktreeCountFromFs(cwd: string): number +export function isShallowClone(cwd: string): boolean +export function resolveGitDir(cwd: string): string | null +``` + +### `git/gitConfigParser.ts` +Parses `.git/config` files. + +### `git/gitignore.ts` +```typescript +export async function addFileGlobRuleToGitignore( + pattern: string, + gitRoot: string +): Promise +export function isIgnoredByGitignore(path: string, cwd: string): boolean +``` + +### `gitDiff.ts` +Git diff parsing and formatting utilities. + +### `gitSettings.ts` +Git configuration utilities for Claude Code settings. + +### `ghPrStatus.ts` +GitHub PR status checking via `gh` CLI. + +### `github/ghAuthStatus.ts` +GitHub authentication status checking. + +### `githubRepoPathMapping.ts` +Maps GitHub repo URLs to local filesystem paths. + +### `detectRepository.ts` +Detects repository type (git, mercurial, etc.) and metadata. + +### `getWorktreePaths.ts` / `getWorktreePathsPortable.ts` +```typescript +export async function getWorktreePaths(): Promise +// Lists all git worktree paths for the current repo +``` + +### `worktree.ts` +Git worktree management utilities. + +### `worktreeModeEnabled.ts` +Feature flag check for worktree mode. + +### `commitAttribution.ts` +Adds Claude Code attribution to git commits. + +--- + +## 9. Session Management + +### `sessionStorage.ts` +Core session storage — reading/writing JSONL transcript files. + +**Key Exports:** +```typescript +export function getTranscriptPathForSession(sessionId: string, cwd?: string): string +export function getAgentTranscriptPath(agentId: AgentId, sessionId: string): string +export async function appendToSession(entry: Entry): Promise +export async function readSession(sessionId: string): Promise +export async function listSessions(cwd?: string): Promise +export async function deleteSession(sessionId: string): Promise +export function switchSession(newSessionId: string): void +``` + +### `sessionStoragePortable.ts` +Portable version of session storage (cross-platform paths). + +### `sessionState.ts` +In-memory session state (current session ID, title, etc.). + +### `sessionStart.ts` +Session initialization logic. + +### `sessionRestore.ts` +Restores a session from its JSONL transcript. + +### `sessionActivity.ts` +Tracks session activity for idle detection. + +### `sessionEnvVars.ts` +Environment variables injected into the session context. + +### `sessionEnvironment.ts` +```typescript +export function getHookEnvFilePath(): string +export function invalidateSessionEnvCache(): void +export async function getSessionEnvironmentVariables(): Promise> +``` + +### `sessionFileAccessHooks.ts` +Hooks for tracking which files were accessed in a session. + +### `sessionIngressAuth.ts` +Auth for incoming session connections (bridge, remote). + +### `sessionTitle.ts` +```typescript +export async function generateSessionTitle(messages: Message[]): Promise +``` + +### `sessionUrl.ts` +URL construction for remote session references. + +### `listSessionsImpl.ts` +Implementation of session listing with sorting and filtering. + +### `agenticSessionSearch.ts` +Search within session transcripts. + +### `crossProjectResume.ts` +Handles resuming sessions across different project directories. + +### `concurrentSessions.ts` +Manages multiple concurrent Claude Code sessions. + +### `cwd.ts` +```typescript +export function pwd(): string // Current working directory +export function getCwd(): string // Alias +export function isValidCwd(path: string): boolean +``` + +--- + +## 10. Message & Content Utilities + +### `messages.ts` +Core message manipulation — the most-used utility in the codebase. + +**Key Exports:** +```typescript +export function getContentText(content: ContentBlockParam[]): string +export function normalizeMessages(messages: Message[]): NormalizedMessage[] +export function isUserMessage(msg: Message): msg is UserMessage +export function isAssistantMessage(msg: Message): msg is AssistantMessage +export function extractToolUses(messages: Message[]): ToolUseBlock[] +export function getLastAssistantMessage(messages: Message[]): AssistantMessage | null +export function createSystemMessage(text: string): SystemMessage +export function createUserMessage(content: ContentBlockParam[]): UserMessage +``` + +### `contentArray.ts` +Utilities for working with `ContentBlockParam[]` arrays. + +### `messagePredicates.ts` +Type guards and predicates for message types. + +### `messageQueueManager.ts` +Queue management for batched message processing. + +### `messages/` (subdirectory) +Message-category-specific utilities: +- `messages/` — subdirectory with specialized message handling utilities + +### `attachments.ts` +Manages message attachments (images, files, pastes). + +```typescript +export function createAttachmentMessage(content: AttachmentContent): AttachmentMessage +export function getAttachmentMessages(messages: Message[]): AttachmentMessage[] +export type AgentMentionAttachment = { agentId: string; agentName: string } +``` + +### `collapseBackgroundBashNotifications.ts` +Collapses multiple bash progress notifications into summaries. + +### `collapseHookSummaries.ts` +Collapses hook execution summaries. + +### `collapseReadSearch.ts` +Collapses file read/search tool uses for compaction. + +### `collapseTeammateShutdowns.ts` +Collapses teammate shutdown messages. + +### `groupToolUses.ts` +Groups sequential tool use messages for display. + +### `controlMessageCompat.ts` +Compatibility layer for control messages across versions. + +### `directMemberMessage.ts` +Direct messaging between swarm members. + +### `images` utilities: +- `imagePaste.ts` — clipboard image paste handling +- `imageResizer.ts` — resizes/downsamples large images +- `imageStore.ts` — temporary image storage keyed by UUID +- `imageValidation.ts` — validates image format/size +- `imageProcessor` (in FileReadTool) — processes image files for model input + +### `pdf.ts` / `pdfUtils.ts` +PDF file handling — extracts text, validates, handles password protection. + +### `notebook.ts` +Jupyter notebook utilities (.ipynb parsing, cell editing). + +--- + +## 11. Model Utilities + +### `model/model.ts` +Model selection and configuration. + +```typescript +export type ModelShortName = string +export type ModelName = string +export type ModelSetting = ModelName | ModelAlias | null + +export function getSmallFastModel(): ModelName // ANTHROPIC_SMALL_FAST_MODEL or haiku +export function isNonCustomOpusModel(model: ModelName): boolean +export function getModelToUse(options?: ModelOptions): ModelName +// Priority: session override > --model flag > ANTHROPIC_MODEL env > settings > default +``` + +### `model/aliases.ts` +```typescript +export type ModelAlias = 'claude-sonnet' | 'claude-haiku' | 'claude-opus' | ... +export function isModelAlias(s: string): s is ModelAlias +export function resolveModelAlias(alias: ModelAlias): ModelName +``` + +### `model/modelStrings.ts` +```typescript +export function getModelStrings(): { + opus40: string; opus41: string; opus45: string; opus46: string + sonnet45: string; sonnet46: string + haiku35: string; haiku40: string + // ... all model IDs, some conditionally included (ANT-only) +} +export function resolveOverriddenModel(model: ModelSetting): ModelName +``` + +### `model/modelOptions.ts` +Model option types and construction. + +### `model/modelCapabilities.ts` +```typescript +export function supportsVision(model: ModelName): boolean +export function supportsExtendedThinking(model: ModelName): boolean +export function supports1MContext(model: ModelName): boolean +export function modelSupportsAdvisor(model: ModelName): boolean +``` + +### `model/configs.ts` +Per-model configuration (temperature, top_p, max_tokens, etc.). + +### `model/providers.ts` +```typescript +export type APIProvider = 'anthropic' | 'bedrock' | 'vertex' +export function getAPIProvider(): APIProvider +``` + +### `model/bedrock.ts` +AWS Bedrock-specific model ID conversions. + +### `model/antModels.ts` +Internal ANT-only model names (dead-code-eliminated in external builds). + +### `model/validateModel.ts` +```typescript +export function validateModel(model: string): ModelName +export function isValidAdvisorModel(model: string): boolean +``` + +### `model/modelAllowlist.ts` +Runtime allowlist of models (from settings/GrowthBook). + +### `model/modelSupportOverrides.ts` +Per-model support overrides for edge cases. + +### `model/deprecation.ts` +Model deprecation notices and migration paths. + +### `model/contextWindowUpgradeCheck.ts` +Checks if 1M context upgrade is available. + +### `model/check1mAccess.ts` +Validates 1M context access for current subscription. + +### `model/agent.ts` +Agent-specific model selection. + +### `modelCost.ts` +```typescript +export function formatModelPricing(model: ModelName): string +export function getOpus46CostTier(): 'standard' | 'premium' +``` + +### `context.ts` +Context window management. + +```typescript +export function has1mContext(): boolean +export function is1mContextDisabled(): boolean +export function modelSupports1M(model: ModelName): boolean +``` + +--- + +## 12. MCP Utilities + +### `utils/mcp/dateTimeParser.ts` +Parses date/time values from MCP tool inputs. + +### `utils/mcp/elicitationValidation.ts` +Validates MCP elicitation schemas. + +### `mcpInstructionsDelta.ts` +Diff-based MCP server instructions management. + +### `mcpOutputStorage.ts` +Storage for MCP tool output. + +### `mcpValidation.ts` +Validates MCP server configurations. + +### `mcpWebSocketTransport.ts` +WebSocket transport implementation for MCP servers. + +--- + +## 13. Swarm / Multi-Agent System + +### `swarm/constants.ts` +Swarm system constants (max workers, timeouts, etc.). + +### `swarm/inProcessRunner.ts` +Runs swarm workers in the same process (no separate terminal). + +### `swarm/spawnInProcess.ts` +Spawns in-process swarm workers. + +### `swarm/spawnUtils.ts` +Common utilities for spawning swarm workers. + +### `swarm/leaderPermissionBridge.ts` +Bridges permission decisions from leader to workers. + +### `swarm/permissionSync.ts` +Synchronizes permission state across swarm members. + +### `swarm/reconnection.ts` +Handles reconnection logic when swarm workers disconnect. + +### `swarm/teamHelpers.ts` +Helper utilities for team coordination. + +### `swarm/teammateInit.ts` +Initialization sequence for swarm teammates. + +### `swarm/teammateLayoutManager.ts` +Manages terminal layout for swarm teammates. + +### `swarm/teammateModel.ts` +Model selection for swarm teammates. + +### `swarm/teammatePromptAddendum.ts` +Adds context to teammate prompts. + +### `swarm/backends/types.ts` +```typescript +export type PaneBackend = { + spawn(config: SpawnConfig): Promise + kill(pane: PaneHandle): Promise + sendInput(pane: PaneHandle, text: string): Promise + // ... +} +``` + +### `swarm/backends/TmuxBackend.ts` +Tmux-based pane backend for swarm. + +### `swarm/backends/ITermBackend.ts` +iTerm2-based pane backend for swarm. + +### `swarm/backends/InProcessBackend.ts` +In-process (no terminal) backend. + +### `swarm/backends/PaneBackendExecutor.ts` +Executes commands in pane backends. + +### `swarm/backends/detection.ts` +Detects available pane backends (tmux, iTerm2, etc.). + +### `swarm/backends/registry.ts` +Registry of available backends. + +### `swarm/backends/teammateModeSnapshot.ts` +Snapshots teammate mode state. + +### `swarm/backends/it2Setup.ts` / `It2SetupPrompt.tsx` +iTerm2 setup flow for swarm. + +### `agentSwarmsEnabled.ts` +```typescript +export function isAgentSwarmsEnabled(): boolean +``` + +### `inProcessTeammateHelpers.ts` +Helpers for in-process teammate coordination. + +### `teammate.ts` / `teammateContext.ts` / `teammateMailbox.ts` +Teammate state, context, and messaging utilities. + +### `teamDiscovery.ts` +Discovers available teammates in the session. + +### `teamMemoryOps.ts` +Team memory read/write operations. + +### `standaloneAgent.ts` +Runs a standalone agent session programmatically. + +### `forkedAgent.ts` +Handles forked agent sessions. + +--- + +## 14. Task & Output Management + +### `task/TaskOutput.ts` +Single source of truth for a shell command's output. + +```typescript +export class TaskOutput { + readonly taskId: string + readonly path: string // Output file path + readonly stdoutToFile: boolean // true = file mode, false = pipe mode + + constructor(taskId: string, onProgress: ProgressCallback | null, stdoutToFile?: boolean) + + writeStdout(data: string): void // pipe mode only + writeStderr(data: string): void // pipe mode only + async getStdout(): Promise + getStderr(): string + get outputFileRedundant(): boolean + get outputFileSize(): number + spillToDisk(): void // flush in-memory buffer to disk + + clear(): void + async deleteOutputFile(): Promise + + // Static registry for polling + static startPolling(taskId: string): void + static stopPolling(taskId: string): void +} +``` + +Uses `CircularBuffer(1000)` for recent lines in file mode. +Polling interval: 1000ms for progress callbacks. +Max in-memory: 8MB before spilling to disk. + +### `task/diskOutput.ts` +```typescript +export const MAX_TASK_OUTPUT_BYTES: number // e.g., 5MB +export const MAX_TASK_OUTPUT_BYTES_DISPLAY: string +export class DiskTaskOutput { ... } +export function getTaskOutputPath(taskId: string): string +export function getTaskOutputDir(): string +``` + +### `task/framework.ts` +Task execution framework — tracks running/completed/failed tasks. + +### `task/outputFormatting.ts` +Formats task output for display (truncation, highlighting). + +### `task/sdkProgress.ts` +SDK-level progress reporting for tasks. + +### `tasks.ts` (top-level) +Task management utilities. + +```typescript +export function getActiveTasks(): ActiveTask[] +export function getTaskById(id: string): ActiveTask | null +``` + +--- + +## 15. Process User Input + +### `processUserInput/processUserInput.ts` +Main entry point for user input processing. + +```typescript +export async function processUserInput( + input: UserInput, + context: ProcessContext +): Promise +``` +Handles dispatch to bash, slash command, or text prompt processors. + +### `processUserInput/processBashCommand.tsx` +Handles `!command` syntax — executes shell commands inline. + +### `processUserInput/processSlashCommand.tsx` +Handles `/command` syntax — finds and dispatches slash commands. + +### `processUserInput/processTextPrompt.ts` +Handles regular text prompts — creates user messages for the model. + +### `handlePromptSubmit.ts` +Orchestrates prompt submission with hooks, attachments, queue management. + +### `slashCommandParsing.ts` +```typescript +export function parseSlashCommand(input: string): { name: string; args: string } | null +``` + +### `promptShellExecution.ts` +`executeShellCommandsInPrompt()` — expands `!`backtick syntax in prompt commands. + +--- + +## 16. Hooks System + +### `hooks.ts` +Core hooks execution engine. Runs user-configured lifecycle hooks. + +**Key Exports:** +```typescript +export type HookEvent = + | 'PreToolUse' | 'PostToolUse' + | 'UserPromptSubmit' | 'Notification' + | 'Stop' | 'SubagentStop' + +export async function executePreToolUseHooks( + toolName: string, input: AnyObject, context: HookContext +): Promise + +export async function executePostToolUseHooks( + toolName: string, input: AnyObject, output: AnyObject, context: HookContext +): Promise + +export async function executeUserPromptSubmitHooks( + prompt: string, context: HookContext +): Promise + +export function getUserPromptSubmitHookBlockingMessage(result: HookResult): string | null + +export async function executeNotificationHooks( + message: string +): Promise +``` + +**Implementation:** +- Reads hooks from settings (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Notification`, `Stop`) +- Uses `wrapSpawn()` for execution (pipe mode) +- Injects env vars: `CLAUDE_TOOL_NAME`, `CLAUDE_TOOL_INPUT`, `TOOL_OUTPUT`, etc. +- Respects `shouldAllowManagedHooksOnly()` and `shouldDisableAllHooksIncludingManaged()` +- Plugin options substituted via `substituteUserConfigVariables()` + +### `hooks/fileChangedWatcher.ts` +Watches for file changes triggered by hooks. + +```typescript +export async function onCwdChangedForHooks(oldCwd: string, newCwd: string): Promise +``` + +### `hooks/AsyncHookRegistry.ts` +Registry for async hook handlers. + +### `hooks/hooksConfigSnapshot.ts` +Cached hooks configuration snapshot. + +--- + +## 17. Analytics & Telemetry + +### `telemetry/` subdirectory +- `telemetry/events.ts` — OpenTelemetry event logging +- Related to OTel span/event emission + +### `telemetryAttributes.ts` +Standard telemetry attribute names. + +### `headlessProfiler.ts` / `profilerBase.ts` +Performance profiling utilities for headless mode. + +### `startupProfiler.ts` +```typescript +export function profileCheckpoint(name: string): void +export function getStartupProfile(): StartupProfile +``` + +### `queryProfiler.ts` +Profiles individual query performance. + +### `slowOperations.ts` +```typescript +export function jsonParse(s: string): T +export function jsonStringify(v: unknown): string +export function clone(v: T): T +// These ops are tracked for performance monitoring +``` + +### `fpsTracker.ts` +Frame rate tracking for UI performance. + +### `heatmap.ts` +Records usage heatmap data for UX analytics. + +### `unaryLogging.ts` +Unary gRPC event logging. + +### `stats.ts` / `statsCache.ts` +Session statistics (token usage, tool calls, etc.). + +--- + +## 18. Background & Cron Systems + +### `cron.ts` / `cronScheduler.ts` +Core cron job scheduling. + +```typescript +export function scheduleCron( + id: string, + schedule: string, + fn: () => Promise +): void +export function cancelCron(id: string): void +``` + +### `cronTasks.ts` +Standard cron task definitions (housekeeping, analytics uploads, etc.). + +### `cronTasksLock.ts` +Distributed locking for cron tasks. + +### `cronJitterConfig.ts` +Adds jitter to cron schedules to prevent thundering herd. + +### `background/remote/remoteSession.ts` +Background remote session management. + +### `background/remote/preconditions.ts` +Precondition checks for remote sessions. + +### `backgroundHousekeeping.ts` +Background cleanup tasks (temp files, old sessions, etc.). + +--- + +## 19. Computer Use + +### `computerUse/common.ts` +Common computer use types and utilities. + +### `computerUse/executor.ts` +Executes computer use actions (mouse clicks, typing, screenshots). + +### `computerUse/gates.ts` +Feature gates for computer use (entitlement checks). + +### `computerUse/hostAdapter.ts` +Platform-specific host adapters (macOS, Linux). + +### `computerUse/setup.ts` / `computerUse/setupPortable.ts` +Computer use setup and initialization. + +### `computerUse/mcpServer.ts` +Exposes computer use capabilities as an MCP server. + +### `computerUse/toolRendering.tsx` +React components for rendering computer use tool results. + +### `computerUse/wrapper.tsx` +React wrapper for computer use sessions. + +### `computerUse/inputLoader.ts` +Loads computer use input configurations. + +### `computerUse/swiftLoader.ts` +macOS Swift bridge for computer use. + +### `computerUse/drainRunLoop.ts` +Drains the macOS run loop for proper screenshot timing. + +### `computerUse/escHotkey.ts` +ESC hotkey handler for computer use sessions. + +### `computerUse/computerUseLock.ts` +Prevents concurrent computer use sessions. + +### `computerUse/cleanup.ts` +Cleans up computer use resources. + +### `computerUse/appNames.ts` +Platform-specific application name detection. + +--- + +## 20. Claude-in-Chrome + +### `claudeInChrome/common.ts` +Common types for Claude-in-Chrome integration. + +### `claudeInChrome/chromeNativeHost.ts` +Native host messaging protocol for Chrome extension communication. + +### `claudeInChrome/mcpServer.ts` +Exposes Chrome tab content as MCP resources. + +### `claudeInChrome/prompt.ts` +System prompt extensions for Chrome integration. + +### `claudeInChrome/setup.ts` / `setupPortable.ts` +Chrome native host setup. + +### `claudeInChrome/toolRendering.tsx` +React rendering for Chrome-specific tool results. + +--- + +## 21. Deep Link System + +### `deepLink/parseDeepLink.ts` +```typescript +export function parseDeepLink(url: string): DeepLinkAction | null +// Parses claude://... URLs +``` + +### `deepLink/protocolHandler.ts` +Registers and handles the `claude://` protocol. + +### `deepLink/registerProtocol.ts` +Registers the deep link protocol with the OS. + +### `deepLink/banner.ts` +Shows deep link activation banners. + +### `deepLink/terminalLauncher.ts` +Launches terminal sessions from deep links. + +### `deepLink/terminalPreference.ts` +```typescript +export function updateDeepLinkTerminalPreference(terminal: string): void +export function getPreferredTerminal(): string | null +``` + +### `desktopDeepLink.ts` +Desktop app deep link integration. + +--- + +## 22. Plugin Utilities + +### `plugins/` subdirectory (in utils) +- `pluginOptionsStorage.ts` — stores/retrieves plugin configuration +- `pluginDirectories.ts` — returns plugin installation directories +- `schemas.ts` — plugin manifest Zod schemas + +### `dxt/helpers.ts` +DXT (Desktop Extension) package helpers. + +### `dxt/zip.ts` +ZIP archive utilities for DXT packages. + +--- + +## 23. Swarm Backends +(Covered in Section 13) + +--- + +## 24. Skills & Suggestions + +### `skills/` subdirectory +Skill-specific utilities: +- `skillUsageTracking.ts` — tracks usage frequency per skill + +### `suggestions/commandSearch.ts` +```typescript +export function searchCommands( + query: string, + commands: Command[] +): SuggestionItem[] +// Uses Fuse.js with memoized index (keyed by commands array identity) +``` + +### `contextSuggestions.ts` +AI-driven context suggestions for the prompt input. + +### `suggestionFiltering.ts` +Filters suggestions by relevance. + +--- + +## 25. Todo System + +### `todo/types.ts` +```typescript +export type TodoItem = { + id: string + content: string + status: 'pending' | 'in_progress' | 'completed' + priority: 'high' | 'medium' | 'low' +} +export type TodoList = TodoItem[] +``` + +--- + +## 26. Teleport System + +### `teleport/api.ts` +API calls for the Teleport remote execution system. + +### `teleport/environmentSelection.ts` +Selects the remote execution environment. + +### `teleport/environments.ts` +Available Teleport environments configuration. + +### `teleport/gitBundle.ts` +Creates and transfers git bundles for Teleport sessions. + +### `teleport.tsx` +```typescript +export async function teleportToRemote( + options: TeleportOptions, + root: Root +): Promise +// Launches a remote Claude Code session via CCR +``` + +--- + +## 27. Memory Utilities + +### `memory/types.ts` +```typescript +export type MemoryType = 'project' | 'user' | 'team' +export type MemoryFile = { + path: string + type: MemoryType + content: string +} +``` + +### `memoryFileDetection.ts` +Detects and loads relevant memory files for the current context. + +### `claudemd.ts` +CLAUDE.md file management (reading, writing, external includes). + +```typescript +export function getMemoryFiles(cwd?: string): MemoryFile[] +export function getExternalClaudeMdIncludes(cwd?: string): string[] +export function shouldShowClaudeMdExternalIncludesWarning(): boolean +``` + +### `markdownConfigLoader.ts` +Loads configuration from CLAUDE.md-style markdown files. + +--- + +## 28. Sandbox Utilities + +### `sandbox/sandbox-adapter.ts` +```typescript +export class SandboxManager { + static async wrapWithSandbox( + command: string, + shellBin: string, + options?: SandboxOptions, + abortSignal?: AbortSignal + ): Promise + static cleanupAfterCommand(): void + static async refreshConfig(): Promise +} +``` + +Uses Linux bwrap or macOS sandbox-exec depending on platform. + +### `sandbox/` (other files) +- Sandbox configuration management +- Platform-specific sandbox profile generation + +--- + +## 29. Security & Secure Storage + +### `secureStorage/` subdirectory +Secure credential storage (macOS Keychain, Windows Credential Manager, Linux secret service). + +### `crypto.ts` +```typescript +export function generateSecureToken(length?: number): string +export function hashString(s: string): string +``` + +### `fingerprint.ts` +Device fingerprinting for trust management. + +### `hash.ts` +Content hashing utilities. + +### `uuid.ts` +```typescript +export function generateUUID(): string +export function generateSessionId(): string +``` + +### `taggedId.ts` +Type-safe tagged ID types (prevents mixing SessionId, AgentId, etc.). + +--- + +## 30. Native Installer + +### `nativeInstaller/` subdirectory +Native binary installation utilities. + +### `localInstaller.ts` +Installs Claude Code to local paths. + +### `autoUpdater.ts` +Auto-update checking and installation. + +--- + +## 31. General Utilities (Top-Level) + +### Environment & Platform +| File | Purpose | +|------|---------| +| `env.ts` | Environment variable constants and getters | +| `envDynamic.ts` | Dynamically-loaded env vars | +| `envUtils.ts` | `isEnvTruthy()`, `isBareMode()`, `isRunningOnHomespace()`, `getClaudeConfigHomeDir()` | +| `envValidation.ts` | Validates required env vars | +| `platform.ts` | `getPlatform(): 'macos' \| 'linux' \| 'windows'` | +| `bundledMode.ts` | `isBundledMode(): boolean` | +| `managedEnv.ts` | Managed environment variable injection from settings | +| `managedEnvConstants.ts` | Constants for managed env vars | + +### String & Formatting +| File | Purpose | +|------|---------| +| `stringUtils.ts` | `capitalize()`, `plural()`, `safeJoinLines()`, etc. | +| `format.ts` | `formatDuration()`, `formatBytes()`, etc. | +| `formatBriefTimestamp.ts` | Brief timestamp formatting | +| `truncate.ts` | Text truncation utilities | +| `words.ts` | Word counting and manipulation | +| `sliceAnsi.ts` | Slices ANSI-escaped strings without breaking escape codes | +| `cliHighlight.ts` | Syntax highlighting for CLI output | +| `markdown.ts` | Markdown rendering utilities | +| `hyperlink.ts` | Terminal hyperlink (OSC 8) utilities | +| `intl.ts` | Internationalization utilities | +| `sanitization.ts` | Input sanitization | + +### JSON & Data +| File | Purpose | +|------|---------| +| `json.ts` | `safeParseJSON()`, JSON read/write | +| `jsonRead.ts` | `stripBOM()`, safe JSON file reading | +| `yaml.ts` | YAML parsing utilities | +| `xml.ts` | XML parsing utilities | +| `zodToJsonSchema.ts` | Converts Zod schemas to JSON Schema | +| `lazySchema.ts` | Deferred Zod schema validation | +| `frontmatterParser.ts` | Parses YAML frontmatter from markdown | +| `semanticBoolean.ts` | Parses "truthy" strings (`'true'`, `'yes'`, `'1'`) | +| `semanticNumber.ts` | Parses number strings with units | + +### HTTP & Networking +| File | Purpose | +|------|---------| +| `http.ts` | Fetch wrappers with timeout and retry | +| `proxy.ts` | HTTP proxy configuration | +| `mtls.ts` | mTLS certificate management | +| `caCerts.ts` | Custom CA certificate loading | +| `caCertsConfig.ts` | CA cert configuration | +| `peerAddress.ts` | Peer address utilities | +| `mcpWebSocketTransport.ts` | MCP WebSocket transport | +| `apiPreconnect.ts` | Pre-connects to API for latency reduction | +| `userAgent.ts` | HTTP User-Agent string construction | + +### API & Claude Communication +| File | Purpose | +|------|---------| +| `api.ts` | High-level API utilities | +| `queryContext.ts` | Query context construction | +| `queryHelpers.ts` | Query helper functions | +| `tokenBudget.ts` | Token budget management | +| `tokens.ts` | Token counting utilities | +| `stream.ts` | Streaming response utilities | +| `streamJsonStdoutGuard.ts` | Guards against JSON corruption in stream | +| `streamlinedTransform.ts` | Transforms streaming API responses | +| `thinking.ts` | Extended thinking utilities | +| `sideQuery.ts` | Side-channel query execution | +| `sideQuestion.ts` | Side-question display | +| `sdkEventQueue.ts` | SDK event queue management | + +### Session & Context +| File | Purpose | +|------|---------| +| `systemPrompt.ts` | `buildEffectiveSystemPrompt()` — assembles the system prompt from multiple sources | +| `systemPromptType.ts` | `SystemPrompt` type and `asSystemPrompt()` | +| `agentContext.ts` | Agent execution context | +| `agentId.ts` | Agent ID generation | +| `context.ts` | Context window utilities | +| `contextAnalysis.ts` | Analyzes context window usage | +| `analyzeContext.ts` | Deeper context analysis | +| `workloadContext.ts` | Workload context for load balancing | +| `queryContext.ts` | Query context construction | + +### Display & UI +| File | Purpose | +|------|---------| +| `displayTags.ts` | UI display tags | +| `logoV2Utils.ts` | Logo V2 utilities | +| `status.tsx` | Status display utilities | +| `statusNoticeDefinitions.tsx` | Status notice type definitions | +| `statusNoticeHelpers.ts` | Status notice helpers | +| `treeify.ts` | Renders tree structures as ASCII | +| `highlightMatch.tsx` | Highlights search matches in text | +| `textHighlighting.ts` | Text highlighting utilities | +| `horizontalScroll.ts` | Horizontal scroll state management | +| `fullscreen.ts` | Full-screen mode utilities | +| `exportRenderer.tsx` | Renders sessions for export | +| `staticRender.tsx` | Static React rendering (headless) | + +### Config & Meta +| File | Purpose | +|------|---------| +| `claudeCodeHints.ts` | Context-sensitive hints for users | +| `claudeDesktop.ts` | Claude Desktop app integration | +| `exampleCommands.ts` | Example slash commands for help | +| `releaseNotes.ts` | Release notes loading and display | +| `version.ts` (referenced) | Version string management | +| `undercover.ts` | ANT-only: undercover/internal mode | +| `bundledMode.ts` | Detects bundled (desktop) mode | + +### Diagnostics & Debugging +| File | Purpose | +|------|---------| +| `debug.ts` | `logForDebugging()`, `logAntError()` | +| `debugFilter.ts` | Debug output filtering | +| `diagLogs.ts` | `logForDiagnosticsNoPII()` — diagnostic logs without PII | +| `log.ts` | `logError()` — structured error logging | +| `errorLogSink.ts` | Error log collection | +| `warningHandler.ts` | Warning deduplication and display | +| `doctorDiagnostic.ts` | Diagnostic check implementations for `/doctor` | +| `doctorContextWarnings.ts` | Context-specific doctor warnings | +| `diagLogs.ts` | Non-PII diagnostic logging | + +### IDE Integration +| File | Purpose | +|------|---------| +| `ide.ts` | VS Code / JetBrains IDE integration | +| `idePathConversion.ts` | Converts paths for IDE display | +| `jetbrains.ts` | JetBrains-specific integration | +| `terminalPanel.ts` | IDE terminal panel integration | + +### Miscellaneous +| File | Purpose | +|------|---------| +| `activityManager.ts` | Tracks user activity | +| `argom entSubstitution.ts` | Substitutes arguments in templates | +| `autoModeDenials.ts` | Tracks auto-mode permission denials | +| `autoRunIssue.tsx` | Auto-run issue detection | +| `binaryCheck.ts` | Detects binary files | +| `browser.ts` | Opens URLs in browser | +| `bufferedWriter.ts` | Buffered async file writer | +| `classifierApprovals.ts` | Classifier-based approval management | +| `classifierApprovalsHook.ts` | React hook for classifier approvals | +| `codeIndexing.ts` | Code indexing for search | +| `commandLifecycle.ts` | Command lifecycle event management | +| `completionCache.ts` | Caches shell completions | +| `conversationRecovery.ts` | Recovers from corrupted conversations | +| `effor.ts` | Effort level management | +| `editor.ts` | External editor integration | +| `earlyInput.ts` | Captures input before UI is ready | +| `embeddedTools.ts` | Embedded tool management | +| `extraUsage.ts` | Extra usage tracking | +| `fastMode.ts` | Fast mode (sonnet default) management | +| `idleTimeout.ts` | Idle session timeout management | +| `immediateCommand.ts` | Immediate command execution (no spinner) | +| `keyboardShortcuts.ts` | Keyboard shortcut definitions | +| `mailbox.ts` | Inter-process mailbox messaging | +| `modifiers.ts` | Keyboard modifier key detection | +| `objectGroupBy.ts` | Object.groupBy polyfill | +| `pasteStore.ts` | Paste content storage | +| `planModeV2.ts` | Plan mode state management | +| `plans.ts` | Plan data management | +| `preflightChecks.tsx` | Pre-launch health checks | +| `privacyLevel.ts` | `getEssentialTrafficOnlyReason()` — privacy mode | +| `promptCategory.ts` | Categorizes prompts for analytics | +| `promptEditor.ts` | Multi-line prompt editing | +| `queueProcessor.ts` | Generic queue processing | +| `renderOptions.ts` | `getBaseRenderOptions()` for Ink | +| `screenshotClipboard.ts` | Screenshot clipboard utilities | +| `shellConfig.ts` | Shell configuration management | +| `sleep.ts` | `sleep(ms: number): Promise` | +| `timeouts.ts` | Timeout utilities | +| `tmuxSocket.ts` | Tmux socket communication | +| `toolErrors.ts` | Tool error message formatting | +| `toolPool.ts` | Tool instance pooling | +| `toolResultStorage.ts` | Storage for tool call results | +| `toolSchemaCache.ts` | Caches compiled tool schemas | +| `toolSearch.ts` | Fuzzy search over available tools | +| `transcriptSearch.ts` | Full-text search within session transcripts | +| `user.ts` | User info utilities | +| `userPromptKeywords.ts` | Extracts keywords from user prompts | +| `which.ts` / `findExecutable.ts` | Finds executables in PATH | +| `windowsPaths.ts` | Windows/POSIX path conversion | +| `withResolvers.ts` | `Promise.withResolvers` polyfill | +| `ansiToPng.ts` / `ansiToSvg.ts` | ANSI terminal output to image | +| `asciicast.ts` | asciinema recording format | +| `appleTerminalBackup.ts` / `iTermBackup.ts` | Terminal state backup | +| `attribution.ts` | Message attribution tracking | +| `ink.ts` (in utils) | Ink-related utility helpers | + +--- + +## 32. `interactiveHelpers.tsx` + +High-level interactive session initialization utilities. Runs in the main thread before the REPL starts. + +**Exports:** +```typescript +export function completeOnboarding(): void +// Sets hasCompletedOnboarding = true in global config + +export function showDialog( + root: Root, + renderer: (done: (result: T) => void) => React.ReactNode +): Promise +// Renders a dialog via Ink and waits for completion + +export async function exitWithError( + root: Root, + message: string, + beforeExit?: () => Promise +): Promise +// Renders error message through Ink then exits (avoids console.error being swallowed) + +export async function exitWithMessage( + root: Root, + message: string, + options?: { color?: TextProps['color']; exitCode?: number; beforeExit?: () => Promise } +): Promise +``` + +**Key Imports/Dependencies:** +- `bootstrap/state` — global state management +- `services/analytics/growthbook` — feature flags +- `state/AppState` — application state +- `utils/claudemd` — CLAUDE.md reading +- `utils/config` — global/project config +- `utils/settings/settings` — user settings +- `services/mcpServerApproval` — MCP approval flow +- `keybindings/KeybindingProviderSetup` — keybinding initialization + +--- + +## 33. `ink.ts` (Top-Level Module) + +The public API for the Ink terminal rendering framework used throughout Claude Code. + +**Purpose:** Wraps the internal `ink/root.ts` with a `ThemeProvider` so every render is automatically themed. + +**Key Exports:** +```typescript +// Rendering +export async function render(node: ReactNode, options?: NodeJS.WriteStream | RenderOptions): Promise +export async function createRoot(options?: RenderOptions): Promise + +// Re-exports from ink/root +export type { RenderOptions, Instance, Root } + +// Theme system +export { color } from './components/design-system/color.js' +export type { Props as BoxProps } +export { default as Box } // ThemedBox +export type { Props as TextProps } +export { default as Text } // ThemedText +export { ThemeProvider, usePreviewTheme, useTheme, useThemeSetting } + +// Core ink components (re-exported from ink/) +export { Ansi } +export { default as BaseBox } +export { default as Button } +export { default as Link } +export { default as Newline } +export { NoSelect } +export { RawAnsi } +export { default as Spacer } +// ... more components +``` + +**Key Detail:** The `render()` and `createRoot()` wrappers apply `withTheme()` which wraps the node in `ThemeProvider`. This means every call site automatically has access to themed colors, eliminating the need for each component to mount its own `ThemeProvider`. + +--- + +## 34. `utils/shell/` — Shell Provider Abstraction + +The `shell/` subdirectory provides a shell-type-agnostic provider interface so BashTool and PowerShellTool can share execution infrastructure. + +### `shell/shellProvider.ts` +Defines the `ShellProvider` interface: + +```typescript +export const SHELL_TYPES = ['bash', 'powershell'] as const +export type ShellType = (typeof SHELL_TYPES)[number] +export const DEFAULT_HOOK_SHELL: ShellType = 'bash' + +export type ShellProvider = { + type: ShellType + shellPath: string + detached: boolean + buildExecCommand(command, opts): Promise<{ commandString, cwdFilePath }> + getSpawnArgs(commandString): string[] + getEnvironmentOverrides(command): Promise> +} +``` + +### `shell/bashProvider.ts` +Factory: `createBashShellProvider(shellPath, options?): Promise` + +Key behaviors: +- **Snapshot sourcing**: Lazy-initializes a shell snapshot and sources it on every command for consistent environment. Falls back to login-shell (`-l` flag) when snapshot is missing. +- **extglob disabling**: Injects `shopt -u extglob` / `setopt NO_EXTENDED_GLOB` for security against malicious filename glob expansion after snapshot load. +- **eval wrapping**: Commands are `eval`-wrapped so aliases sourced from snapshot expand correctly. +- **CWD tracking**: Appends `pwd -P >| ` so `getCwd()` stays synchronized. +- **Pipe rearrangement**: When a command contains `|` and needs stdin redirect, calls `rearrangePipeCommand()` to apply redirect to first pipe segment only. +- **Tmux isolation**: Lazily initializes Claude's isolated tmux socket when `hasTmuxToolBeenUsed()` or the command contains `tmux`. +- **Windows compatibility**: Rewrites `2>nul` to `/dev/null` and converts paths via `windowsPathToPosixPath`. +- **`CLAUDE_CODE_SHELL_PREFIX`**: Wraps the assembled command string in a custom prefix for shell environment wrappers. + +### `shell/powershellProvider.ts` +Factory: `createPowerShellProvider(shellPath): ShellProvider` + +Key behaviors: +- Encodes PS commands as Base64 UTF-16LE (`-EncodedCommand`) to survive shell-quoting layers. +- Non-detached (PowerShell requires synchronous spawning on Windows). +- CWD tracked to a temp file using PS `Set-Content`. +- Uses `$LASTEXITCODE` / `$?` for exit code capture (handles PS 5.1 quirks). + +### `shell/outputLimits.ts` +```typescript +export const BASH_MAX_OUTPUT_UPPER_LIMIT = 150_000 +export const BASH_MAX_OUTPUT_DEFAULT = 30_000 +export function getMaxOutputLength(): number +// Reads BASH_MAX_OUTPUT_LENGTH env var; validated and clamped to [1, 150_000] +``` + +### `shell/shellToolUtils.ts` +```typescript +export const SHELL_TOOL_NAMES: string[] // [BASH_TOOL_NAME, POWERSHELL_TOOL_NAME] +export function isPowerShellToolEnabled(): boolean +// Windows-only gate: ant users default ON (opt-out via env=0), +// external users default OFF (opt-in via env=1) +``` + +### `shell/readOnlyCommandValidation.ts` +Exports comprehensive allow-lists for read-only command validation: +- `GIT_READ_ONLY_COMMANDS` — All safe git subcommands with permitted flags and callbacks +- `GH_READ_ONLY_COMMANDS` — Ant-only GitHub CLI read commands +- `EXTERNAL_READONLY_COMMANDS` — Cross-shell safe commands (ls, cat, grep, etc.) +- `containsVulnerableUncPath(cmd)` — Detects Windows UNC paths that could leak credentials +- `FlagArgType` — `'none' | 'number' | 'string' | 'char' | '{}' | 'EOF'` +- `ExternalCommandConfig` — `{ safeFlags, additionalCommandIsDangerousCallback?, respectsDoubleDash? }` + +### `shell/specPrefix.ts` +Fig-spec-driven command prefix extraction: +```typescript +export const DEPTH_RULES: Record +// Overrides: rg→2, gcloud→4, kubectl→3, docker→3, aws→4, etc. + +export async function buildPrefix( + command: string, + args: string[], + spec: CommandSpec | null +): Promise +// e.g. "git -C /repo status --short" → "git status" +``` + +### `shell/prefix.ts` +LLM-based (Haiku) command prefix extraction factory: +```typescript +export type CommandPrefixResult = { commandPrefix: string | null } +export type PrefixExtractorConfig = { + toolName: string + policySpec: string + eventName: string + querySource: QuerySource + preCheck?: (command: string) => CommandPrefixResult | null +} + +export function createCommandPrefixExtractor(config: PrefixExtractorConfig) +// Returns memoized (LRU 200) async function. Calls Haiku with policySpec. +// Rejects: command_injection_detected, bare shell names, non-prefix responses. + +export function createSubcommandPrefixExtractor(getPrefix, splitCommand) +// Extracts prefix for each subcommand in a compound command (e.g. a && b) +``` + +### `shell/powershellDetection.ts` +Detects available PowerShell executables (`pwsh` vs `powershell.exe`) on Windows. + +--- + +## 35. `utils/ultraplan/` — Ultraplan Remote Session Polling + +The `ultraplan/` subdirectory supports the `/ultraplan` command, which launches a plan-mode CCR session and waits for the user to approve a plan in the browser. + +### `ultraplan/ccrSession.ts` + +Types: +```typescript +export type PollFailReason = + | 'terminated' | 'timeout_pending' | 'timeout_no_plan' + | 'extract_marker_missing' | 'network_or_unknown' | 'stopped' + +export class UltraplanPollError extends Error { + reason: PollFailReason + rejectCount: number +} + +export const ULTRAPLAN_TELEPORT_SENTINEL = '__ULTRAPLAN_TELEPORT_LOCAL__' +// Browser embeds this in the rejection feedback when user clicks "teleport back to terminal" + +export type ScanResult = + | { kind: 'approved'; plan: string } + | { kind: 'teleport'; plan: string } // user wants local execution + | { kind: 'rejected'; id: string } + | { kind: 'pending' } + | { kind: 'terminated'; subtype: string } + | { kind: 'unchanged' } + +export type UltraplanPhase = 'running' | 'needs_input' | 'plan_ready' +``` + +`ExitPlanModeScanner` — pure stateful classifier over the CCR event stream: +```typescript +export class ExitPlanModeScanner { + get rejectCount(): number + get hasPendingPlan(): boolean // ExitPlanMode emitted, no tool_result yet + everSeenPending: boolean + ingest(newEvents: SDKMessage[]): ScanResult + // Tracks ExitPlanMode tool_use IDs and their corresponding tool_results + // Precedence: approved > terminated > rejected > pending > unchanged +} +``` + +`pollForApprovedExitPlanMode` — main polling loop: +```typescript +export type PollResult = { + plan: string + rejectCount: number + executionTarget: 'local' | 'remote' +} + +export async function pollForApprovedExitPlanMode( + sessionId: string, + timeoutMs: number, + onPhaseChange?: (phase: UltraplanPhase) => void, + shouldStop?: () => boolean +): Promise +``` + +Polls `pollRemoteSessionEvents()` every 3 seconds. `MAX_CONSECUTIVE_FAILURES = 5` before giving up on transient network errors. Plan text is extracted from `tool_result.content` via: +- `"## Approved Plan:\n"` or `"## Approved Plan (edited by user):\n"` markers (remote execution) +- `ULTRAPLAN_TELEPORT_SENTINEL + "\n"` marker (local execution) + +### `ultraplan/keyword.ts` + +Detects `ultraplan` / `ultrareview` keyword triggers in user input, with smart exclusion: + +```typescript +export function findUltraplanTriggerPositions(text: string): TriggerPosition[] +export function findUltrareviewTriggerPositions(text: string): TriggerPosition[] +export function hasUltraplanKeyword(text: string): boolean +export function hasUltrareviewKeyword(text: string): boolean +export function replaceUltraplanKeyword(text: string): string +// Replaces first triggerable "ultraplan" with "plan" for the forwarded CCR prompt +``` + +Does NOT trigger when the keyword is inside paired delimiters (backticks, quotes, brackets, braces), in a path/identifier context, followed by `?`, or in a slash command. Apostrophe-aware: `"let's ultraplan"` still triggers. diff --git a/spec/11_special_systems.md b/spec/11_special_systems.md new file mode 100644 index 0000000..f9d92bb --- /dev/null +++ b/spec/11_special_systems.md @@ -0,0 +1,1767 @@ +# Claude Code — Special Systems: Buddy, Memory, Keybindings, Skills, Voice, Plugins & More + +--- + +## Table of Contents + +1. [Buddy (Companion/Tamagotchi) System](#1-buddy-companiontagotchi-system) +2. [Memory Directory (memdir) System](#2-memory-directory-memdir-system) +3. [Keybindings System](#3-keybindings-system) +4. [Skills System](#4-skills-system) +5. [Voice System](#5-voice-system) +6. [Plugins System](#6-plugins-system) +7. [Output Styles](#7-output-styles) +8. [Hooks Schema](#8-hooks-schema) +9. [Native-TypeScript Ports (native-ts/)](#9-native-typescript-ports-native-ts) +10. [MoreRight Hook (moreright/)](#10-moreright-hook-moreright) +11. [Migrations](#11-migrations) +12. [Core Type Definitions (types/)](#12-core-type-definitions-types) +13. [Remote Session System (remote/)](#13-remote-session-system-remote) + +--- + +## 1. Buddy (Companion/Tamagotchi) System + +### 1.1 System Overview + +The Buddy system is a virtual companion ("tamagotchi") that appears as an ASCII art sprite beside the user's prompt input. Each user receives a deterministic companion whose appearance (species, eyes, hat, rarity, shiny status, stats) is derived from their user ID via a seeded PRNG — not stored. The companion's "soul" (name and personality) is generated by the AI and persisted in config. The feature is gated behind a `BUDDY` feature flag and was planned to launch April 1–7, 2026 as a "teaser window." + +**Source directory:** `src/buddy/` + +**Files:** +- `buddy/types.ts` — Type definitions, species/eyes/hats/stats constants, rarity weights +- `buddy/companion.ts` — PRNG, rolling, and companion reconstruction logic +- `buddy/sprites.ts` — ASCII art sprite frames for all 18 species +- `buddy/prompt.ts` — Companion introduction text injection into system prompt +- `buddy/useBuddyNotification.tsx` — React hook for startup teaser notification +- `buddy/CompanionSprite.tsx` — React component rendering the animated sprite + speech bubble + +--- + +### 1.2 Architecture + +The system splits companion data into two parts: + +- **Bones** (`CompanionBones`): Deterministic visual/stat data derived from `hash(userId + SALT)`. Never stored; always recomputed on read. This prevents users from editing their config to claim a legendary rarity. +- **Soul** (`CompanionSoul`): AI-generated name and personality, persisted to `config.companion` as `StoredCompanion`. +- **Full Companion** (`Companion = CompanionBones & CompanionSoul & { hatchedAt: number }`): assembled at read time by `getCompanion()`. + +The rolling process uses a single pass through a seeded PRNG (`mulberry32`) to deterministically select: +1. Rarity tier (weighted random) +2. Species +3. Eye style +4. Hat (none if common) +5. Shiny flag (1% probability) +6. Stats (one peak stat, one dump stat, rest scattered; floor scales with rarity) +7. `inspirationSeed` (passed to AI for soul generation) + +--- + +### 1.3 Species Encoding (Canary Bypass) + +**Critical detail:** Species name strings are encoded as `String.fromCharCode(...)` literals to avoid triggering a build-time string scan that checks for model codenames (`excluded-strings.txt`). One species name collides with an internal model codename canary. All 18 species use this encoding uniformly. + +```typescript +// In buddy/types.ts: +const c = String.fromCharCode +export const duck = c(0x64,0x75,0x63,0x6b) as 'duck' +// ... all 18 species encoded this way +``` + +--- + +### 1.4 PRNG: Mulberry32 + +**Algorithm:** +```typescript +function mulberry32(seed: number): () => number { + let a = seed >>> 0 + return function () { + a |= 0 + a = (a + 0x6d2b79f5) | 0 + let t = Math.imul(a ^ (a >>> 15), 1 | a) + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} +``` + +- Tiny 32-bit PRNG with good statistical distribution +- Seeded from `hashString(userId + SALT)` where `SALT = 'friend-2026-401'` +- `hashString` uses Bun's native hash when available, falls back to FNV-1a (32-bit) + +**Caching:** The `roll()` function memoizes the last result keyed on `userId + SALT`, since it is called from three hot paths: the 500ms sprite tick, per-keystroke PromptInput rendering, and the per-turn observer. + +--- + +### 1.5 Data Structures + +#### `Rarity` +```typescript +export const RARITIES = ['common', 'uncommon', 'rare', 'epic', 'legendary'] as const +export type Rarity = (typeof RARITIES)[number] +``` + +#### `RARITY_WEIGHTS` +```typescript +export const RARITY_WEIGHTS = { + common: 60, // 60% + uncommon: 25, // 25% + rare: 10, // 10% + epic: 4, // 4% + legendary: 1, // 1% +} as const +``` + +#### `RARITY_FLOOR` (stat minimums per rarity) +```typescript +const RARITY_FLOOR: Record = { + common: 5, + uncommon: 15, + rare: 25, + epic: 35, + legendary: 50, +} +``` + +#### `RARITY_STARS` (UI display) +```typescript +export const RARITY_STARS = { + common: '★', + uncommon: '★★', + rare: '★★★', + epic: '★★★★', + legendary: '★★★★★', +} +``` + +#### `RARITY_COLORS` (maps to Theme keys) +```typescript +export const RARITY_COLORS = { + common: 'inactive', + uncommon: 'success', + rare: 'permission', + epic: 'autoAccept', + legendary: 'warning', +} +``` + +#### `Species` (18 total) +```typescript +export const SPECIES = [ + duck, goose, blob, cat, dragon, octopus, owl, penguin, + turtle, snail, ghost, axolotl, capybara, cactus, robot, + rabbit, mushroom, chonk, +] as const +export type Species = (typeof SPECIES)[number] +``` + +#### `Eye` (6 styles) +```typescript +export const EYES = ['·', '✦', '×', '◉', '@', '°'] as const +``` + +#### `Hat` (8 types) +```typescript +export const HATS = [ + 'none', 'crown', 'tophat', 'propeller', + 'halo', 'wizard', 'beanie', 'tinyduck', +] as const +``` + +#### `StatName` (5 stats) +```typescript +export const STAT_NAMES = ['DEBUGGING', 'PATIENCE', 'CHAOS', 'WISDOM', 'SNARK'] as const +``` + +#### `CompanionBones` (deterministic, never stored) +```typescript +export type CompanionBones = { + rarity: Rarity + species: Species + eye: Eye + hat: Hat + shiny: boolean + stats: Record // 1–100 per stat +} +``` + +#### `CompanionSoul` (AI-generated, stored) +```typescript +export type CompanionSoul = { + name: string + personality: string +} +``` + +#### `StoredCompanion` (what persists in config) +```typescript +export type StoredCompanion = CompanionSoul & { hatchedAt: number } +``` + +#### `Companion` (runtime assembled) +```typescript +export type Companion = CompanionBones & CompanionSoul & { hatchedAt: number } +``` + +#### `Roll` (output of `rollFrom`) +```typescript +export type Roll = { + bones: CompanionBones + inspirationSeed: number // Passed to AI for deterministic soul generation +} +``` + +--- + +### 1.6 Stat Rolling Algorithm + +```typescript +function rollStats(rng, rarity): Record { + const floor = RARITY_FLOOR[rarity] // 5–50 depending on rarity + const peak = pick(rng, STAT_NAMES) // one stat gets +50 bonus + let dump = pick(rng, STAT_NAMES) // different stat, penalized + while (dump === peak) dump = pick(rng, STAT_NAMES) + + for (name of STAT_NAMES) { + if (name === peak) stats[name] = min(100, floor + 50 + rng()*30) // 55–130, capped 100 + else if (name === dump) stats[name] = max(1, floor - 10 + rng()*15) // 1–20 (at common) + else stats[name] = floor + rng()*40 // spread + } +} +``` + +--- + +### 1.7 Sprite System + +Each species has **3 animation frames** of ASCII art, each 5 lines tall, 12 chars wide. The `{E}` placeholder in frame templates is substituted with the companion's `eye` character. + +**Frame structure:** +- Line 0: Hat slot (blank if no hat, or ambient detail like `~` for smoke, `*` for sparks) +- Lines 1–4: Body art + +**Hat rendering:** Hats are placed on line 0 only if it is blank. If all frames have a blank line 0, the blank line is stripped (saves terminal row when no hat). + +**Hat ASCII art (in `HAT_LINES`):** +| Hat | ASCII | +|-----|-------| +| none | (empty) | +| crown | ` \^^^/ ` | +| tophat | ` [___] ` | +| propeller | ` -+- ` | +| halo | ` ( ) ` | +| wizard | ` /^\ ` | +| beanie | ` (___) ` | +| tinyduck | ` ,> ` | + +**Idle animation sequence:** +```typescript +const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0] +// -1 = blink on frame 0 (special) +// TICK_MS = 500ms per step +``` + +**Pet animation:** After `/buddy pet`, hearts float upward for 5 ticks (~2.5 seconds): +```typescript +const PET_HEARTS = [ + ` ${H} ${H} `, + ` ${H} ${H} ${H} `, + ` ${H} ${H} ${H} `, + `${H} ${H} ${H} `, + '· · · ' +] +``` + +--- + +### 1.8 All Exports + +#### `buddy/companion.ts` +| Export | Type | Description | +|--------|------|-------------| +| `Roll` | type | `{ bones: CompanionBones; inspirationSeed: number }` | +| `roll(userId: string)` | `() => Roll` | Deterministic roll (memoized per userId+SALT) | +| `rollWithSeed(seed: string)` | `() => Roll` | Roll from arbitrary seed string | +| `companionUserId()` | `() => string` | Returns oauthAccount UUID or userID or 'anon' | +| `getCompanion()` | `() => Companion \| undefined` | Assembles companion from config + roll | + +#### `buddy/sprites.ts` +| Export | Type | Description | +|--------|------|-------------| +| `renderSprite(bones, frame?)` | `(CompanionBones, number?) => string[]` | Returns array of lines for the sprite | +| `spriteFrameCount(species)` | `(Species) => number` | Number of frames for a species (all are 3) | +| `renderFace(bones)` | `(CompanionBones) => string` | Short face string for inline display | + +#### `buddy/prompt.ts` +| Export | Type | Description | +|--------|------|-------------| +| `companionIntroText(name, species)` | `(string, string) => string` | System prompt text for companion introduction | +| `getCompanionIntroAttachment(messages?)` | `(Message[]?) => Attachment[]` | Returns intro attachment if not yet injected | + +#### `buddy/useBuddyNotification.tsx` +| Export | Type | Description | +|--------|------|-------------| +| `isBuddyTeaserWindow()` | `() => boolean` | True if local date is April 1–7, 2026 | +| `isBuddyLive()` | `() => boolean` | True if local date is April 2026 or later | +| `useBuddyNotification()` | `() => void` | React hook: shows rainbow `/buddy` startup hint | +| `findBuddyTriggerPositions(text)` | `(string) => Array<{start,end}>` | Find `/buddy` occurrences in text | + +--- + +### 1.9 Configuration + +- `config.companion` — Stores `StoredCompanion` (name, personality, hatchedAt) +- `config.companionMuted` — When true, suppresses companion intro injection +- Feature flag: `feature('BUDDY')` — gates all buddy functionality +- SALT constant: `'friend-2026-401'` — mixed into hash to prevent replay attacks + +--- + +## 2. Memory Directory (memdir) System + +### 2.1 System Overview + +The memory system provides Claude with persistent, file-based memory across sessions. Memory is stored as markdown files with YAML frontmatter in a per-project directory. It encompasses: + +1. **Auto memory** (`~/.claude/projects//memory/`) — per-user, per-project +2. **Team memory** (`/team/`) — shared across contributors (feature-gated) +3. **Memory scanning** — reads frontmatter headers to build a manifest without loading full content +4. **Relevance selection** — uses a Sonnet model call to pick the most relevant files (up to 5) for a given query +5. **Freshness warnings** — age-based staleness caveats injected as `` tags + +**Source directory:** `src/memdir/` + +--- + +### 2.2 Memory Directory Structure + +``` +~/.claude/ + projects/ + / + memory/ + MEMORY.md -- entrypoint index (always loaded) + .md -- individual memory files with frontmatter + team/ -- team-shared memories (TEAMMEM feature) + MEMORY.md + .md + logs/ -- KAIROS mode: append-only daily logs + YYYY/ + MM/ + YYYY-MM-DD.md +``` + +--- + +### 2.3 Memory Types Taxonomy + +All memory falls into four types (stored in frontmatter `type:` field): + +| Type | Scope (combined mode) | Description | +|------|----------------------|-------------| +| `user` | Always private | User's role, goals, knowledge, preferences | +| `feedback` | Private (default) or team for project-wide conventions | How-to-approach-work guidance from corrections and confirmations | +| `project` | Strongly bias team | Ongoing work, goals, bugs, incidents not derivable from code | +| `reference` | Usually team | Pointers to external systems (Linear, Grafana, Slack) | + +**What NOT to save:** +- Code patterns, architecture, file structure (derivable by reading code) +- Git history, recent changes (use `git log`) +- Debugging solutions/fix recipes (the fix is in the code) +- Anything in CLAUDE.md +- Ephemeral task details, in-progress work + +--- + +### 2.4 Memory Frontmatter Format + +```markdown +--- +name: {{memory name}} +description: {{one-line description — used to decide relevance in future conversations, so be specific}} +type: {{user, feedback, project, reference}} +--- + +{{memory content}} +``` + +For `feedback` and `project` types, the body should include: +- Rule/fact as the lead +- `**Why:**` line with the reason +- `**How to apply:**` line with when/where it kicks in + +--- + +### 2.5 Architecture: Data Flow + +``` +User query arrives + │ + ▼ +scanMemoryFiles(memoryDir) -- reads frontmatter of all .md files + │ returns MemoryHeader[] + ▼ +filterOut(alreadySurfaced) -- skip files shown in prior turns + │ + ▼ +selectRelevantMemories(query, ...) -- sideQuery to Sonnet with manifest + │ returns up to 5 filenames + ▼ +Return RelevantMemory[] (path + mtimeMs) + │ + ▼ +Caller injects file contents -- reads full file content +Caller adds freshness warnings -- memoryFreshnessNote(mtimeMs) +``` + +--- + +### 2.6 Memory Scanning (`memoryScan.ts`) + +**`scanMemoryFiles(memoryDir, signal)`** +- `readdir(memoryDir, { recursive: true })` to find all `.md` files +- Excludes `MEMORY.md` (entrypoint — already in system prompt) +- Reads only first `FRONTMATTER_MAX_LINES = 30` lines of each file +- Parses frontmatter for `description` and `type` +- Sorts newest-first by `mtimeMs` +- Capped at `MAX_MEMORY_FILES = 200` +- Returns `MemoryHeader[]` + +**`MemoryHeader` type:** +```typescript +export type MemoryHeader = { + filename: string + filePath: string + mtimeMs: number + description: string | null + type: MemoryType | undefined +} +``` + +**`formatMemoryManifest(memories)`** +Formats headers as text manifest for the selector prompt: +``` +- [user] user_role.md (2026-01-15T12:00:00Z): user is a senior Go engineer focused on observability +- [feedback] no_mocks.md (2026-01-10T09:30:00Z): integration tests must use real database +``` + +--- + +### 2.7 Relevance Selection (`findRelevantMemories.ts`) + +**System prompt used for selection:** +``` +You are selecting memories that will be useful to Claude Code as it processes a user's query. +Return a list of filenames for the memories that will clearly be useful (up to 5). +Only include memories you are certain will be helpful. Be selective and discerning. +- If unsure, do not include it. +- If no memories are clearly useful, return an empty list. +- If recently-used tools are provided, do not select reference docs for those tools + (skip API usage docs, DO keep warnings/gotchas about active tools). +``` + +**`sideQuery` parameters:** +- Model: `getDefaultSonnetModel()` +- Max tokens: 256 +- Output format: JSON schema `{ selected_memories: string[] }` +- Query source: `'memdir_relevance'` + +**`findRelevantMemories` signature:** +```typescript +export async function findRelevantMemories( + query: string, + memoryDir: string, + signal: AbortSignal, + recentTools: readonly string[] = [], + alreadySurfaced: ReadonlySet = new Set(), +): Promise + +export type RelevantMemory = { + path: string + mtimeMs: number +} +``` + +--- + +### 2.8 Memory Freshness System (`memoryAge.ts`) + +All functions are pure, accepting `mtimeMs` timestamps: + +| Export | Signature | Description | +|--------|-----------|-------------| +| `memoryAgeDays(mtimeMs)` | `(number) => number` | Floor-rounded days since mtime, clamped to ≥0 | +| `memoryAge(mtimeMs)` | `(number) => string` | Human-readable: 'today', 'yesterday', 'N days ago' | +| `memoryFreshnessText(mtimeMs)` | `(number) => string` | Staleness caveat text ('' for ≤1 day, warns for older) | +| `memoryFreshnessNote(mtimeMs)` | `(number) => string` | `` wrapped freshness text | + +**Staleness warning text (for memories >1 day old):** +``` +This memory is N days old. Memories are point-in-time observations, not live state — +claims about code behavior or file:line citations may be outdated. +Verify against current code before asserting as fact. +``` + +--- + +### 2.9 Memory Path Resolution (`paths.ts`) + +**`isAutoMemoryEnabled()`** — Priority chain: +1. `CLAUDE_CODE_DISABLE_AUTO_MEMORY` env var (truthy → OFF, falsy-defined → ON) +2. `CLAUDE_CODE_SIMPLE` (--bare mode) → OFF +3. `CLAUDE_CODE_REMOTE` without `CLAUDE_CODE_REMOTE_MEMORY_DIR` → OFF +4. `settings.autoMemoryEnabled` (project-level opt-out) +5. Default: **enabled** + +**`getAutoMemPath()`** — Resolution order (memoized by `getProjectRoot()`): +1. `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE` env var (Cowork spaces) +2. `autoMemoryDirectory` in settings (policy > flag > local > user sources; supports `~/` expansion) +3. `/projects//memory/` + - `memoryBase` = `CLAUDE_CODE_REMOTE_MEMORY_DIR` or `~/.claude` + +**Security validations in `validateMemoryPath()`:** +- Rejects relative paths (not `isAbsolute`) +- Rejects root/near-root (length < 3) +- Rejects Windows drive-root (`C:`) +- Rejects UNC paths (`\\server\share`, `//`) +- Rejects null bytes +- Normalizes NFC + +**`getAutoMemDailyLogPath(date?)`** — KAIROS mode daily log path: +``` +/logs/YYYY/MM/YYYY-MM-DD.md +``` + +**All path exports:** +| Export | Description | +|--------|-------------| +| `isAutoMemoryEnabled()` | Check if auto-memory is on | +| `isExtractModeActive()` | Whether background memory extraction agent will run | +| `getMemoryBaseDir()` | Base dir: env override or `~/.claude` | +| `getAutoMemPath()` | Full memory directory path (memoized) | +| `getAutoMemDailyLogPath(date?)` | Daily log path for KAIROS mode | +| `getAutoMemEntrypoint()` | `MEMORY.md` path inside auto-mem dir | +| `isAutoMemPath(absolutePath)` | Check if path is inside auto-memory dir | +| `hasAutoMemPathOverride()` | Check if Cowork env override is active | + +--- + +### 2.10 Team Memory Paths (`teamMemPaths.ts`) + +Team memory lives at `/team/`. Heavy path traversal protection: + +**`PathTraversalError`** — Custom error class thrown on injection attempts. + +**`sanitizePathKey(key)`** — Rejects: +- Null bytes +- URL-encoded traversals (e.g. `%2e%2e%2f`) +- Unicode normalization attacks (fullwidth `../` → `../`) +- Backslashes +- Absolute paths + +**`validateTeamMemWritePath(filePath)`** — Two-pass validation: +1. `path.resolve()` to eliminate `..` segments, check string containment +2. `realpathDeepestExisting()` to follow symlinks and verify real containment + +**`realpathDeepestExisting(absolutePath)`** — Walks up directory tree until `realpath()` succeeds, handles dangling symlinks (detects via `lstat`) and symlink loops (ELOOP). + +**All exports:** +| Export | Description | +|--------|-------------| +| `PathTraversalError` | Error class for traversal attempts | +| `isTeamMemoryEnabled()` | GrowthBook gate `tengu_herring_clock` + auto-memory enabled | +| `getTeamMemPath()` | `/team/` | +| `getTeamMemEntrypoint()` | `/team/MEMORY.md` | +| `isTeamMemPath(filePath)` | String-level containment check | +| `isTeamMemFile(filePath)` | Team enabled + path contained | +| `validateTeamMemWritePath(filePath)` | Full symlink-safe write validation | +| `validateTeamMemKey(relativeKey)` | Sanitize relative key + validate write path | + +--- + +### 2.11 Memory Prompt Building (`memdir.ts`) + +**Constants:** +```typescript +export const ENTRYPOINT_NAME = 'MEMORY.md' +export const MAX_ENTRYPOINT_LINES = 200 +export const MAX_ENTRYPOINT_BYTES = 25_000 +``` + +**`truncateEntrypointContent(raw)`** — Line-truncates first, then byte-truncates at last newline before cap. Appends warning message naming which cap(s) fired. + +**`buildMemoryLines(displayName, memoryDir, extraGuidelines?, skipIndex?)`** — Builds behavioral instructions without MEMORY.md content. Used in system prompt. + +**`buildMemoryPrompt({ displayName, memoryDir, extraGuidelines? })`** — Same as `buildMemoryLines` but includes MEMORY.md content. Used by agent memory. + +**`buildSearchingPastContextSection(autoMemDir)`** — Conditionally adds a "Searching past context" section with grep commands (gated on `tengu_coral_fern` GrowthBook feature). + +**`ensureMemoryDirExists(memoryDir)`** — Idempotent mkdir (recursive); logs non-EEXIST errors without throwing. + +**`loadMemoryPrompt()`** — Top-level dispatcher: +- KAIROS + kairosActive → `buildAssistantDailyLogPrompt()` +- TEAMMEM + team enabled → `buildCombinedMemoryPrompt()` +- Auto enabled → `buildMemoryLines()` joined +- Otherwise → `null` + +--- + +### 2.12 Memory Type Constants (`memoryTypes.ts`) + +All exports used in system prompt construction: + +| Export | Type | Description | +|--------|------|-------------| +| `MEMORY_TYPES` | `readonly string[]` | `['user', 'feedback', 'project', 'reference']` | +| `MemoryType` | type | Union of the four type strings | +| `parseMemoryType(raw)` | `(unknown) => MemoryType \| undefined` | Parses frontmatter value | +| `TYPES_SECTION_COMBINED` | `readonly string[]` | Prompt section for dual-directory mode | +| `TYPES_SECTION_INDIVIDUAL` | `readonly string[]` | Prompt section for single-directory mode | +| `WHAT_NOT_TO_SAVE_SECTION` | `readonly string[]` | Exclusion list for memory content | +| `MEMORY_DRIFT_CAVEAT` | `string` | Single bullet warning about memory staleness | +| `WHEN_TO_ACCESS_SECTION` | `readonly string[]` | When to read memories | +| `TRUSTING_RECALL_SECTION` | `readonly string[]` | Verification guidance before recommending from memory | +| `MEMORY_FRONTMATTER_EXAMPLE` | `readonly string[]` | Example frontmatter block for the prompt | + +--- + +### 2.13 Team Memory Prompts (`teamMemPrompts.ts`) + +**`buildCombinedMemoryPrompt(extraGuidelines?, skipIndex?)`** — Builds the combined prompt when both auto and team memory are enabled. Includes: +- Both directory paths +- Two-scope explanation (private vs team) +- Combined `TYPES_SECTION_COMBINED` with `` tags +- Dual MEMORY.md index instructions + +--- + +## 3. Keybindings System + +### 3.1 System Overview + +The keybindings system provides a fully configurable keyboard shortcut layer for Claude Code. Users can customize bindings via `~/.claude/keybindings.json` (gated on `tengu_keybinding_customization_release` GrowthBook feature). The system supports: +- Single-keystroke bindings +- Multi-keystroke chords (e.g., `ctrl+x ctrl+k`) +- Context-scoped bindings (Chat, Global, Confirmation, etc.) +- Null-unbinding (set action to `null` to disable a default) +- Command bindings (`command:help` executes slash commands) +- Hot-reload via `chokidar` file watcher + +**Source directory:** `src/keybindings/` + +--- + +### 3.2 Architecture + +``` +DEFAULT_BINDINGS (KeybindingBlock[]) + │ + ▼ parseBindings() +ParsedBinding[] <── default bindings parsed at startup + │ + ▼ merge with user bindings (user appended after, so user wins) +ParsedBinding[] <── merged bindings (last-wins for same key+context) + │ + ▼ +KeybindingProvider (React context) + │ + ├── resolveKeyWithChordState() ◄── called on every keypress + │ ├── chord_started → pending state + │ ├── chord_cancelled → reset + │ ├── match → invoke action + │ ├── unbound → swallow event + │ └── none → pass through + │ + └── useKeybinding(action, context, handler) ◄── component hook +``` + +--- + +### 3.3 Keybinding Contexts + +18 contexts defined in `KEYBINDING_CONTEXTS`: + +| Context | Activation | +|---------|------------| +| `Global` | Active everywhere, regardless of focus | +| `Chat` | When the chat input is focused | +| `Autocomplete` | When autocomplete menu is visible | +| `Confirmation` | When a confirmation/permission dialog is shown | +| `Help` | When the help overlay is open | +| `Transcript` | When viewing the transcript | +| `HistorySearch` | When searching command history (ctrl+r) | +| `Task` | When a task/agent is running in the foreground | +| `ThemePicker` | When the theme picker is open | +| `Settings` | When the settings menu is open | +| `Tabs` | When tab navigation is active | +| `Attachments` | When navigating image attachments in a select dialog | +| `Footer` | When footer indicators are focused | +| `MessageSelector` | When the message selector (rewind) is open | +| `DiffDialog` | When the diff dialog is open | +| `ModelPicker` | When the model picker is open | +| `Select` | When a select/list component is focused | +| `Plugin` | When the plugin dialog is open | + +--- + +### 3.4 Default Bindings (`defaultBindings.ts`) + +**Platform-specific keys:** +- `IMAGE_PASTE_KEY`: `alt+v` (Windows), `ctrl+v` (others) +- `MODE_CYCLE_KEY`: `meta+m` (Windows without VT mode), `shift+tab` (others) +- VT mode support check: Node ≥22.17.0 or ≥24.2.0, Bun ≥1.2.23 + +**Complete default bindings by context:** + +**Global context:** +| Key | Action | +|-----|--------| +| `ctrl+c` | `app:interrupt` (non-rebindable) | +| `ctrl+d` | `app:exit` (non-rebindable) | +| `ctrl+l` | `app:redraw` | +| `ctrl+t` | `app:toggleTodos` | +| `ctrl+o` | `app:toggleTranscript` | +| `ctrl+shift+b` | `app:toggleBrief` (KAIROS/KAIROS_BRIEF only) | +| `ctrl+shift+o` | `app:toggleTeammatePreview` | +| `ctrl+r` | `history:search` | +| `ctrl+shift+f` / `cmd+shift+f` | `app:globalSearch` (QUICK_SEARCH feature) | +| `ctrl+shift+p` / `cmd+shift+p` | `app:quickOpen` (QUICK_SEARCH feature) | +| `meta+j` | `app:toggleTerminal` (TERMINAL_PANEL feature) | + +**Chat context:** +| Key | Action | +|-----|--------| +| `escape` | `chat:cancel` | +| `ctrl+x ctrl+k` | `chat:killAgents` (chord) | +| `shift+tab` / `meta+m` | `chat:cycleMode` | +| `meta+p` | `chat:modelPicker` | +| `meta+o` | `chat:fastMode` | +| `meta+t` | `chat:thinkingToggle` | +| `enter` | `chat:submit` | +| `up` | `history:previous` | +| `down` | `history:next` | +| `ctrl+_` / `ctrl+shift+-` | `chat:undo` | +| `ctrl+x ctrl+e` / `ctrl+g` | `chat:externalEditor` | +| `ctrl+s` | `chat:stash` | +| `ctrl+v` / `alt+v` | `chat:imagePaste` | +| `shift+up` | `chat:messageActions` (MESSAGE_ACTIONS feature) | +| `space` | `voice:pushToTalk` (VOICE_MODE feature) | + +**Autocomplete:** +| Key | Action | +|-----|--------| +| `tab` | `autocomplete:accept` | +| `escape` | `autocomplete:dismiss` | +| `up` / `down` | `autocomplete:previous` / `autocomplete:next` | + +**Confirmation:** +| Key | Action | +|-----|--------| +| `y` / `enter` | `confirm:yes` | +| `n` / `escape` | `confirm:no` | +| `up` / `down` | `confirm:previous` / `confirm:next` | +| `tab` | `confirm:nextField` | +| `space` | `confirm:toggle` | +| `shift+tab` | `confirm:cycleMode` | +| `ctrl+e` | `confirm:toggleExplanation` | +| `ctrl+d` | `permission:toggleDebug` | + +**Scroll:** +| Key | Action | +|-----|--------| +| `pageup` / `pagedown` | `scroll:pageUp` / `scroll:pageDown` | +| `wheelup` / `wheeldown` | `scroll:lineUp` / `scroll:lineDown` | +| `ctrl+home` / `ctrl+end` | `scroll:top` / `scroll:bottom` | +| `ctrl+shift+c` / `cmd+c` | `selection:copy` | + +**Task:** +| Key | Action | +|-----|--------| +| `ctrl+b` | `task:background` | + +--- + +### 3.5 Parser (`parser.ts`) + +**`parseKeystroke(input: string): ParsedKeystroke`** +Splits on `+`, recognizes modifiers (case-insensitive aliases): +- `ctrl` / `control` +- `alt` / `opt` / `option` +- `shift` +- `meta` +- `cmd` / `command` / `super` / `win` +- Special keys: `esc`→`escape`, `return`→`enter`, `space`→` `, `↑↓←→` + +**`parseChord(input: string): Chord`** +Splits space-separated steps into `ParsedKeystroke[]`. Special case: lone space `" "` is the space key, not a separator. + +**`keystrokeToString(ks: ParsedKeystroke): string`** +Canonical string representation (for internal use). + +**`keystrokeToDisplayString(ks, platform?): string`** +Platform-appropriate display: `opt` on macOS, `alt` elsewhere; `cmd` on macOS, `super` elsewhere. + +**`parseBindings(blocks: KeybindingBlock[]): ParsedBinding[]`** +Converts `KeybindingBlock[]` (raw config) to flat `ParsedBinding[]`. + +--- + +### 3.6 Matching (`match.ts`) + +**`getKeyName(input: string, key: Key): string | null`** +Maps Ink's boolean key flags to string names (`key.escape` → `'escape'`, `key.upArrow` → `'up'`, single char → lowercased, etc.). + +**`matchesKeystroke(input, key, target): boolean`** +- Extracts key name and modifiers from Ink's Key object +- **Quirk:** `key.meta = true` when escape is pressed in Ink (legacy terminal behavior). Ignored for escape key matching itself. +- Alt and meta are treated as aliases in Ink (terminal limitation): `target.alt || target.meta` matched against `key.meta`. +- Super/cmd is distinct — only arrives via kitty keyboard protocol. + +**`matchesBinding(input, key, binding): boolean`** +Single-keystroke binding match only (chord length must be 1). + +--- + +### 3.7 Resolver (`resolver.ts`) + +**`resolveKey(input, key, activeContexts, bindings): ResolveResult`** +Pure function for single-keystroke resolution: +- Last matching binding wins (for user overrides) +- Returns `{ type: 'match', action }`, `{ type: 'none' }`, or `{ type: 'unbound' }` + +**`resolveKeyWithChordState(input, key, activeContexts, bindings, pending): ChordResolveResult`** +Multi-keystroke chord resolution with state: +- Cancels on escape when in a chord +- Groups chord candidates by `chordToString(chord)` key to handle null-unbinding properly (a null override on a chord prevents its prefix from entering chord-wait) +- Returns additional types: `{ type: 'chord_started', pending }`, `{ type: 'chord_cancelled' }` + +**`ChordResolveResult`:** +```typescript +type ChordResolveResult = + | { type: 'match'; action: string } + | { type: 'none' } + | { type: 'unbound' } + | { type: 'chord_started'; pending: ParsedKeystroke[] } + | { type: 'chord_cancelled' } +``` + +**`getBindingDisplayText(action, context, bindings): string | undefined`** +Searches bindings in reverse order (last wins) for the given action+context. + +**`keystrokesEqual(a, b): boolean`** +Compares with alt/meta collapsed into one logical modifier. + +--- + +### 3.8 Type Definitions (`types.ts` — inferred) + +```typescript +type KeybindingContextName = (typeof KEYBINDING_CONTEXTS)[number] + +type ParsedKeystroke = { + key: string + ctrl: boolean + alt: boolean + shift: boolean + meta: boolean + super: boolean +} + +type Chord = ParsedKeystroke[] + +type ParsedBinding = { + chord: Chord + action: string | null // null = unbound + context: KeybindingContextName +} + +type KeybindingBlock = { + context: string + bindings: Record +} +``` + +--- + +### 3.9 Schema (`schema.ts`) + +Zod v4 schemas for `keybindings.json` validation: + +**`KeybindingBlockSchema`** — Validates `{ context, bindings: { key: action | null } }`. Bindings accept: +- Known action strings from `KEYBINDING_ACTIONS` +- `command:` prefix bindings (`/^command:[a-zA-Z0-9:\-_]+$/`) +- `null` (to unbind) + +**`KeybindingsSchema`** — Wraps with optional `$schema` and `$docs` metadata: +```json +{ + "$schema": "https://www.schemastore.org/claude-code-keybindings.json", + "$docs": "https://code.claude.com/docs/en/keybindings", + "bindings": [...] +} +``` + +**Complete `KEYBINDING_ACTIONS` list (80 actions):** +Categories: `app:*`, `history:*`, `chat:*`, `autocomplete:*`, `confirm:*`, `tabs:*`, `transcript:*`, `historySearch:*`, `task:*`, `theme:*`, `help:*`, `attachments:*`, `footer:*`, `messageSelector:*`, `messageActions:*`, `diff:*`, `modelPicker:*`, `select:*`, `plugin:*`, `permission:*`, `settings:*`, `voice:*` + +--- + +### 3.10 Validation (`validate.ts`) + +**`KeybindingWarningType`:** `'parse_error' | 'duplicate' | 'reserved' | 'invalid_context' | 'invalid_action'` + +**`KeybindingWarning`:** +```typescript +type KeybindingWarning = { + type: KeybindingWarningType + severity: 'error' | 'warning' + message: string + key?: string + context?: string + action?: string + suggestion?: string +} +``` + +**Validation checks:** +1. `validateUserConfig(blocks)` — validates block structure (context string, bindings object) +2. `checkDuplicates(blocks)` — within-context duplicate keys (normalized comparison) +3. `checkReservedShortcuts(bindings)` — against `NON_REBINDABLE` and platform `TERMINAL_RESERVED`/`MACOS_RESERVED` +4. `checkDuplicateKeysInJson(jsonString)` — raw JSON string scan (JSON.parse silently drops earlier duplicates) +5. Special case: `voice:pushToTalk` with bare alphabetic key warns (prints into input during warmup) +6. `command:` bindings must be in `Chat` context + +--- + +### 3.11 Reserved Shortcuts (`reservedShortcuts.ts`) + +**`NON_REBINDABLE`** (error severity): +- `ctrl+c` — interrupt/exit (hardcoded double-press logic) +- `ctrl+d` — exit (hardcoded) +- `ctrl+m` — identical to Enter in terminals (both send CR) + +**`TERMINAL_RESERVED`:** +- `ctrl+z` — Unix SIGTSTP (warning) +- `ctrl+\` — terminal SIGQUIT (error) + +**`MACOS_RESERVED`** (macOS only, all errors): +- `cmd+c/v/x/q/w/tab/space` + +Note: `ctrl+s` (XOFF) and `ctrl+q` (XON) are intentionally NOT in reserved list — modern terminals disable flow control and Claude Code uses `ctrl+s` for stash. + +--- + +### 3.12 User Bindings Loader (`loadUserBindings.ts`) + +**File location:** `~/.claude/keybindings.json` + +**`isKeybindingCustomizationEnabled()`** — GrowthBook gate: `tengu_keybinding_customization_release`. + +**File watching:** Uses `chokidar` with: +- `stabilityThreshold: 500ms` — waits for write to stabilize +- `pollInterval: 200ms` +- `atomic: true` +- Watches parent directory, not just the file (handles file creation) + +**`loadKeybindings()` / `loadKeybindingsSyncWithWarnings()`** — Load+merge+validate. Returns `{ bindings: ParsedBinding[], warnings: KeybindingWarning[] }`. + +**All exports:** +| Export | Description | +|--------|-------------| +| `isKeybindingCustomizationEnabled()` | GrowthBook gate check | +| `getKeybindingsPath()` | `~/.claude/keybindings.json` | +| `loadKeybindings()` | Async load (used in watcher) | +| `loadKeybindingsSync()` | Sync load (React useState initializer) | +| `loadKeybindingsSyncWithWarnings()` | Sync with warnings | +| `initializeKeybindingWatcher()` | Set up chokidar watcher | +| `disposeKeybindingWatcher()` | Tear down watcher | +| `subscribeToKeybindingChanges` | Subscribe to reload events | +| `getCachedKeybindingWarnings()` | Current cached warnings | +| `resetKeybindingLoaderForTesting()` | Test reset | + +--- + +### 3.13 Template Generator (`template.ts`) + +**`generateKeybindingsTemplate()`** — Generates a well-documented template JSON file with all default bindings. Filters out `NON_REBINDABLE` shortcuts to avoid `/doctor` warnings. Includes `$schema` and `$docs` metadata. + +--- + +### 3.14 Shortcut Display (`shortcutFormat.ts`) + +**`getShortcutDisplay(action, context, fallback)`** — Non-React shortcut display helper. Logs `tengu_keybinding_fallback_used` analytics event (once per action+context) when the binding is not found. Falls back to hardcoded string during migration. + +--- + +## 4. Skills System + +### 4.1 System Overview + +Skills are Claude's slash commands that execute AI prompts or local logic. They come from multiple sources: +- **Bundled skills** — compiled into the binary, available to all users +- **Disk-based skills** — markdown files in `.claude/skills/` directories +- **Plugin skills** — provided by installed plugins +- **MCP skills** — generated from MCP server tool definitions + +**Source directory:** `src/skills/` + +--- + +### 4.2 Bundled Skills Registry (`bundledSkills.ts`) + +**`BundledSkillDefinition`** type: +```typescript +type BundledSkillDefinition = { + name: string + description: string + aliases?: string[] + whenToUse?: string + argumentHint?: string + allowedTools?: string[] + model?: string + disableModelInvocation?: boolean + userInvocable?: boolean + isEnabled?: () => boolean + hooks?: HooksSettings + context?: 'inline' | 'fork' // fork = run as sub-agent + agent?: string + files?: Record // reference files extracted to disk on first use + getPromptForCommand: (args: string, context: ToolUseContext) => Promise +} +``` + +**File extraction (`files` field):** When a skill has `files`, they are extracted to a per-process nonce directory on first invocation using `O_NOFOLLOW | O_EXCL | O_CREAT` flags (mode 0o600, directories mode 0o700). A `Base directory for this skill: ` prefix is prepended to the skill prompt so the model can read these files. + +**Registry API:** +| Export | Description | +|--------|-------------| +| `registerBundledSkill(definition)` | Register a bundled skill at startup | +| `getBundledSkills()` | Get copy of all registered bundled skills | +| `clearBundledSkills()` | Clear registry (for testing) | +| `getBundledSkillExtractDir(skillName)` | Deterministic extract directory | + +--- + +### 4.3 Bundled Skill Initialization (`bundled/index.ts`) + +**`initBundledSkills()`** registers the following skills: + +| Skill | Feature Gate | Description | +|-------|-------------|-------------| +| `updateConfig` | — | Update Claude Code configuration | +| `keybindings` | — | Manage keyboard shortcuts | +| `verify` | — | Verify work / run checks | +| `debug` | — | Debug assistance | +| `loremIpsum` | — | Lorem ipsum text generation | +| `skillify` | — | Create new skills | +| `remember` | — | Explicitly save to memory | +| `simplify` | — | Simplify code/text | +| `batch` | — | Batch operations | +| `stuck` | — | Help when stuck | +| `dream` | `KAIROS \| KAIROS_DREAM` | Nightly memory distillation | +| `hunter` | `REVIEW_ARTIFACT` | Code review | +| `loop` | `AGENT_TRIGGERS` | Recurring agent triggers | +| `scheduleRemoteAgents` | `AGENT_TRIGGERS_REMOTE` | Schedule remote agent runs | +| `claudeApi` | `BUILDING_CLAUDE_APPS` | Claude API reference skill | +| `claudeInChrome` | `shouldAutoEnableClaudeInChrome()` | Claude in Chrome integration | +| `runSkillGenerator` | `RUN_SKILL_GENERATOR` | Skill generation tool | + +--- + +### 4.4 Skills Directory Loader (`loadSkillsDir.ts`) + +**`LoadedFrom` type:** +```typescript +type LoadedFrom = + | 'commands_DEPRECATED' + | 'skills' + | 'plugin' + | 'managed' + | 'bundled' + | 'mcp' +``` + +**`getSkillsPath(source, dir)`** — Returns the config directory path for a given source and subdirectory (`skills` or `commands`). + +The loader handles: +- Frontmatter parsing (`name`, `description`, `argument-hint`, `when-to-use`, `allowed-tools`, `model`, `disableNonInteractive`, `hooks`, `context`, `agent`, `effort`, `paths`) +- Shell execution in prompts (backtick commands in frontmatter) +- Gitignore integration +- Argument substitution (`$ARGUMENTS`, named args `$ARG_NAME`) +- `isRestrictedToPluginOnly()` policy check +- Multi-level discovery: project dirs up to home + user config + +--- + +## 5. Voice System + +### 5.1 System Overview + +Voice mode enables hold-to-talk interaction with Claude Code using a push-to-talk model. Voice requires: +1. OAuth authentication (uses `voice_stream` endpoint on claude.ai) +2. GrowthBook feature flag `VOICE_MODE` +3. Kill-switch gate: `tengu_amber_quartz_disabled` (negative flag) + +**Source:** `src/voice/voiceModeEnabled.ts` + +--- + +### 5.2 API + +| Export | Description | +|--------|-------------| +| `isVoiceGrowthBookEnabled()` | `feature('VOICE_MODE') && !tengu_amber_quartz_disabled` | +| `hasVoiceAuth()` | OAuth provider + valid access token exists | +| `isVoiceModeEnabled()` | Full check: `hasVoiceAuth() && isVoiceGrowthBookEnabled()` | + +**Why OAuth required:** +- Voice uses `voice_stream` endpoint on claude.ai +- Not available with API keys, Bedrock, Vertex, or Foundry +- `isAnthropicAuthEnabled()` checks the provider, `getClaudeAIOAuthTokens()` verifies a token actually exists + +**Kill-switch design:** +- Flag default is `false` = not killed +- A missing/stale disk cache reads as "not killed" → voice works immediately on fresh installs +- Emergency kill: flip `tengu_amber_quartz_disabled` to `true` + +**Default binding:** `space` → `voice:pushToTalk` (defined in `defaultBindings.ts` under VOICE_MODE feature gate) + +**Validation warning:** Binding `voice:pushToTalk` to a bare alphabetic key (no modifiers) raises a warning because the key prints into the input during the activation warmup period. Recommended: `space` or a modifier combo like `meta+k`. + +--- + +## 6. Plugins System + +### 6.1 System Overview + +Plugins extend Claude Code with additional skills, hooks, MCP servers, LSP servers, and output styles. There are two plugin categories: +1. **Built-in plugins** — ship with the CLI, appear in `/plugin` UI, user-toggleable +2. **Marketplace plugins** — installed from GitHub repos via `/plugin install` + +**Source:** `src/plugins/` + +--- + +### 6.2 Built-in Plugin Registry (`builtinPlugins.ts`) + +Built-in plugins use the ID format `{name}@builtin`. + +**`BuiltinPluginDefinition`** type (from `types/plugin.ts`): +```typescript +type BuiltinPluginDefinition = { + name: string + description: string + version?: string + skills?: BundledSkillDefinition[] + hooks?: HooksSettings + mcpServers?: Record + isAvailable?: () => boolean + defaultEnabled?: boolean // defaults to true +} +``` + +**`LoadedPlugin`** type: +```typescript +type LoadedPlugin = { + name: string + manifest: PluginManifest + path: string + source: string + repository: string + enabled?: boolean + isBuiltin?: boolean + sha?: string + commandsPath?: string + commandsPaths?: string[] + commandsMetadata?: Record + agentsPath?: string + agentsPaths?: string[] + skillsPath?: string + skillsPaths?: string[] + outputStylesPath?: string + outputStylesPaths?: string[] + hooksConfig?: HooksSettings + mcpServers?: Record + lspServers?: Record + settings?: Record +} +``` + +**`getBuiltinPlugins()`** — Returns `{ enabled: LoadedPlugin[], disabled: LoadedPlugin[] }`. Enabled state priority: user setting > `defaultEnabled` > `true`. Plugins with `isAvailable() === false` are omitted entirely. + +**All exports:** +| Export | Description | +|--------|-------------| +| `BUILTIN_MARKETPLACE_NAME` | `'builtin'` — sentinel marketplace name | +| `registerBuiltinPlugin(definition)` | Register at startup | +| `isBuiltinPluginId(pluginId)` | Check if ends with `@builtin` | +| `getBuiltinPluginDefinition(name)` | Get definition by name | +| `getBuiltinPlugins()` | Get enabled/disabled split | +| `getBuiltinPluginSkillCommands()` | Get skills from enabled plugins as Commands | +| `clearBuiltinPlugins()` | Test reset | + +--- + +### 6.3 Built-in Plugin Initialization (`bundled/index.ts`) + +```typescript +export function initBuiltinPlugins(): void { + // No built-in plugins registered yet — scaffolding for migrating + // bundled skills that should be user-toggleable. +} +``` + +The file is scaffolding — as of the code snapshot, no built-in plugins have been registered. The infrastructure is complete for migrating bundled skills into the toggleable plugin system. + +--- + +### 6.4 Plugin Error Types (`types/plugin.ts`) + +`PluginError` is a large discriminated union with 20+ error variants: + +| Type | Key fields | +|------|-----------| +| `path-not-found` | `path`, `component` | +| `git-auth-failed` | `gitUrl`, `authType: 'ssh' \| 'https'` | +| `git-timeout` | `gitUrl`, `operation: 'clone' \| 'pull'` | +| `network-error` | `url`, `details?` | +| `manifest-parse-error` | `manifestPath`, `parseError` | +| `manifest-validation-error` | `manifestPath`, `validationErrors[]` | +| `plugin-not-found` | `pluginId`, `marketplace` | +| `marketplace-not-found` | `marketplace`, `availableMarketplaces[]` | +| `marketplace-load-failed` | `marketplace`, `reason` | +| `mcp-config-invalid` | `serverName`, `validationError` | +| `mcp-server-suppressed-duplicate` | `serverName`, `duplicateOf` | +| `lsp-config-invalid` | `serverName`, `validationError` | +| `lsp-server-start-failed` | `serverName`, `reason` | +| `lsp-server-crashed` | `exitCode`, `signal?` | +| `lsp-request-timeout` | `method`, `timeoutMs` | +| `lsp-request-failed` | `method`, `error` | +| `marketplace-blocked-by-policy` | `marketplace`, `blockedByBlocklist?`, `allowedSources[]` | +| `dependency-unsatisfied` | `dependency`, `reason: 'not-enabled' \| 'not-found'` | +| `plugin-cache-miss` | `installPath` | +| `hook-load-failed` | `hookPath`, `reason` | +| `component-load-failed` | `component`, `path`, `reason` | +| `mcpb-download-failed` | `url`, `reason` | +| `mcpb-extract-failed` | `mcpbPath`, `reason` | +| `mcpb-invalid-manifest` | `mcpbPath`, `validationError` | +| `generic-error` | `error` | + +**`getPluginErrorMessage(error)`** — Returns a human-readable string for any `PluginError` variant. + +**`PluginLoadResult`:** +```typescript +type PluginLoadResult = { + enabled: LoadedPlugin[] + disabled: LoadedPlugin[] + errors: PluginError[] +} +``` + +--- + +## 7. Output Styles + +### 7.1 System Overview + +Output styles are markdown files that define custom formatting instructions for Claude's responses. Loaded from `output-styles/` subdirectories in project `.claude/` and user `~/.claude/` directories. + +**Source:** `src/outputStyles/loadOutputStylesDir.ts` + +--- + +### 7.2 Configuration + +- **Location:** `.claude/output-styles/*.md` (project) and `~/.claude/output-styles/*.md` (user) +- **File naming:** `filename.md` → style name `filename` +- **Frontmatter fields:** + - `name`: Override the style name (defaults to filename without `.md`) + - `description`: Description shown in output style picker + - `keep-coding-instructions`: Boolean — whether to preserve coding-specific instructions (`true`/`false` or boolean) + - `force-for-plugin`: Only valid for plugin output styles; ignored (with warning) on regular styles + +--- + +### 7.3 API + +**`getOutputStyleDirStyles(cwd: string): Promise`** — Memoized async loader. Scans `output-styles` subdirectory across project hierarchy and user config. Returns `OutputStyleConfig[]`. + +**`OutputStyleConfig`** (from `constants/outputStyles.ts`): +```typescript +type OutputStyleConfig = { + name: string + description: string + prompt: string // file content (trimmed) + source: string // setting source (userSettings, localSettings, etc.) + keepCodingInstructions?: boolean +} +``` + +**`clearOutputStyleCaches()`** — Clears memoized `getOutputStyleDirStyles`, `loadMarkdownFilesForSubdir`, and plugin output style caches. + +--- + +## 8. Hooks Schema + +### 8.1 System Overview + +Hooks execute side effects at specific lifecycle events (PreToolUse, PostToolUse, PostResponse, etc.). The schema defines four hook types in a discriminated union, with conditional execution via `if` conditions. + +**Source:** `src/schemas/hooks.ts` + +--- + +### 8.2 Hook Types + +#### `BashCommandHook` (`type: 'command'`) +```typescript +{ + type: 'command' + command: string // Shell command to execute + if?: string // Permission rule syntax filter (e.g., "Bash(git *)") + shell?: 'bash' | 'powershell' + timeout?: number // Seconds + statusMessage?: string // Spinner message + once?: boolean // Run once, then remove + async?: boolean // Non-blocking background execution + asyncRewake?: boolean // Background + wake model on exit code 2 +} +``` + +#### `PromptHook` (`type: 'prompt'`) +```typescript +{ + type: 'prompt' + prompt: string // LLM prompt (use $ARGUMENTS for hook input JSON) + if?: string + timeout?: number + model?: string // Default: small fast model + statusMessage?: string + once?: boolean +} +``` + +#### `HttpHook` (`type: 'http'`) +```typescript +{ + type: 'http' + url: string // POST destination + if?: string + timeout?: number + headers?: Record // May use $VAR_NAME interpolation + allowedEnvVars?: string[] // Explicit allowlist for env var interpolation + statusMessage?: string + once?: boolean +} +``` + +#### `AgentHook` (`type: 'agent'`) +```typescript +{ + type: 'agent' + prompt: string // Verification prompt (use $ARGUMENTS for hook input JSON) + if?: string + timeout?: number // Default: 60 seconds + model?: string // Default: Haiku + statusMessage?: string + once?: boolean +} +``` + +**Note on `AgentHook`:** Must NOT use `.transform()` in the Zod schema — the schema is used in `parseSettingsFile`, and round-tripping a transformed function through `JSON.stringify` silently drops the prompt field. + +--- + +### 8.3 Hook Matcher and Settings Structure + +```typescript +type HookMatcher = { + matcher?: string // e.g., "Write", "Bash(git *)" + hooks: HookCommand[] +} + +type HooksSettings = Partial> +``` + +**`IfConditionSchema`:** Shared `if` field uses permission rule syntax (`Bash(git *)`, `Read(*.ts)`) to filter hook execution before spawning. Evaluated against `tool_name` and `tool_input`. + +--- + +### 8.4 Exports + +| Export | Description | +|--------|-------------| +| `HookCommandSchema` | Discriminated union of all 4 hook command types | +| `HookMatcherSchema` | `{ matcher?, hooks[] }` | +| `HooksSchema` | `Partial>` | +| `HookCommand` | Inferred type from schema | +| `BashCommandHook` | `Extract` | +| `PromptHook` | `Extract` | +| `AgentHook` | `Extract` | +| `HttpHook` | `Extract` | +| `HookMatcher` | Inferred from matcher schema | +| `HooksSettings` | `Partial>` | + +--- + +## 9. Native-TypeScript Ports (native-ts/) + +The `native-ts/` directory contains pure-TypeScript ports of Rust NAPI native modules. These are used when the native modules cannot be loaded (platforms without precompiled binaries, etc.). + +### 9.1 Color Diff (`native-ts/color-diff/index.ts`) + +**Purpose:** Port of `vendor/color-diff-src` — syntax-highlighted word-level diff rendering for the diff view. + +**Implementation:** Uses `highlight.js` (lazy-loaded to defer ~50MB grammar registration) and the `diff` npm package's `diffArrays`. + +**Semantic differences from native:** +- Syntax highlighting via highlight.js instead of syntect/bat +- Scope colors approximate syntect output, but plain identifiers and `=` `:` operators render in default fg (no scope in hljs grammar) +- `BAT_THEME` env is a stub (always returns default theme) +- Output structure (line numbers, markers, backgrounds, word-diff) is identical + +**Lazy loading pattern:** +```typescript +let cachedHljs: HLJSApi | null = null +function hljs(): HLJSApi { + if (cachedHljs) return cachedHljs + const mod = require('highlight.js') + // Handles both ESM default export and CJS module-is-API interop + cachedHljs = 'default' in mod && mod.default ? mod.default : mod + return cachedHljs! +} +``` + +**Key types:** +```typescript +type Hunk = { + oldStart: number + oldLines: number + newStart: number + newLines: number + lines: string[] +} + +type SyntaxTheme = { ... } +``` + +--- + +### 9.2 File Index (`native-ts/file-index/index.ts`) + +**Purpose:** Port of `vendor/file-index-src` — high-performance fuzzy file path search, replacing nucleo (Rust). + +**Algorithm:** Approximates fzf-v2/nucleo scoring with: +- Bitmap reject: paths missing any needle letter rejected in O(1) +- Greedy-earliest position scan via `indexOf` (SIMD-accelerated in JSC/V8) +- Scoring constants: + +| Constant | Value | Description | +|----------|-------|-------------| +| `SCORE_MATCH` | 16 | Base score per matched char | +| `BONUS_BOUNDARY` | 8 | Bonus for path boundary match (`/ \ - _ . space`) | +| `BONUS_CAMEL` | 6 | Bonus for camelCase boundary | +| `BONUS_CONSECUTIVE` | 4 | Bonus for consecutive matches | +| `BONUS_FIRST_CHAR` | 8 | Bonus for matching first char | +| `PENALTY_GAP_START` | 3 | Penalty for starting a gap | +| `PENALTY_GAP_EXTENSION` | 1 | Penalty per gap character | + +**Test file penalty:** Paths containing `"test"` get a 1.05× score penalty (capped at 1.0). + +**Score semantics:** Lower = better. `score = position_in_results / result_count`, so best match is 0.0. + +**Smart case:** Lowercase query → case-insensitive; any uppercase → case-sensitive. + +**`FileIndex` class:** +```typescript +class FileIndex { + loadFromFileList(fileList: string[]): void + loadFromFileListAsync(fileList: string[]): { queryable: Promise; done: Promise } + search(query: string, limit: number): SearchResult[] +} + +type SearchResult = { + path: string + score: number +} +``` + +**Async loading:** `loadFromFileListAsync` yields to event loop every `CHUNK_MS = 4ms` of sync work. Resolves `queryable` after first chunk (partial results immediately searchable). For a 270k-path list: ~5–10ms to first queryable state. + +**Top-level cache:** Stores 100 most-common top-level path segments for empty-query results. + +**`TOP_LEVEL_CACHE_LIMIT = 100`**, **`MAX_QUERY_LEN = 64`** + +--- + +### 9.3 Yoga Layout (`native-ts/yoga-layout/`) + +**Purpose:** Pure-TypeScript port of Meta's Yoga flexbox engine (`yoga-layout/load`), used by Ink's layout engine. + +**`enums.ts` — Yoga constants as `const` objects:** + +| Enum | Values | +|------|--------| +| `Align` | Auto(0), FlexStart(1), Center(2), FlexEnd(3), Stretch(4), Baseline(5), SpaceBetween(6), SpaceAround(7), SpaceEvenly(8) | +| `BoxSizing` | BorderBox(0), ContentBox(1) | +| `Dimension` | Width(0), Height(1) | +| `Direction` | Inherit(0), LTR(1), RTL(2) | +| `Display` | Flex(0), None(1), Contents(2) | +| `Edge` | Left(0), Top(1), Right(2), Bottom(3), ... | +| `Errata` | None(0), StretchFlexBasis(1), All(2147483647) | +| `ExperimentalFeature` | WebFlexBasis(0) | +| `FlexDirection` | Column(0), ColumnReverse(1), Row(2), RowReverse(3) | +| `Gutter` | Column(0), Row(1), All(2) | +| `Justify` | FlexStart(0), Center(1), FlexEnd(2), SpaceBetween(3), SpaceAround(4), SpaceEvenly(5) | +| `MeasureMode` | Undefined(0), Exactly(1), AtMost(2) | +| `NodeType` | Default(0), Text(1) | +| `Overflow` | Visible(0), Hidden(1), Scroll(2) | +| `PositionType` | Static(0), Relative(1), Absolute(2) | +| `Unit` | Undefined(0), Point(1), Percent(2), Auto(3) | +| `Wrap` | NoWrap(0), Wrap(1), WrapReverse(2) | + +**`index.ts` — Subset of Yoga implemented:** +- flex-direction (row/column + reverse) +- flex-grow / flex-shrink / flex-basis +- align-items / align-self (all except baseline in practice) +- justify-content (all 6 values) +- margin / padding / border / gap +- width / height / min / max (point, percent, auto) +- position: relative / absolute +- display: flex / none / contents +- measure functions (text nodes) +- margin: auto +- flex-wrap + align-content +- baseline alignment (spec parity, not used by Ink) + +**Not implemented:** aspect-ratio, box-sizing: content-box, RTL direction + +--- + +## 10. MoreRight Hook (moreright/) + +### 10.1 System Overview + +`useMoreRight` is a React hook that exists as a **stub for external builds**. The real implementation is internal-only (ant users). The stub returns no-op implementations of the hook interface. + +**Source:** `src/moreright/useMoreRight.tsx` + +--- + +### 10.2 Interface + +```typescript +export function useMoreRight(_args: { + enabled: boolean + setMessages: (action: M[] | ((prev: M[]) => M[])) => void + inputValue: string + setInputValue: (s: string) => void + setToolJSX: (args: M) => void +}): { + onBeforeQuery: (input: string, all: M[], n: number) => Promise + onTurnComplete: (all: M[], aborted: boolean) => Promise + render: () => null +} +``` + +**Stub behavior:** +- `onBeforeQuery` always returns `true` (allow query) +- `onTurnComplete` is a no-op +- `render` returns `null` + +The real internal implementation provides pre-query preprocessing and post-turn processing functionality whose details are redacted from the external build. + +--- + +## 11. Migrations + +Migrations run at startup to update stored config/settings to current formats. All migrations are idempotent (safe to re-run). + +### 11.1 Migration Overview + +| Migration | Source | Target | Condition | +|-----------|--------|--------|-----------| +| `migrateAutoUpdatesToSettings` | `globalConfig.autoUpdates: false` | `userSettings.env.DISABLE_AUTOUPDATER: '1'` | Only when user-preference disabled (not native protection) | +| `migrateBypassPermissionsAcceptedToSettings` | `globalConfig.bypassPermissionsModeAccepted` | `userSettings.skipDangerousModePermissionPrompt: true` | globalConfig field present | +| `migrateEnableAllProjectMcpServersToSettings` | `projectConfig.enableAllProjectMcpServers` etc. | `localSettings` | Any of the 3 fields present | +| `migrateFennecToOpus` | `userSettings.model` fennec aliases | Opus 4.6 aliases | ant users only | +| `migrateLegacyOpusToCurrent` | Explicit Opus 4.0/4.1 model strings | `'opus'` alias | 1P provider + legacy remap enabled | +| `migrateOpusToOpus1m` | `userSettings.model: 'opus'` | `'opus[1m]'` | isOpus1mMergeEnabled() | +| `migrateReplBridgeEnabledToRemoteControlAtStartup` | `globalConfig.replBridgeEnabled` | `globalConfig.remoteControlAtStartup` | Old key exists, new key absent | +| `migrateSonnet1mToSonnet45` | `userSettings.model: 'sonnet[1m]'` | `'sonnet-4-5-20250929[1m]'` | Once (sonnet1m45MigrationComplete flag) | +| `migrateSonnet45ToSonnet46` | Explicit Sonnet 4.5 strings | `'sonnet'` or `'sonnet[1m]'` | 1P + Pro/Max/TeamPremium | +| `resetAutoModeOptInForDefaultOffer` | `userSettings.skipAutoPermissionPrompt` | Clear the setting | TRANSCRIPT_CLASSIFIER + specific conditions | +| `resetProToOpusDefault` | Default model settings | Sets migration timestamp | 1P Pro users | + +--- + +### 11.2 Model Naming Evolution (revealed by migrations) + +The migration history reveals the complete model codename/alias evolution: + +**Opus lineage:** +1. `fennec-latest` — internal codename for early Opus 4.x +2. `claude-opus-4-0` / `claude-opus-4-20250514` — Opus 4.0 release +3. `claude-opus-4-1` / `claude-opus-4-1-20250805` — Opus 4.1 release +4. `opus-4-5-fast` — Opus 4.5 fast variant +5. `opus` — Current Opus 4.6 alias +6. `opus[1m]` — Opus 4.6 with 1M context window + +**Sonnet lineage:** +1. `sonnet[1m]` — Early Sonnet with 1M context (targeted Sonnet 4.5) +2. `claude-sonnet-4-5-20250929` / `sonnet-4-5-20250929` — Sonnet 4.5 explicit +3. `claude-sonnet-4-5-20250929[1m]` / `sonnet-4-5-20250929[1m]` — Sonnet 4.5 with 1M +4. `sonnet` — Current Sonnet 4.6 alias +5. `sonnet[1m]` — Sonnet 4.6 with 1M context (re-aliased) + +**Internal codenames:** +- `fennec-latest` → Opus 4.x series (pre-release internal name) +- `fennec-fast-latest` → Opus fast variant + +--- + +### 11.3 Migration Details + +#### `migrateAutoUpdatesToSettings` +- Only migrates when `globalConfig.autoUpdates === false` AND `autoUpdatesProtectedForNative !== true` +- Adds `DISABLE_AUTOUPDATER: '1'` to `userSettings.env` +- Sets `process.env.DISABLE_AUTOUPDATER = '1'` immediately +- Removes `autoUpdates` and `autoUpdatesProtectedForNative` from globalConfig + +#### `migrateFennecToOpus` (ant-only) +Alias mapping: +- `fennec-latest[1m]` → `opus[1m]` +- `fennec-latest` → `opus` +- `fennec-fast-latest` / `opus-4-5-fast` → `opus[1m]` + `fastMode: true` + +#### `migrateLegacyOpusToCurrent` (1P only) +Migrates: `claude-opus-4-20250514`, `claude-opus-4-1-20250805`, `claude-opus-4-0`, `claude-opus-4-1` → `'opus'` +Sets `legacyOpusMigrationTimestamp` for one-time notification. + +#### `migrateOpusToOpus1m` +Migrates `'opus'` → `'opus[1m]'` for eligible Max/Team Premium 1P users. +Idempotent: only acts when `userSettings.model === 'opus'` exactly. +If the migrated value equals the current default, clears `model` setting instead. + +#### `migrateSonnet1mToSonnet45` +Run once (tracked by `sonnet1m45MigrationComplete` in globalConfig). +Also migrates the in-memory `mainLoopModelOverride` if set. + +#### `migrateSonnet45ToSonnet46` +Migrates: +- `claude-sonnet-4-5-20250929` / `sonnet-4-5-20250929` → `'sonnet'` +- `claude-sonnet-4-5-20250929[1m]` / `sonnet-4-5-20250929[1m]` → `'sonnet[1m]'` +Sets `sonnet45To46MigrationTimestamp` for notification (skipped for new users with `numStartups <= 1`). + +#### `migrateReplBridgeEnabledToRemoteControlAtStartup` +Renames `replBridgeEnabled` (internal implementation detail) to `remoteControlAtStartup` (user-facing key). Only acts when old key exists and new key is absent. + +#### `resetAutoModeOptInForDefaultOffer` +One-shot (guarded by `hasResetAutoModeOptInForDefaultOffer` flag). +Clears `skipAutoPermissionPrompt` for users who accepted the old 2-option dialog but don't have auto as their default mode. Re-surfaces the new dialog with the "make it my default" option. +Only runs when `getAutoModeEnabledState() === 'enabled'` (not 'opt-in'). + +#### `resetProToOpusDefault` +Sets migration timestamp for Pro subscribers on 1P with no custom model setting (UI notification that Opus is now the default). For all other users, simply marks migration complete. + +--- + +## 12. Core Type Definitions (types/) + +### 12.1 Branded ID Types (`types/ids.ts`) + +Prevents mixing SessionId and AgentId at compile time: + +```typescript +type SessionId = string & { readonly __brand: 'SessionId' } +type AgentId = string & { readonly __brand: 'AgentId' } +``` + +**AgentId format:** `a` + optional `