_ β _ _
___ _ __ ___ _ __ ___ ___ __| | ___ β__ _(_) |__ ___
/ _ \| '_ \ / _ \ '_ \ / __/ _ \ / _` |/ _ \β\ \ / / | '_ \ / _ \
| (_) | |_) | __/ | | | (_| (_) | (_| | __/β \ 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