nandi/oripublic Fork 0
main
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 · 262 lines · 11.3 KBGDScript3 Blame HistoryRaw
  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).