The preview panel worked by proxying a dev server through OpenChamber's own origin and rewriting the HTML that came back. Anything the rewriter did not anticipate broke, and pages that refuse to be embedded never loaded at all. This deletes the proxy (-1604 lines and its tests) and merges the preview and browser panels into one surface backed by a real Chromium view. What the panel is now - A `<webview>` in its own session partition: logins and cookies persist, hot reload works because nothing is rewritten, DevTools are one click away. - Annotation: pick one element, drag a region, or draw freehand, write a note, and it reaches chat with a screenshot of the visible page with the marks on it. - Toolbar: hard reload, page zoom, device sizes, a light/dark switch that applies to the page rather than the app, and cookie/cache clearing scoped to the panel alone. - Several pages at once, each tab showing the page's own favicon, and an address bar that suggests pages already visited in this project. - Dev servers are listed from what is actually listening on the machine, checked against what a project announced, so a server is offered no matter how it was started. One that is still starting is waited for instead of failing. Remote dev servers The desktop app binds a local port and pipes raw bytes to the OpenChamber host over the existing authenticated connection, so the page keeps its own origin at the root of its own host. The reachable set is exactly what discovery reports and is re-checked per connection, so an authenticated client cannot dial arbitrary local services on the host. Links and redirects to another loopback port stay on the machine that served the page. A tunnel that cannot be opened is reported; it is never replaced by the plain loopback URL, which would answer from the user's own machine under a remote address. Agent control Browser actions are a separate `openchamber_web` tool: open, snapshot, click, type, scroll, inspect computed styles, resize between mobile/tablet/desktop, and capture a screenshot into `.openchamber/screenshots/` in the project. The existing `openchamber` tool keeps sessions, worktrees and scheduled tasks. Each has its own setting in the new Settings -> General -> OpenChamber Tools section, and the plugin is not injected at all when both are off. Capability belongs to the connected client, not to configuration: a client declares on its event stream that it can drive a page, which only a Chromium host does. Exactly one client performs each request — it claims the request before acting, and the first claim wins — because deciding by whose result arrives first would be too late for a click that already happened. No client listening is answered immediately with an explanation rather than a timeout. Runtime boundaries Web tabs get a plain iframe that can display a page but not inspect one. The VS Code extension no longer offers the surface at all, since nothing that makes the panel worth having works there. Mobile is unaffected. Native boundary Camera, microphone, location and device-picker requests from panel pages are denied — Electron grants them by default when no handler is set, and the panel loads whatever address the user types. Page capture, appearance emulation and storage clearing verify that their target belongs to the panel's own session instead of trusting a web-contents id from the renderer. Persisted state Stored `preview` tabs migrate to `browser` (v13 -> v14). Context panel tab limits are now per surface, so filling one surface no longer evicts another's tabs. Address history is stored per project and per runtime. Documentation `preview.mdx` and `desktop-browser.mdx` rewritten across all locales, the agent tool settings path corrected, new `DOCUMENTATION.md` for the browser-control broker and the dev tunnel, and the `ui-api-decoupling` skill updated where it still described the deleted proxy.
6.3 KiB
name, description
| name | description |
|---|---|
| ui-api-decoupling | Use when creating or modifying OpenChamber shared UI data access, OpenCode SDK calls, `RuntimeAPIs`, runtime fetch/auth/URLs, authenticated browser assets, bridges/proxies, runtime switching, or server API routes. |
UI API Decoupling
Core Boundary
- Official OpenCode API calls use
@opencode-ai/sdk/v2throughopencodeClient. - OpenChamber-owned HTTP capabilities use
RuntimeAPIswhere runtime-specific behavior exists, otherwise explicit OpenChamber routes throughruntimeFetch. - Browser/realtime consumers use shared runtime URL/socket helpers.
- Shared UI never hardcodes localhost, ports, API origins, credentials, or one runtime's transport assumptions.
- Treat runtime adapters as the imperative shell: they own transport, auth, serialization, and platform mechanics. Shared feature code receives trusted contracts and owns domain decisions.
Classify First
| Need | Correct path |
|---|---|
| Official OpenCode endpoint | opencodeClient or its SDK client |
| SDK gap for official OpenCode | Narrow documented wrapper in opencodeClient preserving request fidelity |
| OpenChamber HTTP route | runtimeFetch('/api/...') |
| Runtime-owned capability | Extend RuntimeAPIs and implement each applicable runtime |
| Browser-owned authenticated URL | Runtime URL resolver and scoped URL auth |
| SSE/WebSocket | Owning realtime transport; also load relay-transport |
Load References By Task
| Task | Required reference |
|---|---|
| Iframes, downloads, raw images, object URLs, URL tokens | references/browser-assets-and-auth.md |
| Adding runtime capabilities, VS Code behavior, Electron privilege/security, unsupported runtime behavior | references/runtime-parity.md |
| Locating implementations, route registration, runtime switching, or focused tests | references/implementation-map.md |
Load every matching reference before editing.
Mandatory Rules
- Do not bypass the SDK for official OpenCode APIs. Preserve SDK-generated method, body, headers, query, auth, and abort signal.
- Keep OpenChamber routes explicit. Register them before the generic OpenCode proxy.
- Use runtime APIs for runtime-owned capabilities. Components consume hooks/providers, not runtime globals.
- Resolve runtime state at call time. Do not cache runtime base URLs, resolver output, credentials, or SDK clients across endpoint switches.
- Let transport own auth. HTTP uses runtime bearer handling; browser/realtime URLs use scoped short-lived URL auth where headers are impossible.
- Never put long-lived client credentials in URLs. Do not manually append URL tokens.
- Define runtime parity explicitly. Shared UI needs deliberate web, Electron, VS Code, hosted-mobile, and Capacitor behavior or stable unsupported responses.
- Authoritative fetches must signal failure. Do not convert failure into a valid empty value that callers use to clear state.
- Keep privileges at the native/runtime boundary. UI visibility and prompts are not authorization.
- Confirm trust-boundary mutations. Host imports, credential writes, privileged deep links, and runtime switching require explicit user intent.
- Parse at the boundary. Treat external, persisted, bridge, IPC, and network payloads as unknown until a schema, parser, or narrow constructor produces the trusted type consumed by shared code. Do not validate fields and then continue passing the raw payload.
- Model the real contract. Prefer precise result/state unions and required dependencies over loose strings, boolean combinations, optional callback bags,
any, or repeated casts. Make unsupported runtime behavior and failure distinct from valid empty success. - Keep adapters deep and bridges thin. Hide meaningful protocol or platform mechanics behind an intention-revealing runtime operation; do not add pass-through layers that only rename SDK, fetch, or bridge calls.
HTTP Decision Rules
Pass route paths directly to runtimeFetch:
await runtimeFetch('/health');
await runtimeFetch('/api/config/settings');
await runtimeFetch('/api/fs/raw', { query: { path } });
Do not immediately fetch a URL produced by getRuntimeUrlResolver(). Use the resolver only when the browser/realtime API itself consumes the URL:
const imageSrc = getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw?path=diagram.png');
const eventUrl = getRuntimeUrlResolver().sse('/api/event');
Plain fetch is reserved for intentional external origins that are not the active OpenChamber/OpenCode runtime.
Runtime Switch Safety
Review runtime base URL, auth, SDK clients, terminal/realtime transports, stores, session memory, and caches. Key caches by runtime identity where IDs, paths, or URLs can collide. Reset or reconnect affected state through the established runtime-switch flow.
Re-parse values obtained after a switch at their owning boundary. A type established for one runtime response does not make cached raw data from another runtime trustworthy.
Common Anti-Patterns
| Avoid | Use |
|---|---|
Raw feature fetch to official OpenCode |
SDK wrapper/client |
| Component reads runtime globals | useRuntimeAPIs() / provider |
| Hardcoded runtime URL | runtimeFetch or runtime URL resolver |
| Browser URL containing bearer/client token | Scoped URL-auth helper |
| Web-only shared route | Explicit VS Code/mobile decision |
Returning [] after authoritative fetch failure |
Throw or distinct failure result |
Rebuilding SDK Request from URL only |
Preserve original request body/headers/signal |
| Component validates unknown JSON then passes it onward | Adapter parses once and returns a trusted contract |
| Boolean/nullable combinations for exclusive outcomes | Discriminated result or state union |
Verification
- Official calls use SDK paths or documented SDK-gap wrappers.
- OpenChamber routes win before generic proxy fallback.
- Request fidelity, auth, abort, query, and body behavior are tested.
- Browser/realtime auth uses narrow allowlists and scoped tokens.
- Every applicable runtime has implementation or explicit unsupported behavior.
- Runtime switching cannot reuse stale endpoint/auth/cache state.
- Privileged Electron/extension behavior is enforced outside the renderer.
- Focused transport, bridge, proxy, auth, and runtime tests pass; static type/lint checks alone are insufficient.