1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
= 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 Claude Agent SDK ACP adapter (`npx -y @agentclientprotocol/claude-agent-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 → `<code>` 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 `<pre>`, 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 Agent Client Protocol adapter (`@agentclientprotocol/claude-agent-acp`,
which bundles its own Claude Code CLI via the Claude Agent SDK; default since 2026-09-18,
replacing Zed's `@zed-industries/claude-code-acp` whose embedded CLI 2.1.44 the API now
rejects) — 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 <project> --template k33g/ori:0.0.2 --kit <ori-repo>/kits/ori --name ori -p 8888:8888/tcp`
* 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).
|