From 522c76bc5043836c0893564b2e7edad0605c33d7 Mon Sep 17 00:00:00 2001 From: jacobjmc Date: Sat, 7 Mar 2026 13:47:49 +1100 Subject: [PATCH 1/2] Confirm release readiness updates --- .github/workflows/ci.yml | 2 + .github/workflows/release.yml | 2 +- README.md | 32 ++- package.json | 11 + src-tauri/Cargo.lock | 200 ++++++++++++++---- src-tauri/Cargo.toml | 8 +- src-tauri/src/backend/app_server.rs | 43 ++-- src-tauri/src/shared/codex_update_core.rs | 42 +++- src-tauri/src/storage.rs | 196 ++++++++++++++++- src-tauri/tauri.conf.json | 2 +- .../sections/SettingsServerSection.tsx | 6 + 11 files changed, 463 insertions(+), 81 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3afb4c6a..75bbfb63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,7 @@ jobs: sudo apt-get update sudo apt-get install -y \ cmake \ + libdbus-1-dev \ libwebkit2gtk-4.1-dev \ libgtk-3-dev \ libayatana-appindicator3-dev \ @@ -144,6 +145,7 @@ jobs: sudo apt-get update sudo apt-get install -y \ cmake \ + libdbus-1-dev \ libwebkit2gtk-4.1-dev \ libgtk-3-dev \ libayatana-appindicator3-dev \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a51ec069..7e4b3d9b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -181,7 +181,7 @@ jobs: - name: install dependencies (linux only) run: | sudo apt-get update - sudo apt-get install -y cmake libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils libasound2-dev rpm + sudo apt-get install -y cmake libdbus-1-dev libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils libasound2-dev rpm - name: setup node uses: actions/setup-node@v4 diff --git a/README.md b/README.md index 60aa6d50..34934fdf 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,23 @@ OpenCode Monitor is an independent community project and is not affiliated with **Active development** — core REST/SSE support is live for thread/session lifecycle, event translation, messaging, model discovery, approvals, and image attachments. Remaining work is parity polish and OpenCode-specific UX cleanup. +## Install + +Download the latest packaged build from [GitHub Releases](https://github.com/jacobjmc/OpenCodeMonitor/releases). + +Current release targets: + +- macOS Apple Silicon +- Windows x64 +- Linux x64 / arm64 (`AppImage` and `rpm`) + ## Requirements ### 1) OpenCode CLI (required) -This app expects a running OpenCode server (`opencode serve`). Install OpenCode first: +OpenCode Monitor uses your local `opencode` CLI installation and manages its own local `opencode serve` process automatically. You do not need to start the server yourself. + +Install OpenCode first: ```bash # Recommended (macOS/Linux) @@ -30,7 +42,7 @@ Then verify the CLI: opencode --version ``` -### 2) Local tooling for development +### 2) Local tooling for development only - Node.js 20+ - npm 10+ @@ -38,21 +50,23 @@ opencode --version ## First Run -1. Start OpenCode server in a separate terminal: +1. Install the `opencode` CLI and confirm `opencode --version` works in your terminal. -```bash -opencode serve --port 4040 -``` +2. Launch OpenCode Monitor. + +3. Open `Settings -> OpenCode` if you want to verify the managed server status. The app starts and monitors its own local OpenCode server, which defaults to `http://127.0.0.1:14096`. + +4. Add a workspace and start a thread. -2. Start OpenCode Monitor: +## Development + +To run from source: ```bash npm install npm run tauri:dev ``` -3. In Settings -> Server, ensure the backend URL points to your OpenCode server (default: `http://127.0.0.1:4040`). - ## Architecture - **Frontend**: React 19 + Vite + TypeScript diff --git a/package.json b/package.json index 0813288e..976213e4 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,17 @@ "name": "opencode-monitor", "private": true, "version": "0.1.1", + "description": "Desktop app for monitoring and interacting with OpenCode agents across multiple workspaces", + "license": "MIT", + "author": "Jacob", + "homepage": "https://github.com/jacobjmc/OpenCodeMonitor", + "repository": { + "type": "git", + "url": "https://github.com/jacobjmc/OpenCodeMonitor.git" + }, + "bugs": { + "url": "https://github.com/jacobjmc/OpenCodeMonitor/issues" + }, "type": "module", "scripts": { "sync:material-icons": "node scripts/sync-material-icons.mjs", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b8ca97f7..77a5aa18 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -51,7 +51,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "libc", ] @@ -274,7 +274,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -297,7 +297,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -317,9 +317,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] @@ -425,7 +425,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cairo-sys-rs", "glib", "libc", @@ -586,10 +586,10 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "cocoa-foundation", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "foreign-types", "libc", @@ -602,9 +602,9 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "objc", ] @@ -644,6 +644,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -666,8 +676,8 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -679,8 +689,8 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.10.1", "libc", ] @@ -873,6 +883,27 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "dbus", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -950,7 +981,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -1572,7 +1603,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -1587,7 +1618,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "futures-channel", "futures-core", "futures-executor", @@ -2243,11 +2274,26 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "serde", "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -2302,6 +2348,15 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libgit2-sys" version = "0.18.3+1.9.2" @@ -2342,7 +2397,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "redox_syscall 0.7.0", ] @@ -2580,7 +2635,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys 0.5.0+25.2.9519653", @@ -2594,7 +2649,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", @@ -2750,7 +2805,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -2771,7 +2826,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "dispatch2", "objc2", @@ -2803,7 +2858,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-foundation", ] @@ -2826,7 +2881,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", ] @@ -2836,7 +2891,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-foundation", ] @@ -2847,7 +2902,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2", ] @@ -2858,7 +2913,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2", "objc2-core-foundation", @@ -2881,7 +2936,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2", "objc2-core-audio", @@ -2896,7 +2951,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -2908,7 +2963,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -2936,7 +2991,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -2960,7 +3015,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", ] @@ -2993,7 +3048,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-app-kit", "objc2-foundation", @@ -3005,7 +3060,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3017,7 +3072,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", ] @@ -3028,7 +3083,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3040,7 +3095,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "objc2", "objc2-app-kit", @@ -3103,6 +3158,7 @@ dependencies = [ "futures-util", "git2", "ignore", + "keyring", "libc", "objc2", "objc2-app-kit", @@ -3799,7 +3855,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -3808,7 +3864,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -3972,7 +4028,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4007,7 +4063,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4020,7 +4076,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -4140,6 +4196,42 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -4652,9 +4744,9 @@ version = "0.34.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch", @@ -4993,7 +5085,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "log", "serde", "serde_json", @@ -5427,7 +5519,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -6689,6 +6781,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "zerotrie" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0bda31d1..31456e29 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,8 +1,11 @@ [package] name = "opencode-monitor" version = "0.1.1" -description = "A macOS monitor app for OpenCode agents" +description = "Desktop app for monitoring and interacting with OpenCode agents across multiple workspaces" authors = ["Jacob"] +license = "MIT" +homepage = "https://github.com/jacobjmc/OpenCodeMonitor" +repository = "https://github.com/jacobjmc/OpenCodeMonitor" edition = "2021" default-run = "opencode-monitor" @@ -55,6 +58,9 @@ whisper-rs = "0.12" sha2 = "0.10" portable-pty = "0.8" +[target."cfg(not(target_os = \"android\"))".dependencies] +keyring = { version = "3.6.3", default-features = false, features = ["apple-native", "windows-native", "sync-secret-service"] } + [target."cfg(target_os = \"macos\")".dependencies] objc2 = "0.6" objc2-app-kit = { version = "0.3", features = ["NSAppearance", "NSResponder", "NSWindow"] } diff --git a/src-tauri/src/backend/app_server.rs b/src-tauri/src/backend/app_server.rs index 8a8097da..1c3e0ad4 100644 --- a/src-tauri/src/backend/app_server.rs +++ b/src-tauri/src/backend/app_server.rs @@ -131,14 +131,31 @@ struct PidFileData { started_at: String, } -/// Returns the path to the PID file (~/.opencode-monitor/server.pid). +/// Returns the path to the PID file for the managed local OpenCode server. fn pid_file_path() -> Option { - let home = env::var("HOME").ok()?; - Some( - PathBuf::from(home) - .join(".opencode-monitor") - .join("server.pid"), - ) + #[cfg(target_os = "windows")] + { + let base = env::var("LOCALAPPDATA") + .or_else(|_| env::var("APPDATA")) + .or_else(|_| env::var("USERPROFILE")) + .or_else(|_| env::var("HOME")) + .ok()?; + return Some( + PathBuf::from(base) + .join("OpenCodeMonitor") + .join("server.pid"), + ); + } + + #[cfg(not(target_os = "windows"))] + { + let home = env::var("HOME").ok()?; + Some( + PathBuf::from(home) + .join(".opencode-monitor") + .join("server.pid"), + ) + } } /// Write PID file after starting the server. @@ -346,9 +363,7 @@ async fn latest_change_under_dir(root: &Path) -> Option { latest } -async fn latest_change_in_tracked_config_paths( - config_root: &Path, -) -> Option { +async fn latest_change_in_tracked_config_paths(config_root: &Path) -> Option { let tracked_paths = [ config_root.join("opencode.jsonc"), config_root.join("command"), @@ -682,7 +697,9 @@ async fn is_server_owned() -> bool { // Check if we have a valid PID file for the running server if let Some(pid_data) = read_pid_file().await { if is_process_running(pid_data.pid) - && health_check(&rest_base_url_for_port(pid_data.port)).await.is_ok() + && health_check(&rest_base_url_for_port(pid_data.port)) + .await + .is_ok() { return true; } @@ -691,7 +708,9 @@ async fn is_server_owned() -> bool { } pub(crate) async fn opencode_server_status() -> Value { - let base_url = tracked_server_base_url().await.unwrap_or_else(rest_base_url); + let base_url = tracked_server_base_url() + .await + .unwrap_or_else(rest_base_url); let managed = is_server_owned().await; match health_check(&base_url).await { Ok(health) => json!({ diff --git a/src-tauri/src/shared/codex_update_core.rs b/src-tauri/src/shared/codex_update_core.rs index ee5541a2..f08b8228 100644 --- a/src-tauri/src/shared/codex_update_core.rs +++ b/src-tauri/src/shared/codex_update_core.rs @@ -167,11 +167,41 @@ pub(crate) async fn codex_update_core( .ok() .flatten(); - let (method, package, upgrade_ok, output, upgraded) = if detect_brew_cask("codex").await? { - let (ok, output) = run_brew_upgrade(&["--cask", "codex"]).await?; + let (method, package, upgrade_ok, output, upgraded) = if detect_brew_formula("opencode").await? + { + let (ok, output) = run_brew_upgrade(&["opencode"]).await?; + let upgraded = brew_output_indicates_upgrade(&output); + ( + "brew_formula".to_string(), + Some("opencode".to_string()), + ok, + output, + upgraded, + ) + } else if detect_brew_cask("opencode").await? { + let (ok, output) = run_brew_upgrade(&["--cask", "opencode"]).await?; let upgraded = brew_output_indicates_upgrade(&output); ( "brew_cask".to_string(), + Some("opencode".to_string()), + ok, + output, + upgraded, + ) + } else if npm_has_package("opencode-ai").await? { + let (ok, output) = run_npm_install_latest("opencode-ai").await?; + ( + "npm".to_string(), + Some("opencode-ai".to_string()), + ok, + output, + ok, + ) + } else if detect_brew_cask("codex").await? { + let (ok, output) = run_brew_upgrade(&["--cask", "codex"]).await?; + let upgraded = brew_output_indicates_upgrade(&output); + ( + "brew_cask_legacy".to_string(), Some("codex".to_string()), ok, output, @@ -181,7 +211,7 @@ pub(crate) async fn codex_update_core( let (ok, output) = run_brew_upgrade(&["codex"]).await?; let upgraded = brew_output_indicates_upgrade(&output); ( - "brew_formula".to_string(), + "brew_formula_legacy".to_string(), Some("codex".to_string()), ok, output, @@ -190,7 +220,7 @@ pub(crate) async fn codex_update_core( } else if npm_has_package("@openai/codex").await? { let (ok, output) = run_npm_install_latest("@openai/codex").await?; ( - "npm".to_string(), + "npm_legacy".to_string(), Some("@openai/codex".to_string()), ok, output, @@ -222,11 +252,11 @@ pub(crate) async fn codex_update_core( }; let details = if method == "unknown" { - Some("Unable to detect Codex installation method (brew/npm).".to_string()) + Some("Unable to detect OpenCode installation method (brew/npm).".to_string()) } else if upgrade_ok { None } else { - Some("Codex update failed.".to_string()) + Some("OpenCode update failed.".to_string()) }; let result = CodexUpdateResult { diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs index 2a63aa54..2cbb91c1 100644 --- a/src-tauri/src/storage.rs +++ b/src-tauri/src/storage.rs @@ -1,8 +1,105 @@ use std::collections::HashMap; use std::path::PathBuf; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; + use crate::types::{AppSettings, WorkspaceEntry}; +#[cfg(all(not(test), not(target_os = "android")))] +const REMOTE_BACKEND_TOKEN_SERVICE: &str = "com.jmcdev.opencodemonitor.remote-backend"; + +fn remote_backend_token_account(path: &PathBuf) -> String { + format!( + "settings:{}", + URL_SAFE_NO_PAD.encode(path.to_string_lossy().as_bytes()) + ) +} + +#[cfg(test)] +fn test_remote_backend_token_store() -> &'static std::sync::Mutex> { + static STORE: std::sync::OnceLock>> = + std::sync::OnceLock::new(); + STORE.get_or_init(|| std::sync::Mutex::new(HashMap::new())) +} + +#[cfg(test)] +fn read_remote_backend_token(path: &PathBuf) -> Result, String> { + let store = test_remote_backend_token_store() + .lock() + .map_err(|_| "Failed to access test remote backend token store".to_string())?; + Ok(store.get(&remote_backend_token_account(path)).cloned()) +} + +#[cfg(test)] +fn write_remote_backend_token(path: &PathBuf, token: Option<&str>) -> Result { + let mut store = test_remote_backend_token_store() + .lock() + .map_err(|_| "Failed to access test remote backend token store".to_string())?; + let key = remote_backend_token_account(path); + if let Some(token) = token.map(str::trim).filter(|value| !value.is_empty()) { + store.insert(key, token.to_string()); + } else { + store.remove(&key); + } + Ok(true) +} + +#[cfg(test)] +fn clear_test_remote_backend_tokens() { + if let Ok(mut store) = test_remote_backend_token_store().lock() { + store.clear(); + } +} + +#[cfg(all(not(test), not(target_os = "android")))] +fn remote_backend_token_entry(path: &PathBuf) -> Result { + keyring::Entry::new( + REMOTE_BACKEND_TOKEN_SERVICE, + &remote_backend_token_account(path), + ) + .map_err(|err| format!("Failed to open system keychain entry: {err}")) +} + +#[cfg(all(not(test), not(target_os = "android")))] +fn read_remote_backend_token(path: &PathBuf) -> Result, String> { + let entry = remote_backend_token_entry(path)?; + match entry.get_password() { + Ok(token) => Ok(Some(token)), + Err(keyring::Error::NoEntry) => Ok(None), + Err(err) => Err(format!( + "Failed to read remote backend token from system keychain: {err}" + )), + } +} + +#[cfg(all(not(test), not(target_os = "android")))] +fn write_remote_backend_token(path: &PathBuf, token: Option<&str>) -> Result { + let entry = remote_backend_token_entry(path)?; + if let Some(token) = token.map(str::trim).filter(|value| !value.is_empty()) { + entry.set_password(token).map_err(|err| { + format!("Failed to store remote backend token in system keychain: {err}") + })?; + return Ok(true); + } + + match entry.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(true), + Err(err) => Err(format!( + "Failed to remove remote backend token from system keychain: {err}" + )), + } +} + +#[cfg(all(not(test), target_os = "android"))] +fn read_remote_backend_token(_path: &PathBuf) -> Result, String> { + Ok(None) +} + +#[cfg(all(not(test), target_os = "android"))] +fn write_remote_backend_token(_path: &PathBuf, _token: Option<&str>) -> Result { + Ok(false) +} + pub(crate) fn read_workspaces(path: &PathBuf) -> Result, String> { if !path.exists() { return Ok(HashMap::new()); @@ -28,21 +125,72 @@ pub(crate) fn read_settings(path: &PathBuf) -> Result { return Ok(AppSettings::default()); } let data = std::fs::read_to_string(path).map_err(|e| e.to_string())?; - serde_json::from_str(&data).map_err(|e| e.to_string()) + let mut settings: AppSettings = serde_json::from_str(&data).map_err(|e| e.to_string())?; + let legacy_token = settings.remote_backend_token.clone(); + + match read_remote_backend_token(path) { + Ok(Some(token)) => { + settings.remote_backend_token = Some(token); + if legacy_token.is_some() { + let _ = write_settings(path, &settings); + } + } + Ok(None) => { + if let Some(token) = legacy_token { + match write_remote_backend_token(path, Some(&token)) { + Ok(true) => { + settings.remote_backend_token = Some(token); + let _ = write_settings(path, &settings); + } + Ok(false) | Err(_) => { + settings.remote_backend_token = Some(token); + } + } + } else { + settings.remote_backend_token = None; + } + } + Err(_) => { + settings.remote_backend_token = legacy_token; + } + } + + Ok(settings) } pub(crate) fn write_settings(path: &PathBuf, settings: &AppSettings) -> Result<(), String> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; } - let data = serde_json::to_string_pretty(settings).map_err(|e| e.to_string())?; + let mut persisted = settings.clone(); + let normalized_token = persisted + .remote_backend_token + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string); + + // Prefer the system keychain, but keep settings writes resilient if the + // platform secret store is unavailable in the current environment. + let stored_securely = + write_remote_backend_token(path, normalized_token.as_deref()).unwrap_or(false); + persisted.remote_backend_token = if stored_securely { + None + } else { + normalized_token + }; + + let data = serde_json::to_string_pretty(&persisted).map_err(|e| e.to_string())?; std::fs::write(path, data).map_err(|e| e.to_string()) } #[cfg(test)] mod tests { - use super::{read_workspaces, write_workspaces}; - use crate::types::{WorkspaceEntry, WorkspaceKind, WorkspaceSettings}; + use super::{ + clear_test_remote_backend_tokens, read_settings, read_workspaces, write_settings, + write_workspaces, + }; + use crate::types::{AppSettings, WorkspaceEntry, WorkspaceKind, WorkspaceSettings}; use uuid::Uuid; #[test] @@ -81,4 +229,44 @@ mod tests { Some("--profile personal") ); } + + #[test] + fn write_settings_moves_remote_token_out_of_plaintext_file() { + clear_test_remote_backend_tokens(); + let temp_dir = + std::env::temp_dir().join(format!("opencode-monitor-settings-{}", Uuid::new_v4())); + std::fs::create_dir_all(&temp_dir).expect("create temp dir"); + let path = temp_dir.join("settings.json"); + + let mut settings = AppSettings::default(); + settings.remote_backend_token = Some("token-123".to_string()); + + write_settings(&path, &settings).expect("write settings"); + let on_disk = std::fs::read_to_string(&path).expect("read settings file"); + + assert!(!on_disk.contains("token-123")); + + let loaded = read_settings(&path).expect("read settings"); + assert_eq!(loaded.remote_backend_token.as_deref(), Some("token-123")); + } + + #[test] + fn read_settings_migrates_legacy_plaintext_remote_token() { + clear_test_remote_backend_tokens(); + let temp_dir = + std::env::temp_dir().join(format!("opencode-monitor-legacy-{}", Uuid::new_v4())); + std::fs::create_dir_all(&temp_dir).expect("create temp dir"); + let path = temp_dir.join("settings.json"); + + let mut settings = AppSettings::default(); + settings.remote_backend_token = Some("legacy-token".to_string()); + let data = serde_json::to_string_pretty(&settings).expect("serialize settings"); + std::fs::write(&path, data).expect("write legacy settings"); + + let loaded = read_settings(&path).expect("read settings"); + assert_eq!(loaded.remote_backend_token.as_deref(), Some("legacy-token")); + + let on_disk = std::fs::read_to_string(&path).expect("read sanitized settings"); + assert!(!on_disk.contains("legacy-token")); + } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index dee43685..02475384 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCode Monitor", "version": "0.1.1", - "identifier": "dev.opencode.monitor", + "identifier": "com.jmcdev.opencodemonitor", "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", diff --git a/src/features/settings/components/sections/SettingsServerSection.tsx b/src/features/settings/components/sections/SettingsServerSection.tsx index 9bd2d2ac..7908a64a 100644 --- a/src/features/settings/components/sections/SettingsServerSection.tsx +++ b/src/features/settings/components/sections/SettingsServerSection.tsx @@ -262,6 +262,9 @@ export function SettingsServerSection({ ? "Use the Tailscale host from your desktop OpenCode Monitor app (Server section), for example `macbook.your-tailnet.ts.net:4732`." : "This host/token is used by mobile clients and desktop remote-mode testing."} +
+ Remote backend tokens are stored in your system keychain when available. +
{isMobileSimplified && ( @@ -468,6 +471,9 @@ export function SettingsServerSection({
Use the same token configured on your desktop Orbit daemon setup.
+
+ Remote backend tokens are stored in your system keychain when available. +
Connection test
From 2b8905dda782ed00203d84e23dcaa987c43b74b7 Mon Sep 17 00:00:00 2001 From: jacobjmc Date: Sat, 7 Mar 2026 14:03:28 +1100 Subject: [PATCH 2/2] Clarify macOS Apple Silicon support --- src/features/app/components/RequestUserInputMessage.tsx | 5 ++++- src/features/layout/components/DesktopLayout.tsx | 2 +- src/features/threads/hooks/useThreadActions.ts | 1 + src/features/threads/hooks/useThreadMessaging.ts | 4 ++-- src/features/threads/hooks/useThreads.ts | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/features/app/components/RequestUserInputMessage.tsx b/src/features/app/components/RequestUserInputMessage.tsx index c73d6564..93e0c4fa 100644 --- a/src/features/app/components/RequestUserInputMessage.tsx +++ b/src/features/app/components/RequestUserInputMessage.tsx @@ -42,7 +42,10 @@ export function RequestUserInputMessage({ [requests, activeThreadId, activeWorkspaceId], ); const activeRequest = activeRequests[0] ?? null; - const questions = activeRequest?.params.questions ?? []; + const questions = useMemo( + () => activeRequest?.params.questions ?? [], + [activeRequest], + ); const totalRequests = activeRequests.length; const [selections, setSelections] = useState({}); diff --git a/src/features/layout/components/DesktopLayout.tsx b/src/features/layout/components/DesktopLayout.tsx index 8b790a23..bcea9c39 100644 --- a/src/features/layout/components/DesktopLayout.tsx +++ b/src/features/layout/components/DesktopLayout.tsx @@ -133,7 +133,7 @@ export function DesktopLayout({ ) { activeElement.blur(); } - }, [centerMode, splitChatDiffView]); + }, [chatLayerActive, diffLayerActive, splitChatDiffView]); return ( <> diff --git a/src/features/threads/hooks/useThreadActions.ts b/src/features/threads/hooks/useThreadActions.ts index 66df1b3c..5dca41c6 100644 --- a/src/features/threads/hooks/useThreadActions.ts +++ b/src/features/threads/hooks/useThreadActions.ts @@ -812,6 +812,7 @@ export function useThreadActions({ dispatch, getCustomName, onDebug, + threadActivityRef, threadListCursorByWorkspace, threadsByWorkspace, threadSortKey, diff --git a/src/features/threads/hooks/useThreadMessaging.ts b/src/features/threads/hooks/useThreadMessaging.ts index 86098f4b..518558c0 100644 --- a/src/features/threads/hooks/useThreadMessaging.ts +++ b/src/features/threads/hooks/useThreadMessaging.ts @@ -162,7 +162,7 @@ export function useThreadMessaging({ customPrompts, threadStatusById, activeTurnIdByThread, - rateLimitsByWorkspace, + rateLimitsByWorkspace: _rateLimitsByWorkspace, pendingInterruptsRef, dispatch, getCustomName, @@ -798,7 +798,6 @@ export function useThreadMessaging({ effort, ensureThreadForActiveWorkspace, model, - rateLimitsByWorkspace, recordThreadActivity, safeMessageActivity, ], @@ -1069,6 +1068,7 @@ export function useThreadMessaging({ activeThreadId, activeWorkspace, ensureThreadForActiveWorkspace, + model, pushThreadErrorMessage, safeMessageActivity, ], diff --git a/src/features/threads/hooks/useThreads.ts b/src/features/threads/hooks/useThreads.ts index 7c738507..4b17409c 100644 --- a/src/features/threads/hooks/useThreads.ts +++ b/src/features/threads/hooks/useThreads.ts @@ -508,7 +508,7 @@ export function useThreads({ void resumeThreadForWorkspace(targetId, threadId); } }, - [activeWorkspaceId, resumeThreadForWorkspace, state.activeThreadIdByWorkspace], + [activeWorkspaceId, resumeThreadForWorkspace], ); const removeThread = useCallback(