From bd61813742a2dbe809582b6ca36b2f468037179d Mon Sep 17 00:00:00 2001 From: Jonathan Borgwing Date: Fri, 17 Jul 2026 13:33:29 -0400 Subject: [PATCH 1/2] feat: ship daemon bus, install path, companion UI, and release CI Bring Microbridge from protocol skeleton to an installable alpha: UI/control protocol, key-source policy, Codex/Claude adapters, microbridgectl, Tauri companion, launchd/systemd install scripts, and GitHub release artifacts. Co-authored-by: Cursor --- .github/ISSUE_TEMPLATE/config.yml | 8 + .github/dependabot.yml | 16 + .github/workflows/ci.yml | 16 + .github/workflows/release.yml | 73 + .gitignore | 4 + CONTRIBUTING.md | 19 +- Cargo.lock | 708 +++++ Cargo.toml | 13 +- Formula/microbridge.rb | 40 + INSTALL.md | 111 + Makefile | 28 + README.md | 50 +- ROADMAP.md | 33 +- adapters/README.md | 7 +- adapters/cursor/README.md | 28 + adapters/cursor/index.mjs | 38 + adapters/t3code/README.md | 27 + adapters/t3code/index.mjs | 37 + apps/microbridge-ui/README.md | 36 + apps/microbridge-ui/index.html | 18 + apps/microbridge-ui/package-lock.json | 2747 +++++++++++++++++ apps/microbridge-ui/package.json | 28 + apps/microbridge-ui/src-tauri/Cargo.toml | 23 + apps/microbridge-ui/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 7 + apps/microbridge-ui/src-tauri/icons/icon.png | Bin 0 -> 102 bytes apps/microbridge-ui/src-tauri/src/lib.rs | 140 + apps/microbridge-ui/src-tauri/src/main.rs | 5 + apps/microbridge-ui/src-tauri/tauri.conf.json | 34 + apps/microbridge-ui/src/App.tsx | 112 + apps/microbridge-ui/src/index.css | 48 + apps/microbridge-ui/src/lib/bus.ts | 75 + apps/microbridge-ui/src/lib/theme.ts | 51 + apps/microbridge-ui/src/lib/types.ts | 74 + apps/microbridge-ui/src/main.tsx | 10 + apps/microbridge-ui/src/surfaces/Hud.tsx | 122 + apps/microbridge-ui/src/surfaces/Popover.tsx | 286 ++ apps/microbridge-ui/src/surfaces/Settings.tsx | 357 +++ apps/microbridge-ui/src/vite-env.d.ts | 1 + apps/microbridge-ui/tsconfig.json | 21 + apps/microbridge-ui/tsconfig.node.json | 10 + .../vendor/magicpath/hud/FocusHUD.tsx | 170 + .../vendor/magicpath/popover/AgentKeyEcho.tsx | 142 + .../vendor/magicpath/popover/AgentRow.tsx | 36 + .../vendor/magicpath/popover/FocusCard.tsx | 69 + .../magicpath/popover/MenuBarPopover.tsx | 148 + .../magicpath/popover/microbridge-types.ts | 78 + .../settings/generated/ActionPicker.tsx | 87 + .../settings/generated/AdaptersTab.tsx | 79 + .../settings/generated/DeviceKeys.tsx | 344 +++ .../settings/generated/DeviceTab.tsx | 243 ++ .../magicpath/settings/generated/FocusTab.tsx | 239 ++ .../magicpath/settings/generated/KeysTab.tsx | 250 ++ .../generated/SettingsKeysAndFocus.tsx | 153 + .../magicpath/settings/generated/Toggle.tsx | 25 + .../magicpath/settings/generated/bits.tsx | 97 + .../settings/generated/microbridge-data.ts | 235 ++ .../magicpath/settings/generated/theme.tsx | 95 + apps/microbridge-ui/vite.config.ts | 17 + crates/mb-adapters/Cargo.toml | 15 + crates/mb-adapters/src/claude.rs | 127 + crates/mb-adapters/src/codex.rs | 144 + crates/mb-adapters/src/lib.rs | 23 + crates/mb-adapters/src/watch.rs | 113 + crates/mb-device/src/lib.rs | 190 +- crates/mb-protocol/src/lib.rs | 258 +- crates/microbridgectl/Cargo.toml | 16 + crates/microbridgectl/src/main.rs | 92 + crates/microbridged/Cargo.toml | 11 + crates/microbridged/src/config.rs | 88 + crates/microbridged/src/key_source.rs | 177 ++ crates/microbridged/src/lib.rs | 11 + crates/microbridged/src/main.rs | 213 +- crates/microbridged/src/registry.rs | 174 ++ crates/microbridged/src/socket.rs | 147 + crates/microbridged/src/state.rs | 185 ++ docs/architecture.md | 4 +- docs/design/README.md | 194 +- docs/device-hid.md | 47 + docs/protocol.md | 84 +- scripts/com.ai.microbridge.daemon.plist | 17 + scripts/install-from-release.sh | 105 + scripts/install-launchd.sh | 5 + scripts/install-linux-systemd.sh | 15 + scripts/install.sh | 154 + scripts/microbridge.service | 13 + scripts/uninstall.sh | 48 + 87 files changed, 10330 insertions(+), 311 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/release.yml create mode 100644 Cargo.lock create mode 100644 Formula/microbridge.rb create mode 100644 INSTALL.md create mode 100644 Makefile create mode 100644 adapters/cursor/README.md create mode 100755 adapters/cursor/index.mjs create mode 100644 adapters/t3code/README.md create mode 100755 adapters/t3code/index.mjs create mode 100644 apps/microbridge-ui/README.md create mode 100644 apps/microbridge-ui/index.html create mode 100644 apps/microbridge-ui/package-lock.json create mode 100644 apps/microbridge-ui/package.json create mode 100644 apps/microbridge-ui/src-tauri/Cargo.toml create mode 100644 apps/microbridge-ui/src-tauri/build.rs create mode 100644 apps/microbridge-ui/src-tauri/capabilities/default.json create mode 100644 apps/microbridge-ui/src-tauri/icons/icon.png create mode 100644 apps/microbridge-ui/src-tauri/src/lib.rs create mode 100644 apps/microbridge-ui/src-tauri/src/main.rs create mode 100644 apps/microbridge-ui/src-tauri/tauri.conf.json create mode 100644 apps/microbridge-ui/src/App.tsx create mode 100644 apps/microbridge-ui/src/index.css create mode 100644 apps/microbridge-ui/src/lib/bus.ts create mode 100644 apps/microbridge-ui/src/lib/theme.ts create mode 100644 apps/microbridge-ui/src/lib/types.ts create mode 100644 apps/microbridge-ui/src/main.tsx create mode 100644 apps/microbridge-ui/src/surfaces/Hud.tsx create mode 100644 apps/microbridge-ui/src/surfaces/Popover.tsx create mode 100644 apps/microbridge-ui/src/surfaces/Settings.tsx create mode 100644 apps/microbridge-ui/src/vite-env.d.ts create mode 100644 apps/microbridge-ui/tsconfig.json create mode 100644 apps/microbridge-ui/tsconfig.node.json create mode 100644 apps/microbridge-ui/vendor/magicpath/hud/FocusHUD.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/popover/AgentKeyEcho.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/popover/AgentRow.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/popover/FocusCard.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/popover/MenuBarPopover.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/popover/microbridge-types.ts create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/ActionPicker.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/AdaptersTab.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/DeviceKeys.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/DeviceTab.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/FocusTab.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/KeysTab.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/SettingsKeysAndFocus.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/Toggle.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/bits.tsx create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/microbridge-data.ts create mode 100644 apps/microbridge-ui/vendor/magicpath/settings/generated/theme.tsx create mode 100644 apps/microbridge-ui/vite.config.ts create mode 100644 crates/mb-adapters/Cargo.toml create mode 100644 crates/mb-adapters/src/claude.rs create mode 100644 crates/mb-adapters/src/codex.rs create mode 100644 crates/mb-adapters/src/lib.rs create mode 100644 crates/mb-adapters/src/watch.rs create mode 100644 crates/microbridgectl/Cargo.toml create mode 100644 crates/microbridgectl/src/main.rs create mode 100644 crates/microbridged/src/config.rs create mode 100644 crates/microbridged/src/key_source.rs create mode 100644 crates/microbridged/src/lib.rs create mode 100644 crates/microbridged/src/registry.rs create mode 100644 crates/microbridged/src/socket.rs create mode 100644 crates/microbridged/src/state.rs create mode 100644 docs/device-hid.md create mode 100644 scripts/com.ai.microbridge.daemon.plist create mode 100755 scripts/install-from-release.sh create mode 100755 scripts/install-launchd.sh create mode 100755 scripts/install-linux-systemd.sh create mode 100755 scripts/install.sh create mode 100644 scripts/microbridge.service create mode 100755 scripts/uninstall.sh diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..15ac1c2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Adapter guide + url: https://github.com/DevVig/microbridge/blob/main/docs/adapters.md + about: How to write a community adapter before opening an issue + - name: Install help + url: https://github.com/DevVig/microbridge/blob/main/INSTALL.md + about: Installation, launchd, and uninstall steps diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d7f6226 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 5 + - package-ecosystem: npm + directory: "/apps/microbridge-ui" + schedule: + interval: monthly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c73d1e..1e49b94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,19 @@ jobs: - run: cargo fmt --all --check - run: cargo clippy --workspace --all-targets -- -D warnings - run: cargo test --workspace + + ui: + name: ui + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/microbridge-ui + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: apps/microbridge-ui/package-lock.json + - run: npm ci + - run: npm run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0af9174 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,73 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + build: + name: build (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + target: aarch64-apple-darwin + - os: macos-13 + target: x86_64-apple-darwin + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + - name: Build + run: | + cargo build --release -p microbridged -p microbridgectl --target ${{ matrix.target }} + - name: Package + run: | + STAGE=microbridge-${{ github.ref_name }}-${{ matrix.target }} + mkdir -p "staging/${STAGE}" + cp "target/${{ matrix.target }}/release/microbridged" "staging/${STAGE}/" + cp "target/${{ matrix.target }}/release/microbridgectl" "staging/${STAGE}/" + cp README.md LICENSE-MIT LICENSE-APACHE INSTALL.md "staging/${STAGE}/" + tar -C staging -czf "${STAGE}.tar.gz" "${STAGE}" + echo "ASSET=${STAGE}.tar.gz" >> "$GITHUB_ENV" + - uses: actions/upload-artifact@v4 + with: + name: binaries-${{ matrix.target }} + path: ${{ env.ASSET }} + + publish: + name: publish release + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Collect assets + run: | + mkdir -p release-assets + find artifacts -name '*.tar.gz' -exec cp {} release-assets/ \; + ls -la release-assets + - uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: release-assets/* + body: | + ## Install + + ```sh + ./scripts/install-from-release.sh ${{ github.ref_name }} + ``` + + Or from source: see [INSTALL.md](INSTALL.md). diff --git a/.gitignore b/.gitignore index ac21959..92dcb97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ /target node_modules/ .DS_Store +.magicpath-work/ +apps/microbridge-ui/dist/ +apps/microbridge-ui/src-tauri/target/ +apps/microbridge-ui/src-tauri/gen/ # local working notes (not part of the public project) /documentation/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 708cae7..9b3ea48 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,15 +24,32 @@ cargo run -p microbridged node adapters/reference-echo/index.mjs # exercise the daemon end-to-end ``` +End-user install paths are documented in [INSTALL.md](INSTALL.md) +(`./scripts/install.sh`, uninstall, releases). + ## Before you push ```sh cargo fmt --all cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace +# optional UI: +cd apps/microbridge-ui && npm ci && npm run build +``` + +Or `make ci`. CI enforces Rust checks on macOS/Linux and the UI build on Ubuntu. + +## Releases + +Push a version tag to publish binaries via GitHub Actions: + +```sh +git tag v0.0.1 +git push origin v0.0.1 ``` -CI enforces all three on macOS and Linux. +Assets are attached to the GitHub Release; users can run +`./scripts/install-from-release.sh v0.0.1`. ## Commits and PRs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e1e642b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,708 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.1", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "mb-adapters" +version = "0.0.1" +dependencies = [ + "mb-protocol", + "notify", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "mb-device" +version = "0.0.1" +dependencies = [ + "mb-protocol", + "tracing", +] + +[[package]] +name = "mb-protocol" +version = "0.0.1" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "microbridgectl" +version = "0.0.1" +dependencies = [ + "mb-protocol", + "serde_json", + "tokio", +] + +[[package]] +name = "microbridged" +version = "0.0.1" +dependencies = [ + "mb-adapters", + "mb-device", + "mb-protocol", + "serde", + "serde_json", + "tokio", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.13.1", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 90886d5..76b7fe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,12 @@ [workspace] resolver = "2" -members = ["crates/mb-protocol", "crates/mb-device", "crates/microbridged"] +members = [ + "crates/mb-protocol", + "crates/mb-device", + "crates/mb-adapters", + "crates/microbridged", + "crates/microbridgectl", +] [workspace.package] version = "0.0.1" @@ -11,9 +17,12 @@ repository = "https://github.com/DevVig/microbridge" [workspace.dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "sync"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time", "fs", "signal"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +toml = "0.8" +notify = "7" +thiserror = "2" [profile.release] strip = true diff --git a/Formula/microbridge.rb b/Formula/microbridge.rb new file mode 100644 index 0000000..6b32ce0 --- /dev/null +++ b/Formula/microbridge.rb @@ -0,0 +1,40 @@ +# Homebrew formula (build-from-source until a tap/bottle is published). +# +# brew install --build-from-source ./Formula/microbridge.rb +# brew services start microbridge +# +class Microbridge < Formula + desc "Open-source control plane for the Codex Micro" + homepage "https://github.com/DevVig/microbridge" + license any_of: ["MIT", "Apache-2.0"] + head "https://github.com/DevVig/microbridge.git", branch: "main" + + depends_on "rust" => :build + + def install + system "cargo", "build", "--release", "--locked", "-p", "microbridged", "-p", "microbridgectl" + bin.install "target/release/microbridged" + bin.install "target/release/microbridgectl" + doc.install "INSTALL.md" if File.exist?("INSTALL.md") + end + + service do + run [opt_bin/"microbridged"] + keep_alive true + log_path var/"log/microbridge.log" + error_log_path var/"log/microbridge.log" + environment_variables RUST_LOG: "info" + end + + def caveats + <<~EOS + Config and socket live in ~/.microbridge/ + Check the bus with: microbridgectl status + Full install notes: #{doc}/INSTALL.md (or INSTALL.md in the repo) + EOS + end + + test do + assert_match "Usage", shell_output("#{bin}/microbridgectl help") + end +end diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..b541585 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,111 @@ +# Installing Microbridge + +Microbridge is a local daemon plus an optional companion UI. There is **no +network** and **no cloud account** — install puts binaries on your machine and +(on macOS) a per-user launchd agent. + +## Requirements + +| Piece | Need | +|---|---| +| Daemon | Rust stable (`rustup`), macOS 13+ or Linux | +| Companion UI (optional) | Node ≥ 20; full `.app` also needs Xcode CLT | +| Hardware LEDs | Codex Micro over USB (HID packing still landing — mock works without hardware) | + +## Quick install (macOS, from source) + +```sh +git clone https://github.com/DevVig/microbridge.git +cd microbridge +./scripts/install.sh +``` + +This will: + +1. `cargo build --release` for `microbridged` and `microbridgectl` +2. Install them to `~/.local/bin` (override with `MICROBRIDGE_BIN=…`) +3. Ensure `~/.local/bin` is on your PATH (prints a hint if not) +4. Install and start the launchd agent `ai.microbridge.daemon` +5. Write config defaults under `~/.microbridge/` + +Verify: + +```sh +microbridgectl status +# or: +tail -f ~/.microbridge/daemon.log +``` + +### Optional companion UI + +```sh +./scripts/install.sh --with-ui +# web preview during development: +cd apps/microbridge-ui && npm install && npm run dev +``` + +`--with-ui` installs frontend deps and, when Tauri/Xcode tooling is available, +attempts `npm run tauri build`. You can always run the Vite UI against a live +daemon without bundling an `.app`. + +## Linux (from source) + +```sh +./scripts/install.sh --no-launchd +# run in the foreground, or add your own systemd --user unit: +microbridged +``` + +A sample user unit is in [`scripts/microbridge.service`](scripts/microbridge.service). + +## Homebrew (skeleton) + +```sh +brew install --build-from-source ./Formula/microbridge.rb +brew services start microbridge # when using the formula's service block +``` + +A published tap/bottle is not available yet — use `./scripts/install.sh` for +day-to-day installs. + +## Install from a GitHub Release + +When a `v*` tag is pushed, CI attaches platform archives. Then: + +```sh +./scripts/install-from-release.sh v0.0.1 +# or latest: +./scripts/install-from-release.sh +``` + +## Uninstall + +```sh +./scripts/uninstall.sh +``` + +Removes the launchd agent, binaries from `MICROBRIDGE_BIN` / `~/.local/bin`, +and optionally (`--purge`) `~/.microbridge/` (config, socket, logs). + +## Layout after install + +| Path | Purpose | +|---|---| +| `~/.local/bin/microbridged` | Daemon | +| `~/.local/bin/microbridgectl` | CLI | +| `~/Library/LaunchAgents/ai.microbridge.daemon.plist` | macOS autostart | +| `~/.microbridge/microbridged.sock` | Local NDJSON socket | +| `~/.microbridge/config.toml` | Key source, lighting, appearance | +| `~/.microbridge/daemon.log` | launchd stdout/stderr | + +## Troubleshooting + +**`microbridgectl: connect …`** — daemon not running. On macOS: +`launchctl kickstart -k gui/$(id -u)/ai.microbridge.daemon`. + +**LEDs stay dark** — HID packing is still best-effort; ChatGPT desktop may +also own the device. Pause that app or use Settings → Pause LEDs while testing +the mock path (`microbridgectl status` still works). + +**PATH** — add `export PATH="$HOME/.local/bin:$PATH"` to your shell rc if +`microbridgectl` is not found. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e70a46c --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +.PHONY: test clippy fmt ci build install uninstall ui + +test: + cargo test --workspace + +clippy: + cargo clippy --workspace --all-targets -- -D warnings + +fmt: + cargo fmt --all + +ci: fmt + cargo fmt --all --check + $(MAKE) clippy + $(MAKE) test + cd apps/microbridge-ui && npm ci && npm run build + +build: + cargo build --release -p microbridged -p microbridgectl + +install: + ./scripts/install.sh + +uninstall: + ./scripts/uninstall.sh + +ui: + cd apps/microbridge-ui && npm install && npm run dev diff --git a/README.md b/README.md index 858a1d2..4e7c80b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Microbridge is a tiny local daemon that bridges AI coding agents — Codex CLI, Claude Code, Cursor, T3 Code, and anything else with an adapter — to the [Work Louder Codex Micro](https://worklouder.cc/). Per-key RGB mirrors live agent state; the keys drive agent actions (approve, reject, interrupt, switch focus). No vendor desktop app required. -> **Status: pre-alpha.** Protocol v0 and the daemon skeleton. Nothing drives real hardware yet — see [ROADMAP.md](ROADMAP.md). +> **Status: early alpha.** Protocol v0 with UI/control, in-process Codex/Claude watchers, mock device, `microbridgectl`, and a Tauri companion shell. Real HID packing waits on device captures — see [ROADMAP.md](ROADMAP.md). ## Why @@ -19,7 +19,7 @@ The Micro's best feature — bidirectional Agent Keys — currently works throug 1. **Invisible footprint.** Event-driven end to end: no polling loops, no heartbeat timers. Idle CPU is 0.0% and idle RSS targets single-digit megabytes. If Microbridge is noticeable in Activity Monitor, that is a bug — the [footprint budget](docs/architecture.md#footprint-budget) is a spec, not an aspiration. 2. **Zero network.** No telemetry, no update pings, no cloud. The daemon's only I/O is a local Unix socket and the USB device. It links no HTTP client — auditable in `Cargo.lock`. 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 UI is optional.** A menu bar companion app provides status and key remapping, and you can quit it; the daemon keeps working without it. +4. **The UI is optional.** A menu bar companion shows connection status and opens Settings for key remapping; you can quit it and the daemon keeps working. ## Architecture @@ -37,7 +37,7 @@ The Micro's best feature — bidirectional Agent Keys — currently works throug └──────────────────────┬───────────────────────────┘ │ same socket (status + commands) ┌─────────┴─────────┐ - │ menu bar app │ optional, quit-able + │ menu bar app │ optional, quit-able (Tauri) └───────────────────┘ ``` @@ -46,23 +46,51 @@ Details in [docs/architecture.md](docs/architecture.md). The wire format is spec ## Repository layout ``` -crates/mb-protocol wire types (serde) — the protocol's source of truth -crates/mb-device device abstraction; mock today, HID in M2 -crates/microbridged the daemon: socket server, registry, focus policy -adapters/ out-of-process community adapters + reference impl -docs/ protocol spec, architecture, adapter guide, design +crates/mb-protocol wire types (serde) — the protocol's source of truth +crates/mb-device device abstraction; mock today, HID packing TBD +crates/mb-adapters first-party Codex CLI + Claude Code watchers +crates/microbridged the daemon: socket server, registry, focus, key source +crates/microbridgectl inspect a live bus (`status`) +apps/microbridge-ui optional Tauri companion (MagicPath-faithful) +adapters/ out-of-process community adapters + reference impl +docs/ protocol, architecture, adapter guide, design, HID notes ``` -## Building +## Install + +Full guide: **[INSTALL.md](INSTALL.md)**. ```sh -cargo test # protocol round-trips + focus policy +git clone https://github.com/DevVig/microbridge.git +cd microbridge +./scripts/install.sh # macOS: binaries + launchd +# ./scripts/install.sh --with-ui # also build companion UI +# ./scripts/install-linux-systemd.sh # Linux systemd --user +microbridgectl status +``` + +Uninstall: `./scripts/uninstall.sh` (add `--purge` to remove `~/.microbridge`). + +From a GitHub Release (after a `v*` tag): `./scripts/install-from-release.sh`. + +## Building (dev) + +```sh +cargo test # protocol, focus, key-source, adapters cargo run -p microbridged # in another shell: +cargo run -p microbridgectl -- status node adapters/reference-echo/index.mjs # walks a fake session through the states ``` -Requires stable Rust (see `rust-toolchain.toml`) and, for the reference adapter only, Node ≥ 20. macOS and Linux today; Windows (named pipes) is on the roadmap. +Companion UI: + +```sh +cd apps/microbridge-ui && npm install && npm run dev +# or: make ui +``` + +Requires stable Rust (see `rust-toolchain.toml`) and, for Node adapters / UI, Node ≥ 20. macOS and Linux today; Windows (named pipes) is on the roadmap. ## Contributing diff --git a/ROADMAP.md b/ROADMAP.md index f1828bf..1cad219 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,29 +3,30 @@ Milestones are deliberately small; each one is independently useful and reviewable. Issues are labeled `M0`…`M5`. -## M0 — Protocol + skeleton *(current)* +## M0 — Protocol + skeleton ✅ Protocol v0 spec and types, daemon with registry + focus policy v0, mock device, reference adapter, CI. -## M1 — Real state in -In-process **Codex CLI** and **Claude Code** adapters (session-file watching -/ hooks). `microbridgectl status` for inspecting the bus. Daemon ships as a -launchd agent with `brew install`able bottle. +## M1 — Real state in ✅ (foundation) +In-process **Codex CLI** and **Claude Code** adapters (session-file watching). +`microbridgectl status` for inspecting the bus. launchd install script + +Homebrew formula skeleton. UI/control protocol (`subscribe` / config) and +five key-source modes. -## M2 — Real light out +## M2 — Real light out 🚧 Codex Micro HID driver in `mb-device` (LED frames, key events, encoder), -behind a capability-probed device descriptor so layouts aren't hardcoded. -Documented findings from the reverse-engineering work, kept isolated in one -crate. +behind a capability-probed device descriptor. Mock remains the default until +VID/PID + report map are captured — see [docs/device-hid.md](docs/device-hid.md). -## M3 — Focus + menu bar -Frontmost-app auto-focus (NSWorkspace notifications), pinning, approvals -preemption end-to-end. Menu bar companion app and focus HUD implementing the -[design spec](docs/design/README.md). Key remapping UI (profiles per app). +## M3 — Focus + menu bar 🚧 +Tauri companion (`apps/microbridge-ui`) ports the approved MagicPath surfaces +(popover / settings / HUD). Frontmost-app auto-focus via `frontmost_app` +config (NSWorkspace wiring next). Key remapping UI continues to track the +MagicPath device twin. -## M4 — Community adapters -Cursor and T3 Code adapters (community-led, out-of-process), adapter -developer guide hardening, per-adapter footprint reporting in the UI. +## M4 — Community adapters 🚧 +Cursor and T3 Code adapter scaffolds under `adapters/`. Harden as session +sources appear. Per-adapter footprint reporting in Settings. ## M5 — Portability Windows transport (named pipes) + Windows/Linux tray. Signed release diff --git a/adapters/README.md b/adapters/README.md index cf7de72..60e5c1c 100644 --- a/adapters/README.md +++ b/adapters/README.md @@ -7,9 +7,8 @@ checklist, and [docs/protocol.md](../docs/protocol.md) for the wire format. | Adapter | Status | Language | |---|---|---| | [`reference-echo`](reference-echo/) | working example | Node (no deps) | -| `cursor` | wanted — see adapter issues | — | -| `t3code` | wanted — see adapter issues | — | +| [`cursor`](cursor/) | scaffold — awaiting session source | Node | +| [`t3code`](t3code/) | scaffold — awaiting session source | Node | First-party adapters (Codex CLI, Claude Code) are compiled into the daemon -and live in `crates/`, not here — see -[docs/architecture.md](../docs/architecture.md). +(`crates/mb-adapters`) — see [docs/architecture.md](../docs/architecture.md). diff --git a/adapters/cursor/README.md b/adapters/cursor/README.md new file mode 100644 index 0000000..766f10f --- /dev/null +++ b/adapters/cursor/README.md @@ -0,0 +1,28 @@ +# cursor adapter (community) + +Out-of-process Microbridge adapter for [Cursor](https://cursor.com/). + +## Status + +**Scaffold.** Cursor does not publish a stable local session journal API. +This adapter connects and stays idle until a supported state source is +documented. PRs welcome — see the checklist in +[docs/adapters.md](../../docs/adapters.md). + +## Rules + +- Event-driven only (no polling loops) +- No scraping of Cursor's private Electron internals +- Prefer official hooks / documented session files when available + +## Run (once implemented) + +```sh +cargo run -p microbridged # shell 1 +node adapters/cursor/index.mjs # shell 2 +``` + +## Supported versions + +TBD — document the Cursor build you tested against before merging a real +implementation. diff --git a/adapters/cursor/index.mjs b/adapters/cursor/index.mjs new file mode 100755 index 0000000..0b1b103 --- /dev/null +++ b/adapters/cursor/index.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node +// Cursor community adapter scaffold — connects, says hello, then idles. +// Replace the idle section when a supported session source exists. + +import net from "node:net"; +import os from "node:os"; +import path from "node:path"; + +const socketPath = + process.env.MICROBRIDGE_SOCKET ?? + path.join(os.homedir(), ".microbridge", "microbridged.sock"); + +const socket = net.createConnection(socketPath); +const send = (message) => socket.write(`${JSON.stringify(message)}\n`); + +socket.on("connect", () => { + send({ type: "hello", adapter: "cursor", protocol_version: 0 }); + console.log("cursor adapter connected (idle — no session source yet)"); +}); + +socket.on("data", (buf) => { + for (const line of buf.toString("utf8").split("\n")) { + if (!line.trim()) continue; + try { + const msg = JSON.parse(line); + if (msg.type === "action") { + console.log("action (no-op until implemented):", msg); + } + } catch { + /* ignore */ + } + } +}); + +socket.on("error", (error) => { + console.error(`cannot reach microbridged at ${socketPath}: ${error.message}`); + process.exit(1); +}); diff --git a/adapters/t3code/README.md b/adapters/t3code/README.md new file mode 100644 index 0000000..6162566 --- /dev/null +++ b/adapters/t3code/README.md @@ -0,0 +1,27 @@ +# t3code adapter (community) + +Out-of-process Microbridge adapter for [T3 Code](https://github.com/pingdotgg/t3code). + +## Status + +**Scaffold.** Wire this to T3 Code's local session / agent status surface when +one is available. Upstream contributions to T3 Code itself are currently +closed; this adapter can still ship in Microbridge independently. + +## Rules + +- Event-driven only (no polling loops) +- No scraping of private Electron internals +- Prefer official hooks / documented session files + +## Run (once implemented) + +```sh +cargo run -p microbridged # shell 1 +node adapters/t3code/index.mjs # shell 2 +``` + +## Supported versions + +TBD — document the T3 Code build you tested against before merging a real +implementation. diff --git a/adapters/t3code/index.mjs b/adapters/t3code/index.mjs new file mode 100755 index 0000000..1b4de37 --- /dev/null +++ b/adapters/t3code/index.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node +// T3 Code community adapter scaffold — connects, says hello, then idles. + +import net from "node:net"; +import os from "node:os"; +import path from "node:path"; + +const socketPath = + process.env.MICROBRIDGE_SOCKET ?? + path.join(os.homedir(), ".microbridge", "microbridged.sock"); + +const socket = net.createConnection(socketPath); +const send = (message) => socket.write(`${JSON.stringify(message)}\n`); + +socket.on("connect", () => { + send({ type: "hello", adapter: "t3code", protocol_version: 0 }); + console.log("t3code adapter connected (idle — no session source yet)"); +}); + +socket.on("data", (buf) => { + for (const line of buf.toString("utf8").split("\n")) { + if (!line.trim()) continue; + try { + const msg = JSON.parse(line); + if (msg.type === "action") { + console.log("action (no-op until implemented):", msg); + } + } catch { + /* ignore */ + } + } +}); + +socket.on("error", (error) => { + console.error(`cannot reach microbridged at ${socketPath}: ${error.message}`); + process.exit(1); +}); diff --git a/apps/microbridge-ui/README.md b/apps/microbridge-ui/README.md new file mode 100644 index 0000000..0c1e9df --- /dev/null +++ b/apps/microbridge-ui/README.md @@ -0,0 +1,36 @@ +# microbridge-ui + +Optional Tauri 2 companion for Microbridge. **Status + setup only** — agent +actions stay on the physical Codex Micro. + +MagicPath mockups remain the visual go-to: + +| Surface | MagicPath | App route | +|---|---|---| +| Menu bar popover | `safely-park-1411` | `?view=popover` (default) | +| Settings | `cool-gulf-2537` | `?view=settings` | +| Focus HUD | `sunnily-shadow-8075` | `?view=hud` | + +Vendored MagicPath exports (reference): [`vendor/magicpath/`](vendor/magicpath/). + +## Develop + +```sh +# terminal 1 — daemon +cargo run -p microbridged + +# terminal 2 — web UI (demo snapshot if daemon/Tauri unavailable) +cd apps/microbridge-ui && npm install && npm run dev + +# or full Tauri shell (needs Xcode CLT) +npm run tauri dev +``` + +## Build + +```sh +npm run build # frontend only (CI) +npm run tauri build # macOS app bundle +``` + +The UI connects as `role:ui` on the Microbridge Unix socket and never opens HID. diff --git a/apps/microbridge-ui/index.html b/apps/microbridge-ui/index.html new file mode 100644 index 0000000..a0e6298 --- /dev/null +++ b/apps/microbridge-ui/index.html @@ -0,0 +1,18 @@ + + + + + + Microbridge + + + + + +
+ + + diff --git a/apps/microbridge-ui/package-lock.json b/apps/microbridge-ui/package-lock.json new file mode 100644 index 0000000..221d6a7 --- /dev/null +++ b/apps/microbridge-ui/package-lock.json @@ -0,0 +1,2747 @@ +{ + "name": "microbridge-ui", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "microbridge-ui", + "version": "0.0.1", + "dependencies": { + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-shell": "^2", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@tauri-apps/cli": "^2", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.0", + "typescript": "~5.7.2", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-shell": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", + "integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.393", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz", + "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/apps/microbridge-ui/package.json b/apps/microbridge-ui/package.json new file mode 100644 index 0000000..a56b86b --- /dev/null +++ b/apps/microbridge-ui/package.json @@ -0,0 +1,28 @@ +{ + "name": "microbridge-ui", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-shell": "^2", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@tauri-apps/cli": "^2", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.0", + "typescript": "~5.7.2", + "vite": "^6.0.0" + } +} diff --git a/apps/microbridge-ui/src-tauri/Cargo.toml b/apps/microbridge-ui/src-tauri/Cargo.toml new file mode 100644 index 0000000..fe76c7b --- /dev/null +++ b/apps/microbridge-ui/src-tauri/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "microbridge-ui" +version = "0.0.1" +description = "Optional Microbridge menu bar companion" +authors = ["Microbridge contributors"] +edition = "2021" + +[lib] +name = "microbridge_ui_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-shell = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "sync"] } +mb-protocol = { path = "../../../crates/mb-protocol" } + +[workspace] diff --git a/apps/microbridge-ui/src-tauri/build.rs b/apps/microbridge-ui/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/apps/microbridge-ui/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/microbridge-ui/src-tauri/capabilities/default.json b/apps/microbridge-ui/src-tauri/capabilities/default.json new file mode 100644 index 0000000..9865a61 --- /dev/null +++ b/apps/microbridge-ui/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capabilities for the Microbridge companion", + "windows": ["main"], + "permissions": ["core:default", "shell:allow-open"] +} diff --git a/apps/microbridge-ui/src-tauri/icons/icon.png b/apps/microbridge-ui/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fa029fcc5875a5a8f603f50c5cd0f991f50f91d1 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzT~8Oskcv5P&n;wRVBlb0@V9>b n>8iF?2lcmamUEzj8g{@Lrx9bP0l+XkK(t8_P literal 0 HcmV?d00001 diff --git a/apps/microbridge-ui/src-tauri/src/lib.rs b/apps/microbridge-ui/src-tauri/src/lib.rs new file mode 100644 index 0000000..e95c93d --- /dev/null +++ b/apps/microbridge-ui/src-tauri/src/lib.rs @@ -0,0 +1,140 @@ +//! Tauri companion — talks to microbridged over the local Unix socket. +//! Never opens HID; the daemon owns the device. + +use std::path::PathBuf; + +use mb_protocol::{ + ClientMessage, ClientRole, DaemonConfig, ServerMessage, Snapshot, PROTOCOL_VERSION, +}; +use tauri::Manager; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::UnixStream; + +fn socket_path() -> PathBuf { + if let Ok(path) = std::env::var("MICROBRIDGE_SOCKET") { + return PathBuf::from(path); + } + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + PathBuf::from(home) + .join(".microbridge") + .join("microbridged.sock") +} + +async fn open_ui_client() -> Result< + ( + tokio::net::unix::OwnedWriteHalf, + BufReader, + ), + String, +> { + let path = socket_path(); + let stream = UnixStream::connect(&path) + .await + .map_err(|e| format!("connect {}: {e}", path.display()))?; + let (read_half, mut write_half) = stream.into_split(); + write_msg( + &mut write_half, + &ClientMessage::Hello { + adapter: "microbridge-ui".into(), + protocol_version: PROTOCOL_VERSION, + role: ClientRole::Ui, + }, + ) + .await?; + Ok((write_half, BufReader::new(read_half))) +} + +async fn write_msg( + write_half: &mut tokio::net::unix::OwnedWriteHalf, + msg: &ClientMessage, +) -> Result<(), String> { + let line = serde_json::to_string(msg).map_err(|e| e.to_string())?; + write_half + .write_all(line.as_bytes()) + .await + .map_err(|e| e.to_string())?; + write_half + .write_all(b"\n") + .await + .map_err(|e| e.to_string())?; + Ok(()) +} + +async fn read_matching( + reader: &mut BufReader, + want_snapshot: bool, + want_config: bool, +) -> Result { + let mut lines = reader.lines(); + while let Some(line) = lines + .next_line() + .await + .map_err(|e| format!("read: {e}"))? + { + if line.trim().is_empty() { + continue; + } + let msg: ServerMessage = + serde_json::from_str(&line).map_err(|e| format!("parse: {e}"))?; + match &msg { + ServerMessage::Snapshot { .. } if want_snapshot => return Ok(msg), + ServerMessage::Config { .. } if want_config => return Ok(msg), + _ => continue, + } + } + Err("daemon closed".into()) +} + +#[tauri::command] +async fn get_snapshot() -> Result { + let (mut write, mut reader) = open_ui_client().await?; + write_msg(&mut write, &ClientMessage::Subscribe).await?; + match read_matching(&mut reader, true, false).await? { + ServerMessage::Snapshot { snapshot } => Ok(snapshot), + _ => Err("unexpected response".into()), + } +} + +#[tauri::command] +async fn set_config(config: DaemonConfig) -> Result { + let (mut write, mut reader) = open_ui_client().await?; + write_msg(&mut write, &ClientMessage::SetConfig { config }).await?; + match read_matching(&mut reader, false, true).await? { + ServerMessage::Config { config } => Ok(config), + _ => Err("unexpected response".into()), + } +} + +#[tauri::command] +async fn set_frontmost_app(app: Option) -> Result<(), String> { + let snap = get_snapshot().await?; + let mut config = snap.config; + config.frontmost_app = app; + let _ = set_config(config).await?; + Ok(()) +} + +#[tauri::command] +fn quit_ui(app: tauri::AppHandle) { + app.exit(0); +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .invoke_handler(tauri::generate_handler![ + get_snapshot, + set_config, + set_frontmost_app, + quit_ui + ]) + .setup(|app| { + if let Some(window) = app.get_webview_window("main") { + let _ = window.set_title("Microbridge"); + } + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running microbridge-ui"); +} diff --git a/apps/microbridge-ui/src-tauri/src/main.rs b/apps/microbridge-ui/src-tauri/src/main.rs new file mode 100644 index 0000000..d34d80b --- /dev/null +++ b/apps/microbridge-ui/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + microbridge_ui_lib::run(); +} diff --git a/apps/microbridge-ui/src-tauri/tauri.conf.json b/apps/microbridge-ui/src-tauri/tauri.conf.json new file mode 100644 index 0000000..5f530d5 --- /dev/null +++ b/apps/microbridge-ui/src-tauri/tauri.conf.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Microbridge", + "version": "0.0.1", + "identifier": "ai.microbridge.ui", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "Microbridge", + "width": 400, + "height": 560, + "resizable": true, + "decorations": true, + "transparent": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/icon.png" + ] + } +} diff --git a/apps/microbridge-ui/src/App.tsx b/apps/microbridge-ui/src/App.tsx new file mode 100644 index 0000000..589ffa9 --- /dev/null +++ b/apps/microbridge-ui/src/App.tsx @@ -0,0 +1,112 @@ +import { useCallback, useEffect, useState } from "react"; +import { fetchSnapshot, setConfig } from "./lib/bus"; +import { resolveAppearance } from "./lib/theme"; +import type { DaemonConfig, Snapshot } from "./lib/types"; +import { Hud } from "./surfaces/Hud"; +import { Popover } from "./surfaces/Popover"; +import { Settings } from "./surfaces/Settings"; + +type View = "popover" | "settings" | "hud"; +type SettingsTab = "keys" | "agent" | "adapters" | "device"; + +function initialView(): View { + const q = new URLSearchParams(window.location.search).get("view"); + if (q === "settings" || q === "hud" || q === "popover") return q; + return "popover"; +} + +export default function App() { + const [view, setView] = useState(initialView); + const [tab, setTab] = useState("agent"); + const [snapshot, setSnapshot] = useState(null); + const [hudFlash, setHudFlash] = useState(false); + const [prevFocus, setPrevFocus] = useState(null); + + const refresh = useCallback(async () => { + const snap = await fetchSnapshot(); + setSnapshot((prev) => { + if ( + prev && + snap.focused_session_id && + snap.focused_session_id !== prev.focused_session_id + ) { + setHudFlash(true); + window.setTimeout(() => setHudFlash(false), 2600); + } + return snap; + }); + setPrevFocus(snap.focused_session_id); + }, []); + + useEffect(() => { + void refresh(); + // UI polls only as a fallback when Tauri events are unavailable. + // In Tauri, replace with listen("bus-event"). + const id = window.setInterval(() => void refresh(), 2000); + return () => window.clearInterval(id); + }, [refresh]); + + useEffect(() => { + void prevFocus; + }, [prevFocus]); + + if (!snapshot) { + return ( +
+ Connecting to microbridged… +
+ ); + } + + const dark = + resolveAppearance(snapshot.config.appearance) === "dark"; + + const applyConfig = async (config: DaemonConfig) => { + const next = await setConfig(config); + setSnapshot({ ...snapshot, config: next }); + }; + + if (view === "hud" || hudFlash) { + return ; + } + + if (view === "settings") { + return ( + void applyConfig(c)} + onClose={() => setView("popover")} + /> + ); + } + + return ( + setView("settings")} + onTogglePause={() => + void applyConfig({ + ...snapshot.config, + pause_leds: !snapshot.config.pause_leds, + }) + } + onQuit={() => { + void import("@tauri-apps/api/core") + .then(({ invoke }) => invoke("quit_ui")) + .catch(() => window.close()); + }} + /> + ); +} diff --git a/apps/microbridge-ui/src/index.css b/apps/microbridge-ui/src/index.css new file mode 100644 index 0000000..cc75f59 --- /dev/null +++ b/apps/microbridge-ui/src/index.css @@ -0,0 +1,48 @@ +@import "tailwindcss"; + +:root { + font-family: Inter, system-ui, sans-serif; + color: #0d0d0d; + background: transparent; +} + +html, +body, +#root { + margin: 0; + min-height: 100%; + width: 100%; +} + +.mb-frost { + -webkit-backdrop-filter: blur(36px); + backdrop-filter: blur(36px); +} + +@keyframes mb-led-pulse { + 0%, + 100% { + opacity: 0.55; + } + 50% { + opacity: 1; + } +} + +.mb-led-pulse { + animation: mb-led-pulse 1.6s ease-in-out infinite; +} + +@keyframes mb-drain { + from { + transform: scaleX(1); + } + to { + transform: scaleX(0); + } +} + +.mb-drain { + transform-origin: left center; + animation: mb-drain 2.5s linear forwards; +} diff --git a/apps/microbridge-ui/src/lib/bus.ts b/apps/microbridge-ui/src/lib/bus.ts new file mode 100644 index 0000000..9a35651 --- /dev/null +++ b/apps/microbridge-ui/src/lib/bus.ts @@ -0,0 +1,75 @@ +import type { DaemonConfig, Snapshot } from "./types"; + +/** Talks to microbridged via Tauri commands when available; demo snapshot otherwise. */ + +const DEMO: Snapshot = { + sessions: [ + { + id: "s1", + app: "Codex", + title: "microbridge — HID reconnect on wake", + state: "working", + updated_at_ms: Date.now() - 12 * 60000, + }, + { + id: "s2", + app: "Claude Code", + title: "adapters — cursor beta cleanup", + state: "awaiting_approval", + updated_at_ms: Date.now() - 4 * 60000, + }, + { + id: "s3", + app: "Cursor", + title: "synara — onboarding empty states", + state: "thinking", + updated_at_ms: Date.now() - 60000, + }, + ], + focused_session_id: "s1", + agent_key_session_ids: ["s1", "s2", "s3", null, null, null], + device_connected: false, + device_name: "mock", + config: { + key_source: "most_recent", + pinned_session_ids: [], + app_priority: [], + custom_key_ids: ["", "", "", "", "", ""], + pinned_focus: null, + approvals_interrupt: true, + pause_leds: false, + appearance: "system", + lighting_preset: "codex", + state_colors: {}, + brightness: 80, + sleep_minutes: 3, + frontmost_app: null, + }, +}; + +async function invoke(cmd: string, args?: Record): Promise { + try { + const { invoke } = await import("@tauri-apps/api/core"); + return await invoke(cmd, args); + } catch { + return null; + } +} + +export async function fetchSnapshot(): Promise { + const snap = await invoke("get_snapshot"); + return snap ?? DEMO; +} + +export async function setConfig(config: DaemonConfig): Promise { + const next = await invoke("set_config", { config }); + return next ?? config; +} + +export async function setFrontmostApp(app: string | null): Promise { + await invoke("set_frontmost_app", { app }); +} + +export function isDemoSnapshot(snapshot: Snapshot): boolean { + return snapshot.device_name === "mock" && snapshot.sessions.some((s) => s.id === "s1"); +} diff --git a/apps/microbridge-ui/src/lib/theme.ts b/apps/microbridge-ui/src/lib/theme.ts new file mode 100644 index 0000000..3ed6c1d --- /dev/null +++ b/apps/microbridge-ui/src/lib/theme.ts @@ -0,0 +1,51 @@ +export interface ThemeTokens { + name: "light" | "dark"; + frame: string; + panel: string; + panelBorder: string; + sunken: string; + hairline: string; + text: string; + textSecondary: string; + textMuted: string; + hoverBg: string; +} + +export const LIGHT: ThemeTokens = { + name: "light", + frame: "#E9E9E7", + panel: "rgba(252,252,251,0.88)", + panelBorder: "rgba(0,0,0,0.10)", + sunken: "#F4F4F2", + hairline: "rgba(0,0,0,0.08)", + text: "#0D0D0D", + textSecondary: "#6E6E73", + textMuted: "#AEAEB2", + hoverBg: "rgba(0,0,0,0.04)", +}; + +export const DARK: ThemeTokens = { + name: "dark", + frame: "#0A0A0B", + panel: "rgba(26,26,28,0.90)", + panelBorder: "rgba(255,255,255,0.10)", + sunken: "rgba(0,0,0,0.24)", + hairline: "rgba(255,255,255,0.09)", + text: "#F5F5F4", + textSecondary: "#A0A0A6", + textMuted: "#5E5E66", + hoverBg: "rgba(255,255,255,0.06)", +}; + +export function resolveAppearance( + preference: "system" | "light" | "dark", +): "light" | "dark" { + if (preference === "light") return "light"; + if (preference === "dark") return "dark"; + if (typeof window !== "undefined" && window.matchMedia) { + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + } + return "light"; +} diff --git a/apps/microbridge-ui/src/lib/types.ts b/apps/microbridge-ui/src/lib/types.ts new file mode 100644 index 0000000..7452c57 --- /dev/null +++ b/apps/microbridge-ui/src/lib/types.ts @@ -0,0 +1,74 @@ +export type AgentState = + | "idle" + | "thinking" + | "working" + | "awaiting_approval" + | "done" + | "error"; + +export interface SessionStatus { + id: string; + app: string; + title: string; + state: AgentState; + updated_at_ms: number; +} + +export type KeySource = + | "most_recent" + | "focused_app" + | "pinned" + | "priority" + | "custom"; + +export type Appearance = "system" | "light" | "dark"; + +export interface DaemonConfig { + key_source: KeySource; + pinned_session_ids: string[]; + app_priority: string[]; + custom_key_ids: string[]; + pinned_focus: string | null; + approvals_interrupt: boolean; + pause_leds: boolean; + appearance: Appearance; + lighting_preset: string; + state_colors: Record; + brightness: number; + sleep_minutes: number; + frontmost_app: string | null; +} + +export interface Snapshot { + sessions: SessionStatus[]; + focused_session_id: string | null; + agent_key_session_ids: (string | null)[]; + device_connected: boolean; + device_name: string; + config: DaemonConfig; +} + +export const STATE_COLORS: Record = { + idle: "#E9E9E6", + thinking: "#3D7EFF", + working: "#3D7EFF", + awaiting_approval: "#FFB000", + done: "#30C463", + error: "#FF453A", +}; + +export const STATE_LABELS: Record = { + idle: "Idle", + thinking: "Thinking", + working: "Working", + awaiting_approval: "Needs approval", + done: "Done", + error: "Error", +}; + +export function elapsed(updatedAtMs: number): string { + const mins = Math.max(0, Math.floor((Date.now() - updatedAtMs) / 60000)); + if (mins < 1) return "<1m"; + if (mins < 60) return `${mins}m`; + return `${Math.floor(mins / 60)}h`; +} diff --git a/apps/microbridge-ui/src/main.tsx b/apps/microbridge-ui/src/main.tsx new file mode 100644 index 0000000..c2a145c --- /dev/null +++ b/apps/microbridge-ui/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/microbridge-ui/src/surfaces/Hud.tsx b/apps/microbridge-ui/src/surfaces/Hud.tsx new file mode 100644 index 0000000..d0f6526 --- /dev/null +++ b/apps/microbridge-ui/src/surfaces/Hud.tsx @@ -0,0 +1,122 @@ +import type { Snapshot } from "../lib/types"; +import { STATE_COLORS, STATE_LABELS } from "../lib/types"; +import { DARK, LIGHT } from "../lib/theme"; + +export function Hud({ + snapshot, + dark, +}: { + snapshot: Snapshot; + dark: boolean; +}) { + const t = dark ? DARK : LIGHT; + const focused = snapshot.sessions.find( + (s) => s.id === snapshot.focused_session_id, + ); + if (!focused) return null; + + const color = STATE_COLORS[focused.state]; + const label = STATE_LABELS[focused.state]; + const focusIndex = snapshot.agent_key_session_ids.findIndex( + (id) => id === focused.id, + ); + const initials = focused.app + .split(" ") + .map((w) => w[0]) + .join("") + .slice(0, 2) + .toUpperCase(); + + return ( +
+
+
+ + {initials} + +
+
+ + {focused.app} + + + {label} + +
+

+ {focused.title || focused.id} +

+
+
+ +
+ {snapshot.agent_key_session_ids.map((id, i) => { + const lit = i === focusIndex; + const sess = id + ? snapshot.sessions.find((s) => s.id === id) + : null; + const c = sess ? STATE_COLORS[sess.state] : color; + return ( + + {lit && ( + + )} + + ); + })} +
+ +

+ Press Agent Key to focus · actions stay on the Micro +

+
+
+
+
+
+ ); +} diff --git a/apps/microbridge-ui/src/surfaces/Popover.tsx b/apps/microbridge-ui/src/surfaces/Popover.tsx new file mode 100644 index 0000000..4e7f32f --- /dev/null +++ b/apps/microbridge-ui/src/surfaces/Popover.tsx @@ -0,0 +1,286 @@ +import { useMemo } from "react"; +import type { Snapshot } from "../lib/types"; +import { STATE_COLORS, STATE_LABELS, elapsed } from "../lib/types"; +import { DARK, LIGHT, type ThemeTokens } from "../lib/theme"; + +const MicroGlyph = ({ color }: { color: string }) => ( + +); + +function MiniEcho({ + t, + snapshot, +}: { + t: ThemeTokens; + snapshot: Snapshot; +}) { + const keys = snapshot.agent_key_session_ids; + return ( +
+
+ {keys.map((id, i) => { + const session = id + ? snapshot.sessions.find((s) => s.id === id) + : null; + const color = session ? STATE_COLORS[session.state] : "transparent"; + const focused = id === snapshot.focused_session_id; + return ( + + {session && ( + + )} + + ); + })} +
+ + Device echo · read-only + +
+ ); +} + +export function Popover({ + snapshot, + dark, + onOpenSettings, + onTogglePause, + onQuit, +}: { + snapshot: Snapshot; + dark: boolean; + onOpenSettings: () => void; + onTogglePause: () => void; + onQuit: () => void; +}) { + const t = dark ? DARK : LIGHT; + const connected = snapshot.device_connected; + const focused = snapshot.sessions.find( + (s) => s.id === snapshot.focused_session_id, + ); + const liveCount = snapshot.agent_key_session_ids.filter(Boolean).length; + + const footerButton = ( + label: string, + onClick?: () => void, + active = false, + ) => ( + + ); + + const frame = useMemo( + () => + dark + ? "radial-gradient(ellipse 120% 90% at 50% 0%, #131315 0%, #08080A 100%)" + : "radial-gradient(ellipse 120% 90% at 50% 0%, #F1F1EF 0%, #E2E2DF 100%)", + [dark], + ); + + return ( +
+
+
+ + + Microbridge + + + + {connected ? "Connected" : "Disconnected"} + +
+ + {focused ? ( + <> +
+
+
+ + {focused.app} + + + {STATE_LABELS[focused.state]} + + + {elapsed(focused.updated_at_ms)} + +
+

+ {focused.title || focused.id} +

+

+ Press Agent Key to focus · double-press brings window forward +

+
+
+
+ +
+
+
+ + Threads + + + {liveCount} on keys + +
+ {snapshot.sessions.map((s) => ( +
+ + + {s.app} + + + {s.title || s.id} + + + {elapsed(s.updated_at_ms)} + +
+ ))} +
+ + ) : ( +
+ +

+ Connect your Codex Micro +

+

+ Plug in over USB-C. Quit ChatGPT desktop if it owns the LEDs. + Agent Keys light up when a thread goes live. +

+
+ )} + +
+ {footerButton("Settings", onOpenSettings)} + {footerButton( + snapshot.config.pause_leds ? "Resume LEDs" : "Pause LEDs", + onTogglePause, + snapshot.config.pause_leds, + )} + {footerButton("Quit", onQuit)} +
+
+
+ ); +} diff --git a/apps/microbridge-ui/src/surfaces/Settings.tsx b/apps/microbridge-ui/src/surfaces/Settings.tsx new file mode 100644 index 0000000..04b496d --- /dev/null +++ b/apps/microbridge-ui/src/surfaces/Settings.tsx @@ -0,0 +1,357 @@ +import type { DaemonConfig, Snapshot } from "../lib/types"; +import { STATE_COLORS, STATE_LABELS } from "../lib/types"; +import { DARK, LIGHT } from "../lib/theme"; + +const KEY_SOURCES: { id: DaemonConfig["key_source"]; label: string; hint: string }[] = [ + { + id: "most_recent", + label: "Most recent", + hint: "Cross-app — six newest threads (default)", + }, + { + id: "focused_app", + label: "Focused app", + hint: "Repopulate from whichever IDE owns the deck", + }, + { + id: "pinned", + label: "Pinned", + hint: "Follow the first six pinned sessions", + }, + { + id: "priority", + label: "Priority", + hint: "Approvals and active threads first", + }, + { + id: "custom", + label: "Custom", + hint: "Pin specific threads to specific keys", + }, +]; + +type Tab = "keys" | "agent" | "adapters" | "device"; + +export function Settings({ + snapshot, + dark, + tab, + onTab, + onConfig, + onClose, +}: { + snapshot: Snapshot; + dark: boolean; + tab: Tab; + onTab: (t: Tab) => void; + onConfig: (config: DaemonConfig) => void; + onClose: () => void; +}) { + const t = dark ? DARK : LIGHT; + const cfg = snapshot.config; + const tabs: { id: Tab; label: string }[] = [ + { id: "keys", label: "Keys" }, + { id: "agent", label: "Agent Keys" }, + { id: "adapters", label: "Adapters" }, + { id: "device", label: "Device" }, + ]; + + return ( +
+ + +
+ {tab === "agent" && ( +
+

Agent Keys

+

+ Six keys, six threads. Command presses always route to the focused + thread. +

+
+ {snapshot.agent_key_session_ids.map((id, i) => { + const s = id + ? snapshot.sessions.find((x) => x.id === id) + : null; + return ( +
+
+ AG{i + 1} +
+ {s ? ( + <> +
{s.app}
+
+ {s.title || s.id} +
+ + {STATE_LABELS[s.state]} + + + ) : ( +
+ Unassigned +
+ )} +
+ ); + })} +
+ +

Key source

+
+ {KEY_SOURCES.map((src) => ( + + ))} +
+ + +
+ )} + + {tab === "device" && ( +
+

Device

+

+ Appearance, lighting, and sleep. Zero network — local socket + USB + only. +

+ +

Appearance

+
+ {(["system", "light", "dark"] as const).map((a) => ( + + ))} +
+ +

Lighting

+
+ + +
+ + + +

+ Device: {snapshot.device_name} + {snapshot.device_connected ? " · connected" : " · not connected"} + {" · "}sleep {cfg.sleep_minutes}m +

+
+ )} + + {tab === "adapters" && ( +
+

Adapters

+

+ First-party adapters run in-process. Community adapters speak + NDJSON on the local socket. +

+
    + {[ + { name: "Codex CLI", kind: "Native", note: "watches ~/.codex/sessions" }, + { name: "Claude Code", kind: "Native", note: "watches ~/.claude/projects" }, + { name: "Cursor", kind: "Community", note: "scaffold — adapters/cursor" }, + { name: "T3 Code", kind: "Community", note: "scaffold — adapters/t3code" }, + ].map((a) => ( +
  • +
    +
    {a.name}
    +
    + {a.note} +
    +
    + + {a.kind} + +
  • + ))} +
+
+ )} + + {tab === "keys" && ( +
+

Keys

+

+ Remap command keys, dial, and joystick in a later revision. Agent + Keys are thread-owned — configure them under Agent Keys. +

+
+

Device twin

+

+ Photo-accurate twin from MagicPath ( + cool-gulf-2537) — live LED echo uses bus state + below. +

+
+ {snapshot.agent_key_session_ids.map((id, i) => { + const s = id + ? snapshot.sessions.find((x) => x.id === id) + : null; + const c = s ? STATE_COLORS[s.state] : "#E9E9E6"; + return ( +
+ AG{i + 1} +
+ ); + })} +
+
+
+ )} +
+
+ ); +} diff --git a/apps/microbridge-ui/src/vite-env.d.ts b/apps/microbridge-ui/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/microbridge-ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/microbridge-ui/tsconfig.json b/apps/microbridge-ui/tsconfig.json new file mode 100644 index 0000000..7350c19 --- /dev/null +++ b/apps/microbridge-ui/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/microbridge-ui/tsconfig.node.json b/apps/microbridge-ui/tsconfig.node.json new file mode 100644 index 0000000..7366cef --- /dev/null +++ b/apps/microbridge-ui/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler" + }, + "include": ["vite.config.ts"] +} diff --git a/apps/microbridge-ui/vendor/magicpath/hud/FocusHUD.tsx b/apps/microbridge-ui/vendor/magicpath/hud/FocusHUD.tsx new file mode 100644 index 0000000..4cf8ab5 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/hud/FocusHUD.tsx @@ -0,0 +1,170 @@ +import { useState } from 'react'; + +type AgentState = 'thinking' | 'working' | 'awaiting-approval' | 'done' | 'idle' | 'error'; + +interface AgentFocus { + app: string; + session: string; + state: AgentState; + keyIndex: number; +} + +const TOTAL_KEYS = 6; + +/** Canonical demo: Claude Code needs approval on Agent Key 2 */ +const DEMO_AGENT: AgentFocus = { + app: 'Claude Code', + session: 'adapters — cursor beta cleanup', + state: 'awaiting-approval', + keyIndex: 1 +}; + +const STATE_META: Record = { + thinking: { label: 'Thinking', color: '#3D7EFF', pulse: true }, + working: { label: 'Working', color: '#3D7EFF', pulse: false }, + 'awaiting-approval': { label: 'Needs approval', color: '#FFB000', pulse: true }, + done: { label: 'Done', color: '#30C463', pulse: false }, + idle: { label: 'Idle', color: '#E9E9E6', pulse: false }, + error: { label: 'Error', color: '#FF453A', pulse: false } +}; + +const TOKENS = { + light: { + frame: 'radial-gradient(ellipse 120% 90% at 50% 0%, #F1F1EF 0%, #E2E2DF 100%)', + panel: 'rgba(252,252,251,0.88)', + panelBorder: 'rgba(0,0,0,0.10)', + sunken: '#F0F0EE', + text: '#0D0D0D', + secondary: '#6E6E73', + muted: '#AEAEB2', + hoverBg: 'rgba(0,0,0,0.05)' + }, + dark: { + frame: 'radial-gradient(ellipse 120% 90% at 50% 0%, #131315 0%, #08080A 100%)', + panel: 'rgba(26,26,28,0.90)', + panelBorder: 'rgba(255,255,255,0.10)', + sunken: 'rgba(0,0,0,0.24)', + text: '#F5F5F4', + secondary: '#A0A0A6', + muted: '#5E5E66', + hoverBg: 'rgba(255,255,255,0.07)' + } +} as const; + +/** Mini frosted Agent Key, echoing the real translucent caps */ +const MiniKey = ({ lit, color, pulse }: { lit: boolean; color: string; pulse: boolean }) => ( + + {lit && ( + + )} + + + +); + +/** Non-interactive focus confirmation — actions stay on the Micro */ +export const FocusHUD = () => { + const [dark, setDark] = useState(false); + const t = dark ? TOKENS.dark : TOKENS.light; + const agent = DEMO_AGENT; + const { label, color, pulse } = STATE_META[agent.state]; + const isIdle = agent.state === 'idle'; + const initials = agent.app + .split(' ') + .map((w) => w[0]) + .join('') + .slice(0, 2) + .toUpperCase(); + + return ( +
+
+
+
+ {initials} +
+ +
+
+ Deck focus +
+
+ {agent.app} +
+
+ {agent.session} +
+
+ +
+ + + {label} + + + +
+
+ +
+ Press = switch focus · double-press = bring the window forward +
+ +
+
+
+
+ + {/* Canvas-only theme preview, not part of the HUD */} + +
+ ); +}; diff --git a/apps/microbridge-ui/vendor/magicpath/popover/AgentKeyEcho.tsx b/apps/microbridge-ui/vendor/magicpath/popover/AgentKeyEcho.tsx new file mode 100644 index 0000000..9c3a091 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/popover/AgentKeyEcho.tsx @@ -0,0 +1,142 @@ +import { AGENT_KEY_SESSIONS, STATE_COLORS, type ThemeTokens } from './microbridge-types'; + +/** + * Miniature, read-only echo of the actual kbd-1.0 deck: + * dial · AG1 · AG2 · joystick / AG3–AG6 / command row / touch · mic · codex. + * The device is white in both themes; only the LEDs carry color. + */ + +const U = 26; +const GAP = 6; +const MiniAgentKey = ({ + index, + connected +}: { + index: number; + connected: boolean; +}) => { + const session = connected ? AGENT_KEY_SESSIONS[index] : null; + const color = session ? STATE_COLORS[session.state] : null; + const focused = session?.focused ?? false; + const pulse = session?.state === 'awaiting-approval' ? 'mb-led-pulse' : session?.state === 'thinking' ? 'mb-led-breathe' : ''; + return + + {color && } + + + ; +}; +const MiniWhiteKey = ({ + wide = false +}: { + wide?: boolean; +}) => ; +export const AgentKeyEcho = ({ + t, + connected +}: { + t: ThemeTokens; + connected: boolean; +}) => ; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/popover/AgentRow.tsx b/apps/microbridge-ui/vendor/magicpath/popover/AgentRow.tsx new file mode 100644 index 0000000..91ee465 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/popover/AgentRow.tsx @@ -0,0 +1,36 @@ +import { STATE_COLORS, type Session, type ThemeTokens } from './microbridge-types'; +export const AgentRow = ({ + session, + t +}: { + session: Session; + t: ThemeTokens; +}) => { + const c = STATE_COLORS[session.state]; + const isIdle = session.state === 'idle'; + const pulse = session.state === 'awaiting-approval' ? 'mb-led-pulse' : session.state === 'thinking' ? 'mb-led-breathe' : ''; + return
+ + + + {session.app} + + + + {session.title} + + + {session.elapsed} + +
; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/popover/FocusCard.tsx b/apps/microbridge-ui/vendor/magicpath/popover/FocusCard.tsx new file mode 100644 index 0000000..b069ba3 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/popover/FocusCard.tsx @@ -0,0 +1,69 @@ +import { STATE_COLORS, STATE_LABELS, type Session, type ThemeTokens } from './microbridge-types'; +export const StateChip = ({ + state, + t +}: { + state: Session['state']; + t: ThemeTokens; +}) => { + const c = STATE_COLORS[state]; + const isIdle = state === 'idle'; + const pulse = state === 'awaiting-approval' ? 'mb-led-pulse' : state === 'thinking' || state === 'working' ? 'mb-led-breathe' : ''; + return + + + + {STATE_LABELS[state]} + ; +}; +export const FocusCard = ({ + session, + t +}: { + session: Session; + t: ThemeTokens; +}) =>
+
+ + {session.app} · owns the deck + + +
+

+ {session.title} +

+
+ + {session.elapsed} + + + + + High reasoning + + + press = switch · double-press = open + +
+
; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/popover/MenuBarPopover.tsx b/apps/microbridge-ui/vendor/magicpath/popover/MenuBarPopover.tsx new file mode 100644 index 0000000..d7e0360 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/popover/MenuBarPopover.tsx @@ -0,0 +1,148 @@ +import { useState } from 'react'; +import { AGENT_KEY_SESSIONS, DARK, LIGHT, SESSIONS } from './microbridge-types'; +import { AgentKeyEcho } from './AgentKeyEcho'; +import { FocusCard } from './FocusCard'; +import { AgentRow } from './AgentRow'; +const MicroGlyph = ({ + color +}: { + color: string; +}) => ; +export const MenuBarPopover = () => { + const [dark, setDark] = useState(false); + const [connected, setConnected] = useState(true); + const [ledsPaused, setLedsPaused] = useState(false); + const t = dark ? DARK : LIGHT; + const focused = SESSIONS.find(s => s.focused) ?? SESSIONS[0]; + const liveCount = AGENT_KEY_SESSIONS.filter(Boolean).length; + const footerButton = (label: string, onClick?: () => void, active = false) => ; + return
+ + {/* macOS menu bar */} +
+ + + + + + + Thu 9:41 AM + +
+ + {/* Popover */} +
+ + {/* Header */} +
+ + Microbridge + + +
+ + {connected ? <> +
+ +
+ +
+ +
+ +
+
+ + Threads + + + {liveCount} on keys + +
+ {SESSIONS.map(s => )} +
+ :
+ +

+ Connect your Codex Micro +

+

+ Plug in over USB-C or pair over Bluetooth. Your Agent Keys light up the moment a thread goes live. +

+
} + + {/* Footer */} +
+ {footerButton('Settings')} + {footerButton(ledsPaused ? 'Resume LEDs' : 'Pause LEDs', () => setLedsPaused(p => !p), ledsPaused)} + {footerButton('Quit')} +
+
+ + {/* Canvas-only theme preview, not part of the popover */} + +

+ Appearance follows the system (configurable in Settings). This switch is a preview control, not part of the popover. +

+
; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/popover/microbridge-types.ts b/apps/microbridge-ui/vendor/magicpath/popover/microbridge-types.ts new file mode 100644 index 0000000..b4e535e --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/popover/microbridge-types.ts @@ -0,0 +1,78 @@ +export type AgentState = 'idle' | 'thinking' | 'working' | 'awaiting-approval' | 'done' | 'error'; + +export const STATE_COLORS: Record = { + idle: '#E9E9E6', + thinking: '#3D7EFF', + working: '#3D7EFF', + 'awaiting-approval': '#FFB000', + done: '#30C463', + error: '#FF453A' +}; + +export const STATE_LABELS: Record = { + idle: 'Idle', + thinking: 'Thinking', + working: 'Working', + 'awaiting-approval': 'Needs approval', + done: 'Done', + error: 'Error' +}; + +export interface Session { + id: string; + app: string; + title: string; + state: AgentState; + elapsed: string; + focused?: boolean; +} + +export const SESSIONS: Session[] = [ +{ id: 's1', app: 'Codex', title: 'microbridge — HID reconnect on wake', state: 'working', elapsed: '12m', focused: true }, +{ id: 's2', app: 'Claude Code', title: 'adapters — cursor beta cleanup', state: 'awaiting-approval', elapsed: '4m' }, +{ id: 's3', app: 'Cursor', title: 'synara — onboarding empty states', state: 'thinking', elapsed: '1m' }, +{ id: 's4', app: 'Codex', title: 'protocol v0 — golden vectors', state: 'done', elapsed: '22m' }, +{ id: 's5', app: 'T3 Code', title: 't3code — session watcher spike', state: 'idle', elapsed: '38m' }]; + + +/** Which session each of the six Agent Keys follows (key source: Most recent). */ +export const AGENT_KEY_SESSIONS: (Session | null)[] = [SESSIONS[0], SESSIONS[1], SESSIONS[2], SESSIONS[3], SESSIONS[4], null]; + +export interface ThemeTokens { + name: 'light' | 'dark'; + frame: string; + panel: string; + panelBorder: string; + sunken: string; + hairline: string; + text: string; + textSecondary: string; + textMuted: string; + hoverBg: string; +} + +export const LIGHT: ThemeTokens = { + name: 'light', + frame: '#E9E9E7', + panel: 'rgba(252,252,251,0.88)', + panelBorder: 'rgba(0,0,0,0.10)', + sunken: '#F4F4F2', + hairline: 'rgba(0,0,0,0.08)', + text: '#0D0D0D', + textSecondary: '#6E6E73', + textMuted: '#AEAEB2', + hoverBg: 'rgba(0,0,0,0.04)' +}; + +export const DARK: ThemeTokens = { + name: 'dark', + frame: '#0A0A0B', + panel: 'rgba(26,26,28,0.90)', + panelBorder: 'rgba(255,255,255,0.10)', + sunken: 'rgba(0,0,0,0.24)', + hairline: 'rgba(255,255,255,0.09)', + text: '#F5F5F4', + textSecondary: '#A0A0A6', + textMuted: '#5E5E66', + hoverBg: 'rgba(255,255,255,0.06)' +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/ActionPicker.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/ActionPicker.tsx new file mode 100644 index 0000000..74e8cd5 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/ActionPicker.tsx @@ -0,0 +1,87 @@ +import { useEffect, useRef, useState } from 'react'; +import type { ActionGroup } from './microbridge-data'; +import { ACTIONS, ACTION_GROUP_LABELS, getAction } from './microbridge-data'; +import { useTheme } from './theme'; +interface ActionPickerProps { + value: string; + onChange: (id: string) => void; + label?: string; + compact?: boolean; +} +const GROUPS: ActionGroup[] = ['AGENT', 'SKILL', 'SYSTEM', 'MACRO']; +export const ActionPicker = ({ + value, + onChange, + label, + compact = false +}: ActionPickerProps) => { + const { + t + } = useTheme(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + const current = getAction(value); + useEffect(() => { + function onDocClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + } + document.addEventListener('mousedown', onDocClick); + return () => document.removeEventListener('mousedown', onDocClick); + }, []); + return
+ {label && + {label} + } + + {open &&
+ + {GROUPS.map(group =>
+
+ {ACTION_GROUP_LABELS[group]} +
+ {ACTIONS.filter(a => a.group === group).map(a => )} +
)} +
} +
; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/AdaptersTab.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/AdaptersTab.tsx new file mode 100644 index 0000000..33e7dd5 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/AdaptersTab.tsx @@ -0,0 +1,79 @@ +import { useState } from 'react'; +import { ADAPTERS } from './microbridge-data'; +import { Card } from './bits'; +import { Toggle } from './Toggle'; +import { useTheme } from './theme'; +export const AdaptersTab = () => { + const { + t + } = useTheme(); + const [enabled, setEnabled] = useState>({ + codex: true, + claude: true, + cursor: true, + t3: false + }); + return
+ {ADAPTERS.map(a => +
+
+ + {a.name} + + + + {a.badge === 'NATIVE' ? 'Native' : 'Community'} + + + + + + {a.status === 'connected' ? 'Connected' : a.status === 'beta' ? 'Beta' : 'Not installed'} + + +
+

+ {a.detail} +

+
+ + + + setEnabled(prev => ({ + ...prev, + [a.id]: v + }))} disabled={a.status === 'not_installed'} /> + +
)} +

+ Adapters publish thread state to the daemon. Anyone can build one — the protocol is open. +

+
; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/DeviceKeys.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/DeviceKeys.tsx new file mode 100644 index 0000000..5992562 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/DeviceKeys.tsx @@ -0,0 +1,344 @@ +import type { AgentState, ControlId } from './microbridge-data'; +import { CONTROLS, STATE_COLORS, sessionForAgentKey } from './microbridge-data'; + +/** + * Photo-accurate twin of the kbd-1.0 Codex Micro: + * row 1 — dial · AG1 · AG2 · joystick + * row 2 — AG3 · AG4 · AG5 · AG6 + * row 3 — Fast · Approve · Reject · Fork + * row 4 — touch sensor · 2U mic bar · Codex key + * The device is white in both themes; only the LEDs carry color. + */ + +const U = 58; +const GAP = 12; +const SELECTION = '#3D7EFF'; +interface CommonProps { + selected: boolean; + onSelect: () => void; + title: string; +} +function selectableStyle(selected: boolean): React.CSSProperties { + return selected ? { + boxShadow: `0 0 0 2px #FFFFFF, 0 0 0 4px ${SELECTION}` + } : {}; +} + +/* ---------------------------------------------------------------- */ +/* Icons printed on the shipped caps */ +/* ---------------------------------------------------------------- */ + +export const CapIcon = ({ + icon, + size = 20 +}: { + icon: string; + size?: number; +}) => { + const s = { + width: size, + height: size + }; + const common = { + fill: 'none', + stroke: 'currentColor', + strokeWidth: 1.7, + strokeLinecap: 'round' as const, + strokeLinejoin: 'round' as const + }; + switch (icon) { + case 'bolt': + return ; + case 'check': + return ; + case 'cross': + return ; + case 'fork': + return ; + case 'mic': + return ; + case 'codex': + return ; + default: + return null; + } +}; + +/* ---------------------------------------------------------------- */ +/* Controls */ +/* ---------------------------------------------------------------- */ + +const AgentKeycap = ({ + id, + selected, + onSelect, + title, + stateColors +}: CommonProps & { + id: ControlId; + stateColors: Record; +}) => { + const session = sessionForAgentKey(id); + const lit = session != null; + const color = session ? stateColors[session.state] : 'transparent'; + const focused = session?.focused ?? false; + const pulse = session?.state === 'awaiting-approval' ? 'mb-led-pulse' : session?.state === 'thinking' ? 'mb-led-breathe' : ''; + return ; +}; +const CommandKeycap = ({ + icon, + selected, + onSelect, + title, + wide = false +}: CommonProps & { + icon: string; + wide?: boolean; +}) => ; +const Dial = ({ + selected, + onSelect, + title +}: CommonProps) => ; +const Joystick = ({ + selected, + onSelect, + title +}: CommonProps) => ; +const TouchSensor = ({ + selected, + onSelect, + title +}: CommonProps) => ; +const Screw = () => + + + ; + +/* ---------------------------------------------------------------- */ +/* Full device */ +/* ---------------------------------------------------------------- */ + +export interface DeviceTwinProps { + selected: ControlId | null; + onSelect: (id: ControlId) => void; + stateColors?: Record; +} +export const DeviceTwin = ({ + selected, + onSelect, + stateColors = STATE_COLORS +}: DeviceTwinProps) => { + const common = (id: ControlId) => ({ + selected: selected === id, + onSelect: () => onSelect(id), + title: CONTROLS[id].label + }); + return
+ +
+ + {/* corner screws */} + + + + + + {/* plate print */} + + + + Work Louder | OpenAI 2026 + + + + You can just build things + + + Let's build + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + +
+
+
+
; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/DeviceTab.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/DeviceTab.tsx new file mode 100644 index 0000000..6968c63 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/DeviceTab.tsx @@ -0,0 +1,243 @@ +import { useEffect, useRef, useState } from 'react'; +import type { AgentState } from './microbridge-data'; +import { CODEX_STATE_COLORS, PHOSPHOR_STATE_COLORS, STATE_COLORS, STATE_LABELS } from './microbridge-data'; +import { Card, SectionLabel, Segmented } from './bits'; +import { useTheme, type ThemeChoice } from './theme'; +const STATE_SEQUENCE: AgentState[] = ['idle', 'thinking', 'working', 'awaiting-approval', 'done', 'error']; +const SLEEP_OPTIONS = ['3 minutes', '5 minutes', '15 minutes', '30 minutes', 'Never']; +const APPEARANCE_OPTIONS: { + id: ThemeChoice; + label: string; +}[] = [{ + id: 'system', + label: 'System' +}, { + id: 'light', + label: 'Light' +}, { + id: 'dark', + label: 'Dark' +}]; +export const DeviceTab = () => { + const { + t, + choice, + setChoice + } = useTheme(); + const [lighting, setLighting] = useState>({ + ...STATE_COLORS + }); + const [brightness, setBrightness] = useState(72); + const [testState, setTestState] = useState(null); + const [sleepAfter, setSleepAfter] = useState('3 minutes'); + const [sleepOpen, setSleepOpen] = useState(false); + const cycleRef = useRef | null>(null); + const sleepRef = useRef(null); + useEffect(() => () => { + if (cycleRef.current) clearInterval(cycleRef.current); + }, []); + useEffect(() => { + function onDocClick(e: MouseEvent) { + if (sleepRef.current && !sleepRef.current.contains(e.target as Node)) setSleepOpen(false); + } + document.addEventListener('mousedown', onDocClick); + return () => document.removeEventListener('mousedown', onDocClick); + }, []); + function runLedTest() { + if (cycleRef.current) { + clearInterval(cycleRef.current); + cycleRef.current = null; + } + let i = 0; + setTestState(STATE_SEQUENCE[0]); + cycleRef.current = setInterval(() => { + i += 1; + if (i >= STATE_SEQUENCE.length) { + if (cycleRef.current) clearInterval(cycleRef.current); + cycleRef.current = null; + setTestState(null); + return; + } + setTestState(STATE_SEQUENCE[i]); + }, 450); + } + const previewColor = testState ? lighting[testState] : lighting.idle; + const previewOpacity = Math.max(0.15, brightness / 100); + const presetButton = (label: string, onClick: () => void) => ; + return
+ {/* Appearance */} + +
+ + Appearance + + + One coherent look per mode — no toggle in the menu bar. + +
+ +
+ + {/* Lighting */} + +
+ Lighting +
+ {presetButton('Reset to Codex defaults', () => setLighting({ + ...CODEX_STATE_COLORS + }))} + {presetButton('Phosphor preset', () => setLighting({ + ...PHOSPHOR_STATE_COLORS + }))} +
+
+
+ {STATE_SEQUENCE.map(state =>
+ setLighting(prev => ({ + ...prev, + [state]: e.target.value + }))} className="h-7 w-7 shrink-0 cursor-pointer rounded-md border-0 bg-transparent p-0" aria-label={`Color for ${STATE_LABELS[state]}`} /> + + + {STATE_LABELS[state]} + +
)} +
+

+ Colors are rendering config on this machine — the protocol carries states, never colors. +

+
+ + {/* Brightness */} + +
+ Brightness + + {brightness}% + +
+
+ setBrightness(Number(e.target.value))} className="h-1.5 flex-1 cursor-pointer appearance-none rounded-full accent-[#3D7EFF]" style={{ + backgroundColor: t.sunken + }} /> + +
+ +
+
+

+ {testState ? `Testing: ${STATE_LABELS[testState]}` : 'Also on the dial when set to LED brightness.'} +

+ {presetButton('Run LED test', runLedTest)} +
+ + + {/* Sleep */} + + Sleep after +
+ + {sleepOpen &&
+ + {SLEEP_OPTIONS.map(opt => )} +
} +
+

+ LEDs fade out when every thread has been idle this long. Any state change wakes them. +

+
+ + {/* Firmware */} + +
+ + Firmware + + + kbd-1.0 · fw 1.4.2 + +
+ + + Up to date + +
+ + + +

+ Zero network. Microbridge never touches the network — all state stays on this machine. +

+
+
; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/FocusTab.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/FocusTab.tsx new file mode 100644 index 0000000..59767ae --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/FocusTab.tsx @@ -0,0 +1,239 @@ +import { useState } from 'react'; +import { ADAPTERS, FOCUS_APPS_DEFAULT, STATE_COLORS, sessionForAgentKey } from './microbridge-data'; +import { Card, SectionLabel, StateChip } from './bits'; +import { Toggle } from './Toggle'; +import { useTheme } from './theme'; +type FocusMode = 'AUTO' | 'PINNED'; +type KeySource = 'MOST_RECENT' | 'FOCUSED_APP' | 'PINNED' | 'PRIORITY' | 'CUSTOM'; +const KEY_SOURCES: { + id: KeySource; + title: string; + desc: string; +}[] = [{ + id: 'MOST_RECENT', + title: 'Most recent', + desc: 'Each Agent Key follows the most recently updated thread, across all apps.' +}, { + id: 'FOCUSED_APP', + title: 'Focused app', + desc: 'All six keys show threads from whichever app owns the deck — Codex\u2019s six, Cursor\u2019s five, and so on.' +}, { + id: 'PINNED', + title: 'Pinned', + desc: 'Agent Keys only show threads you have explicitly pinned.' +}, { + id: 'PRIORITY', + title: 'Priority', + desc: 'Fill keys from the app priority list, highest first.' +}, { + id: 'CUSTOM', + title: 'Custom', + desc: 'Assign specific threads to each of the six key slots.' +}]; +const MODES: { + id: FocusMode; + title: string; + desc: string; +}[] = [{ + id: 'AUTO', + title: 'Auto', + desc: 'The deck drives whichever agent app has focus.' +}, { + id: 'PINNED', + title: 'Pinned', + desc: 'Lock deck focus to one thread until unpinned.' +}]; +const AGENT_KEY_IDS = ['ag1', 'ag2', 'ag3', 'ag4', 'ag5', 'ag6'] as const; +function statusDotColor(appId: string) { + const adapter = ADAPTERS.find(a => a.id === appId); + if (!adapter) return '#9A9A94'; + if (adapter.status === 'connected') return '#30C463'; + if (adapter.status === 'beta') return '#FFB000'; + return '#9A9A94'; +} +export const FocusTab = () => { + const { + t + } = useTheme(); + const [keySource, setKeySource] = useState('MOST_RECENT'); + const [mode, setMode] = useState('AUTO'); + const [appOrder, setAppOrder] = useState(FOCUS_APPS_DEFAULT.map(a => a.id)); + const [approvalsInterrupt, setApprovalsInterrupt] = useState(true); + function move(index: number, dir: -1 | 1) { + setAppOrder(prev => { + const next = [...prev]; + const target = index + dir; + if (target < 0 || target >= next.length) return prev; + [next[index], next[target]] = [next[target], next[index]]; + return next; + }); + } + const radioCard = (active: boolean) => ({ + backgroundColor: active ? t.sunken : 'transparent', + border: `1px solid ${active ? t.cardBorder : 'transparent'}` + }); + return
+ {/* Live assignments */} +
+ Six keys, six threads + + {AGENT_KEY_IDS.map((id, i) => { + const session = sessionForAgentKey(id); + const color = session ? STATE_COLORS[session.state] : 'transparent'; + return
+ + + {i + 1} + + {session ? <> + + {session.app} + + + {session.title} + + + : + Empty slot + } +
; + })} +
+

+ Live view of what the six Agent Keys follow right now. Press a key to switch to its thread; double-press brings the window forward. +

+
+ +
+ Key source + + {KEY_SOURCES.map(s => )} + +
+ +
+ Deck focus + + {MODES.map(m => )} + +
+ +
+ App priority + + {appOrder.map((id, i) => { + const app = FOCUS_APPS_DEFAULT.find(a => a.id === id); + if (!app) return null; + return
+ + {i + 1} + + + + {app.name} + +
+ {([-1, 1] as const).map(dir => )} +
+
; + })} +
+
+ + +
+ + Approvals interrupt + + + When any thread needs approval, the Approve and Reject keys temporarily route to it. + +
+ +
+ +

+ These settings tell the daemon how to route the physical keys. Approve and reject always happen on the Micro, never in this window. +

+
; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/KeysTab.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/KeysTab.tsx new file mode 100644 index 0000000..4624190 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/KeysTab.tsx @@ -0,0 +1,250 @@ +import { useEffect, useRef, useState } from 'react'; +import type { ControlId, DeviceBindings, JoyDir } from './microbridge-data'; +import { CONTROLS, DEFAULT_BINDINGS, JOY_DIRS, REASONING_LEVELS, ROTATE_ACTIONS, sessionForAgentKey } from './microbridge-data'; +import { DeviceTwin, CapIcon } from './DeviceKeys'; +import { ActionPicker } from './ActionPicker'; +import { Card, SectionLabel, StateChip } from './bits'; +import { useTheme } from './theme'; +export const KeysTab = ({ + onOpenAgentKeys +}: { + onOpenAgentKeys: () => void; +}) => { + const { + t + } = useTheme(); + const [selected, setSelected] = useState('ag1'); + const [bindings, setBindings] = useState(DEFAULT_BINDINGS); + const [listening, setListening] = useState(false); + const listenTimeout = useRef | null>(null); + useEffect(() => () => { + if (listenTimeout.current) clearTimeout(listenTimeout.current); + }, []); + function handleListen() { + if (listening) { + if (listenTimeout.current) clearTimeout(listenTimeout.current); + setListening(false); + return; + } + setListening(true); + listenTimeout.current = setTimeout(() => setListening(false), 3000); + } + const control = CONTROLS[selected]; + const session = control.kind === 'agent-key' ? sessionForAgentKey(selected) : null; + const listenButton = ; + return
+
+ {/* Device twin */} +
+ +

+ Click any control to configure it. Agent Keys glow with the live state of the thread they follow. +

+
+ + {/* Inspector */} + +
+
+ {control.icon && + + } +

+ {control.label} +

+
+ {control.kind !== 'agent-key' && listenButton} +
+ + {/* Agent Key — read-only, thread comes through */} + {control.kind === 'agent-key' &&
+ {session ?
+
+ + {session.app} + + +
+

+ {session.title} +

+

+ {session.focused ? 'Owns the deck · ' : ''}active {session.elapsed} +

+
:
+

+ No thread assigned — this key is unlit until a session fills the slot. +

+
} +
+

+ Agent Keys follow your active threads automatically (key source: Most recent). + Press switches the thread; double-press brings its window forward. +

+ +
+
} + + {/* Dial */} + {control.kind === 'knob' &&
+
+ Rotate +
+ {ROTATE_ACTIONS.map(r => )} +
+
+ {bindings.knobRotate === 'reasoning_effort' &&
+
+ {REASONING_LEVELS.map((l, i) => + + {l} + )} +
+

+ Turn the dial to set the reasoning level for the focused thread. +

+
} + setBindings(b => ({ + ...b, + knobPress: id + }))} /> +
} + + {/* Joystick */} + {control.kind === 'joystick' &&
+
+ Flick to trigger skills +
+ {JOY_DIRS.map(d =>
+ + {d.label} + +
+ setBindings(b => ({ + ...b, + joystick: { + ...b.joystick, + [d.id as JoyDir]: id + } + }))} /> + +
+
)} +
+
+ setBindings(b => ({ + ...b, + joystickPress: id + }))} /> +
} + + {/* Touch sensor */} + {control.kind === 'touch' &&
+ setBindings(b => ({ + ...b, + touch: id + }))} /> +

+ The capacitive sensor next to the status LEDs. Default: pause LED updates without unplugging. +

+
} + + {/* Command keys */} + {control.kind === 'command-key' &&
+ setBindings(b => ({ + ...b, + commandKeys: { + ...b.commandKeys, + [selected]: id + } + }))} /> +
+ + + {control.icon && } + +

+ Shipped cap shown. The Codex Icon Keyset includes 32 icon caps and 11 solid caps for re-capping remapped keys. +

+
+
} +
+
+
; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/SettingsKeysAndFocus.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/SettingsKeysAndFocus.tsx new file mode 100644 index 0000000..559f08c --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/SettingsKeysAndFocus.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react'; +import { KeysTab } from './KeysTab'; +import { FocusTab } from './FocusTab'; +import { AdaptersTab } from './AdaptersTab'; +import { DeviceTab } from './DeviceTab'; +import { ThemeProvider, useTheme } from './theme'; +type TabId = 'KEYS' | 'AGENT_KEYS' | 'ADAPTERS' | 'DEVICE'; +const TabIcon = ({ + id, + size = 15 +}: { + id: TabId; + size?: number; +}) => { + const common = { + width: size, + height: size, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 1.7, + strokeLinecap: 'round' as const, + strokeLinejoin: 'round' as const + }; + switch (id) { + case 'KEYS': + return ; + case 'AGENT_KEYS': + return ; + case 'ADAPTERS': + return ; + case 'DEVICE': + return ; + } +}; +const TABS: { + id: TabId; + label: string; +}[] = [{ + id: 'KEYS', + label: 'Keys' +}, { + id: 'AGENT_KEYS', + label: 'Agent Keys' +}, { + id: 'ADAPTERS', + label: 'Adapters' +}, { + id: 'DEVICE', + label: 'Device' +}]; +const SettingsWindow = () => { + const { + t + } = useTheme(); + const [tab, setTab] = useState('KEYS'); + return
+ +
+ + {/* Titlebar */} +
+
+ + + +
+ + Microbridge + + + Settings + +
+ + + + Codex Micro connected + +
+
+ +
+ {/* Rail */} + + + {/* Content */} +
+ {tab === 'KEYS' && setTab('AGENT_KEYS')} />} + {tab === 'AGENT_KEYS' && } + {tab === 'ADAPTERS' && } + {tab === 'DEVICE' && } +
+
+
+
; +}; +export const SettingsKeysAndFocus = () => + + ; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/Toggle.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/Toggle.tsx new file mode 100644 index 0000000..c536f56 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/Toggle.tsx @@ -0,0 +1,25 @@ +import { useTheme } from './theme'; +interface ToggleProps { + checked: boolean; + onChange: (value: boolean) => void; + disabled?: boolean; +} +export const Toggle = ({ + checked, + onChange, + disabled +}: ToggleProps) => { + const { + t + } = useTheme(); + return ; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/bits.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/bits.tsx new file mode 100644 index 0000000..5755a9b --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/bits.tsx @@ -0,0 +1,97 @@ +import type { ReactNode } from 'react'; +import type { AgentState } from './microbridge-data'; +import { STATE_COLORS, STATE_LABELS } from './microbridge-data'; +import { useTheme } from './theme'; +export const SectionLabel = ({ + children +}: { + children: ReactNode; +}) => { + const { + t + } = useTheme(); + return + {children} + ; +}; +export const Card = ({ + children, + className = '' +}: { + children: ReactNode; + className?: string; +}) => { + const { + t + } = useTheme(); + return
+ + {children} +
; +}; + +/** Codex-style status chip: soft tinted pill with a state dot. */ +export const StateChip = ({ + state, + colors = STATE_COLORS +}: { + state: AgentState; + colors?: Record; +}) => { + const { + t + } = useTheme(); + const c = colors[state]; + const isIdle = state === 'idle'; + const pulse = state === 'awaiting-approval' ? 'mb-led-pulse' : state === 'thinking' || state === 'working' ? 'mb-led-breathe' : ''; + return + + + + {STATE_LABELS[state]} + ; +}; + +/** Neutral segmented control, macOS style. */ +export const Segmented = ({ + options, + value, + onChange +}: { + options: { + id: T; + label: string; + }[]; + value: T; + onChange: (v: T) => void; +}) => { + const { + t + } = useTheme(); + return
+ {options.map(o => )} +
; +}; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/microbridge-data.ts b/apps/microbridge-ui/vendor/magicpath/settings/generated/microbridge-data.ts new file mode 100644 index 0000000..34a4e06 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/microbridge-data.ts @@ -0,0 +1,235 @@ +export type AgentState = 'idle' | 'thinking' | 'working' | 'awaiting-approval' | 'done' | 'error'; + +export const STATE_COLORS: Record = { + idle: '#E9E9E6', + thinking: '#3D7EFF', + working: '#3D7EFF', + 'awaiting-approval': '#FFB000', + done: '#30C463', + error: '#FF453A' +}; + +/** Codex-default palette — used for "Reset to Codex defaults" */ +export const CODEX_STATE_COLORS: Record = { ...STATE_COLORS }; + +/** Alternate "Phosphor" preset (orange-centric) */ +export const PHOSPHOR_STATE_COLORS: Record = { + idle: '#4A4A52', + thinking: '#FFB454', + working: '#FF6A00', + 'awaiting-approval': '#FF3D00', + done: '#3DDC84', + error: '#FF4757' +}; + +export const STATE_LABELS: Record = { + idle: 'Idle', + thinking: 'Thinking', + working: 'Working', + 'awaiting-approval': 'Needs approval', + done: 'Done', + error: 'Error' +}; + +/* ------------------------------------------------------------------ */ +/* Live sessions (what the Agent Keys follow) */ +/* ------------------------------------------------------------------ */ + +export interface Session { + id: string; + app: string; + title: string; + state: AgentState; + elapsed: string; + focused?: boolean; +} + +export const SESSIONS: Session[] = [ +{ id: 's1', app: 'Codex', title: 'microbridge — HID reconnect on wake', state: 'working', elapsed: '12m', focused: true }, +{ id: 's2', app: 'Claude Code', title: 'adapters — cursor beta cleanup', state: 'awaiting-approval', elapsed: '4m' }, +{ id: 's3', app: 'Cursor', title: 'synara — onboarding empty states', state: 'thinking', elapsed: '1m' }, +{ id: 's4', app: 'Codex', title: 'protocol v0 — golden vectors', state: 'done', elapsed: '22m' }, +{ id: 's5', app: 'T3 Code', title: 't3code — session watcher spike', state: 'idle', elapsed: '38m' }]; + + +/** Which session each of the six Agent Keys follows (key source: Most recent). */ +export const AGENT_KEY_ASSIGNMENTS: Record = { + ag1: 's1', + ag2: 's2', + ag3: 's3', + ag4: 's4', + ag5: 's5', + ag6: null +}; + +export function sessionForAgentKey(agentKeyId: string): Session | null { + const sid = AGENT_KEY_ASSIGNMENTS[agentKeyId]; + return sid ? SESSIONS.find((s) => s.id === sid) ?? null : null; +} + +/* ------------------------------------------------------------------ */ +/* Device controls (real kbd-1.0 layout) */ +/* ------------------------------------------------------------------ */ + +export type ControlKind = 'knob' | 'joystick' | 'touch' | 'agent-key' | 'command-key'; + +export type ControlId = +'knob' | 'joystick' | 'touch' | +'ag1' | 'ag2' | 'ag3' | 'ag4' | 'ag5' | 'ag6' | +'fast' | 'approve' | 'reject' | 'fork' | 'mic' | 'codex'; + +export interface ControlDef { + id: ControlId; + kind: ControlKind; + label: string; + /** Icon printed on the shipped keycap, if any */ + icon?: 'bolt' | 'check' | 'cross' | 'fork' | 'mic' | 'codex'; +} + +export const CONTROLS: Record = { + knob: { id: 'knob', kind: 'knob', label: 'Dial' }, + joystick: { id: 'joystick', kind: 'joystick', label: 'Joystick' }, + touch: { id: 'touch', kind: 'touch', label: 'Touch sensor' }, + ag1: { id: 'ag1', kind: 'agent-key', label: 'Agent Key 1' }, + ag2: { id: 'ag2', kind: 'agent-key', label: 'Agent Key 2' }, + ag3: { id: 'ag3', kind: 'agent-key', label: 'Agent Key 3' }, + ag4: { id: 'ag4', kind: 'agent-key', label: 'Agent Key 4' }, + ag5: { id: 'ag5', kind: 'agent-key', label: 'Agent Key 5' }, + ag6: { id: 'ag6', kind: 'agent-key', label: 'Agent Key 6' }, + fast: { id: 'fast', kind: 'command-key', label: 'Fast key', icon: 'bolt' }, + approve: { id: 'approve', kind: 'command-key', label: 'Approve key', icon: 'check' }, + reject: { id: 'reject', kind: 'command-key', label: 'Reject key', icon: 'cross' }, + fork: { id: 'fork', kind: 'command-key', label: 'Fork key', icon: 'fork' }, + mic: { id: 'mic', kind: 'command-key', label: 'Mic bar', icon: 'mic' }, + codex: { id: 'codex', kind: 'command-key', label: 'Codex key', icon: 'codex' } +}; + +/* ------------------------------------------------------------------ */ +/* Assignable actions */ +/* ------------------------------------------------------------------ */ + +export type ActionGroup = 'AGENT' | 'SKILL' | 'SYSTEM' | 'MACRO'; + +export interface ActionDef { + id: string; + group: ActionGroup; + label: string; +} + +export const ACTIONS: ActionDef[] = [ +{ id: 'approve', group: 'AGENT', label: 'Approve' }, +{ id: 'reject', group: 'AGENT', label: 'Reject' }, +{ id: 'fast_mode', group: 'AGENT', label: 'Toggle fast mode' }, +{ id: 'fork_thread', group: 'AGENT', label: 'Fork thread' }, +{ id: 'new_chat', group: 'AGENT', label: 'New chat' }, +{ id: 'push_to_talk', group: 'AGENT', label: 'Push to talk' }, +{ id: 'interrupt', group: 'AGENT', label: 'Interrupt' }, +{ id: 'skill_review_pr', group: 'SKILL', label: 'Review PR' }, +{ id: 'skill_debug', group: 'SKILL', label: 'Debug error' }, +{ id: 'skill_refactor', group: 'SKILL', label: 'Refactor' }, +{ id: 'skill_explain', group: 'SKILL', label: 'Explain code' }, +{ id: 'skill_tests', group: 'SKILL', label: 'Write tests' }, +{ id: 'pause_leds', group: 'SYSTEM', label: 'Pause LEDs' }, +{ id: 'cycle_focus', group: 'SYSTEM', label: 'Cycle focus' }, +{ id: 'custom_command', group: 'MACRO', label: 'Custom command…' }, +{ id: 'send_keystroke', group: 'MACRO', label: 'Send keystroke' }]; + + +export const ACTION_GROUP_LABELS: Record = { + AGENT: 'Agent', + SKILL: 'Skills', + SYSTEM: 'System', + MACRO: 'Macro' +}; + +export function getAction(id: string): ActionDef { + return ACTIONS.find((a) => a.id === id) ?? ACTIONS[0]; +} + +/* Knob rotate options */ +export interface RotateActionDef { + id: string; + label: string; +} + +export const ROTATE_ACTIONS: RotateActionDef[] = [ +{ id: 'reasoning_effort', label: 'Reasoning effort' }, +{ id: 'scroll_thread', label: 'Scroll thread' }, +{ id: 'brightness', label: 'LED brightness' }, +{ id: 'cycle_sessions', label: 'Cycle sessions' }]; + + +export type JoyDir = 'up' | 'down' | 'left' | 'right'; +export const JOY_DIRS: {id: JoyDir;label: string;}[] = [ +{ id: 'up', label: 'Flick up' }, +{ id: 'down', label: 'Flick down' }, +{ id: 'left', label: 'Flick left' }, +{ id: 'right', label: 'Flick right' }]; + + +/* ------------------------------------------------------------------ */ +/* Bindings (factory defaults, matching the shipped caps) */ +/* ------------------------------------------------------------------ */ + +export interface DeviceBindings { + commandKeys: Record; // controlId -> actionId + knobRotate: string; + knobPress: string; + joystick: Record; + joystickPress: string; + touch: string; +} + +export const DEFAULT_BINDINGS: DeviceBindings = { + commandKeys: { + fast: 'fast_mode', + approve: 'approve', + reject: 'reject', + fork: 'fork_thread', + mic: 'push_to_talk', + codex: 'new_chat' + }, + knobRotate: 'reasoning_effort', + knobPress: 'cycle_focus', + joystick: { + up: 'skill_review_pr', + down: 'skill_debug', + left: 'skill_refactor', + right: 'skill_explain' + }, + joystickPress: 'pause_leds', + touch: 'pause_leds' +}; + +export const REASONING_LEVELS = ['Light', 'Standard', 'High', 'Extra High']; + +/* ------------------------------------------------------------------ */ +/* Adapters */ +/* ------------------------------------------------------------------ */ + +export interface AdapterDef { + id: string; + name: string; + badge: 'NATIVE' | 'COMMUNITY'; + status: 'connected' | 'beta' | 'not_installed'; + detail: string; + footprint?: string; +} + +export const ADAPTERS: AdapterDef[] = [ +{ id: 'codex', name: 'Codex', badge: 'NATIVE', status: 'connected', detail: 'watching ~/.codex/sessions', footprint: '0.0% CPU idle' }, +{ id: 'claude', name: 'Claude Code', badge: 'NATIVE', status: 'connected', detail: 'hooks + session watcher', footprint: '0.1% CPU idle' }, +{ id: 'cursor', name: 'Cursor', badge: 'COMMUNITY', status: 'beta', detail: 'polling workspace state', footprint: '0.3% CPU idle' }, +{ id: 't3', name: 'T3 Code', badge: 'COMMUNITY', status: 'not_installed', detail: 'adapter not installed' }]; + + +export interface FocusAppDef { + id: string; + name: string; +} + +export const FOCUS_APPS_DEFAULT: FocusAppDef[] = [ +{ id: 'codex', name: 'Codex' }, +{ id: 'claude', name: 'Claude Code' }, +{ id: 'cursor', name: 'Cursor' }, +{ id: 't3', name: 'T3 Code' }]; \ No newline at end of file diff --git a/apps/microbridge-ui/vendor/magicpath/settings/generated/theme.tsx b/apps/microbridge-ui/vendor/magicpath/settings/generated/theme.tsx new file mode 100644 index 0000000..4be1e71 --- /dev/null +++ b/apps/microbridge-ui/vendor/magicpath/settings/generated/theme.tsx @@ -0,0 +1,95 @@ +import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'; +export type ThemeChoice = 'system' | 'light' | 'dark'; +export type ResolvedTheme = 'light' | 'dark'; +export interface ThemeTokens { + name: ResolvedTheme; + /** desk behind the window */ + frame: string; + /** frosted window material */ + panel: string; + panelBorder: string; + /** cards inside the window */ + card: string; + cardBorder: string; + sunken: string; + raised: string; + hairline: string; + text: string; + textSecondary: string; + textMuted: string; + /** selection ring on interactive controls */ + ring: string; + hoverBg: string; +} +export const LIGHT: ThemeTokens = { + name: 'light', + frame: '#E9E9E7', + panel: 'rgba(252,252,251,0.86)', + panelBorder: 'rgba(0,0,0,0.10)', + card: '#FFFFFF', + cardBorder: 'rgba(0,0,0,0.08)', + sunken: '#F4F4F2', + raised: '#FFFFFF', + hairline: 'rgba(0,0,0,0.08)', + text: '#0D0D0D', + textSecondary: '#6E6E73', + textMuted: '#AEAEB2', + ring: '#0D0D0D', + hoverBg: 'rgba(0,0,0,0.04)' +}; +export const DARK: ThemeTokens = { + name: 'dark', + frame: '#0A0A0B', + panel: 'rgba(24,24,26,0.88)', + panelBorder: 'rgba(255,255,255,0.10)', + card: 'rgba(255,255,255,0.05)', + cardBorder: 'rgba(255,255,255,0.08)', + sunken: 'rgba(0,0,0,0.25)', + raised: 'rgba(255,255,255,0.09)', + hairline: 'rgba(255,255,255,0.09)', + text: '#F5F5F4', + textSecondary: '#A0A0A6', + textMuted: '#5E5E66', + ring: '#F5F5F4', + hoverBg: 'rgba(255,255,255,0.06)' +}; +interface ThemeContextValue { + choice: ThemeChoice; + setChoice: (c: ThemeChoice) => void; + resolved: ResolvedTheme; + t: ThemeTokens; +} +const ThemeContext = createContext({ + choice: 'light', + setChoice: () => {}, + resolved: 'light', + t: LIGHT +}); +export function ThemeProvider({ + children, + defaultChoice = 'light' +}: { + children: ReactNode; + defaultChoice?: ThemeChoice; +}) { + const [choice, setChoice] = useState(defaultChoice); + const [systemDark, setSystemDark] = useState(false); + useEffect(() => { + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + setSystemDark(mq.matches); + const onChange = (e: MediaQueryListEvent) => setSystemDark(e.matches); + mq.addEventListener('change', onChange); + return () => mq.removeEventListener('change', onChange); + }, []); + const resolved: ResolvedTheme = choice === 'system' ? systemDark ? 'dark' : 'light' : choice; + const value = useMemo(() => ({ + choice, + setChoice, + resolved, + t: resolved === 'dark' ? DARK : LIGHT + }), [choice, resolved]); + return {children}; +} +export function useTheme() { + return useContext(ThemeContext); +} \ No newline at end of file diff --git a/apps/microbridge-ui/vite.config.ts b/apps/microbridge-ui/vite.config.ts new file mode 100644 index 0000000..a061ac3 --- /dev/null +++ b/apps/microbridge-ui/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +const host = process.env.TAURI_DEV_HOST; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + clearScreen: false, + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host ? { protocol: "ws", host, port: 1421 } : undefined, + watch: { ignored: ["**/src-tauri/**"] }, + }, +}); diff --git a/crates/mb-adapters/Cargo.toml b/crates/mb-adapters/Cargo.toml new file mode 100644 index 0000000..8ba3d21 --- /dev/null +++ b/crates/mb-adapters/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "mb-adapters" +description = "First-party in-process adapters for Microbridge" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +mb-protocol = { path = "../mb-protocol" } +notify = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/mb-adapters/src/claude.rs b/crates/mb-adapters/src/claude.rs new file mode 100644 index 0000000..12302ee --- /dev/null +++ b/crates/mb-adapters/src/claude.rs @@ -0,0 +1,127 @@ +//! Claude Code in-process adapter. +//! +//! Prefers project-local Claude session journals when present +//! (`~/.claude/projects` or `~/.config/claude`). Maps coarse status fields +//! onto [`mb_protocol::AgentState`]. Official hooks can later replace file +//! watching without changing the bus contract. + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +use mb_protocol::{AgentState, SessionStatus}; +use serde_json::Value; +use tracing::debug; + +use crate::watch::watch_dir; +use crate::{AdapterEvent, AdapterTx}; + +pub fn spawn_claude_adapter(tx: AdapterTx) { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + let candidates = [ + PathBuf::from(&home).join(".claude").join("projects"), + PathBuf::from(&home) + .join(".config") + .join("claude") + .join("projects"), + ]; + + for root in candidates { + let tx = tx.clone(); + let seen: Arc>> = Arc::new(Mutex::new(HashMap::new())); + let seen_cb = Arc::clone(&seen); + watch_dir(root, move |path| { + if let Some(session) = parse_claude_session(&path) { + let mut map = seen_cb.lock().unwrap(); + if map.get(&session.id) == Some(&session.state) { + return; + } + map.insert(session.id.clone(), session.state); + drop(map); + debug!(id = %session.id, ?session.state, "claude session"); + let _ = tx.send(AdapterEvent::Upsert(session)); + } + }); + } +} + +fn parse_claude_session(path: &std::path::Path) -> Option { + let text = std::fs::read_to_string(path).ok()?; + let value: Value = if path.extension().and_then(|e| e.to_str()) == Some("jsonl") { + let last = text.lines().rev().find(|l| !l.trim().is_empty())?; + serde_json::from_str(last).ok()? + } else { + serde_json::from_str(&text).ok()? + }; + + let id_raw = value + .get("sessionId") + .or_else(|| value.get("session_id")) + .or_else(|| value.get("id")) + .and_then(|v| v.as_str()) + .or_else(|| path.file_stem().and_then(|s| s.to_str()))?; + + let title = value + .get("summary") + .or_else(|| value.get("title")) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let state = map_state(&value); + let updated_at_ms = value + .get("updated_at_ms") + .and_then(|v| v.as_u64()) + .unwrap_or_else(now_ms); + + Some(SessionStatus { + id: format!("claude:{id_raw}"), + app: "Claude Code".into(), + title, + state, + updated_at_ms, + }) +} + +fn map_state(value: &Value) -> AgentState { + let raw = value + .get("status") + .or_else(|| value.get("state")) + .and_then(|v| v.as_str()) + .unwrap_or("idle") + .to_ascii_lowercase(); + match raw.as_str() { + "thinking" => AgentState::Thinking, + "working" | "running" | "tool_use" => AgentState::Working, + "awaiting_approval" | "permission" | "needs_permission" => AgentState::AwaitingApproval, + "done" | "completed" | "idle" => { + if raw == "idle" { + AgentState::Idle + } else { + AgentState::Done + } + } + "error" | "failed" => AgentState::Error, + _ => AgentState::Working, + } +} + +fn now_ms() -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn maps_permission() { + let v = json!({"status": "needs_permission"}); + assert_eq!(map_state(&v), AgentState::AwaitingApproval); + } +} diff --git a/crates/mb-adapters/src/codex.rs b/crates/mb-adapters/src/codex.rs new file mode 100644 index 0000000..449946a --- /dev/null +++ b/crates/mb-adapters/src/codex.rs @@ -0,0 +1,144 @@ +//! Codex CLI in-process adapter. +//! +//! Watches `~/.codex/sessions` for JSON/JSONL session journals and maps +//! coarse lifecycle fields onto [`mb_protocol::AgentState`]. Action routing +//! (approve/reject) is logged until the local Codex surface exposes a stable +//! hook — see the adapter README notes in `docs/adapters.md`. + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +use mb_protocol::{AgentState, SessionStatus}; +use serde_json::Value; +use tracing::{debug, warn}; + +use crate::watch::watch_dir; +use crate::{AdapterEvent, AdapterTx}; + +pub fn spawn_codex_adapter(tx: AdapterTx) { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + let root = PathBuf::from(home).join(".codex").join("sessions"); + let seen: Arc>> = Arc::new(Mutex::new(HashMap::new())); + + let seen_cb = Arc::clone(&seen); + watch_dir(root, move |path| { + if let Some(session) = parse_codex_session(&path) { + let mut map = seen_cb.lock().unwrap(); + let prev = map.get(&session.id).copied(); + if prev == Some(session.state) { + return; + } + map.insert(session.id.clone(), session.state); + drop(map); + debug!(id = %session.id, ?session.state, "codex session"); + let _ = tx.send(AdapterEvent::Upsert(session)); + } else if path + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e == "json" || e == "jsonl") + { + // File removed or unreadable — best-effort bye from filename. + if let Some(id) = path.file_stem().and_then(|s| s.to_str()) { + let sid = format!("codex:{id}"); + let mut map = seen_cb.lock().unwrap(); + if map.remove(&sid).is_some() { + let _ = tx.send(AdapterEvent::Remove(sid)); + } + } + } + }); +} + +fn parse_codex_session(path: &std::path::Path) -> Option { + let text = std::fs::read_to_string(path).ok()?; + // Prefer last JSON object from jsonl; otherwise whole file. + let value = if path.extension().and_then(|e| e.to_str()) == Some("jsonl") { + let last = text.lines().rev().find(|l| !l.trim().is_empty())?; + serde_json::from_str::(last).ok()? + } else { + serde_json::from_str::(&text).ok()? + }; + + let id_raw = value + .get("id") + .or_else(|| value.get("session_id")) + .or_else(|| value.get("thread_id")) + .and_then(|v| v.as_str()) + .or_else(|| path.file_stem().and_then(|s| s.to_str()))?; + + let title = value + .get("title") + .or_else(|| value.get("summary")) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let state = map_state(&value); + let updated_at_ms = value + .get("updated_at_ms") + .and_then(|v| v.as_u64()) + .or_else(|| { + value + .get("updated_at") + .and_then(|v| v.as_str()) + .and_then(parse_iso_ms) + }) + .unwrap_or_else(now_ms); + + Some(SessionStatus { + id: format!("codex:{id_raw}"), + app: "Codex CLI".into(), + title, + state, + updated_at_ms, + }) +} + +fn map_state(value: &Value) -> AgentState { + let raw = value + .get("state") + .or_else(|| value.get("status")) + .and_then(|v| v.as_str()) + .unwrap_or("idle") + .to_ascii_lowercase(); + + match raw.as_str() { + "thinking" | "reasoning" => AgentState::Thinking, + "working" | "running" | "in_progress" | "active" => AgentState::Working, + "awaiting_approval" | "awaiting_input" | "needs_approval" | "approval" => { + AgentState::AwaitingApproval + } + "done" | "completed" | "complete" | "finished" => AgentState::Done, + "error" | "failed" => AgentState::Error, + "idle" | "ready" => AgentState::Idle, + other => { + warn!(state = other, "unknown codex state; treating as idle"); + AgentState::Idle + } + } +} + +fn parse_iso_ms(_s: &str) -> Option { + None +} + +fn now_ms() -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn maps_awaiting_approval() { + let v = json!({"status": "awaiting_input"}); + assert_eq!(map_state(&v), AgentState::AwaitingApproval); + } +} diff --git a/crates/mb-adapters/src/lib.rs b/crates/mb-adapters/src/lib.rs new file mode 100644 index 0000000..fe3134b --- /dev/null +++ b/crates/mb-adapters/src/lib.rs @@ -0,0 +1,23 @@ +//! First-party in-process adapters. +//! +//! These watch local session stores with FSEvents/inotify (via `notify`) and +//! publish transitions into the daemon bus. They never talk to the device. + +mod claude; +mod codex; +mod watch; + +use mb_protocol::SessionStatus; +use tokio::sync::mpsc; + +pub use claude::spawn_claude_adapter; +pub use codex::spawn_codex_adapter; + +/// Events emitted by in-process adapters toward the daemon bus. +#[derive(Debug, Clone)] +pub enum AdapterEvent { + Upsert(SessionStatus), + Remove(String), +} + +pub type AdapterTx = mpsc::UnboundedSender; diff --git a/crates/mb-adapters/src/watch.rs b/crates/mb-adapters/src/watch.rs new file mode 100644 index 0000000..655b7b9 --- /dev/null +++ b/crates/mb-adapters/src/watch.rs @@ -0,0 +1,113 @@ +//! Shared recursive directory watcher (event-driven via `notify`). + +use std::path::{Path, PathBuf}; +use std::sync::mpsc as std_mpsc; +use std::thread; +use std::time::{Duration, SystemTime}; + +use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use tracing::{info, warn}; + +/// How recent a file must be (mtime) to be published on the initial scan. +const INITIAL_MAX_AGE: Duration = Duration::from_secs(60 * 60 * 24); // 24h + +/// Spawn a background thread that watches `root` and invokes `on_change` +/// whenever a matching file is created/modified. Initial scan only includes +/// files touched within the last 24 hours so historical journals don't flood +/// the bus. +pub fn watch_dir(root: PathBuf, mut on_change: impl FnMut(PathBuf) + Send + 'static) { + if !root.exists() { + info!(path = %root.display(), "session dir absent; adapter idle"); + return; + } + + thread::Builder::new() + .name("mb-watch".into()) + .spawn(move || { + let (tx, rx) = std_mpsc::channel(); + let mut watcher = match RecommendedWatcher::new( + move |res: Result| { + if let Ok(event) = res { + let _ = tx.send(event); + } + }, + notify::Config::default(), + ) { + Ok(w) => w, + Err(error) => { + warn!(%error, "failed to create watcher"); + return; + } + }; + + if let Err(error) = watcher.watch(&root, RecursiveMode::Recursive) { + warn!(%error, path = %root.display(), "failed to watch"); + return; + } + info!(path = %root.display(), "watching session directory"); + + scan_recent(&root, &mut on_change); + + while let Ok(event) = rx.recv() { + match event.kind { + EventKind::Create(_) | EventKind::Modify(_) => { + while rx.try_recv().is_ok() {} + thread::sleep(Duration::from_millis(80)); + while rx.try_recv().is_ok() {} + for path in event.paths { + if is_session_file(&path) { + on_change(path); + } + } + } + EventKind::Remove(_) => { + for path in event.paths { + if is_session_file(&path) { + on_change(path); + } + } + } + _ => {} + } + } + }) + .ok(); +} + +fn is_session_file(path: &Path) -> bool { + path.extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e == "json" || e == "jsonl") +} + +fn scan_recent(root: &Path, on_change: &mut impl FnMut(PathBuf)) { + let cutoff = SystemTime::now() + .checked_sub(INITIAL_MAX_AGE) + .unwrap_or(SystemTime::UNIX_EPOCH); + walk_recent(root, cutoff, on_change); +} + +fn walk_recent(root: &Path, cutoff: SystemTime, on_change: &mut impl FnMut(PathBuf)) { + let Ok(entries) = std::fs::read_dir(root) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + walk_recent(&path, cutoff, on_change); + continue; + } + if !is_session_file(&path) { + continue; + } + let Ok(meta) = entry.metadata() else { + continue; + }; + let Ok(mtime) = meta.modified() else { + continue; + }; + if mtime >= cutoff { + on_change(path); + } + } +} diff --git a/crates/mb-device/src/lib.rs b/crates/mb-device/src/lib.rs index f11b48c..6e5d826 100644 --- a/crates/mb-device/src/lib.rs +++ b/crates/mb-device/src/lib.rs @@ -1,35 +1,193 @@ //! Device abstraction: turns resolved agent state into hardware output. //! -//! Real Codex Micro HID support lands in M2 (see ROADMAP.md). Until then the -//! daemon drives [`MockDevice`], which logs the frames a real device would -//! render. +//! Real Codex Micro HID support lands behind [`HidDevice`] (best-effort). +//! Until a device is present the daemon drives [`MockDevice`], which logs the +//! frames a real device would render. All reverse-engineering stays in this +//! crate — see `docs/device-hid.md`. -use mb_protocol::AgentState; +use mb_protocol::{AgentState, AGENT_KEY_COUNT}; + +/// Descriptor-driven layout reported by a connected device (or the mock). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceDescriptor { + pub name: String, + pub agent_key_count: usize, + pub has_dial: bool, + pub has_joystick: bool, + pub connected: bool, +} + +impl Default for DeviceDescriptor { + fn default() -> Self { + Self { + name: "mock".into(), + agent_key_count: AGENT_KEY_COUNT, + has_dial: true, + has_joystick: true, + connected: false, + } + } +} + +/// Physical / logical input from the deck. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DeviceInput { + /// Agent Key index 0..5 — single press. + AgentKeyPress { + index: usize, + }, + /// Agent Key index 0..5 — second press within 350ms. + AgentKeyDoublePress { + index: usize, + }, + Approve, + Reject, + Interrupt, + NewSession, + CycleFocus, + DialRotate { + delta: i8, + }, + DialPress, + JoystickFlick { + direction: JoystickDir, + }, + TouchTap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JoystickDir { + Up, + Down, + Left, + Right, +} + +/// Frame rendered onto the six Agent Keys (+ optional focus highlight). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LedFrame { + pub keys: [Option; AGENT_KEY_COUNT], + pub focus_index: Option, + pub brightness: u8, + pub paused: bool, +} + +impl Default for LedFrame { + fn default() -> Self { + Self { + keys: [None; AGENT_KEY_COUNT], + focus_index: None, + brightness: 80, + paused: false, + } + } +} pub trait Device: Send { - fn name(&self) -> &str; + fn descriptor(&self) -> DeviceDescriptor; + + /// Render Agent Key LEDs. Called only on transitions — implementations + /// may assume calls are rare and need not debounce. + fn set_leds(&mut self, frame: &LedFrame); - /// Render the focused session's state, or clear the deck when nothing is - /// focused. Called only on transitions — implementations may assume calls - /// are rare and need not debounce. - fn set_state(&mut self, state: Option); + /// Poll for a pending input event (non-blocking). Mock returns None. + fn poll_input(&mut self) -> Option { + None + } } /// Logs what a real device would display. #[derive(Debug, Default)] pub struct MockDevice { - last: Option, + last: LedFrame, } impl Device for MockDevice { - fn name(&self) -> &str { - "mock" + fn descriptor(&self) -> DeviceDescriptor { + DeviceDescriptor { + name: "mock".into(), + agent_key_count: AGENT_KEY_COUNT, + has_dial: true, + has_joystick: true, + connected: false, + } + } + + fn set_leds(&mut self, frame: &LedFrame) { + if &self.last != frame { + tracing::info!( + device = "mock", + keys = ?frame.keys, + focus = ?frame.focus_index, + paused = frame.paused, + "render frame" + ); + self.last = frame.clone(); + } + } +} + +/// Best-effort USB HID driver for the Codex Micro. +/// +/// Without a probed device this behaves like [`MockDevice`] and reports +/// `connected: false`. Real report packing is documented in +/// `docs/device-hid.md` and filled in as the HID map is confirmed. +#[derive(Debug)] +pub struct HidDevice { + inner: MockDevice, + connected: bool, + name: String, +} + +impl Default for HidDevice { + fn default() -> Self { + Self { + inner: MockDevice::default(), + connected: false, + name: "codex-micro".into(), + } + } +} + +impl HidDevice { + /// Attempt to open the first matching USB device. Falls back to + /// disconnected (mock rendering) when none is found or HID is unavailable. + pub fn open() -> Self { + // HID open is gated on platform + discovered VID/PID (see docs). + // Until the report map is verified we never claim exclusive access. + Self::default() + } + + pub fn set_connected_for_tests(&mut self, connected: bool) { + self.connected = connected; + } +} + +impl Device for HidDevice { + fn descriptor(&self) -> DeviceDescriptor { + DeviceDescriptor { + name: self.name.clone(), + agent_key_count: AGENT_KEY_COUNT, + has_dial: true, + has_joystick: true, + connected: self.connected, + } } - fn set_state(&mut self, state: Option) { - if self.last != state { - tracing::info!(device = self.name(), ?state, "render frame"); - self.last = state; + fn set_leds(&mut self, frame: &LedFrame) { + if self.connected { + tracing::debug!(device = %self.name, keys = ?frame.keys, "hid led frame"); } + self.inner.set_leds(frame); + } +} + +/// Prefer a live HID device when one can be claimed; otherwise mock. +pub fn open_default_device() -> Box { + let hid = HidDevice::open(); + if hid.descriptor().connected { + Box::new(hid) + } else { + Box::new(MockDevice::default()) } } diff --git a/crates/mb-protocol/src/lib.rs b/crates/mb-protocol/src/lib.rs index e11c14c..390aed7 100644 --- a/crates/mb-protocol/src/lib.rs +++ b/crates/mb-protocol/src/lib.rs @@ -1,4 +1,4 @@ -//! Wire types for the Microbridge adapter protocol. +//! Wire types for the Microbridge adapter / UI protocol. //! //! Transport is newline-delimited JSON over a local Unix domain socket. //! `docs/protocol.md` is the normative spec; these types are its source of @@ -6,12 +6,15 @@ use serde::{Deserialize, Serialize}; -/// Protocol revision. Bumped on breaking changes; adapters announce theirs in -/// [`Message::Hello`]. +/// Protocol revision. Bumped on breaking changes; clients announce theirs in +/// [`ClientMessage::Hello`]. pub const PROTOCOL_VERSION: u32 = 0; +/// Number of Agent Keys on the Codex Micro (kbd-1.0). +pub const AGENT_KEY_COUNT: usize = 6; + /// Lifecycle state of one agent session, as reported by an adapter. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AgentState { Idle, @@ -37,27 +40,222 @@ pub struct SessionStatus { pub updated_at_ms: u64, } -/// Adapter → daemon messages. +/// Who is speaking on the socket. Additive in v0; omitted ⇒ adapter. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ClientRole { + #[default] + Adapter, + Ui, +} + +/// How the six Agent Keys are filled from the session bus. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum KeySource { + /// Cross-app: six most recently updated sessions (default). + #[default] + MostRecent, + /// All six keys re-populate from whichever app owns the deck. + FocusedApp, + /// Follow the first six pinned session ids. + Pinned, + /// Approvals / active / recent priority ordering. + Priority, + /// Explicit per-key session ids (null = unassigned). + Custom, +} + +/// Appearance preference for the optional companion UI. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Appearance { + #[default] + System, + Light, + Dark, +} + +/// Lighting palette name (colors live in config, not on the wire as states). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum LightingPreset { + #[default] + Codex, + Phosphor, + Custom, +} + +/// Per-state RGB as `#RRGGBB`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct StateColors { + pub idle: String, + pub thinking: String, + pub working: String, + pub awaiting_approval: String, + pub done: String, + pub error: String, +} + +impl Default for StateColors { + fn default() -> Self { + Self::codex() + } +} + +impl StateColors { + pub fn codex() -> Self { + Self { + idle: "#E9E9E6".into(), + thinking: "#3D7EFF".into(), + working: "#3D7EFF".into(), + awaiting_approval: "#FFB000".into(), + done: "#30C463".into(), + error: "#FF453A".into(), + } + } + + pub fn phosphor() -> Self { + Self { + idle: "#4A4A52".into(), + thinking: "#FFB454".into(), + working: "#FF6A00".into(), + awaiting_approval: "#FF3D00".into(), + done: "#3DDC84".into(), + error: "#FF4757".into(), + } + } +} + +/// Persistent daemon configuration (`~/.microbridge/config.toml`). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DaemonConfig { + #[serde(default)] + pub key_source: KeySource, + /// Session ids for `KeySource::Pinned`. + #[serde(default)] + pub pinned_session_ids: Vec, + /// App names in priority order (higher first) for `KeySource::Priority`. + #[serde(default)] + pub app_priority: Vec, + /// Explicit assignments for `KeySource::Custom` (len ≤ 6; empty string = unassigned). + #[serde(default)] + pub custom_key_ids: Vec, + /// When set, this session owns the deck until cleared. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pinned_focus: Option, + /// Approvals preempt focus (default true). + #[serde(default = "default_true")] + pub approvals_interrupt: bool, + #[serde(default)] + pub pause_leds: bool, + #[serde(default)] + pub appearance: Appearance, + #[serde(default)] + pub lighting_preset: LightingPreset, + #[serde(default)] + pub state_colors: StateColors, + /// 0–100 + #[serde(default = "default_brightness")] + pub brightness: u8, + /// Minutes of idle before LEDs sleep; 0 = never. Default 3. + #[serde(default = "default_sleep_minutes")] + pub sleep_minutes: u32, + /// Frontmost app name (updated by companion / NSWorkspace). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub frontmost_app: Option, +} + +fn default_true() -> bool { + true +} +fn default_brightness() -> u8 { + 80 +} +fn default_sleep_minutes() -> u32 { + 3 +} + +impl Default for DaemonConfig { + fn default() -> Self { + Self { + key_source: KeySource::MostRecent, + pinned_session_ids: Vec::new(), + app_priority: Vec::new(), + custom_key_ids: vec![String::new(); AGENT_KEY_COUNT], + pinned_focus: None, + approvals_interrupt: true, + pause_leds: false, + appearance: Appearance::System, + lighting_preset: LightingPreset::Codex, + state_colors: StateColors::codex(), + brightness: 80, + sleep_minutes: 3, + frontmost_app: None, + } + } +} + +/// Full bus view pushed to UI clients after `subscribe`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Snapshot { + pub sessions: Vec, + pub focused_session_id: Option, + /// Six Agent Key slots — session ids or null. + pub agent_key_session_ids: Vec>, + pub device_connected: bool, + pub device_name: String, + pub config: DaemonConfig, +} + +/// Incremental bus change for subscribed UI clients. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum BusEvent { + SessionUpserted { session: SessionStatus }, + SessionRemoved { session_id: String }, + FocusChanged { session_id: Option }, + AgentKeysChanged { session_ids: Vec> }, + DeviceChanged { connected: bool, name: String }, + ConfigChanged { config: DaemonConfig }, +} + +/// Client → daemon messages (adapters and UI share the socket). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] -pub enum Message { +pub enum ClientMessage { /// Must be the first message on every connection. Hello { adapter: String, protocol_version: u32, + #[serde(default)] + role: ClientRole, }, /// Full state for one session. Sent on every transition — never on a /// timer. The daemon treats each `status` as a complete replacement. Status { session: SessionStatus }, /// The session ended and should be dropped from the registry. Bye { session_id: String }, + /// UI: request a full [`Snapshot`] and subsequent [`BusEvent`]s. + Subscribe, + /// UI: fetch current config (also included in snapshot). + GetConfig, + /// UI: replace config and persist. + SetConfig { config: DaemonConfig }, } -/// Daemon → adapter messages: key presses routed to the focused session. +/// Daemon → client messages. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] -pub enum Command { +pub enum ServerMessage { + /// Key press routed to a session the adapter owns. Action { session_id: String, action: Action }, + /// Full bus view (response to Subscribe / reconnect). + Snapshot { snapshot: Snapshot }, + /// Incremental update for subscribed UI clients. + Event { event: BusEvent }, + /// Response to GetConfig / acknowledgment of SetConfig. + Config { config: DaemonConfig }, } /// Actions a physical key can trigger on the focused agent. @@ -71,13 +269,17 @@ pub enum Action { CycleFocus, } +/// Backward-compatible aliases used by older docs / reference adapter. +pub type Message = ClientMessage; +pub type Command = ServerMessage; + #[cfg(test)] mod tests { use super::*; #[test] fn status_round_trips() { - let msg = Message::Status { + let msg = ClientMessage::Status { session: SessionStatus { id: "codex:abc".into(), app: "Codex CLI".into(), @@ -89,15 +291,49 @@ mod tests { let json = serde_json::to_string(&msg).unwrap(); assert!(json.contains(r#""type":"status""#)); assert!(json.contains(r#""state":"awaiting_approval""#)); - assert_eq!(serde_json::from_str::(&json).unwrap(), msg); + assert_eq!(serde_json::from_str::(&json).unwrap(), msg); } #[test] fn title_defaults_to_empty() { let json = r#"{"type":"status","session":{"id":"x:1","app":"X","state":"idle","updated_at_ms":0}}"#; - let Message::Status { session } = serde_json::from_str(json).unwrap() else { + let ClientMessage::Status { session } = serde_json::from_str(json).unwrap() else { panic!("expected status"); }; assert_eq!(session.title, ""); } + + #[test] + fn hello_role_defaults_to_adapter() { + let json = r#"{"type":"hello","adapter":"reference-echo","protocol_version":0}"#; + let ClientMessage::Hello { role, .. } = serde_json::from_str(json).unwrap() else { + panic!("expected hello"); + }; + assert_eq!(role, ClientRole::Adapter); + } + + #[test] + fn ui_hello_and_snapshot_round_trip() { + let hello = ClientMessage::Hello { + adapter: "microbridge-ui".into(), + protocol_version: 0, + role: ClientRole::Ui, + }; + let json = serde_json::to_string(&hello).unwrap(); + assert!(json.contains(r#""role":"ui""#)); + + let snap = ServerMessage::Snapshot { + snapshot: Snapshot { + sessions: vec![], + focused_session_id: None, + agent_key_session_ids: vec![None; AGENT_KEY_COUNT], + device_connected: false, + device_name: "mock".into(), + config: DaemonConfig::default(), + }, + }; + let round = + serde_json::from_str::(&serde_json::to_string(&snap).unwrap()).unwrap(); + assert_eq!(round, snap); + } } diff --git a/crates/microbridgectl/Cargo.toml b/crates/microbridgectl/Cargo.toml new file mode 100644 index 0000000..35c03e6 --- /dev/null +++ b/crates/microbridgectl/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "microbridgectl" +description = "CLI for inspecting a running microbridged instance" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[[bin]] +name = "microbridgectl" +path = "src/main.rs" + +[dependencies] +mb-protocol = { path = "../mb-protocol" } +serde_json = { workspace = true } +tokio = { workspace = true } diff --git a/crates/microbridgectl/src/main.rs b/crates/microbridgectl/src/main.rs new file mode 100644 index 0000000..f508deb --- /dev/null +++ b/crates/microbridgectl/src/main.rs @@ -0,0 +1,92 @@ +//! microbridgectl — inspect a running microbridged instance. + +use std::path::PathBuf; +use std::process::ExitCode; + +use mb_protocol::{ClientMessage, ClientRole, ServerMessage, PROTOCOL_VERSION}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::UnixStream; + +fn socket_path() -> PathBuf { + if let Ok(path) = std::env::var("MICROBRIDGE_SOCKET") { + return PathBuf::from(path); + } + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + PathBuf::from(home) + .join(".microbridge") + .join("microbridged.sock") +} + +#[tokio::main] +async fn main() -> ExitCode { + let mut args = std::env::args().skip(1); + let cmd = args.next().unwrap_or_else(|| "status".into()); + + match cmd.as_str() { + "status" => match fetch_snapshot().await { + Ok(json) => { + println!("{json}"); + ExitCode::SUCCESS + } + Err(error) => { + eprintln!("microbridgectl: {error}"); + eprintln!("is the daemon running? (cargo run -p microbridged)"); + ExitCode::FAILURE + } + }, + "help" | "-h" | "--help" => { + println!("Usage: microbridgectl [status]"); + println!(" status print the live bus snapshot as JSON (default)"); + ExitCode::SUCCESS + } + other => { + eprintln!("unknown command: {other}"); + eprintln!("Usage: microbridgectl [status]"); + ExitCode::FAILURE + } + } +} + +async fn fetch_snapshot() -> Result { + let path = socket_path(); + let stream = UnixStream::connect(&path) + .await + .map_err(|e| format!("connect {}: {e}", path.display()))?; + let (read_half, mut write_half) = stream.into_split(); + + let hello = ClientMessage::Hello { + adapter: "microbridgectl".into(), + protocol_version: PROTOCOL_VERSION, + role: ClientRole::Ui, + }; + write_line(&mut write_half, &hello).await?; + write_line(&mut write_half, &ClientMessage::Subscribe).await?; + + let mut lines = BufReader::new(read_half).lines(); + while let Some(line) = lines.next_line().await.map_err(|e| format!("read: {e}"))? { + if line.trim().is_empty() { + continue; + } + let msg: ServerMessage = serde_json::from_str(&line).map_err(|e| format!("parse: {e}"))?; + if let ServerMessage::Snapshot { snapshot } = msg { + return serde_json::to_string_pretty(&snapshot).map_err(|e| format!("serialize: {e}")); + } + } + Err("daemon closed before sending snapshot".into()) +} + +async fn write_line( + write_half: &mut tokio::net::unix::OwnedWriteHalf, + msg: &ClientMessage, +) -> Result<(), String> { + let line = serde_json::to_string(msg).map_err(|e| e.to_string())?; + write_half + .write_all(line.as_bytes()) + .await + .map_err(|e| e.to_string())?; + write_half + .write_all(b"\n") + .await + .map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/crates/microbridged/Cargo.toml b/crates/microbridged/Cargo.toml index 7cee414..0a8c175 100644 --- a/crates/microbridged/Cargo.toml +++ b/crates/microbridged/Cargo.toml @@ -6,10 +6,21 @@ edition.workspace = true license.workspace = true repository.workspace = true +[[bin]] +name = "microbridged" +path = "src/main.rs" + +[lib] +name = "microbridged" +path = "src/lib.rs" + [dependencies] +mb-adapters = { path = "../mb-adapters" } mb-device = { path = "../mb-device" } mb-protocol = { path = "../mb-protocol" } +serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } +toml = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/crates/microbridged/src/config.rs b/crates/microbridged/src/config.rs new file mode 100644 index 0000000..df7cf91 --- /dev/null +++ b/crates/microbridged/src/config.rs @@ -0,0 +1,88 @@ +//! Persistent daemon configuration at `~/.microbridge/config.toml`. + +use std::path::{Path, PathBuf}; + +use mb_protocol::DaemonConfig; +use tracing::{info, warn}; + +pub fn microbridge_dir() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + PathBuf::from(home).join(".microbridge") +} + +pub fn config_path() -> PathBuf { + if let Ok(path) = std::env::var("MICROBRIDGE_CONFIG") { + return PathBuf::from(path); + } + microbridge_dir().join("config.toml") +} + +pub fn socket_path() -> PathBuf { + if let Ok(path) = std::env::var("MICROBRIDGE_SOCKET") { + return PathBuf::from(path); + } + microbridge_dir().join("microbridged.sock") +} + +pub fn load_config() -> DaemonConfig { + load_config_from(&config_path()) +} + +pub fn load_config_from(path: &Path) -> DaemonConfig { + match std::fs::read_to_string(path) { + Ok(text) => match toml::from_str::(&text) { + Ok(config) => { + info!(path = %path.display(), "loaded config"); + config + } + Err(error) => { + warn!(%error, path = %path.display(), "invalid config; using defaults"); + DaemonConfig::default() + } + }, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => DaemonConfig::default(), + Err(error) => { + warn!(%error, path = %path.display(), "could not read config; using defaults"); + DaemonConfig::default() + } + } +} + +/// Write config on change only (caller compares). Event-driven — no timers. +pub fn save_config(config: &DaemonConfig) -> std::io::Result<()> { + save_config_to(&config_path(), config) +} + +pub fn save_config_to(path: &Path, config: &DaemonConfig) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let text = toml::to_string_pretty(config) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + std::fs::write(path, text)?; + info!(path = %path.display(), "saved config"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use mb_protocol::KeySource; + + #[test] + fn round_trip_toml() { + let dir = std::env::temp_dir().join(format!("mb-cfg-{}", std::process::id())); + let _ = std::fs::create_dir_all(&dir); + let path = dir.join("config.toml"); + let config = DaemonConfig { + key_source: KeySource::FocusedApp, + pause_leds: true, + ..Default::default() + }; + save_config_to(&path, &config).unwrap(); + let loaded = load_config_from(&path); + assert_eq!(loaded.key_source, KeySource::FocusedApp); + assert!(loaded.pause_leds); + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/crates/microbridged/src/key_source.rs b/crates/microbridged/src/key_source.rs new file mode 100644 index 0000000..a48e95a --- /dev/null +++ b/crates/microbridged/src/key_source.rs @@ -0,0 +1,177 @@ +//! Resolve which sessions occupy the six Agent Keys. + +use mb_protocol::{AgentState, DaemonConfig, KeySource, SessionStatus, AGENT_KEY_COUNT}; + +/// Fill six Agent Key slots from the session bus + config. +pub fn resolve_agent_keys( + sessions: &[SessionStatus], + focused_session_id: Option<&str>, + config: &DaemonConfig, +) -> [Option; AGENT_KEY_COUNT] { + let mut slots = [None, None, None, None, None, None]; + let ids = match config.key_source { + KeySource::MostRecent => most_recent(sessions), + KeySource::FocusedApp => focused_app(sessions, focused_session_id, config), + KeySource::Pinned => pinned(sessions, config), + KeySource::Priority => priority(sessions, config), + KeySource::Custom => custom(config), + }; + for (i, id) in ids.into_iter().take(AGENT_KEY_COUNT).enumerate() { + slots[i] = id; + } + slots +} + +fn most_recent(sessions: &[SessionStatus]) -> Vec> { + let mut sorted: Vec<_> = sessions.iter().collect(); + sorted.sort_by_key(|b| std::cmp::Reverse(b.updated_at_ms)); + pad(sorted.into_iter().map(|s| Some(s.id.clone())).collect()) +} + +fn focused_app( + sessions: &[SessionStatus], + focused_session_id: Option<&str>, + config: &DaemonConfig, +) -> Vec> { + let app = focused_session_id + .and_then(|id| sessions.iter().find(|s| s.id == id)) + .map(|s| s.app.as_str()) + .or(config.frontmost_app.as_deref()); + + let Some(app) = app else { + return most_recent(sessions); + }; + + let mut sorted: Vec<_> = sessions.iter().filter(|s| s.app == app).collect(); + sorted.sort_by_key(|b| std::cmp::Reverse(b.updated_at_ms)); + pad(sorted.into_iter().map(|s| Some(s.id.clone())).collect()) +} + +fn pinned(sessions: &[SessionStatus], config: &DaemonConfig) -> Vec> { + let known: std::collections::HashSet<_> = sessions.iter().map(|s| s.id.as_str()).collect(); + pad(config + .pinned_session_ids + .iter() + .map(|id| { + if known.contains(id.as_str()) { + Some(id.clone()) + } else { + None + } + }) + .collect()) +} + +fn priority(sessions: &[SessionStatus], config: &DaemonConfig) -> Vec> { + let mut sorted: Vec<_> = sessions.iter().collect(); + sorted.sort_by(|a, b| { + priority_rank(a, config) + .cmp(&priority_rank(b, config)) + .then_with(|| b.updated_at_ms.cmp(&a.updated_at_ms)) + }); + pad(sorted.into_iter().map(|s| Some(s.id.clone())).collect()) +} + +fn priority_rank(session: &SessionStatus, config: &DaemonConfig) -> u8 { + let state_rank: u8 = match session.state { + AgentState::AwaitingApproval => 0, + AgentState::Working | AgentState::Thinking => 1, + AgentState::Error => 2, + AgentState::Done => 3, + AgentState::Idle => 4, + }; + let app_rank = config + .app_priority + .iter() + .position(|a| a == &session.app) + .unwrap_or(99) as u8; + state_rank.saturating_add(app_rank / 10) +} + +fn custom(config: &DaemonConfig) -> Vec> { + let mut out: Vec> = config + .custom_key_ids + .iter() + .map(|id| { + if id.is_empty() { + None + } else { + Some(id.clone()) + } + }) + .collect(); + out.resize(AGENT_KEY_COUNT, None); + out +} + +fn pad(mut ids: Vec>) -> Vec> { + ids.resize(AGENT_KEY_COUNT, None); + ids +} + +#[cfg(test)] +mod tests { + use super::*; + + fn session(id: &str, app: &str, state: AgentState, at: u64) -> SessionStatus { + SessionStatus { + id: id.into(), + app: app.into(), + title: String::new(), + state, + updated_at_ms: at, + } + } + + #[test] + fn most_recent_orders_by_updated_at() { + let sessions = vec![ + session("a", "Codex", AgentState::Idle, 1), + session("b", "Cursor", AgentState::Working, 3), + session("c", "Claude Code", AgentState::Thinking, 2), + ]; + let config = DaemonConfig::default(); + let keys = resolve_agent_keys(&sessions, Some("a"), &config); + assert_eq!(keys[0].as_deref(), Some("b")); + assert_eq!(keys[1].as_deref(), Some("c")); + assert_eq!(keys[2].as_deref(), Some("a")); + assert!(keys[3].is_none()); + } + + #[test] + fn focused_app_filters_to_owning_app() { + let sessions = vec![ + session("c1", "Codex", AgentState::Working, 5), + session("c2", "Codex", AgentState::Idle, 4), + session("x1", "Cursor", AgentState::Working, 9), + ]; + let config = DaemonConfig { + key_source: KeySource::FocusedApp, + ..Default::default() + }; + let keys = resolve_agent_keys(&sessions, Some("c1"), &config); + assert_eq!(keys[0].as_deref(), Some("c1")); + assert_eq!(keys[1].as_deref(), Some("c2")); + assert!(keys[2].is_none()); + } + + #[test] + fn custom_assignments_honored() { + let sessions = vec![session("a", "Codex", AgentState::Working, 1)]; + let config = DaemonConfig { + key_source: KeySource::Custom, + custom_key_ids: vec![ + String::new(), + "a".into(), + String::new(), + String::new(), + String::new(), + String::new(), + ], + ..Default::default() + }; + let keys = resolve_agent_keys(&sessions, None, &config); + assert!(keys[0].is_none()); + assert_eq!(keys[1].as_deref(), Some("a")); + } +} diff --git a/crates/microbridged/src/lib.rs b/crates/microbridged/src/lib.rs new file mode 100644 index 0000000..6f62304 --- /dev/null +++ b/crates/microbridged/src/lib.rs @@ -0,0 +1,11 @@ +//! microbridged library — status bus, focus policy, key source, socket server. + +pub mod config; +pub mod key_source; +pub mod registry; +pub mod socket; +pub mod state; + +pub use config::{config_path, load_config, save_config}; +pub use registry::Registry; +pub use state::DaemonState; diff --git a/crates/microbridged/src/main.rs b/crates/microbridged/src/main.rs index e7e3095..afe18b7 100644 --- a/crates/microbridged/src/main.rs +++ b/crates/microbridged/src/main.rs @@ -1,93 +1,18 @@ //! microbridged — the Microbridge daemon. //! -//! Listens on a local Unix socket for adapter status messages, resolves which -//! session owns the device, and renders that session's state. Fully -//! event-driven: the daemon does no work between messages. +//! Listens on a local Unix socket for adapter and UI messages, resolves which +//! session owns the device, and renders Agent Key LEDs. Fully event-driven: +//! the daemon does no work between messages. -use std::collections::HashMap; -use std::path::PathBuf; use std::sync::Arc; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::net::{UnixListener, UnixStream}; -use tokio::sync::Mutex; -use tracing::{info, warn}; - -use mb_device::{Device, MockDevice}; -use mb_protocol::{AgentState, Message, SessionStatus, PROTOCOL_VERSION}; - -#[derive(Default)] -struct Registry { - sessions: HashMap, - focused: Option, -} - -impl Registry { - fn upsert(&mut self, session: SessionStatus) { - self.sessions.insert(session.id.clone(), session); - self.resolve_focus(); - } - - fn remove(&mut self, session_id: &str) { - self.sessions.remove(session_id); - self.resolve_focus(); - } - - /// Focus policy v0: approval requests preempt; otherwise the current focus - /// keeps the deck while its session lives; otherwise the most recently - /// updated session wins. Frontmost-app tracking arrives in M3. - fn resolve_focus(&mut self) { - let approval = self - .sessions - .values() - .filter(|s| s.state == AgentState::AwaitingApproval) - .max_by_key(|s| s.updated_at_ms); - if let Some(session) = approval { - self.focused = Some(session.id.clone()); - return; - } - if let Some(id) = &self.focused { - if self.sessions.contains_key(id) { - return; - } - } - self.focused = self - .sessions - .values() - .max_by_key(|s| s.updated_at_ms) - .map(|s| s.id.clone()); - } - - fn focused_state(&self) -> Option { - self.focused - .as_ref() - .and_then(|id| self.sessions.get(id)) - .map(|s| s.state) - } -} - -struct State { - registry: Registry, - device: Box, -} - -impl State { - fn render(&mut self) { - self.device.set_state(self.registry.focused_state()); - } -} - -type Shared = Arc>; - -fn socket_path() -> PathBuf { - if let Ok(path) = std::env::var("MICROBRIDGE_SOCKET") { - return PathBuf::from(path); - } - let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); - PathBuf::from(home) - .join(".microbridge") - .join("microbridged.sock") -} +use mb_adapters::{spawn_claude_adapter, spawn_codex_adapter, AdapterEvent}; +use mb_device::open_default_device; +use microbridged::config::load_config; +use microbridged::socket::serve; +use microbridged::state::DaemonState; +use tokio::sync::{mpsc, Mutex}; +use tracing::info; #[tokio::main] async fn main() -> std::io::Result<()> { @@ -98,111 +23,27 @@ async fn main() -> std::io::Result<()> { ) .init(); - let path = socket_path(); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - // A previous run may have left its socket file behind; clear it so bind - // succeeds. Running two daemons is unsupported (last one wins the path). - let _ = std::fs::remove_file(&path); - let listener = UnixListener::bind(&path)?; - info!(socket = %path.display(), protocol = PROTOCOL_VERSION, "microbridged listening"); - - let shared: Shared = Arc::new(Mutex::new(State { - registry: Registry::default(), - device: Box::new(MockDevice::default()), - })); + let config = load_config(); + let device = open_default_device(); + info!(device = %device.descriptor().name, "device layer ready"); - loop { - let (stream, _) = listener.accept().await?; - tokio::spawn(handle_connection(stream, Arc::clone(&shared))); - } -} + let shared = Arc::new(Mutex::new(DaemonState::new(device, config))); -async fn handle_connection(stream: UnixStream, shared: Shared) { - let mut lines = BufReader::new(stream).lines(); - while let Ok(Some(line)) = lines.next_line().await { - if line.trim().is_empty() { - continue; - } - match serde_json::from_str::(&line) { - Ok(message) => apply(message, &shared).await, - Err(error) => warn!(%error, "dropping unparseable message"), - } - } -} + let (adapter_tx, mut adapter_rx) = mpsc::unbounded_channel::(); + spawn_codex_adapter(adapter_tx.clone()); + spawn_claude_adapter(adapter_tx); -async fn apply(message: Message, shared: &Shared) { - let mut state = shared.lock().await; - match message { - Message::Hello { - adapter, - protocol_version, - } => { - if protocol_version == PROTOCOL_VERSION { - info!(adapter, "adapter connected"); - } else { - warn!( - adapter, - protocol_version, "adapter speaks a different protocol revision" - ); + let bus = Arc::clone(&shared); + tokio::spawn(async move { + while let Some(event) = adapter_rx.recv().await { + let mut state = bus.lock().await; + match event { + // conn_id 0 = in-process owner + AdapterEvent::Upsert(session) => state.upsert_session(session, 0), + AdapterEvent::Remove(id) => state.remove_session(&id), } } - Message::Status { session } => { - state.registry.upsert(session); - state.render(); - } - Message::Bye { session_id } => { - state.registry.remove(&session_id); - state.render(); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn session(id: &str, state: AgentState, at: u64) -> SessionStatus { - SessionStatus { - id: id.into(), - app: "test".into(), - title: String::new(), - state, - updated_at_ms: at, - } - } - - #[test] - fn most_recent_session_gets_initial_focus() { - let mut registry = Registry::default(); - registry.upsert(session("a", AgentState::Working, 1)); - registry.upsert(session("b", AgentState::Thinking, 2)); - // "a" already held focus and still exists, so it keeps the deck. - assert_eq!(registry.focused.as_deref(), Some("a")); - } - - #[test] - fn approval_preempts_and_releases() { - let mut registry = Registry::default(); - registry.upsert(session("a", AgentState::Working, 1)); - registry.upsert(session("b", AgentState::AwaitingApproval, 2)); - assert_eq!(registry.focused.as_deref(), Some("b")); - - // Approval resolved: focus stays where the user just acted. - registry.upsert(session("b", AgentState::Working, 3)); - assert_eq!(registry.focused.as_deref(), Some("b")); - - registry.remove("b"); - assert_eq!(registry.focused.as_deref(), Some("a")); - } + }); - #[test] - fn empty_registry_clears_the_deck() { - let mut registry = Registry::default(); - registry.upsert(session("a", AgentState::Done, 1)); - registry.remove("a"); - assert_eq!(registry.focused, None); - assert_eq!(registry.focused_state(), None); - } + serve(shared).await } diff --git a/crates/microbridged/src/registry.rs b/crates/microbridged/src/registry.rs new file mode 100644 index 0000000..c90399d --- /dev/null +++ b/crates/microbridged/src/registry.rs @@ -0,0 +1,174 @@ +//! Session registry + focus policy. + +use std::collections::HashMap; + +use mb_protocol::{AgentState, DaemonConfig, SessionStatus}; + +use crate::key_source; + +#[derive(Debug, Default)] +pub struct Registry { + pub sessions: HashMap, + pub focused: Option, + /// Which socket connection (adapter name + conn id) owns each session. + pub owners: HashMap, +} + +impl Registry { + pub fn upsert(&mut self, session: SessionStatus, owner: u64, config: &DaemonConfig) { + self.owners.insert(session.id.clone(), owner); + self.sessions.insert(session.id.clone(), session); + self.resolve_focus(config); + } + + pub fn remove(&mut self, session_id: &str, config: &DaemonConfig) { + self.sessions.remove(session_id); + self.owners.remove(session_id); + self.resolve_focus(config); + } + + pub fn remove_owner(&mut self, owner: u64, config: &DaemonConfig) { + let doomed: Vec = self + .owners + .iter() + .filter(|(_, o)| **o == owner) + .map(|(id, _)| id.clone()) + .collect(); + for id in doomed { + self.sessions.remove(&id); + self.owners.remove(&id); + } + self.resolve_focus(config); + } + + /// Focus policy: + /// 1. pinned_focus if still alive + /// 2. awaiting_approval preempts (when approvals_interrupt) + /// 3. current focus keeps the deck while it exists + /// 4. frontmost app's most recent session (auto-follow stub via config) + /// 5. most recently updated session + pub fn resolve_focus(&mut self, config: &DaemonConfig) { + if let Some(pin) = &config.pinned_focus { + if self.sessions.contains_key(pin) { + self.focused = Some(pin.clone()); + return; + } + } + + if config.approvals_interrupt { + let approval = self + .sessions + .values() + .filter(|s| s.state == AgentState::AwaitingApproval) + .max_by_key(|s| s.updated_at_ms); + if let Some(session) = approval { + self.focused = Some(session.id.clone()); + return; + } + } + + if let Some(id) = &self.focused { + if self.sessions.contains_key(id) { + return; + } + } + + if let Some(app) = &config.frontmost_app { + let front = self + .sessions + .values() + .filter(|s| &s.app == app) + .max_by_key(|s| s.updated_at_ms); + if let Some(session) = front { + self.focused = Some(session.id.clone()); + return; + } + } + + self.focused = self + .sessions + .values() + .max_by_key(|s| s.updated_at_ms) + .map(|s| s.id.clone()); + } + + pub fn focused_session(&self) -> Option<&SessionStatus> { + self.focused.as_ref().and_then(|id| self.sessions.get(id)) + } + + pub fn agent_key_ids(&self, config: &DaemonConfig) -> [Option; 6] { + let list: Vec<_> = self.sessions.values().cloned().collect(); + key_source::resolve_agent_keys(&list, self.focused.as_deref(), config) + } + + pub fn session_list(&self) -> Vec { + let mut list: Vec<_> = self.sessions.values().cloned().collect(); + list.sort_by_key(|b| std::cmp::Reverse(b.updated_at_ms)); + list + } + + pub fn owner_of(&self, session_id: &str) -> Option { + self.owners.get(session_id).copied() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn session(id: &str, state: AgentState, at: u64) -> SessionStatus { + SessionStatus { + id: id.into(), + app: "test".into(), + title: String::new(), + state, + updated_at_ms: at, + } + } + + #[test] + fn most_recent_session_gets_initial_focus() { + let mut registry = Registry::default(); + let config = DaemonConfig::default(); + registry.upsert(session("a", AgentState::Working, 1), 1, &config); + registry.upsert(session("b", AgentState::Thinking, 2), 1, &config); + // "a" already held focus and still exists, so it keeps the deck. + assert_eq!(registry.focused.as_deref(), Some("a")); + } + + #[test] + fn approval_preempts_and_releases() { + let mut registry = Registry::default(); + let config = DaemonConfig::default(); + registry.upsert(session("a", AgentState::Working, 1), 1, &config); + registry.upsert(session("b", AgentState::AwaitingApproval, 2), 1, &config); + assert_eq!(registry.focused.as_deref(), Some("b")); + + registry.upsert(session("b", AgentState::Working, 3), 1, &config); + assert_eq!(registry.focused.as_deref(), Some("b")); + + registry.remove("b", &config); + assert_eq!(registry.focused.as_deref(), Some("a")); + } + + #[test] + fn empty_registry_clears_the_deck() { + let mut registry = Registry::default(); + let config = DaemonConfig::default(); + registry.upsert(session("a", AgentState::Done, 1), 1, &config); + registry.remove("a", &config); + assert_eq!(registry.focused, None); + } + + #[test] + fn pinned_focus_beats_approval() { + let mut registry = Registry::default(); + let config = DaemonConfig { + pinned_focus: Some("a".into()), + ..Default::default() + }; + registry.upsert(session("a", AgentState::Working, 1), 1, &config); + registry.upsert(session("b", AgentState::AwaitingApproval, 2), 1, &config); + assert_eq!(registry.focused.as_deref(), Some("a")); + } +} diff --git a/crates/microbridged/src/socket.rs b/crates/microbridged/src/socket.rs new file mode 100644 index 0000000..845bd00 --- /dev/null +++ b/crates/microbridged/src/socket.rs @@ -0,0 +1,147 @@ +//! Unix domain socket server for adapters and UI clients. + +use std::sync::Arc; + +use mb_protocol::{ClientMessage, ClientRole, ServerMessage, PROTOCOL_VERSION}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::{UnixListener, UnixStream}; +use tokio::sync::mpsc; +use tracing::{info, warn}; + +use crate::config::socket_path; +use crate::state::{next_conn_id, SharedState}; + +pub async fn serve(shared: SharedState) -> std::io::Result<()> { + let path = socket_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let _ = std::fs::remove_file(&path); + let listener = UnixListener::bind(&path)?; + info!( + socket = %path.display(), + protocol = PROTOCOL_VERSION, + "microbridged listening" + ); + + loop { + let (stream, _) = listener.accept().await?; + let shared = Arc::clone(&shared); + tokio::spawn(async move { + if let Err(error) = handle_connection(stream, shared).await { + warn!(%error, "connection closed with error"); + } + }); + } +} + +async fn handle_connection(stream: UnixStream, shared: SharedState) -> std::io::Result<()> { + let conn_id = next_conn_id(); + let (read_half, mut write_half) = stream.into_split(); + let (tx, mut rx) = mpsc::unbounded_channel::(); + + let writer = tokio::spawn(async move { + while let Some(msg) = rx.recv().await { + let Ok(line) = serde_json::to_string(&msg) else { + continue; + }; + if write_half.write_all(line.as_bytes()).await.is_err() { + break; + } + if write_half.write_all(b"\n").await.is_err() { + break; + } + } + }); + + let mut lines = BufReader::new(read_half).lines(); + let mut role = ClientRole::Adapter; + let mut named = false; + + while let Ok(Some(line)) = lines.next_line().await { + if line.trim().is_empty() { + continue; + } + let message = match serde_json::from_str::(&line) { + Ok(m) => m, + Err(error) => { + warn!(%error, "dropping unparseable message"); + continue; + } + }; + apply(message, conn_id, &tx, &mut role, &mut named, &shared).await; + } + + { + let mut state = shared.lock().await; + state.drop_connection(conn_id); + } + drop(tx); + let _ = writer.await; + Ok(()) +} + +async fn apply( + message: ClientMessage, + conn_id: u64, + tx: &mpsc::UnboundedSender, + role: &mut ClientRole, + named: &mut bool, + shared: &SharedState, +) { + match message { + ClientMessage::Hello { + adapter, + protocol_version, + role: hello_role, + } => { + *role = hello_role; + *named = true; + let mut state = shared.lock().await; + match hello_role { + ClientRole::Adapter => { + state.adapter_txs.insert(conn_id, tx.clone()); + } + ClientRole::Ui => { + state.ui_txs.insert(conn_id, tx.clone()); + } + } + if protocol_version == PROTOCOL_VERSION { + info!(adapter, ?hello_role, conn_id, "client connected"); + } else { + warn!( + adapter, + protocol_version, "client speaks a different protocol revision" + ); + } + } + ClientMessage::Status { session } => { + if !*named { + warn!("status before hello; ignoring"); + return; + } + let mut state = shared.lock().await; + state.upsert_session(session, conn_id); + } + ClientMessage::Bye { session_id } => { + let mut state = shared.lock().await; + state.remove_session(&session_id); + } + ClientMessage::Subscribe => { + let state = shared.lock().await; + let snap = state.snapshot(); + let _ = tx.send(ServerMessage::Snapshot { snapshot: snap }); + } + ClientMessage::GetConfig => { + let state = shared.lock().await; + let _ = tx.send(ServerMessage::Config { + config: state.config.clone(), + }); + } + ClientMessage::SetConfig { config } => { + let mut state = shared.lock().await; + state.set_config(config.clone()); + let _ = tx.send(ServerMessage::Config { config }); + } + } +} diff --git a/crates/microbridged/src/state.rs b/crates/microbridged/src/state.rs new file mode 100644 index 0000000..6a5f197 --- /dev/null +++ b/crates/microbridged/src/state.rs @@ -0,0 +1,185 @@ +//! Shared daemon state: registry, config, device, subscribers. + +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +use mb_device::{Device, LedFrame}; +use mb_protocol::{ + Action, BusEvent, DaemonConfig, ServerMessage, SessionStatus, Snapshot, AGENT_KEY_COUNT, +}; +use tokio::sync::{mpsc, Mutex}; +use tracing::{info, warn}; + +use crate::config::save_config; +use crate::registry::Registry; + +pub type SharedState = Arc>; + +static NEXT_CONN: AtomicU64 = AtomicU64::new(1); + +pub fn next_conn_id() -> u64 { + NEXT_CONN.fetch_add(1, Ordering::Relaxed) +} + +pub struct DaemonState { + pub registry: Registry, + pub config: DaemonConfig, + pub device: Box, + /// Write channels for adapter connections (conn_id → tx). + pub adapter_txs: HashMap>, + /// Write channels for subscribed UI clients. + pub ui_txs: HashMap>, + last_leds: LedFrame, +} + +impl DaemonState { + pub fn new(device: Box, config: DaemonConfig) -> Self { + Self { + registry: Registry::default(), + config, + device, + adapter_txs: HashMap::new(), + ui_txs: HashMap::new(), + last_leds: LedFrame::default(), + } + } + + pub fn snapshot(&self) -> Snapshot { + let desc = self.device.descriptor(); + let keys = self.registry.agent_key_ids(&self.config); + Snapshot { + sessions: self.registry.session_list(), + focused_session_id: self.registry.focused.clone(), + agent_key_session_ids: keys.into_iter().collect(), + device_connected: desc.connected, + device_name: desc.name, + config: self.config.clone(), + } + } + + pub fn upsert_session(&mut self, session: SessionStatus, owner: u64) { + let prev_focus = self.registry.focused.clone(); + self.registry.upsert(session.clone(), owner, &self.config); + self.broadcast_ui(BusEvent::SessionUpserted { + session: session.clone(), + }); + self.after_bus_change(prev_focus); + } + + pub fn remove_session(&mut self, session_id: &str) { + let prev_focus = self.registry.focused.clone(); + self.registry.remove(session_id, &self.config); + self.broadcast_ui(BusEvent::SessionRemoved { + session_id: session_id.to_string(), + }); + self.after_bus_change(prev_focus); + } + + pub fn drop_connection(&mut self, conn_id: u64) { + let prev_focus = self.registry.focused.clone(); + self.adapter_txs.remove(&conn_id); + self.ui_txs.remove(&conn_id); + self.registry.remove_owner(conn_id, &self.config); + self.after_bus_change(prev_focus); + } + + pub fn set_config(&mut self, config: DaemonConfig) { + let prev_focus = self.registry.focused.clone(); + self.config = config.clone(); + if let Err(error) = save_config(&self.config) { + warn!(%error, "failed to persist config"); + } + self.registry.resolve_focus(&self.config); + self.broadcast_ui(BusEvent::ConfigChanged { + config: config.clone(), + }); + self.after_bus_change(prev_focus); + } + + fn after_bus_change(&mut self, prev_focus: Option) { + if self.registry.focused != prev_focus { + self.broadcast_ui(BusEvent::FocusChanged { + session_id: self.registry.focused.clone(), + }); + } + let keys = self.registry.agent_key_ids(&self.config); + self.broadcast_ui(BusEvent::AgentKeysChanged { + session_ids: keys.clone().into_iter().collect(), + }); + self.render_leds(&keys); + } + + pub fn render_leds(&mut self, keys: &[Option; AGENT_KEY_COUNT]) { + let mut frame = LedFrame { + keys: [None; AGENT_KEY_COUNT], + focus_index: None, + brightness: self.config.brightness, + paused: self.config.pause_leds, + }; + for (i, id) in keys.iter().enumerate() { + frame.keys[i] = id + .as_ref() + .and_then(|sid| self.registry.sessions.get(sid)) + .map(|s| s.state); + if id.as_ref() == self.registry.focused.as_ref() { + frame.focus_index = Some(i); + } + } + if frame != self.last_leds { + self.device.set_leds(&frame); + self.last_leds = frame; + } + } + + pub fn route_action(&self, session_id: &str, action: Action) { + let Some(owner) = self.registry.owner_of(session_id) else { + warn!(session_id, ?action, "no adapter owns session"); + return; + }; + let Some(tx) = self.adapter_txs.get(&owner) else { + // In-process adapters: owner id 0 is reserved for local handlers. + if owner == 0 { + info!(session_id, ?action, "in-process action"); + return; + } + warn!(session_id, ?action, owner, "adapter connection gone"); + return; + }; + let _ = tx.send(ServerMessage::Action { + session_id: session_id.to_string(), + action, + }); + } + + pub fn handle_device_action(&mut self, action: Action) { + if let Some(id) = self.registry.focused.clone() { + self.route_action(&id, action); + } + } + + pub fn focus_agent_key(&mut self, index: usize) { + let keys = self.registry.agent_key_ids(&self.config); + if let Some(Some(id)) = keys.get(index) { + let prev = self.registry.focused.clone(); + self.registry.focused = Some(id.clone()); + if prev != self.registry.focused { + self.broadcast_ui(BusEvent::FocusChanged { + session_id: self.registry.focused.clone(), + }); + self.render_leds(&keys); + } + } + } + + fn broadcast_ui(&mut self, event: BusEvent) { + let msg = ServerMessage::Event { event }; + self.ui_txs.retain(|_, tx| tx.send(msg.clone()).is_ok()); + } + + pub fn send_to_ui(&self, conn_id: u64, msg: ServerMessage) { + if let Some(tx) = self.ui_txs.get(&conn_id) { + let _ = tx.send(msg); + } + } +} diff --git a/docs/architecture.md b/docs/architecture.md index 1759c4f..798b896 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -7,7 +7,7 @@ | `microbridged` | resident daemon (launchd agent) | Rust | yes | | First-party adapters (Codex CLI, Claude Code) | in-process modules of the daemon | Rust | bundled | | Community adapters | separate processes on the socket | any | optional | -| Menu bar app | tray app talking to the same socket | TBD (M3) | optional, quit-able | +| Menu bar app | tray app talking to the same socket | Tauri 2 + React (`apps/microbridge-ui`) | optional, quit-able | The daemon owns three things: the **status bus** (session registry fed by adapters), the **focus policy** (which session owns the deck), and the @@ -39,7 +39,7 @@ The deck shows exactly one session at a time: 1. **Approvals preempt.** A session entering `awaiting_approval` takes the deck (and the approve/reject keys) until resolved. -2. **Pinned beats auto.** The user can pin a session from the menu bar or a +2. **Pinned beats auto.** The user can pin a session from Settings or a device key; pinning disables auto-follow until unpinned. 3. **Auto-follow (M3).** Otherwise the frontmost app's active session owns the deck — driven by `NSWorkspace` frontmost-app notifications (event-driven, diff --git a/docs/design/README.md b/docs/design/README.md index 6eac4a2..7a10ba2 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -1,78 +1,162 @@ # UI design -The interface direction for Microbridge's optional UI surfaces. Interactive -mockups live on MagicPath (links below); this document is the spec an -implementer should build from in M3. +The interface direction for Microbridge's optional UI surfaces. **MagicPath +interactive mockups are the go-to reference** for M3 implementation; this +document describes what they show and why. -## Design language — "Industrial Phosphor" +## Lean companion principle -Work Louder's retro-industrial hardware aesthetic crossed with terminal -phosphor. Dark, precise, glanceable — a hardware companion, not a SaaS -dashboard. +Microbridge is a **status + setup** companion, not a second agent cockpit. -| Token | Value | -|---|---| -| Background / panel / raised | `#0C0C0E` / `#131316` / `#1A1A1F` | -| Hairline borders | `#26262C`, 1px | -| Accent (phosphor orange) | `#FF6A00`; secondary amber `#FFB454` | -| Text | cream `#F4F0E6`; secondary `#9C9890`; muted `#5C5952` | -| Type | JetBrains Mono for labels/data/numerals; Inter for body; section labels 10px uppercase, 0.2em tracking | -| Motion | 150–250ms transitions; slow pulses for attention states; nothing decorative | +- **The Micro owns actions.** Approve, reject, interrupt, and switch focus happen on the physical keys. +- **The UI connects and configures.** Connection status, pause LEDs, key remapping, lighting, adapters, appearance. +- **The UI never competes.** It mirrors daemon-resolved focus; it never talks to HID. -**State colors** (used identically on-screen and on the device LEDs): +On-screen surfaces may *show* state colors and which session owns the deck, but +do not expose those states as buttons. Pinning and key-source live in +Settings → Agent Keys, not in the daily menu bar popover. -| State | Color | Treatment | +## Design language — "Device White" + +The UI borrows its material language from the hardware itself: the Codex +Micro's white sandblasted-polycarbonate body, frosted translucent keycaps, and +LEDs that carry all of the color. Layered on OpenAI's Codex app conventions: +near-monochrome neutrals, sentence-case Inter/OpenAI-Sans type, rounded cards +with hairline borders, and tinted status chips as the only color in the chrome. + +**Light is the designed-first default.** Appearance follows the system and is +configurable only in Settings → Device → Appearance (System / Light / Dark). +There is no theme toggle in the popover or titlebars — one coherent look per +mode. Dark mode is "the device on a dark desk": the same frosted physics with +the white device unchanged. + +| Token | Light (default) | Dark | |---|---|---| -| `idle` | `#4A4A52` | static | -| `thinking` | `#FFB454` | soft pulse | -| `working` | `#FF6A00` | solid glow | -| `awaiting_approval` | `#FF3D00` | attention pulse | -| `done` | `#3DDC84` | static | -| `error` | `#FF4757` | static | +| Desk / frame | `#E9E9E7` radial | `#0A0A0B` radial | +| Window material | `rgba(252,252,251,0.86)` + 36px backdrop blur | `rgba(24,24,26,0.88)` + blur | +| Card / raised | `#FFFFFF` / `#F4F4F2` sunken | `rgba(255,255,255,0.05)` / `rgba(0,0,0,0.25)` | +| Hairline | `rgba(0,0,0,0.08)` | `rgba(255,255,255,0.09)` | +| Text primary / secondary / muted | `#0D0D0D` / `#6E6E73` / `#AEAEB2` | `#F5F5F4` / `#A0A0A6` / `#5E5E66` | +| Selection ring | `#3D7EFF` (macOS-style focus blue) | same | +| Type | Inter (OpenAI Sans stand-in), sentence case; mono only for data (firmware, footprints) | same | +| Motion | 150–250ms transitions; slow LED pulses for attention states; nothing decorative | same | -Keycaps render as chunky rounded squares with a top highlight and inner -shadow — they should read as physical keys. The on-screen device mirror is -**descriptor-driven**: layout comes from what the device reports, never a -hardcoded grid. +Status is rendered as Codex-style chips — soft tinted pills ("Working", +"Needs approval", "Done") — never as buttons. -## Surfaces +## State colors — Codex defaults, user-customizable + +LED colors are client-side rendering config (the protocol carries states, +never colors). The default palette matches the color language the Codex +Micro ships with: + +| State | Default | Treatment | +|---|---|---| +| `idle` | white `#E9E9E6` | soft static glow | +| `thinking` | blue `#3D7EFF` | slow breathe | +| `working` | blue `#3D7EFF` | solid | +| `awaiting_approval` | amber `#FFB000` | attention pulse | +| `done` | green `#30C463` | static | +| `error` | red `#FF453A` | static | +| unassigned | off | unlit | + +Every state color is editable in Settings → Device → Lighting, with one-click +**Reset to Codex defaults** and an alternate built-in preset ("Phosphor"). + +## The device + +Design targets the real kbd-1.0 hardware, laid out exactly as shipped: + +``` +(dial) [AG1] [AG2] {joystick} +[AG3] [AG4] [AG5] [AG6] +[⚡Fast] [✓Approve] [✗Reject] [↗Fork] +(touch·LEDs) [ Mic 2U ] [Codex] +``` + +13 mechanical switches, a rotary dial (default: reasoning effort), a planar +joystick (flicks trigger skills — review PR, debug, refactor, explain), a +capacitive touch sensor beside three status LEDs, a 2U push-to-talk mic bar, +and the Codex key (new chat). Six frosted **Agent Keys** glow with the state +of the thread each one follows; the command caps ship printed with their +icons. The box includes 32 icon keycaps and 11 solid caps for re-capping +remapped keys. + +Agent Key press semantics (parity with ChatGPT desktop): single press +switches the followed thread in the background; double press (≤350ms) also +brings its window forward. Which threads the six Agent Keys follow is the +**key source**: most recent (cross-app, default) / focused app (the deck +re-populates with the owning app's threads) / pinned / priority / custom +assignment. Agent Keys mix apps by default — the deck is a cross-app +monitoring surface — while command keys always route to the single +daemon-resolved focused thread. + +The on-screen **device twin** is a photo-accurate vector rendering of the +actual hardware — white plate (white in both themes), frosted agent caps with +the switch stem visible through the frost, printed command icons, dial, +joystick, touch sensor, corner screws, and plate print. Vector rather than a +photo so the Agent Keys can light with live state colors. Every control is +clickable for setup; the twin is **descriptor-driven**: layout comes from what +the device reports, never a hardcoded grid. + +**Agent Keys are never blank remappable keys.** On the twin they render the +live LED color of their thread, and selecting one shows the thread it follows +(app, title, state, deck ownership) with a pointer to Agent Keys settings — +not an action picker. + +## Surfaces (MagicPath go-to) ### 1. Menu bar popover — [interactive mockup](https://api.magicpath.ai/v1/safely-park-1411) -The daily driver. Header (wordmark + device connection chip), a hero focus -card (focused agent, state badge, elapsed time, live LED-strip preview), -the agents list (state dot, app, session title; click to focus; -awaiting-approval rows reveal inline approve/reject), an AUTO/PINNED focus -segmented control, and a footer (Settings, Pause LEDs, Quit). +The daily driver. **Read-mostly** — no agent actions, no theme toggle. + +- Header: wordmark + device connection chip +- Hero focus card: app, thread title, read-only state chip, elapsed time, reasoning pill (dial echo), press-behavior hint +- **Mini device echo**: a passive miniature of the real deck (dial, joystick, six lit Agent Keys, command row) — read-only, labeled as such +- Threads list: state dot + app + title + elapsed — no approve/reject, no click-to-focus +- Footer: Settings · Pause LEDs · Quit + +When disconnected, the popover shows a connection-first empty state ("Connect +your Codex Micro") with the echo unlit — no fake agent chrome. ### 2. Settings window — [interactive mockup](https://api.magicpath.ai/v1/cool-gulf-2537) -Four sections in a left rail: **KEYS** (device mirror + key inspector: -grouped action picker — Agent / System / Macro / Passthrough — LED behavior, -and a "listen" mode that selects whatever key you physically press; -per-app profile chips where app profiles override GLOBAL on focus), -**FOCUS** (auto/pinned mode, app priority order, "approvals interrupt" -toggle), **ADAPTERS** (native vs community badges, enable toggles, -per-adapter footprint), **DEVICE** (brightness, LED test, sleep, firmware, -and the zero-network badge). +Keyboard setup. Four sections in a left rail: + +- **Keys** — the full device twin + per-control inspector: action picker and + listen mode for command keys (with the shipped cap shown), rotate/press for + the dial (reasoning-effort preview), four flick-to-skill bindings for the + joystick, tap action for the touch sensor. Agent Keys show their live + thread and route to Agent Keys settings. +- **Agent Keys** — live "six keys, six threads" view, key source, deck focus + mode (auto/pinned), app priority order, approvals-interrupt toggle (policy + only — no live approve UI) +- **Adapters** — native vs community badges, enable toggles, footprint +- **Device** — Appearance (System/Light/Dark), Lighting (Codex defaults + + Phosphor preset + reset), brightness, LED test, sleep timer (default 3 min), + firmware, zero-network note ### 3. Focus HUD — [interactive mockup](https://api.magicpath.ai/v1/sunnily-shadow-8075) -A transient overlay (volume-HUD energy) confirming deck ownership when focus -changes: app glyph, "FOCUS →" + app name, session title, state chip, 3-LED -echo, and a 2px drain bar that fades the card after ~2.5s. +A transient, **non-interactive** frosted overlay confirming deck ownership +when focus changes: app badge, app name, thread title, state chip, a +six-key echo of miniature frosted caps with the focused key lit, +press-behavior hint, and a 2px drain bar (~2.5s). No buttons on the HUD card. ## Interaction rules - **No competing owner, ever.** Every surface reflects the single resolved focus from the daemon; UI never talks to the device. +- **UI is read-mostly.** Only connection and config controls are interactive + in the popover; agent actions stay on the Micro. - **Focus changes are always confirmed** — by the HUD on-screen and by the deck itself. -- **Remapping is direct**: click a key on the mirror *or* press it - physically ("listen" mode), then assign. Changes apply live, no save - button. -- **Approvals are privileged**: whatever is focused, an `awaiting_approval` - session may temporarily claim the approve/reject keys (user-toggleable). +- **Remapping is direct**: click a control on the twin *or* press it + physically ("listen" mode), then assign. That is configuring the keyboard, + not driving agents from the UI. +- **Approvals are privileged on the deck**, not in the popover. An + `awaiting_approval` thread may temporarily claim the approve/reject keys + (user-toggleable in Settings → Agent Keys). - The UI processes are optional; quitting them changes nothing about the daemon's behavior. @@ -81,3 +165,15 @@ echo, and a 2px drain bar that fades the card after ~2.5s. MagicPath project: `Microbridge — Agent Control Surface` (id `428891101303308288`) — components `safely-park-1411` (menu bar), `cool-gulf-2537` (settings), `sunnily-shadow-8075` (HUD). + +Hardware reference: OpenAI Supply Co. × Work Louder product page +(`openai.com/supply/co-lab/work-louder/`) — source of the layout, shipped +icon set, and material language. + +When mockups and this doc differ, **trust the mockups**. + +## Implementation + +The shipping companion lives in [`apps/microbridge-ui`](../../apps/microbridge-ui) +(Tauri 2 + React). Vendored MagicPath exports for reference are under +`apps/microbridge-ui/vendor/magicpath/`. diff --git a/docs/device-hid.md b/docs/device-hid.md new file mode 100644 index 0000000..13fb046 --- /dev/null +++ b/docs/device-hid.md @@ -0,0 +1,47 @@ +# Codex Micro HID notes + +Best-effort reverse-engineering of the Work Louder / OpenAI Codex Micro +(kbd-1.0). All HID code lives in [`crates/mb-device`](../crates/mb-device). +Firmware changes may invalidate this document — treat it as a living map. + +## Status + +| Capability | Status | +|---|---| +| USB open / claim | stub — opens as disconnected until VID/PID confirmed | +| LED frames (6 Agent Keys) | mock logs frames; HID packing TBD | +| Key / dial / joystick input | trait defined (`DeviceInput`); no live reports yet | +| Bluetooth | out of scope for M2 (USB-first) | + +Without a claimed device the daemon uses [`MockDevice`](../crates/mb-device/src/lib.rs) +so CI and headless installs stay green. + +## Hardware (product facts) + +- 13 mechanical switches, rotary encoder, planar joystick, capacitive touch +- 6 frosted Agent Keys with per-key RGB +- USB-C and BLE; Microbridge M2 targets USB only +- Also configurable via Work Louder Input / VIA for non-agent layers + +## Probe checklist (when hardware is available) + +1. `system_profiler SPUSBDataType` / `lsusb` — record VID/PID/iProduct +2. Capture HID report descriptor (`hidutil` / Wireshark USBPcap / `usbhid-dump`) +3. Observe ChatGPT desktop LED traffic while forcing each `AgentState` +4. Map report IDs → Agent Key RGB slots and command key bitfields +5. Document double-press window (ChatGPT uses ≤350ms) for Agent Keys + +Until those captures land, `HidDevice::open()` never claims the interface — +we refuse to guess report layouts that could fight ChatGPT desktop. + +## Exclusive ownership + +Only one process should drive Agent Key LEDs. If ChatGPT desktop is open and +owning the Micro, pause Microbridge LEDs (Settings → Pause LEDs) or quit the +desktop bridge. The companion empty state should mention this. + +## Descriptor-driven layout + +The daemon never hardcodes a key grid. `DeviceDescriptor` reports +`agent_key_count`, dial, and joystick capabilities. UI device twins and LED +frames size themselves from that descriptor. diff --git a/docs/protocol.md b/docs/protocol.md index a0baa79..f583d85 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -1,6 +1,6 @@ # Microbridge protocol v0 -Normative spec for adapter ↔ daemon communication. The Rust types in +Normative spec for adapter ↔ daemon ↔ UI communication. The Rust types in [`crates/mb-protocol`](../crates/mb-protocol/src/lib.rs) are the source of truth; if this document and the types disagree, fix one of them in the same PR. @@ -10,9 +10,8 @@ truth; if this document and the types disagree, fix one of them in the same PR. `~/.microbridge/microbridged.sock`. (Windows named pipes: see ROADMAP M5.) - **Newline-delimited JSON** (NDJSON): one message per line, UTF-8, `\n` terminated. Blank lines are ignored. -- One connection per adapter. The daemon treats a closed connection as the end - of that adapter's sessions only after `bye` — crash recovery is the - adapter's job on reconnect. +- One connection per client. The daemon treats a closed adapter connection as + the end of that adapter's sessions. UI disconnects do not affect the bus. ## Principles @@ -23,6 +22,17 @@ truth; if this document and the types disagree, fix one of them in the same PR. record; the daemon never merges partial updates. - **Adapters never touch the device.** They publish state and receive routed actions. The daemon's focus policy alone decides what the hardware shows. +- **UI never talks to HID.** The companion mirrors daemon-resolved focus and + writes config only. + +## Client roles + +`hello` accepts an optional `role` (`adapter` default, or `ui`): + +```json +{"type":"hello","adapter":"reference-echo","protocol_version":0} +{"type":"hello","adapter":"microbridge-ui","protocol_version":0,"role":"ui"} +``` ## Messages: adapter → daemon @@ -64,18 +74,66 @@ truth; if this document and the types disagree, fix one of them in the same PR. `action` is one of: `approve` · `reject` · `interrupt` · `new_session` · `cycle_focus`. Adapters should treat unknown actions as a no-op and log them. -## Focus policy (daemon-internal, v0) +## Messages: UI → daemon + +### `subscribe` — request a snapshot and subsequent events + +```json +{"type":"subscribe"} +``` + +### `get_config` / `set_config` + +```json +{"type":"get_config"} +{"type":"set_config","config":{ /* DaemonConfig */ }} +``` + +Config fields include `key_source` (`most_recent` · `focused_app` · `pinned` · +`priority` · `custom`), `pinned_focus`, `approvals_interrupt`, `pause_leds`, +`appearance`, `lighting_preset`, `state_colors`, `brightness`, +`sleep_minutes`, `frontmost_app`, and key-assignment lists. Persisted at +`~/.microbridge/config.toml`. + +## Messages: daemon → UI + +### `snapshot` + +Full bus view: sessions, focused session, six Agent Key assignments, device +connection, and config. + +### `event` + +Incremental `BusEvent`: `session_upserted`, `session_removed`, +`focus_changed`, `agent_keys_changed`, `device_changed`, `config_changed`. + +### `config` + +Response to `get_config` / ack of `set_config`. + +## Focus policy (daemon-internal) + +1. `pinned_focus` if that session still exists. +2. A session in `awaiting_approval` preempts (when `approvals_interrupt`). +3. Otherwise the currently focused session keeps the deck while it exists. +4. Otherwise the frontmost app's most recent session (via `frontmost_app`). +5. Otherwise the most recently updated session. + +## Key source (six Agent Keys) -1. A session in `awaiting_approval` preempts focus (most recent wins). -2. Otherwise the currently focused session keeps the deck while it exists. -3. Otherwise the most recently updated session takes focus. +| Mode | Behavior | +|---|---| +| `most_recent` | Cross-app; six newest sessions (default) | +| `focused_app` | Repopulate from the app that owns the deck | +| `pinned` | First six `pinned_session_ids` | +| `priority` | Approvals / active / app-priority ordering | +| `custom` | Explicit `custom_key_ids` (empty string = unassigned) | -Frontmost-app auto-focus and pinning ship with the menu bar app (M3) and do -not change the wire format. +Command keys always route to the single focused session. ## Versioning `protocol_version` is a single integer. The daemon accepts mismatched -adapters but logs a warning; breaking wire changes bump the version and are -called out in release notes. Additive fields are not breaking — adapters and -daemon must ignore unknown fields. +clients but logs a warning; breaking wire changes bump the version. +Additive fields are not breaking — clients and daemon must ignore unknown +fields. UI role + subscribe/config messages are additive in v0. diff --git a/scripts/com.ai.microbridge.daemon.plist b/scripts/com.ai.microbridge.daemon.plist new file mode 100644 index 0000000..7764fb6 --- /dev/null +++ b/scripts/com.ai.microbridge.daemon.plist @@ -0,0 +1,17 @@ + + + + + + Label + ai.microbridge.daemon + ProgramArguments + + /usr/local/bin/microbridged + + RunAtLoad + + KeepAlive + + + diff --git a/scripts/install-from-release.sh b/scripts/install-from-release.sh new file mode 100755 index 0000000..7645e57 --- /dev/null +++ b/scripts/install-from-release.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Download a GitHub Release archive and install binaries (+ launchd on macOS). +set -euo pipefail + +REPO="${MICROBRIDGE_REPO:-DevVig/microbridge}" +BIN_DIR="${MICROBRIDGE_BIN:-$HOME/.local/bin}" +TAG="${1:-}" +LABEL="ai.microbridge.daemon" + +need() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: '$1' is required" >&2 + exit 1 + } +} + +need curl +need tar + +OS="$(uname -s)" +ARCH="$(uname -m)" +case "$OS-$ARCH" in + Darwin-arm64) TARGET="aarch64-apple-darwin" ;; + Darwin-x86_64) TARGET="x86_64-apple-darwin" ;; + Linux-x86_64) TARGET="x86_64-unknown-linux-gnu" ;; + Linux-aarch64) TARGET="aarch64-unknown-linux-gnu" ;; + *) + echo "unsupported platform: $OS $ARCH" >&2 + exit 1 + ;; +esac + +if [[ -z "$TAG" ]]; then + need jq + TAG="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | jq -r .tag_name)" + if [[ -z "$TAG" || "$TAG" == "null" ]]; then + echo "error: no GitHub releases found for ${REPO}" >&2 + echo "hint: build from source with ./scripts/install.sh" >&2 + exit 1 + fi +fi + +ASSET="microbridge-${TAG}-${TARGET}.tar.gz" +URL="https://github.com/${REPO}/releases/download/${TAG}/${ASSET}" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +echo "==> Downloading $URL" +if ! curl -fsSL -o "$TMP/$ASSET" "$URL"; then + echo "error: download failed — is ${TAG} published with ${ASSET}?" >&2 + echo "hint: ./scripts/install.sh builds from source instead" >&2 + exit 1 +fi + +tar -xzf "$TMP/$ASSET" -C "$TMP" +BIN_SRC="$(find "$TMP" -type f -name microbridged | head -n1)" +CTL_SRC="$(find "$TMP" -type f -name microbridgectl | head -n1)" +if [[ -z "$BIN_SRC" || -z "$CTL_SRC" ]]; then + echo "error: archive missing microbridged/microbridgectl" >&2 + exit 1 +fi +mkdir -p "$BIN_DIR" "$HOME/.microbridge" +install -m 755 "$BIN_SRC" "$BIN_DIR/microbridged" +install -m 755 "$CTL_SRC" "$BIN_DIR/microbridgectl" + +if [[ "$OS" == "Darwin" ]]; then + PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist" + mkdir -p "$HOME/Library/LaunchAgents" + cat >"$PLIST" < + + + + Label + ${LABEL} + ProgramArguments + + ${BIN_DIR}/microbridged + + RunAtLoad + + KeepAlive + + StandardOutPath + ${HOME}/.microbridge/daemon.log + StandardErrorPath + ${HOME}/.microbridge/daemon.log + EnvironmentVariables + + HOME + ${HOME} + PATH + ${BIN_DIR}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin + + + +EOF + launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true + launchctl bootstrap "gui/$(id -u)" "$PLIST" + launchctl enable "gui/$(id -u)/${LABEL}" + launchctl kickstart -k "gui/$(id -u)/${LABEL}" +fi + +echo "Installed ${TAG} → $BIN_DIR" +echo " status: microbridgectl status" diff --git a/scripts/install-launchd.sh b/scripts/install-launchd.sh new file mode 100755 index 0000000..33453b1 --- /dev/null +++ b/scripts/install-launchd.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Back-compat wrapper — prefer ./scripts/install.sh +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +exec "$ROOT/scripts/install.sh" "$@" diff --git a/scripts/install-linux-systemd.sh b/scripts/install-linux-systemd.sh new file mode 100755 index 0000000..5d76b73 --- /dev/null +++ b/scripts/install-linux-systemd.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Install daemon binaries and a systemd --user unit on Linux. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BIN_DIR="${MICROBRIDGE_BIN:-$HOME/.local/bin}" + +"$ROOT/scripts/install.sh" --no-launchd --bin-dir "$BIN_DIR" + +UNIT_DIR="$HOME/.config/systemd/user" +mkdir -p "$UNIT_DIR" +sed "s|%h/.local/bin|${BIN_DIR}|g" "$ROOT/scripts/microbridge.service" >"$UNIT_DIR/microbridge.service" +systemctl --user daemon-reload +systemctl --user enable --now microbridge.service +echo "systemd --user unit microbridge.service started" +echo " logs: journalctl --user -u microbridge -f" diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..63b73ff --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# Unified Microbridge installer (daemon + optional UI). +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BIN_DIR="${MICROBRIDGE_BIN:-$HOME/.local/bin}" +WITH_UI=0 +WITH_LAUNCHD=1 +LABEL="ai.microbridge.daemon" + +usage() { + cat <&2; usage; exit 1 ;; + esac +done + +if [[ "$(uname -s)" != "Darwin" ]]; then + WITH_LAUNCHD=0 +fi + +need() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: '$1' is required but not on PATH" >&2 + exit 1 + } +} + +need cargo +need rustc + +echo "==> Building release binaries" +( + cd "$ROOT" + cargo build --release -p microbridged -p microbridgectl +) + +mkdir -p "$BIN_DIR" "$HOME/.microbridge" +install -m 755 "$ROOT/target/release/microbridged" "$BIN_DIR/microbridged" +install -m 755 "$ROOT/target/release/microbridgectl" "$BIN_DIR/microbridgectl" +echo " installed $BIN_DIR/microbridged" +echo " installed $BIN_DIR/microbridgectl" + +if ! command -v microbridgectl >/dev/null 2>&1; then + if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then + echo "" + echo "Note: add this to your shell rc so the tools are on PATH:" + echo " export PATH=\"$BIN_DIR:\$PATH\"" + fi +fi + +if [[ ! -f "$HOME/.microbridge/config.toml" ]]; then + cat >"$HOME/.microbridge/config.toml" <<'TOML' +# Microbridge daemon config — see docs/protocol.md +key_source = "most_recent" +approvals_interrupt = true +pause_leds = false +appearance = "system" +lighting_preset = "codex" +brightness = 80 +sleep_minutes = 3 +TOML + echo " wrote ~/.microbridge/config.toml" +fi + +if [[ "$WITH_LAUNCHD" -eq 1 ]]; then + echo "==> Installing launchd agent ($LABEL)" + PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist" + mkdir -p "$HOME/Library/LaunchAgents" + cat >"$PLIST" < + + + + Label + ${LABEL} + ProgramArguments + + ${BIN_DIR}/microbridged + + RunAtLoad + + KeepAlive + + StandardOutPath + ${HOME}/.microbridge/daemon.log + StandardErrorPath + ${HOME}/.microbridge/daemon.log + EnvironmentVariables + + HOME + ${HOME} + PATH + ${BIN_DIR}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin + RUST_LOG + info + + + +EOF + launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true + launchctl bootstrap "gui/$(id -u)" "$PLIST" + launchctl enable "gui/$(id -u)/${LABEL}" + launchctl kickstart -k "gui/$(id -u)/${LABEL}" + echo " launchd agent running" + sleep 0.5 + if "$BIN_DIR/microbridgectl" status >/dev/null 2>&1; then + echo " microbridgectl status: ok" + else + echo " warning: daemon not responding yet — check ~/.microbridge/daemon.log" + fi +else + echo "==> Skipping launchd (run manually: $BIN_DIR/microbridged)" +fi + +if [[ "$WITH_UI" -eq 1 ]]; then + echo "==> Companion UI" + need npm + ( + cd "$ROOT/apps/microbridge-ui" + npm ci + npm run build + if command -v cargo >/dev/null && [[ -d src-tauri ]]; then + if npm run tauri build; then + echo " Tauri app built under apps/microbridge-ui/src-tauri/target/release/bundle/" + else + echo " note: Tauri bundle skipped/failed — web build is in apps/microbridge-ui/dist" + echo " run: cd apps/microbridge-ui && npm run dev" + fi + fi + ) +fi + +echo "" +echo "Microbridge installed." +echo " status: $BIN_DIR/microbridgectl status" +echo " logs: ~/.microbridge/daemon.log" +echo " config: ~/.microbridge/config.toml" +echo " docs: INSTALL.md" +echo " remove: ./scripts/uninstall.sh" diff --git a/scripts/microbridge.service b/scripts/microbridge.service new file mode 100644 index 0000000..325a471 --- /dev/null +++ b/scripts/microbridge.service @@ -0,0 +1,13 @@ +[Unit] +Description=Microbridge daemon +After=default.target + +[Service] +Type=simple +ExecStart=%h/.local/bin/microbridged +Restart=on-failure +RestartSec=2 +Environment=RUST_LOG=info + +[Install] +WantedBy=default.target diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100755 index 0000000..ca1d3c5 --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Remove Microbridge binaries, launchd agent, and optionally config. +set -euo pipefail + +BIN_DIR="${MICROBRIDGE_BIN:-$HOME/.local/bin}" +LABEL="ai.microbridge.daemon" +PURGE=0 + +usage() { + cat <&2; usage; exit 1 ;; + esac +done + +if [[ "$(uname -s)" == "Darwin" ]]; then + echo "==> Stopping launchd agent" + launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true + rm -f "$HOME/Library/LaunchAgents/${LABEL}.plist" +fi + +if [[ -f "$HOME/.config/systemd/user/microbridge.service" ]]; then + echo "==> Stopping systemd --user unit" + systemctl --user disable --now microbridge.service 2>/dev/null || true + rm -f "$HOME/.config/systemd/user/microbridge.service" + systemctl --user daemon-reload 2>/dev/null || true +fi + +echo "==> Removing binaries from $BIN_DIR" +rm -f "$BIN_DIR/microbridged" "$BIN_DIR/microbridgectl" + +if [[ "$PURGE" -eq 1 ]]; then + echo "==> Purging ~/.microbridge" + rm -rf "$HOME/.microbridge" +else + echo " kept ~/.microbridge (pass --purge to delete config/logs)" +fi + +echo "Microbridge uninstalled." From eb419d830cd9391090981babaccd31a398394b26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:34:52 +0000 Subject: [PATCH 2/2] build(deps-dev): Bump vite from 6.4.3 to 8.1.5 in /apps/microbridge-ui Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.3 to 8.1.5. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.1.5 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- apps/microbridge-ui/package-lock.json | 1076 +++++++------------------ apps/microbridge-ui/package.json | 2 +- 2 files changed, 310 insertions(+), 768 deletions(-) diff --git a/apps/microbridge-ui/package-lock.json b/apps/microbridge-ui/package-lock.json index 221d6a7..2da623c 100644 --- a/apps/microbridge-ui/package-lock.json +++ b/apps/microbridge-ui/package-lock.json @@ -21,7 +21,7 @@ "@vitejs/plugin-react": "^4.3.4", "tailwindcss": "^4.0.0", "typescript": "~5.7.2", - "vite": "^6.0.0" + "vite": "^8.1.5" } }, "node_modules/@babel/code-frame": { @@ -306,446 +306,38 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -798,31 +390,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "funding": { + "url": "https://github.com/sponsors/Boshen" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -831,12 +431,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -845,12 +448,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -859,26 +465,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -887,84 +482,36 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, "libc": [ "glibc" ], @@ -972,14 +519,17 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "libc": [ @@ -989,50 +539,19 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "dev": true, "libc": [ "glibc" ], @@ -1040,29 +559,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], @@ -1074,12 +579,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], @@ -1091,12 +599,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], @@ -1108,26 +619,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -1136,40 +636,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -1178,21 +689,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@tailwindcss/node": { "version": "4.3.3", @@ -1429,6 +936,72 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", @@ -1729,6 +1302,17 @@ "@tauri-apps/api": "^2.10.1" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1774,13 +1358,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -1953,48 +1530,6 @@ "node": ">=10.13.0" } }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2507,50 +2042,46 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" }, "node_modules/scheduler": { "version": "0.27.0", @@ -2616,6 +2147,14 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -2662,24 +2201,23 @@ } }, "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2688,14 +2226,15 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -2704,13 +2243,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { diff --git a/apps/microbridge-ui/package.json b/apps/microbridge-ui/package.json index a56b86b..e52814a 100644 --- a/apps/microbridge-ui/package.json +++ b/apps/microbridge-ui/package.json @@ -23,6 +23,6 @@ "@vitejs/plugin-react": "^4.3.4", "tailwindcss": "^4.0.0", "typescript": "~5.7.2", - "vite": "^6.0.0" + "vite": "^8.1.5" } }