# Ori — project summary ## What it is Ori is a web client for ACP (Agent Client Protocol, https://agentclientprotocol.com) code agents: a Go backend serves an embedded React SPA and connects over ACP (JSON-RPC on stdio) to an agent subprocess — Claude Code via the `@agentclientprotocol/claude-agent-acp` adapter by default (since 2026-09-18). The browser gets a Zed-style agent panel: streamed markdown answers, collapsible thoughts, tool call cards (with statuses, locations and diffs), the agent's plan, and permission prompts answered from the UI. Implements ticket `.tickets/issues/0004-specifications.yaml`. ## Architecture ``` browser ⇆ WebSocket /ws ⇆ Go backend ⇆ stdio (ACP) ⇆ agent subprocess ``` - Go module: `rickub.com/bots-garden/ori` (NOT github — the repo is not hosted on GitHub; the user corrected this explicitly). - `cmd/ori` — server binary; flags `--addr` (default `:8888`), `--cwd`, `--agent-cmd` (whitespace-split). - `cmd/ori-mock-agent` — deterministic demo ACP agent on stdio (thought → plan → read tool call → permission → edit tool call with diff → answer); used by `make run-mock` and the e2e test. - `internal/config` — flag parsing/validation. - `internal/httpserver` — embedded SPA (via `ui/embed.go`, `go:embed all:dist`), `/healthz`, SPA fallback, extra-route injection point for `/ws`. - `internal/agent` — spawns the agent, drives the ACP lifecycle through `github.com/coder/acp-go-sdk` v0.13.5 (community SDK listed by the official ACP docs; the zed-industries repo itself has NO Go library); generic `Handler` interface (session updates + blocking permission requests); real fs read/write; terminals refused (capability not announced). - `internal/files` — workspace file API (`GET /api/files`, `GET /api/file`, `PUT /api/file`, plus `GET /api/files/search?q=&limit=` — recursive, case-insensitive, skips `.git`/`node_modules`, default 50 / max 500 results, walk capped at 50 000 entries — and `GET /api/raw?path=` streaming bytes with pinned image Content-Types, Range support, no size cap): list/read/write, relative paths against `--cwd`, absolute allowed; access deliberately unrestricted (user's choice: ori targets already-isolated sandboxes); binary → 415, >5MiB → 413. - `internal/terminal` — one shell per WebSocket on `GET /ws/terminal` in a PTY (`creack/pty` v1.1.24); binary frames out, JSON `input`/`resize` frames in; shell from `$SHELL`→bash→sh. - `internal/skills` — `GET /api/skills`: discovers `/.claude/skills/*/SKILL.md` and `~/.claude/skills/*/SKILL.md` (frontmatter `name`/`description`, directory-name fallback, project shadows user, sorted). - `internal/bridge` — hub between agent and browsers: JSON protocol (see `docs/*/reference/websocket-protocol.md`), broadcast with per-client buffers (slow client = dropped), append-only history (cap 4096) replayed on every connection, `user_message` echo so the server is the single source of truth, permission routing by requestId (pending requests replayed to late joiners; agent-side ctx cancellation handled). The `prompt` message carries optional `attachments: [{path,name}]`, turned server-side into ACP `resource_link` blocks (`file://`) next to the text block — `Prompter.Prompt(ctx, []acp.ContentBlock)` replaced `PromptText`; relative paths are joined to the root given by `Bridge.SetWorkspaceRoot` (wired in `cmd/ori/main.go`); `user_message` echoes the attachments. Uses `github.com/coder/websocket` v1.8.15; origins restricted to localhost. - `ui/` — Vite + React 19 + TS strict; zustand store around a pure reducer (`ui/src/reducer.ts`, handler-table style); WS client with backoff reconnect; thread resets on `hello` so replays are idempotent; unknown ACP update kinds ignored by design. Workspace panel (separate zustand store `ui/src/workspace.ts`): file tree (click=preview, double-click=edit; column resizable via `ResizeHandle` — ARIA separator, pointer drag / arrows / Home-End / double-click reset — width `fileTreeWidth` in the store, bounded 120–800 px, default 224, applied through the `--filetree-width` CSS custom property, persisted in localStorage `ori.fileTreeWidth`). Colour theme (`ui/src/theme.ts`, own zustand store): `light` (default) | `dark`, stamped as `data-theme` on `` at module load and on change, remembered in localStorage `ori.theme`, toggled by the header ☾/☀ button; `app.css` keys the dark palette on `:root[data-theme="dark"]` (the former `prefers-color-scheme` media query is gone — decision: explicit choice, light default), `MonacoViewer` reads the store (`vs-dark`/`light`), the terminal stays dark. Preview tab (Monaco read-only for code — bundled workers, no CDN, lazy chunk ~1MB gzip; rendered Markdown; rendered AsciiDoc via lazily-imported `@asciidoctor/core`, Rendered/Source toggle), Editor tab (Monaco, dirty ●, Save/Ctrl+S via PUT /api/file), Terminal tab (xterm.js on /ws/terminal, starts on first activation); inactive panes are hidden not unmounted so shell/edit state survives tab switches; `execute` tool-call output renders in a monospace `
`, never markdown. Composer completions (`ui/src/mentions.ts` pure helpers, `ui/src/useCompletion.ts`, `CompletionPopup`): `@` opens a workspace file search (→ attachments), `/` opens a merged list of local skills (`/api/skills`) and ACP `available_commands_update` commands (stored as `commands` in the reducer, cleared on `hello`; deduped by name, skill wins) and inserts `/ ` as plain text — the text is the invocation mechanism, there is no ACP "run command" method. Preview kinds `image` (`ImageView`, `` over checkerboard via `/api/raw`, natural size shown) and `drawio` (`DrawioView`: iframe on `embed.diagrams.net` fed by postMessage, Rendered/Source toggle, 8 s timeout → Source fallback; `.drawio.svg/.png` are images). The mock agent emits `available_commands_update` (`review`, `compact`) on session start and echoes `[attached: name]`.
- `ori-desktop/` — separate Go module `rickub.com/bots-garden/ori-desktop` (Wails v2.16.0, Go ≥ 1.25, "plain" template, `//go:embed all:frontend/src`, no npm; `frontend/src/main.js` is loaded as an ES module — `type="module"` — since the 2026-09-17 lint cleanup): connection screen (URL remembered in `/ori-desktop/settings.json`, key `serverUrl`; auto-connect at startup), `/healthz` probe via bound Go methods (GetConfig, SaveConfig, CheckHealth, Connect, OpenInBrowser, SettingsPath), then the ori SPA loaded in an `