forked from bots-garden/ori
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 @zed-industries/claude-code-acp adapter by default. 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 bymake run-mockand the e2e test.internal/config— flag parsing/validation.internal/httpserver— embedded SPA (viaui/embed.go,go:embed all:dist),/healthz, SPA fallback, extra-route injection point for/ws.internal/agent— spawns the agent, drives the ACP lifecycle throughgithub.com/coder/acp-go-sdkv0.13.5 (community SDK listed by the official ACP docs; the zed-industries repo itself has NO Go library); genericHandlerinterface (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, plusGET /api/files/search?q=&limit=— recursive, case-insensitive, skips.git/node_modules, default 50 / max 500 results, walk capped at 50 000 entries — andGET /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 onGET /ws/terminalin a PTY (creack/ptyv1.1.24); binary frames out, JSONinput/resizeframes in; shell from$SHELL→bash→sh.internal/skills—GET /api/skills: discovers<cwd>/.claude/skills/*/SKILL.mdand~/.claude/skills/*/SKILL.md(frontmattername/description, directory-name fallback, project shadows user, sorted).internal/bridge— hub between agent and browsers: JSON protocol (seedocs/*/reference/websocket-protocol.md), broadcast with per-client buffers (slow client = dropped), append-only history (cap 4096) replayed on every connection,user_messageecho so the server is the single source of truth, permission routing by requestId (pending requests replayed to late joiners; agent-side ctx cancellation handled). Thepromptmessage carries optionalattachments: [{path,name}], turned server-side into ACPresource_linkblocks (file://<abs>) next to the text block —Prompter.Prompt(ctx, []acp.ContentBlock)replacedPromptText; relative paths are joined to the root given byBridge.SetWorkspaceRoot(wired incmd/ori/main.go);user_messageechoes the attachments. Usesgithub.com/coder/websocketv1.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 onhelloso replays are idempotent; unknown ACP update kinds ignored by design. Workspace panel (separate zustand storeui/src/workspace.ts): file tree (click=preview, double-click=edit; column resizable viaResizeHandle— ARIA separator, pointer drag / arrows / Home-End / double-click reset — widthfileTreeWidthin the store, bounded 120–800 px, default 224, applied through the--filetree-widthCSS custom property, persisted in localStorageori.fileTreeWidth). Colour theme (ui/src/theme.ts, own zustand store):light(default) |dark, stamped asdata-themeon<html>at module load and on change, remembered in localStorageori.theme, toggled by the header ☾/☀ button;app.csskeys the dark palette on:root[data-theme="dark"](the formerprefers-color-schememedia query is gone — decision: explicit choice, light default),MonacoViewerreads 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;executetool-call output renders in a monospace<pre>, never markdown. Composer completions (ui/src/mentions.tspure helpers,ui/src/useCompletion.ts,CompletionPopup):@opens a workspace file search (→ attachments),/opens a merged list of local skills (/api/skills) and ACPavailable_commands_updatecommands (stored ascommandsin the reducer, cleared onhello; deduped by name, skill wins) and inserts/<name>as plain text — the text is the invocation mechanism, there is no ACP "run command" method. Preview kindsimage(ImageView,<img>over checkerboard via/api/raw, natural size shown) anddrawio(DrawioView: iframe onembed.diagrams.netfed by postMessage, Rendered/Source toggle, 8 s timeout → Source fallback;.drawio.svg/.pngare images). The mock agent emitsavailable_commands_update(review,compact) on session start and echoes[attached: name].ori-desktop/— separate Go modulerickub.com/bots-garden/ori-desktop(Wails v2.16.0, Go ≥ 1.25, "plain" template,//go:embed all:frontend/src, no npm;frontend/src/main.jsis loaded as an ES module —type="module"— since the 2026-09-17 lint cleanup): connection screen (URL remembered in<UserConfigDir>/ori-desktop/settings.json, keyserverUrl; auto-connect at startup),/healthzprobe via bound Go methods (GetConfig, SaveConfig, CheckHealth, Connect, OpenInBrowser, SettingsPath), then the ori SPA loaded in an<iframe>(Wails v2 cannot navigate the main webview to an external URL;runtime.BrowserOpenURLis the fallback). Ticket 0005.make desktop/make desktop-test; rootgo test ./...unaffected (nested module).
Key decisions in force
- Claude Code is reached through the official Zed adapter (
npx -y @zed-industries/claude-code-acp) — theclaudeCLI (v2.1.274) has no native ACP mode; verified. - draw.io diagrams render online via
embed.diagrams.net(no bundlable offline renderer exists:viewer.min.jsis not on npm,mxgraphis archived and lacks draw.io's shapes,drawio2svgis GPL); future offline path = vendoringviewer-static.min.js(~3 MB, Apache-2.0). Documented in the architecture explanations. - ACP payloads cross the WebSocket verbatim (raw
SessionUpdate/RequestPermissionRequest), keeping the front forward-compatible. - One agent session shared by all browsers in v1; multi-session is a planned evolution seam (
bridge.Prompterinterface, per-connection subscriptions). - The server owns the thread (history + user_message echo); the SPA rebuilds from replay on every (re)connection.
- Quality config decisions (all user-approved):
.claude/**excluded from qlty (vendored kit sources, the measurement instrument itself);radarlint-pythonplugin removed (its ~300MB JDK cannot fit the dedicated 488MB~/.qltyvolume — diagnosis in.qlty/qlty.tomlcomments);golangci-lint+biomeadded manually (init missed both languages);biome.jsonat root parses.vscode/**as JSONC.
Build / test / run
make deps— npm install inui/.make build— SPA build then Go binaries (bin/ori,bin/ori-mock-agent). Order matters: the Go build embedsui/dist.make test—go test ./...+vitest run. Also useful:go test -race ./...,go test -short ./...(skips e2e).make run— with Claude Code adapter;make run-mock— with the demo agent (no Claude/network needed).make template— builds thek33g/orisandbox template image (template/Dockerfile, FROMdocker/sandbox-templates:claude-code); launch withsbx run -d claude <project> --template k33g/ori:0.0.0 --kit <ori repo>/kits/ori --name ori -p 8888:8888(detached — see the auto-stop trap below) (seekits/ori/README.mdand docs how-torun-in-a-sandbox).- sbx auto-stop trap (verified in the sbx source + daemon log, 2026-09-17): sandboxd stops a sandbox 30 s (hard-coded
WithAutoStopDelay(30*time.Second),sandboxd/pkg/server/backend_dockernext.go) after its last CLI "sentinel" session closes;sbx create/sbx exec/sbx runall hold one for their own duration, browser traffic on a published port counts for nothing. Only a sandbox created with the spec fielddetached: true(CLI:sbx run -d …, absent fromsbx create) is exempt; the field is create-time only (no PATCH), so an existing sandbox must besbx rm'd and recreated withsbx run -d. No settings/env knob for the delay. - Quality:
python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .(gate PASS, run #16, 2026-09-17 late evening: 0 errors, 0 warnings, 0 smells, complex 514 — covers the selectors/previews/desktop work, the resizable file tree and the light/dark theme). docs/source-code-analysis.adoc— AsciiDoc snapshot analysis of the codebase (2026-09-17), outside the bilingual Diátaxis set.
Environment facts (sandbox)
~/.qltyis a dedicated 488MB volume, too small for qlty's tool cache; the cache was moved to~/.qlty-cacheon the main disk with a symlink~/.qlty/cache -> /home/agent/.qlty-cache. If qlty reports "No space left on device", check this symlink survived. (It did NOT survive a sandbox recreation on 2026-09-17: the volume came back empty and the first run failed exactly that way; recreating the symlink fixed it.)~/.npmis the same kind of 488MB dedicated volume and filled up when installing Monaco; the npm cache now lives at~/.npm-bigviaNPM_CONFIG_CACHEexported in/etc/sandbox-persistent.sh.- Running ori inside this Claude-Code-driven sandbox requires
env -u CLAUDECODE ./bin/ori(Claude refuses nested sessions). Also setCLAUDE_CODE_EXECUTABLE=<current claude binary>(e.g./home/agent/.local/share/claude/versions/2.1.275): the Zed adapter 0.16.2 bundles Claude Code 2.1.44, which rejects thefable[1m]model from~/.claude/settings.json. - npm blocks install scripts by default here; esbuild's postinstall was approved via
npm approve-scripts(recorded inui/package.jsonallowScripts).
Known limitations
--agent-cmdis split on whitespace; arguments containing spaces need a wrapper script.- One prompt turn at a time; a second
promptwhile a turn runs is rejected with an error event. - Terminal ACP methods are not supported (declared absent; answered with JSON-RPC method-not-found).
user_message/ thought / message chunks only render text content blocks; images/audio/resources are not rendered yet;user_message.attachmentsare not rendered as chips (only the@pathtext mention).- Skills list is fetched once per page load; the file search walks whatever sits under
--cwd(an untrackedsandboxes/clone at the repo root gets walked).
Not yet established
-
ori-desktop: the window was launched by the user on their Mac (build OK, Check OK); after the CSS specificity fix (
#connect-screenoutranked.screen[hidden], so the connect screen never hid and the viewer rendered below the fold) it has not yet been confirmed that the iframe actually shows ori inside WKWebView (wails://origin loadinghttp://localhost;NSAllowsLocalNetworkingset inbuild/darwin/Info.plist). -
Behaviour against a real Claude Code session (auth flow, real tool calls) has not been exercised in this sandbox — only against the mock agent and unit doubles.
-
Whether the repo has a remote to push to (no git remote configured at the time of writing).
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 |
|