# How OpenCode Implements Terminal UI Server Routes: A Deep Dive into the TUI HTTP API

> Explore how OpenCode implements TUI server routes with its Hono powered HTTP API. Learn about its AsyncQueue and event-driven Bus for seamless request-response handling.

- Repository: [Anomaly/opencode](https://github.com/anomalyco/opencode)
- Tags: deep-dive
- Published: 2026-02-16

---

**OpenCode implements terminal UI server routes using a lightweight HTTP API built on Hono, featuring an AsyncQueue-based control queue for request-response handling and event-driven UI actions published through a global Bus.**

The OpenCode project (available at `anomalyco/opencode`) exposes its built-in terminal UI (TUI) functionality through a structured set of HTTP endpoints. This architecture allows any client—from webviews to CLI tools—to programmatically drive the terminal interface without maintaining persistent WebSocket connections. The implementation relies on Hono for routing, Zod for validation, and a custom AsyncQueue pattern to bridge synchronous UI processes with asynchronous server logic.

## Architecture of the OpenCode TUI Router

The TUI routing system in OpenCode is designed around two primary concerns: a control queue for bidirectional request-response communication, and discrete action endpoints for specific UI operations.

### Lazy-Loaded Hono Router

To optimize startup performance, the `TuiRoutes` router is created lazily using Hono's lazy loading pattern. This ensures that heavy dependencies—particularly the Bus module—are only imported when the `/tui` route is first accessed.

```typescript
export const TuiRoutes = lazy(() =>
  new Hono()
    /* … all POST routes … */
)

```

*Source:* [`packages/opencode/src/server/routes/tui.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/tui.ts) (lines 78-80)

### The AsyncQueue Control Queue

At the heart of the system lies a simple request-response protocol backed by two in-memory **AsyncQueue** instances. These queues, defined in [`packages/opencode/src/util/queue.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/util/queue.ts), buffer requests from the UI process and responses from the server logic.

```typescript
const request = new AsyncQueue<TuiRequest>()
const response = new AsyncQueue<any>()

```

*Source:* [`packages/opencode/src/server/routes/tui.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/tui.ts) (lines 18-20)

## Implementing the Control Queue Pattern

The control queue endpoints under `/tui/control/*` enable a push-pull communication model that decouples the UI process from the server's request handling.

### Request Handling with callTui

When the UI process needs the server to handle a UI request, it invokes the **`callTui`** function. This handler pushes the incoming request onto the `request` queue and blocks until a response appears on the `response` queue.

```typescript
export async function callTui(ctx: Context) {
  const body = await ctx.req.json()
  request.push({ path: ctx.req.path, body })
  return response.next()
}

```

*Source:* [`packages/opencode/src/server/routes/tui.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/tui.ts) (lines 21-27)

### The /next and /response Endpoints

The control routes expose the other side of the queue to the server logic:

- **GET `/tui/control/next`** – Retrieves the next pending request from the UI.

```typescript
.get("/next", async (c) => {
  const req = await request.next()
  return c.json(req)
})

```

*Source:* [`packages/opencode/src/server/routes/tui.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/tui.ts) (lines 30-51)

- **POST `/tui/control/response`** – Accepts the UI's response to a previous request and pushes it onto the response queue.

```typescript
.post("/response", validator("json", z.any()), async (c) => {
  const body = c.req.valid("json")
  response.push(body)
  return c.json(true)
})

```

*Source:* [`packages/opencode/src/server/routes/tui.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/tui.ts) (lines 52-75)

## Event-Based UI Action Endpoints

Beyond the control queue, OpenCode exposes specific action endpoints that trigger UI changes. Each endpoint validates its payload using Zod schemas defined in [`packages/opencode/src/cli/cmd/tui/event.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/cli/cmd/tui/event.ts), then publishes a strongly-typed `TuiEvent` to the global Bus.

### Publishing TuiEvents to the Bus

Every concrete TUI action follows the same pattern: validate the JSON body, publish to the Bus, and return a success boolean.

For example, appending a prompt:

```typescript
.post(
  "/append-prompt",
  validator("json", TuiEvent.PromptAppend.properties),
  async (c) => {
    await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json"))
    return c.json(true)
  },
)

```

*Source:* [`packages/opencode/src/server/routes/tui.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/tui.ts) (lines 80-102)

Other actions follow identical patterns:
- `/open-help`, `/open-sessions`, `/open-themes`, `/open-models`
- `/submit-prompt`, `/clear-prompt`
- `/show-toast`
- `/select-session`

### Command Mapping in /execute-command

The **`/execute-command`** endpoint translates high-level command identifiers (like `"session.list"`) into internal Bus commands using an object literal lookup:

```typescript
await Bus.publish(TuiEvent.CommandExecute, {
  command: {
    session_new: "session.new",
    // … other mappings …
    agent_cycle: "agent.cycle",
  }[command],
})

```

*Source:* [`packages/opencode/src/server/routes/tui.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/tui.ts) (lines 66-86, within the execute-command handler)

## Integrating TUI Routes into the Main Server

The TUI router is mounted on the central Hono application in [`packages/opencode/src/server/server.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/server.ts). This registration occurs during server initialization:

```typescript
.route("/tui", TuiRoutes())

```

*Source:* [`packages/opencode/src/server/server.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/server.ts) (lines 35-38)

When `Server.listen` is invoked, the TUI endpoints become available at `<host>:<port>/tui/...`, allowing external clients to interact with the terminal interface via standard HTTP requests.

## Practical Example: Driving the TUI via HTTP

Below is a complete Node.js example demonstrating how to interact with OpenCode's TUI server routes. This illustrates the full request-response cycle and action triggering:

```javascript
const base = 'http://localhost:3000';

// 1️⃣ Poll for the next UI request from the server
const next = await fetch(`${base}/tui/control/next`).then(r => r.json());
console.log('Server wants you to do:', next.path, next.body);

// 2️⃣ Send the response back to the server
await fetch(`${base}/tui/control/response`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ ok: true, data: 'result' })
});

// 3️⃣ Trigger a specific UI action (e.g., open help dialog)
await fetch(`${base}/tui/open-help`, { method: 'POST' });

// 4️⃣ Append a prompt to the TUI
await fetch(`${base}/tui/append-prompt`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ text: 'Hello from API', metadata: {} })
});

```

All routes return a JSON boolean (`true`) on success, making them composable in async workflows.

## Summary

OpenCode's terminal UI server routes provide a lightweight, HTTP-based API for programmatically controlling the built-in TUI:

- **Lazy-loaded Hono router** (`TuiRoutes`) minimizes startup overhead by deferring heavy imports until the `/tui` endpoint is first accessed.
- **AsyncQueue control pattern** (`/tui/control/next` and `/tui/control/response`) enables bidirectional request-response communication without WebSocket persistence.
- **Event-driven architecture** where action endpoints validate payloads with Zod and publish strongly-typed `TuiEvent` objects to the global Bus.
- **Command mapping** in `/execute-command` translates public API identifiers to internal Bus commands using a lookup table.
- **Simple integration** via `.route("/tui", TuiRoutes())` in the main server file.

This design decouples the transport layer from the core TUI logic, allowing any HTTP client to drive the terminal interface.

## Frequently Asked Questions

### How does OpenCode handle bidirectional communication without WebSockets?

OpenCode uses an **AsyncQueue-based control queue** pattern exposed through `/tui/control/next` and `/tui/control/response` endpoints. The client polls `/next` to receive requests from the server, performs the requested UI work, then POSTs the result to `/response`. This push-pull mechanism mimics request-response semantics over stateless HTTP, eliminating the need for persistent WebSocket connections while maintaining synchronous-like communication flow.

### What validation library does OpenCode use for TUI server routes?

OpenCode uses **Zod** for schema validation across all TUI server routes. The implementation combines Hono's `validator` middleware with Zod schemas defined in [`packages/opencode/src/cli/cmd/tui/event.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/cli/cmd/tui/event.ts). For example, the `/append-prompt` endpoint validates incoming JSON against `TuiEvent.PromptAppend.properties` before publishing to the Bus, ensuring type safety and runtime validation without manual checks.

### How are TUI action endpoints registered in the main server?

The TUI router is registered in the main server through Hono's route mounting mechanism. In [`packages/opencode/src/server/server.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/server.ts), the code calls `.route("/tui", TuiRoutes())` during server initialization. This mounts all TUI endpoints under the `/tui` path prefix, making them available at runtime when `Server.listen` is invoked. The `TuiRoutes` function is lazy-loaded to defer heavy dependency imports until the first request hits the endpoint.

### Can external clients trigger specific TUI commands through the API?

Yes, external clients can trigger specific TUI commands through the **`/tui/execute-command`** endpoint. This endpoint accepts high-level command identifiers (such as `"session_new"` or `"agent_cycle"`) and maps them to internal Bus commands using an object literal lookup table. After validation, the endpoint publishes a `TuiEvent.CommandExecute` event to the global Bus, which the TUI process consumes to execute the corresponding internal command.