How OpenCode Implements Terminal UI Server Routes: A Deep Dive into the TUI HTTP API
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.
export const TuiRoutes = lazy(() =>
new Hono()
/* … all POST routes … */
)
Source: 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, buffer requests from the UI process and responses from the server logic.
const request = new AsyncQueue<TuiRequest>()
const response = new AsyncQueue<any>()
Source: 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.
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 (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.
.get("/next", async (c) => {
const req = await request.next()
return c.json(req)
})
Source: 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.
.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 (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, 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:
.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 (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:
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 (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. This registration occurs during server initialization:
.route("/tui", TuiRoutes())
Source: 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:
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/tuiendpoint is first accessed. - AsyncQueue control pattern (
/tui/control/nextand/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
TuiEventobjects to the global Bus. - Command mapping in
/execute-commandtranslates 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. 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, 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →