nandi/oripublic Fork 0
7895c1d1c9bb1048807dc04f7246dc456c47e025
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

source-code-analysis.adoc · 261 lines · 11.1 KBGDScript3 Blame HistoryRaw
✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme 76d62ac k33g yesterday1= Ori Source Code Analysis
2:toc: left
3:toclevels: 3
4:icons: font
5:source-highlighter: highlight.js
6:revdate: 2026-09-17 (updated 2026-09-17)
7
8== Overview
9
10Ori is a web client for https://agentclientprotocol.com[ACP (Agent Client Protocol)] code agents.
11A Go backend serves an embedded React SPA and connects over ACP (JSON-RPC on stdio) to an agent
12subprocess Claude Code via the Zed adapter (`npx -y @zed-industries/claude-code-acp`) by default.
13The browser gets a Zed-style agent panel: streamed markdown answers, collapsible thoughts (live
14during streaming, collapsible when complete), working spinner with rotating phrases, tool call
15cards (status, locations, diffs), the agent's plan, and permission prompts answered from the UI —
16plus a workspace panel with a VS Code-style file tree (Material icons, indent guides, chevrons),
17Monaco preview/editor with rendered Markdown and AsciiDoc, and an xterm.js terminal.
18
19[source]
20----
21browser WebSocket /ws Go backend stdio (ACP) agent subprocess
22----
23
24* Go module: `rickub.com/bots-garden/ori`
25* ~4,063 lines of source: Go 1,555 (21 files), TypeScript/TSX 2,508 (39 files)
26
27== Backend (Go)
28
29=== Entry points
30
31[cols="1,3"]
32|===
33| Package | Responsibility
34
35| `cmd/ori`
36| Server binary. Parses flags (`--addr`, default `:8888`; `--cwd`; `--agent-cmd`,
37whitespace-split), spawns the agent, wires the bridge and HTTP routes.
38
39| `cmd/ori-mock-agent`
40| Deterministic demo ACP agent on stdio (thought plan read tool call permission
41edit tool call with diff answer). Used by `make run-mock` and the e2e test.
42|===
43
44=== Internal packages
45
46[cols="1,3"]
47|===
48| Package | Responsibility
49
50| `internal/config`
51| CLI flag parsing and validation (`Config`, `FromArgs()`).
52
53| `internal/agent`
54| Spawns the agent subprocess and drives the ACP lifecycle via
55`github.com/coder/acp-go-sdk` v0.13.5. Exposes a generic `Handler` interface (session
56updates + blocking permission requests). Implements the `acp.Client` side: real
57filesystem read/write; terminal methods refused (capability not announced).
58
59| `internal/bridge`
60| Hub between the agent and the browsers (~550 LOC). Broadcast with per-client buffers
61(256 items; slow clients are dropped), append-only history capped at 4,096 events and
62replayed on every connection, `user_message` echo so the server is the single source of
63truth, permission routing by `requestId` (pending requests replayed to late joiners).
64Uses `github.com/coder/websocket` v1.8.15; origins restricted to localhost. A
65`Prompter` interface is the seam for future multi-session support.
66
67| `internal/httpserver`
68| Root HTTP mux: `/healthz`, embedded SPA with fallback routing, injection point for
69extra routes (`/ws`, `/ws/terminal`, `/api/*`).
70
71| `internal/files`
72| Workspace file API: list / read / write. Relative paths resolved against `--cwd`,
73absolute paths allowed access is deliberately unrestricted (ori targets
74already-isolated sandboxes). Binary content 415, files > 5 MiB 413.
75
76| `internal/terminal`
77| One shell per WebSocket on `/ws/terminal`, in a PTY (`creack/pty` v1.1.24). Binary
78frames out; JSON `input` / `resize` frames in. Shell chosen from `$SHELL` bash sh.
79
80| `internal/mockagent`
81| The scripted demo agent implementation behind `cmd/ori-mock-agent`.
82
83| `ui/embed.go`
84| `go:embed all:dist` the built SPA embedded into the Go binary.
85|===
86
87== Frontend (`ui/`)
88
89Vite 7 + React 19 + TypeScript strict. State is held in zustand stores wrapped around pure
90reducers, keeping the logic testable without React or WebSocket coupling.
91
92=== Core modules
93
94[cols="1,3"]
95|===
96| File | Role
97
98| `src/App.tsx`
99| Root layout: topbar (connection status, workspace toggle), chat column (thread, plan,
100permission prompts, input), workspace panel.
101
102| `src/reducer.ts`
103| Pure state machine (handler-table style) folding server messages and connection
104events into the chat state.
105
106| `src/store.ts`
107| Zustand wrapper around the reducer (`useChatState()`, `dispatch()`).
108
109| `src/ws.ts`
110| WebSocket client with exponential-backoff reconnect; the thread resets on `hello` so
111history replays are idempotent.
112
113| `src/protocol.ts`
114| Wire contract mirroring `internal/bridge/protocol.go`; ACP payloads are typed but
115relayed verbatim, keeping the front forward-compatible (unknown update kinds ignored).
116
117| `src/workspace.ts`
118| Separate zustand store for the workspace panel (file tree, tabs, open files).
119
120| `src/api.ts`
121| Client for `/api/files` and `/api/file`.
122
123| `src/lang.ts`
124| File extension language ID mapping for Monaco editor.
125
126| `src/monaco-setup.ts`
127| Monaco editor environment configuration (bundled workers, no CDN).
128|===
129
130=== Components
131
132* **Chat panel**: `ChatThread` (with `ThoughtEntry` for live/collapsed thoughts), `Markdown`,
133 `InlineText` (backtick spans `<code>` for tool titles), `ToolCallCard` (status badge,
134 collapsible content, diffs, file locations), `PermissionPrompt`, `PlanView`, `PromptInput`,
135 `WorkingIndicator` (spinner with rotating phrases).
136* **Workspace panel**: `Workspace` (tabs), `FileTree` (VS Code style with Material icons from
137 `vscode-material-icons` 910 SVGs copied to `public/material-icons/` by
138 `scripts/copy-icons.mjs`; click = preview, double-click = edit, chevrons, indent guides,
139 active highlight), `PreviewPane` (Monaco read-only for code, rendered Markdown via
140 `react-markdown` + `remark-gfm`, rendered AsciiDoc via lazily-imported `@asciidoctor/core`
141 v4 async API, Rendered/Source toggle), `EditorPane` (Monaco, dirty indicator, Save /
142 Ctrl+S), `TerminalPane` (xterm.js on `/ws/terminal` with `@xterm/addon-fit`, dark theme,
143 started on first activation), `MonacoViewer` (shared by preview and editor), `CodeView`
144 (syntax-highlighted code block for diffs).
145* Inactive panes are hidden, not unmounted, so shell and editor state survive tab switches.
146* `execute` tool-call output renders in a monospace `<pre>`, never as markdown.
147* Monaco workers are bundled (no CDN); the editor loads as a lazy chunk (~1 MB gzip).
148
149== HTTP / WebSocket endpoints
150
151[cols="1,1,3"]
152|===
153| Endpoint | Method | Purpose
154
155| `/healthz` | GET | Health check (`{"status":"ok"}`).
156| `/ws` | GET (upgrade) | Agent browser relay: history replay then live events.
157| `/ws/terminal` | GET (upgrade) | Interactive PTY shell.
158| `/api/files` | GET | List a directory (`?path=`).
159| `/api/file` | GET | Read a file (max 5 MiB).
160| `/api/file` | PUT | Write a file.
161| `/` | GET | Embedded SPA (index.html fallback).
162|===
163
164== WebSocket protocol (bridge)
165
166Browser server: `prompt`, `cancel`, `permission_response` (by `requestId`).
167
168Server browser: `hello` (session id, turn state resets the client thread),
169`user_message` (echo), `session_update` (raw ACP `SessionUpdate`), `permission_request`
170(raw ACP `RequestPermissionRequest` + `requestId`), `permission_resolved`,
171`turn_started`, `turn_ended` (stop reason), `error`.
172
173Reference: `internal/bridge/protocol.go`, `ui/src/protocol.ts`, and
174`docs/en/reference/websocket-protocol.md`.
175
176== Tests
177
178* *Go*: 55 tests across 8 packages plus one end-to-end test (`cmd/ori/e2e_test.go`,
179 full server + mock agent subprocess + real WebSocket; skipped with `-short`).
180 Race-clean (`go test -race ./...`).
181* *Frontend*: 75 vitest tests across 13 test files reducer (48), components
182 (WorkingIndicator, InlineText, AsciiDocView with real converter, ToolCallCard, FileTree,
183 EditorPane, PreviewPane, TerminalPane, App), WebSocket client, workspace store, language
184 detection.
185* Quality gate (qlty): PASS as of 2026-09-17 0 errors, 0 warnings, 0 smells (2 formatting
186 notes).
187
188== Build and run
189
190[cols="1,3"]
191|===
192| Target | Effect
193
194| `make deps` | `npm install` in `ui/`.
195| `make build` | SPA build then Go binaries order matters: the Go build embeds `ui/dist`.
196| `make test` | `go test ./...` + `vitest run`.
197| `make run` | Run with the Claude Code ACP adapter.
198| `make run-mock` | Run with the bundled demo agent (no Claude, no network needed).
199| `make dev` | Vite dev server on :5173.
200|===
201
202== Key design decisions
203
204* ACP payloads cross the WebSocket *verbatim*, keeping the frontend forward-compatible.
205* The server owns the thread (history + `user_message` echo); the SPA rebuilds from
206 replay on every (re)connection.
207* One agent session shared by all browsers in v1; a second `prompt` during an active
208 turn is rejected. Multi-session is a planned evolution behind the `bridge.Prompter`
209 seam.
210* File API access is deliberately unrestricted: ori targets already-isolated sandboxes.
211* Claude Code is reached through the Zed adapter (`@zed-industries/claude-code-acp`,
212 note: renamed upstream to `@agentclientprotocol/claude-agent-acp` but default not yet
213 updated) the `claude` CLI has no native ACP mode.
214* Material icons are copied from `vscode-material-icons` package to `public/` via an npm
215 pre-hook script, making them static assets bundled with the SPA.
216* AsciiDoc rendering uses `@asciidoctor/core` v4's async `convert` named export (v3's
217 factory function no longer exists).
218* Terminal always renders with a dark theme (`#1e1e1e`) regardless of app theme.
219
220== Known limitations
221
222* `--agent-cmd` is split on whitespace; arguments containing spaces need a wrapper script.
223* One prompt turn at a time per shared session.
224* ACP terminal methods are not supported (capability not announced; answered with JSON-RPC
225 method-not-found).
226* Only text content blocks are rendered in messages; images/audio/resources are not yet
227 supported.
228* Running ori inside a Claude Code sandbox requires `env -u CLAUDECODE ./bin/ori` to bypass
229 the nested-session guard.
230
231== Sandbox deployment
232
233Ori ships as a Docker Sandbox template (`template/Dockerfile`, FROM
234`docker/sandbox-templates:claude-code`) paired with a kit (`kits/ori/`):
235
236* Create: `sbx run -d claude <project> --template k33g/ori:0.0.0 --kit <ori-repo>/kits/ori --name ori -p 8888:8888`
237* The `--detached` (`-d`) flag is **required**: sandboxd auto-stops a sandbox 30 seconds
238 after its last CLI sentinel session closes; without `-d`, the container stops shortly after
239 the prompt returns. Browser traffic on a published port does not hold the sandbox up.
240* The `detached` field is create-time only (no PATCH); an existing non-detached sandbox must
241 be `sbx rm`'d and recreated with `sbx run -d`.
242* Build the template image: `make template` (builds binaries first, copies to the image).
243
244=== Environment considerations (sandbox development)
245
246When developing ori inside a Claude Code sandbox:
247
248* `~/.qlty` and `~/.npm` are 488MB dedicated volumes. The qlty tool cache was moved to
249 `~/.qlty-cache` (symlinked from `~/.qlty/cache`), and npm cache to `~/.npm-big` (via
250 `NPM_CONFIG_CACHE` in `/etc/sandbox-persistent.sh`) to avoid "No space left" errors.
251* npm blocks install scripts by default; esbuild's postinstall was approved via
252 `npm approve-scripts` (recorded in `ui/package.json` `allowScripts` field).
253* The `radarlint-python` qlty plugin was removed (its ~300MB JDK cannot fit the dedicated
254 volume; diagnosis in `.qlty/qlty.toml` comments).
255
256== Documentation
257
258`docs/` follows the Diátaxis method, in English (`docs/en/`) and French (`docs/fr/`):
259tutorials (getting started), how-to guides (run the tests, run with Claude Code, run in a
260sandbox, use the workspace, use another agent), reference (CLI, WebSocket protocol, workspace
261API) and explanation (architecture).