# ADR-0006 — Embed Claude Code via ttyd, not a native chat backend

**Status:** Accepted
**Date:** 2026-05-05
**Deciders:** JT, Claude (in dialogue)
**Supersedes:** none
**Superseded by:** none

> *This ADR was drafted during the embedded-claude-terminal Phase 11 cleanup commit. It locks in the substrate decision made in BUILD_SPEC §4 (item ADR-0003 in spec text; renumbered to 0006 because the repo already has ADRs 0001–0005).*

## Context

Console v2's right column needs a chat surface that can invoke the Recoil engine, drive the visual pipeline, edit files, run shell commands, and dispatch agents. Two substrates were available:

1. **Native chat backend.** Build `/api/chat` against the Anthropic SDK, wire tool calls back into FastAPI handlers, re-implement file I/O / Bash / MCP / skills / hooks inside that surface.
2. **Embed Claude Code itself.** Run `claude --resume <session_id>` inside `ttyd --writable`, mount the resulting xterm endpoint in an iframe, and let Claude Code's existing tool surface do the work.

Phase 20 of the console-v2 build had stubbed `/api/chat` (canned SSE stream, no SDK call). Promoting that stub to a real chat backend would have required rebuilding Bash, Edit, Read, MCP, skills, and hooks — months of engineering, plus permanent maintenance drift against upstream Claude Code.

## Decision

The right column embeds Claude Code via `ttyd --writable`. `claude --resume <session_id>` runs inside the ttyd-served xterm, the React shell wraps it in a `<TerminalIframe>`, and the iframe's parent window receives lifted keyboard shortcuts via `postMessage` (the keystroke bubbler).

The native `/api/chat` endpoint is unregistered from `recoil/api/main.py` in this phase. `chat_routes.py` remains on disk for git-history rollback safety but is no longer reachable from the FastAPI app surface.

## Consequences

**Tool surface comes free.** Bash, Edit, Read, Glob, Grep, MCP tools, all of `~/.claude/skills/`, all installed hooks — the entire Claude Code surface area is available inside the right column with zero re-implementation.

**Process-management complexity.** ttyd is an external process tree per project; lifecycle (start/stop/status), zombie cleanup, port allocation, and atexit/signal handlers all live in `recoil/api/ttyd_routes.py`. This complexity is mitigated by Phase 3 of the build but is real — a native chat backend would have folded into the existing FastAPI process.

**No `/api/chat` to maintain.** The 200-line canned stub at `chat_routes.py` is dormant. CP-N+ is no longer pressured to wire a real Anthropic SDK round-trip there — that work is implicitly out of scope.

**Frame boundaries are real.** The iframe runs in its own JS context; cross-frame state sharing is `postMessage`-only. The keystroke bubbler enumerates the lifted shortcut whitelist (⌘K, ⌘\, ⌘$, ⌘B, ⌘[, ⌘], ⇧H, ⌘J).

## Alternatives considered

- **Native `/api/chat` against Anthropic SDK.** Rejected — multi-month build, permanent drift against Claude Code upstream, zero leverage on the existing tool surface.
- **WebSocket proxy to a headless `claude` subprocess.** Rejected — re-implements ttyd badly. ttyd is a battle-tested xterm-over-WS server; rebuilding that piece adds risk for no gain.
- **Electron-wrap Claude Code as a native panel.** Rejected — couples the console to a desktop runtime; console-v2 is browser-first by design.

## Implementation

- `recoil/api/main.py` — `chat_router` import + `include_router` call removed in Phase 11.
- `recoil/api/chat_routes.py` — left on disk for rollback; no longer registered.
- `recoil/api/ttyd_routes.py` — per-project ttyd lifecycle (start/stop/status/context-window).
- `recoil/console-v2/packages/desktop/src/components/ChatColumn.tsx` — mounts `<TerminalIframe>`.

## Verification

```
$ ! grep -q 'chat_router' recoil/api/main.py
$ curl -X POST localhost:8431/api/chat -d '{}'
{"detail":"Not Found"}   # 404 confirms route surface intent
```
