Skip to content

Documentation: Web Interface Client Interaction Architecture #11616

@BenceBertalan

Description

@BenceBertalan

Overview

This issue documents the architecture and implementation details of how the OpenCode web interface handles client interaction, including messaging, event handling, and real-time updates.

Architecture

The opencode web command starts a local Hono-based HTTP server that serves as the backend API for the web interface. The architecture follows a Local Server + Remote UI Proxy model:

  • Backend: Local Hono server running on the user's machine (default port 4096)
  • Frontend: Static UI assets proxied from https://app.opencode.ai
  • Communication: RESTful API + Server-Sent Events (SSE) for real-time updates

Key Files

  • packages/opencode/src/cli/cmd/web.ts - Web command entry point
  • packages/opencode/src/server/server.ts - Main server configuration and routing
  • packages/opencode/src/server/routes/session.ts - Session and message handling
  • packages/opencode/src/server/event.ts - Server event definitions
  • packages/opencode/src/bus/bus-event.ts - Event bus system
  • packages/opencode/src/server/routes/question.ts - Question handling
  • packages/opencode/src/server/routes/permission.ts - Permission handling

Message Handling

Sending Messages

The web client sends messages to sessions via HTTP POST requests:

Endpoint: POST /session/:sessionID/message

Request Body:

{
  text: string,
  files?: FilePart[],
  agent?: string,
  model?: { providerID: string, modelID: string },
  // ... other optional fields
}

Response: Streams the assistant's response as JSON

Implementation: packages/opencode/src/server/routes/session.ts:697-736

Related Endpoints:

  • POST /session/:sessionID/prompt_async - Asynchronous message sending (returns immediately)
  • POST /session/:sessionID/command - Send slash commands
  • POST /session/:sessionID/shell - Execute shell commands

Receiving Messages

Messages are received in two ways:

  1. Initial Fetch: GET /session/:sessionID/message

    • Returns all messages in a session with their parts
    • Implementation: packages/opencode/src/server/routes/session.ts:546-583
  2. Real-time Updates: Via SSE stream (see Event Handling below)

Message Structure

Messages follow a structured format:

type Message = {
  info: UserMessage | AssistantMessage,
  parts: Part[]
}

type Part = 
  | TextPart 
  | ToolPart 
  | FilePart 
  | ReasoningPart 
  | SnapshotPart
  | PatchPart
  | AgentPart
  | RetryPart
  | CompactionPart
  | SubtaskPart
  | StepStartPart
  | StepFinishPart

Reference: packages/opencode/src/session/message-v2.ts

Event Handling (Real-time Updates)

Event Stream Connection

Endpoint: GET /event

Protocol: Server-Sent Events (SSE)

Implementation: packages/opencode/src/server/server.ts:474-529

The client establishes a persistent SSE connection to receive real-time updates. The server:

  1. Immediately sends a server.connected event upon connection
  2. Subscribes to all bus events and forwards them to the client
  3. Sends heartbeat events every 30 seconds to prevent connection timeouts
  4. Automatically closes the stream when the instance is disposed

Event Bus System

OpenCode uses a centralized event bus (BusEvent) to broadcast updates across the system:

// Event definition pattern
BusEvent.define("event.type", zodSchema)

Core Implementation: packages/opencode/src/bus/bus-event.ts

All events follow a discriminated union pattern:

type Event = {
  type: string,
  properties: { /* event-specific data */ }
}

Message Events

Event Types (from packages/opencode/src/session/message-v2.ts:401-430):

  1. message.updated

    {
      type: "message.updated",
      properties: {
        info: Message
      }
    }
    • Fired when a message is created or modified
    • Contains the full message info
  2. message.removed

    {
      type: "message.removed",
      properties: {
        sessionID: string,
        messageID: string
      }
    }
    • Fired when a message is deleted
  3. message.part.updated

    {
      type: "message.part.updated",
      properties: {
        part: Part,
        delta?: string
      }
    }
    • Fired when a message part is created or updated
    • Used for streaming responses (text parts updated incrementally)
    • The delta field contains incremental text updates
  4. message.part.removed

    {
      type: "message.part.removed",
      properties: {
        sessionID: string,
        messageID: string,
        partID: string
      }
    }
    • Fired when a message part is deleted

Non-Message Event Handling

Todo Events

Event Type: todo.updated (from packages/opencode/src/session/todo.ts:18)

{
  type: "todo.updated",
  properties: {
    sessionID: string,
    todos: Todo[]
  }
}

REST Endpoints:

  • GET /session/:sessionID/todo - Fetch current todos

Implementation: packages/opencode/src/server/routes/session.ts:155-183

Question Events

Event Type: question.asked (from packages/opencode/src/question/index.ts:64)

{
  type: "question.asked",
  properties: {
    requestID: string,
    sessionID: string,
    questions: Question[]
  }
}

REST Endpoints:

  • GET /question/ - List all pending questions
  • POST /question/:requestID/reply - Answer a question
  • POST /question/:requestID/reject - Reject a question

Implementation: packages/opencode/src/server/routes/question.ts

When the AI needs user input, it:

  1. Creates a question request with a unique requestID
  2. Broadcasts question.asked event via SSE
  3. Waits for client response via POST
  4. Broadcasts question.replied or question.rejected event

Permission Events

Event Type: permission.asked (from packages/opencode/src/permission/next.ts:97)

{
  type: "permission.asked",
  properties: {
    requestID: string,
    sessionID: string,
    // ... permission details
  }
}

REST Endpoints:

  • GET /permission/ - List all pending permissions
  • POST /permission/:requestID/reply - Approve/deny permission

Implementation: packages/opencode/src/server/routes/permission.ts

The permission flow:

  1. AI requests permission (e.g., to execute a command)
  2. Server broadcasts permission.asked event
  3. Client displays permission UI
  4. User approves/denies via POST request
  5. Server broadcasts permission.replied event

Session Events

Additional session-level events (from packages/opencode/src/session/index.ts:106-131):

  • session.created - New session created
  • session.updated - Session metadata updated
  • session.deleted - Session removed
  • session.diff - File changes summary
  • session.error - Session error occurred

Status Events

Event Type: session.status (from packages/opencode/src/session/status.ts:28)

{
  type: "session.status",
  properties: {
    sessionID: string,
    status: "idle" | "active" | "error"
  }
}

Indicates the current execution state of a session.

Authentication

If OPENCODE_SERVER_PASSWORD is set, the server uses HTTP Basic Auth:

  • Username: OPENCODE_SERVER_USERNAME (default: "opencode")
  • Password: OPENCODE_SERVER_PASSWORD

Implementation: packages/opencode/src/server/server.ts:80-85

CORS Configuration

The server allows connections from:

  • localhost on any port
  • 127.0.0.1 on any port
  • tauri://localhost (for desktop app)
  • *.opencode.ai (HTTPS only)
  • Custom origins via the --cors flag

Implementation: packages/opencode/src/server/server.ts:103-123

Client Implementation Pattern

A typical web client would:

  1. Initialize:

    • Connect to local server (detect via mDNS or user config)
    • Establish SSE connection to /event
  2. Handle Events:

    const eventSource = new EventSource('/event');
    eventSource.onmessage = (event) => {
      const { type, properties } = JSON.parse(event.data);
      switch(type) {
        case 'message.part.updated':
          // Update UI with new message part
          break;
        case 'question.asked':
          // Show question modal
          break;
        case 'permission.asked':
          // Show permission request
          break;
        // ... handle other events
      }
    };
  3. Send Messages:

    await fetch(`/session/${sessionID}/message`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: userInput })
    });
  4. Respond to Questions/Permissions:

    await fetch(`/question/${requestID}/reply`, {
      method: 'POST',
      body: JSON.stringify({ answers: [...] })
    });

Key Observations

  1. Separation of Concerns: The backend logic runs locally while UI assets are served remotely, allowing UI updates without CLI updates

  2. Event-Driven Architecture: All real-time updates flow through a centralized event bus with strong typing via Zod schemas

  3. Streaming Support: Message parts can be streamed incrementally using the delta field in message.part.updated events

  4. Type Safety: All API contracts are defined using Zod schemas and exposed via OpenAPI documentation at /doc

  5. Flexible Deployment: Supports network access (0.0.0.0), mDNS discovery, and custom CORS origins for various deployment scenarios

Related Documentation

  • OpenAPI spec available at: GET /doc
  • Full event type definitions: Search codebase for BusEvent.define
  • Message schemas: packages/opencode/src/session/message-v2.ts

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions