Skip to content

Transformer Math

Module 58 · AI Engineering

🌉 Bridges & IDE Integration

A WebSocket reconnect drops to 0ms perceived latency for the user — but rebuilds the entire IDE state in 3 round trips. Here’s why that’s a design constraint, not a bug.

Status:

Claude Code runs the same QueryEngine whether you use it in a terminal, VS Code, JetBrains, or the web. The bridge pattern connects each frontend to this shared engine — same AI logic, different UI surfaces.

  • Terminal: Ink renderer (React to ANSI), direct stdin/stdout
  • IDE (VS Code, JetBrains): WebSocket bridge, permission dialogs rendered in IDE panels
  • Web: Next.js app with Zustand, separate API client, ink-compat adapter for shared renderers
🎮

Bridge Architecture

What you are seeing

How one QueryEngine instance serves three different frontends. Each frontend connects through its own bridge adapter, but they all share the same session, tool execution, and streaming logic.

What to try

Trace the IDE path: an extension sends a user message over WebSocket, the bridge creates a session, the engine streams events back, and the extension renders them in its panel. Compare this to the terminal path where everything happens in-process.

Bridge Architecture — One Engine, Three FacesClick a frontend to highlight its pathTerminal (Ink)stdin / stdoutIDE ExtensionVS Code / JetBrainsWeb (Next.js)browser frontendQueryEngineone brain, three facessame tools · same permissions · same API callsClaude APIPOST /v1/messagesIDE Bridge FlowIDE ExtensionWebSocketbridgeMain.tscreateSession()QueryEngine← approval · tokens →permission reqapproval / tokensShared engineFrontend / APITerminal tint
💡

The Intuition

Why Not Three Separate Codebases?

If you fix a bug in the terminal version, you would have to fix it again in the IDE version and again in the web version. The bridge pattern means ONE QueryEngine, ONE tool system, ONE permission system — the UI is just a thin rendering layer. New frontends cost one adapter, not a full reimplementation.

Why a Bridge Pattern?

An AI coding agent is complex: tool execution, context window management, permission enforcement, streaming, error recovery. If you build separate codebases for terminal, IDE, and web, you maintain 3 copies of this logic. Bugs fixed in terminal don't get fixed in IDE. The bridge pattern keeps one canonical engine and lets each frontend connect through a thin adapter layer.

The IDE Bridge

When Claude Code runs in VS Code, the extension spawns a headless CLI process and connects via WebSocket. The bridgeMain.ts entry point creates a session, initializes the REPL and QueryEngine, and routes messages between the WebSocket and the engine. Outbound events (assistant messages, tool calls, streaming tokens) are serialized and sent to the extension, which renders them in its webview panel.

💡 Tip · The bridge is bidirectional: the IDE sends user messages and permission responses downstream, while the engine streams events and permission requests upstream. This is the same pattern used by language server protocol (LSP) — a generic transport with typed messages.

Permission in Bridge Mode

In terminal mode, permission dialogs are simple stdin prompts. In IDE mode, the engine sends a permission_requestmessage over WebSocket with the tool name and input. The IDE extension renders a native dialog (VS Code's showInformationMessage), and sends back the user's decision. The engine's permission_callback is injected at session creation — it never knows which UI rendered the dialog.

The ink-compat Adapter

Tool output renderers (file diffs, search results, command output) are written using Ink components: Box, Text, Spinner, Static. In the terminal, Ink renders these to ANSI. In the web, the ink-compat adapter maps them to HTML: Box becomes a div with flexbox, Text becomes a span with CSS, Spinner becomes a CSS animation. This lets the same renderer code work in all three frontends without duplication.

✨ Insight · Remote sessions add another layer: WebSocket with exponential backoff reconnect, message routing for multiple viewers, and a viewer-only mode that injects a permission_callback always returning false — observers can watch but never approve tool calls.

The WebSocket Message Protocol

The bridge communicates via typed JSON messages over WebSocket — the same pattern as the Language Server Protocol (LSP). Each message has a type discriminant that drives routing on both sides. Upstream messages (engine → IDE) include: assistant_token (streaming text chunk), tool_call (tool name + input), tool_result (output after execution), permission_request (awaiting user approval), and session_end. Downstream messages (IDE → engine) include: user_message and permission_response. This typed protocol means the IDE extension never needs to parse raw ANSI or understand agent internals — it only handles the message types it cares about. New message types are additive and backward-compatible, the same evolution strategy as LSP.

Reconnection and State Recovery

WebSocket connections drop — flaky Wi-Fi, laptop sleep, network switches. The bridge handles this with exponential backoff reconnect: on disconnect, the IDE extension retries at 1s, 2s, 4s, 8s intervals up to a cap. But reconnection alone is not enough — the session must resume where it left off. The engine assigns each session a stable ID. On reconnect, the extension sends the session ID; the engine replays buffered events from the point of disconnection. This is analogous to HTTP range requests: the client tells the server how far it got, the server sends only what the client missed. Without replay, a reconnected session would show a blank panel — the user loses the conversation history and any in-progress tool output that arrived during the disconnection window.

Quick Check

What changes when Claude Code runs in VS Code vs terminal?

📐

Key Code Patterns

Bridge (TypeScript pseudocode)

typescript
class Bridge {
  // Connects IDE extension to CLI engine via WebSocket
  private ws: WebSocket;
  private engine: QueryEngine | null = null;

  constructor(wsUrl: string) {
    this.ws = new WebSocket(wsUrl);
  }

  onConnect(): void {
    createSession();
    this.engine = new QueryEngine({
      permissionCallback: (tool, input) => this.idePermissionDialog(tool, input),
    });
  }

  onMessage(userMsg: string): void {
    for (const event of this.engine!.submit(userMsg)) {
      this.ws.send(JSON.stringify(event));
    }
  }

  async idePermissionDialog(tool: string, input: unknown): Promise<boolean> {
    this.ws.send(JSON.stringify({ type: "permission_request", tool, input }));
    // ws.receive() is pseudocode — real impl wraps ws.onmessage in a Promise
    const response = await this.ws.receive();
    return response.allowed;
  }
}

ink-compat Adapter (TypeScript pseudocode)

typescript
type InkNodeType = "Box" | "Text" | "Spinner" | "Static";

// Adapts Ink terminal components to browser HTML equivalents
const componentMap: Record<InkNodeType, (props: InkProps) => string> = {
  Box:     (props) => `<div style="display:flex; ${flexboxCss(props)}">`,
  Text:    (props) => `<span style="${textCss(props)}">`,
  Spinner: (_props) => '<span class="css-spinner">',
  Static:  (_props) => '<div class="static-output">',
};

function* renderInkTree(inkTree: InkNode): Generator<string> {
  // Walk Ink component tree, emit HTML
  for (const node of walk(inkTree)) {
    const adapter = componentMap[node.type as InkNodeType];
    yield adapter(node.props);
    yield* renderInkTree(node.children);
    yield closingTag(node.type);
  }
}

function flexboxCss(props: InkProps): string {
  const direction = props.flexDirection ?? "column";
  const justify = props.justifyContent ?? "flex-start";
  const padding = props.padding ?? 0;
  return `flex-direction:${direction}; justify-content:${justify}; padding:${padding}ch;`;
}

Permission Callback Injection

typescript
type SessionMode = "terminal" | "ide" | "web" | "viewer";
type PermissionCallback = (tool: string, input: unknown) => Promise<boolean>;

// Factory: same engine, different permission UIs
function createSession(mode: SessionMode): QueryEngine {
  let callback: PermissionCallback;

  if (mode === "terminal") {
    callback = terminalPermission;      // stdin prompt
  } else if (mode === "ide") {
    callback = idePermission;           // WebSocket dialog
  } else if (mode === "web") {
    callback = webPermission;           // Zustand modal
  } else {
    callback = async () => false;       // viewer — read-only
  }

  return new QueryEngine({ permissionCallback: callback });
}

async function terminalPermission(tool: string, _input: unknown): Promise<boolean> {
  const answer = await readLine(`Allow ${tool}? [y/n] `);
  return answer.toLowerCase() === "y";
}

async function idePermission(tool: string, input: unknown): Promise<boolean> {
  ws.send(JSON.stringify({ type: "permission_request", tool, input }));
  // ws.receive() is pseudocode — real impl: wrap ws.onmessage in a Promise
  const response = await ws.receive();
  return response.allowed;
}

async function webPermission(tool: string, _input: unknown): Promise<boolean> {
  // store.dispatch/waitFor is pseudocode — real Zustand impl uses
  // setState + a subscribe-based Promise wrapper (Zustand has no built-in waitFor)
  store.dispatch({ type: "SHOW_PERMISSION_MODAL", tool });
  return store.waitFor("PERMISSION_RESPONSE").then((r) => r.allowed);
}
🔧

Break It — See What Happens

No bridge layer (separate codebases per platform)
No permission bridging
📊

Real-World Numbers

AspectValue
Frontends served3 (terminal, IDE, web)
Shared engine1 QueryEngine instance per session
IDE bridge transportWebSocket with exponential backoff reconnect
ink-compat adapted components4 core (Box, Text, Spinner, Static)
Remote session modesFull control, viewer-only (permission always denied)
✨ Insight · The bridge pattern means a new frontend (e.g., a mobile app) only requires a new thin adapter — the QueryEngine, tool execution, and permission model are already built and tested.
🧠

Key Takeaways

What to remember for interviews

  1. 1One QueryEngine serves terminal, IDE, and web — each frontend is a thin adapter, so bugs fixed in the engine are fixed everywhere simultaneously.
  2. 2The IDE bridge spawns a headless CLI process and communicates over WebSocket using typed JSON messages — the same pattern as the Language Server Protocol.
  3. 3Permission callbacks are injected at session creation, so the engine never knows which UI renders the dialog: stdin prompt, VS Code dialog, or React modal.
  4. 4The ink-compat adapter maps 4 Ink primitives (Box, Text, Spinner, Static) to HTML equivalents, letting tool renderers be written once and work across all frontends.
  5. 5On WebSocket disconnect, the engine buffers events by session ID and replays them on reconnect — preventing blank panels when the user's network drops.
📚

Further Reading

  • VS Code Extension API Official docs for building VS Code extensions — the primary IDE integration surface for Claude Code.
  • WebSocket RFC 6455 The protocol spec underlying IDE-to-engine communication in bridge mode.
  • Ink: React for interactive command-line apps The terminal React renderer whose components are adapted by ink-compat for cross-platform rendering.
  • VS Code Extension Host Architecture How VS Code isolates extensions in a separate process — the same isolation model used by the Claude Code IDE bridge to sandbox the engine from the editor.
  • Adapter Pattern (Refactoring Guru) The structural design pattern at the heart of the bridge — converting one interface (QueryEngine) into multiple frontend-specific interfaces.
  • React Native Architecture Overview The gold standard for a single React tree rendering to multiple native targets — the same multi-renderer problem Claude Code's bridge solves for terminal, IDE, and web.
🎯

Interview Questions

Difficulty:
Company:

Showing 4 of 4

Design a system where the same AI engine serves terminal, IDE, and web interfaces.

★★★
AnthropicMeta

How would you handle permission dialogs across different UI frontends?

★★☆
Google

What's the benefit of an ink-compat adapter layer?

★★☆
Anthropic

What are the tradeoffs between IDE-native and stdio-based bridges for editor integration?

★★☆
Anthropic