@oonacode/vscode-ext — OonaCode for VS CodeA VS Code extension that is a thin client of the local OonaCode host service ("the brain"),
exactly like the desktop app, the It does not bundle or start an agent runtime. It discovers the running host service through the
discovery file ( What it adds to the editor
A status bar item on the right shows the connection state — All diagnostics go to the OonaCode output channel; bearer tokens are never logged
(
|
| Setting | Default | Scope | Meaning |
|---|---|---|---|
oonacode.home |
"" |
machine | Override the OonaCode home used to find the discovery file (same as OONACODE_HOME) |
oonacode.autoStartBrain |
false |
window | Start the host service automatically on activation when it is down |
oonacode.hostCommand |
"oona" |
machine | Command used to start the brain (oona, an absolute path, or a bin.js run with Node) |
oonacode.hostArgs |
["serve"] |
machine | Arguments for that command |
oonacode.refreshIntervalMs |
5000 |
window | Discovery + health poll interval |
oonacode.openPanelOnStartup |
false |
window | Open the panel on startup when the brain is reachable |
oonacode.webUiDist |
"" |
machine | Absolute path to a built @oonacode/web-ui dist (auto-detected when empty) |
The four machine-scoped settings are the ones a repository must never be able to set: they name
a program to execute (hostCommand/hostArgs), decide where the bearer token is read from
(home), and decide which HTML runs in a scripted webview (webUiDist). "scope": "machine"
means a checked-in .vscode/settings.json is ignored for them — only user/machine settings apply.
The extension also declares capabilities.untrustedWorkspaces.supported: false, so it does not
activate at all until the workspace is trusted, and on win32 it refuses a host command line
containing cmd.exe metacharacters (&, |, ^, <, >, ", %, !).
How it connects
- Discovery —
src/discovery.tsreuses the sharedresolveOonaPaths()/parseDiscovery()from@oonacode/shared(no path logic is reimplemented) to readhost.json, then classifies it:ok·missing·invalid·stale(the recorded pid is gone). - Health — an authenticated
GET /api/v1/health. Health is the one route the host answers without a token, and it does so with a reduced body ({ ok, appVersion }, nohostId) — so a200withouthostIdis what a rejected token actually looks like, and it is reported as its own state ("the token in the discovery file was rejected") exactly like a401/403. A differenthostIdmeans another process grabbed the port. All of these are distinct from "not running". ThebaseUrlrecorded in the discovery file is only honoured when it is an http(s) URL on a loopback host at the port the same file records; anything else falls back tohttp://127.0.0.1:<port>(parseDiscoveryin@oonacode/shareddoes not validate that field, and it decides where the bearer token is sent). - State machine —
src/connection.tsfolds those outcomes intounknown | checking | connected | not_running | unauthorized | error, which drives the status bar, the command guards and the panel. - Client —
src/host.tsis a typed REST client overHOST_ROUTES/HOST_ROUTE_METHODSplus a small/api/v1/wsclient overHostFrame/ClientFrame. Sockets come fromsrc/ws-client.ts, which prefers the runtime's ownWebSocketand otherwise uses a built-in, dependency-free RFC 6455 client (the extension bundles to one CommonJS file, sowsis not an option, and an editor whose extension host predates Node 22 has noWebSocketglobal).
How the panel embeds the shared UI
src/panel.ts + src/webview-html.ts:
- the built
dist/index.htmlof@oonacode/web-uiis read from disk (src/web-ui-dist.tslooks at theoonacode.webUiDistsetting →require.resolve('@oonacode/web-ui/package.json')→<extension>/media/web-ui→<extension>/../web-ui/dist→<extension>/node_modules/...); - every local
src=/href=is rewritten throughwebview.asWebviewUri(), andcrossoriginis stripped; - a strict CSP is injected:
default-src 'none', scripts only fromwebview.cspSourceplus one nonce, andconnect-srcnarrowed to the exact loopback HTTP + WS origin of the running brain; - the connection is injected as
window.oonacode— the sameDesktopBridgecontract the Electron preload implements — so the shared UI needs no VS Code specific code.openExternalandpickDirectoryround-trip to the extension overpostMessage(openExternalonly acceptshttp(s)/mailto); enableScripts: true,retainContextWhenHidden: true, andlocalResourceRootspinned to the dist directory. Because a live stream must survive it, revealing an already-open panel never re-assignswebview.html(that reloads the document and throws the transcript away): the panel only re-renders for a different connection, a fallback view, or a new#session=deep link.
The transport is proxied (this is what makes the panel work at all)
A webview document's origin is vscode-webview://<uuid>. The host service allow-lists
app://oonacode and loopback http(s) origins only, answers anything else with
403 forbidden_origin, and emits no CORS headers at all — so a fetch() or a /api/v1/ws upgrade
made from the document can never succeed, no matter what the CSP allows.
So the injected bridge replaces window.fetch and window.WebSocket with shims:
- anything aimed at the brain's origin is tunnelled to the extension host over
postMessage(hostFetch,wsOpen/wsSend/wsClose), where the panel performs it as a non-browser client — noOriginheader, so the host's allow-list is satisfied; - anything aimed elsewhere falls through to the real
fetch/WebSocket, where the CSP still rules; - the panel resolves every proxied path against the brain's base URL and refuses anything that
would leave that origin (
//evil.example.com/xand/\evil.example.com/xincluded), allows only the/api/v1/wspath for sockets, caps the number of open sockets, and forwards onlyacceptandcontent-typefrom the document.
Consequently the real bearer token is never written into the webview: the document is handed an
inert stand-in (vscode-webview-proxy-<nonce>) and the extension attaches the real
Authorization: Bearer … itself. A token rotation therefore needs no re-render — the panel just
starts using the new one — and an attacker-authored dist cannot read a usable token out of the
page.
When the web UI build is missing, the panel renders a built-in fallback view with the connection state and buttons for "Open web UI in browser" / "Start host service" / "Retry" — never a blank panel.
Running it in the Extension Development Host
# once: build the contracts and the shared UI the panel embeds
pnpm --filter @oonacode/shared build
pnpm --filter @oonacode/web-ui build
# build the extension bundle (dist/extension.js)
pnpm --filter @oonacode/vscode-ext build
# start the brain in another terminal (or let the extension start it)
pnpm --filter @oonacode/host-service dev
Then open packages/vscode-ext in VS Code and press F5 ("Run Extension"). VS Code
launches a second window (the Extension Development Host) with the extension loaded; the status bar
should show OonaCode and OonaCode: Open OonaCode Panel should render the chat UI.
For an edit/reload loop run pnpm --filter @oonacode/vscode-ext dev (tsup --watch) and use
Developer: Reload Window in the development host.
Packaging with vsce
pnpm --filter @oonacode/web-ui build # the UI the panel embeds
pnpm --filter @oonacode/vscode-ext package # build + copy media/web-ui + vsce package
package runs tsc --noEmit, bundles src/extension.ts to a single CommonJS
dist/extension.js with tsup, copies the built web UI into media/web-ui
(scripts/copy-web-ui.mjs, needed because vsce does not follow pnpm workspace symlinks), and then
calls npx @vscode/vsce package --no-dependencies, producing oonacode-vscode.vsix. Install it
with code --install-extension oonacode-vscode.vsix.
vsce is intentionally not a dependency of this package — it is fetched on demand by the
package script.
Notes
- CommonJS on purpose. This is the one package in the monorepo without
"type": "module": the VS Code extension host loadsmainwithrequire(). The sources are still ESM TypeScript and are bundled to CJS by tsup, so the repo conventions (.jsimport extensions, named exports, shared contracts) are unchanged. - No VS Code at test time. Every module takes the editor API as a parameter typed by
VSCodeApi(src/vscode-api.ts, a structural subset of@types/vscode), sopnpm testruns the discovery, state-machine, webview-rewriting and command-mapping logic in plain Node against a fake. The injected bridge is not exempt:test/bridge-script.test.tsexecutes it in anode:vmrealm with a fakeacquireVsCodeApiand drives it the way the real web UI does, andtest/ws-client.test.tsruns the built-in WebSocket client against a realnode:httpupgrade. - Not published. The extension is not on the VS Code Marketplace / Open VSX yet; install the
.vsixlocally. A JetBrains plugin (the other half of requirement R11) is not built.