From 8007368941d2bd4da882d0d07ee00f4db3013c89 Mon Sep 17 00:00:00 2001 From: Jonathan Borgwing Date: Tue, 21 Jul 2026 12:16:49 -0400 Subject: [PATCH] feat: integration expansion, auto-discovery engine, embedded MCP server, adapter SDK, and v0.3.7 release bump --- Cargo.lock | 10 +- Cargo.toml | 2 +- adapters/cursor/hooks/event.mjs | 1 + adapters/sdk/index.mjs | 105 ++++++ adapters/sdk/package.json | 19 ++ apps/microbridge-ui/package.json | 2 +- apps/microbridge-ui/src-tauri/Cargo.lock | 181 +++++----- apps/microbridge-ui/src-tauri/tauri.conf.json | 2 +- .../src/components/IntegrationCard.tsx | 77 ++--- apps/microbridge-ui/src/lib/hosts.test.ts | 34 +- apps/microbridge-ui/src/lib/hosts.ts | 11 +- .../src/lib/integrationSetup.test.ts | 23 ++ .../src/lib/integrationSetup.ts | 15 + apps/microbridge-ui/src/lib/openHostApp.ts | 25 ++ apps/microbridge-ui/src/surfaces/Settings.tsx | 171 +++++++--- .../src/surfaces/surfaces.test.tsx | 72 +++- crates/mb-adapters/src/claude.rs | 1 + crates/mb-adapters/src/codex.rs | 1 + crates/mb-protocol/src/lib.rs | 13 + crates/microbridgectl/src/main.rs | 2 + crates/microbridged/src/auto_discover.rs | 160 +++++++++ crates/microbridged/src/cnvs.rs | 1 + crates/microbridged/src/key_source.rs | 1 + crates/microbridged/src/lib.rs | 2 + crates/microbridged/src/main.rs | 2 + crates/microbridged/src/mcp.rs | 322 ++++++++++++++++++ crates/microbridged/src/registry.rs | 1 + crates/microbridged/src/state.rs | 33 +- crates/microbridged/src/t3code.rs | 2 + 29 files changed, 1085 insertions(+), 206 deletions(-) create mode 100644 adapters/sdk/index.mjs create mode 100644 adapters/sdk/package.json create mode 100644 apps/microbridge-ui/src/lib/integrationSetup.test.ts create mode 100644 apps/microbridge-ui/src/lib/integrationSetup.ts create mode 100644 apps/microbridge-ui/src/lib/openHostApp.ts create mode 100644 crates/microbridged/src/auto_discover.rs create mode 100644 crates/microbridged/src/mcp.rs diff --git a/Cargo.lock b/Cargo.lock index 1be81d5..ec8ed3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,7 +616,7 @@ dependencies = [ [[package]] name = "mb-adapters" -version = "0.3.6" +version = "0.3.7" dependencies = [ "mb-protocol", "notify", @@ -628,7 +628,7 @@ dependencies = [ [[package]] name = "mb-device" -version = "0.3.6" +version = "0.3.7" dependencies = [ "hidapi", "mb-protocol", @@ -639,7 +639,7 @@ dependencies = [ [[package]] name = "mb-protocol" -version = "0.3.6" +version = "0.3.7" dependencies = [ "serde", "serde_json", @@ -653,7 +653,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "microbridgectl" -version = "0.3.6" +version = "0.3.7" dependencies = [ "mb-device", "mb-protocol", @@ -663,7 +663,7 @@ dependencies = [ [[package]] name = "microbridged" -version = "0.3.6" +version = "0.3.7" dependencies = [ "keyring", "mb-adapters", diff --git a/Cargo.toml b/Cargo.toml index e24cfa0..f6de07d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.3.6" +version = "0.3.7" edition = "2021" license = "MIT" repository = "https://github.com/DevVig/microbridge" diff --git a/adapters/cursor/hooks/event.mjs b/adapters/cursor/hooks/event.mjs index 94d306b..5f4216c 100644 --- a/adapters/cursor/hooks/event.mjs +++ b/adapters/cursor/hooks/event.mjs @@ -59,6 +59,7 @@ export function lifecycleMessages(event, now = Date.now()) { title: event.title, state, updated_at_ms: now, + focus_uri: event.workspace ? `cursor://file${event.workspace}` : null, }, ttl_ms: event.lifecycle === "session_end" ? 1_000 : 30 * 60 * 1_000, }, diff --git a/adapters/sdk/index.mjs b/adapters/sdk/index.mjs new file mode 100644 index 0000000..0e18767 --- /dev/null +++ b/adapters/sdk/index.mjs @@ -0,0 +1,105 @@ +// @microbridge/adapter-sdk +// Zero-dependency Node.js/JS SDK for building Microbridge adapters. + +import net from "node:net"; +import os from "node:os"; +import path from "node:path"; + +export class MicrobridgeAdapter { + constructor({ id, version = "1.0.0", capabilities = {} }) { + this.id = id; + this.version = version; + this.capabilities = { + lifecycle_observation: true, + approval_acceptance: false, + approval_rejection: false, + interrupt: false, + new_session: false, + focus_open: false, + reasoning_effort: false, + tty_control: false, + mcp_native: false, + uri_focus: false, + ...capabilities, + }; + + this.socketPath = + process.env.MICROBRIDGE_SOCKET || + path.join(os.homedir(), ".microbridge", "microbridged.sock"); + this.socket = null; + this.actionListeners = new Map(); + } + + connect() { + return new Promise((resolve, reject) => { + this.socket = net.createConnection(this.socketPath, () => { + const hello = { + type: "hello", + adapter: this.id, + protocol_version: 0, + adapter_version: this.version, + capabilities: this.capabilities, + }; + this.socket.write(`${JSON.stringify(hello)}\n`); + resolve(true); + }); + + this.socket.on("error", (err) => { + reject(err); + }); + + let buffer = ""; + this.socket.on("data", (chunk) => { + buffer += chunk.toString("utf8"); + const lines = buffer.split("\n"); + buffer = lines.pop(); + for (const line of lines) { + if (!line.trim()) continue; + try { + const msg = JSON.parse(line); + if (msg.type === "action") { + const handler = this.actionListeners.get(msg.action); + if (handler) handler(msg.session_id); + } + } catch (_) {} + } + }); + }); + } + + reportStatus({ id, app, title = "", state, focusUri }) { + if (!this.socket || !this.socket.writable) return; + const msg = { + type: "status", + session: { + id: `${this.id}:${id}`, + app: app || this.id, + title, + state, + updated_at_ms: Date.now(), + focus_uri: focusUri || null, + }, + }; + this.socket.write(`${JSON.stringify(msg)}\n`); + } + + reportBye(id) { + if (!this.socket || !this.socket.writable) return; + const msg = { + type: "bye", + session_id: `${this.id}:${id}`, + }; + this.socket.write(`${JSON.stringify(msg)}\n`); + } + + onAction(actionName, callback) { + this.actionListeners.set(actionName, callback); + } + + disconnect() { + if (this.socket) { + this.socket.end(); + this.socket = null; + } + } +} diff --git a/adapters/sdk/package.json b/adapters/sdk/package.json new file mode 100644 index 0000000..aec6bc8 --- /dev/null +++ b/adapters/sdk/package.json @@ -0,0 +1,19 @@ +{ + "name": "@microbridge/adapter-sdk", + "version": "0.3.7", + "description": "Zero-dependency SDK for publishing AI agent session states to Microbridge", + "main": "index.mjs", + "type": "module", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/DevVig/microbridge.git" + }, + "keywords": [ + "microbridge", + "agent", + "macropad", + "codex-micro", + "adapter" + ] +} diff --git a/apps/microbridge-ui/package.json b/apps/microbridge-ui/package.json index 2dec727..5e74d9b 100644 --- a/apps/microbridge-ui/package.json +++ b/apps/microbridge-ui/package.json @@ -1,7 +1,7 @@ { "name": "microbridge-ui", "private": true, - "version": "0.3.6", + "version": "0.3.7", "type": "module", "scripts": { "dev": "vite", diff --git a/apps/microbridge-ui/src-tauri/Cargo.lock b/apps/microbridge-ui/src-tauri/Cargo.lock index f2472a6..0f85993 100644 --- a/apps/microbridge-ui/src-tauri/Cargo.lock +++ b/apps/microbridge-ui/src-tauri/Cargo.lock @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" @@ -200,9 +200,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.1" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -273,7 +273,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -288,9 +288,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "shlex", @@ -790,9 +790,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fdeflate" @@ -889,24 +889,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -915,15 +915,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", @@ -932,21 +932,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-io", @@ -1182,9 +1182,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "gobject-sys" @@ -1330,9 +1330,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -1643,7 +1643,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link 0.2.1", ] @@ -1759,9 +1759,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.186" +version = "0.2.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "22053b6a34f84abc97f9129e61334f40174659a1b9bd18c970b83db6a9a6348b" [[package]] name = "libdbus-sys" @@ -1831,7 +1831,7 @@ dependencies = [ [[package]] name = "mb-protocol" -version = "0.3.6" +version = "0.3.7" dependencies = [ "serde", ] @@ -1918,7 +1918,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "windows-sys 0.61.2", ] @@ -2247,7 +2247,7 @@ dependencies = [ "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -2484,9 +2484,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -2502,9 +2502,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -2555,27 +2555,27 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.2", ] [[package]] @@ -2920,9 +2920,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -2942,22 +2942,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.2", ] [[package]] @@ -2973,9 +2973,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -2986,13 +2986,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.2", ] [[package]] @@ -3307,6 +3307,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3449,7 +3460,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tray-icon", "url", @@ -3500,7 +3511,7 @@ dependencies = [ "sha2", "syn 2.0.119", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "url", "uuid", @@ -3548,14 +3559,14 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "tauri-plugin-dialog" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" dependencies = [ "log", "raw-window-handle", @@ -3565,7 +3576,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", ] @@ -3588,7 +3599,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml 1.1.3+spec-1.1.0", "url", ] @@ -3620,7 +3631,7 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", ] @@ -3649,7 +3660,7 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tokio", "url", @@ -3675,7 +3686,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", "webkit2gtk", "webview2-com", @@ -3738,7 +3749,7 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml 1.1.3+spec-1.1.0", "url", "urlpattern", @@ -3790,11 +3801,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -3810,20 +3821,20 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.2", ] [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "num-conv", @@ -3841,9 +3852,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -3876,9 +3887,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -3912,9 +3923,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", @@ -4125,7 +4136,7 @@ dependencies = [ "once_cell", "png 0.18.1", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "windows-sys 0.61.2", ] @@ -4489,7 +4500,7 @@ version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", "windows", "windows-core 0.61.2", ] @@ -5016,7 +5027,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", "webkit2gtk", "webkit2gtk-sys", diff --git a/apps/microbridge-ui/src-tauri/tauri.conf.json b/apps/microbridge-ui/src-tauri/tauri.conf.json index 9218846..7653026 100644 --- a/apps/microbridge-ui/src-tauri/tauri.conf.json +++ b/apps/microbridge-ui/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Microbridge", - "version": "0.3.6", + "version": "0.3.7", "identifier": "ai.microbridge.ui", "build": { "beforeDevCommand": "npm run dev", diff --git a/apps/microbridge-ui/src/components/IntegrationCard.tsx b/apps/microbridge-ui/src/components/IntegrationCard.tsx index 56c0742..f8e07de 100644 --- a/apps/microbridge-ui/src/components/IntegrationCard.tsx +++ b/apps/microbridge-ui/src/components/IntegrationCard.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import { forwardRef, type ReactNode } from "react"; import type { ThemeTokens } from "../lib/theme"; import { TRAFFIC_COLORS, @@ -12,6 +12,7 @@ function TileFace({ light, label, theme, + busy, }: { name: string; iconSrc?: string; @@ -19,6 +20,7 @@ function TileFace({ light: TrafficLight; label: string; theme: ThemeTokens; + busy?: boolean; }) { const colors = TRAFFIC_COLORS[light]; return ( @@ -56,7 +58,7 @@ function TileFace({ className="mt-0.5 truncate text-[9.5px] font-medium leading-tight" style={{ color: colors.fg }} > - {label} + {busy ? "Installing…" : label}
void; }) { const colors = TRAFFIC_COLORS[light]; const shellStyle = { - backgroundColor: theme.panel, - border: `1px solid ${expanded ? colors.dot : theme.hairline}`, - boxShadow: expanded ? `0 0 0 1px ${colors.dot}33` : undefined, + backgroundColor: expanded ? `${colors.dot}14` : theme.panel, + border: `1.5px solid ${expanded ? colors.dot : theme.hairline}`, + boxShadow: expanded ? `0 0 0 2px ${colors.dot}28` : undefined, color: theme.text, + opacity: busy ? 0.72 : 1, } as const; const face = ( @@ -111,55 +116,43 @@ export function IntegrationCard({ light={light} label={label} theme={theme} + busy={busy} /> ); const className = - "group relative flex h-[72px] w-full flex-col items-stretch justify-between rounded-xl px-2 py-1.5 text-left transition-colors"; + "group relative flex h-[72px] w-full cursor-pointer flex-col items-stretch justify-between rounded-xl px-2 py-1.5 text-left transition-[background-color,border-color,box-shadow,opacity]"; return (
  • - {onSelect ? ( - - ) : ( -
    - {face} -
    - )} +
  • ); } -export function IntegrationDetail({ - name, - iconSrc, - diagnostic, - theme, - children, -}: { - name: string; - iconSrc?: string; - diagnostic: string; - theme: ThemeTokens; - children?: ReactNode; -}) { +export const IntegrationDetail = forwardRef< + HTMLDivElement, + { + name: string; + iconSrc?: string; + diagnostic: string; + theme: ThemeTokens; + children?: ReactNode; + } +>(function IntegrationDetail({ name, iconSrc, diagnostic, theme, children }, ref) { return (
    ); -} +}); diff --git a/apps/microbridge-ui/src/lib/hosts.test.ts b/apps/microbridge-ui/src/lib/hosts.test.ts index cf70ad5..85a0e2a 100644 --- a/apps/microbridge-ui/src/lib/hosts.test.ts +++ b/apps/microbridge-ui/src/lib/hosts.test.ts @@ -54,13 +54,13 @@ describe("integrationView", () => { expect(view.diagnostic).toContain("no separate adapter"); }); - it("marks Synara yellow while waiting for sessions", () => { + it("marks Synara idle when there are no sessions", () => { const view = integrationView( adapter({ id: "synara", display_name: "Synara", state: "connected" }), [], ); expect(view.light).toBe("yellow"); - expect(view.label).toBe("Waiting"); + expect(view.label).toBe("Idle"); expect(view.connectedGroup).toBe(false); }); @@ -88,6 +88,36 @@ describe("integrationView", () => { expect(view.label).toBe("Connected"); }); + it("puts limited adapters in the Connected group", () => { + const view = integrationView( + adapter({ + id: "cursor", + display_name: "Cursor", + kind: "community", + state: "limited", + diagnostic: "Lifecycle is connected; unsupported IDE commands remain disabled.", + }), + [], + ); + expect(view.label).toBe("Limited"); + expect(view.connectedGroup).toBe(true); + }); + + it("labels needs_setup as Setup needed", () => { + const view = integrationView( + adapter({ + id: "opencode", + display_name: "OpenCode", + kind: "community", + state: "needs_setup", + diagnostic: "The bundled OpenCode integration is installed.", + }), + [], + ); + expect(view.label).toBe("Setup needed"); + expect(view.connectedGroup).toBe(false); + }); + it("maps adapter errors to red", () => { const view = integrationView( adapter({ diff --git a/apps/microbridge-ui/src/lib/hosts.ts b/apps/microbridge-ui/src/lib/hosts.ts index e46d29b..32ce1d3 100644 --- a/apps/microbridge-ui/src/lib/hosts.ts +++ b/apps/microbridge-ui/src/lib/hosts.ts @@ -70,7 +70,7 @@ export interface IntegrationView { const STATE_LABELS: Record = { disabled: "Not connected", - needs_setup: "Waiting", + needs_setup: "Setup needed", connecting: "Connecting", connected: "Connected", limited: "Limited", @@ -84,6 +84,11 @@ function lightForState(state: AdapterConnectionState): TrafficLight { return "yellow"; } +/** Live links belong in Connected — including partial (limited) capability. */ +function connectedGroupForState(state: AdapterConnectionState): boolean { + return state === "connected" || state === "limited"; +} + /** * Derive the card's traffic light, label, and diagnostic from daemon adapter * state plus live session attribution. @@ -128,7 +133,7 @@ export function integrationView( } return { light: "yellow", - label: "Waiting", + label: "Idle", diagnostic: "via Claude & Codex journals — no separate adapter needed. Waiting for sessions.", connectedGroup: false, @@ -157,7 +162,7 @@ export function integrationView( light, label: STATE_LABELS[adapter.state], diagnostic: adapter.diagnostic, - connectedGroup: light === "green", + connectedGroup: connectedGroupForState(adapter.state), }; } diff --git a/apps/microbridge-ui/src/lib/integrationSetup.test.ts b/apps/microbridge-ui/src/lib/integrationSetup.test.ts new file mode 100644 index 0000000..403b6af --- /dev/null +++ b/apps/microbridge-ui/src/lib/integrationSetup.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { setupNextStep } from "./integrationSetup"; +import { openableHostApp } from "./openHostApp"; + +describe("setupNextStep", () => { + it("returns host-specific checklist copy", () => { + expect(setupNextStep("cursor")).toContain("Reload Cursor"); + expect(setupNextStep("opencode")).toContain("Restart OpenCode"); + expect(setupNextStep("factory")).toContain("Droid"); + expect(setupNextStep("t3code")).toContain("pairing link"); + expect(setupNextStep("synara")).toBeNull(); + }); +}); + +describe("openableHostApp", () => { + it("names apps we can open from Integrations", () => { + expect(openableHostApp("cursor")).toBe("Cursor"); + expect(openableHostApp("opencode")).toBe("OpenCode"); + expect(openableHostApp("t3code")).toBe("T3 Code"); + expect(openableHostApp("factory")).toBeNull(); + }); +}); diff --git a/apps/microbridge-ui/src/lib/integrationSetup.ts b/apps/microbridge-ui/src/lib/integrationSetup.ts new file mode 100644 index 0000000..8ce77b3 --- /dev/null +++ b/apps/microbridge-ui/src/lib/integrationSetup.ts @@ -0,0 +1,15 @@ +/** Short host checklist after install / while needs_setup. */ +export function setupNextStep(adapterId: string): string | null { + switch (adapterId) { + case "cursor": + return "Reload Cursor’s window (or quit and reopen Cursor) so the bundled hooks load."; + case "opencode": + return "Restart OpenCode (CLI or app) so the Microbridge plugin loads."; + case "factory": + return "Start or continue a Factory Droid session — lifecycle events connect automatically."; + case "t3code": + return "In T3 Code → Settings → Connections, enable Network access, then paste a one-time pairing link below."; + default: + return null; + } +} diff --git a/apps/microbridge-ui/src/lib/openHostApp.ts b/apps/microbridge-ui/src/lib/openHostApp.ts new file mode 100644 index 0000000..0085167 --- /dev/null +++ b/apps/microbridge-ui/src/lib/openHostApp.ts @@ -0,0 +1,25 @@ +import { hasTauri } from "./tauri"; + +/** Best-effort /Applications bundle names for Integrations “Open …” buttons. */ +const HOST_APP_BUNDLE: Record = { + cursor: "Cursor.app", + opencode: "OpenCode.app", + t3code: "T3 Code.app", +}; + +export function openableHostApp(adapterId: string): string | null { + const bundle = HOST_APP_BUNDLE[adapterId]; + return bundle ? bundle.replace(/\.app$/, "") : null; +} + +/** Open a macOS app bundle. Ignores errors (missing app, browser preview). */ +export async function openHostApp(adapterId: string): Promise { + const bundle = HOST_APP_BUNDLE[adapterId]; + if (!bundle || !hasTauri()) return; + try { + const { open } = await import("@tauri-apps/plugin-shell"); + await open(`/Applications/${bundle}`); + } catch { + /* host app missing or shell unavailable — detail checklist still guides the user */ + } +} diff --git a/apps/microbridge-ui/src/surfaces/Settings.tsx b/apps/microbridge-ui/src/surfaces/Settings.tsx index 1a13a4a..cb07c2c 100644 --- a/apps/microbridge-ui/src/surfaces/Settings.tsx +++ b/apps/microbridge-ui/src/surfaces/Settings.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import type { AdapterCapabilities, DaemonConfig, Snapshot, StateColors } from "../lib/types"; import { CODEX_PALETTE, @@ -36,6 +36,8 @@ import { isHostAttributed, } from "../lib/hosts"; import { integrationIcon } from "../lib/integrationIcons"; +import { openHostApp, openableHostApp } from "../lib/openHostApp"; +import { setupNextStep } from "../lib/integrationSetup"; const LIGHTING_STATES: { id: keyof StateColors; label: string }[] = [ { id: "idle", label: "Idle" }, @@ -137,11 +139,23 @@ export function Settings({ const [selectedIntegration, setSelectedIntegration] = useState( null, ); + const pairingInputRef = useRef(null); + const integrationDetailRef = useRef(null); // null until the login item has been read, and permanently null where a login // item is meaningless: outside Tauri, or in a dev build whose executable path // points into `target/debug`. const [atLogin, setAtLogin] = useState(null); + const selectIntegration = (adapterId: string) => { + setSelectedIntegration(adapterId); + requestAnimationFrame(() => { + integrationDetailRef.current?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + }); + }; + useEffect(() => { void appVersion().then(setVersion); void updateChannel().then(setChannel); @@ -613,38 +627,39 @@ export function Settings({ .map((adapter) => ({ adapter, view: integrationView(adapter, snapshot.sessions), - actionable: + optIn: adapter.kind === "community" && !isHostAttributed(adapter.id), })); - const groups = [ + const connected = views.filter((item) => item.view.connectedGroup); + const notConnected = views.filter((item) => !item.view.connectedGroup); + const ordered = [ { label: "Connected", light: "green" as const, - items: views.filter((item) => item.view.connectedGroup), + items: connected, }, { label: "Not connected", light: "yellow" as const, - items: views.filter((item) => !item.view.connectedGroup), + items: notConnected, }, ]; - const selectable = views.filter((item) => item.actionable); - const activeId = - selectedIntegration && - selectable.some((item) => item.adapter.id === selectedIntegration) - ? selectedIntegration - : (selectable.find((item) => !item.view.connectedGroup)?.adapter - .id ?? - selectable[0]?.adapter.id ?? - null); + const activeId = selectedIntegration; const selected = views.find((item) => item.adapter.id === activeId); + const nextStep = selected + ? setupNextStep(selected.adapter.id) + : null; + const openAppLabel = selected + ? openableHostApp(selected.adapter.id) + : null; return (

    Integrations

    - Compact tiles with each app's icon. Status color is always - visible; hover for detail. Click Cursor, Factory, T3 Code, or - OpenCode to enable or repair. + Click any tile for details. Cursor, Factory, T3 Code, and OpenCode + install on first click when they're off; they turn green or + Limited only after that host talks to Microbridge (reload / + restart / pair).

    Synara and the desktop apps share Claude/Codex journals — no @@ -656,44 +671,95 @@ export function Settings({ {adapterMessage}

    )} - {groups.map((group) => ( -
    -
    - - {group.label} · {group.items.length} +
    + {ordered.map((group) => ( +
    +
    + + {group.label} · {group.items.length} +
    + {group.items.map(({ adapter, view, optIn }) => ( + { + selectIntegration(adapter.id); + if (optIn && adapter.state === "disabled") { + requestAnimationFrame(() => { + void runAdapterOperation(adapter.id, () => + setAdapterEnabled(adapter.id, true), + ); + }); + return; + } + if ( + adapter.id === "t3code" && + adapter.state === "needs_setup" + ) { + queueMicrotask(() => + pairingInputRef.current?.focus(), + ); + } + }} + /> + ))}
    -
      - {group.items.map(({ adapter, view, actionable }) => ( - - setSelectedIntegration((current) => - current === adapter.id ? current : adapter.id, - ) - : undefined - } - /> ))} -
    -
    - ))} +
    + {!selected && ( +

    + Select a tile for details. +

    + )} {selected && ( + {(selected.adapter.state === "needs_setup" || + selected.adapter.state === "disabled") && + nextStep && ( +
    +
    + {selected.adapter.state === "disabled" + ? "Do this next (after install)" + : "Do this next"} +
    +

    {nextStep}

    + {selected.adapter.state === "needs_setup" && ( +

    + {selected.view.diagnostic} +

    + )} +
    + )}
    {CAPABILITIES.map((capability) => ( setPairingUrl(event.target.value)} @@ -735,7 +802,17 @@ export function Settings({
    )} -
    +
    + {openAppLabel && ( + + )} {selected.adapter.kind === "community" && !cfg.adapters[selected.adapter.id]?.enabled && (