= Ori — Source Code Analysis :toc: left :toclevels: 3 :icons: font :source-highlighter: highlight.js :revdate: 2026-09-17 (updated 2026-09-17) == Overview Ori is a web client for https://agentclientprotocol.com[ACP (Agent Client Protocol)] 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 Zed adapter (`npx -y @zed-industries/claude-code-acp`) by default. The browser gets a Zed-style agent panel: streamed markdown answers, collapsible thoughts (live during streaming, collapsible when complete), working spinner with rotating phrases, tool call cards (status, locations, diffs), the agent's plan, and permission prompts answered from the UI — plus a workspace panel with a VS Code-style file tree (Material icons, indent guides, chevrons), Monaco preview/editor with rendered Markdown and AsciiDoc, and an xterm.js terminal. [source] ---- browser ⇆ WebSocket /ws ⇆ Go backend ⇆ stdio (ACP) ⇆ agent subprocess ---- * Go module: `rickub.com/bots-garden/ori` * ~4,063 lines of source: Go ≈ 1,555 (21 files), TypeScript/TSX ≈ 2,508 (39 files) == Backend (Go) === Entry points [cols="1,3"] |=== | Package | Responsibility | `cmd/ori` | Server binary. Parses flags (`--addr`, default `:8888`; `--cwd`; `--agent-cmd`, whitespace-split), spawns the agent, wires the bridge and HTTP routes. | `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 packages [cols="1,3"] |=== | Package | Responsibility | `internal/config` | CLI flag parsing and validation (`Config`, `FromArgs()`). | `internal/agent` | Spawns the agent subprocess and drives the ACP lifecycle via `github.com/coder/acp-go-sdk` v0.13.5. Exposes a generic `Handler` interface (session updates + blocking permission requests). Implements the `acp.Client` side: real filesystem read/write; terminal methods refused (capability not announced). | `internal/bridge` | Hub between the agent and the browsers (~550 LOC). Broadcast with per-client buffers (256 items; slow clients are dropped), append-only history capped at 4,096 events and 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). Uses `github.com/coder/websocket` v1.8.15; origins restricted to localhost. A `Prompter` interface is the seam for future multi-session support. | `internal/httpserver` | Root HTTP mux: `/healthz`, embedded SPA with fallback routing, injection point for extra routes (`/ws`, `/ws/terminal`, `/api/*`). | `internal/files` | Workspace file API: list / read / write. Relative paths resolved against `--cwd`, absolute paths allowed — access is deliberately unrestricted (ori targets already-isolated sandboxes). Binary content → 415, files > 5 MiB → 413. | `internal/terminal` | One shell per WebSocket on `/ws/terminal`, in a PTY (`creack/pty` v1.1.24). Binary frames out; JSON `input` / `resize` frames in. Shell chosen from `$SHELL` → bash → sh. | `internal/mockagent` | The scripted demo agent implementation behind `cmd/ori-mock-agent`. | `ui/embed.go` | `go:embed all:dist` — the built SPA embedded into the Go binary. |=== == Frontend (`ui/`) Vite 7 + React 19 + TypeScript strict. State is held in zustand stores wrapped around pure reducers, keeping the logic testable without React or WebSocket coupling. === Core modules [cols="1,3"] |=== | File | Role | `src/App.tsx` | Root layout: topbar (connection status, workspace toggle), chat column (thread, plan, permission prompts, input), workspace panel. | `src/reducer.ts` | Pure state machine (handler-table style) folding server messages and connection events into the chat state. | `src/store.ts` | Zustand wrapper around the reducer (`useChatState()`, `dispatch()`). | `src/ws.ts` | WebSocket client with exponential-backoff reconnect; the thread resets on `hello` so history replays are idempotent. | `src/protocol.ts` | Wire contract mirroring `internal/bridge/protocol.go`; ACP payloads are typed but relayed verbatim, keeping the front forward-compatible (unknown update kinds ignored). | `src/workspace.ts` | Separate zustand store for the workspace panel (file tree, tabs, open files). | `src/api.ts` | Client for `/api/files` and `/api/file`. | `src/lang.ts` | File extension → language ID mapping for Monaco editor. | `src/monaco-setup.ts` | Monaco editor environment configuration (bundled workers, no CDN). |=== === Components * **Chat panel**: `ChatThread` (with `ThoughtEntry` for live/collapsed thoughts), `Markdown`, `InlineText` (backtick spans → `` for tool titles), `ToolCallCard` (status badge, collapsible content, diffs, file locations), `PermissionPrompt`, `PlanView`, `PromptInput`, `WorkingIndicator` (spinner with rotating phrases). * **Workspace panel**: `Workspace` (tabs), `FileTree` (VS Code style with Material icons from `vscode-material-icons` — 910 SVGs copied to `public/material-icons/` by `scripts/copy-icons.mjs`; click = preview, double-click = edit, chevrons, indent guides, active highlight), `PreviewPane` (Monaco read-only for code, rendered Markdown via `react-markdown` + `remark-gfm`, rendered AsciiDoc via lazily-imported `@asciidoctor/core` v4 async API, Rendered/Source toggle), `EditorPane` (Monaco, dirty ● indicator, Save / Ctrl+S), `TerminalPane` (xterm.js on `/ws/terminal` with `@xterm/addon-fit`, dark theme, started on first activation), `MonacoViewer` (shared by preview and editor), `CodeView` (syntax-highlighted code block for diffs). * Inactive panes are hidden, not unmounted, so shell and editor state survive tab switches. * `execute` tool-call output renders in a monospace `
`, never as markdown.
* Monaco workers are bundled (no CDN); the editor loads as a lazy chunk (~1 MB gzip).

== HTTP / WebSocket endpoints

[cols="1,1,3"]
|===
| Endpoint | Method | Purpose

| `/healthz` | GET | Health check (`{"status":"ok"}`).
| `/ws` | GET (upgrade) | Agent ⇆ browser relay: history replay then live events.
| `/ws/terminal` | GET (upgrade) | Interactive PTY shell.
| `/api/files` | GET | List a directory (`?path=`).
| `/api/file` | GET | Read a file (max 5 MiB).
| `/api/file` | PUT | Write a file.
| `/` | GET | Embedded SPA (index.html fallback).
|===

== WebSocket protocol (bridge)

Browser → server: `prompt`, `cancel`, `permission_response` (by `requestId`).

Server → browser: `hello` (session id, turn state — resets the client thread),
`user_message` (echo), `session_update` (raw ACP `SessionUpdate`), `permission_request`
(raw ACP `RequestPermissionRequest` + `requestId`), `permission_resolved`,
`turn_started`, `turn_ended` (stop reason), `error`.

Reference: `internal/bridge/protocol.go`, `ui/src/protocol.ts`, and
`docs/en/reference/websocket-protocol.md`.

== Tests

* *Go*: 55 tests across 8 packages plus one end-to-end test (`cmd/ori/e2e_test.go`,
  full server + mock agent subprocess + real WebSocket; skipped with `-short`).
  Race-clean (`go test -race ./...`).
* *Frontend*: 75 vitest tests across 13 test files — reducer (48), components
  (WorkingIndicator, InlineText, AsciiDocView with real converter, ToolCallCard, FileTree,
  EditorPane, PreviewPane, TerminalPane, App…), WebSocket client, workspace store, language
  detection.
* Quality gate (qlty): PASS as of 2026-09-17 — 0 errors, 0 warnings, 0 smells (2 formatting
  notes).

== Build and run

[cols="1,3"]
|===
| Target | Effect

| `make deps` | `npm install` in `ui/`.
| `make build` | SPA build then Go binaries — order matters: the Go build embeds `ui/dist`.
| `make test` | `go test ./...` + `vitest run`.
| `make run` | Run with the Claude Code ACP adapter.
| `make run-mock` | Run with the bundled demo agent (no Claude, no network needed).
| `make dev` | Vite dev server on :5173.
|===

== Key design decisions

* ACP payloads cross the WebSocket *verbatim*, keeping the frontend forward-compatible.
* The server owns the thread (history + `user_message` echo); the SPA rebuilds from
  replay on every (re)connection.
* One agent session shared by all browsers in v1; a second `prompt` during an active
  turn is rejected. Multi-session is a planned evolution behind the `bridge.Prompter`
  seam.
* File API access is deliberately unrestricted: ori targets already-isolated sandboxes.
* Claude Code is reached through the Zed adapter (`@zed-industries/claude-code-acp`,
  note: renamed upstream to `@agentclientprotocol/claude-agent-acp` but default not yet
  updated) — the `claude` CLI has no native ACP mode.
* Material icons are copied from `vscode-material-icons` package to `public/` via an npm
  pre-hook script, making them static assets bundled with the SPA.
* AsciiDoc rendering uses `@asciidoctor/core` v4's async `convert` named export (v3's
  factory function no longer exists).
* Terminal always renders with a dark theme (`#1e1e1e`) regardless of app theme.

== Known limitations

* `--agent-cmd` is split on whitespace; arguments containing spaces need a wrapper script.
* One prompt turn at a time per shared session.
* ACP terminal methods are not supported (capability not announced; answered with JSON-RPC
  method-not-found).
* Only text content blocks are rendered in messages; images/audio/resources are not yet
  supported.
* Running ori inside a Claude Code sandbox requires `env -u CLAUDECODE ./bin/ori` to bypass
  the nested-session guard.

== Sandbox deployment

Ori ships as a Docker Sandbox template (`template/Dockerfile`, FROM
`docker/sandbox-templates:claude-code`) paired with a kit (`kits/ori/`):

* Create: `sbx run -d claude  --template k33g/ori:0.0.0 --kit /kits/ori --name ori -p 8888:8888`
* The `--detached` (`-d`) flag is **required**: sandboxd auto-stops a sandbox 30 seconds
  after its last CLI sentinel session closes; without `-d`, the container stops shortly after
  the prompt returns. Browser traffic on a published port does not hold the sandbox up.
* The `detached` field is create-time only (no PATCH); an existing non-detached sandbox must
  be `sbx rm`'d and recreated with `sbx run -d`.
* Build the template image: `make template` (builds binaries first, copies to the image).

=== Environment considerations (sandbox development)

When developing ori inside a Claude Code sandbox:

* `~/.qlty` and `~/.npm` are 488MB dedicated volumes. The qlty tool cache was moved to
  `~/.qlty-cache` (symlinked from `~/.qlty/cache`), and npm cache to `~/.npm-big` (via
  `NPM_CONFIG_CACHE` in `/etc/sandbox-persistent.sh`) to avoid "No space left" errors.
* npm blocks install scripts by default; esbuild's postinstall was approved via
  `npm approve-scripts` (recorded in `ui/package.json` `allowScripts` field).
* The `radarlint-python` qlty plugin was removed (its ~300MB JDK cannot fit the dedicated
  volume; diagnosis in `.qlty/qlty.toml` comments).

== Documentation

`docs/` follows the Diátaxis method, in English (`docs/en/`) and French (`docs/fr/`):
tutorials (getting started), how-to guides (run the tests, run with Claude Code, run in a
sandbox, use the workspace, use another agent), reference (CLI, WebSocket protocol, workspace
API) and explanation (architecture).