_ โ _ _
___ _ __ ___ _ __ ___ ___ __| | ___ โ__ _(_) |__ ___
/ _ \| '_ \ / _ \ '_ \ / __/ _ \ / _` |/ _ \โ\ \ / / | '_ \ / _ \
| (_) | |_) | __/ | | | (_| (_) | (_| | __/โ \ V /| | |_) | __/
\___/| .__/ \___|_| |_|\___\___/ \__,_|\___โ \_/ |_|_.__/ \___|
|_| โ
Next.js 16 rebuild of the OpenCode web application. Real-time chat UI with streaming message display, SSE sync, and React Server Components.
Warning: This project uses Next.js 16 canary - bleeding edge, expect rough edges. Catppuccin-themed because we're not savages.
- Bun v1.3+ (required - we don't use npm/pnpm)
- OpenCode CLI running locally
bun installThe web UI discovers running OpenCode processes automatically. Use whatever mode you want:
# TUI mode (interactive terminal)
cd /path/to/your/project
opencode
# Or serve mode (headless)
opencode serveRun as many as you want, in different directories. The web UI finds them all.
# From the opencode-next root directory
bun devThis starts the Next.js dev server on port 8423.
Navigate to: http://localhost:8423
You should see the OpenCode web interface with your sessions.
- Multi-server discovery - Finds all running OpenCode processes (TUIs, serves) automatically via
lsof - Cross-process messaging - Send from web UI, appears in your TUI. Routes to the server that owns the session
- Real-time streaming - Messages stream in as the AI generates them
- SSE sync - All updates pushed via Server-Sent Events, merged from all discovered servers
- Slash commands - Type
/for actions like/fix,/test,/refactor - File references - Type
@to fuzzy-search and attach files as context - Catppuccin theme - Latte (light) / Mocha (dark) with proper syntax highlighting
Zero-config server discovery. The web UI finds all running OpenCode processes automatically.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ YOUR MACHINE โ
โ โ
โ Terminal 1 Terminal 2 Terminal 3 โ
โ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โ
โ โopencode โ โopencode โ โopencode โ โ
โ โ tui โ โ tui โ โ serve โ โ
โ โ :4096 โ โ :5123 โ โ :6421 โ โ
โ โ ~/foo โ โ ~/bar โ โ ~/baz โ โ
โ โโโโโโฌโโโโโ โโโโโโฌโโโโโ โโโโโโฌโโโโโ โ
โ โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโดโโโโโโโ โ
โ โ lsof โ discovers all โ
โ โ + verify โ opencode processes โ
โ โโโโโโโโฌโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ WEB UI (:8423) โ โ
โ โ โ โ
โ โ ~/foo sessions โโโ โ โ
โ โ ~/bar sessions โโโผโโ all projects, one view โ โ
โ โ ~/baz sessions โโโ โ โ
โ โ โ โ
โ โ send message โ routes to server that owns the session โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- API route runs
lsofto find processes listening on TCP with "bun" or "opencode" in the command - Hits
/projectendpoint on each candidate to verify it's actually OpenCode - Opens SSE stream to each verified server
- Events include
directoryfield โ routes to correct project in the store
The cool part: Send a message from the web UI and it appears in your TUI. The web discovers which server owns the session and routes there.
Mostly unnecessary - discovery handles it. But if you need overrides:
# apps/web/.env.local
# Fallback URL if discovery finds nothing (default: http://localhost:4056)
NEXT_PUBLIC_OPENCODE_URL=http://localhost:4056
# Force a specific directory (optional)
NEXT_PUBLIC_OPENCODE_DIRECTORY=/path/to/your/projectopencode-next/
โโโ apps/
โ โโโ web/ # Next.js 16 application
โ โโโ src/
โ โ โโโ app/ # App Router pages
โ โ โ โโโ page.tsx # Session list
โ โ โ โโโ session/
โ โ โ โโโ [id]/ # Session detail view
โ โ โโโ components/ # UI components
โ โ โ โโโ ai-elements/ # Chat UI components
โ โ โ โโโ ui/ # Shared UI primitives
โ โ โโโ core/ # SDK client setup
โ โ โโโ lib/ # Utilities
โ โ โโโ react/ # React hooks & providers
โ โ โโโ provider.tsx # OpenCodeProvider
โ โ โโโ store.ts # Zustand store
โ โ โโโ use-sse.tsx # SSE connection hook
โ โ โโโ use-session.ts # Session data hook
โ โ โโโ use-messages.ts # Messages hook
โ โ โโโ use-send-message.ts # Send message hook
โ โโโ package.json
โโโ docs/
โ โโโ adr/ # Architecture Decision Records
โ โโโ guides/ # Implementation guides
โโโ package.json # Root package.json
โโโ turbo.json # Turborepo config
Start here to understand the codebase:
The magic that finds all running OpenCode processes. Uses lsof to find TCP listeners, hits /project to verify they're OpenCode, returns the list. Called on page load.
Opens SSE streams to ALL discovered servers simultaneously. Events include a directory field that routes updates to the correct project in the store. This is how TUI โ Web sync works.
Central state management. Directory-scoped (each project has isolated state). Handles SSE events via handleEvent() which dispatches to specific handlers for sessions, messages, parts, etc. Uses Immer for immutable updates.
Converts OpenCode SDK types โ ai-elements UIMessage format. The SDK returns {info, parts} envelopes; this flattens them for rendering. Also handles tool state mapping.
Server Component that fetches initial data. Uses limit=20 for fast initial load (pagination). Passes data to client components for hydration.
Client Component that renders the message list. Hydrates Zustand store on first render, then subscribes to real-time updates. Uses memoization to prevent re-renders during streaming.
The input box. Handles slash commands (/), file references (@), and message sending. Autocomplete powered by fuzzy search over commands and files.
Chat UI components: Message, Tool, Reasoning, Conversation, etc. Adapted from Vercel's ai-elements patterns. Each component handles its own streaming states.
# Development
bun dev # Start Next.js dev server (port 8423 = VIBE)
bun build # Production build
bun start # Start production server
# Code Quality
bun run typecheck # TypeScript check (via turbo, checks all packages)
bun lint # Run oxlint
bun format # Format with Biome
bun format:check # Check formatting
# Testing
bun test # Run tests
bun test --watch # Watch modeThe web UI provides several hooks for interacting with OpenCode. All hooks use the Effect-based router for type-safe, composable request handling with built-in timeouts, retries, and error handling.
import {
useSession, // Get session data
useMessages, // Get messages for a session
useSendMessage, // Send a message (uses caller internally)
useSessionStatus, // Get session status (idle/busy/error)
useProviders, // List available AI providers (uses caller internally)
useOpenCode, // Access the caller directly
} from "@/react";
// Example: Display session messages
function SessionView({ sessionId }: { sessionId: string }) {
const session = useSession(sessionId);
const messages = useMessages(sessionId);
const { send, isPending } = useSendMessage(sessionId);
const status = useSessionStatus(sessionId);
return (
<div>
<h1>{session?.title}</h1>
<div>Status: {status}</div>
{messages.map((msg) => (
<Message key={msg.id} message={msg} />
))}
<input
onKeyDown={(e) => {
if (e.key === "Enter") {
send(e.currentTarget.value);
}
}}
disabled={isPending}
/>
</div>
);
}
// Example: Using the caller directly
function CustomComponent() {
const { caller } = useOpenCode();
const handleClick = async () => {
// Type-safe route invocation with built-in timeout
const session = await caller("session.create", { title: "New Session" });
console.log(session);
};
return <button onClick={handleClick}>Create Session</button>;
}# Check what's actually running
lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -E 'bun|opencode'Should show at least one process. If not, start OpenCode somewhere.
- OpenCode needs to be running in a project directory
- Check browser console for discovery/SSE errors
- Try the discovery endpoint directly:
curl http://localhost:8423/api/opencode-servers
Check SSE connections in DevTools โ Network โ filter by "event". Should see active streams to discovered servers.
| Layer | Technology | Why |
|---|---|---|
| Runtime | Bun | Fast all-in-one runtime |
| Framework | Next.js 16 | React Server Components, App Router |
| Bundler | Turbopack | Next-gen bundler |
| Language | TypeScript 5+ | Type safety |
| Linting | oxlint | Fast Rust-based linter |
| Formatting | Biome | Fast formatter |
| Styling | Tailwind CSS | Utility-first CSS |
| State | Zustand | Lightweight state management |
| SDK | @opencode-ai/sdk | OpenCode API client |
- ADR 001: Next.js Rebuild - Architecture rationale
- ADR 002: Effect Router - Effect-powered async router
- Router Migration Guide - Migrating to Effect router
- Sync Implementation Guide - SSE sync details
- Subagent Display Guide - Rendering subagent messages
- Mobile Client Guide - Mobile considerations
- Use Bun (not npm/pnpm)
- Follow TDD: RED โ GREEN โ REFACTOR
- Run
bun formatbefore committing - Check
bun lintpasses
MIT