ui-api-decoupling

star 5.4k

Use when creating or modifying OpenChamber UI data access, RuntimeAPIs, runtimeFetch/runtime-url auth, authenticated browser assets, OpenCode SDK calls, VS Code bridges, Electron runtime switching, or web server API endpoints.

openchamber By openchamber schedule Updated 6/2/2026

name: ui-api-decoupling description: Use when creating or modifying OpenChamber UI data access, RuntimeAPIs, runtimeFetch/runtime-url auth, authenticated browser assets, OpenCode SDK calls, VS Code bridges, Electron runtime switching, or web server API endpoints. license: MIT compatibility: opencode

Overview

OpenChamber shared UI runs against web, Electron desktop, remote server URLs, and VS Code webviews. API code must preserve that runtime boundary.

Core principle: official OpenCode API calls go through @opencode-ai/sdk/v2 via opencodeClient; OpenChamber-owned capabilities go through RuntimeAPIs or explicit OpenChamber routes; runtime transport preserves SDK-generated requests exactly.

Scope

Use this skill for changes touching UI data loading, session/message operations, provider/auth/config calls, filesystem/git/terminal/settings APIs, runtime switching, desktop/VS Code bridges, or server routes under /api/*.

Do not use this skill for pure visual-only UI work unless the change adds, removes, or reshapes data access.

First Step

Before editing, classify every endpoint or capability involved:

Need Correct path
Official OpenCode endpoint opencodeClient or opencodeClient.getSdkClient()
SDK gap to official OpenCode Central helper in opencodeClient using runtimeFetch, documented as SDK gap
OpenChamber-owned feature route RuntimeAPIs first, otherwise runtimeFetch to explicit OC route
Native/runtime capability Extend RuntimeAPIs, implement per runtime, consume via hook/registry
Browser/realtime URL that cannot send headers (iframe, download/open link, SSE, WebSocket, preview subresource) getRuntimeUrlResolver() helpers plus oc_url_token allowlist, not hardcoded URLs
UI-controlled authenticated asset fetch (small icons/thumbnails where JS can fetch) runtimeFetch with Authorization, then URL.createObjectURL(blob)

Mandatory Rules

  1. Never bypass the SDK for official OpenCode APIs

    • Do not add raw fetch or direct runtimeFetch from feature UI to official endpoints such as /api/session, /api/permission, /api/question, /api/auth, /api/provider, /api/command, /api/app.
    • Use opencodeClient wrappers or opencodeClient.getSdkClient().
    • If the SDK lacks a method, add a narrow wrapper in packages/ui/src/lib/opencode/client.ts, mark it as an SDK gap, and add transport coverage when body/method/query/signal matters.
  2. Preserve SDK request fidelity

    • Runtime transport must preserve Request method, body, headers, query string, auth, and abort signal.
    • Do not rebuild a request from only url and init.
    • Regression tests belong near packages/ui/src/lib/runtime-fetch.test.ts, packages/vscode/webview/api/bridge.test.ts, and proxy tests when transport changes.
  3. Use RuntimeAPIs for runtime-owned capabilities

    • Files, git, terminal, settings, notifications, GitHub helpers, client auth, editor/VS Code actions, and tools belong in RuntimeAPIs when shared UI needs runtime-specific behavior.
    • React components use useRuntimeAPIs() or useRuntimeAPI().
    • Non-React modules use getRegisteredRuntimeAPIs() only when a hook cannot be used.
    • Direct window.__OPENCHAMBER_RUNTIME_APIS__ reads are entrypoint/legacy escape hatches, not a new feature pattern.
  4. Keep OpenChamber routes explicit

    • Direct runtimeFetch is acceptable for OpenChamber-only routes such as /api/config/settings, /api/config/skills, /api/config/commands, /api/fs, /api/git, /api/terminal, /api/preview, /api/magic-prompts, /api/tts, and /api/openchamber/tunnel.
    • Register OpenChamber routes before the generic OpenCode proxy, or the proxy will steal the path.
    • Shared UI depending on an OC route requires web and VS Code parity, or an explicit deterministic unsupported response.
  5. Do not hardcode local runtime URLs

    • Do not infer localhost, server ports, or /api origins in shared UI.
    • Use getRuntimeUrlResolver() at call time.
    • Do not use the exported runtimeUrl singleton for new code because it can capture stale resolver state.
  6. Treat runtime auth as transport state

    • HTTP auth is owned by runtime-auth and runtimeFetch; callers pass route paths and let transport attach Authorization only for the active runtime service URL.
    • Browser/realtime transports that cannot set headers use runtime-url helpers and short-lived oc_url_token query auth.
    • Never put long-lived client bearer tokens in URLs. oc_client_token should appear only in legacy stripping/rejection paths, tests, or migration compatibility code.
    • Do not manually append oc_url_token; use resolver helpers and add server-side allowlist coverage when a new browser-consumed route needs URL auth.
  7. Runtime switch must reset stale state

    • Runtime base URL, runtime key, bearer token, SDK clients, terminal transports, session memory, and UI runtime-scoped state must not be cached blindly.
    • Use switchRuntimeEndpoint, subscribeRuntimeEndpointChanged, opencodeClient.reconnectToRuntimeBaseUrl(), and runtime-keyed store state.
  8. Authoritative fetches must signal failure

    • If a caller uses returned data to replace, delete, or clear authoritative state, the method must throw or return null on failure.
    • Do not swallow errors and return [], {}, or null when that value is also a valid empty success unless the caller treats it as display-only.
  9. Privileged runtime switching requires explicit user intent

    • Electron connect/deep-link flows that import a remote host, store a client token, change default host, or switch active runtime must show an in-app confirmation before writing config or switching.
    • The confirmation may show the label and server URL, but never the token.
    • Existing-host imports still require confirmation because they can overwrite the stored token or change the active runtime.

HTTP Request Decision Rules

For normal HTTP requests to the active OpenChamber runtime, use runtimeFetch with the route path. Let runtimeFetch resolve the current runtime base URL and auth at call time.

// Good: runtimeFetch owns base URL, runtime auth, and runtime switching.
await runtimeFetch('/health');
await runtimeFetch('/auth/session', { method: 'GET' });
await runtimeFetch('/api/config/settings');
await runtimeFetch('/api/fs/raw', { query: { path: absolutePath } });

// Bad: callers should not prebuild runtime HTTP URLs for fetches.
await fetch(getRuntimeUrlResolver().health());
await runtimeFetch(getRuntimeUrlResolver().api('/api/config/settings'));
await runtimeFetch(getRuntimeUrlResolver().rawFile(absolutePath));

Use runtimeFetch(..., { query }) instead of manually appending query strings when the request targets /api, /auth, or /health.

// Good
await runtimeFetch('/api/git/status', { query: { directory, mode: 'light' } });

// Avoid
await runtimeFetch(`/api/git/status?directory=${encodeURIComponent(directory)}&mode=light`);

Use getRuntimeUrlResolver() only when the resulting URL is consumed by the browser or a realtime transport, not immediately fetched as HTTP:

// Good resolver usage: URL is assigned to browser/realtime consumers.
const rawImageSrc = getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', { path });
const iframeSrc = getRuntimeUrlResolver().authenticatedAsset(proxyPath);
const eventUrl = getRuntimeUrlResolver().sse('/api/event');
const socketUrl = getRuntimeUrlResolver().websocket('/api/terminal/ws');

Plain fetch is acceptable only for intentional external network requests that do not target the OpenChamber runtime, such as npm registry, models.dev, or a user-provided https://... URL.

Authenticated Browser Assets

Authenticated assets need an explicit transport choice. Pick based on who owns the request:

Asset/request shape Correct pattern
React/UI code can fetch it and the object is small (project icons, small thumbnails, generated previews) runtimeFetch('/api/...') with Authorization, read blob(), render a URL.createObjectURL(blob)
Browser must own the URL (iframe src, image/download/open-link for large raw files, rewritten preview subresources) getRuntimeUrlResolver().authenticatedAsset(...) so the URL carries short-lived oc_url_token
Realtime transports getRuntimeUrlResolver().sse(...) or .websocket(...); never generic fetch/proxy paths

For object-URL assets:

  • Key caches by runtime identity (getRuntimeApiBaseUrl() or runtime key), entity ID, version/update timestamp, and render-affecting options.
  • Cap caches and revoke evicted object URLs with URL.revokeObjectURL.
  • Render a deterministic fallback while loading or after failure; do not leave empty chrome.
  • Keep the fetch display-only unless the caller intentionally treats failure as authoritative.

For URL-auth assets:

  • The server route must explicitly allow oc_url_token in packages/web/server/lib/ui-auth/ui-auth.js and have coverage in ui-auth.test.js.
  • Scope allowlists narrowly to browser-readable GET routes or specific realtime upgrade paths. Do not allow arbitrary /api/*.
  • Use short-lived oc_url_token only. Do not revive oc_client_token in query strings.

Preview iframe/subresource rules:

  • Use preview proxy helpers so oc_preview_token and oc_url_token propagate to rewritten resources and redirects.
  • Strip legacy oc_client_token before forwarding to dev servers.
  • Do not use postMessage('*'); target the known preview origin.
  • Preserve CSP where possible. If injecting a bridge, prefer a per-response nonce and remove only directives that block framing or the bridge.

Runtime API Extension Pattern

When adding a native/per-runtime capability:

  1. Add or extend the interface in packages/ui/src/lib/api/types.ts.
  2. Implement web HTTP behavior in packages/web/src/api/* and compose it in packages/web/src/api/index.ts.
  3. Implement VS Code webview API in packages/vscode/webview/api/* and compose it in packages/vscode/webview/api/index.ts.
  4. Add extension-host handlers in packages/vscode/src/bridge-*-runtime.ts when filesystem, git, settings, or OpenCode manager access is required.
  5. Keep Electron shared through the web runtime unless it needs shell-only IPC in packages/electron/main.mjs or packages/electron/preload.mjs.
  6. Register the runtime APIs through app entrypoints and consume through RuntimeAPIProvider.

VS Code Route Parity

For any shared UI call to /api/*, decide the VS Code behavior explicitly:

Route type VS Code handling
OpenChamber local route Handle in packages/vscode/webview/main.tsx and bridge to extension host when needed
Official OpenCode route Let generic fetch proxy forward to OpenCode via api:proxy
SSE route Use api:sse:start / stream messages / api:sse:stop, never generic proxy
Session message POST Use api:session:message special proxy path
Unsupported native feature Return stable 501/unsupported JSON, not silent fallback

Electron Security Boundary

Electron exposes API base and shell identity broadly, but privileged local capabilities stay local-only.

  • __OPENCHAMBER_API_BASE_URL__ and __OPENCHAMBER_LOCAL_ORIGIN__ route requests.
  • __OPENCHAMBER_CLIENT_TOKEN__, __OPENCHAMBER_HOME__, and privileged desktop IPC are local-page gated.
  • Do not expose filesystem, shell, or host secrets to remote pages for UI convenience.
  • Do not trust arbitrary loopback, file://, or about:blank origins as local UI. Gate privileged preload/IPC/token access to the packaged UI origin and exact runtime origins.
  • Deep-links that add or switch remote runtimes are trust-boundary changes. Confirm before storing tokens or switching hosts.

Common Anti-Patterns

Anti-pattern Use instead
fetch('/api/session/...') in shared UI SDK through opencodeClient
runtimeFetch('/api/session/...') from a component SDK wrapper or documented SDK-gap helper
fetch(getRuntimeUrlResolver().health()) runtimeFetch('/health')
runtimeFetch(getRuntimeUrlResolver().api('/api/foo')) runtimeFetch('/api/foo')
runtimeFetch(getRuntimeUrlResolver().rawFile(path)) runtimeFetch('/api/fs/raw', { query: { path } })
New /api/foo only in web server Web + VS Code route decision
Component reads window.__OPENCHAMBER_RUNTIME_APIS__ useRuntimeAPIs() / useRuntimeAPI()
Rebuilding new Request(newUrl) only new Request(newUrl, oldRequest) plus merged headers
Returning [] on authoritative SDK failure Throw or return null and preserve state
Caching getRuntimeUrlResolver() output forever Read resolver/client at call time or reset on runtime switch
Manually appending oc_client_token or oc_url_token runtimeFetch for HTTP, resolver helpers for browser/realtime URLs
Direct <img src> to a small authenticated app asset runtimeFetch + blob() + object URL with fallback and bounded cache
Adding URL-auth access to a route without server allowlist tests Narrow oc_url_token allowlist in ui-auth.js plus ui-auth.test.js coverage
Connect deep-link writes host config before consent Confirm first, then import/switch

Verification Checklist

Before finalizing a UI/API decoupling change:

  1. Official OpenCode routes use SDK wrappers or documented SDK-gap helpers.
  2. OpenChamber routes are registered before the generic proxy.
  3. VS Code has parity, proxy fallback, or explicit unsupported behavior.
  4. Runtime transport preserves body, method, headers, query, auth, and abort signal.
  5. Runtime auth/token handling uses runtime-auth and runtime-url.
  6. No long-lived client bearer token is placed in a URL; browser/realtime URL auth uses scoped short-lived oc_url_token only.
  7. Browser-consumed routes that need oc_url_token have narrow server allowlist and tests.
  8. Runtime switch clears or scopes affected client/store/object-URL state.
  9. Authoritative loaders distinguish failure from empty success.
  10. Targeted tests cover changed transport, bridge, proxy, auth allowlist, or runtime API behavior.

Implementation Map

Shared UI Sources Of Truth

packages/ui/src/lib/opencode/client.ts is the central OpenCode SDK wrapper. It creates @opencode-ai/sdk/v2 clients with fetch: runtimeFetch, runtime auth headers, current-directory handling, scoped clients, and convenience wrappers. Add official OpenCode API behavior here unless a feature directly consumes getSdkClient() in sync/runtime code.

packages/ui/src/lib/runtime-fetch.ts rewrites /api, /auth, and /health through the active runtime URL resolver and injects runtime auth. Its key contract is preserving SDK-created Request objects, including method, body, headers, query, and signal. For ordinary HTTP calls, pass route paths directly to runtimeFetch; do not pre-resolve them with getRuntimeUrlResolver() first.

packages/ui/src/lib/runtime-url.ts owns HTTP, auth, health, raw-file, SSE, WebSocket, and authenticated browser URL construction. getRuntimeUrlResolver() is the call-time source for browser-consumed URLs like iframe src, large/raw image src, download/open links, SSE URLs, and WebSocket URLs. runtimeUrl is not safe for new code that must survive runtime switches.

packages/ui/src/lib/runtime-auth.ts owns bearer-token state and short-lived URL-token minting. runtimeFetch merges Authorization unless a caller already supplied one. Runtime URL helpers add scoped oc_url_token where headers are impossible; they must never expose long-lived client bearer tokens in URLs.

Runtime API Contract

packages/ui/src/lib/api/types.ts defines RuntimeAPIs and all per-runtime capability contracts.

packages/ui/src/contexts/RuntimeAPIProvider.tsx provides APIs to React and wraps files with a content cache that invalidates on write, delete, and rename.

packages/ui/src/hooks/useRuntimeAPIs.ts is the React consumption path. packages/ui/src/contexts/runtimeAPIRegistry.ts is the non-React escape hatch for modules that cannot use hooks.

packages/ui/src/App.tsx and app variants register APIs and reset runtime-scoped stores on openchamber:runtime-endpoint-changed.

Web Runtime

packages/web/src/runtimeConfig.ts reads injected globals, configures the runtime URL resolver, sets the runtime bearer token, installs the runtime fetch bridge, and creates web APIs.

packages/web/src/main.tsx, mobile-main.tsx, and mini-chat-main.tsx assign window.__OPENCHAMBER_RUNTIME_APIS__ before rendering shared UI.

packages/web/src/api/index.ts composes web RuntimeAPIs from implementations such as files.ts, git.ts, terminal.ts, settings.ts, permissions.ts, github.ts, clientAuth.ts, push.ts, and tools.ts.

Web runtime API implementations are normally HTTP clients for OpenChamber-owned server routes. Use runtimeFetch for HTTP requests; use getRuntimeUrlResolver() only when producing browser/realtime URLs that will not be immediately fetched by code.

Server Routes And Proxy

packages/web/server/index.js starts the OpenChamber web server. Electron imports this server in-process.

packages/web/server/lib/opencode/core-routes.js installs JSON parsing for OpenChamber-owned /api/* route families.

packages/web/server/lib/opencode/feature-routes-runtime.js registers OpenChamber feature routes before the generic OpenCode proxy: filesystem, git, GitHub, quota, config entities, skills/plugins, magic prompts, session folders, scheduled tasks, and related features.

packages/web/server/lib/opencode/proxy.js is the generic /api/* proxy to upstream OpenCode. It strips the /api prefix, injects OpenCode auth headers, replays parsed bodies for non-GET requests, handles /api/event and /api/global/event as SSE, applies readiness gating, and canonicalizes directory query parameters.

OpenChamber-owned routes must be explicit and registered before the proxy. If a route is shared UI contract, add VS Code parity or a deterministic unsupported response.

If an OpenChamber route is consumed directly by the browser with oc_url_token, update the readable/realtime allowlist in packages/web/server/lib/ui-auth/ui-auth.js and add tests in ui-auth.test.js. Do not use URL tokens as a blanket /api/* auth bypass.

VS Code Runtime

packages/vscode/webview/api/index.ts composes VS Code RuntimeAPIs. Terminal is a stub; files, git, settings, permissions, notifications, GitHub, tools, editor, and VS Code actions use the bridge.

packages/vscode/webview/main.tsx installs window.__OPENCHAMBER_RUNTIME_APIS__ and overrides window.fetch. It handles OpenChamber local routes, then proxies generic OpenCode /api/* calls to the extension host. It has special branches for SSE and session message POST.

packages/vscode/webview/requestBodyTransport.ts extracts request bodies from SDK-style Request objects and init.body without losing bytes.

packages/vscode/webview/api/bridge.ts sends bridge messages, supports abort propagation, exposes proxyApiRequest, proxySessionMessageRequest, and SSE start/stop helpers.

packages/vscode/src/bridge-proxy-runtime.ts forwards generic OpenCode proxy requests to the live OpenCode API URL, merges sanitized headers with OpenCode auth, forwards body bytes, and rejects SSE through the generic proxy.

packages/vscode/src/bridge-config-runtime.ts, bridge-fs-runtime.ts, bridge-git-runtime.ts, and related bridge modules implement OpenChamber-owned route behavior in the extension host.

Electron Runtime

packages/electron/main.mjs starts the web server in-process, resolves local/remote runtime target, tracks apiBaseUrl and clientToken, injects init scripts, confirms remote connect deep-links before storing tokens, and handles host switching.

packages/electron/preload.mjs exposes runtime globals. API base and local origin are broadly available for routing. Client token, home directory, and privileged desktop IPC stay local-page gated so remote pages cannot access local host capabilities.

Shared UI should not branch on Electron for backend behavior. Prefer web runtime APIs and the __OPENCHAMBER_DESKTOP__ bridge only for shell capabilities that already exist in the shared runtime contract.

Runtime Switch Flow

packages/ui/src/lib/runtime-switch.ts updates __OPENCHAMBER_API_BASE_URL__, __OPENCHAMBER_CLIENT_TOKEN__, runtime URL resolver, bearer token, and dispatches openchamber:runtime-endpoint-changed.

packages/ui/src/App.tsx reacts by preparing/restoring runtime-keyed session and UI state, reconnecting opencodeClient, clearing provider/agent connection state, disposing terminal transports, resetting streaming state, and triggering re-bootstrap.

Any cache keyed only by session ID, directory, or URL should be reviewed when runtime switching is involved. Use runtime keys when local and remote instances can share IDs or paths.

Tests To Prefer

Use targeted transport/auth tests when changing request forwarding or URL auth: packages/ui/src/lib/runtime-fetch.test.ts, packages/ui/src/lib/runtime-url.test.ts, packages/ui/src/lib/runtime-auth.test.ts, packages/web/server/lib/ui-auth/ui-auth.test.js, packages/vscode/webview/api/bridge.test.ts, packages/vscode/src/bridge-proxy-runtime.test.js, packages/web/server/opencode-proxy.test.js, and packages/web/server/lib/preview/proxy-runtime.test.js.

Use runtime API tests near the implementation when adding or changing per-runtime behavior, for example web API tests under packages/web/src/api/*.test.ts, VS Code bridge tests under packages/vscode/src/*test.js, and UI wrapper tests under packages/ui/src/lib/*test.ts.

Run bun run type-check and bun run lint before finalizing code changes unless the user explicitly narrows validation.

References

  • SDK wrapper: packages/ui/src/lib/opencode/client.ts
  • Runtime fetch/auth/url: packages/ui/src/lib/runtime-fetch.ts, runtime-auth.ts, runtime-url.ts
  • Runtime API contract: packages/ui/src/lib/api/types.ts
  • Web API composition: packages/web/src/api/index.ts, packages/web/src/runtimeConfig.ts
  • VS Code bridge/proxy: packages/vscode/webview/main.tsx, packages/vscode/webview/api/bridge.ts, packages/vscode/src/bridge-proxy-runtime.ts
  • Server proxy: packages/web/server/lib/opencode/proxy.js, packages/web/server/lib/opencode/core-routes.js
  • UI auth and URL-token allowlists: packages/web/server/lib/ui-auth/ui-auth.js
  • Preview proxy and rewritten browser subresources: packages/web/server/lib/preview/proxy-runtime.js
Install via CLI
npx skills add https://github.com/openchamber/openchamber --skill ui-api-decoupling
Repository Details
star Stars 5,432
call_split Forks 562
navigation Branch main
article Path SKILL.md
More from Creator