diff --git a/INSTALL.md b/INSTALL.md
index 628ea16..0c7cd0f 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -102,7 +102,7 @@ right-click the menu bar icon → **Check for Updates…**, or turn on *Settings
Updates → check automatically at launch* (off by default). The app downloads
the signed update, verifies it, and relaunches. Update checks are the only
app-originated network call. The daemon also contacts a T3 Code environment
-only after you explicitly enable that adapter and exchange a one-time pairing
+only after you explicitly enable that integration and exchange a one-time pairing
link; Microbridge has no telemetry or cloud relay.
Homebrew installs are managed by brew instead: the app detects the brew
@@ -111,7 +111,7 @@ so the formula version and the on-disk app never drift apart.
### Cursor integration
-Cursor support ships inside Microbridge. Open **Settings → Adapters** and click
+Cursor support ships inside Microbridge. Open **Settings → Integrations** and click
**Enable Cursor**; Microbridge installs its bundled lifecycle integration into
Cursor's supported local-plugin directory after that explicit consent. Reload
Cursor once if it is already open. **Remove** disables the adapter and removes
@@ -119,7 +119,7 @@ only Microbridge's local integration. No Marketplace download is required.
### Factory integration
-Factory support ships inside Microbridge. Open **Settings → Adapters** and
+Factory support ships inside Microbridge. Open **Settings → Integrations** and
click **Enable Factory**. Microbridge copies its signed `microbridgectl` helper
to `~/.microbridge/integrations/factory/` and merges only its own entries into
Factory's supported `~/.factory/hooks.json`; existing hooks are preserved.
@@ -140,8 +140,18 @@ contract. Microbridge leaves approval, new-session, and reasoning-effort
controls disabled until CNVS exposes stable targets for them.
Synara and Conductor do not need an installer: their Codex/Claude sessions are
-named by the built-in journal watchers. T3 Code controls require the one-time
-pairing flow shown in **Settings → Adapters**.
+named by the built-in journal watchers. ChatGPT, Claude Desktop, Codex CLI, and
+Claude Code are distinguished automatically by those same watchers. T3 Code
+controls require the one-time pairing flow shown in **Settings → Integrations**.
+
+### OpenCode integration
+
+OpenCode support ships inside Microbridge. Open **Settings → Integrations** and
+click **Enable OpenCode**. Microbridge installs its dependency-free global plugin
+at `~/.config/opencode/plugins/microbridge.mjs`. Restart OpenCode if it is already
+running. The plugin publishes local lifecycle state and routes Interrupt to the
+exact OpenCode session; it does not read or send prompts, transcripts, source
+code, or tool arguments. **Remove** deletes only the Microbridge-owned file.
**Note:** Homebrew installs **prebuilt** release binaries (not a from-source
Tauri build). The formula checksums are refreshed by CI after each `v*` tag.
@@ -160,6 +170,7 @@ Tauri build). The formula checksums are refreshed by CI after each `v*` tag.
| `~/.cursor/plugins/local/microbridge` | Bundled Cursor lifecycle integration (only after consent) |
| `~/.factory/hooks.json` | Existing Factory hooks plus Microbridge-owned lifecycle entries (only after consent) |
| `~/.microbridge/integrations/factory/microbridgectl` | Signed Factory hook helper (only after consent) |
+| `~/.config/opencode/plugins/microbridge.mjs` | Bundled OpenCode lifecycle and interrupt integration (only after consent) |
| `~/Library/LaunchAgents/ai.microbridge.ui.plist` | Login item (only if you enable launch at login) |
## Launch at login
diff --git a/PRIVACY.md b/PRIVACY.md
index 2afaaa8..5639ff2 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -22,16 +22,21 @@ no unconfigured network traffic.
- No unconfigured network traffic; update checks and T3 access require opt-in
- No uploading of session text or source code
-## Adapters
+## Integrations
-First-party adapters watch local session stores. The bundled Cursor integration
+First-party integrations watch local session stores. The bundled Cursor integration
sends metadata-only lifecycle events over the local socket and never sends
prompt, response, transcript, or tool argument content. The T3 Code adapter
talks only to the exact environment the user pairs, using scoped orchestration
access.
+The optional OpenCode plugin sends only the local session identifier, title,
+state, and update time over Microbridge's mode-`0600` Unix socket. It does not
+send prompts, responses, transcripts, source code, tool arguments, or files.
+Interrupt is executed inside OpenCode against the exact local session ID.
+
The one-time T3 pairing token is exchanged immediately, never logged, and not
-stored. Removing the adapter deletes its Keychain credential.
+stored. Removing the integration deletes its Keychain credential.
## Hardware
diff --git a/README.md b/README.md
index 9b10643..d5ac773 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
**An open-source control plane for the Codex Micro — one macropad, every coding agent.**
-Microbridge is a tiny local daemon that bridges AI coding agents — Codex CLI, Claude Code, CNVS, Cursor, T3 Code, Synara, Conductor, Factory, and anything else with an adapter — to the [Work Louder Codex Micro](https://worklouder.cc/). Per-key RGB mirrors live agent state; keys route only the actions each adapter explicitly advertises, so unsupported controls never report false success. No vendor desktop app is required for Microbridge itself.
+Microbridge is a tiny local daemon that bridges AI coding agents — ChatGPT, Claude Desktop, Codex CLI, Claude Code, CNVS, Cursor, T3 Code, Synara, Conductor, Factory, OpenCode, and anything else with an integration — to the [Work Louder Codex Micro](https://worklouder.cc/). Per-key RGB mirrors live agent state; keys route only the actions each integration explicitly advertises, so unsupported controls never report false success. No vendor desktop app is required for Microbridge itself.
-> **Status: early public alpha (`v0.3.x`).** Menu bar UI, local daemon, in-process Codex/Claude host attribution, native CNVS control, and signed macOS packages are shipping. Cursor and Factory lifecycle reception and paired T3 Code control are opt-in and capability-gated. **HID protocol (VID/PID, framing, `v.oai.thstatus`) is implemented from ChatGPT’s Work Louder kit**; hardware control stays off until enabled in Device settings (or `MICROBRIDGE_HID_CLAIM=1` is set for diagnostics) while physical validation is completed. See [docs/device-hid.md](docs/device-hid.md).
+> **Status: early public alpha (`v0.3.x`).** Menu bar UI, local daemon, in-process ChatGPT/Codex and Claude Desktop/Claude Code attribution, native CNVS control, and signed macOS packages are shipping. Cursor, Factory, OpenCode, and paired T3 Code control are opt-in and capability-gated. **HID protocol (VID/PID, framing, `v.oai.thstatus`) is implemented from ChatGPT’s Work Louder kit**; hardware control stays off until enabled in Device settings (or `MICROBRIDGE_HID_CLAIM=1` is set for diagnostics) while physical validation is completed. See [docs/device-hid.md](docs/device-hid.md).
## Screenshots
@@ -34,19 +34,19 @@ Design spec: [docs/design/README.md](docs/design/README.md).
The Micro's best feature — bidirectional Agent Keys — currently works through exactly one vendor's desktop app. Most of us run agents in more than one place. Microbridge turns the deck into a shared, neutral surface:
-- **Adapters publish state.** Each agent session reports `thinking`, `working`, `awaiting_approval`, … as transitions happen.
-- **The focus policy decides.** Exactly one session owns the deck at a time; approval requests can preempt. Adapters never touch the device, so two apps can never fight over your keys.
+- **Integrations publish state.** Each agent session reports `thinking`, `working`, `awaiting_approval`, … as transitions happen.
+- **The focus policy decides.** Exactly one session owns the deck at a time; approval requests can preempt. Integrations never touch the device, so two apps can never fight over your keys.
- **The device layer renders.** State becomes LEDs; key presses become routed actions.
## Design principles
1. **Invisible footprint.** Local watchers are event-driven; device input, CNVS's local snapshot API, and an explicitly paired T3 connection use bounded polling and backoff. Idle CPU and RSS remain part of the [footprint budget](docs/architecture.md#footprint-budget).
-2. **Local-first and explicit network access.** There is no telemetry or Microbridge cloud relay. The app checks for updates only when requested or enabled, CNVS traffic is restricted to its authenticated loopback endpoint, and the daemon contacts a T3 environment only after the user enables the adapter and supplies a one-time pairing link. Factory controls invoke the user-installed `droid` CLI only when a hardware action is requested.
-3. **Rust core, any-language adapters.** The always-resident part is a single static Rust binary. First-party adapters compile into it (in-process, ~zero overhead). Community adapters are separate processes speaking [newline-delimited JSON](docs/protocol.md) — write one in whatever you like.
-4. **The menu bar app is the product UI.** Configure keys, lighting, and adapters there. The daemon keeps the hardware alive underneath; `microbridgectl` is a support/debug escape hatch.
+2. **Local-first and explicit network access.** There is no telemetry or Microbridge cloud relay. The app checks for updates only when requested or enabled, CNVS traffic is restricted to its authenticated loopback endpoint, and the daemon contacts a T3 environment only after the user enables the integration and supplies a one-time pairing link. Factory controls invoke the user-installed `droid` CLI only when a hardware action is requested.
+3. **Rust core, any-language integrations.** The always-resident part is a single static Rust binary. First-party integrations compile into it (in-process, ~zero overhead). Optional integrations use official host hooks/APIs or speak [newline-delimited JSON](docs/protocol.md).
+4. **The menu bar app is the product UI.** Configure keys, lighting, and integrations there. The daemon keeps the hardware alive underneath; `microbridgectl` is a support/debug escape hatch.
Cursor support is included in the Microbridge app and repository. Enable it
-once in **Settings → Adapters**; Microbridge installs its bundled lifecycle
+once in **Settings → Integrations**; Microbridge installs its bundled lifecycle
integration into Cursor's supported local-plugin directory. There is no
separate Marketplace download or second product to maintain.
@@ -57,6 +57,11 @@ active model advertises. Synara and Conductor sessions are attributed through
the built-in Codex/Claude watchers, so they need no pairing code or extra
background adapter.
+OpenCode support is bundled too. Enabling it installs one dependency-free
+global plugin through OpenCode's official plugin directory. Lifecycle events
+stay in the OpenCode process, and Interrupt uses the documented session API;
+there is no polling or additional helper process.
+
CNVS support is native and automatic: Microbridge reads CNVS's authenticated
loopback control API, represents each agent terminal by its exact canvas and
node identifiers, and routes focus or interrupt back to that target. No pairing
@@ -66,8 +71,8 @@ code, private database access, or CNVS modification is required.
```
┌───────────┐ ┌─────────────┐ ┌──────────────────┐
-│ Codex CLI │ │ Claude Code │ │ community adapters │
-│ (in-proc) │ │ (in-proc) │ │ (any language) │
+│ ChatGPT / │ │ Claude app /│ │ optional integrations │
+│ Codex CLI │ │ Claude Code │ │ OpenCode, Cursor, … │
└─────┬─────┘ └─────┬───────┘ └────────┬─────────┘
│ in-process │ NDJSON over unix socket
▼ ▼ ▼
@@ -144,7 +149,7 @@ Requires stable Rust (see `rust-toolchain.toml`) and, for Node adapters / UI, No
## Contributing
-Adapter PRs are explicitly welcome — that is the point of the project. Start with [docs/adapters.md](docs/adapters.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
+Integration PRs are explicitly welcome — that is the point of the project. Start with [docs/adapters.md](docs/adapters.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
## Relationship to Work Louder / OpenAI
@@ -156,7 +161,7 @@ Microbridge only exists because the Codex Micro is *open* — and that was a cho
OpenAI is a for-profit company, and it would have been easy to lock the Micro to a single first-party app: a closed protocol, an exclusive USB claim, no way for anyone else to light a key. They did the opposite — they ship the device kit in the open, keep the HID interface **non-exclusive** so third-party software can coexist with the official experience instead of fighting it, and keep giving users a choice (Codex CLI is open source; the models are reachable over documented APIs). None of that was required of them. **Thank you.**
-Thanks too to **Work Louder** for designing a genuinely hackable macropad, and to **everyone who writes an adapter, files an issue, or plugs in a device and tells us what really happens** — adapters are the point of this project.
+Thanks too to **Work Louder** for designing a genuinely hackable macropad, and to **everyone who writes an integration, files an issue, or plugs in a device and tells us what really happens** — integrations are the point of this project.
Full notes: [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md).
diff --git a/adapters/README.md b/adapters/README.md
index 05c4ba4..ed1871d 100644
--- a/adapters/README.md
+++ b/adapters/README.md
@@ -1,12 +1,14 @@
-# Adapter catalog
+# Integration catalog
-Adapter documentation lives here, one folder per runtime.
+Integration documentation lives here, one folder per runtime. The local socket
+protocol still calls an out-of-process connector an `adapter`; that is an
+implementation term, not a separate product category.
Read [docs/adapters.md](../docs/adapters.md) for the contract and the review
checklist, and [docs/protocol.md](../docs/protocol.md) for the wire format.
## Built-in integrations
-| Adapter | Status | Language |
+| Integration | Status | Language |
|---|---|---|
| [`t3code`](t3code/) | daemon-owned paired HTTP | Rust |
| [`factory`](factory/) | bundled official hooks + JSON-RPC controls | Rust helper |
@@ -17,11 +19,12 @@ checklist, and [docs/protocol.md](../docs/protocol.md) for the wire format.
These integrations are daemon-owned or reuse watchers compiled into the daemon.
See [docs/architecture.md](../docs/architecture.md).
-## Managed and out-of-process adapters
+## Managed and out-of-process integrations
-| Adapter | Status | Language |
+| Integration | Status | Language |
|---|---|---|
| [`cursor`](cursor/) | bundled managed hooks | Node |
+| [`opencode`](opencode/) | bundled global plugin + session interrupt | Node (no deps) |
| [`reference-echo`](reference-echo/) | community adapter example | Node (no deps) |
Out-of-process adapters may be written in any language and communicate over the
diff --git a/adapters/cursor/README.md b/adapters/cursor/README.md
index 2b5a8b6..5c017d2 100644
--- a/adapters/cursor/README.md
+++ b/adapters/cursor/README.md
@@ -7,7 +7,7 @@ replacement sessions.
## Install and consent
-1. Open **Microbridge Settings → Adapters**.
+1. Open **Microbridge Settings → Integrations**.
2. Click **Enable Cursor**. Microbridge installs its bundled integration into
Cursor's supported local-plugin directory after this explicit consent.
3. Reload Cursor once if it is already open. **Remove** disables the adapter
diff --git a/adapters/factory/README.md b/adapters/factory/README.md
index 07677e0..1165601 100644
--- a/adapters/factory/README.md
+++ b/adapters/factory/README.md
@@ -1,6 +1,6 @@
# Factory integration
-Enable Factory in **Microbridge Settings → Adapters**. The app transactionally
+Enable Factory in **Microbridge Settings → Integrations**. The app transactionally
merges Microbridge-owned commands into Factory's official user hooks at
`~/.factory/hooks.json` and installs a signed helper at
`~/.microbridge/integrations/factory/microbridgectl`. Existing hooks are
diff --git a/adapters/opencode/README.md b/adapters/opencode/README.md
new file mode 100644
index 0000000..5b91c33
--- /dev/null
+++ b/adapters/opencode/README.md
@@ -0,0 +1,24 @@
+# OpenCode integration
+
+Enable OpenCode in **Microbridge Settings → Integrations**. Microbridge installs
+one dependency-free global plugin at
+`~/.config/opencode/plugins/microbridge.mjs`. OpenCode loads global plugins for
+both its CLI and desktop surfaces.
+
+The plugin uses OpenCode's documented plugin events and SDK client. It opens a
+local Unix-socket connection to `microbridged`; there is no helper process,
+polling loop, telemetry, or external network request.
+
+## Supported behavior
+
+- Lifecycle: session create/update/status/idle/error/delete, tool activity, and
+ permission-waiting state.
+- Interrupt: OpenCode's documented `client.session.abort()` method for the exact
+ session owning the Agent Key.
+
+OpenCode does not currently expose a stable exact-session focus operation or a
+model-aware per-session reasoning-effort mutation through this plugin contract,
+so Microbridge does not advertise those controls.
+
+**Remove** deletes only the Microbridge-owned plugin. Restart OpenCode after
+installing, repairing, or removing the integration if it was already running.
diff --git a/adapters/opencode/microbridge.mjs b/adapters/opencode/microbridge.mjs
new file mode 100644
index 0000000..13e7928
--- /dev/null
+++ b/adapters/opencode/microbridge.mjs
@@ -0,0 +1,229 @@
+// MICROBRIDGE_OPENCODE_PLUGIN
+// Installed by Microbridge into ~/.config/opencode/plugins/microbridge.mjs.
+
+import net from "node:net";
+import os from "node:os";
+import path from "node:path";
+
+const ADAPTER_ID = "opencode";
+const ADAPTER_VERSION = "__MICROBRIDGE_VERSION__";
+const MAX_PENDING_MESSAGES = 128;
+const MIN_RECONNECT_DELAY_MS = 1_000;
+const MAX_RECONNECT_DELAY_MS = 30_000;
+
+const capabilities = {
+ lifecycle_observation: true,
+ approval_acceptance: false,
+ approval_rejection: false,
+ interrupt: true,
+ new_session: false,
+ focus_open: false,
+ reasoning_effort: false,
+};
+
+export function sessionIdFromEvent(event) {
+ const properties = event?.properties ?? {};
+ return (
+ properties.sessionID ??
+ properties.sessionId ??
+ properties.info?.id ??
+ properties.session?.id ??
+ null
+ );
+}
+
+export function stateFromEvent(event) {
+ const type = event?.type ?? "";
+ if (type === "permission.asked") return "awaiting_approval";
+ if (type === "session.error") return "error";
+ if (type === "session.idle") return "done";
+ if (type === "session.created" || type === "session.updated") return "idle";
+ if (type !== "session.status") return null;
+
+ const status = event?.properties?.status;
+ const value = typeof status === "string" ? status : status?.type;
+ switch (value) {
+ case "busy":
+ case "active":
+ case "working":
+ return "working";
+ case "retry":
+ case "thinking":
+ return "thinking";
+ case "error":
+ return "error";
+ case "idle":
+ return "done";
+ default:
+ return null;
+ }
+}
+
+export function titleFromInfo(info, directory) {
+ const title = info?.title?.trim();
+ if (title) return title.slice(0, 72);
+ const candidate = info?.directory || directory;
+ const basename = candidate ? path.basename(candidate) : "";
+ return basename || "OpenCode session";
+}
+
+function createBridge({ client, directory }) {
+ const socketPath =
+ process.env.MICROBRIDGE_SOCKET ||
+ path.join(os.homedir(), ".microbridge", "microbridged.sock");
+ const metadata = new Map();
+ const latestStates = new Map();
+ const pending = [];
+ let socket = null;
+ let reconnectTimer = null;
+ let reconnectDelayMs = MIN_RECONNECT_DELAY_MS;
+ let disposed = false;
+ let input = "";
+
+ const send = (message) => {
+ const line = `${JSON.stringify(message)}\n`;
+ if (socket?.writable) {
+ socket.write(line);
+ return;
+ }
+ pending.push(line);
+ if (pending.length > MAX_PENDING_MESSAGES) pending.shift();
+ };
+
+ const publish = async (sessionID, state, suppliedInfo) => {
+ if (!sessionID || !state) return;
+ if (suppliedInfo) metadata.set(sessionID, suppliedInfo);
+ let info = metadata.get(sessionID);
+ if (!info) {
+ try {
+ const response = await client.session.get({ path: { id: sessionID } });
+ info = response?.data ?? response;
+ if (info) metadata.set(sessionID, info);
+ } catch {
+ // Lifecycle truth is still useful when a title lookup races deletion.
+ }
+ }
+ latestStates.set(sessionID, state);
+ send({
+ type: "status",
+ session: {
+ id: `${ADAPTER_ID}:${sessionID}`,
+ app: "OpenCode",
+ title: titleFromInfo(info, directory),
+ state,
+ updated_at_ms: Date.now(),
+ },
+ });
+ };
+
+ const handleAction = async (message) => {
+ if (message?.type !== "action" || message.action !== "interrupt") return;
+ const prefix = `${ADAPTER_ID}:`;
+ if (!message.session_id?.startsWith(prefix)) return;
+ const sessionID = message.session_id.slice(prefix.length);
+ if (!sessionID) return;
+ try {
+ await client.session.abort({ path: { id: sessionID } });
+ } catch {
+ // The daemon keeps capability routing honest; OpenCode owns user-facing errors.
+ }
+ };
+
+ const scheduleReconnect = () => {
+ if (disposed || reconnectTimer) return;
+ const delay = reconnectDelayMs;
+ reconnectDelayMs = Math.min(reconnectDelayMs * 2, MAX_RECONNECT_DELAY_MS);
+ reconnectTimer = setTimeout(() => {
+ reconnectTimer = null;
+ connect();
+ }, delay);
+ };
+
+ const connect = () => {
+ if (disposed || socket) return;
+ const next = net.createConnection({ path: socketPath });
+ socket = next;
+ next.setEncoding("utf8");
+ next.on("connect", () => {
+ reconnectDelayMs = MIN_RECONNECT_DELAY_MS;
+ next.write(
+ `${JSON.stringify({
+ type: "hello",
+ adapter: ADAPTER_ID,
+ protocol_version: 0,
+ adapter_version: ADAPTER_VERSION,
+ capabilities,
+ })}\n`,
+ );
+ for (const line of pending.splice(0)) next.write(line);
+ for (const [sessionID, state] of latestStates) {
+ void publish(sessionID, state, metadata.get(sessionID));
+ }
+ });
+ next.on("data", (chunk) => {
+ input += chunk;
+ while (input.includes("\n")) {
+ const newline = input.indexOf("\n");
+ const line = input.slice(0, newline).trim();
+ input = input.slice(newline + 1);
+ if (!line) continue;
+ try {
+ void handleAction(JSON.parse(line));
+ } catch {
+ // Ignore malformed daemon lines and retain the connection.
+ }
+ }
+ });
+ next.on("error", () => {});
+ next.on("close", () => {
+ if (socket === next) socket = null;
+ input = "";
+ scheduleReconnect();
+ });
+ };
+
+ connect();
+
+ return {
+ async event(event) {
+ const sessionID = sessionIdFromEvent(event);
+ if (event?.type === "session.deleted") {
+ if (sessionID) {
+ metadata.delete(sessionID);
+ latestStates.delete(sessionID);
+ send({ type: "bye", session_id: `${ADAPTER_ID}:${sessionID}` });
+ }
+ return;
+ }
+ const info = event?.properties?.info ?? event?.properties?.session;
+ const state = stateFromEvent(event);
+ if (sessionID && (state || info)) {
+ await publish(sessionID, state ?? latestStates.get(sessionID) ?? "idle", info);
+ }
+ },
+ async activity(sessionID, state) {
+ await publish(sessionID, state);
+ },
+ dispose() {
+ disposed = true;
+ if (reconnectTimer) clearTimeout(reconnectTimer);
+ reconnectTimer = null;
+ socket?.destroy();
+ socket = null;
+ },
+ };
+}
+
+export const Microbridge = async ({ client, directory }) => {
+ const bridge = createBridge({ client, directory });
+ return {
+ event: async ({ event }) => bridge.event(event),
+ "chat.message": async ({ sessionID }) => bridge.activity(sessionID, "thinking"),
+ "permission.ask": async (input, output) => {
+ if (output.status === "ask") await bridge.activity(input.sessionID, "awaiting_approval");
+ },
+ "tool.execute.before": async ({ sessionID }) => bridge.activity(sessionID, "working"),
+ "tool.execute.after": async ({ sessionID }) => bridge.activity(sessionID, "working"),
+ dispose: async () => bridge.dispose(),
+ };
+};
diff --git a/adapters/opencode/microbridge.test.mjs b/adapters/opencode/microbridge.test.mjs
new file mode 100644
index 0000000..d7cd925
--- /dev/null
+++ b/adapters/opencode/microbridge.test.mjs
@@ -0,0 +1,27 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { sessionIdFromEvent, stateFromEvent, titleFromInfo } from "./microbridge.mjs";
+
+test("maps official OpenCode lifecycle events", () => {
+ assert.equal(
+ sessionIdFromEvent({ type: "session.status", properties: { sessionID: "ses_1" } }),
+ "ses_1",
+ );
+ assert.equal(
+ sessionIdFromEvent({ type: "session.updated", properties: { info: { id: "ses_2" } } }),
+ "ses_2",
+ );
+ assert.equal(
+ stateFromEvent({ type: "session.status", properties: { status: { type: "busy" } } }),
+ "working",
+ );
+ assert.equal(stateFromEvent({ type: "permission.asked", properties: {} }), "awaiting_approval");
+ assert.equal(stateFromEvent({ type: "session.idle", properties: {} }), "done");
+ assert.equal(stateFromEvent({ type: "session.error", properties: {} }), "error");
+});
+
+test("uses session title and falls back to the workspace name", () => {
+ assert.equal(titleFromInfo({ title: "Ship OpenCode support" }, "/tmp/repo"), "Ship OpenCode support");
+ assert.equal(titleFromInfo({}, "/Users/me/dev/microbridge"), "microbridge");
+});
diff --git a/adapters/t3code/README.md b/adapters/t3code/README.md
index 1060f08..c1ece3e 100644
--- a/adapters/t3code/README.md
+++ b/adapters/t3code/README.md
@@ -5,7 +5,7 @@ its host contract and replaces the former idle Node scaffold.
## Pairing
-1. Enable T3 Code in **Microbridge Settings → Adapters**.
+1. Enable T3 Code in **Microbridge Settings → Integrations**.
2. In T3 Code, open **Settings → Connections**, enable **Network access**, then
create a one-time link under **Authorized clients**.
3. Paste the link into Microbridge. The one-time token is exchanged immediately.
diff --git a/adapters/t3code/index.mjs b/adapters/t3code/index.mjs
index 41f602e..dfbea68 100755
--- a/adapters/t3code/index.mjs
+++ b/adapters/t3code/index.mjs
@@ -1,5 +1,5 @@
#!/usr/bin/env node
console.error(
- "The T3 Code adapter is daemon-owned in Microbridge v0.2.1. Enable and pair it in Microbridge Settings → Adapters.",
+ "The T3 Code integration is daemon-owned in Microbridge v0.2.1. Enable and pair it in Microbridge Settings → Integrations.",
);
process.exitCode = 1;
diff --git a/apps/microbridge-ui/README.md b/apps/microbridge-ui/README.md
index 14e7b2e..7f4cd53 100644
--- a/apps/microbridge-ui/README.md
+++ b/apps/microbridge-ui/README.md
@@ -7,7 +7,7 @@ actions (approve / reject / interrupt) stay on the physical Codex Micro.
|---|---|
| Menu bar icon | Template tray icon; click toggles the popover |
| Popover | Connection, focus card, device echo, threads, Settings / Pause LEDs / Quit |
-| Settings | Keys (device twin) · Agent Keys · Adapters · Device |
+| Settings | Keys (device twin) · Agent Keys · Integrations · Device · Updates |
| Focus HUD | ~2.5s toast when deck focus changes |
MagicPath mockups remain the visual go-to (`vendor/magicpath/`).
diff --git a/apps/microbridge-ui/src-tauri/src/lib.rs b/apps/microbridge-ui/src-tauri/src/lib.rs
index cee115e..4b3e544 100644
--- a/apps/microbridge-ui/src-tauri/src/lib.rs
+++ b/apps/microbridge-ui/src-tauri/src/lib.rs
@@ -553,6 +553,156 @@ mod factory_integration_tests {
}
}
+const OPENCODE_PLUGIN_MARKER: &str = "MICROBRIDGE_OPENCODE_PLUGIN";
+const OPENCODE_PLUGIN_NAME: &str = "microbridge.mjs";
+static OPENCODE_PLUGIN_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
+
+fn validate_opencode_plugin(path: &Path) -> Result
- CNVS connects automatically through its authenticated local control - API and targets exact canvas terminals. Cursor and Factory ship - inside Microbridge and install locally with one click. Synara and - Conductor are identified through the built-in Codex and Claude - journal watchers. State and capabilities below are live. + Every supported app is listed here. Green integrations are ready; + everything else shows exactly what it needs. ChatGPT, Claude + Desktop, Synara, Conductor, and CNVS are built in. Cursor, Factory, + T3 Code, and OpenCode connect only after you choose to enable them.
CNVS-hosted Codex and Claude terminals replace matching raw journal - cards while CNVS owns them. T3-hosted threads are identified automatically. For controls, enable + cards while CNVS owns them. OpenCode uses its official global plugin + API for lifecycle and interrupt. T3-hosted threads are identified automatically. For controls, enable Network access in T3 Code Settings → Connections, create a link under Authorized clients, then paste it below. Factory hooks are merged without replacing your existing hooks. @@ -598,8 +622,27 @@ export function Settings({ {adapterMessage}
)} -Update checks run only when requested or explicitly enabled. A - paired T3 Code adapter contacts only its approved environment. + paired T3 Code integration contacts only its approved environment.