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 @agentclientprotocol/claude-agent-acp adapter by default (since 2026-09-18). 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). On Linux,wails buildneeds-tags webkit2_41(Ubuntu 26.04 ships onlywebkit2gtk-4.1; the plain command dies onPackage 'webkit2gtk-4.0' not found).make desktopdoes NOT pass that tag, so it fails in this sandbox — runcd ori-desktop && wails build -tags webkit2_41instead. Build outputs coexist inbuild/bin/:ori-desktop(Linux ELF),ori-desktop.app(macOS, built on the Mac),ori-desktop.exe(Windows cross-build); Wails cannot cross-compile to macOS from Linux. Build instructions for the three platforms:ori-desktop/how-to-build.md(2026-09-18, EN only — no.fr.mdcounterpart yet, and no README links to it).
Key decisions in force
- Claude Code is reached through
@agentclientprotocol/claude-agent-acp(defaultnpx -y @agentclientprotocol/claude-agent-acp, since 2026-09-18;internal/config.DefaultAgentCommand, pinned byTestDefaultAgentCommandIsClaudeAgentACP). It bundles its own Claude Code CLI via@anthropic-ai/claude-agent-sdk0.3.274 → CLI 2.1.274 delivered as per-platform optional deps (…-sdk-linux-arm64,-linux-x64, …; nocli.jsany more). Replaced Zed's@zed-industries/claude-code-acp0.16.2, whose embedded CLI 2.1.44 the API rejects for current models ("version 2.1.251 or newer is required"). TheclaudeCLI itself 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(the uitestscript setsNODE_OPTIONS=--no-experimental-webstorage: Node 25's built-inlocalStoragelacksclear()and shadows jsdom's — 15 store tests fail without it; verified 2026-09-18 on Node 25.9). 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.2 --kit docker.io/k33g/ori-kit:latest --name ori -p 8888:8888/tcp(detached — see the auto-stop trap below) (seekits/ori/README.mdand docs how-torun-in-a-sandbox).scripts/launch-ori.applescript(2026-09-18) — macOS launcher:sbx ls -q→ restart by name or create (k33g/ori:0.0.2,./kits/ori,-p 5555:8888/tcp), pollhttp://localhost:5555/healthzup to 90 s, thenopen ori-desktop/build/bin/ori-desktop.app. Run withosascript, orosacompileintoscripts/Launch Ori.app; repo root derived from the script's own location. Compile-checked and handlers unit-tested; full end-to-end run not yet done.- Kit publishing (sbx v0.43.0, verified 2026-09-18):
sbx kit validate kits/ori,sbx kit pack kits/ori -o ori-kit.zip,sbx kit push kits/ori docker.io/k33g/ori-kit:<tag>— directory argument, notspec.yaml; there is nosbx kit package. Auth:sbx loginsession, thensbx secret set --registry, then the Docker credential store. - Kit packaging trap (verified 2026-09-18):
pack/pushdecodespec.yamlraw (no${{ kit.args }}expansion) — an arg placeholder in an integer field (ports[].container) breaks them even thoughvalidate/runaccept it.ports[0].containeris therefore a literal 8888; theportarg only drives the startup command and agentInstructions. - Published artefacts (2026-09-18): template
k33g/ori:0.0.2on Docker Hub (multi-arch amd64+arm64, pushed by the user with./template/build.sh, ships onlyclaude-agent-acp); kitdocker.io/k33g/ori-kit:latestre-pushed with--agent-cmd claude-agent-acp+ template 0.0.2. They must move together: the firsthello-worldsandbox paired image 0.0.2 with the morning's kit (claude-code-acp) and ori died with "executable file not found". - Port publishing traps:
-p H:8888without protocol bindstcp4only — browsers resolvinglocalhostto::1fail; always-p H:8888/tcp(dual-stack). Chrome refuses ports 6665–6669 (ERR_UNSAFE_PORT) whatever the server does.sbx ports NAMElists;sbx ports lslooks for a sandbox calledls. - 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 .(the script lives in the repo, not~/.claude). Gate PASS, run #18, 2026-09-18: 0 errors, 0 warnings, 0 smells, complex 532 (covers the adapter switch, the MIME pin, the copy-paste feature). Mac trap:.qlty/{logs,out,plugin_cachedir,results}are symlinks qlty creates into its cache; ones created inside the sandbox point at/home/agent/…and dangle on the Mac, making every plugin invocation "FATAL" with no detail — delete the dangling links, qlty recreates them..qlty/qlty.tomlpins golangci-lint 2.13.2 (built with Go 1.27): qlty's default 1.61.0 and its latest-known 2.6.2 refuse this repo'sgo 1.26.5. 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). With the current template/kit (claude-agent-acp, bundled CLI 2.1.274) noCLAUDE_CODE_EXECUTABLEis needed. It remains the lever if something spawns the old Zed adapter (points it at anotherclaudebinary) — but the template's own CLI is 2.1.246 (base image 2026-08-26), below the 2.1.251 floor the API enforces forclaude-fable-5-1[1m](host~/.claude/settings.json), soclaude updateinside the sandbox would come first. Handoffs2026-09-18-claude-version-too-old.md,2026-09-18-claude-agent-acp-adapter.md. - npm blocks install scripts by default here; esbuild's postinstall was approved via
npm approve-scripts(recorded inui/package.jsonallowScripts). - The Wails GUI toolchain is installable here (verified 2026-09-18):
go install github.com/wailsapp/wails/v2/cmd/wails@v2.16.0thensudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev(~15 GB free on/, plenty).wails doctorstill reportslibwebkit: Not Foundbecause it only looks forwebkit2gtk-4.0— ignore it;pkg-config --modversion webkit2gtk-4.1→ 2.52.6 is what matters. These are apt packages on the container disk, so a sandbox recreation loses them.
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). -
Licence: MIT,
LICENSEat the repository root (added 2026-09-18, holderk33g— the name used in the tickets and git identity). No per-module licence file;ori-desktop/is covered by the root one. Nothing references it yet from the READMEs orui/package.json. -
Git: remote
origin=https://git.rickub.com/bots-garden/ori.git(Gitea-style self-hosted, not GitHub).mainfast-forwarded tofeat/acp-webappat commit76d62acon 2026-09-17; not pushed yet.hello.md(a story generated during an ori test) and.claude/settings.local.jsonare deliberately left untracked.
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). -
Real Claude Code sessions: exercised by the user on 2026-09-18 (sandbox
hello-world, template 0.0.2 + published kit, real prompt answered). Still missing: an automated end-to-end test that drives a real prompt.
Tickets (.tickets/)
- Managed by the IssueSpec VSCodium extension (
k33g.issuespec0.1.28, author = the user; source clone at~/CodeBerg/issuespec, format referencedocs/en/reference/ticket-files.md). Hand edits are allowed but must keep the schema: a task isid(int ≥ 1, unique in the issue),title,state: open|closed,priority: none|low|medium|high|urgent(always written),author {name,email},createdAt, optionalupdatedAt, andclosedAtonly while closed. Thetext/donechecklist shape is invalid (fixed in ticket 0005 on 2026-09-18; validation recipe in handoff2026-09-18-ticket-0005-format.md).
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 |
|