A mobile-friendly web UI for coding agents, written in Go. Ships as a single static binary (~24 MB) with zero runtime dependencies.
Supports four ACP agents and Pi native RPC. Built for personal use over Tailscale.
This is the Go port — single binary, embedded frontend, pure-Go SQLite.
The original Python implementation is on thepythonbranch.
- Streaming chat — real-time SSE with Markdown, KaTeX, and Mermaid rendering
- Workspace explorer — file tree with previews, drag-and-drop upload, folder downloads (ZIP)
- Code editor — tabbed CodeMirror 6 editor (13 languages, Vim mode, search/replace, pop-out windows)
- Agent controls — approve/deny tool calls, queue follow-ups, mid-turn steering, slash commands
- Rich media — paste images, attach files, OpenGraph link previews
- SQLite storage — messages, media, and full-text search in a single file
- PWA — installable, dark/light themes, responsive from phone to desktop
- Single binary — no Python, no Node.js, no virtualenv, no runtime dependencies
- Extension system — backend routes, SSE events, and frontend UI panels
- Multi-agent — switch between Copilot, Codex, Claude, OpenCode, and Pi at runtime
| Agent | Binary | Version | Status |
|---|---|---|---|
| GitHub Copilot | copilot-language-server --acp --stdio |
v1.472.0 | ✅ |
| OpenAI Codex | codex-acp |
v0.11.1 | ✅ |
| Claude | claude-agent-acp |
v0.29.2 | ✅ |
| Pi | pi-acp |
v0.0.26 | ✅ |
| OpenCode | opencode acp |
v1.14.31 | ✅ |
| Feature | Copilot | Codex | Claude | Pi | OpenCode |
|---|---|---|---|---|---|
| Image support | ✅ | ✅ | ✅ | ✅ | ✅ |
| Embedded context | ✅ | ✅ | ✅ | — | ✅ |
| MCP support | — | ✅ (HTTP) | ✅ (HTTP + SSE) | — | ✅ (HTTP + SSE) |
| Session list/close | ✅ | ✅ | ✅ | ✅ | ✅ |
| Session fork/resume | — | — | ✅ | — | ✅ |
| Prompt queueing | — | — | ✅ | — | — |
| Free models | — | — | — | — | ✅ |
| Auth | GitHub OAuth | ChatGPT / API key | Pre-configured | Terminal login | opencode auth |
All five implement ACP protocol v1 over stdio JSON-RPC. OpenCode provides free models (gpt-5-nano, hy3-preview-free, etc.) making it ideal for CI testing. Pi also supports a richer native RPC mode with streaming drafts, thinking traces, tool events, and live model control.
# Build
git clone https://github.com/rcarmo/vibes.git && cd vibes
make build
# Run with GitHub Copilot (default)
./vibes
# Run with any ACP agent
VIBES_ACP_AGENT="codex-acp" ./vibes
VIBES_ACP_AGENT="claude-agent-acp" ./vibes
VIBES_ACP_AGENT="pi-acp" ./vibes
VIBES_ACP_AGENT="opencode acp" ./vibes # free models available
# Run with Pi native RPC (richer features)
VIBES_DEFAULT_AGENT=pi ./vibes
# Open in browser
open http://localhost:8080make build # bun install → bun run build.js → go buildThis produces a fully self-contained binary with all HTML, CSS, JS, fonts, and icons embedded at compile time via //go:embed.
make build-linux-arm64 # Raspberry Pi, ARM Proxmox
make build-darwin-arm64 # macOS Apple Silicon
make build-all # all platformsdocker build -t vibes .
docker run -p 8080:8080 vibesThe Dockerfile produces a scratch-based image (~24 MB) — just the binary + CA certificates.
Install the ACP agents you want to use:
npm install -g @github/copilot-language-server # GitHub Copilot
npm install -g @openai/codex # OpenAI Codex (includes codex-acp)
npm install -g @agentclientprotocol/claude-agent-acp # Claude
npm install -g pi-acp # Pi (ACP adapter)
npm install -g opencode-ai # OpenCode (free models)
npm install -g @mariozechner/pi-coding-agent # Pi (native, for RPC mode)All configuration is via environment variables with VIBES_ prefix:
| Variable | Default | Description |
|---|---|---|
VIBES_HOST |
0.0.0.0 |
Bind address |
VIBES_PORT |
8080 |
Port |
VIBES_DB_PATH |
database/vibes.db |
SQLite database |
VIBES_DEBUG |
false |
Verbose logging |
VIBES_ACP_AGENT |
copilot-language-server --acp --stdio |
ACP agent command |
VIBES_DEFAULT_AGENT |
acp |
Default agent (acp or pi) |
VIBES_PI_ENABLED |
false |
Enable Pi native RPC |
VIBES_PI_AGENT |
pi |
Pi binary path |
VIBES_PERMISSION_TIMEOUT |
30 |
Permission request timeout (seconds) |
VIBES_PERMISSION_AUTO_APPROVE |
false |
Auto-approve all tool calls |
VIBES_DISCONNECT_TIMEOUT |
300 |
Agent keepalive after last SSE client disconnects |
VIBES_ACP_DEBUG |
false |
Wire-level ACP logging |
VIBES_EXTENSIONS_DIR |
extensions |
Extension scan directory |
See docs/CONFIGURATION.md for full reference.
┌─────────────────────────────────────────────────────────────────┐
│ Browser (SPA) │
│ Preact + HTM │ CodeMirror 6 │ KaTeX │ Mermaid │ SSE client │
└────────────────────────────────┬────────────────────────────────┘
│ HTTP + SSE
┌────────────────────────────────┴────────────────────────────────┐
│ Go HTTP Server (chi) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Timeline │ │ Media │ │Workspace │ │ Agents │ │
│ │ Routes │ │ Routes │ │ Routes │ │ Routes │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────┴─────────────┴────────────┴─────────────┴──────────────┐ │
│ │ SQLite (WAL, FTS5, JSON columns) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────┐ ┌────────────────────────────────────┐ │
│ │ SSE Broker │◄──────►│ Agent Registry │ │
│ │ (per-client │ │ ┌─────────┐ ┌─────────┐ │ │
│ │ buffered │ │ │ ACP │ │ Pi │ │ │
│ │ fanout) │ │ │Provider │ │Provider │ │ │
│ └───────────────┘ │ └────┬────┘ └────┬────┘ │ │
│ │ │ │ │ │
│ ┌───────────────┐ │ ┌────┴────────────┴────┐ │ │
│ │ Extensions │ │ │ Subprocess Manager │ │ │
│ │ (registry) │ │ └────┬─────────────────┘ │ │
│ └───────────────┘ └───────┼────────────────────────────┘ │
└───────────────────────────────────┼──────────────────────────────┘
│ stdio (JSON-RPC / NDJSON)
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌────────────┐ ┌────────────┐
│copilot-lang- │ │ codex-acp │ │claude-agent│
│server --acp │ │ │ │ -acp │
└──────────────┘ └────────────┘ └────────────┘
│
┌───────┴────────┐
│ pi --mode │
│ rpc │
└────────────────┘
1. User types message in compose box
2. Frontend POSTs to /agent/default/message
3. Server stores user message in SQLite
4. Server broadcasts "new_post" via SSE
5. Server spawns prompt on the active AgentProvider (async)
6. Agent streams events on stdout:
- ACP: session/update notifications (draft/thought/tool_call)
- Pi: NDJSON events (text_delta/thinking_delta/tool_execution)
7. Provider routes events → SSE broker → all connected clients
8. Frontend renders streaming draft/thought/status panels
9. Agent completes turn → final response stored in SQLite
10. Server broadcasts "agent_response" via SSE
11. Frontend renders final message in timeline
Server → Client (text/event-stream):
connected Connection established
new_post User posted a message
agent_draft Streaming text from agent
agent_thought Agent thinking/reasoning stream
agent_status Tool call / status update
agent_response Final agent response (turn complete)
agent_request Permission request (approve/deny)
agent_request_timeout Permission timed out
agent_error Agent error
interaction_updated Post metadata changed
interaction_deleted Post removed
| Concern | Python (original) | Go (this port) |
|---|---|---|
| Deployment | virtualenv + pip + Python runtime | Single static binary |
| Memory | ~80 MB baseline (Python + aiohttp) | ~15 MB baseline |
| Boot time | 2–3s (Python import chain) | <100 ms |
| Cross-compile | Not practical | GOOS=linux GOARCH=arm64 go build |
| Concurrency | asyncio (single thread) | Goroutines (multi-core) |
| SQLite binding | C extension (requires build tools) | Pure Go (modernc.org/sqlite) |
| Asset delivery | Separate static directory | Embedded via //go:embed |
| Container | ~200 MB Python image | ~24 MB scratch image |
- stdlib
net/httpcompatible — works with any middleware - Minimal API surface, no framework lock-in
- Context-based routing with URL parameters
- No reflection, no code generation
- Pure Go — no CGo, no C compiler needed at build time
- Enables
CGO_ENABLED=0for truly static binaries - Enables cross-compilation without C toolchain
- Same SQLite feature set (WAL, FTS5, JSON functions, virtual columns)
- ~10% slower than CGo version — acceptable for a single-user app
- Single binary deployment — copy one file, run it
- No "missing static files" errors in production
- Atomic updates (binary = code + assets together)
- Same pattern as rcarmo/webterm
- Trade-off: rebuild binary to update frontend (use
make build)
- Full typed schema for all ACP types (content blocks, tool calls, sessions)
ClientSideConnectionwith notification/request handlerstransport.Spawnfor subprocess lifecycle- Contrib helpers: ToolCallTracker, PermissionBroker, SessionAccumulator
- The only Go ACP SDK with schema parity as of April 2026
Inspired by piclaw's extension architecture:
type Extension interface {
ID() string
Name() string
Version() string
Init(ctx context.Context) error
Shutdown(ctx context.Context) error
Routes() []Route // HTTP routes to mount
EventTypes() []string // SSE event types emitted
StaticDir() string // Static assets (/ext/{id}/)
Manifest() *Manifest // Frontend UI panels
}
type Manifest struct {
Panels []PanelDef // sidebar, main, overlay, toolbar
CSS []string // CSS to inject
JS []string // JS to load
}Extensions can:
- Register backend API routes (mounted on chi router)
- Emit custom SSE event types
- Serve static assets under
/ext/{id}/ - Declare frontend UI panels (sidebar, toolbar, overlays, main area)
vibes/
├── cmd/
│ ├── vibes/main.go # Entry point
│ └── acp-test/main.go # ACP agent verification tool
├── internal/
│ ├── app/app.go # Application wiring (chi + DB + agents + SSE)
│ ├── config/config.go # Environment-based configuration
│ ├── db/
│ │ ├── db.go # SQLite open + WAL + connection management
│ │ ├── migrations.go # Schema v1 (interactions, media, whitelist, FTS5)
│ │ ├── queries.go # Typed CRUD + search + whitelist
│ │ ├── db_test.go # 11 unit tests
│ │ └── db_fuzz_test.go # 3 fuzz targets
│ ├── agent/
│ │ ├── provider.go # Provider interface + Registry
│ │ ├── provider_test.go # Registry tests
│ │ ├── acp/client.go # ACP client (wraps acp-sdk-go)
│ │ └── pi/
│ │ ├── client.go # Pi native RPC client
│ │ └── client_test.go # Event routing tests
│ ├── server/sse/
│ │ ├── broker.go # SSE connection management + fanout
│ │ └── broker_test.go # Subscribe/broadcast/HTTP tests
│ ├── routes/
│ │ ├── timeline.go # GET/POST/DELETE timeline, thread, search
│ │ ├── media.go # Upload, serve, thumbnail, info
│ │ ├── workspace.go # Tree, file CRUD, rename, download, upload
│ │ ├── agents.go # List, status, message, whitelist
│ │ ├── commands.go # Slash commands (/model, /shell, etc.)
│ │ ├── opengraph.go # Link preview fetcher
│ │ ├── permissions.go # Permission broker + follow-up queue
│ │ ├── routes_test.go # 12 route tests
│ │ ├── routes_fuzz_test.go # Path traversal + parsing fuzz
│ │ ├── commands_test.go # Command dispatch tests
│ │ └── permissions_test.go # Queue + shell + permission tests
│ └── extensions/
│ ├── registry.go # Extension discovery + lifecycle
│ ├── registry_test.go # Registration + manifest tests
│ └── registry_fuzz_test.go # ID fuzzing
├── static/ # Frontend (embedded at compile time)
│ ├── index.html # SPA entry point
│ ├── manifest.json # PWA manifest
│ ├── css/styles.css # Main stylesheet
│ ├── dist/app.{js,css} # Bundled frontend (Bun)
│ ├── js/ # Source modules (Preact + HTM)
│ │ ├── app.js # Root SPA component
│ │ ├── api.js # API client
│ │ ├── components/ # UI components
│ │ ├── panes/ # Popout editor
│ │ └── vendor/ # Vendored Preact, CodeMirror, KaTeX
│ └── fonts/ # KaTeX math fonts
├── tests/e2e/ui.spec.mjs # Playwright E2E tests (14 tests)
├── config/endpoints.json # Custom action definitions
├── docs/ # Protocol and integration docs
├── embed.go # //go:embed all:static
├── Dockerfile # Multi-stage: builder → scratch
├── Makefile # build, test, fuzz, lint, e2e, cross-compile
├── playwright.config.mjs
├── go.mod / go.sum
├── GO_PORT.md # Architecture deep-dive
└── LICENSE # MIT
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
| GET | /timeline |
Paginated posts (newest first) |
| POST | /post |
Create post |
| DELETE | /post/{id} |
Delete post (optional cascade) |
| GET | /thread/{id} |
Get thread |
| POST | /thread |
Reply to thread |
| GET | /search?q= |
Full-text search |
| POST | /media/upload |
Upload media file |
| GET | /media/{id} |
Serve media |
| GET | /media/{id}/thumbnail |
Serve thumbnail |
| GET | /media/{id}/info |
Media metadata |
| GET | /workspace/tree |
File tree |
| GET | /workspace/file?path= |
Read file content |
| PUT | /workspace/file |
Write file |
| DELETE | /workspace/file?path= |
Delete file |
| POST | /workspace/create |
Create file/directory |
| POST | /workspace/rename |
Rename/move |
| GET | /workspace/raw?path= |
Raw file download |
| GET | /workspace/download?path= |
Download file/dir (ZIP) |
| POST | /workspace/upload |
Upload to workspace |
| GET | /agents |
List agents |
| GET | /agent/status |
Current agent state |
| GET | /agent/context |
Context window usage |
| GET | /agent/commands |
List slash commands |
| POST | /agent/{id}/message |
Send message to agent |
| GET | /agent/whitelist |
List permission patterns |
| POST | /agent/whitelist |
Add pattern |
| DELETE | /agent/whitelist |
Remove pattern |
| POST | /agent/respond |
Respond to permission request |
| GET | /avatar/{kind} |
Agent/user avatar |
| GET | /sse/stream |
SSE event stream |
make help # Show all targets
make build # Full build (frontend + Go binary)
make dev # Run from source with debug logging
make serve # Build and run
make test # Unit tests
make race # Race detector
make coverage # Coverage report
make fuzz # All 9 fuzz targets
make check # lint + test + coverage
make test-acp # Verify ACP agent handshakes
make e2e # Playwright E2E tests against Pi
make build-all # Cross-compile (linux-amd64, linux-arm64, darwin-arm64)
make clean # Remove artifacts| Layer | Tests | Coverage |
|---|---|---|
internal/config |
3 unit + 3 fuzz | 100% |
internal/agent |
6 unit | 100% |
internal/agent/pi |
4 unit | 20% |
internal/extensions |
7 unit + 1 fuzz | 96% |
internal/server/sse |
4 unit | 85% |
internal/db |
11 unit + 3 fuzz | 79% |
internal/routes |
20 unit + 2 fuzz | 41% |
| Total | 55+ tests, 9 fuzz targets | 52% |
Plus 14 Playwright E2E tests for UI validation.
| Shortcut | Action |
|---|---|
Cmd/Ctrl+S |
Save current editor tab |
Cmd/Ctrl+F |
Search/replace in editor |
Escape |
Close active editor tab |
Alt+V |
Toggle Vim mode |
Alt+W |
Toggle whitespace visibility |
↑ / ↓ |
Cycle compose history |
The original Python implementation (aiohttp, 388 unit tests, 34 E2E tests) is on the python branch:
pip install -U git+https://github.com/rcarmo/vibes.git@pythonThe Go and Python versions share the same frontend, API, SSE events, and SQLite schema — they are interchangeable.
MIT
