Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>OonaCodeNew to Visual Studio Code? Get it now.
OonaCode

OonaCode

OonaCode

| (0) | Free
OonaCode for VS Code — a thin client of the local OonaCode host service ("the brain"): chat panel, sessions in the current workspace folder, and the local web UI, without a second agent runtime
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

@oonacode/vscode-ext — OonaCode for VS Code

A VS Code extension that is a thin client of the local OonaCode host service ("the brain"), exactly like the desktop app, the oona CLI and the local web UI.

It does not bundle or start an agent runtime. It discovers the running host service through the discovery file (%LOCALAPPDATA%\OonaCode\host\host.json on Windows), authenticates with the per-install bearer token from that file, and hosts the shared @oonacode/web-ui React app in a webview. If the brain is not running, it says so and offers to start it — it never silently does nothing.

What it adds to the editor

Command id What it does
OonaCode: Open OonaCode Panel oonacode.open Opens (or reveals) the webview panel with the shared chat UI
OonaCode: New Session in This Folder oonacode.newSessionHere Creates a session whose cwd is the workspace folder that owns the active editor, then reveals it in the panel (#session=<id>)
OonaCode: Open Web UI in Browser oonacode.openInBrowser Opens http://127.0.0.1:<port>/#token=… in the default browser
OonaCode: Start / Restart Host Service oonacode.restartBrain Spawns the host service detached and waits until it is healthy

oonacode.open also takes an optional session id (vscode.commands.executeCommand('oonacode.open', sessionId)), which the panel turns into the shared UI's #session=<id> deep link.

A status bar item on the right shows the connection state — OonaCode when connected, OonaCode: brain not running / auth failed / error otherwise (with a warning background) — and runs oonacode.open when clicked. All diagnostics go to the OonaCode output channel; bearer tokens are never logged (maskToken redacts them everywhere).

Settings

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

  1. Discovery — src/discovery.ts reuses the shared resolveOonaPaths() / parseDiscovery() from @oonacode/shared (no path logic is reimplemented) to read host.json, then classifies it: ok · missing · invalid · stale (the recorded pid is gone).
  2. 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 }, no hostId) — so a 200 without hostId is 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 a 401/403. A different hostId means another process grabbed the port. All of these are distinct from "not running". The baseUrl recorded 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 to http://127.0.0.1:<port> (parseDiscovery in @oonacode/shared does not validate that field, and it decides where the bearer token is sent).
  3. State machine — src/connection.ts folds those outcomes into unknown | checking | connected | not_running | unauthorized | error, which drives the status bar, the command guards and the panel.
  4. Client — src/host.ts is a typed REST client over HOST_ROUTES/HOST_ROUTE_METHODS plus a small /api/v1/ws client over HostFrame/ClientFrame. Sockets come from src/ws-client.ts, which prefers the runtime's own WebSocket and otherwise uses a built-in, dependency-free RFC 6455 client (the extension bundles to one CommonJS file, so ws is not an option, and an editor whose extension host predates Node 22 has no WebSocket global).

How the panel embeds the shared UI

src/panel.ts + src/webview-html.ts:

  • the built dist/index.html of @oonacode/web-ui is read from disk (src/web-ui-dist.ts looks at the oonacode.webUiDist setting → require.resolve('@oonacode/web-ui/package.json') → <extension>/media/web-ui → <extension>/../web-ui/dist → <extension>/node_modules/...);
  • every local src=/href= is rewritten through webview.asWebviewUri(), and crossorigin is stripped;
  • a strict CSP is injected: default-src 'none', scripts only from webview.cspSource plus one nonce, and connect-src narrowed to the exact loopback HTTP + WS origin of the running brain;
  • the connection is injected as window.oonacode — the same DesktopBridge contract the Electron preload implements — so the shared UI needs no VS Code specific code. openExternal and pickDirectory round-trip to the extension over postMessage (openExternal only accepts http(s)/mailto);
  • enableScripts: true, retainContextWhenHidden: true, and localResourceRoots pinned to the dist directory. Because a live stream must survive it, revealing an already-open panel never re-assigns webview.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 — no Origin header, 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/x and /\evil.example.com/x included), allows only the /api/v1/ws path for sockets, caps the number of open sockets, and forwards only accept and content-type from 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 loads main with require(). The sources are still ESM TypeScript and are bundled to CJS by tsup, so the repo conventions (.js import 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), so pnpm test runs 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.ts executes it in a node:vm realm with a fake acquireVsCodeApi and drives it the way the real web UI does, and test/ws-client.test.ts runs the built-in WebSocket client against a real node:http upgrade.
  • Not published. The extension is not on the VS Code Marketplace / Open VSX yet; install the .vsix locally. A JetBrains plugin (the other half of requirement R11) is not built.
  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft