OCDX is a typed SDK and mod loader for extending the production OpenCode Desktop V2 interface without patching the installed app.
Cap.2026-07-15.at.14.19.45.mp4
import { defineExtension } from "@hona/ocdx";
export default defineExtension({
activate(ocdx) {
ocdx.desktop.titlebar.action({
id: "hello",
label: "Say hello",
icon: "status",
onPress: () => alert("Hello from OCDX"),
});
},
});The extension ID, name, and version come from manifest.json. The build automatically registers the default export, bundles its CSS and renderer code, and creates the .ocdx archive.
OCDX uses Electron-Hook to start the installed production app with a temporary in-memory bootstrap. Its macOS backend uses dyld interposition while leaving the installed app and its signature untouched.
OCDX runs the installed OpenCode payload in its own OCDX channel. Its app identity, single-instance lock, Desktop storage, Chromium profile, server registration, credentials, configuration, cache, and session database are separate from OpenCode Beta and Stable. The runtime loads independent .ocdx archives from a mods folder.
The channel lives under %APPDATA%/OCDX on Windows and ~/Library/Application Support/OCDX on macOS. OCDX_HOME overrides this root for isolated testing. The service defaults to port 49376; its preference is in config/opencode/service.json. Desktop state is in desktop/, Chromium state in session/, and the server database in data/opencode/ocdx.db.
The source installation owns application updates. OCDX disables the injected application's updater and protocol registration, so it cannot update or take over the source app. Update OpenCode normally, then start OCDX with the updated payload. Existing Beta/Stable sessions and sign-ins are not imported automatically.
release/
builtin/
ocdx.extension-manager.ocdx
mods/
subway-surfers.ocdx
vertical-tabs.ocdx
keep-awake.ocdx
ocdx[.exe]
ocdx_launcher.dll | libocdx_launcher.so | libocdx_launcher.dylib
runtime.js
Each .ocdx file is a ZIP archive:
my-extension.ocdx
manifest.json
renderer.js
assets/
Drop archives into either folder:
release/mods/
%APPDATA%/OCDX/mods/
~/Library/Application Support/OCDX/mods/
The built-in Extensions settings page can also install local files, accept drag and drop, download an .ocdx URL, and enable or disable extensions live.
OCDX validates archive paths and manifests, extracts changed archives into its cache, and loads extensions independently. Extensions are trusted renderer code with access to OpenCode's exposed preload API. Only install extensions you trust.
Requirements:
- Windows or macOS with the production OpenCode Desktop app installed
- Bun
- Git LFS for example assets
- Stable Rust and platform build tools (MSVC on Windows, Xcode Command Line Tools on macOS)
bun install
git lfs pull
bun run build:all
bun run launchOpenCode Beta or Stable can stay open while OCDX runs. To target another production installation:
bun run launch -- --executable "D:\Apps\OpenCode\OpenCode.exe"On Windows, release/ocdx.exe embeds the same blue development icon. You can pin that executable directly to the taskbar or create a normal desktop shortcut to it.
On macOS, OCDX discovers both stable and beta installations. An explicit app bundle or executable also works:
bun run launch -- --executable "/Applications/OpenCode Beta.app"bun run build:all also creates release/OCDX.app with OpenCode's blue icon. To install a Dock launcher, copy that app to /Applications, open it once, then drag OCDX from Applications to the Dock:
cp -R release/OCDX.app /Applications/OCDX.app
open /Applications/OCDX.appThe macOS equivalent of the Windows taskbar is the Dock. OCDX is a launcher rather than a background menu-bar app, so it does not remain in the menu bar after OpenCode starts.
After OpenCode installs an application update, quit the normally relaunched app and start it through OCDX again.
An extension repository needs a manifest, a renderer entry, and any local assets. A trusted main-process entry is optional:
my-extension/
assets/
index.tsx
main.ts # optional trusted main-process entry
manifest.json
styles.css
{
"schema": 1,
"id": "my-extension",
"name": "My Extension",
"version": "1.0.0",
"entry": "renderer.js",
"main": "main.cjs"
}import { defineExtension } from "@hona/ocdx";
import styles from "./styles.css?inline";
export default defineExtension({
styles,
activate(ocdx) {
// Register contributions here.
},
});Styles are inserted and removed with the extension lifecycle. There is no manual registration or stylesheet cleanup.
activate(ocdx) {
ocdx.id
ocdx.lifecycle
ocdx.assets
ocdx.state
ocdx.main
ocdx.desktop
ocdx.unsafe
}Extensions that need trusted Electron main-process behavior can add main.ts and declare "main": "main.cjs". Main entries are unsandboxed code with the same privileges as OpenCode; only install archives you fully trust.
Core OCDX provides lifecycle ownership and an extension-namespaced typed method bridge, not feature-specific host APIs. Renderer extensions share one JavaScript realm, so this namespacing is not a security boundary.
// main.ts
import { defineMainExtension } from "@hona/ocdx/main";
export default defineMainExtension((ocdx) => ({
status() {
return { extension: ocdx.id };
},
}));// index.tsx
import type mainExtension from "./main";
const main = ocdx.main<typeof mainExtension>();
const status = await main.status();Listeners, observers, asynchronous work, and arbitrary resources can share the extension lifecycle:
ocdx.lifecycle.listen(window, "resize", update);
ocdx.lifecycle.observe(document.body, { childList: true }, update);
ocdx.lifecycle.own(() => thirdPartyLibrary.destroy());
await fetch(url, { signal: ocdx.lifecycle.signal });Everything registered through semantic Desktop APIs is owned automatically.
State cells validate persisted JSON, notify subscribers, and save changes automatically:
const enabled = ocdx.state.boolean("enabled", true);
const width = ocdx.state.number("width", 320, { min: 240, max: 640 });
const label = ocdx.state.string("label", "Inspector");
enabled.get();
enabled.set(false);
enabled.set((current) => !current);
enabled.subscribe(console.log);effect runs immediately with the current value and again on every change, so applying state to the world is one line:
ocdx.lifecycle.own(enabled.effect(applyVisibility));For structured state, decoding is explicit and type-safe:
const order = ocdx.state.value<string[]>("order", {
default: [],
decode: (value) =>
Array.isArray(value) && value.every((item) => typeof item === "string")
? value
: undefined,
});Asset URLs are scoped to the current manifest ID:
const icon = ocdx.assets.url("assets/icon.svg");
const data = await ocdx.assets
.fetch("assets/data.json")
.then((response) => response.json());No extension ID is repeated in source code.
OCDX exposes the published @opencode-ai/client promise client with its own channel's local server credentials already configured:
const client = await ocdx.opencode.client();
const current = ocdx.opencode.currentSession.get();
if (current) {
const session = await client.session.get({ sessionID: current.id });
console.log(session.data);
}The current session is a reactive cell derived from the active Desktop tab:
ocdx.lifecycle.own(
ocdx.opencode.currentSession.subscribe((session) => {
console.log(session?.id, session?.server);
}),
);Select another known HTTP server when creating a client; V2 requests scope to directories and locations through their request inputs:
const client = await ocdx.opencode.client({
server: "https://opencode.example.com",
});The built-in sidecar is authenticated through OCDX's main-process bridge. This supports current Desktop builds, which no longer expose window.api. Plain HTTP server keys resolve directly; WSL and SSH credentials are not exposed.
OCDX only provides primitives for real OpenCode Desktop V2 surfaces and behavior. It does not ship a competing component library.
Use the published @opencode-ai/ui package for buttons, switches, inputs, badges, tabs, and other UI components.
const visible = ocdx.state.boolean("visible", false);
ocdx.desktop.titlebar.toggle({
id: "inspector",
label: "Toggle inspector",
icon: (checked) => checked ? <InspectorOpenIcon /> : <InspectorClosedIcon />,
checked: visible,
});Custom icons can use normal Solid JSX:
ocdx.desktop.titlebar.action({
id: "custom",
label: "Custom action",
icon: () => <img src={ocdx.assets.url("assets/icon.png")} alt="" />,
onPress: run,
});ocdx.desktop.settings.toggle({
id: "enabled",
title: "My extension",
description: "Show the extension UI.",
value: enabled,
badge: "New",
});Settings pages use the real Desktop navigation and accept extension-owned content:
import { Button } from "@opencode-ai/ui/button";
import { mountSolid } from "@hona/ocdx/solid";
ocdx.desktop.settings.page({
id: "my-extension",
title: "My Extension",
icon: "sliders",
after: "shortcuts",
mount: mountSolid(() => <Button>Run action</Button>),
});Pane visibility and size are reactive cells, so titlebar actions, settings, resizing, and persistence share one state source:
import { mountSolid } from "@hona/ocdx/solid";
const pane = ocdx.desktop.panes.add({
id: "inspector",
side: "right",
open: ocdx.state.boolean("open", false),
size: ocdx.state.number("size", 360, { min: 240, max: 640 }),
minSize: 240,
maxSize: 640,
mount: mountSolid(() => <Inspector />),
});
pane.show();
pane.hide();
pane.toggle();Set surface: "review" to attach the pane to OpenCode's review surface instead of the app layout.
The tabs service exposes actual Desktop tab state and actions. It does not provide a Vertical Tabs component.
const dispose = ocdx.desktop.tabs.subscribe((tabs) => {
console.log(tabs);
});
ocdx.lifecycle.own(dispose);
ocdx.desktop.tabs.activate(tabID);
ocdx.desktop.tabs.close(tabID);
ocdx.desktop.tabs.create();An extension decides how to present those tabs using normal Solid code and official OpenCode components.
Add extension content beside browser tabs, open files, and Review. This primitive works for inspectors, test results, dashboards, and other session-side tools:
const tab = ocdx.desktop.sidePanel.add({
id: "inspector",
title: "Inspector",
icon: "code",
badge: ocdx.state.number("pending", 0),
mount: mountSolid(() => <Inspector />),
});
tab.show(); // Reveals the panel and selects the tab.
tab.hide(); // Returns to native content; keeps the extension connected.
ocdx.lifecycle.own(tab.active.effect((active) => updateVisibility(active)));active is read-only and is false when the side panel is hidden. badge accepts a read-only cell containing text, a number, or undefined; zero and undefined hide it. Registration IDs are scoped to the extension. Disabling the extension disposes its tab, mount, and listeners automatically; tab.dispose() removes it earlier.
Keep connections and drafts in activate, outside mount. Switching tabs keeps mounted content; closing the panel or changing its host can remount it. The mount's abort signal and returned disposer own view resources. Selecting a different session clears selection. On the home/new-session screen, no session panel is available; the extension can keep its connection alive there.
OCDX coordinates native browser visibility, restores native tabs, and supports the complete row's Arrow/Home/End keyboard navigation in LTR and RTL. It targets current Desktop DOM contracts, not private Solid internals.
Windows verification targets OpenCode Desktop 0.0.0-beta-19278 and published UI/client packages 0.0.0-beta-19271. Run bun typecheck, bun run test, and, after bun run build, cargo test --manifest-path launcher/Cargo.toml --locked. Browser tests use the published production Tabs component and cover native/extension switching, drafts, lifecycle, session changes, hidden panels, and RTL keyboard navigation. The channel test verifies that host environment resets cannot redirect storage or child processes into Beta. Live checks also cover the native browser view and full-page Settings.
The launcher bundles its own ZIP reader rather than depending on modules inside the source application's ASAR.
@hona/ocdx/solid contains integration utilities, not UI components:
import { mountSolid, useCell } from "@hona/ocdx/solid";
mountSolid(() => {
const enabled = useCell(cell);
return <Show when={enabled()}>...</Show>;
});All actual controls come from @opencode-ai/ui.
The typed APIs cover common Desktop V2 integration, but they do not sandbox or constrain extensions. Authors can use normal DOM APIs, Solid, browser packages, and the published OpenCode UI package.
For experiments that do not fit semantic contribution points, ocdx.unsafe exposes the raw titlebar, settings, app-layout, and review mounts. Raw mounts still participate in OCDX lifecycle cleanup.
Once published:
bun add @hona/ocdx @opencode-ai/client @opencode-ai/ui solid-jsPublic exports:
@hona/ocdx
@hona/ocdx/main
@hona/ocdx/solid
The macOS launcher requires OpenCode's signed main executable to allow DYLD environment variables and disable library validation. Electron-Hook removes its DYLD entry before OpenCode spawns helpers, and verifies bootstrap readiness instead of silently opening an unmodified app if injection fails.
Random footnote: a small
ocdxcommand may eventually package and install archives, but the MVP deliberately uses ordinary build scripts and drag-and-drop files instead of shipping a CLI.
MIT