NEVER use chrome-devtools_* tools directly in the main conversation.
Chrome DevTools dumps massive snapshots that exhaust context. Always spawn a subagent:
Task(subagent_type="explore", description="Debug via Chrome DevTools", prompt="...")
opencode-next - Next.js 16 rebuild of OpenCode web application.
Turborepo monorepo transitioning from SolidJS to Next.js 16+ with React Server Components. Uses effect-atom based reactive World Stream for state management, with ai-elements chat UI.
Architecture Philosophy:
- Core owns computation, React binds UI - Smart boundary pattern (ADR-016)
- World Stream is THE API - Push-based reactive state via
createWorldStream()(ADR-018) - Router is DELETED - 4,377 LOC removed, it was solving the wrong problem
Why Next.js 16?
- Flat RSC hierarchy (eliminates 13+ nested providers)
- Better mobile patterns (React hooks map to scroll behavior)
- 30-40% code reduction with ai-elements
- React is 10x more common than SolidJS (easier hiring)
| Layer | Technology | Why |
|---|---|---|
| Runtime | Bun | Fast, 10x faster installs |
| Testing | Vitest | Isolated tests, proper ESM support |
| Framework | Next.js 16 canary | RSC, App Router, Turbopack |
| Monorepo | Turborepo | Incremental builds |
| Language | TypeScript 5+ | Type safety |
| Linting | oxlint | Fast Rust-based linter |
| Formatting | Biome | Fast Prettier replacement |
| Chat UI | ai-elements | Battle-tested React chat components |
| Styling | Tailwind CSS | Utility-first CSS |
# Setup
bun install
# Dev
bun dev
bun build
# Type check (MANDATORY - checks full monorepo)
bun run typecheck # Runs: turbo type-check
# Code quality
bun lint
bun format
bun format:fix
# Testing (use Vitest, NOT bun test - better isolation)
bun run test # Runs: vitest run
bun run test:watch # Runs: vitest
bun run test:coverage # Runs: vitest run --coverageCRITICAL:
- Always
bun run typecheckfrom repo root before committing - Never use
bun testdirectly - Vitest has proper isolation, Bun test leaks state
Every changeset MUST include:
- A relevant quote from pdf-brain - Search for wisdom related to your change
- Sick ASCII art - Creative, memorable
# Example changeset:
---
"@opencode-vibe/core": minor
---
feat(api): add session caching layer
╔═══════════════════════════════════════╗
║ 🧠 CACHE ME OUTSIDE, HOW BOW DAH 🧠 ║
╠═══════════════════════════════════════╣
║ ┌─────────┐ ║
║ │ REQUEST │──┐ ║
║ └─────────┘ │ ┌───────┐ ║
║ ├──│ CACHE │ ║
║ ┌─────────┐ │ └───────┘ ║
║ │ BACKEND │──┘ ║
║ └─────────┘ ║
╚═══════════════════════════════════════╝
> "The purpose of abstraction is not to be vague, but to create
> a new semantic level in which one can be absolutely precise."
> — Dijkstra, via pdf-brain
Adds LRU cache for session lookups, reducing backend calls by 80%.
ASCII Art Inspiration:
# The Swarm 🐝
\ /
`. _\|/_ .'
- ( o bg) - bzzzz
.' /|\ `.
/ | \ ╭─────────────╮
🐝 🐝 🐝 │ HIVE MIND │
🐝 🐝 🐝 🐝 │ ACTIVATED │
🐝 🐝 🐝 🐝 🐝 ╰─────────────╯
# The Phoenix Refactor 🔥
,//
///
////
/////
,///////
////////
'////'
'||'
|| FROM THE ASHES
|| OF LEGACY CODE
/||\ WE RISE
//||\\
# The Crab Rave 🦀
(\/) (°,,°) (\/)
RUST SAYS:
"MEMORY SAFE, BB"
╱|、
(˚ˎ 。7
|、˜〵
じしˍ,)ノ
# The Kraken Release 🦑
___
.-' '-.
/ .-=-. \ RELEASE
| / \ | THE
| | O O | | KRAKEN
\| ___ |/
'.___.'
/||||||||\
//||||||||\\
RED → GREEN → REFACTOR
Every feature. Every bug fix. No exceptions.
- RED - Write failing test first
- GREEN - Minimum code to pass
- REFACTOR - Clean up while green
Bug fixes: Write test that reproduces bug FIRST, then fix. Prevents regression forever.
NO DOM TESTING. If the DOM is in the mix, we already lost.
renderHookandrenderfrom@testing-libraryare code smells- Test pure functions and hooks logic directly
- Test state management (Zustand stores) in isolation
- Test API/SDK integration with mocks
- Use E2E tests (Playwright) for actual UI verification
USE VITEST, NOT BUN TEST. Bun test has poor isolation - Zustand stores and singletons leak state between tests causing flaky failures.
FIND IT → FIX IT → DON'T BLAME OTHERS
If you encounter broken code, fix it. No excuses.
- Pre-existing type errors? Fix them.
- Failing tests unrelated to your task? Fix them or file a cell.
- Broken imports? Fix them.
- Dead code? Delete it.
What NOT to do:
- ❌ "That's a pre-existing issue" (it's YOUR issue now)
- ❌ "Another agent broke this" (doesn't matter, fix it)
- ❌ "Out of scope" (broken code is always in scope)
- ❌ Leave
// TODOcomments for others (do it yourself)
The codebase should be BETTER after every session, not just different.
If you can't fix it immediately, file a hive cell with priority 1.
CRITICAL: Never edit package.json manually.
# ✅ CORRECT - Use bun CLI
bun add <package> # Production dependency
bun add -d <package> # Dev dependency
bun remove <package> # Uninstall
# ❌ WRONG - Manual edits break lockfile integrityUse Bun instead of Node.js, npm, pnpm, or vite.
# ✅ Use Bun equivalents
bun <file> # Instead of node <file>
bun test # Instead of jest/vitest
bun build <file.html> # Instead of webpack/vite
bun install # Instead of npm/pnpm install
bunx <package> # Instead of npxNo app-level auth needed. Tailscale provides network-level authentication.
- No OAuth flows in the web app
- No JWT tokens in cookies
- No user login/logout UI
- Trust the network layer
Preserved from backend. Elegant, portable, per-directory instance scoping.
// Backend: packages/opencode/src/util/context.ts
export namespace Context {
export function create<T>(name: string) {
const storage = new AsyncLocalStorage<T>();
return {
use() { return storage.getStore()!; },
provide<R>(value: T, fn: () => R) { return storage.run(value, fn); }
};
}
}
// Usage: Per-directory instance scoping
Instance.provide({ directory: "/path" }, async () => {
const dir = Instance.directory; // All code has access to directory context
});Push-based state management with effect-atom. Core derives ALL state, emits complete consistent world snapshots.
SSE events → effect-atom invalidation → derived world state → consumers
The API - createWorldStream():
import { createWorldStream } from "@opencode-vibe/core/world"
const stream = createWorldStream({
baseUrl: "http://localhost:3000",
directory: "/path/to/project"
})
// Subscribe pattern (React)
const unsubscribe = stream.subscribe((world) => {
console.log(world.sessions, world.activeSessionCount)
})
// Async iterator pattern (CLI/TUI)
for await (const world of stream) {
render(world)
}
// One-shot snapshot
const world = await stream.getSnapshot()Key Principle: Core pushes complete world state; consumers just subscribe. No coordination burden on clients.
Key Files:
packages/core/src/world/merged-stream.ts- Unified streaming (SSE + pluggable sources)packages/core/src/world/stream.ts- Public API (delegates to merged-stream)packages/core/src/world/atoms.ts- World store (effect-atom state)packages/react/src/hooks/use-world.ts- React binding (calls Core promise APIs)packages/core/src/world/sse.ts- SSE connection feeds atoms
Core owns computation, React binds UI. This is the smart boundary pattern.
┌─────────────────────────────────────────┐
│ REACT LAYER (lean) │
│ • UI binding only │
│ • Hooks call Core promise APIs │
│ • NEVER imports Effect │
└─────────────────────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ CORE (smart boundary) │
│ • Computed APIs (pre-joined data) │
│ • Effect services (internal) │
│ • Domain logic, status computation │
│ • Promise APIs (external surface) │
└─────────────────────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ SDK / BACKEND │
└─────────────────────────────────────────┘
What Core provides:
sessions.getStatus(id)- Computed session statussessions.listWithStatus()- Pre-joined datamessages.listWithParts(sessionId)- Messages with parts embeddedformat.relativeTime(),format.tokens()- Formatting utils
What React provides:
- UI binding via hooks (
useWorld(),useSession(id)) - Never imports Effect types
- Zustand only for UI-local state (selected session, flags)
Preserved workflow. No changes to SDK generation.
OpenAPI Spec (openapi.json) → @hey-api/openapi-ts → Generated Types/Client
→ SDK Wrapper (namespaced classes) → createOpencodeClient() → Consumer
Source of truth: packages/sdk/openapi.json (OpenAPI 3.1.1)
High-level: Backend → SDK → Core → React/CLI
┌─────────────────────────────────────────────────────────────┐
│ BACKEND (packages/opencode) │
│ • Hono HTTP server │
│ • Filesystem state (~/.local/state/opencode/) │
│ • SSE event stream (/api/events) │
│ • Instance metadata registry │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ SDK (packages/sdk) │
│ • OpenAPI-generated client │
│ • Type-safe API surface │
│ • Discovery service (filesystem scan) │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ CORE (packages/core) │
│ • World Stream (reactive state via effect-atom) │
│ • SSE connection management │
│ • Computed APIs (pre-joined data) │
│ • Promise-based external surface │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────┴─────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ REACT │ │ CLI │
│ (packages/react)│ │ (apps/swarm-cli)│
│ • Hooks │ │ • Commands │
│ • UI binding │ │ • TUI │
└──────────────────┘ └──────────────────┘
Key Patterns:
- Discovery - Filesystem-based server discovery via
~/.local/state/opencode/instances/ - World Stream - Push-based reactive state (SSE → effect-atom → subscribers)
- Smart Boundary - Core owns computation, React/CLI bind UI
- Single vs Multi - Web app discovers multiple servers, CLI connects to one
Data Types:
InstanceMetadata- Server discovery (directory, pid, port, startTime)WorldState- Reactive snapshot (sessions, messages, parts)Session,Message,Part- SDK types (never hand-roll)
Key Files:
packages/core/src/discovery/- Server discoverypackages/core/src/world/- Reactive world streampackages/core/src/client/- SDK client factorypackages/react/src/hooks/- React bindingsapps/swarm-cli/src/world-state.ts- CLI wrapper
Problem: Every store update creates new array/object references, even if content is identical.
Impact: React.memo with shallow comparison always triggers re-renders because references change.
// Even if metadata.summary hasn't changed, this creates new references
set((state) => {
const partIndex = state.parts.findIndex((p) => p.id === id);
state.parts[partIndex].state.metadata.summary = newSummary; // New part object
});Fix: Content-aware React.memo comparison
export const Task = React.memo(TaskComponent, (prev, next) => {
return (
prev.part.id === next.part.id &&
prev.part.state?.metadata?.summary === next.part.state?.metadata?.summary
);
});ALWAYS use official @opencode-ai/sdk types. Never hand-roll domain types.
// ✅ CORRECT - Import from sdk.ts re-export layer
import type { Session, Message, Part } from "@opencode-vibe/core/types/sdk"
import type { Event, EventMessagePartUpdated } from "@opencode-vibe/core/types/sdk"
// ❌ WRONG - Hand-rolled types cause drift
interface Session { id: string; ... } // DON'T DO THISKey files:
packages/core/src/types/sdk.ts- Central re-export of SDK typespackages/core/src/types/domain.ts- Re-exports SDK types for backwards compatpackages/core/src/types/events.ts- Re-exports SDK event types
When SDK updates:
- Update
@opencode-ai/sdkversion inpackages/core/package.json - Run
bun install - Run
bun run typecheck- fix any breaking changes - Update
sdk.tsif new types need re-exporting
SDK Event Shapes:
EventMessagePartUpdatedhas{ type, properties: { part: Part } }- part is NESTED- Other events have flat
{ type, properties: { sessionID, ... } }
- No timeout on requests - AI operations can run for minutes.
req.timeout = falsein client factory. - Directory scoping -
x-opencode-directoryheader routes requests to specific project instance. - Dual SDK instances - One for SSE (no timeout), one for requests (10min timeout).
- No database - All data in filesystem (
~/.local/state/opencode/). No migrations, no transactions. - Event bus is global -
GlobalBus.emit()broadcasts to ALL clients. No per-client filtering. - Instance caching -
Instance.provide()caches per directory. Dispose required to clear cache. - SSE heartbeat required - 30s heartbeat prevents WKWebView 60s timeout on mobile Safari.
- Binary search everywhere - Updates use binary search on sorted arrays. Assumes IDs are sortable (they are - ULIDs).
- Session limit - UI loads 5 sessions by default + any updated in last 4 hours. Older sessions lazy-loaded.
- ADR 001: Next.js Rebuild - Full architecture rationale
- ADR 016: Core Layer Responsibility - Smart boundary pattern
- ADR 018: Reactive World Stream - Push-based state with effect-atom
- Bun API Docs - Local Bun reference
- Next.js Docs - Next.js 16 App Router
- ai-elements - Chat UI components
packages/opencode- Backend (Hono server, AsyncLocalStorage DI)packages/sdk- OpenAPI-generated SDK with 15 namespacespackages/app- Current SolidJS app (being replaced)
| File | Purpose |
|---|---|
docs/adr/001-nextjs-rebuild.md |
Architecture rationale, migration plan |
CLAUDE.md |
AI agent conventions, Bun usage |
.hive/issues.jsonl |
Work tracking (git-backed) |
package.json |
Bun dependencies |
tsconfig.json |
TypeScript configuration |
- Architecture questions: See
docs/adr/001-nextjs-rebuild.md - Bun usage: See
CLAUDE.md - Work tracking: Check
.hive/issues.jsonlor runbd list - SDK reference:
packages/sdk/openapi.json(OpenAPI 3.1.1)