From c7bc026b4b30ae2ab33c6482f04267c8bcbe0d05 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 3 Jun 2026 02:42:00 +0300 Subject: [PATCH] refactor: remove legacy Tauri desktop support Electron updater now uses Electron release metadata only Removed legacy Tauri package and migration workflow Replaced Tauri shim usage with the desktop bridge --- .agents/skills/ui-api-decoupling/SKILL.md | 6 +- .github/workflows/release.yml | 201 +- .opencode/agent/triage.md | 5 +- AGENTS.md | 21 +- CONTRIBUTING.md | 8 +- README.md | 1 - bun.lock | 46 +- docs/TAURI_TO_ELECTRON_CUTOVER.md | 414 - package.json | 9 - packages/desktop/.gitignore | 21 - packages/desktop/README.md | 68 - packages/desktop/noop-dist/index.html | 110 - packages/desktop/package.json | 26 - .../ibm-plex-mono-latin-600-normal.woff2 | Bin 15620 -> 0 bytes .../ibm-plex-mono-latin-700-normal.woff2 | Bin 14908 -> 0 bytes packages/desktop/scripts/build-sidecar.mjs | 135 - packages/desktop/scripts/desktop-dev.mjs | 138 - packages/desktop/scripts/dev-web-server.mjs | 197 - packages/desktop/scripts/opencode-cli.mjs | 237 - packages/desktop/src-tauri/Cargo.lock | 6688 ----------------- packages/desktop/src-tauri/Cargo.toml | 42 - packages/desktop/src-tauri/Info.plist | 27 - packages/desktop/src-tauri/build.rs | 3 - .../src-tauri/capabilities/default.json | 41 - packages/desktop/src-tauri/entitlements.plist | 23 - packages/desktop/src-tauri/icons/app-icon.png | Bin 24006 -> 0 bytes packages/desktop/src-tauri/icons/app-icon.svg | 33 - .../desktop/src-tauri/icons/dev-icon.icns | Bin 173094 -> 0 bytes packages/desktop/src-tauri/icons/dev-icon.png | Bin 66751 -> 0 bytes packages/desktop/src-tauri/icons/icon.icns | Bin 160844 -> 0 bytes packages/desktop/src-tauri/icons/icon.png | Bin 55246 -> 0 bytes packages/desktop/src-tauri/resources/.gitkeep | 0 packages/desktop/src-tauri/sidecars/.gitkeep | 0 packages/desktop/src-tauri/src/main.rs | 4714 ------------ packages/desktop/src-tauri/src/remote_ssh.rs | 3002 -------- packages/desktop/src-tauri/tauri.conf.json | 82 - .../desktop/src-tauri/tauri.dev.conf.json | 9 - packages/electron/main.mjs | 22 +- packages/electron/preload.mjs | 24 +- packages/ui/package.json | 1 - packages/ui/src/App.tsx | 4 +- .../src/components/auth/SessionAuthGate.tsx | 8 +- packages/ui/src/components/chat/ChatInput.tsx | 119 +- .../desktop/DesktopHostSwitcher.tsx | 30 +- .../components/desktop/OpenInAppButton.tsx | 4 +- packages/ui/src/components/layout/Header.tsx | 25 +- .../components/onboarding/ChooserScreen.tsx | 39 +- .../onboarding/LocalSetupScreen.tsx | 41 +- .../components/onboarding/RecoveryScreen.tsx | 14 +- .../onboarding/RemoteConnectionForm.tsx | 7 +- .../openchamber/OpenCodeCliSettings.tsx | 23 +- .../ui/src/components/views/FilesView.tsx | 6 +- .../ui/src/contexts/ThemeSystemContext.tsx | 4 +- packages/ui/src/hooks/useMenuActions.ts | 10 +- packages/ui/src/hooks/useRouter.ts | 2 +- packages/ui/src/hooks/useWindowTitle.ts | 4 +- packages/ui/src/lib/debug.ts | 4 +- packages/ui/src/lib/desktop.ts | 158 +- packages/ui/src/lib/desktopBoot.test.ts | 8 +- packages/ui/src/lib/desktopBoot.ts | 12 +- packages/ui/src/lib/desktopHosts.ts | 17 +- packages/ui/src/lib/desktopNative.ts | 66 +- packages/ui/src/lib/desktopSsh.ts | 32 +- packages/ui/src/lib/device.ts | 4 +- packages/ui/src/lib/ime.ts | 3 +- .../ui/src/lib/preview/screenshot-capture.ts | 9 +- packages/ui/src/lib/router/index.ts | 2 +- packages/ui/src/lib/shortcuts.ts | 8 +- packages/ui/src/lib/url.ts | 20 +- packages/ui/src/lib/utils.ts | 10 +- packages/ui/src/stores/useOpenInAppsStore.ts | 6 +- packages/ui/src/stores/useUpdateStore.ts | 5 +- packages/vscode/src/bridge-system-runtime.ts | 4 +- packages/vscode/src/extension.ts | 2 +- packages/vscode/src/sessionActivityWatcher.ts | 2 +- .../lib/notifications/emitter-runtime.js | 7 +- packages/web/server/lib/package-manager.js | 4 +- .../web/server/lib/package-manager.test.js | 21 - packages/web/src/api/notifications.ts | 16 +- scripts/bump-version.mjs | 64 +- tsconfig.json | 5 +- 81 files changed, 269 insertions(+), 16914 deletions(-) delete mode 100644 docs/TAURI_TO_ELECTRON_CUTOVER.md delete mode 100644 packages/desktop/.gitignore delete mode 100644 packages/desktop/README.md delete mode 100644 packages/desktop/noop-dist/index.html delete mode 100644 packages/desktop/package.json delete mode 100644 packages/desktop/public/ibm-plex-mono-latin-600-normal.woff2 delete mode 100644 packages/desktop/public/ibm-plex-mono-latin-700-normal.woff2 delete mode 100644 packages/desktop/scripts/build-sidecar.mjs delete mode 100644 packages/desktop/scripts/desktop-dev.mjs delete mode 100644 packages/desktop/scripts/dev-web-server.mjs delete mode 100644 packages/desktop/scripts/opencode-cli.mjs delete mode 100644 packages/desktop/src-tauri/Cargo.lock delete mode 100644 packages/desktop/src-tauri/Cargo.toml delete mode 100644 packages/desktop/src-tauri/Info.plist delete mode 100644 packages/desktop/src-tauri/build.rs delete mode 100644 packages/desktop/src-tauri/capabilities/default.json delete mode 100644 packages/desktop/src-tauri/entitlements.plist delete mode 100644 packages/desktop/src-tauri/icons/app-icon.png delete mode 100644 packages/desktop/src-tauri/icons/app-icon.svg delete mode 100644 packages/desktop/src-tauri/icons/dev-icon.icns delete mode 100644 packages/desktop/src-tauri/icons/dev-icon.png delete mode 100644 packages/desktop/src-tauri/icons/icon.icns delete mode 100644 packages/desktop/src-tauri/icons/icon.png delete mode 100644 packages/desktop/src-tauri/resources/.gitkeep delete mode 100644 packages/desktop/src-tauri/sidecars/.gitkeep delete mode 100644 packages/desktop/src-tauri/src/main.rs delete mode 100644 packages/desktop/src-tauri/src/remote_ssh.rs delete mode 100644 packages/desktop/src-tauri/tauri.conf.json delete mode 100644 packages/desktop/src-tauri/tauri.dev.conf.json mode change 100644 => 100755 scripts/bump-version.mjs diff --git a/.agents/skills/ui-api-decoupling/SKILL.md b/.agents/skills/ui-api-decoupling/SKILL.md index 6bb2e0ce..eb01c27d 100644 --- a/.agents/skills/ui-api-decoupling/SKILL.md +++ b/.agents/skills/ui-api-decoupling/SKILL.md @@ -171,7 +171,7 @@ For any shared UI call to `/api/*`, decide the VS Code behavior explicitly: Electron exposes API base and shell identity broadly, but privileged local capabilities stay local-only. - `__OPENCHAMBER_API_BASE_URL__` and `__OPENCHAMBER_LOCAL_ORIGIN__` route requests. -- `__OPENCHAMBER_CLIENT_TOKEN__`, `__OPENCHAMBER_HOME__`, and `__TAURI__`-style IPC are local-page gated. +- `__OPENCHAMBER_CLIENT_TOKEN__`, `__OPENCHAMBER_HOME__`, and privileged desktop IPC are local-page gated. - Do not expose filesystem, shell, or host secrets to remote pages for UI convenience. - Do not trust arbitrary loopback, `file://`, or `about:blank` origins as local UI. Gate privileged preload/IPC/token access to the packaged UI origin and exact runtime origins. - Deep-links that add or switch remote runtimes are trust-boundary changes. Confirm before storing tokens or switching hosts. @@ -274,9 +274,9 @@ If an OpenChamber route is consumed directly by the browser with `oc_url_token`, `packages/electron/main.mjs` starts the web server in-process, resolves local/remote runtime target, tracks `apiBaseUrl` and `clientToken`, injects init scripts, confirms remote connect deep-links before storing tokens, and handles host switching. -`packages/electron/preload.mjs` exposes runtime globals. API base and local origin are broadly available for routing. Client token, home directory, and `__TAURI__` IPC stay local-page gated so remote pages cannot access local host capabilities. +`packages/electron/preload.mjs` exposes runtime globals. API base and local origin are broadly available for routing. Client token, home directory, and privileged desktop IPC stay local-page gated so remote pages cannot access local host capabilities. -Shared UI should not branch on Electron for backend behavior. Prefer web runtime APIs and the preload-provided `__TAURI__` compatibility shim only for shell capabilities that already exist in the shared runtime contract. +Shared UI should not branch on Electron for backend behavior. Prefer web runtime APIs and the `__OPENCHAMBER_DESKTOP__` bridge only for shell capabilities that already exist in the shared runtime contract. ### Runtime Switch Flow diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e14cf22..6cf51a4d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -242,32 +242,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Stage signed Electron app for Tauri updater repackage - run: | - set -euo pipefail - - APP_DIR="packages/electron/dist/mac" - [ -d "packages/electron/dist/mac-arm64" ] && APP_DIR="packages/electron/dist/mac-arm64" - - APP_PATH=$(find "$APP_DIR" -maxdepth 2 -name "*.app" -print -quit) - if [ -z "$APP_PATH" ]; then - echo "Error: .app not found under packages/electron/dist/mac*" - ls -la packages/electron/dist/ - exit 1 - fi - - rm -rf electron-app-artifact - mkdir -p electron-app-artifact - cp -R "$APP_PATH" electron-app-artifact/OpenChamber.app - tar -C electron-app-artifact -czf electron-app-${{ matrix.arch }}.tar.gz OpenChamber.app - - - name: Upload signed Electron app for Tauri updater repackage - uses: actions/upload-artifact@v7 - with: - name: electron-app-${{ matrix.arch }} - path: electron-app-${{ matrix.arch }}.tar.gz - retention-days: 1 - - name: Upload per-arch latest-mac.yml for merge uses: actions/upload-artifact@v7 with: @@ -338,179 +312,6 @@ jobs: path: packages/electron/dist/latest.yml retention-days: 1 - repackage-electron-as-tauri-update: - needs: [create-release, build-desktop-electron-macos] - runs-on: macos-26 - strategy: - fail-fast: false - matrix: - include: - - arch: arm64 - platform: darwin-aarch64 - - arch: x64 - platform: darwin-x86_64 - steps: - - uses: actions/checkout@v6 - - - name: Setup bun - uses: oven-sh/setup-bun@v2 - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Download signed Electron app - uses: actions/download-artifact@v4 - with: - name: electron-app-${{ matrix.arch }} - path: staged - - - name: Tar and sign Electron app as Tauri update payload - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - VERSION: ${{ needs.create-release.outputs.version }} - run: | - set -euo pipefail - - if [ -f staged/electron-app-${{ matrix.arch }}.tar.gz ]; then - tar -C staged -xzf staged/electron-app-${{ matrix.arch }}.tar.gz - elif [ ! -d staged/OpenChamber.app ] && [ -d staged/Contents ]; then - mkdir -p staged/OpenChamber.app - mv staged/Contents staged/OpenChamber.app/Contents - fi - - if [ ! -d staged/OpenChamber.app ]; then - echo "Error: staged/OpenChamber.app not found" - ls -la staged - exit 1 - fi - - APP_EXECUTABLE=$(find staged/OpenChamber.app/Contents/MacOS -type f -maxdepth 1 -print -quit) - if [ -z "$APP_EXECUTABLE" ] || [ ! -x "$APP_EXECUTABLE" ]; then - echo "Error: Electron app executable is missing or not executable" - ls -la staged/OpenChamber.app/Contents/MacOS - exit 1 - fi - - cd staged - TARBALL="OpenChamber.app.tar.gz" - tar -czf "$TARBALL" OpenChamber.app - - bun run --cwd ../packages/desktop tauri signer sign "$PWD/$TARBALL" - - mv "$TARBALL" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz" - mv "${TARBALL}.sig" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig" - - - name: Generate Tauri latest platform manifest - env: - VERSION: ${{ needs.create-release.outputs.version }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - - SIG=$(cat staged/OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig) - TAR="OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz" - jq -n \ - --arg version "$VERSION" \ - --arg notes "OpenChamber has moved to Electron. This update replaces the Tauri shell with the Electron build. Subsequent updates will be delivered via the Electron auto-updater." \ - --arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - --arg platform "${{ matrix.platform }}" \ - --arg signature "$SIG" \ - --arg url "https://github.com/${REPO}/releases/download/v${VERSION}/${TAR}" \ - '{ version: $version, notes: $notes, pub_date: $pub_date, platforms: { ($platform): { signature: $signature, url: $url } } }' \ - > staged/latest-${{ matrix.platform }}.json - - - name: Upload tarball and signature to release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ needs.create-release.outputs.version }} - files: | - staged/*.app.tar.gz - staged/*.app.tar.gz.sig - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload per-platform Tauri manifest for merge - uses: actions/upload-artifact@v7 - with: - name: tauri-manifest-${{ matrix.platform }} - path: staged/latest-${{ matrix.platform }}.json - retention-days: 1 - - combine-manifests: - needs: [create-release, repackage-electron-as-tauri-update] - runs-on: ubuntu-latest - steps: - - name: Download Tauri updater manifests - uses: actions/download-artifact@v4 - with: - pattern: tauri-manifest-* - path: artifacts - merge-multiple: true - - - name: Combine manifests - run: | - set -euo pipefail - - VERSION="${{ needs.create-release.outputs.version }}" - PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - - if [ ! -f artifacts/latest-darwin-aarch64.json ]; then - echo "Error: aarch64 manifest not found" - exit 1 - fi - - if [ ! -f artifacts/latest-darwin-x86_64.json ]; then - echo "Error: x86_64 manifest not found" - exit 1 - fi - - if ! jq empty artifacts/latest-darwin-aarch64.json 2>/dev/null; then - echo "Error: aarch64 manifest is not valid JSON" - exit 1 - fi - - if ! jq empty artifacts/latest-darwin-x86_64.json 2>/dev/null; then - echo "Error: x86_64 manifest is not valid JSON" - exit 1 - fi - - if ! jq -e '.platforms["darwin-aarch64"]' artifacts/latest-darwin-aarch64.json > /dev/null; then - echo "Error: darwin-aarch64 platform data not found in manifest" - exit 1 - fi - - if ! jq -e '.platforms["darwin-x86_64"]' artifacts/latest-darwin-x86_64.json > /dev/null; then - echo "Error: darwin-x86_64 platform data not found in manifest" - exit 1 - fi - - jq -n \ - --arg version "$VERSION" \ - --arg notes "OpenChamber has moved to Electron. This update replaces the Tauri shell with the Electron build. Subsequent updates will be delivered via the Electron auto-updater." \ - --arg pub_date "$PUB_DATE" \ - --slurpfile aarch64 artifacts/latest-darwin-aarch64.json \ - --slurpfile x86_64 artifacts/latest-darwin-x86_64.json \ - '{ - version: $version, - notes: $notes, - pub_date: $pub_date, - platforms: { - "darwin-aarch64": $aarch64[0].platforms["darwin-aarch64"], - "darwin-x86_64": $x86_64[0].platforms["darwin-x86_64"] - } - }' > artifacts/latest.json - - cat artifacts/latest.json - - - name: Upload combined Tauri updater manifest - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ needs.create-release.outputs.version }} - files: artifacts/latest.json - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - combine-electron-manifests: needs: [create-release, build-desktop-electron-macos] runs-on: ubuntu-latest @@ -545,7 +346,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} finalize-release: - needs: [create-release, build-desktop-electron-macos, repackage-electron-as-tauri-update, publish-npm, combine-manifests, combine-electron-manifests] + needs: [create-release, build-desktop-electron-macos, publish-npm, combine-electron-manifests] runs-on: ubuntu-latest env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 9690cdea..d7d11811 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -49,7 +49,7 @@ Only use labels that already exist in this repository. Do not create labels. | `area:sync` | State sync, cross-runtime consistency | | `area:auth` | Authentication, passwords, OAuth, tunnels | | `area:installation` | Install, Docker, Nix, deployment | -| `area:desktop` | Desktop shell (Electron/Tauri), window management | +| `area:desktop` | Desktop shell (Electron), window management | | `area:keyboard` | Keyboard shortcuts, keybinds, input handling | | `area:permissions` | Permission prompts, allow/deny flows | | `area:compact` | Context compaction, /compact command | @@ -63,7 +63,7 @@ Only use labels that already exist in this repository. Do not create labels. | Label | Covers | |---|---| | `platform:web` | Desktop web browser (incl. CLI serve) | -| `platform:macos` | macOS desktop (Electron/Tauri) | +| `platform:macos` | macOS desktop (Electron) | | `platform:linux` | Linux desktop | | `platform:windows` | Windows desktop / WSL | | `platform:mobile` | Mobile web/PWA (iOS/Android) | @@ -105,4 +105,3 @@ For each issue: - Add a small set of accurate existing labels following the steps above. - In a single comment summarize the issue and ask the reporter for any additional information needed to complete the request. - Keep the comment friendly and concise. - diff --git a/AGENTS.md b/AGENTS.md index cb4c5747..e19d8ecb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,18 +7,15 @@ OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an O ## Runtime architecture (IMPORTANT) - `Desktop` (Electron) boots the web server **in the same Node process** as the Electron main, then loads the web UI from `http://127.0.0.1:`. No sidecar subprocess. -- `Desktop` (Tauri, legacy) still spawns `openchamber-server` as a bun-compiled sidecar binary. Kept only for auto-update compatibility with existing Tauri installs. - Backend/domain logic lives in `packages/web/server/*` (and `packages/vscode/*` for VS Code bridge/runtime parity). Electron owns the desktop shell/security boundary: windows, menus, dialogs, notifications, updater, deep-links, runtime host switching, local IPC gates, and SSH/tunnel management. - Do not add OpenCode feature backends to the native shell. Shared UI features should remain server/runtime APIs unless the capability is inherently native. -### Desktop shell: Electron is the target, Tauri is legacy +### Desktop Shell -- **New desktop work goes into `packages/electron/`.** This is the forward path. -- `packages/desktop/` (Tauri) is kept running in parallel only to preserve auto-update for existing installs until the cutover. Do **not** add features to it; do **not** port bug fixes back unless they actually affect currently-released Tauri users. -- Desktop-side changes (IPC handlers, native integrations, window/quit/notification behavior) land in `packages/electron/main.mjs` + `packages/electron/preload.mjs`. The `__TAURI__` shim exposed by the preload keeps the shared UI working against both shells, so renderer-side code should not branch on shell type. +- **Desktop work goes into `packages/electron/`.** +- Desktop-side changes (IPC handlers, native integrations, window/quit/notification behavior) land in `packages/electron/main.mjs` + `packages/electron/preload.mjs`. - Electron imports the server via `@openchamber/web/server/index.js` (workspace dep) and calls `startWebUiServer({...})`. The returned handle has `getPort()` / `stop()`. Notifications flow via an `onDesktopNotification` callback injected at startup — no stdout-parsing IPC. -- Build/release: Electron is the release target. The release workflow also repackages the signed Electron app as a Tauri updater payload for the one-shot migration path documented in `docs/TAURI_TO_ELECTRON_CUTOVER.md`. -- After the cutover ships and stabilises, `packages/desktop/` is deleted; this note collapses back to "Desktop is Electron". +- Build/release: Electron is the desktop release target. ## Tech stack (source of truth: `package.json`, resolved: `bun.lock`) @@ -27,8 +24,7 @@ OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an O - State: Zustand stores and sync layer (`packages/ui/src/stores/`, `packages/ui/src/sync/`) - UI primitives: Base UI (`@base-ui/react`, primary source for dropdown/select/dialog/menu/tooltip/etc. — wrappers live in `packages/ui/src/components/ui/`), Radix UI (`package.json` deps, legacy usages being migrated), HeroUI (`package.json` deps), Remixicon as SVG sprite source only (use shared `Icon`, never direct `@remixicon/react` imports) - Server: Express (`packages/web/server/index.js`) -- Desktop (forward): Electron 41 (`packages/electron/`) -- Desktop (legacy, maintenance-only): Tauri v2 (`packages/desktop/src-tauri/`) +- Desktop: Electron 41 (`packages/electron/`) - VS Code: extension + webview (`packages/vscode/`) ## Monorepo layout @@ -37,8 +33,7 @@ Workspaces are `packages/*` (see `package.json`). - Shared UI: `packages/ui` - Web app + server + CLI: `packages/web` -- Desktop shell (Electron — forward): `packages/electron` -- Desktop shell (Tauri — legacy, maintenance-only): `packages/desktop` +- Desktop shell: `packages/electron` - VS Code extension: `packages/vscode` ## Documentation map @@ -173,7 +168,6 @@ All scripts are in `package.json`. - Build all: `bun run build` - Desktop build (Electron — primary): `bun run electron:build` - Desktop dev (Electron): `bun run electron:dev` -- Desktop build (Tauri — legacy): `bun run desktop:build` - VS Code build: `bun run vscode:build` - Release smoke build: `bun run release:test` (shell script: `scripts/test-release-build.sh`) @@ -182,8 +176,7 @@ All scripts are in `package.json`. - Web bootstrap: `packages/web/src/main.tsx` - Web server: `packages/web/server/index.js` - Web CLI: `packages/web/bin/cli.js` (package bin: `packages/web/package.json`) -- Desktop (Electron — primary): `packages/electron/main.mjs` (boots the web server in-process via `startWebUiServer`, loads web UI over loopback; preload at `packages/electron/preload.mjs` exposes the `__TAURI__` IPC shim so shared UI code is shell-agnostic) -- Desktop (Tauri — legacy): `packages/desktop/src-tauri/src/main.rs` +- Desktop: `packages/electron/main.mjs` (boots the web server in-process via `startWebUiServer`, loads web UI over loopback; preload at `packages/electron/preload.mjs` exposes the desktop IPC bridge) - VS Code extension host: `packages/vscode/src/extension.ts` - VS Code webview bootstrap: `packages/vscode/webview/main.tsx` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95b0b7fa..7777e41d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,13 +19,13 @@ bun install Both are configurable via env vars: `OPENCHAMBER_PORT`, `OPENCHAMBER_HMR_UI_PORT`, `OPENCHAMBER_HMR_API_PORT`. -### Desktop (Tauri) +### Desktop (Electron) ```bash -bun run desktop:dev +bun run electron:dev ``` -Launches Tauri in dev mode with WebView devtools enabled and a distinct dev icon. +Launches the Electron desktop shell in dev mode. ### VS Code Extension @@ -72,7 +72,7 @@ bun run build # Must succeed packages/ ui/ Shared React components, hooks, stores, and theme system web/ Web server (Express) + frontend (Vite) + CLI - desktop/ Tauri macOS app (thin shell around the web UI) + electron/ Electron desktop shell vscode/ VS Code extension (extension host + webview) ``` diff --git a/README.md b/README.md index bc2889ce..36a1c1d2 100644 --- a/README.md +++ b/README.md @@ -419,7 +419,6 @@ Independent project, not affiliated with the OpenCode team. - [OpenCode](https://opencode.ai) - For the excellent API and extensible architecture. - [Flexoki](https://github.com/kepano/flexoki) - Beautiful color scheme by [Steph Ango](https://stephango.com/flexoki). - [Pierre](https://pierrejs-docs.vercel.app/) - Fast, beautiful diff viewer with syntax highlighting. -- [Tauri](https://github.com/tauri-apps/tauri) - Desktop application framework. - [Ghostty-web](https://github.com/coder/ghostty-web) - Great implementation of a Ghostty web renderer. - [David Hill](https://x.com/iamdavidhill) - Who inspired me to release this without [overthinking](https://x.com/iamdavidhill/status/1993648326450020746). - [My wife](https://github.com/yulia-ivashko), who - with zero AI background - sat down with the app for the first time and built the firework celebration that plays on every successful push. diff --git a/bun.lock b/bun.lock index e962c162..9ba24423 100644 --- a/bun.lock +++ b/bun.lock @@ -96,18 +96,9 @@ "vite": "^7.1.2", }, }, - "packages/desktop": { - "name": "@openchamber/desktop", - "version": "1.11.7", - "devDependencies": { - "@tauri-apps/cli": "^2", - "@types/node": "^24.3.1", - "typescript": "~5.8.3", - }, - }, "packages/electron": { "name": "@openchamber/electron", - "version": "1.11.7", + "version": "1.12.0", "dependencies": { "@openchamber/web": "workspace:*", "electron-context-menu": "^4.1.2", @@ -122,7 +113,7 @@ }, "packages/ui": { "name": "@openchamber/ui", - "version": "1.11.7", + "version": "1.12.0", "dependencies": { "@base-ui/react": "^1.4.0", "@codemirror/autocomplete": "^6.20.0", @@ -197,7 +188,6 @@ "@eslint/js": "^9.33.0", "@remixicon/react": "^4.7.0", "@tailwindcss/postcss": "^4.0.0", - "@tauri-apps/api": "^2.10.1", "@types/node": "^24.3.1", "@types/prismjs": "^1.26.6", "@types/qrcode": "^1.5.5", @@ -223,7 +213,7 @@ }, "packages/vscode": { "name": "openchamber", - "version": "1.11.7", + "version": "1.12.0", "dependencies": { "@openchamber/ui": "workspace:*", "@opencode-ai/sdk": "^1.15.10", @@ -246,7 +236,7 @@ }, "packages/web": { "name": "@openchamber/web", - "version": "1.11.7", + "version": "1.12.0", "bin": { "openchamber": "./bin/cli.js", }, @@ -942,8 +932,6 @@ "@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@openchamber/desktop": ["@openchamber/desktop@workspace:packages/desktop"], - "@openchamber/electron": ["@openchamber/electron@workspace:packages/electron"], "@openchamber/ui": ["@openchamber/ui@workspace:packages/ui"], @@ -1254,32 +1242,6 @@ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.19", "", {}, "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g=="], - "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="], - - "@tauri-apps/cli": ["@tauri-apps/cli@2.10.0", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.0", "@tauri-apps/cli-darwin-x64": "2.10.0", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0", "@tauri-apps/cli-linux-arm64-gnu": "2.10.0", "@tauri-apps/cli-linux-arm64-musl": "2.10.0", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.0", "@tauri-apps/cli-linux-x64-gnu": "2.10.0", "@tauri-apps/cli-linux-x64-musl": "2.10.0", "@tauri-apps/cli-win32-arm64-msvc": "2.10.0", "@tauri-apps/cli-win32-ia32-msvc": "2.10.0", "@tauri-apps/cli-win32-x64-msvc": "2.10.0" }, "bin": { "tauri": "tauri.js" } }, "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q=="], - - "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA=="], - - "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q=="], - - "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.0", "", { "os": "linux", "cpu": "arm" }, "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g=="], - - "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA=="], - - "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA=="], - - "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.0", "", { "os": "linux", "cpu": "none" }, "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw=="], - - "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng=="], - - "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q=="], - - "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g=="], - - "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A=="], - - "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ=="], - "@textlint/ast-node-types": ["@textlint/ast-node-types@15.5.2", "", {}, "sha512-fCaOxoup5LIyBEo7R1oYWE7V4bSX0KQeHh66twon9e9usaLE3ijgF8QjYsR6joCssdeCHVd0wHm7ppsEyTr6vg=="], "@textlint/linter-formatter": ["@textlint/linter-formatter@15.5.2", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.5.2", "@textlint/resolver": "15.5.2", "@textlint/types": "15.5.2", "chalk": "^4.1.2", "debug": "^4.4.3", "js-yaml": "^4.1.1", "lodash": "^4.17.23", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-jAw7jWM8+wU9cG6Uu31jGyD1B+PAVePCvnPKC/oov+2iBPKk3ao30zc/Itmi7FvXo4oPaL9PmzPPQhyniPVgVg=="], diff --git a/docs/TAURI_TO_ELECTRON_CUTOVER.md b/docs/TAURI_TO_ELECTRON_CUTOVER.md deleted file mode 100644 index ec5b596d..00000000 --- a/docs/TAURI_TO_ELECTRON_CUTOVER.md +++ /dev/null @@ -1,414 +0,0 @@ -# Tauri → Electron auto-update cutover - -> Self-contained playbook. The branch and conversation where this plan was -> designed will not be around when the cutover happens — read this file top to -> bottom and execute; do not assume prior context. - -> Current status: the release workflow cutover is implemented. Desktop releases -> now build Electron and repackage that Electron `.app` into the old Tauri -> updater format for existing Tauri installs. The next safe engineering step is -> [Step 5 — Remove Tauri-specific code](#step-5--remove-tauri-specific-code), -> but only after the transition release has shipped and lived for at least 2 -> weeks with no rollback. - -## What this is - -OpenChamber historically shipped as a Tauri app. A parallel Electron shell was -added on branch `electron-app` (merged to `main` as part of a larger migration). -Since then, both desktop shells have been released in the same GitHub release -and each has its own auto-update channel: - -| Shell | Manifest | Update format | Secret used to sign | -|----------|-------------------|---------------------|---------------------| -| Tauri | `latest.json` | `.tar.gz` + `.sig` | `TAURI_SIGNING_PRIVATE_KEY` (Tauri signer / minisign format) | -| Electron | `latest-mac.yml` | `.zip` + `blockmap` | Developer ID codesign (APPLE_* secrets) | - -Existing Tauri installs keep their own auto-update path (`latest.json`). -Electron installs auto-update through `latest-mac.yml`. They coexist without -conflict because filenames and manifests differ. - -At some point the user wants to **stop maintaining the Tauri build** and make -the Tauri installs migrate themselves into Electron via auto-update. This -document describes how to do that in a single "transition release". - -## The core trick - -Tauri's updater downloads whatever `.tar.gz` the `latest.json` points at, -verifies the minisign signature, unpacks the contents **over** the existing -`.app` directory, and restarts. It does **not** introspect the payload — it -just replaces files. - -So: produce a `.tar.gz` of the Electron `.app`, sign it with the existing -Tauri minisign key, point `latest.json` at it. Tauri users receive the update, -their `OpenChamber.app` becomes the Electron bundle in-place, and next launch -starts Electron. Subsequent updates go through `latest-mac.yml` -(electron-updater). One-way migration, one-shot workflow change. - -## Prerequisites before running the cutover - -Check all of these before making any release: - -1. **Electron has shipped stable through its own `latest-mac.yml` path for at - least 2 releases.** Verify: - ``` - gh release list --repo btriapitsyn/openchamber - gh release view vX.Y.Z --repo btriapitsyn/openchamber \ - | grep -E 'OpenChamber-.*\.zip|latest-mac\.yml' - ``` - A user on Electron should have successfully auto-updated at least once. - If not, pause and stabilise that path first — don't stack risk. - -2. **`~/.config/openchamber/settings.json` is still the shared state path.** - Tauri `src-tauri/src/main.rs:settings_file_path` and Electron - `packages/electron/main.mjs:settingsFilePath` must both resolve to - `$HOME/.config/openchamber/settings.json`. If either has moved, data parity - breaks and this migration loses user data. Audit both paths, update the - non-migrated shell to match before proceeding. - -3. **Electron `appId` is `dev.openchamber.desktop`** (check - `packages/electron/package.json` `build.appId`). Tauri identifier is - `ai.opencode.openchamber`. These differ intentionally — it means macOS - LaunchServices will re-register after the in-place replace. That's fine but - see "Risks" below. - -4. **All GitHub secrets still valid:** `APPLE_CERTIFICATE`, - `APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`, - `TAURI_SIGNING_PRIVATE_KEY`, `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. A - workflow_dispatch dry-run should succeed before the real tag. - -5. **`minisign` CLI is available on the macOS runner** (or installable via - brew). Used to sign the Electron tarball with the Tauri key. - -## Current release workflow - -The release workflow no longer builds a Tauri desktop app. It now does this: - -```text -create-release -├── build-desktop-electron-macos (Electron .dmg/.zip/blockmap/latest-mac.yml) -├── repackage-electron-as-tauri-update (Electron .app -> Tauri .app.tar.gz/.sig) -├── publish-npm -├── combine-manifests (Tauri latest.json for migration only) -├── combine-electron-manifests (Electron latest-mac.yml) -└── finalize-release -``` - -The transition works like this: - -1. `build-desktop-electron-macos` builds, signs, and notarizes the Electron app. -2. It wraps the signed `OpenChamber.app` in a tarball and uploads that tarball - as a short-lived Actions artifact. -3. `repackage-electron-as-tauri-update` downloads that Electron `.app`. -4. It packs it into `OpenChamber--darwin-*.app.tar.gz`. -5. It signs that tarball with `tauri signer sign` and the existing Tauri signing key. -6. It uploads the tarball and `.sig` to the GitHub release. -7. It generates Tauri-compatible manifests and `combine-manifests` merges them into `latest.json`. - -So old Tauri installs still see the update contract they expect: - -```text -latest.json -> .app.tar.gz -> .sig -``` - -But the payload inside the `.app.tar.gz` is Electron, not Tauri. Tauri's updater -only verifies the signature and extracts the bundle over the existing -`/Applications/OpenChamber.app`. After restart, the app is Electron and future -updates use `latest-mac.yml` through `electron-updater`. - -Note: do not upload the `.app` directory directly with `actions/upload-artifact`. -That action can flatten the app to its inner `Contents/` folder and can also -normalize file modes. Losing the executable bit on `Contents/MacOS/*` makes the -updated app fail to launch with a permissions/package error. The workflow wraps -the `.app` in a tarball before upload so permissions survive the handoff between -jobs, then verifies the app executable is still executable before creating the -Tauri updater tarball. - -## Historical release workflow changes - -The file edited for the cutover was `.github/workflows/release.yml`. - -Before the cutover it had these jobs (simplified): - -``` -create-release -├── build-desktop-macos (Tauri .dmg/.tar.gz/.tar.gz.sig) -├── build-desktop-electron-macos (Electron .dmg/.zip/blockmap/latest-mac.yml) -├── publish-npm -├── combine-manifests (merges Tauri per-arch JSONs → latest.json) -├── combine-electron-manifests (merges Electron per-arch YMLs → latest-mac.yml) -└── finalize-release -``` - -### Step 1 — Remove the Tauri build - -Status: done. - -Delete these jobs entirely: -- `build-desktop-macos` -- `combine-manifests` - -They are replaced by the repackage job (below). `finalize-release` `needs:` -list must be updated to drop both. - -### Step 2 — Add a repackage job - -Status: done. - -Insert after `build-desktop-electron-macos`: - -```yaml -repackage-electron-as-tauri-update: - needs: [create-release, build-desktop-electron-macos] - runs-on: macos-26 - strategy: - fail-fast: false - matrix: - include: - - arch: arm64 - platform: darwin-aarch64 - - arch: x64 - platform: darwin-x86_64 - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - uses: actions/setup-node@v4 - with: - node-version: '20' - - # Pull the signed+notarized Electron .app that build-desktop-electron-macos - # already produced. Either re-download the dmg and mount+copy the .app, or - # (cleaner) modify build-desktop-electron-macos to upload the .app itself - # as an artifact so this job can download it. Prefer the latter — adds one - # `actions/upload-artifact@v4` step uploading `packages/electron/dist/mac-/OpenChamber.app`. - - - name: Download signed Electron .app - uses: actions/download-artifact@v4 - with: - name: electron-app-${{ matrix.arch }} - path: staged - - - name: Tar and sign Electron .app as Tauri update payload - env: - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - VERSION: ${{ needs.create-release.outputs.version }} - run: | - set -euo pipefail - cd staged - # The tarball name convention Tauri's updater expects. Must end in - # `.app.tar.gz`. Name stays stable — Tauri updater does not care about - # the inner .app name. - TARBALL="OpenChamber.app.tar.gz" - tar -czf "$TARBALL" OpenChamber.app - - # Use Tauri's signer instead of minisign directly. The CI secret is in - # the format consumed by TAURI_SIGNING_PRIVATE_KEY. - bun run --cwd ../packages/desktop tauri signer sign "$PWD/$TARBALL" - - # Rename per platform so the release has distinct names for arm64/x64. - mv "$TARBALL" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz" - mv "${TARBALL}.sig" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig" - - - name: Generate Tauri latest-.json - env: - VERSION: ${{ needs.create-release.outputs.version }} - REPO: ${{ github.repository }} - run: | - SIG=$(cat staged/OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig) - TAR=OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz - cat > staged/latest-${{ matrix.platform }}.json < OpenChamber Desktop - -[![GitHub stars](https://img.shields.io/github/stars/btriapitsyn/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0yMjkuMDYsMTA4Ljc5bC00OC43LDQyLDE0Ljg4LDYyLjc5YTguNCw4LjQsMCwwLDEtMTIuNTIsOS4xN0wxMjgsMTg5LjA5LDczLjI4LDIyMi43NGE4LjQsOC40LDAsMCwxLTEyLjUyLTkuMTdsMTQuODgtNjIuNzktNDguNy00MkE4LjQ2LDguNDYsMCwwLDEsMzEuNzMsOTRMOTUuNjQsODguOGwyNC42Mi01OS42YTguMzYsOC4zNiwwLDAsMSwxNS40OCwwbDI0LjYyLDU5LjZMMjI0LjI3LDk0QTguNDYsOC40NiwwLDAsMSwyMjkuMDYsMTA4Ljc5WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIzOS4xOCw5Ny4yNkExNi4zOCwxNi4zOCwwLDAsMCwyMjQuOTIsODZsLTU5LTQuNzZMMTQzLjE0LDI2LjE1YTE2LjM2LDE2LjM2LDAsMCwwLTMwLjI3LDBMOTAuMTEsODEuMjMsMzEuMDgsODZhMTYuNDYsMTYuNDYsMCwwLDAtOS4zNywyOC44Nmw0NSwzOC44M0w1MywyMTEuNzVhMTYuMzgsMTYuMzgsMCwwLDAsMjQuNSwxNy44MkwxMjgsMTk4LjQ5bDUwLjUzLDMxLjA4QTE2LjQsMTYuNCwwLDAsMCwyMDMsMjExLjc1bC0xMy43Ni01OC4wNyw0NS0zOC44M0ExNi40MywxNi40MywwLDAsMCwyMzkuMTgsOTcuMjZabS0xNS4zNCw1LjQ3LTQ4LjcsNDJhOCw4LDAsMCwwLTIuNTYsNy45MWwxNC44OCw2Mi44YS4zNy4zNywwLDAsMS0uMTcuNDhjLS4xOC4xNC0uMjMuMTEtLjM4LDBsLTU0LjcyLTMzLjY1YTgsOCwwLDAsMC04LjM4LDBMNjkuMDksMjE1Ljk0Yy0uMTUuMDktLjE5LjEyLS4zOCwwYS4zNy4zNywwLDAsMS0uMTctLjQ4bDE0Ljg4LTYyLjhhOCw4LDAsMCwwLTIuNTYtNy45MWwtNDguNy00MmMtLjEyLS4xLS4yMy0uMTktLjEzLS41cy4xOC0uMjcuMzMtLjI5bDYzLjkyLTUuMTZBOCw4LDAsMCwwLDEwMyw5MS44NmwyNC42Mi01OS42MWMuMDgtLjE3LjExLS4yNS4zNS0uMjVzLjI3LjA4LjM1LjI1TDE1Myw5MS44NmE4LDgsMCwwLDAsNi43NSw0LjkybDYzLjkyLDUuMTZjLjE1LDAsLjI0LDAsLjMzLjI5UzIyNCwxMDIuNjMsMjIzLjg0LDEwMi43M1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=66800B)](https://github.com/btriapitsyn/openchamber/stargazers) -[![GitHub release](https://img.shields.io/github/v/release/btriapitsyn/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0xMjgsMTI5LjA5VjIzMmE4LDgsMCwwLDEtMy44NC0xbC04OC00OC4xOGE4LDgsMCwwLDEtNC4xNi03VjgwLjE4YTgsOCwwLDAsMSwuNy0zLjI1WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIyMy42OCw2Ni4xNSwxMzUuNjgsMThhMTUuODgsMTUuODgsMCwwLDAtMTUuMzYsMGwtODgsNDguMTdhMTYsMTYsMCwwLDAtOC4zMiwxNHY5NS42NGExNiwxNiwwLDAsMCw4LjMyLDE0bDg4LDQ4LjE3YTE1Ljg4LDE1Ljg4LDAsMCwwLDE1LjM2LDBsODgtNDguMTdhMTYsMTYsMCwwLDAsOC4zMi0xNFY4MC4xOEExNiwxNiwwLDAsMCwyMjMuNjgsNjYuMTVaTTEyOCwzMmw4MC4zNCw0NC0yOS43NywxNi4zLTgwLjM1LTQ0Wk0xMjgsMTIwLDQ3LjY2LDc2bDMzLjktMTguNTYsODAuMzQsNDRaTTQwLDkwbDgwLDQzLjc4djg1Ljc5TDQwLDE3NS44MlptMTc2LDg1Ljc4aDBsLTgwLDQzLjc5VjEzMy44MmwzMi0xNy41MVYxNTJhOCw4LDAsMCwwLDE2LDBWMTA3LjU1TDIxNiw5MHY4NS43N1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=205EA6)](https://github.com/btriapitsyn/openchamber/releases/latest) -[![Discord](https://img.shields.io/badge/Discord-join.svg?style=flat&labelColor=100F0F&color=8B7EC8&logo=discord&logoColor=FFFCF0)](https://discord.gg/ZYRSdnwwKA) - -A native macOS app for [OpenCode](https://opencode.ai). Feels like home - multiple windows, SSH remotes, project actions, and everything running locally. - -Full project overview, screenshots, and all features: [github.com/btriapitsyn/openchamber](https://github.com/btriapitsyn/openchamber) - -## Install - -Download from [Releases](https://github.com/btriapitsyn/openchamber/releases). Available for macOS (Apple Silicon and Intel). - -> **Prerequisite:** [OpenCode CLI](https://opencode.ai) installed. - -## What makes the desktop app special - -- **Remote instances over SSH** - connect to remote OpenChamber servers with dedicated lifecycle and UX flows -- **Project Actions** - run dev servers, configure SSH port forwarding, open remote URLs locally -- **Multi-window** - work on several projects in parallel, each in its own window -- **"Open In" shortcuts** - open workspace in Finder, Terminal, or your editor of choice -- **Local + remote switching** - jump between local and remote OpenChamber instances -- **Native macOS integration** - menus, deep-links, auto-update, and polished window management - -Plus everything from the shared OpenChamber UI: branchable timeline, Git sidebar, terminal, voice mode, and more. - -## Features - -### Core UI - -- Branchable chat timeline with `/undo`, `/redo`, and one-click forks from earlier turns -- Smart tool UIs for diffs, file operations, permissions, and long-running task progress -- Multi-agent runs from one prompt with isolated worktrees for safe comparisons -- Git workflows in-app: identities, commits, PR creation, checks, and merge actions -- Context visibility tools (token/cost breakdowns, raw message inspection, and activity summaries) -- Integrated terminal with per-directory sessions and stable performance on heavy output - -### Desktop (macOS) - -- Native macOS menu integration with polished app actions and deep-link handling -- Multi-window support for parallel project/session workflows -- "Open In" shortcuts for Finder, Terminal, and your preferred editor -- Fast switching between local and remote instances -- Workspace-first startup flow with directory picker and steadier window restore behavior - -### Remote Tunnel (Desktop) - -- Configure in **Settings -> OpenChamber -> Remote Tunnel**. -- Supported Cloudflare modes: **Quick**, **Managed Remote**, **Managed Local**. -- One active tunnel per Desktop instance. Starting a different mode replaces the current tunnel. -- Replacing or stopping a tunnel revokes existing connect links and invalidates remote tunnel sessions. -- Connect links are one-time tokens; generate a new link for each new connection attempt. - -## Development - -```bash -git clone https://github.com/btriapitsyn/openchamber.git -cd openchamber -bun install -bun run desktop:dev -``` - - - -## License - -MIT diff --git a/packages/desktop/noop-dist/index.html b/packages/desktop/noop-dist/index.html deleted file mode 100644 index 9d4b4ba6..00000000 --- a/packages/desktop/noop-dist/index.html +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - OpenChamber - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/desktop/package.json b/packages/desktop/package.json deleted file mode 100644 index 7e22bb1e..00000000 --- a/packages/desktop/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@openchamber/desktop", - "version": "1.12.0", - "private": true, - "type": "module", - "desktopPrerequisites": [ - "Rust stable toolchain (via rustup)", - "Xcode Command Line Tools installed", - "Tauri CLI installed (cargo install tauri-cli@^2)" - ], - "scripts": { - "tauri": "tauri", - "tauri:dev": "tauri dev --features devtools", - "tauri:build": "tauri build", - "build:sidecar": "node ./scripts/build-sidecar.mjs", - "build": "bun -e \"process.exit(0)\"", - "type-check": "bun -e \"process.exit(0)\"", - "lint": "bun -e \"process.exit(0)\"" - }, - "dependencies": {}, - "devDependencies": { - "@tauri-apps/cli": "^2", - "@types/node": "^24.3.1", - "typescript": "~5.8.3" - } -} diff --git a/packages/desktop/public/ibm-plex-mono-latin-600-normal.woff2 b/packages/desktop/public/ibm-plex-mono-latin-600-normal.woff2 deleted file mode 100644 index 67aeeb0104a02cf060b813abf6dcb6fdb97b7512..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15620 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 diff --git a/packages/desktop/public/ibm-plex-mono-latin-700-normal.woff2 b/packages/desktop/public/ibm-plex-mono-latin-700-normal.woff2 deleted file mode 100644 index 5d7ff0c5f53380486db43fd746bd66cf3511ccf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14908 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 uPxY;>+eP#z&}WNNefTrzTlyRNw!YK5DY@ { - if (typeof process.env.TAURI_ENV_TARGET_TRIPLE === 'string' && process.env.TAURI_ENV_TARGET_TRIPLE.trim()) { - return process.env.TAURI_ENV_TARGET_TRIPLE.trim(); - } - - if (process.platform === 'darwin') { - return process.arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin'; - } - - if (process.platform === 'win32') { - return 'x86_64-pc-windows-msvc'; - } - - if (process.platform === 'linux') { - return process.arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 'x86_64-unknown-linux-gnu'; - } - - return `${process.arch}-${process.platform}`; -}; - -const targetTriple = inferTargetTriple(); - -const bunCompileTargetByTriple = { - 'aarch64-apple-darwin': 'bun-darwin-arm64', - 'x86_64-apple-darwin': 'bun-darwin-x64', - 'aarch64-unknown-linux-gnu': 'bun-linux-arm64', - 'x86_64-unknown-linux-gnu': 'bun-linux-x64', - 'x86_64-pc-windows-msvc': 'bun-windows-x64', -}; - -const compileTarget = bunCompileTargetByTriple[targetTriple]; - -if (!compileTarget) { - console.warn( - `[desktop] unknown target triple '${targetTriple}', falling back to host-arch sidecar build` - ); -} - -const sidecarBaseName = process.platform === 'win32' - ? `openchamber-server-${targetTriple}.exe` - : `openchamber-server-${targetTriple}`; -const sidecarOutPath = path.join(sidecarsDir, sidecarBaseName); - - -const run = (cmd, args, cwd) => { - const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' }); - if (result.error) throw result.error; - if (result.status !== 0) { - throw new Error(`Command failed: ${cmd} ${args.join(' ')}`); - } -}; - -const resolveBun = () => { - if (typeof process.env.BUN === 'string' && process.env.BUN.trim()) { - return process.env.BUN.trim(); - } - - const result = spawnSync('/bin/bash', ['-lc', 'command -v bun'], { encoding: 'utf8' }); - const resolved = (result.stdout || '').trim(); - if (resolved) { - return resolved; - } - - return 'bun'; -}; - -const bunExe = resolveBun(); - - -const copyDir = async (src, dst) => { - await fs.mkdir(dst, { recursive: true }); - const entries = await fs.readdir(src, { withFileTypes: true }); - for (const entry of entries) { - const from = path.join(src, entry.name); - const to = path.join(dst, entry.name); - if (entry.isDirectory()) { - await copyDir(from, to); - } else if (entry.isSymbolicLink()) { - const link = await fs.readlink(from); - await fs.symlink(link, to); - } else { - await fs.copyFile(from, to); - } - } -}; - -console.log('[desktop] building web UI dist...'); -run(bunExe, ['run', 'build'], webDir); - -console.log('[desktop] preparing tauri resources...'); -await fs.mkdir(resourcesDir, { recursive: true }); -await fs.rm(resourcesWebDistDir, { recursive: true, force: true }); -await copyDir(webDistDir, resourcesWebDistDir); - -console.log('[desktop] building openchamber-server sidecar...'); -await fs.mkdir(sidecarsDir, { recursive: true }); - -const buildArgs = [ - 'build', - '--compile', - path.join(webDir, 'server', 'index.js'), - '--outfile', - sidecarOutPath, -]; - -if (compileTarget) { - buildArgs.push('--target', compileTarget); -} - -run(bunExe, buildArgs, repoRoot); - -if (process.platform !== 'win32') { - await fs.chmod(sidecarOutPath, 0o755); -} - -console.log(`[desktop] sidecar ready: ${sidecarOutPath}`); -console.log(`[desktop] web assets ready: ${resourcesWebDistDir}`); diff --git a/packages/desktop/scripts/desktop-dev.mjs b/packages/desktop/scripts/desktop-dev.mjs deleted file mode 100644 index c80ed907..00000000 --- a/packages/desktop/scripts/desktop-dev.mjs +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env node -import { spawn } from 'node:child_process'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const repoRoot = path.resolve(__dirname, '../../..'); -const desktopDir = path.join(repoRoot, 'packages/desktop'); - -function spawnProcess(command, args, opts = {}) { - return spawn(command, args, { - cwd: repoRoot, - env: { ...process.env }, - stdio: 'inherit', - detached: process.platform !== 'win32', - ...opts, - }); -} - -function waitForExit(child, timeoutMs) { - return new Promise((resolve) => { - if (!child || child.exitCode !== null || child.signalCode !== null) { - resolve(); - return; - } - - const onExit = () => { - clearTimeout(timer); - resolve(); - }; - - const timer = setTimeout(() => { - child.off('exit', onExit); - resolve(); - }, timeoutMs); - - child.once('exit', onExit); - }); -} - -function signalChild(child, signal) { - if (!child || child.exitCode !== null || child.signalCode !== null) { - return; - } - - try { - if (process.platform !== 'win32') { - process.kill(-child.pid, signal); - return; - } - } catch { - } - - try { - child.kill(signal); - } catch { - } -} - -async function stopChildTree(child) { - if (!child || child.exitCode !== null || child.signalCode !== null) { - return; - } - - signalChild(child, 'SIGINT'); - await waitForExit(child, 2500); - - if (child.exitCode === null && child.signalCode === null) { - signalChild(child, 'SIGTERM'); - await waitForExit(child, 2500); - } - - if (child.exitCode === null && child.signalCode === null) { - signalChild(child, 'SIGKILL'); - await waitForExit(child, 1000); - } -} - -async function main() { - const tauriProcess = spawnProcess('bun', [ - '--cwd', - desktopDir, - 'tauri', - 'dev', - '--features', - 'devtools', - '--config', - './src-tauri/tauri.dev.conf.json', - ]); - - let cleaning = false; - - const teardown = async (code) => { - if (cleaning) { - return; - } - cleaning = true; - - await stopChildTree(tauriProcess); - process.exit(typeof code === 'number' ? code : 0); - }; - - const handleChildExit = (childName) => (code, signal) => { - if (code !== 0 || signal) { - console.warn(`[desktop:dev] ${childName} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}.`); - } - teardown(code).catch((error) => { - console.error('[desktop:dev] Cleanup error:', error); - process.exit(code ?? 1); - }); - }; - - tauriProcess.on('exit', handleChildExit('Tauri dev process')); - const errorHandler = (label) => (error) => { - console.error(`[desktop:dev] Failed to start ${label}:`, error); - teardown(1).catch(() => process.exit(1)); - }; - - tauriProcess.on('error', errorHandler('Tauri dev process')); - - const signalExitCodes = { - SIGINT: 130, - SIGTERM: 143, - SIGQUIT: 131, - }; - - Object.entries(signalExitCodes).forEach(([signal, exitCode]) => { - process.on(signal, () => { - teardown(exitCode).catch(() => process.exit(exitCode)); - }); - }); -} - -main().catch((error) => { - console.error('[desktop:dev] Unexpected error:', error); - process.exit(1); -}); diff --git a/packages/desktop/scripts/dev-web-server.mjs b/packages/desktop/scripts/dev-web-server.mjs deleted file mode 100644 index 0af68b5e..00000000 --- a/packages/desktop/scripts/dev-web-server.mjs +++ /dev/null @@ -1,197 +0,0 @@ -import path from 'node:path'; -import { spawn, spawnSync } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; - -const DESKTOP_DEV_PORT = 3901; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const repoRoot = path.resolve(__dirname, '..', '..', '..'); -const desktopDir = path.join(repoRoot, 'packages', 'desktop'); -const tauriDir = path.join(desktopDir, 'src-tauri'); - -const inferTargetTriple = () => { - const fromEnv = typeof process.env.TAURI_ENV_TARGET_TRIPLE === 'string' ? process.env.TAURI_ENV_TARGET_TRIPLE.trim() : ''; - if (fromEnv) return fromEnv; - - if (process.platform === 'darwin') { - return process.arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin'; - } - - if (process.platform === 'win32') { - return 'x86_64-pc-windows-msvc'; - } - - if (process.platform === 'linux') { - return process.arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 'x86_64-unknown-linux-gnu'; - } - - return `${process.arch}-${process.platform}`; -}; - -const targetTriple = inferTargetTriple(); -const sidecarName = process.platform === 'win32' - ? `openchamber-server-${targetTriple}.exe` - : `openchamber-server-${targetTriple}`; - -const sidecarPath = path.join(tauriDir, 'sidecars', sidecarName); -const distDir = path.join(tauriDir, 'resources', 'web-dist'); -const webDir = path.join(repoRoot, 'packages', 'web'); - -const run = (cmd, args, cwd) => { - const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' }); - if (result.error) throw result.error; - if (result.status !== 0) { - throw new Error(`Command failed: ${cmd} ${args.join(' ')}`); - } -}; - -console.log('[desktop] ensuring sidecar + web-dist...'); -run('node', ['./scripts/build-sidecar.mjs'], desktopDir); - -console.log(`[desktop] starting API server on http://127.0.0.1:${DESKTOP_DEV_PORT} ...`); - -const apiChild = spawn(sidecarPath, ['--port', String(DESKTOP_DEV_PORT)], { - cwd: repoRoot, - stdio: 'inherit', - detached: process.platform !== 'win32', - env: { - ...process.env, - OPENCHAMBER_HOST: '127.0.0.1', - OPENCHAMBER_DIST_DIR: distDir, - NO_PROXY: process.env.NO_PROXY || 'localhost,127.0.0.1', - no_proxy: process.env.no_proxy || 'localhost,127.0.0.1', - }, -}); - -console.log('[desktop] starting Vite HMR server on http://127.0.0.1:5173 ...'); - -const webChild = spawn('bun', ['x', 'vite', '--host', '127.0.0.1', '--port', '5173', '--strictPort'], { - cwd: webDir, - stdio: 'inherit', - detached: process.platform !== 'win32', - env: { - ...process.env, - OPENCHAMBER_PORT: String(DESKTOP_DEV_PORT), - NO_PROXY: process.env.NO_PROXY || 'localhost,127.0.0.1', - no_proxy: process.env.no_proxy || 'localhost,127.0.0.1', - }, -}); - -let shuttingDown = false; - -function waitForExit(child, timeoutMs) { - return new Promise((resolve) => { - if (!child || child.exitCode !== null || child.signalCode !== null) { - resolve(); - return; - } - - const onExit = () => { - clearTimeout(timer); - resolve(); - }; - - const timer = setTimeout(() => { - child.off('exit', onExit); - resolve(); - }, timeoutMs); - - child.once('exit', onExit); - }); -} - -function signalChild(child, signal) { - if (!child || child.exitCode !== null || child.signalCode !== null) { - return; - } - - try { - if (process.platform !== 'win32') { - process.kill(-child.pid, signal); - return; - } - } catch { - } - - try { - child.kill(signal); - } catch { - } -} - -async function requestApiShutdown() { - const url = `http://127.0.0.1:${DESKTOP_DEV_PORT}/api/system/shutdown`; - try { - await fetch(url, { method: 'POST' }); - } catch { - } -} - -async function stopChildTree(child) { - if (!child || child.exitCode !== null || child.signalCode !== null) { - return; - } - - signalChild(child, 'SIGINT'); - await waitForExit(child, 2500); - - if (child.exitCode === null && child.signalCode === null) { - signalChild(child, 'SIGTERM'); - await waitForExit(child, 2500); - } - - if (child.exitCode === null && child.signalCode === null) { - signalChild(child, 'SIGKILL'); - await waitForExit(child, 1000); - } -} - -const shutdown = async (exitCode = 0) => { - if (shuttingDown) return; - shuttingDown = true; - - await requestApiShutdown(); - await Promise.all([stopChildTree(webChild), stopChildTree(apiChild)]); - process.exit(exitCode); -}; - -const handleExit = (label) => (code, signal) => { - if (shuttingDown) { - return; - } - - if (code !== 0 || signal) { - console.error(`[desktop] ${label} exited unexpectedly (code=${code ?? 'null'} signal=${signal ?? 'none'})`); - } - - shutdown(typeof code === 'number' ? code : 1).catch((error) => { - console.error('[desktop] shutdown failed:', error); - process.exit(1); - }); -}; - -apiChild.on('exit', handleExit('API server')); -webChild.on('exit', handleExit('Vite server')); - -const handleError = (label) => (error) => { - if (shuttingDown) { - return; - } - console.error(`[desktop] failed to start ${label}:`, error); - shutdown(1).catch(() => process.exit(1)); -}; - -apiChild.on('error', handleError('API server')); -webChild.on('error', handleError('Vite server')); - -process.on('SIGINT', () => { - shutdown(130).catch(() => process.exit(130)); -}); -process.on('SIGTERM', () => { - shutdown(143).catch(() => process.exit(143)); -}); -process.on('SIGHUP', () => { - shutdown(129).catch(() => process.exit(129)); -}); diff --git a/packages/desktop/scripts/opencode-cli.mjs b/packages/desktop/scripts/opencode-cli.mjs deleted file mode 100644 index 199fc967..00000000 --- a/packages/desktop/scripts/opencode-cli.mjs +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env node -import { spawn } from 'node:child_process'; -import { access, readFile, unlink, writeFile } from 'node:fs/promises'; -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const desktopDir = path.resolve(__dirname, '..'); -const stateFile = path.join(desktopDir, '.opencode-cli-state.json'); -const DEFAULT_BIN_CANDIDATES = [ - process.env.OPENCHAMBER_OPENCODE_PATH, - process.env.OPENCHAMBER_OPENCODE_BIN, - process.env.OPENCODE_PATH, - process.env.OPENCODE_BINARY, - '/opt/homebrew/bin/opencode', - '/usr/local/bin/opencode', - '/usr/bin/opencode', - path.join(os.homedir(), '.local/bin/opencode'), -].filter(Boolean); -const CLI_ARGS_ENV = process.env.OPENCHAMBER_OPENCODE_ARGS; -const DEFAULT_ARGS = CLI_ARGS_ENV - ? parseArgs(CLI_ARGS_ENV) - : ['api']; - -function parseArgs(raw) { - if (!raw || typeof raw !== 'string') { - return []; - } - const trimmed = raw.trim(); - if (!trimmed) { - return []; - } - if (trimmed.startsWith('[')) { - try { - const parsed = JSON.parse(trimmed); - if (Array.isArray(parsed) && parsed.every((item) => typeof item === 'string')) { - return parsed; - } - } catch { - // fall through to whitespace split - } - } - return trimmed.split(/\s+/g); -} - -async function fileExists(targetPath) { - try { - await access(targetPath, fs.constants.X_OK); - return true; - } catch { - return false; - } -} - -async function resolveCliPath() { - for (const candidate of DEFAULT_BIN_CANDIDATES) { - if (candidate && await fileExists(candidate)) { - return candidate; - } - } - - const envPath = process.env.PATH || ''; - for (const segment of envPath.split(path.delimiter)) { - const candidate = path.join(segment, 'opencode'); - if (await fileExists(candidate)) { - return candidate; - } - } - - throw new Error('Unable to locate the OpenCode CLI. Set OPENCHAMBER_OPENCODE_PATH to the executable.'); -} - -async function readState() { - try { - const raw = await readFile(stateFile, 'utf8'); - const data = JSON.parse(raw); - if (typeof data?.pid === 'number') { - return data; - } - } catch { - // ignore - } - return null; -} - -function isProcessAlive(pid) { - if (!pid || typeof pid !== 'number') { - return false; - } - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -async function writeState(pid) { - await writeFile(stateFile, JSON.stringify({ pid }), 'utf8'); -} - -async function removeStateFile() { - try { - await unlink(stateFile); - } catch { - // already removed - } -} - -function spawnCli(cliPath, args) { - const env = { - ...process.env, - OPENCHAMBER_OPENCODE_PORT: process.env.OPENCHAMBER_OPENCODE_PORT || process.env.OPENCODE_PORT || process.env.OPENCHAMBER_INTERNAL_PORT || '0', - }; - const cwd = process.env.OPENCHAMBER_OPENCODE_CWD || process.cwd(); - - const child = spawn(cliPath, args.length > 0 ? args : DEFAULT_ARGS, { - cwd, - env, - detached: true, - stdio: 'ignore', - }); - - child.unref(); - return child; -} - -export async function startCli({ silent = false } = {}) { - const existing = await readState(); - if (existing?.pid && isProcessAlive(existing.pid)) { - if (!silent) { - console.log(`[desktop:start-cli] OpenCode CLI already running (pid ${existing.pid}).`); - } - return existing.pid; - } - - const cliPath = await resolveCliPath(); - const child = spawnCli(cliPath, DEFAULT_ARGS); - await writeState(child.pid); - if (!silent) { - console.log(`[desktop:start-cli] OpenCode CLI started (${cliPath}) pid ${child.pid}.`); - } - return child.pid; -} - -export async function stopCli({ silent = false } = {}) { - const state = await readState(); - if (!state?.pid) { - if (!silent) { - console.log('[desktop:stop-cli] No OpenCode CLI PID recorded.'); - } - return; - } - - const { pid } = state; - if (!isProcessAlive(pid)) { - await removeStateFile(); - if (!silent) { - console.log('[desktop:stop-cli] CLI already stopped.'); - } - return; - } - - try { - process.kill(pid, 'SIGTERM'); - } catch (error) { - if (!silent) { - console.error(`[desktop:stop-cli] Failed to send SIGTERM to pid ${pid}:`, error); - } - } - - const timeoutMs = 5000; - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - if (!isProcessAlive(pid)) { - await removeStateFile(); - if (!silent) { - console.log('[desktop:stop-cli] OpenCode CLI stopped.'); - } - return; - } - await new Promise((resolve) => setTimeout(resolve, 250)); - } - - try { - process.kill(pid, 'SIGKILL'); - if (!silent) { - console.warn(`[desktop:stop-cli] Forced termination sent to pid ${pid}.`); - } - } catch (error) { - if (!silent) { - console.error(`[desktop:stop-cli] Unable to terminate pid ${pid}:`, error); - } - } finally { - await removeStateFile(); - } -} - -async function main() { - const [, , command] = process.argv; - if (!command || command === '--help' || command === '-h') { - console.log('Usage: node opencode-cli.mjs '); - process.exit(0); - } - - if (command === 'start') { - await startCli(); - return; - } - if (command === 'stop') { - await stopCli(); - return; - } - if (command === 'status') { - const state = await readState(); - if (state?.pid && isProcessAlive(state.pid)) { - console.log(`OpenCode CLI running (pid ${state.pid}).`); - } else { - console.log('OpenCode CLI not running.'); - } - process.exit(0); - return; - } - - console.error(`Unknown command: ${command}`); - process.exit(1); -} - -if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { - main().catch((error) => { - console.error('[desktop:opencode-cli] Unexpected error:', error); - process.exit(1); - }); -} diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock deleted file mode 100644 index 5863a04d..00000000 --- a/packages/desktop/src-tauri/Cargo.lock +++ /dev/null @@ -1,6688 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_log-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" - -[[package]] -name = "android_logger" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" -dependencies = [ - "android_log-sys", - "env_filter", - "log", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "ashpd" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" -dependencies = [ - "async-fs", - "async-net", - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "raw-window-handle", - "serde", - "serde_repr", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" -dependencies = [ - "once_cell", - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "byte-unit" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" -dependencies = [ - "rust_decimal", - "schemars 1.2.1", - "serde", - "utf8-width", -] - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -dependencies = [ - "serde", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.11.0", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "cc" -version = "1.2.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link 0.2.1", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" -dependencies = [ - "bitflags 2.11.0", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.11.0", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.29.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - -[[package]] -name = "cssparser" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf 0.13.1", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.11.0", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dom_query" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" -dependencies = [ - "bit-set", - "cssparser 0.36.0", - "foldhash 0.2.0", - "html5ever 0.38.0", - "precomputed-hash", - "selectors 0.36.1", - "tendril 0.5.0", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "embed-resource" -version = "3.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 0.9.12+spec-1.1.0", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[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 = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "fern" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" -dependencies = [ - "log", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.11.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever 0.14.1", - "match_token", -] - -[[package]] -name = "html5ever" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" -dependencies = [ - "log", - "markup5ever 0.38.0", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" -dependencies = [ - "byteorder", - "png", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "js-sys" -version = "0.3.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.11.0", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser 0.29.6", - "html5ever 0.29.1", - "indexmap 2.13.0", - "selectors 0.24.0", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading", - "once_cell", -] - -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libredox" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" -dependencies = [ - "bitflags 2.11.0", - "libc", - "plain", - "redox_syscall 0.7.3", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "value-bag", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "mac-notification-sys" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" -dependencies = [ - "cc", - "objc2", - "objc2-foundation", - "time", -] - -[[package]] -name = "markup5ever" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" -dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", - "tendril 0.4.3", -] - -[[package]] -name = "markup5ever" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" -dependencies = [ - "log", - "tendril 0.5.0", - "web_atoms", -] - -[[package]] -name = "match_token" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minisign-verify" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.61.2", -] - -[[package]] -name = "muda" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 2.0.18", - "windows-sys 0.60.2", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.11.0", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - -[[package]] -name = "notify-rust" -version = "4.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" -dependencies = [ - "futures-lite", - "log", - "mac-notification-sys", - "serde", - "tauri-winrt-notification", - "zbus", -] - -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.11.0", - "block2", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.0", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.11.0", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.11.0", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-javascript-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" -dependencies = [ - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-osa-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-app-kit", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-security" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" -dependencies = [ - "bitflags 2.11.0", - "block2", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-javascript-core", - "objc2-security", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "open" -version = "5.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" -dependencies = [ - "dunce", - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "openchamber-desktop" -version = "1.12.0" -dependencies = [ - "anyhow", - "base64 0.22.1", - "log", - "objc2", - "objc2-web-kit", - "reqwest 0.12.28", - "rfd 0.15.4", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-log", - "tauri-plugin-notification", - "tauri-plugin-shell", - "tauri-plugin-updater", - "tokio", - "url", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "os_pipe" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "osakit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" -dependencies = [ - "objc2", - "objc2-foundation", - "objc2-osa-kit", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher 1.0.2", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher 1.0.2", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64 0.22.1", - "indexmap 2.13.0", - "quick-xml 0.38.4", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.8+spec-1.1.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[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 = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.39.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" -dependencies = [ - "memchr", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "reqwest" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rfd" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" -dependencies = [ - "ashpd", - "block2", - "dispatch2", - "js-sys", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "pollster", - "raw-window-handle", - "urlencoding", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rfd" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" -dependencies = [ - "block2", - "dispatch2", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.60.2", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rkyv" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rust_decimal" -version = "1.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.5", - "rkyv", - "serde", - "serde_json", - "wasm-bindgen", -] - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[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 = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[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", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser 0.29.6", - "derive_more 0.99.20", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc 0.2.0", - "smallvec", -] - -[[package]] -name = "selectors" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" -dependencies = [ - "bitflags 2.11.0", - "cssparser 0.36.0", - "derive_more 2.1.1", - "log", - "new_debug_unreachable", - "phf 0.13.1", - "phf_codegen 0.13.1", - "precomputed-hash", - "rustc-hash", - "servo_arc 0.4.3", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] - -[[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-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[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 2.0.117", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.13.0", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "servo_arc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" -dependencies = [ - "nodrop", - "stable_deref_trait", -] - -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shared_child" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" -dependencies = [ - "libc", - "sigchld", - "windows-sys 0.60.2", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "sigchld" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" -dependencies = [ - "libc", - "os_pipe", - "signal-hook", -] - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[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 = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "softbuffer" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" -dependencies = [ - "bytemuck", - "js-sys", - "ndk", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall 0.5.18", - "tracing", - "wasm-bindgen", - "web-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.13.1", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "swift-rs" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.34.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" -dependencies = [ - "bitflags 2.11.0", - "block2", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dispatch2", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "parking_lot", - "raw-window-handle", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tar" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs", - "dunce", - "embed_plist", - "getrandom 0.3.4", - "glob", - "gtk", - "heck 0.5.0", - "http", - "jni", - "libc", - "log", - "mime", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest 0.13.2", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.18", - "tokio", - "tray-icon", - "url", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows", -] - -[[package]] -name = "tauri-build" -version = "2.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "toml 0.9.12+spec-1.1.0", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.117", - "tauri-utils", - "thiserror 2.0.18", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-plugin" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "toml 0.9.12+spec-1.1.0", - "walkdir", -] - -[[package]] -name = "tauri-plugin-dialog" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" -dependencies = [ - "log", - "raw-window-handle", - "rfd 0.16.0", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "tauri-plugin-fs" -version = "2.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" -dependencies = [ - "anyhow", - "dunce", - "glob", - "percent-encoding", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "tauri-utils", - "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", - "url", -] - -[[package]] -name = "tauri-plugin-log" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" -dependencies = [ - "android_logger", - "byte-unit", - "fern", - "log", - "objc2", - "objc2-foundation", - "serde", - "serde_json", - "serde_repr", - "swift-rs", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "tauri-plugin-notification" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" -dependencies = [ - "log", - "notify-rust", - "rand 0.9.2", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "time", - "url", -] - -[[package]] -name = "tauri-plugin-shell" -version = "2.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" -dependencies = [ - "encoding_rs", - "log", - "open", - "os_pipe", - "regex", - "schemars 0.8.22", - "serde", - "serde_json", - "shared_child", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "tauri-plugin-updater" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61" -dependencies = [ - "base64 0.22.1", - "dirs", - "flate2", - "futures-util", - "http", - "infer", - "log", - "minisign-verify", - "osakit", - "percent-encoding", - "reqwest 0.13.2", - "rustls", - "semver", - "serde", - "serde_json", - "tar", - "tauri", - "tauri-plugin", - "tempfile", - "thiserror 2.0.18", - "time", - "tokio", - "url", - "windows-sys 0.60.2", - "zip", -] - -[[package]] -name = "tauri-runtime" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http", - "jni", - "objc2", - "objc2-ui-kit", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webview2-com", - "windows", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2", - "objc2-app-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dunce", - "glob", - "html5ever 0.29.1", - "http", - "infer", - "json-patch", - "kuchikiki", - "log", - "memchr", - "phf 0.11.3", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" -dependencies = [ - "dunce", - "embed-resource", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "tauri-winrt-notification" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" -dependencies = [ - "quick-xml 0.37.5", - "thiserror 2.0.18", - "windows", - "windows-version", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "tendril" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" -dependencies = [ - "new_debug_unreachable", - "utf-8", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.13.0", - "serde_core", - "serde_spanned 1.1.0", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.1.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.13.0", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.13.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.25.8+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" -dependencies = [ - "indexmap 2.13.0", - "toml_datetime 1.1.0+spec-1.1.0", - "toml_parser", - "winnow 1.0.0", -] - -[[package]] -name = "toml_parser" -version = "1.1.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" -dependencies = [ - "winnow 1.0.0", -] - -[[package]] -name = "toml_writer" -version = "1.1.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.11.0", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[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 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tray-icon" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" -dependencies = [ - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 2.0.18", - "windows-sys 0.60.2", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset", - "tempfile", - "windows-sys 0.61.2", -] - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[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 = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.13.0", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "semver", -] - -[[package]] -name = "wayland-backend" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" -dependencies = [ - "cc", - "downcast-rs", - "rustix", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" -dependencies = [ - "bitflags 2.11.0", - "rustix", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" -dependencies = [ - "bitflags 2.11.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" -dependencies = [ - "proc-macro2", - "quick-xml 0.39.2", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" -dependencies = [ - "dlib", - "log", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" -dependencies = [ - "phf 0.13.1", - "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webview2-com" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "webview2-com-sys" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" -dependencies = [ - "thiserror 2.0.18", - "windows", - "windows-core 0.61.2", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[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 = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.13.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.13.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "wry" -version = "0.54.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" -dependencies = [ - "base64 0.22.1", - "block2", - "cookie", - "crossbeam-channel", - "dirs", - "dom_query", - "dpi", - "dunce", - "gdkx11", - "gtk", - "http", - "javascriptcore-rs", - "jni", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zbus" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 0.7.15", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" -dependencies = [ - "serde", - "winnow 0.7.15", - "zvariant", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zip" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" -dependencies = [ - "arbitrary", - "crc32fast", - "indexmap 2.13.0", - "memchr", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zvariant" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" -dependencies = [ - "endi", - "enumflags2", - "serde", - "url", - "winnow 0.7.15", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.117", - "winnow 0.7.15", -] diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml deleted file mode 100644 index 2ba5aa6f..00000000 --- a/packages/desktop/src-tauri/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "openchamber-desktop" -version = "1.12.0" -edition = "2021" -publish = false - -[[bin]] -name = "openchamber-desktop" -path = "src/main.rs" - -[features] -default = [] -devtools = ["tauri/devtools"] - -[dependencies] -anyhow = "1.0.86" -base64 = "0.22.1" -log = "0.4.28" -reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls", "json"] } -serde = { version = "1.0.210", features = ["derive"] } -serde_json = "1.0.143" -tauri = { version = "2.10.3", features = ["macos-private-api"] } -tauri-plugin-dialog = "2.6.0" -tauri-plugin-log = "2.8.0" -tauri-plugin-shell = "2.3.5" -tauri-plugin-notification = "2.3.3" -tauri-plugin-updater = "2.10.0" -tokio = { version = "1.38", features = ["rt-multi-thread", "time", "macros", "sync"] } -url = "2.5" - -[build-dependencies] -tauri-build = { version = "2.5.6", features = [] } - -[profile.release] -lto = "thin" -codegen-units = 1 -strip = true - -[target.'cfg(target_os = "macos")'.dependencies] -objc2 = "0.6" -objc2-web-kit = "0.3" -rfd = "0.15" diff --git a/packages/desktop/src-tauri/Info.plist b/packages/desktop/src-tauri/Info.plist deleted file mode 100644 index 9225c2ca..00000000 --- a/packages/desktop/src-tauri/Info.plist +++ /dev/null @@ -1,27 +0,0 @@ - - - - - NSSupportsAutomaticTermination - - NSSupportsSuddenTermination - - NSAppleEventsUsageDescription - OpenChamber needs to run the OpenCode CLI to provide AI coding assistance. - NSDesktopFolderUsageDescription - OpenChamber needs access to work with your projects. - NSDocumentsFolderUsageDescription - OpenChamber needs access to work with your projects. - NSDownloadsFolderUsageDescription - OpenChamber needs access to work with your projects. - NSMicrophoneUsageDescription - OpenChamber needs microphone access for voice input. - NSSpeechRecognitionUsageDescription - OpenChamber needs speech recognition to transcribe voice input. - NSAppTransportSecurity - - NSAllowsArbitraryLoadsInWebContent - - - - diff --git a/packages/desktop/src-tauri/build.rs b/packages/desktop/src-tauri/build.rs deleted file mode 100644 index 261851f6..00000000 --- a/packages/desktop/src-tauri/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - tauri_build::build(); -} diff --git a/packages/desktop/src-tauri/capabilities/default.json b/packages/desktop/src-tauri/capabilities/default.json deleted file mode 100644 index 9e9a8841..00000000 --- a/packages/desktop/src-tauri/capabilities/default.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Default capabilities for OpenChamber desktop runtime", - "remote": { - "urls": [ - "http://127.0.0.1:*/*", - "http://localhost:*/*", - "http://*", - "http://*/*", - "https://*", - "https://*/*" - ] - }, - "windows": ["main", "main-*"], - "permissions": [ - "core:default", - "core:window:default", - "core:window:allow-close", - "core:window:allow-set-title", - "core:window:allow-set-size", - "core:window:allow-set-position", - "core:window:allow-start-dragging", - "core:webview:default", - "core:webview:allow-webview-close", - "shell:allow-open", - "shell:allow-execute", - "dialog:allow-open", - "dialog:allow-save", - "dialog:allow-message", - "dialog:allow-ask", - "dialog:allow-confirm", - "notification:default", - "notification:allow-is-permission-granted", - "notification:allow-request-permission", - "notification:allow-notify", - "updater:default", - "updater:allow-check", - "updater:allow-download-and-install" - ] -} diff --git a/packages/desktop/src-tauri/entitlements.plist b/packages/desktop/src-tauri/entitlements.plist deleted file mode 100644 index 79a8b23a..00000000 --- a/packages/desktop/src-tauri/entitlements.plist +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.disable-executable-page-protection - - com.apple.security.cs.disable-library-validation - - com.apple.security.device.audio-input - - - diff --git a/packages/desktop/src-tauri/icons/app-icon.png b/packages/desktop/src-tauri/icons/app-icon.png deleted file mode 100644 index c3f9a038f17fe5305f67038915c7b236f002eea4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24006 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 diff --git a/packages/desktop/src-tauri/icons/app-icon.svg b/packages/desktop/src-tauri/icons/app-icon.svg deleted file mode 100644 index f271cda8..00000000 --- a/packages/desktop/src-tauri/icons/app-icon.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/desktop/src-tauri/icons/dev-icon.icns b/packages/desktop/src-tauri/icons/dev-icon.icns deleted file mode 100644 index 04ac8d54096e7a3d66a5d9ffec0d34e7399ace29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173094 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 diff --git a/packages/desktop/src-tauri/icons/dev-icon.png b/packages/desktop/src-tauri/icons/dev-icon.png deleted file mode 100644 index bab0ca21ca11b65f91c81829863ba035857cb782..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66751 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 ?)ZU+`JF{HUMVq3#D6a;` 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 z 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 diff --git a/packages/desktop/src-tauri/icons/icon.icns b/packages/desktop/src-tauri/icons/icon.icns deleted file mode 100644 index b6def08054d43652c4d469fcfa6de7ae9fb6b02e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160844 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 diff --git a/packages/desktop/src-tauri/icons/icon.png b/packages/desktop/src-tauri/icons/icon.png deleted file mode 100644 index b9c1331d951b7825160d3683605a45308cfb9e6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55246 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

  • Wad7s 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�Eb8EGH+ZFiC4wILwDbzGc_0 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 diff --git a/packages/desktop/src-tauri/resources/.gitkeep b/packages/desktop/src-tauri/resources/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/desktop/src-tauri/sidecars/.gitkeep b/packages/desktop/src-tauri/sidecars/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs deleted file mode 100644 index fe9ea696..00000000 --- a/packages/desktop/src-tauri/src/main.rs +++ /dev/null @@ -1,4714 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -mod remote_ssh; - -use anyhow::{anyhow, Result}; -use base64::{engine::general_purpose, Engine as _}; -use remote_ssh::DesktopSshManagerState; -use serde::{Deserialize, Serialize}; -use std::env; -use std::time::{SystemTime, UNIX_EPOCH}; -use std::{ - collections::{HashMap, HashSet}, - fs, - path::{Path, PathBuf}, -}; -use std::{ - net::{TcpListener, UdpSocket}, - process::Command, - sync::{ - atomic::{AtomicU32, AtomicU64, Ordering}, - Mutex, - }, - time::Duration, -}; -use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; - -/// Disable pinch-to-zoom / magnification gestures on macOS to avoid accidental -/// zoom and the continuous gesture event processing overhead. -#[cfg(target_os = "macos")] -fn disable_pinch_zoom(window: &tauri::WebviewWindow) { - let _ = window.with_webview(|webview| unsafe { - use objc2::rc::Retained; - use objc2_web_kit::WKWebView; - let wk_webview: Retained = - Retained::retain(webview.inner().cast()).unwrap(); - wk_webview.setAllowsMagnification(false); - }); -} - -#[cfg(not(target_os = "macos"))] -fn disable_pinch_zoom(_window: &tauri::WebviewWindow) {} - -/// Global counter for generating unique window labels. -static WINDOW_COUNTER: AtomicU64 = AtomicU64::new(1); - -fn next_window_label(app: &tauri::AppHandle) -> String { - loop { - let n = WINDOW_COUNTER.fetch_add(1, Ordering::Relaxed); - let candidate = if n == 1 { - "main".to_string() - } else { - format!("main-{n}") - }; - - if !app.webview_windows().contains_key(&candidate) { - return candidate; - } - } -} - -/// Evaluate a script in all open webview windows. -fn eval_in_all_windows(app: &tauri::AppHandle, script: &str) { - for window in app.webview_windows().values() { - let _ = window.eval(script); - } -} - -/// Evaluate a script in the currently focused window, falling back to any window. -fn eval_in_focused_window(app: &tauri::AppHandle, script: &str) { - let windows = app.webview_windows(); - // Try the focused window first. - for window in windows.values() { - if window.is_focused().unwrap_or(false) { - let _ = window.eval(script); - return; - } - } - // Fallback: try "main", then any window. - if let Some(window) = windows.get("main") { - let _ = window.eval(script); - } else if let Some(window) = windows.values().next() { - let _ = window.eval(script); - } -} - -fn dispatch_menu_action(app: &tauri::AppHandle, action: &str) { - let _ = app.emit("openchamber:menu-action", action); - - let event = serde_json::to_string("openchamber:menu-action") - .unwrap_or_else(|_| "\"openchamber:menu-action\"".into()); - let detail = serde_json::to_string(action).unwrap_or_else(|_| "\"\"".into()); - let script = format!("window.dispatchEvent(new CustomEvent({event}, {{ detail: {detail} }}));"); - eval_in_focused_window(app, &script); -} - -fn dispatch_check_for_updates(app: &tauri::AppHandle) { - let _ = app.emit("openchamber:check-for-updates", ()); - - let event = serde_json::to_string("openchamber:check-for-updates") - .unwrap_or_else(|_| "\"openchamber:check-for-updates\"".into()); - let script = format!("window.dispatchEvent(new Event({event}));"); - eval_in_all_windows(app, &script); -} -use tauri_plugin_shell::{process::CommandChild, process::CommandEvent, ShellExt}; -use tauri_plugin_updater::UpdaterExt; - -#[cfg(target_os = "macos")] -const MENU_ITEM_ABOUT_ID: &str = "menu_about"; -#[cfg(target_os = "macos")] -const MENU_ITEM_CHECK_FOR_UPDATES_ID: &str = "menu_check_for_updates"; -#[cfg(target_os = "macos")] -const MENU_ITEM_NEW_WINDOW_ID: &str = "menu_new_window"; -#[cfg(target_os = "macos")] -const MENU_ITEM_SETTINGS_ID: &str = "menu_settings"; -#[cfg(target_os = "macos")] -const MENU_ITEM_COMMAND_PALETTE_ID: &str = "menu_command_palette"; -#[cfg(target_os = "macos")] -#[cfg(target_os = "macos")] -const MENU_ITEM_NEW_SESSION_ID: &str = "menu_new_session"; -#[cfg(target_os = "macos")] -const MENU_ITEM_WORKTREE_CREATOR_ID: &str = "menu_worktree_creator"; -#[cfg(target_os = "macos")] -const MENU_ITEM_CHANGE_WORKSPACE_ID: &str = "menu_change_workspace"; -#[cfg(target_os = "macos")] -const MENU_ITEM_OPEN_GIT_TAB_ID: &str = "menu_open_git_tab"; -#[cfg(target_os = "macos")] -const MENU_ITEM_OPEN_DIFF_TAB_ID: &str = "menu_open_diff_tab"; -#[cfg(target_os = "macos")] -const MENU_ITEM_OPEN_FILES_TAB_ID: &str = "menu_open_files_tab"; -#[cfg(target_os = "macos")] -const MENU_ITEM_OPEN_TERMINAL_TAB_ID: &str = "menu_open_terminal_tab"; -#[cfg(target_os = "macos")] -const MENU_ITEM_COPY_ID: &str = "menu_copy"; -#[cfg(target_os = "macos")] -const MENU_ITEM_THEME_LIGHT_ID: &str = "menu_theme_light"; -#[cfg(target_os = "macos")] -const MENU_ITEM_THEME_DARK_ID: &str = "menu_theme_dark"; -#[cfg(target_os = "macos")] -const MENU_ITEM_THEME_SYSTEM_ID: &str = "menu_theme_system"; -#[cfg(target_os = "macos")] -const MENU_ITEM_TOGGLE_SIDEBAR_ID: &str = "menu_toggle_sidebar"; -#[cfg(target_os = "macos")] -const MENU_ITEM_TOGGLE_MEMORY_DEBUG_ID: &str = "menu_toggle_memory_debug"; -#[cfg(target_os = "macos")] -const MENU_ITEM_HELP_DIALOG_ID: &str = "menu_help_dialog"; -#[cfg(target_os = "macos")] -const MENU_ITEM_DOWNLOAD_LOGS_ID: &str = "menu_download_logs"; -#[cfg(target_os = "macos")] -const MENU_ITEM_REPORT_BUG_ID: &str = "menu_report_bug"; -#[cfg(target_os = "macos")] -const MENU_ITEM_REQUEST_FEATURE_ID: &str = "menu_request_feature"; -#[cfg(target_os = "macos")] -const MENU_ITEM_JOIN_DISCORD_ID: &str = "menu_join_discord"; -#[cfg(target_os = "macos")] -const MENU_ITEM_CLEAR_CACHE_ID: &str = "menu_clear_cache"; -#[cfg(target_os = "macos")] -const MENU_ITEM_QUIT_ID: &str = "menu_quit"; - -#[cfg(target_os = "macos")] -const GITHUB_BUG_REPORT_URL: &str = - "https://github.com/btriapitsyn/openchamber/issues/new?template=bug_report.yml"; -#[cfg(target_os = "macos")] -const GITHUB_FEATURE_REQUEST_URL: &str = - "https://github.com/btriapitsyn/openchamber/issues/new?template=feature_request.yml"; -#[cfg(target_os = "macos")] -const DISCORD_INVITE_URL: &str = "https://discord.gg/ZYRSdnwwKA"; - -static QUIT_CONFIRMED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); -static QUIT_CONFIRMATION_PENDING: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); -static QUIT_RISK_HAS_ENABLED_SCHEDULED_TASKS: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); -static QUIT_RISK_HAS_RUNNING_SCHEDULED_TASKS: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); -static QUIT_RISK_ENABLED_SCHEDULED_TASKS_COUNT: AtomicU32 = AtomicU32::new(0); -static QUIT_RISK_RUNNING_SCHEDULED_TASKS_COUNT: AtomicU32 = AtomicU32::new(0); -static QUIT_RISK_HAS_ACTIVE_TUNNEL: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); -static QUIT_RISK_POLLER_STARTED: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); - -#[cfg(target_os = "macos")] -const QUIT_RISK_POLL_INTERVAL: Duration = Duration::from_secs(5); - -#[cfg(target_os = "macos")] -fn should_require_quit_confirmation() -> bool { - use std::sync::atomic::Ordering; - - QUIT_RISK_HAS_ACTIVE_TUNNEL.load(Ordering::Relaxed) - || QUIT_RISK_HAS_RUNNING_SCHEDULED_TASKS.load(Ordering::Relaxed) - || QUIT_RISK_HAS_ENABLED_SCHEDULED_TASKS.load(Ordering::Relaxed) -} - -#[cfg(target_os = "macos")] -fn quit_confirmation_message() -> String { - use std::sync::atomic::Ordering; - - let has_active_tunnel = QUIT_RISK_HAS_ACTIVE_TUNNEL.load(Ordering::Relaxed); - let running_tasks_count = QUIT_RISK_RUNNING_SCHEDULED_TASKS_COUNT.load(Ordering::Relaxed); - let enabled_tasks_count = QUIT_RISK_ENABLED_SCHEDULED_TASKS_COUNT.load(Ordering::Relaxed); - - let mut reasons: Vec = Vec::new(); - if has_active_tunnel { - reasons.push("an active tunnel".to_string()); - } - if running_tasks_count > 0 { - reasons.push(format!( - "{} running scheduled task{}", - running_tasks_count, - if running_tasks_count == 1 { "" } else { "s" } - )); - } - if enabled_tasks_count > 0 { - reasons.push(format!( - "{} enabled scheduled task{}", - enabled_tasks_count, - if enabled_tasks_count == 1 { "" } else { "s" } - )); - } - - if reasons.is_empty() { - "Background processes (sidecar, SSH sessions) will be stopped.".to_string() - } else { - format!( - "OpenChamber detected {}. Quitting now will stop sidecar/background processes and may interrupt pending work.", - reasons.join(", ") - ) - } -} - -#[cfg(target_os = "macos")] -const NS_TERMINATE_CANCEL: isize = 0; -#[cfg(target_os = "macos")] -const NS_TERMINATE_NOW: isize = 1; - -#[cfg(target_os = "macos")] -unsafe extern "C-unwind" fn application_should_terminate_with_confirmation( - _: &objc2::runtime::AnyObject, - _: objc2::runtime::Sel, - _: *mut std::ffi::c_void, -) -> isize { - use std::sync::atomic::Ordering; - - if QUIT_CONFIRMED.load(Ordering::SeqCst) { - return NS_TERMINATE_NOW; - } - - if !should_require_quit_confirmation() { - QUIT_CONFIRMED.store(true, Ordering::SeqCst); - return NS_TERMINATE_NOW; - } - - if QUIT_CONFIRMATION_PENDING.swap(true, Ordering::SeqCst) { - return NS_TERMINATE_CANCEL; - } - - let message = quit_confirmation_message(); - let confirmed = matches!( - rfd::MessageDialog::new() - .set_title("Quit OpenChamber?") - .set_description(&message) - .set_level(rfd::MessageLevel::Warning) - .set_buttons(rfd::MessageButtons::OkCancel) - .show(), - rfd::MessageDialogResult::Ok | rfd::MessageDialogResult::Yes - ); - - QUIT_CONFIRMATION_PENDING.store(false, Ordering::SeqCst); - - if confirmed { - QUIT_CONFIRMED.store(true, Ordering::SeqCst); - NS_TERMINATE_NOW - } else { - NS_TERMINATE_CANCEL - } -} - -#[cfg(target_os = "macos")] -fn install_macos_quit_confirmation_hook() { - use objc2::ffi; - use objc2::runtime::{AnyClass, AnyObject, Imp, Sel}; - use std::ffi::CStr; - - unsafe { - let Some(delegate_class) = AnyClass::get(CStr::from_bytes_with_nul_unchecked( - b"TaoAppDelegateParent\0", - )) else { - log::warn!("[desktop] TaoAppDelegateParent class not found; dock Quit confirmation hook skipped"); - return; - }; - - let selector = Sel::register(c"applicationShouldTerminate:"); - if !ffi::class_getInstanceMethod(delegate_class, selector).is_null() { - return; - } - - let imp: Imp = std::mem::transmute( - application_should_terminate_with_confirmation - as unsafe extern "C-unwind" fn(&AnyObject, Sel, *mut std::ffi::c_void) -> isize, - ); - - let added = ffi::class_addMethod( - delegate_class as *const _ as *mut _, - selector, - imp, - b"q@:@\0".as_ptr().cast(), - ); - - if !added.as_bool() { - log::warn!("[desktop] failed to install applicationShouldTerminate hook"); - } - } -} - -#[cfg(not(target_os = "macos"))] -fn install_macos_quit_confirmation_hook() {} - -#[cfg(target_os = "macos")] -fn request_quit_with_confirmation(app: &tauri::AppHandle) { - use std::sync::atomic::Ordering; - use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; - - if !should_require_quit_confirmation() { - QUIT_CONFIRMED.store(true, Ordering::SeqCst); - app.exit(0); - return; - } - - if QUIT_CONFIRMATION_PENDING.swap(true, Ordering::SeqCst) { - return; - } - - // When app has only hidden windows (common after closing last window), - // ensure at least one window is visible so native dialog reliably appears. - let windows = app.webview_windows(); - let has_visible = windows.values().any(|w| w.is_visible().unwrap_or(false)); - if !has_visible { - if let Some(hidden) = windows.values().find(|w| !w.is_visible().unwrap_or(true)) { - let _ = hidden.show(); - let _ = hidden.set_focus(); - } - } - - let message = quit_confirmation_message(); - let handle = app.clone(); - app.dialog() - .message(message) - .title("Quit OpenChamber?") - .buttons(MessageDialogButtons::OkCancel) - .kind(tauri_plugin_dialog::MessageDialogKind::Warning) - .show(move |confirmed| { - QUIT_CONFIRMATION_PENDING.store(false, Ordering::SeqCst); - if confirmed { - QUIT_CONFIRMED.store(true, Ordering::SeqCst); - handle.exit(0); - } - }); -} - -#[cfg(target_os = "macos")] -fn build_macos_menu( - app: &tauri::AppHandle, -) -> tauri::Result> { - use tauri::menu::{ - Menu, MenuItem, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID, - }; - - let pkg_info = app.package_info(); - - let new_session_shortcut = "Cmd+N"; - let new_worktree_shortcut = "Cmd+Shift+N"; - - let about = MenuItem::with_id( - app, - MENU_ITEM_ABOUT_ID, - format!("About {}", pkg_info.name), - true, - None::<&str>, - )?; - - let check_for_updates = MenuItem::with_id( - app, - MENU_ITEM_CHECK_FOR_UPDATES_ID, - "Check for Updates", - true, - None::<&str>, - )?; - - let settings = MenuItem::with_id(app, MENU_ITEM_SETTINGS_ID, "Settings", true, Some("Cmd+,"))?; - - let command_palette = MenuItem::with_id( - app, - MENU_ITEM_COMMAND_PALETTE_ID, - "Command Palette", - true, - Some("Cmd+P"), - )?; - - let new_window = MenuItem::with_id( - app, - MENU_ITEM_NEW_WINDOW_ID, - "New Window", - true, - Some("Cmd+Shift+Alt+N"), - )?; - - let new_session = MenuItem::with_id( - app, - MENU_ITEM_NEW_SESSION_ID, - "New Session", - true, - Some(new_session_shortcut), - )?; - - let worktree_creator = MenuItem::with_id( - app, - MENU_ITEM_WORKTREE_CREATOR_ID, - "New Worktree", - true, - Some(new_worktree_shortcut), - )?; - - let change_workspace = MenuItem::with_id( - app, - MENU_ITEM_CHANGE_WORKSPACE_ID, - "Add Workspace", - true, - None::<&str>, - )?; - - let open_git_tab = - MenuItem::with_id(app, MENU_ITEM_OPEN_GIT_TAB_ID, "Git", true, Some("Cmd+G"))?; - let open_diff_tab = - MenuItem::with_id(app, MENU_ITEM_OPEN_DIFF_TAB_ID, "Diff", true, Some("Cmd+E"))?; - let open_files_tab = MenuItem::with_id( - app, - MENU_ITEM_OPEN_FILES_TAB_ID, - "Files", - true, - None::<&str>, - )?; - let open_terminal_tab = MenuItem::with_id( - app, - MENU_ITEM_OPEN_TERMINAL_TAB_ID, - "Terminal", - true, - Some("Cmd+T"), - )?; - let copy = MenuItem::with_id(app, MENU_ITEM_COPY_ID, "Copy", true, Some("Cmd+C"))?; - - let theme_light = MenuItem::with_id( - app, - MENU_ITEM_THEME_LIGHT_ID, - "Light Theme", - true, - None::<&str>, - )?; - let theme_dark = MenuItem::with_id( - app, - MENU_ITEM_THEME_DARK_ID, - "Dark Theme", - true, - None::<&str>, - )?; - let theme_system = MenuItem::with_id( - app, - MENU_ITEM_THEME_SYSTEM_ID, - "System Theme", - true, - None::<&str>, - )?; - - let toggle_sidebar = MenuItem::with_id( - app, - MENU_ITEM_TOGGLE_SIDEBAR_ID, - "Toggle Session Sidebar", - true, - Some("Cmd+L"), - )?; - - let toggle_memory_debug = MenuItem::with_id( - app, - MENU_ITEM_TOGGLE_MEMORY_DEBUG_ID, - "Toggle Memory Debug", - true, - Some("CmdOrCtrl+Shift+D"), - )?; - - let help_dialog = MenuItem::with_id( - app, - MENU_ITEM_HELP_DIALOG_ID, - "Keyboard Shortcuts", - true, - Some("Cmd+."), - )?; - - let download_logs = MenuItem::with_id( - app, - MENU_ITEM_DOWNLOAD_LOGS_ID, - "Show Diagnostics", - true, - Some("Cmd+Shift+L"), - )?; - - let report_bug = MenuItem::with_id( - app, - MENU_ITEM_REPORT_BUG_ID, - "Report a Bug", - true, - None::<&str>, - )?; - let request_feature = MenuItem::with_id( - app, - MENU_ITEM_REQUEST_FEATURE_ID, - "Request a Feature", - true, - None::<&str>, - )?; - let join_discord = MenuItem::with_id( - app, - MENU_ITEM_JOIN_DISCORD_ID, - "Join Discord", - true, - None::<&str>, - )?; - - let clear_cache = MenuItem::with_id( - app, - MENU_ITEM_CLEAR_CACHE_ID, - "Clear Cache", - true, - None::<&str>, - )?; - - let theme_submenu = Submenu::with_items( - app, - "Theme", - true, - &[&theme_light, &theme_dark, &theme_system], - )?; - - let window_menu = Submenu::with_id_and_items( - app, - WINDOW_SUBMENU_ID, - "Window", - true, - &[ - &PredefinedMenuItem::minimize(app, None)?, - &PredefinedMenuItem::maximize(app, None)?, - &PredefinedMenuItem::separator(app)?, - &PredefinedMenuItem::close_window(app, None)?, - ], - )?; - - let help_menu = Submenu::with_id_and_items( - app, - HELP_SUBMENU_ID, - "Help", - true, - &[ - &help_dialog, - &download_logs, - &PredefinedMenuItem::separator(app)?, - &clear_cache, - &PredefinedMenuItem::separator(app)?, - &report_bug, - &request_feature, - &PredefinedMenuItem::separator(app)?, - &join_discord, - ], - )?; - - Menu::with_items( - app, - &[ - &Submenu::with_items( - app, - pkg_info.name.clone(), - true, - &[ - &about, - &check_for_updates, - &PredefinedMenuItem::separator(app)?, - &settings, - &command_palette, - &PredefinedMenuItem::separator(app)?, - &PredefinedMenuItem::services(app, None)?, - &PredefinedMenuItem::separator(app)?, - &PredefinedMenuItem::hide(app, None)?, - &PredefinedMenuItem::hide_others(app, None)?, - &PredefinedMenuItem::separator(app)?, - &MenuItem::with_id( - app, - MENU_ITEM_QUIT_ID, - format!("Quit {}", pkg_info.name), - true, - Some("Cmd+Q"), - )?, - ], - )?, - &Submenu::with_items( - app, - "File", - true, - &[ - &new_window, - &PredefinedMenuItem::separator(app)?, - &new_session, - &worktree_creator, - &PredefinedMenuItem::separator(app)?, - &change_workspace, - &PredefinedMenuItem::separator(app)?, - &PredefinedMenuItem::close_window(app, None)?, - ], - )?, - &Submenu::with_items( - app, - "Edit", - true, - &[ - &PredefinedMenuItem::undo(app, None)?, - &PredefinedMenuItem::redo(app, None)?, - &PredefinedMenuItem::separator(app)?, - &PredefinedMenuItem::cut(app, None)?, - ©, - &PredefinedMenuItem::paste(app, None)?, - &PredefinedMenuItem::select_all(app, None)?, - ], - )?, - &Submenu::with_items( - app, - "View", - true, - &[ - &open_git_tab, - &open_diff_tab, - &open_files_tab, - &open_terminal_tab, - &PredefinedMenuItem::separator(app)?, - &theme_submenu, - &PredefinedMenuItem::separator(app)?, - &toggle_sidebar, - &toggle_memory_debug, - &PredefinedMenuItem::separator(app)?, - &PredefinedMenuItem::fullscreen(app, None)?, - ], - )?, - &window_menu, - &help_menu, - ], - ) -} - -#[tauri::command] -fn desktop_clear_cache(app: tauri::AppHandle) -> Result<(), String> { - #[cfg(target_os = "macos")] - { - let mut failures: Vec = Vec::new(); - - for (label, window) in app.webview_windows() { - if let Err(err) = window.clear_all_browsing_data() { - failures.push(format!("{label}: {err}")); - } - } - - if !failures.is_empty() { - return Err(format!( - "Failed to clear browsing data for some windows: {}", - failures.join("; ") - )); - } - - // Reload all windows after clearing persisted browsing data so in-memory state is reset too. - eval_in_all_windows(&app, "window.location.reload();"); - - log::info!("[desktop] Cleared all webview browsing data and reloaded windows"); - return Ok(()); - } - - #[cfg(not(target_os = "macos"))] - { - Err("desktop_clear_cache is only supported on macOS".to_string()) - } -} - -#[tauri::command] -fn desktop_open_path(path: String, app: Option) -> Result<(), String> { - let trimmed = path.trim(); - if trimmed.is_empty() { - return Err("Path is required".to_string()); - } - - #[cfg(target_os = "macos")] - { - let mut command = Command::new("open"); - if let Some(app_name) = app - .as_ref() - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - { - command.arg("-a").arg(app_name); - } - command.arg(trimmed); - command.spawn().map_err(|err| err.to_string())?; - return Ok(()); - } - - #[cfg(not(target_os = "macos"))] - { - Err("desktop_open_path is only supported on macOS".to_string()) - } -} - -#[cfg(target_os = "macos")] -#[derive(Clone)] -struct OpenCommandSpec { - program: &'static str, - args: Vec, -} - -#[cfg(target_os = "macos")] -fn run_open_command_chain(specs: &[OpenCommandSpec]) -> Result<(), String> { - let mut failures: Vec = Vec::new(); - - for spec in specs { - match Command::new(spec.program).args(&spec.args).status() { - Ok(status) if status.success() => return Ok(()), - Ok(status) => failures.push(format!( - "{} {} exited with status {}", - spec.program, - spec.args.join(" "), - status - )), - Err(error) => failures.push(format!( - "{} {} failed: {}", - spec.program, - spec.args.join(" "), - error - )), - } - } - - if failures.is_empty() { - return Err("No launch strategies available".to_string()); - } - - Err(failures.join("; ")) -} - -#[cfg(target_os = "macos")] -fn is_jetbrains_app_id(app_id: &str) -> bool { - matches!( - app_id, - "pycharm" - | "intellij" - | "webstorm" - | "phpstorm" - | "rider" - | "rustrover" - | "android-studio" - ) -} - -#[cfg(target_os = "macos")] -fn cli_for_app_id(app_id: &str) -> Option<&'static str> { - match app_id { - "vscode" => Some("code"), - "cursor" => Some("cursor"), - "vscodium" => Some("codium"), - "windsurf" => Some("windsurf"), - "zed" => Some("zed"), - _ => None, - } -} - -#[tauri::command] -fn desktop_open_in_app( - project_path: String, - app_id: String, - app_name: String, - file_path: Option, -) -> Result<(), String> { - let trimmed_project_path = project_path.trim(); - if trimmed_project_path.is_empty() { - return Err("Project path is required".to_string()); - } - - let trimmed_app_id = app_id.trim().to_lowercase(); - if trimmed_app_id.is_empty() { - return Err("App id is required".to_string()); - } - - let trimmed_app_name = app_name.trim(); - if trimmed_app_name.is_empty() { - return Err("App name is required".to_string()); - } - - let normalized_file_path = file_path - .as_ref() - .map(|value| value.trim()) - .filter(|value| !value.is_empty()); - - #[cfg(target_os = "macos")] - { - let project = trimmed_project_path.to_string(); - let app_name_owned = trimmed_app_name.to_string(); - let file = normalized_file_path.map(|value| value.to_string()); - let mut specs: Vec = Vec::new(); - - if trimmed_app_id == "finder" { - specs.push(OpenCommandSpec { - program: "open", - args: vec![project.clone()], - }); - return run_open_command_chain(&specs); - } - - if matches!(trimmed_app_id.as_str(), "terminal" | "iterm2" | "ghostty") { - specs.push(OpenCommandSpec { - program: "open", - args: vec!["-a".to_string(), app_name_owned.clone(), project.clone()], - }); - return run_open_command_chain(&specs); - } - - if let Some(cli) = cli_for_app_id(trimmed_app_id.as_str()) { - let mut cli_args = vec!["-n".to_string(), project.clone()]; - if let Some(file_path) = file.as_ref() { - cli_args.push("-g".to_string()); - cli_args.push(file_path.clone()); - } - specs.push(OpenCommandSpec { - program: cli, - args: cli_args, - }); - } - - if is_jetbrains_app_id(trimmed_app_id.as_str()) { - let mut args = vec![ - "-na".to_string(), - app_name_owned.clone(), - "--args".to_string(), - project.clone(), - ]; - if let Some(file_path) = file.as_ref() { - args.push(file_path.clone()); - } - specs.push(OpenCommandSpec { - program: "open", - args, - }); - } - - if let Some(file_path) = file.as_ref() { - specs.push(OpenCommandSpec { - program: "open", - args: vec![ - "-na".to_string(), - app_name_owned.clone(), - "--args".to_string(), - project.clone(), - file_path.clone(), - ], - }); - } - - specs.push(OpenCommandSpec { - program: "open", - args: vec!["-a".to_string(), app_name_owned.clone(), project.clone()], - }); - - if let Some(file_path) = file { - specs.push(OpenCommandSpec { - program: "open", - args: vec!["-a".to_string(), app_name_owned, file_path], - }); - } - - return run_open_command_chain(&specs); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = normalized_file_path; - Err("desktop_open_in_app is only supported on macOS".to_string()) - } -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -struct InstalledAppInfo { - name: String, - icon_data_url: Option, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct InstalledAppsCache { - updated_at: u64, - apps: Vec, -} - -const INSTALLED_APPS_CACHE_TTL_SECS: u64 = 60 * 60 * 24; -const INSTALLED_APPS_CACHE_FILE: &str = "discovered-apps.json"; - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct InstalledAppsResponse { - apps: Vec, - has_cache: bool, - is_cache_stale: bool, -} - -#[tauri::command] -fn desktop_filter_installed_apps(apps: Vec) -> Result, String> { - #[cfg(target_os = "macos")] - { - let mut installed: Vec = Vec::new(); - - for raw in apps { - let trimmed = raw.trim(); - if trimmed.is_empty() { - continue; - } - - let bundle_name = if trimmed.ends_with(".app") { - trimmed.to_string() - } else { - format!("{trimmed}.app") - }; - - if is_app_bundle_installed(&bundle_name) { - installed.push(trimmed.to_string()); - } - } - - return Ok(installed); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = apps; - Err("desktop_filter_installed_apps is only supported on macOS".to_string()) - } -} - -#[tauri::command] -fn desktop_get_installed_apps( - app: tauri::AppHandle, - apps: Vec, - force: Option, -) -> Result { - #[cfg(target_os = "macos")] - { - let cache_path = installed_apps_cache_path(); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|err| err.to_string())? - .as_secs(); - - let cache = read_installed_apps_cache(&cache_path); - let cached_apps = cache - .as_ref() - .map(|entry| entry.apps.clone()) - .unwrap_or_default(); - let has_cache = cache.is_some(); - let is_cache_stale = cache - .as_ref() - .map(|entry| now.saturating_sub(entry.updated_at) > INSTALLED_APPS_CACHE_TTL_SECS) - .unwrap_or(false); - - if has_cache { - if is_cache_stale { - log::info!("[open-in] cache hit (stale): {} apps", cached_apps.len()); - } else { - log::info!("[open-in] cache hit (fresh): {} apps", cached_apps.len()); - } - if log::log_enabled!(log::Level::Info) { - let names: Vec = cached_apps.iter().map(|app| app.name.clone()).collect(); - log::info!("[open-in] cache apps: {:?}", names); - } - } - - if !has_cache { - log::info!("[open-in] cache missing: refreshing app list"); - let app_handle = app.clone(); - let app_names = apps.clone(); - let force_icon_refresh = false; - let cached_icon_map: HashMap = HashMap::new(); - tauri::async_runtime::spawn_blocking(move || { - log::info!("[open-in] scan start: {} candidates", app_names.len()); - let refreshed = - build_installed_apps(&app_names, &cached_icon_map, force_icon_refresh); - if log::log_enabled!(log::Level::Info) { - let names: Vec = - refreshed.iter().map(|entry| entry.name.clone()).collect(); - log::info!("[open-in] scan apps: {:?}", names); - } - log::info!("[open-in] scan done: {} installed", refreshed.len()); - let cache_entry = InstalledAppsCache { - updated_at: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|value| value.as_secs()) - .unwrap_or(0), - apps: refreshed.clone(), - }; - let cache_path = installed_apps_cache_path(); - let _ = write_installed_apps_cache(&cache_path, &cache_entry); - dispatch_installed_apps_update(&app_handle, &refreshed); - }); - } else if force.unwrap_or(false) { - log::info!("[open-in] manual refresh: refreshing app list"); - let app_handle = app.clone(); - let app_names = apps.clone(); - let force_icon_refresh = true; - let cached_icon_map: HashMap = HashMap::new(); - tauri::async_runtime::spawn_blocking(move || { - log::info!("[open-in] scan start: {} candidates", app_names.len()); - let refreshed = - build_installed_apps(&app_names, &cached_icon_map, force_icon_refresh); - if log::log_enabled!(log::Level::Info) { - let names: Vec = - refreshed.iter().map(|entry| entry.name.clone()).collect(); - log::info!("[open-in] scan apps: {:?}", names); - } - log::info!("[open-in] scan done: {} installed", refreshed.len()); - let cache_entry = InstalledAppsCache { - updated_at: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|value| value.as_secs()) - .unwrap_or(0), - apps: refreshed.clone(), - }; - let cache_path = installed_apps_cache_path(); - let _ = write_installed_apps_cache(&cache_path, &cache_entry); - dispatch_installed_apps_update(&app_handle, &refreshed); - }); - } - - return Ok(InstalledAppsResponse { - apps: cached_apps, - has_cache, - is_cache_stale, - }); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = apps; - Err("desktop_get_installed_apps is only supported on macOS".to_string()) - } -} - -#[derive(Serialize)] -struct AppIconPayload { - app: String, - data_url: String, -} - -#[tauri::command] -fn desktop_fetch_app_icons(apps: Vec) -> Result, String> { - #[cfg(target_os = "macos")] - { - let mut results: Vec = Vec::new(); - - for raw in apps { - let trimmed = raw.trim(); - if trimmed.is_empty() { - continue; - } - - let Some(app_path) = resolve_app_bundle_path(trimmed) else { - continue; - }; - - let Some(icon_path) = resolve_app_icon_path(&app_path) else { - continue; - }; - - let Some(data_url) = icon_to_data_url(&icon_path, trimmed) else { - continue; - }; - - results.push(AppIconPayload { - app: trimmed.to_string(), - data_url, - }); - } - - return Ok(results); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = apps; - Err("desktop_fetch_app_icons is only supported on macOS".to_string()) - } -} - -#[cfg(target_os = "macos")] -fn resolve_app_bundle_path(app_name: &str) -> Option { - if app_name.trim().is_empty() { - return None; - } - - let bundle_name = if app_name.ends_with(".app") { - app_name.to_string() - } else { - format!("{app_name}.app") - }; - - let candidates = [ - format!("/Applications/{bundle_name}"), - format!("/System/Applications/{bundle_name}"), - format!("/System/Applications/Utilities/{bundle_name}"), - ]; - - for candidate in candidates { - let path = PathBuf::from(&candidate); - if path.exists() { - return Some(path); - } - } - - if let Some(home) = env::var_os("HOME") { - let user_app_path = PathBuf::from(home).join("Applications").join(&bundle_name); - if user_app_path.exists() { - return Some(user_app_path); - } - } - - if let Ok(output) = Command::new("mdfind") - .args(["-name", &bundle_name]) - .output() - { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - for line in stdout.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - let path = PathBuf::from(trimmed); - if path.exists() { - return Some(path); - } - } - } - } - - None -} - -#[cfg(target_os = "macos")] -fn installed_apps_cache_path() -> PathBuf { - let home = env::var_os("HOME") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/")); - home.join(".config") - .join("openchamber") - .join(INSTALLED_APPS_CACHE_FILE) -} - -#[cfg(target_os = "macos")] -fn read_installed_apps_cache(path: &Path) -> Option { - let bytes = fs::read(path).ok()?; - serde_json::from_slice(&bytes).ok() -} - -#[cfg(target_os = "macos")] -fn write_installed_apps_cache(path: &Path, cache: &InstalledAppsCache) -> Result<(), String> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(|err| err.to_string())?; - } - let payload = serde_json::to_vec(cache).map_err(|err| err.to_string())?; - fs::write(path, payload).map_err(|err| err.to_string()) -} - -#[cfg(target_os = "macos")] -fn build_installed_apps( - apps: &[String], - cached_icon_map: &HashMap, - force_icon_refresh: bool, -) -> Vec { - let mut seen = HashSet::new(); - let mut results = Vec::new(); - - for raw in apps { - let trimmed = raw.trim(); - if trimmed.is_empty() || !seen.insert(trimmed.to_string()) { - continue; - } - - if let Some(app_path) = resolve_app_bundle_path(trimmed) { - let icon_data_url = if force_icon_refresh { - resolve_app_icon_path(&app_path).and_then(|icon| icon_to_data_url(&icon, trimmed)) - } else { - cached_icon_map.get(trimmed).cloned().or_else(|| { - resolve_app_icon_path(&app_path) - .and_then(|icon| icon_to_data_url(&icon, trimmed)) - }) - }; - results.push(InstalledAppInfo { - name: trimmed.to_string(), - icon_data_url, - }); - } - } - - results -} - -#[cfg(target_os = "macos")] -fn dispatch_installed_apps_update(app: &tauri::AppHandle, apps: &[InstalledAppInfo]) { - let event = serde_json::to_string("openchamber:installed-apps-updated") - .unwrap_or_else(|_| "\"openchamber:installed-apps-updated\"".into()); - let detail = serde_json::to_string(apps).unwrap_or_else(|_| "[]".into()); - let script = format!("window.dispatchEvent(new CustomEvent({event}, {{ detail: {detail} }}));"); - eval_in_all_windows(app, &script); -} - -#[cfg(target_os = "macos")] -fn resolve_app_icon_path(app_path: &Path) -> Option { - if !app_path.exists() { - return None; - } - - if let Some(icon_file) = read_bundle_icon_file(app_path) { - let icon_path = app_path.join("Contents").join("Resources").join(&icon_file); - if icon_path.exists() { - return Some(icon_path); - } - } - - if let Ok(output) = Command::new("mdls") - .args([ - "-name", - "kMDItemIconFile", - "-raw", - &app_path.to_string_lossy(), - ]) - .output() - { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let icon_name = stdout.trim(); - if !icon_name.is_empty() && icon_name != "(null)" { - let icon_file = if icon_name.ends_with(".icns") { - icon_name.to_string() - } else { - format!("{icon_name}.icns") - }; - let icon_path = app_path.join("Contents").join("Resources").join(icon_file); - if icon_path.exists() { - return Some(icon_path); - } - } - } - } - - let resources_path = app_path.join("Contents").join("Resources"); - if let Ok(entries) = fs::read_dir(resources_path) { - for entry in entries.flatten() { - let path = entry.path(); - if let Some(ext) = path.extension().and_then(|value| value.to_str()) { - if ext.eq_ignore_ascii_case("icns") { - return Some(path); - } - } - } - } - - None -} - -#[cfg(target_os = "macos")] -fn read_bundle_icon_file(app_path: &Path) -> Option { - let plist_path = app_path.join("Contents").join("Info.plist"); - if !plist_path.exists() { - return None; - } - - let output = Command::new("defaults") - .args(["read", &plist_path.to_string_lossy(), "CFBundleIconFile"]) - .output() - .ok()?; - - if !output.status.success() { - return None; - } - - let stdout = String::from_utf8_lossy(&output.stdout); - let icon_name = stdout.trim(); - if icon_name.is_empty() { - return None; - } - - let icon_file = if icon_name.ends_with(".icns") { - icon_name.to_string() - } else { - format!("{icon_name}.icns") - }; - - Some(icon_file) -} - -#[cfg(target_os = "macos")] -fn icon_to_data_url(icon_path: &Path, app_name: &str) -> Option { - if !icon_path.exists() { - return None; - } - - let sanitized: String = app_name - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect(); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|value| value.as_millis()) - .unwrap_or(0); - let tmp_path = env::temp_dir().join(format!("openchamber-icon-{sanitized}-{timestamp}.png")); - - let status = Command::new("sips") - .args([ - "-s", - "format", - "png", - "-Z", - "32", - &icon_path.to_string_lossy(), - "--out", - &tmp_path.to_string_lossy(), - ]) - .status() - .ok()?; - - if !status.success() { - return None; - } - - let bytes = fs::read(&tmp_path).ok()?; - let _ = fs::remove_file(&tmp_path); - if bytes.is_empty() { - return None; - } - - let encoded = general_purpose::STANDARD.encode(bytes); - Some(format!("data:image/png;base64,{encoded}")) -} - -#[cfg(target_os = "macos")] -fn is_app_bundle_installed(bundle_name: &str) -> bool { - if bundle_name.trim().is_empty() { - return false; - } - - if let Ok(output) = Command::new("mdfind").args(["-name", bundle_name]).output() { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - if !stdout.trim().is_empty() { - return true; - } - } - } - - let app_path = format!("/Applications/{bundle_name}"); - let system_app_path = format!("/System/Applications/{bundle_name}"); - let utilities_path = format!("/System/Applications/Utilities/{bundle_name}"); - - if Path::new(&app_path).exists() - || Path::new(&system_app_path).exists() - || Path::new(&utilities_path).exists() - { - return true; - } - - if let Some(home) = env::var_os("HOME") { - let user_app_path = PathBuf::from(home).join("Applications").join(bundle_name); - if user_app_path.exists() { - return true; - } - } - - false -} - -const SIDECAR_NAME: &str = "openchamber-server"; -const SIDECAR_NOTIFY_PREFIX: &str = "[OpenChamberDesktopNotify] "; -const HEALTH_TIMEOUT: Duration = Duration::from_secs(20); -const HEALTH_POLL_INITIAL_INTERVAL: Duration = Duration::from_millis(250); -const HEALTH_POLL_MAX_INTERVAL: Duration = Duration::from_millis(2000); -const LOCAL_SIDECAR_HEALTH_TIMEOUT: Duration = Duration::from_secs(8); -const LOCAL_SIDECAR_HEALTH_POLL_INITIAL_INTERVAL: Duration = Duration::from_millis(100); -const LOCAL_SIDECAR_HEALTH_POLL_MAX_INTERVAL: Duration = Duration::from_millis(1000); -const STARTUP_REMOTE_PROBE_SOFT_TIMEOUT: Duration = Duration::from_secs(2); -const STARTUP_REMOTE_PROBE_HARD_TIMEOUT: Duration = Duration::from_secs(10); - -const DEFAULT_DESKTOP_PORT: u16 = 57123; -const WINDOW_STATE_DEBOUNCE_MS: u64 = 300; -const MIN_WINDOW_WIDTH: u32 = 800; -const MIN_WINDOW_HEIGHT: u32 = 520; -const MIN_RESTORE_WINDOW_WIDTH: u32 = 900; -const MIN_RESTORE_WINDOW_HEIGHT: u32 = 560; - -const LOCAL_HOST_ID: &str = "local"; - -/// Synthetic host ID used when the boot target is forced via -/// `OPENCHAMBER_SERVER_URL` (no config-based host entry). -const ENV_OVERRIDE_HOST_ID: &str = "__env"; - -/// Synthetic host ID used when a window is opened at an explicit URL -/// via `desktop_new_window_at_url` (no config-based host entry). -const DIRECT_URL_HOST_ID: &str = "__direct"; - -/// Compare two URL strings for "same server" identity using normalized -/// origin + path. This avoids misclassification when one URL has a -/// trailing slash and the other does not (e.g. `OPENCHAMBER_SERVER_URL` -/// pointing at the local sidecar without a trailing `/`). -fn same_server_url(a: &str, b: &str) -> bool { - let parsed_a = url::Url::parse(a); - let parsed_b = url::Url::parse(b); - match (parsed_a, parsed_b) { - (Ok(a), Ok(b)) => { - a.origin() == b.origin() - && a.path().trim_end_matches('/') == b.path().trim_end_matches('/') - } - _ => a == b, - } -} - -#[derive(Default)] -struct SidecarState { - child: Mutex>, - url: Mutex>, -} - -/// Holds per-window initialization scripts and a global local origin. -/// Each window gets its own init script (containing the correct boot outcome -/// for that window's target URL), so page reloads re-inject the right data. -#[derive(Default)] -struct DesktopUiInjectionState { - /// Init scripts keyed by window label. Each window's script contains - /// the correct `__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__` for that window. - scripts: Mutex>, - /// Local origin — shared across all windows since the sidecar is global. - local_origin: Mutex>, -} - -/// Tracks the set of currently-focused window labels. -/// Notification suppression triggers when ANY window is focused. -struct WindowFocusState { - focused_windows: Mutex>, -} - -impl Default for WindowFocusState { - fn default() -> Self { - Self { - focused_windows: Mutex::new(HashSet::new()), - } - } -} - -impl WindowFocusState { - fn any_focused(&self) -> bool { - let guard = self.focused_windows.lock().expect("focus mutex"); - !guard.is_empty() - } - - fn set_focused(&self, label: &str, focused: bool) { - let mut guard = self.focused_windows.lock().expect("focus mutex"); - if focused { - guard.insert(label.to_string()); - } else { - guard.remove(label); - } - } - - fn remove_window(&self, label: &str) { - let mut guard = self.focused_windows.lock().expect("focus mutex"); - guard.remove(label); - } -} - -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DesktopHost { - id: String, - label: String, - url: String, -} - -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DesktopHostsConfig { - hosts: Vec, - default_host_id: Option, - #[serde(default)] - initial_host_choice_completed: bool, -} - -/// Input type for `desktop_hosts_set`. Fields may be omitted to preserve -/// existing stored values, ensuring backward-compatible callers don't -/// accidentally reset onboarding state. -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DesktopHostsConfigInput { - hosts: Vec, - default_host_id: Option, - #[serde(default)] - initial_host_choice_completed: Option, -} - -/// Process-wide mutex serializing all read-modify-write operations on the -/// desktop `settings.json`. This prevents concurrent writers (host config, -/// local port, window state, vibrancy, onboarding flag) from clobbering -/// each other's independent fields. -static SETTINGS_FILE_MUTEX: Mutex<()> = Mutex::new(()); - -/// Merge a partial input into an existing config, preserving fields that -/// the caller omitted (`None`). This is the single source of truth for -/// the merge semantics used by `desktop_hosts_set`. -fn merge_desktop_hosts_config( - existing: &DesktopHostsConfig, - input: &DesktopHostsConfigInput, -) -> DesktopHostsConfig { - DesktopHostsConfig { - hosts: input.hosts.clone(), - default_host_id: input.default_host_id.clone(), - initial_host_choice_completed: input - .initial_host_choice_completed - .unwrap_or(existing.initial_host_choice_completed), - } -} - -/// Atomic read-merge-write: reads existing config from `path`, merges -/// `input` into it, and writes the result — all while holding the process -/// lock. Tests and the `desktop_hosts_set` command share this path. -fn write_desktop_hosts_config_input_to_path(path: &Path, input: &DesktopHostsConfigInput) -> Result<()> { - let _guard = SETTINGS_FILE_MUTEX.lock().expect("desktop hosts mutex"); - let existing = read_desktop_hosts_config_from_path(path); - let merged = merge_desktop_hosts_config(&existing, input); - write_desktop_hosts_config_to_path(path, &merged) -} - -#[derive(Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DesktopWindowState { - x: i32, - y: i32, - width: u32, - height: u32, - #[serde(default)] - maximized: bool, - #[serde(default)] - fullscreen: bool, -} - -#[derive(Default)] -struct WindowGeometryDebounceState { - revisions: Mutex>, -} - -fn normalize_host_url(raw: &str) -> Option { - let trimmed = raw.trim(); - if trimmed.is_empty() { - return None; - } - let parsed = url::Url::parse(trimmed).ok()?; - let scheme = parsed.scheme(); - if scheme != "http" && scheme != "https" { - return None; - } - let host = parsed.host_str()?; - let mut normalized = format!("{}://{}", scheme, host); - if let Some(port) = parsed.port() { - normalized.push(':'); - normalized.push_str(&port.to_string()); - } - let path = parsed.path(); - if path.is_empty() { - normalized.push('/'); - } else { - normalized.push_str(path); - } - if let Some(query) = parsed.query() { - normalized.push('?'); - normalized.push_str(query); - } - Some(normalized) -} - -fn sanitize_host_url_for_storage(raw: &str) -> Option { - normalize_host_url(raw) -} - -fn build_health_url(base_url: &str) -> Option { - let normalized = normalize_host_url(base_url)?; - let mut parsed = url::Url::parse(&normalized).ok()?; - let current_path = parsed.path(); - let trimmed_path = current_path.trim_end_matches('/'); - let health_path = if trimmed_path.is_empty() { - "/health".to_string() - } else { - format!("{trimmed_path}/health") - }; - parsed.set_path(&health_path); - Some(parsed.to_string()) -} - -fn settings_file_path() -> PathBuf { - if let Ok(dir) = env::var("OPENCHAMBER_DATA_DIR") { - if !dir.trim().is_empty() { - return PathBuf::from(dir.trim()).join("settings.json"); - } - } - let home = env::var("HOME").unwrap_or_default(); - PathBuf::from(home) - .join(".config") - .join("openchamber") - .join("settings.json") -} - -fn read_desktop_settings_json() -> Option { - fs::read_to_string(settings_file_path()) - .ok() - .and_then(|raw| serde_json::from_str::(&raw).ok()) -} - -fn read_desktop_local_port_from_disk() -> Option { - read_desktop_settings_json() - .as_ref() - .and_then(|v| v.get("desktopLocalPort")) - .and_then(|v| v.as_u64()) - .and_then(|v| { - if v > 0 && v <= u16::MAX as u64 { - Some(v as u16) - } else { - None - } - }) -} - -fn write_desktop_local_port_to_disk(port: u16) -> Result<()> { - let _guard = SETTINGS_FILE_MUTEX.lock().expect("settings file mutex"); - let path = settings_file_path(); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - - let mut root: serde_json::Value = if let Ok(raw) = fs::read_to_string(&path) { - serde_json::from_str(&raw).unwrap_or(serde_json::json!({})) - } else { - serde_json::json!({}) - }; - - if !root.is_object() { - root = serde_json::json!({}); - } - - root["desktopLocalPort"] = serde_json::Value::Number(serde_json::Number::from(port)); - fs::write(&path, serde_json::to_string_pretty(&root)?)?; - Ok(()) -} - -fn read_desktop_hosts_config_from_disk() -> DesktopHostsConfig { - read_desktop_hosts_config_from_path(&settings_file_path()) -} - -fn read_desktop_hosts_config_from_path(path: &Path) -> DesktopHostsConfig { - let raw = fs::read_to_string(path).ok(); - let parsed = raw - .as_deref() - .and_then(|s| serde_json::from_str::(s).ok()); - - let hosts_value = parsed - .as_ref() - .and_then(|v| v.get("desktopHosts")) - .cloned() - .unwrap_or(serde_json::Value::Null); - let default_value = parsed - .as_ref() - .and_then(|v| v.get("desktopDefaultHostId")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let initial_host_choice_completed = parsed - .as_ref() - .and_then(|v| v.get("desktopInitialHostChoiceCompleted")) - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let mut hosts: Vec = Vec::new(); - if let serde_json::Value::Array(items) = hosts_value { - for item in items { - if let Ok(host) = serde_json::from_value::(item) { - if host.id.trim().is_empty() || host.id == LOCAL_HOST_ID { - continue; - } - if let Some(url) = sanitize_host_url_for_storage(&host.url) { - hosts.push(DesktopHost { - id: host.id, - label: if host.label.trim().is_empty() { - url.clone() - } else { - host.label - }, - url, - }); - } - } - } - } - - DesktopHostsConfig { - hosts, - default_host_id: default_value, - initial_host_choice_completed, - } -} - -fn read_desktop_window_state_from_disk() -> Option { - let path = settings_file_path(); - let raw = fs::read_to_string(path).ok(); - let parsed = raw - .as_deref() - .and_then(|s| serde_json::from_str::(s).ok()); - - parsed - .as_ref() - .and_then(|v| v.get("desktopWindowState")) - .cloned() - .and_then(|v| serde_json::from_value::(v).ok()) -} - -fn write_desktop_window_state_to_disk(state: &DesktopWindowState) -> Result<()> { - let _guard = SETTINGS_FILE_MUTEX.lock().expect("settings file mutex"); - let path = settings_file_path(); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - - let mut root: serde_json::Value = if let Ok(raw) = fs::read_to_string(&path) { - serde_json::from_str(&raw).unwrap_or(serde_json::json!({})) - } else { - serde_json::json!({}) - }; - - if !root.is_object() { - root = serde_json::json!({}); - } - - root["desktopWindowState"] = serde_json::to_value(state).unwrap_or(serde_json::Value::Null); - fs::write(&path, serde_json::to_string_pretty(&root)?)?; - Ok(()) -} - -fn write_desktop_hosts_config_to_path(path: &Path, config: &DesktopHostsConfig) -> Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - - let mut root: serde_json::Value = if let Ok(raw) = fs::read_to_string(&path) { - serde_json::from_str(&raw).unwrap_or(serde_json::json!({})) - } else { - serde_json::json!({}) - }; - - if !root.is_object() { - root = serde_json::json!({}); - } - - let hosts: Vec = config - .hosts - .iter() - .filter_map(|h| { - let id = h.id.trim(); - if id.is_empty() || id == LOCAL_HOST_ID { - return None; - } - let url = sanitize_host_url_for_storage(&h.url)?; - Some(DesktopHost { - id: id.to_string(), - label: if h.label.trim().is_empty() { - url.clone() - } else { - h.label.trim().to_string() - }, - url, - }) - }) - .collect(); - - root["desktopHosts"] = serde_json::to_value(hosts).unwrap_or(serde_json::Value::Array(vec![])); - root["desktopDefaultHostId"] = match &config.default_host_id { - Some(id) if !id.trim().is_empty() => serde_json::Value::String(id.trim().to_string()), - _ => serde_json::Value::Null, - }; - root["desktopInitialHostChoiceCompleted"] = - serde_json::Value::Bool(config.initial_host_choice_completed); - - fs::write(&path, serde_json::to_string_pretty(&root)?)?; - Ok(()) -} - -// ── Boot outcome resolution ── - -/// Authoritative desktop boot outcome injected into the webview as -/// `window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__`. -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct DesktopBootOutcome { - target: Option, // "local" | "remote" | null - status: String, // "ok" | "not-configured" | "unreachable" | "wrong-service" | "missing" - #[serde(skip_serializing_if = "Option::is_none")] - host_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - url: Option, -} - -/// Probe status classification for boot resolution. -#[derive(Clone, Copy, PartialEq, Eq)] -enum ProbeClass { - Ok, - Auth, - Unreachable, - WrongService, - NoProbe, -} - -impl ProbeClass { - fn from_probe(probe: Option<&HostProbeResult>) -> Self { - match probe { - Some(p) if p.status == "ok" => ProbeClass::Ok, - Some(p) if p.status == "auth" => ProbeClass::Auth, - Some(p) if p.status == "wrong-service" => ProbeClass::WrongService, - Some(_) => ProbeClass::Unreachable, - None => ProbeClass::NoProbe, - } - } -} - -/// Result of the shared soft+hard probe policy. -struct ProbeWithRetryResult { - /// Whether the target is navigable (ok or auth). - navigable: bool, - /// The final probe result, if available. - probe: Option, -} - -/// Shared probe policy: soft probe first, hard retry on failure. -/// Used by both startup and open_new_window for consistency. -async fn probe_with_retry(url: &str) -> ProbeWithRetryResult { - let soft_probe = - probe_host_with_timeout(url, STARTUP_REMOTE_PROBE_SOFT_TIMEOUT).await; - - let (navigable, final_probe) = match &soft_probe { - Ok(probe) if matches!(probe.status.as_str(), "ok" | "auth") => { - (true, Some(probe.clone())) - } - Ok(_) => { - log::warn!( - "[desktop] host slow/unreachable ({}), retrying with extended timeout", - url - ); - match probe_host_with_timeout(url, STARTUP_REMOTE_PROBE_HARD_TIMEOUT).await { - Ok(hard_probe) if matches!(hard_probe.status.as_str(), "ok" | "auth") => { - (true, Some(hard_probe)) - } - Ok(hard_probe) => (false, Some(hard_probe)), - Err(_) => (false, None), - } - } - Err(_) => { - log::warn!( - "[desktop] host errored ({}), retrying with extended timeout", - url - ); - match probe_host_with_timeout(url, STARTUP_REMOTE_PROBE_HARD_TIMEOUT).await { - Ok(hard_probe) if matches!(hard_probe.status.as_str(), "ok" | "auth") => { - (true, Some(hard_probe)) - } - Ok(hard_probe) => (false, Some(hard_probe)), - Err(_) => (false, None), - } - } - }; - - ProbeWithRetryResult { - navigable, - probe: final_probe, - } -} - -/// Determine the boot outcome from the desktop hosts config, optional probe -/// result, local server availability, and optional env-forced URL. -/// -/// When `env_target_url` is `Some`, it overrides the config-based default -/// host selection. The returned outcome always describes the actual boot -/// target, including env-forced remotes. -/// -/// This is the single source of truth for boot resolution logic. Both the -/// initial startup and `open_new_window` should delegate to this function -/// for consistency. -fn resolve_boot_outcome( - cfg: &DesktopHostsConfig, - probe: Option<&HostProbeResult>, - local_available: bool, - env_target_url: Option<&str>, -) -> DesktopBootOutcome { - let probe_class = ProbeClass::from_probe(probe); - - // Env-forced URL takes precedence over config. This is its own - // authoritative branch — never falls through to config-based resolution. - if let Some(env_url) = env_target_url { - return match probe_class { - ProbeClass::Ok | ProbeClass::Auth | ProbeClass::NoProbe => DesktopBootOutcome { - target: Some("remote".to_string()), - status: "ok".to_string(), - host_id: Some(ENV_OVERRIDE_HOST_ID.to_string()), - url: Some(env_url.to_string()), - }, - ProbeClass::WrongService => DesktopBootOutcome { - target: Some("remote".to_string()), - status: "wrong-service".to_string(), - host_id: Some(ENV_OVERRIDE_HOST_ID.to_string()), - url: Some(env_url.to_string()), - }, - ProbeClass::Unreachable => DesktopBootOutcome { - target: Some("remote".to_string()), - status: "unreachable".to_string(), - host_id: Some(ENV_OVERRIDE_HOST_ID.to_string()), - url: Some(env_url.to_string()), - }, - }; - } - - // No default host configured - let default_id = cfg.default_host_id.as_deref().unwrap_or(""); - if default_id.is_empty() { - // Whether or not choice is completed, no default means not-configured - return DesktopBootOutcome { - target: None, - status: "not-configured".to_string(), - host_id: None, - url: None, - }; - } - - // Default is local - if default_id == LOCAL_HOST_ID { - if local_available { - return DesktopBootOutcome { - target: Some("local".to_string()), - status: "ok".to_string(), - host_id: None, - url: None, - }; - } - return DesktopBootOutcome { - target: Some("local".to_string()), - status: "unreachable".to_string(), - host_id: None, - url: None, - }; - } - - // Default is a remote host — find it - let host = cfg - .hosts - .iter() - .find(|h| h.id == default_id); - - let Some(host) = host else { - return DesktopBootOutcome { - target: Some("remote".to_string()), - status: "missing".to_string(), - host_id: Some(default_id.to_string()), - url: None, - }; - }; - - let host_id = host.id.clone(); - let host_url = host.url.clone(); - - match probe_class { - ProbeClass::Ok | ProbeClass::Auth => DesktopBootOutcome { - target: Some("remote".to_string()), - status: "ok".to_string(), - host_id: Some(host_id), - url: Some(host_url), - }, - ProbeClass::WrongService => DesktopBootOutcome { - target: Some("remote".to_string()), - status: "wrong-service".to_string(), - host_id: Some(host_id), - url: Some(host_url), - }, - ProbeClass::Unreachable => DesktopBootOutcome { - target: Some("remote".to_string()), - status: "unreachable".to_string(), - host_id: Some(host_id), - url: Some(host_url), - }, - ProbeClass::NoProbe => { - // No probe result and choice already completed — treat as recovery - // (the probe hasn't happened yet, but the user has made a choice, - // so this shouldn't normally occur in practice). - DesktopBootOutcome { - target: Some("remote".to_string()), - status: "unreachable".to_string(), - host_id: Some(host_id), - url: Some(host_url), - } - } - } -} - -/// Compute the boot outcome to display when the local server fails to start. -/// -/// This ensures the UI leaves the splash screen and shows an appropriate -/// chooser/recovery state instead of hanging. It delegates to the existing -/// `resolve_boot_outcome` with `local_available = false` and no probe. -fn compute_local_startup_failure_boot_outcome(cfg: &DesktopHostsConfig) -> DesktopBootOutcome { - resolve_boot_outcome(cfg, None, false, None) -} - -/// Build the init script for the startup failure fallback case. -/// -/// Uses an empty `local_origin` since the local server is not running; -/// the UI can fall back to `window.location.origin` when needed. -fn build_startup_failure_init_script(boot_outcome: &DesktopBootOutcome) -> String { - build_init_script("", Some(boot_outcome)) -} - -#[tauri::command] -fn desktop_hosts_get() -> Result { - Ok(read_desktop_hosts_config_from_disk()) -} - -#[tauri::command] -fn desktop_hosts_set(input: DesktopHostsConfigInput) -> Result<(), String> { - write_desktop_hosts_config_input_to_path(&settings_file_path(), &input) - .map_err(|err| err.to_string()) -} - -#[derive(Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct HostProbeResult { - status: String, - latency_ms: u64, -} - -async fn probe_host_with_timeout(url: &str, timeout: Duration) -> Result { - let health = build_health_url(url).ok_or_else(|| "Invalid URL".to_string())?; - let client = reqwest::Client::builder() - .no_proxy() - .timeout(timeout) - .build() - .map_err(|err| err.to_string())?; - let started = std::time::Instant::now(); - - match client.get(&health).send().await { - Ok(resp) => { - let status = resp.status(); - let latency_ms = started.elapsed().as_millis() as u64; - if status.is_success() { - Ok(HostProbeResult { - status: "ok".to_string(), - latency_ms, - }) - } else if status.as_u16() == 401 || status.as_u16() == 403 { - Ok(HostProbeResult { - status: "auth".to_string(), - latency_ms, - }) - } else { - Ok(HostProbeResult { - status: "unreachable".to_string(), - latency_ms, - }) - } - } - Err(_) => Ok(HostProbeResult { - status: "unreachable".to_string(), - latency_ms: started.elapsed().as_millis() as u64, - }), - } -} - -async fn wait_for_local_opencode_ready_with( - url: &str, - timeout: Duration, - initial_interval: Duration, - max_interval: Duration, -) -> Option { - let deadline = std::time::Instant::now() + timeout; - let mut interval = initial_interval; - let mut last_probe: Option = None; - - while std::time::Instant::now() < deadline { - match probe_host_with_timeout(url, max_interval).await { - Ok(probe) if matches!(probe.status.as_str(), "ok" | "auth") => { - return Some(probe); - } - Ok(probe) => { - last_probe = Some(probe); - } - Err(_) => {} - } - - tokio::time::sleep(interval).await; - interval = (interval * 2).min(max_interval); - } - - last_probe -} - -#[cfg(target_os = "macos")] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ScheduledTasksQuitRiskResponse { - has_enabled_scheduled_tasks: bool, - has_running_scheduled_tasks: bool, - #[serde(default)] - enabled_scheduled_tasks_count: u32, - #[serde(default)] - running_scheduled_tasks_count: u32, -} - -#[cfg(target_os = "macos")] -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TunnelStatusResponse { - active: bool, -} - -#[cfg(target_os = "macos")] -async fn refresh_quit_risk_flags(local_base_url: &str) { - use std::sync::atomic::Ordering; - - let trimmed = local_base_url.trim_end_matches('/'); - if trimmed.is_empty() { - return; - } - - let client = match reqwest::Client::builder() - .no_proxy() - .timeout(Duration::from_secs(2)) - .build() - { - Ok(client) => client, - Err(_) => return, - }; - - let scheduled_url = format!("{trimmed}/api/openchamber/scheduled-tasks/status"); - let tunnel_url = format!("{trimmed}/api/openchamber/tunnel/status"); - - let scheduled_future = client.get(scheduled_url).send(); - let tunnel_future = client.get(tunnel_url).send(); - let (scheduled_result, tunnel_result) = tokio::join!(scheduled_future, tunnel_future); - - if let Ok(response) = scheduled_result { - if response.status().is_success() { - if let Ok(payload) = response.json::().await { - let enabled_count = payload.enabled_scheduled_tasks_count; - let running_count = payload.running_scheduled_tasks_count; - QUIT_RISK_ENABLED_SCHEDULED_TASKS_COUNT.store(enabled_count, Ordering::Relaxed); - QUIT_RISK_RUNNING_SCHEDULED_TASKS_COUNT.store(running_count, Ordering::Relaxed); - QUIT_RISK_HAS_ENABLED_SCHEDULED_TASKS - .store(payload.has_enabled_scheduled_tasks || enabled_count > 0, Ordering::Relaxed); - QUIT_RISK_HAS_RUNNING_SCHEDULED_TASKS - .store(payload.has_running_scheduled_tasks || running_count > 0, Ordering::Relaxed); - } - } - } - - if let Ok(response) = tunnel_result { - if response.status().is_success() { - if let Ok(payload) = response.json::().await { - QUIT_RISK_HAS_ACTIVE_TUNNEL.store(payload.active, Ordering::Relaxed); - } - } - } -} - -#[cfg(target_os = "macos")] -fn start_quit_risk_poller(local_base_url: String) { - use std::sync::atomic::Ordering; - - if QUIT_RISK_POLLER_STARTED.swap(true, Ordering::SeqCst) { - return; - } - - tauri::async_runtime::spawn(async move { - loop { - refresh_quit_risk_flags(&local_base_url).await; - tokio::time::sleep(QUIT_RISK_POLL_INTERVAL).await; - } - }); -} - -#[cfg(not(target_os = "macos"))] -fn start_quit_risk_poller(_local_base_url: String) {} - -/// Uses the same probe_with_retry policy as startup/new-window (soft + hard) -/// so that first-launch/recovery remote connect accepts slow-but-valid hosts. -#[tauri::command] -async fn desktop_host_probe(url: String) -> Result { - let result = probe_with_retry(&url).await; - result - .probe - .ok_or_else(|| "Probe failed".to_string()) -} - -#[derive(Clone, Serialize)] -#[serde(tag = "event", content = "data")] -enum UpdateProgressEvent { - #[serde(rename_all = "camelCase")] - Started { - content_length: Option, - }, - #[serde(rename_all = "camelCase")] - Progress { - chunk_length: usize, - downloaded: u64, - total: Option, - }, - Finished, -} - -#[derive(Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct DesktopUpdateInfo { - available: bool, - current_version: String, - version: Option, - body: Option, - date: Option, -} - -struct PendingUpdate(Mutex>); - -fn pick_unused_port() -> Result { - let listener = TcpListener::bind("127.0.0.1:0")?; - let port = listener.local_addr()?.port(); - Ok(port) -} - -fn is_nonempty_string(value: &str) -> bool { - !value.trim().is_empty() -} - -const CHANGELOG_URL: &str = - "https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md"; - -fn parse_semver_num(value: &str) -> Option { - let trimmed = value.trim().trim_start_matches('v'); - let mut parts = trimmed.split('.'); - let major: u32 = parts.next()?.parse().ok()?; - let minor: u32 = parts.next()?.parse().ok()?; - let patch: u32 = parts.next()?.parse().ok()?; - Some(major.saturating_mul(10_000) + minor.saturating_mul(100) + patch) -} - -fn is_placeholder_release_notes(body: &Option) -> bool { - let Some(body) = body.as_ref() else { - return true; - }; - let trimmed = body.trim(); - if trimmed.is_empty() { - return true; - } - trimmed - .to_ascii_lowercase() - .starts_with("see release notes at") -} - -async fn fetch_changelog_notes(from_version: &str, to_version: &str) -> Option { - let from_num = parse_semver_num(from_version)?; - let to_num = parse_semver_num(to_version)?; - if to_num <= from_num { - return None; - } - - let client = reqwest::Client::builder() - .no_proxy() - .timeout(Duration::from_secs(10)) - .build() - .ok()?; - - let response = client.get(CHANGELOG_URL).send().await.ok()?; - if !response.status().is_success() { - return None; - } - let changelog = response.text().await.ok()?; - if changelog.trim().is_empty() { - return None; - } - - let mut markers: Vec<(usize, Option)> = Vec::new(); - let mut offset: usize = 0; - for line in changelog.lines() { - let line_trimmed = line.trim_end_matches('\r'); - if line_trimmed.starts_with("## [") { - let ver = line_trimmed - .strip_prefix("## [") - .and_then(|rest| rest.split(']').next()) - .unwrap_or(""); - markers.push((offset, parse_semver_num(ver))); - } - offset = offset.saturating_add(line.len().saturating_add(1)); - } - - if markers.is_empty() { - return None; - } - - let mut relevant: Vec = Vec::new(); - for idx in 0..markers.len() { - let (start, ver_num) = markers[idx]; - let end = markers - .get(idx + 1) - .map(|m| m.0) - .unwrap_or_else(|| changelog.len()); - let Some(ver_num) = ver_num else { - continue; - }; - if ver_num <= from_num || ver_num > to_num { - continue; - } - if start >= changelog.len() || end <= start { - continue; - } - let end_clamped = end.min(changelog.len()); - let section = changelog[start..end_clamped].trim(); - if !section.is_empty() { - relevant.push(section.to_string()); - } - } - - if relevant.is_empty() { - None - } else { - Some(relevant.join("\n\n")) - } -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct SidecarNotifyPayload { - title: Option, - body: Option, - tag: Option, - require_hidden: Option, -} - -fn maybe_show_sidecar_notification(app: &tauri::AppHandle, payload: SidecarNotifyPayload) { - let require_hidden = payload.require_hidden.unwrap_or(false); - if require_hidden { - let any_focused = app - .try_state::() - .map(|state| state.any_focused()) - .unwrap_or(false); - if any_focused { - return; - } - } - - let title = payload - .title - .filter(|t| is_nonempty_string(t)) - .unwrap_or_else(|| "OpenChamber".to_string()); - let body = payload.body.filter(|b| is_nonempty_string(b)); - let _tag = payload.tag; - - use tauri_plugin_notification::NotificationExt; - - let mut builder = app.notification().builder().title(title); - if let Some(body) = body { - builder = builder.body(body); - } - - #[cfg(target_os = "macos")] - { - builder = builder.sound("Glass"); - } - let _ = builder.show(); -} - -async fn wait_for_health_with( - url: &str, - timeout: Duration, - initial_interval: Duration, - max_interval: Duration, -) -> bool { - let client = match reqwest::Client::builder().no_proxy().build() { - Ok(c) => c, - Err(_) => return false, - }; - - let deadline = std::time::Instant::now() + timeout; - let health_url = format!("{}/health", url.trim_end_matches('/')); - let mut interval = initial_interval; - - while std::time::Instant::now() < deadline { - if let Ok(resp) = client.get(&health_url).send().await { - if resp.status().is_success() { - return true; - } - } - tokio::time::sleep(interval).await; - interval = (interval * 2).min(max_interval); - } - - false -} - -async fn wait_for_health(url: &str) -> bool { - wait_for_health_with(url, HEALTH_TIMEOUT, HEALTH_POLL_INITIAL_INTERVAL, HEALTH_POLL_MAX_INTERVAL).await -} - -fn kill_sidecar(app: tauri::AppHandle) { - let Some(state) = app.try_state::() else { - return; - }; - - let sidecar_url = state.url.lock().expect("sidecar url mutex").clone(); - if let Some(url) = sidecar_url { - // Attempt graceful shutdown via a raw HTTP POST to avoid pulling in - // reqwest::blocking (and its extra thread pool) just for this one call. - if let Ok(parsed) = url::Url::parse(&url) { - let host = parsed.host_str().unwrap_or("127.0.0.1"); - let port = parsed.port().unwrap_or(80); - let path = "/api/system/shutdown"; - if let Ok(mut stream) = - std::net::TcpStream::connect_timeout( - &format!("{host}:{port}").parse().unwrap(), - Duration::from_millis(1500), - ) - { - use std::io::Write; - let _ = stream.set_write_timeout(Some(Duration::from_millis(1500))); - let _ = stream.set_read_timeout(Some(Duration::from_millis(1500))); - let request = format!( - "POST {path} HTTP/1.1\r\nHost: {host}:{port}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" - ); - let _ = stream.write_all(request.as_bytes()); - let _ = stream.flush(); - // Brief pause to let the sidecar begin its shutdown sequence. - std::thread::sleep(Duration::from_millis(100)); - } - } - } - - let mut guard = state.child.lock().expect("sidecar mutex"); - if let Some(child) = guard.take() { - let _ = child.kill(); - } - *state.url.lock().expect("sidecar url mutex") = None; -} - -fn build_local_url(port: u16) -> String { - format!("http://127.0.0.1:{port}") -} - -/// Kills any stale openchamber-server processes that may be lingering from -/// previous app sessions or incomplete shutdowns. This ensures a clean -/// startup and prevents port conflicts. -fn kill_stale_sidecar_processes() { - let process_name = if cfg!(windows) { - "openchamber-server.exe" - } else { - "openchamber-server" - }; - - let result = if cfg!(target_os = "macos") { - // macOS: use pkill to terminate by process name - std::process::Command::new("pkill") - .arg("-x") // exact match - .arg(process_name) - .output() - } else if cfg!(target_os = "linux") { - // Linux: use pkill - std::process::Command::new("pkill") - .arg("-x") - .arg(process_name) - .output() - } else if cfg!(windows) { - // Windows: use taskkill - std::process::Command::new("taskkill") - .arg("/F") - .arg("/IM") - .arg(process_name) - .output() - } else { - return; - }; - - // Log result for debugging (pkill returns 1 if no processes found, which is fine) - if let Ok(output) = result { - log::debug!( - "[sidecar] cleanup result: exit_code={:?}, stdout={}, stderr={}", - output.status.code(), - String::from_utf8_lossy(&output.stdout).trim(), - String::from_utf8_lossy(&output.stderr).trim() - ); - } - - // Brief pause to let the OS clean up the processes - std::thread::sleep(Duration::from_millis(100)); -} - -async fn spawn_local_server(app: &tauri::AppHandle) -> Result { - // Clean up any stale sidecar processes from previous sessions - kill_stale_sidecar_processes(); - - let stored_port = read_desktop_local_port_from_disk(); - let mut candidates: Vec> = Vec::new(); - if let Some(port) = stored_port { - candidates.push(Some(port)); - } - candidates.push(Some(DEFAULT_DESKTOP_PORT)); - candidates.push(None); - - let dist_dir = resolve_web_dist_dir(app)?; - let no_proxy = "localhost,127.0.0.1"; - - // macOS app launch env often lacks user PATH entries. - let mut path_segments: Vec = Vec::new(); - let mut seen = std::collections::HashSet::::new(); - - let resolved_home_dir_path = app.path().home_dir().ok(); - let resolved_home_dir = resolved_home_dir_path.as_ref().and_then(|p| { - let s = p.to_string_lossy().to_string(); - if s.trim().is_empty() { - None - } else { - Some(s) - } - }); - - let desktop_settings = read_desktop_settings_json(); - - let opencode_binary_from_settings: Option = (|| { - let value = desktop_settings.as_ref()?.get("opencodeBinary")?.as_str()?.trim(); - if value.is_empty() { - return None; - } - - let mut candidate = value.to_string(); - if fs::metadata(&candidate) - .map(|m| m.is_dir()) - .unwrap_or(false) - { - let bin_name = if cfg!(windows) { - "opencode.exe" - } else { - "opencode" - }; - candidate = PathBuf::from(candidate) - .join(bin_name) - .to_string_lossy() - .to_string(); - } - - Some(candidate) - })(); - - let sidecar_bind_host = desktop_settings - .as_ref() - .and_then(|value| value.get("desktopLanAccessEnabled")) - .and_then(|value| value.as_bool()) - .map(|enabled| if enabled { "0.0.0.0" } else { "127.0.0.1" }) - .unwrap_or("127.0.0.1"); - - let mut push_unique = |value: String| { - let trimmed = value.trim(); - if trimmed.is_empty() { - return; - } - if seen.insert(trimmed.to_string()) { - path_segments.push(trimmed.to_string()); - } - }; - - // Respect explicit binary overrides by adding their parent dir first. - if let Some(val) = opencode_binary_from_settings.as_deref() { - let trimmed = val.trim(); - if !trimmed.is_empty() { - let path = std::path::Path::new(trimmed); - if let Some(parent) = path.parent() { - push_unique(parent.to_string_lossy().to_string()); - } - } - } - - for var in [ - "OPENCHAMBER_OPENCODE_PATH", - "OPENCHAMBER_OPENCODE_BIN", - "OPENCODE_PATH", - "OPENCODE_BINARY", - ] { - if let Ok(val) = env::var(var) { - let trimmed = val.trim(); - if trimmed.is_empty() { - continue; - } - let path = std::path::Path::new(trimmed); - if let Some(parent) = path.parent() { - push_unique(parent.to_string_lossy().to_string()); - } - } - } - - // Common locations. - push_unique("/opt/homebrew/bin".to_string()); - push_unique("/usr/local/bin".to_string()); - push_unique("/usr/bin".to_string()); - push_unique("/bin".to_string()); - push_unique("/usr/sbin".to_string()); - push_unique("/sbin".to_string()); - - if let Some(home) = resolved_home_dir.as_deref() { - // OpenCode installer default. - push_unique(format!("{home}/.opencode/bin")); - push_unique(format!("{home}/.local/bin")); - push_unique(format!("{home}/.bun/bin")); - push_unique(format!("{home}/.cargo/bin")); - push_unique(format!("{home}/bin")); - } - - if let Ok(existing) = env::var("PATH") { - for segment in existing.split(':') { - push_unique(segment.to_string()); - } - } - - let augmented_path = path_segments.join(":"); - - for candidate in candidates { - let port = match candidate { - Some(p) => p, - None => pick_unused_port()?, - }; - let url = build_local_url(port); - - let mut cmd = app - .shell() - .sidecar(SIDECAR_NAME) - .map_err(|err| anyhow!("Failed to resolve sidecar '{SIDECAR_NAME}': {err}"))? - .args(["--port", &port.to_string()]) - .env("OPENCHAMBER_HOST", sidecar_bind_host) - .env("OPENCHAMBER_DIST_DIR", dist_dir.clone()) - .env("OPENCHAMBER_RUNTIME", "desktop") - .env("OPENCHAMBER_DESKTOP_NOTIFY", "true") - .env("OPENCHAMBER_SKIP_API_COMPRESSION", "true") - .env("PATH", augmented_path.clone()) - .env("NO_PROXY", no_proxy) - .env("no_proxy", no_proxy); - - if let Some(home) = resolved_home_dir.as_deref() { - cmd = cmd.env("HOME", home); - } - - if let Some(bin) = opencode_binary_from_settings.as_deref() { - let trimmed = bin.trim(); - if !trimmed.is_empty() { - cmd = cmd.env("OPENCODE_BINARY", trimmed); - } - } - - if let Ok(password) = env::var("OPENCODE_SERVER_PASSWORD") { - let trimmed = password.trim(); - if !trimmed.is_empty() { - cmd = cmd.env("OPENCODE_SERVER_PASSWORD", trimmed); - } - } - - let (rx, child) = match cmd.spawn() { - Ok(v) => v, - Err(err) => { - log::warn!("[sidecar] spawn failed on port {port}: {err}"); - continue; - } - }; - - let app_handle = app.clone(); - tauri::async_runtime::spawn(async move { - let mut rx = rx; - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Stdout(bytes) => { - let line = String::from_utf8_lossy(&bytes); - if let Some(rest) = line.strip_prefix(SIDECAR_NOTIFY_PREFIX) { - if let Ok(parsed) = - serde_json::from_str::(rest.trim()) - { - maybe_show_sidecar_notification(&app_handle, parsed); - } - } - } - CommandEvent::Error(error) => { - log::warn!("[sidecar] error: {error}"); - } - CommandEvent::Terminated(payload) => { - log::warn!( - "[sidecar] terminated code={:?} signal={:?}", - payload.code, - payload.signal - ); - break; - } - _ => {} - } - } - }); - - if let Some(state) = app.try_state::() { - *state.child.lock().expect("sidecar mutex") = Some(child); - *state.url.lock().expect("sidecar url mutex") = Some(url.clone()); - } - - if !wait_for_health_with( - &url, - LOCAL_SIDECAR_HEALTH_TIMEOUT, - LOCAL_SIDECAR_HEALTH_POLL_INITIAL_INTERVAL, - LOCAL_SIDECAR_HEALTH_POLL_MAX_INTERVAL, - ) - .await - { - kill_sidecar(app.clone()); - continue; - } - - let _ = write_desktop_local_port_to_disk(port); - return Ok(url); - } - - Err(anyhow!("Sidecar health check failed")) -} - -fn resolve_web_dist_dir(app: &tauri::AppHandle) -> Result { - let candidates = ["web-dist", "resources/web-dist"]; - for candidate in candidates { - let path = app - .path() - .resolve(candidate, tauri::path::BaseDirectory::Resource) - .map_err(|err| anyhow!("Failed to resolve '{candidate}' resources: {err}"))?; - let index = path.join("index.html"); - if fs::metadata(&index).is_ok() { - return Ok(path); - } - } - - Err(anyhow!( - "Web assets missing in app resources (expected index.html under web-dist)" - )) -} - -fn normalize_server_url(input: &str) -> Option { - normalize_host_url(input) -} - -#[derive(Deserialize)] -struct DesktopNotifyPayload { - title: Option, - body: Option, - tag: Option, -} - -#[tauri::command] -fn desktop_notify( - app: tauri::AppHandle, - payload: Option, -) -> Result { - let payload = payload.unwrap_or(DesktopNotifyPayload { - title: None, - body: None, - tag: None, - }); - - use tauri_plugin_notification::NotificationExt; - - let mut builder = app - .notification() - .builder() - .title(payload.title.unwrap_or_else(|| "OpenChamber".to_string())); - - if let Some(body) = payload.body { - if is_nonempty_string(&body) { - builder = builder.body(body); - } - } - - if let Some(tag) = payload.tag { - if is_nonempty_string(&tag) { - let _ = tag; - } - } - - #[cfg(target_os = "macos")] - { - builder = builder.sound("Glass"); - } - - builder.show().map(|_| true).map_err(|err| err.to_string()) -} - -#[tauri::command] -async fn desktop_check_for_updates( - app: tauri::AppHandle, - pending: tauri::State<'_, PendingUpdate>, -) -> Result { - let updater = app.updater().map_err(|err| err.to_string())?; - let update = updater.check().await.map_err(|err| err.to_string())?; - - let current_version = app.package_info().version.to_string(); - - let info = if let Some(update) = update { - *pending.0.lock().expect("pending update mutex") = Some(update.clone()); - let mut body = update.body.clone(); - if is_placeholder_release_notes(&body) { - if let Some(notes) = fetch_changelog_notes(¤t_version, &update.version).await { - body = Some(notes); - } - } - DesktopUpdateInfo { - available: true, - current_version, - version: Some(update.version.clone()), - body, - date: update.date.map(|date| date.to_string()), - } - } else { - *pending.0.lock().expect("pending update mutex") = None; - DesktopUpdateInfo { - available: false, - current_version, - version: None, - body: None, - date: None, - } - }; - - Ok(info) -} - -#[tauri::command] -async fn desktop_download_and_install_update( - app: tauri::AppHandle, - pending: tauri::State<'_, PendingUpdate>, -) -> Result<(), String> { - let Some(update) = pending.0.lock().expect("pending update mutex").take() else { - return Err("No pending update".to_string()); - }; - - let mut downloaded: u64 = 0; - let mut total: Option = None; - let mut started = false; - - update - .download_and_install( - |chunk_length, content_length| { - if !started { - total = content_length; - let _ = app.emit( - "openchamber:update-progress", - UpdateProgressEvent::Started { content_length }, - ); - started = true; - } - - downloaded = downloaded.saturating_add(chunk_length as u64); - let _ = app.emit( - "openchamber:update-progress", - UpdateProgressEvent::Progress { - chunk_length, - downloaded, - total, - }, - ); - }, - || { - let _ = app.emit("openchamber:update-progress", UpdateProgressEvent::Finished); - }, - ) - .await - .map_err(|err| err.to_string())?; - - Ok(()) -} - -#[tauri::command] -fn desktop_restart(app: tauri::AppHandle) { - app.restart(); -} - -/// Create a new desktop window from the UI layer. -/// -/// IMPORTANT: This command MUST remain synchronous (not `async`). Tauri runs -/// sync commands on the main thread, which is required on macOS for -/// `WebviewWindowBuilder::build()`. Making this `async` would move execution -/// to the Tokio thread pool and risk crashes or undefined behavior. -#[tauri::command] -fn desktop_new_window(app: tauri::AppHandle) -> Result<(), String> { - open_new_window(&app); - Ok(()) -} - -/// Open a new window pointed at a specific URL (used by the host switcher UI). -/// -/// For remote URLs (not matching local origin), probes the host and only opens -/// the window if the probe returns `ok` or `auth`. Falls back to local if the -/// remote is non-navigable. Window creation is dispatched to the main thread -/// and its result is propagated back to the caller. -#[tauri::command] -async fn desktop_new_window_at_url(app: tauri::AppHandle, url: String) -> Result<(), String> { - // Validate scheme to prevent file://, data:, javascript: etc. - let parsed = url::Url::parse(&url).map_err(|e| format!("Invalid URL: {e}"))?; - match parsed.scheme() { - "http" | "https" => {} - scheme => return Err(format!("Unsupported URL scheme: {scheme}")), - } - - let local_origin = app - .try_state::() - .and_then(|state| { - state - .local_origin - .lock() - .expect("desktop local origin mutex") - .clone() - }) - .ok_or_else(|| "Local origin not yet known (sidecar may still be starting)".to_string())?; - - // If the URL is local, create the window directly. - if same_server_url(&url, &local_origin) { - let boot_outcome = DesktopBootOutcome { - target: Some("local".to_string()), - status: "ok".to_string(), - host_id: None, - url: None, - }; - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = app.clone(); - app.run_on_main_thread(move || { - let result = create_window(&handle, &url, &local_origin, Some(&boot_outcome), false) - .map_err(|e| e.to_string()); - let _ = tx.send(result); - }) - .map_err(|e| e.to_string())?; - return rx.await.map_err(|_| "Window creation cancelled".to_string())?; - } - - // Remote URL: probe with shared retry policy before opening. - let result = probe_with_retry(&url).await; - - let (final_url, boot_outcome) = if result.navigable { - let outcome = DesktopBootOutcome { - target: Some("remote".to_string()), - status: "ok".to_string(), - host_id: Some(DIRECT_URL_HOST_ID.to_string()), - url: Some(url.clone()), - }; - (url, outcome) - } else { - log::info!( - "[desktop] new_window_at_url: remote ({}) probe returned non-navigable status, falling back to local", - url - ); - let local_fallback = format!("{}/", local_origin); - let outcome = match &result.probe { - Some(probe) if probe.status == "wrong-service" => DesktopBootOutcome { - target: Some("remote".to_string()), - status: "wrong-service".to_string(), - host_id: Some(DIRECT_URL_HOST_ID.to_string()), - url: Some(url), - }, - _ => DesktopBootOutcome { - target: Some("remote".to_string()), - status: "unreachable".to_string(), - host_id: Some(DIRECT_URL_HOST_ID.to_string()), - url: Some(url), - }, - }; - (local_fallback, outcome) - }; - - let (tx, rx) = tokio::sync::oneshot::channel(); - let handle = app.clone(); - app.run_on_main_thread(move || { - let result = create_window(&handle, &final_url, &local_origin, Some(&boot_outcome), false) - .map_err(|e| e.to_string()); - let _ = tx.send(result); - }) - .map_err(|e| e.to_string())?; - rx.await.map_err(|_| "Window creation cancelled".to_string())? -} - -/// Read a file and return its content as base64 with mime type detection. -/// Used for drag-drop file attachments in desktop app. -#[tauri::command] -fn desktop_read_file(path: String) -> Result { - use std::path::Path; - - let path = Path::new(&path); - - // Check file size (max 50MB) - let metadata = - std::fs::metadata(path).map_err(|e| format!("Failed to read file metadata: {e}"))?; - let size = metadata.len(); - if size > 50 * 1024 * 1024 { - return Err("File is too large. Maximum size is 50MB.".to_string()); - } - - // Read file bytes - let bytes = std::fs::read(path).map_err(|e| format!("Failed to read file: {e}"))?; - - // Detect mime type from extension - let ext = path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("") - .to_lowercase(); - let mime = match ext.as_str() { - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "gif" => "image/gif", - "webp" => "image/webp", - "svg" => "image/svg+xml", - "bmp" => "image/bmp", - "ico" => "image/x-icon", - "pdf" => "application/pdf", - "txt" => "text/plain", - "md" => "text/markdown", - "json" => "application/json", - "js" => "text/javascript", - "ts" => "text/typescript", - "tsx" => "text/typescript-jsx", - "jsx" => "text/javascript-jsx", - "html" => "text/html", - "css" => "text/css", - "py" => "text/x-python", - _ => "application/octet-stream", - }; - - // Encode as base64 - let base64 = general_purpose::STANDARD.encode(&bytes); - - Ok(FileContent { - mime: mime.to_string(), - base64, - size: bytes.len(), - }) -} - -#[tauri::command] -async fn desktop_save_markdown_file( - app: tauri::AppHandle, - default_file_name: String, - content: String, -) -> Result, String> { - use tauri_plugin_dialog::DialogExt; - - let trimmed_file_name = default_file_name.trim(); - if trimmed_file_name.is_empty() { - return Err("Default file name is required".to_string()); - } - - let (tx, rx) = tokio::sync::oneshot::channel(); - app.dialog() - .file() - .add_filter("Markdown", &["md"]) - .set_file_name(trimmed_file_name) - .save_file(move |file_path| { - let _ = tx.send(file_path); - }); - - let Some(file_path) = rx - .await - .map_err(|_| "Save dialog was closed unexpectedly".to_string())? - else { - return Ok(None); - }; - - let path = file_path - .into_path() - .map_err(|_| "Selected export path is not a local filesystem path".to_string())?; - - std::fs::write(&path, content) - .map_err(|error| format!("Failed to save exported session: {error}"))?; - - Ok(Some(path.to_string_lossy().to_string())) -} - -#[derive(Serialize)] -struct FileContent { - mime: String, - base64: String, - size: usize, -} - -#[cfg(target_os = "macos")] -fn macos_major_version() -> Option { - fn cmd_stdout(cmd: &str, args: &[&str]) -> Option { - let output = Command::new(cmd).args(args).output().ok()?; - if !output.status.success() { - return None; - } - String::from_utf8(output.stdout).ok() - } - - // Use marketing version (sw_vers), but map legacy 10.x to minor (10.15 -> 15). - // This matches WebKit UA fallback logic in the UI. - if let Some(raw) = cmd_stdout("/usr/bin/sw_vers", &["-productVersion"]) - .or_else(|| cmd_stdout("sw_vers", &["-productVersion"])) - { - let raw = raw.trim(); - let mut parts = raw.split('.'); - let major = parts.next().and_then(|v| v.parse::().ok())?; - let minor = parts - .next() - .and_then(|v| v.parse::().ok()) - .unwrap_or(0); - return Some(if major == 10 { minor } else { major }); - } - - // Fallback: derive from Darwin major (kern.osrelease major). - let raw = cmd_stdout("/usr/sbin/sysctl", &["-n", "kern.osrelease"]) - .or_else(|| cmd_stdout("sysctl", &["-n", "kern.osrelease"])) - .or_else(|| cmd_stdout("/usr/bin/uname", &["-r"])) - .or_else(|| cmd_stdout("uname", &["-r"]))?; - let raw = raw.trim(); - let major = raw.split('.').next()?.parse::().ok()?; - if major >= 20 { - return Some(major - 9); - } - if major >= 15 { - return Some(major - 4); - } - Some(major) -} - -#[cfg(not(target_os = "macos"))] -fn macos_major_version() -> Option { - None -} - -/// Build the initialization script injected into every webview window. -/// This is computed once and reused for all windows. -fn build_init_script(local_origin: &str, boot_outcome: Option<&DesktopBootOutcome>) -> String { - let home = - std::env::var(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).unwrap_or_default(); - let macos_major = macos_major_version().unwrap_or(0); - - let home_json = serde_json::to_string(&home).unwrap_or_else(|_| "\"\"".into()); - let local_json = serde_json::to_string(local_origin).unwrap_or_else(|_| "\"\"".into()); - let boot_outcome_json = boot_outcome - .and_then(|o| serde_json::to_string(o).ok()) - .unwrap_or_else(|| "undefined".to_string()); - - let mut init_script = format!( - "(function(){{try{{window.__OPENCHAMBER_HOME__={home_json};window.__OPENCHAMBER_MACOS_MAJOR__={macos_major};window.__OPENCHAMBER_LOCAL_ORIGIN__={local_json};window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__={boot_outcome_json};}}catch(_e){{}}}})();" - ); - - // Cleanup: older builds injected a native-ish Instance switcher button into pages. - // Remove it if present so the UI-owned host switcher is the only one. - init_script.push_str("\ntry{var old=document.getElementById('__oc-instance-switcher');if(old)old.remove();}catch(_e){}"); - - if !cfg!(debug_assertions) { - init_script.push_str("\ntry{document.addEventListener('contextmenu',function(e){var t=e&&e.target;if(!t||typeof t.closest!=='function'){e.preventDefault();return;}if(t.closest('.terminal-viewport-container,[data-oc-allow-native-contextmenu],a,input,textarea,[contenteditable=\"true\"]')){return;}e.preventDefault();},true);}catch(_e){}"); - } - - init_script -} - -fn parse_theme_override(theme_mode: Option<&str>, theme_variant: Option<&str>) -> Option { - match theme_mode.map(str::trim) { - Some("system") => None, - Some("dark") => Some(tauri::Theme::Dark), - Some("light") => Some(tauri::Theme::Light), - _ => match theme_variant.map(str::trim) { - Some("dark") => Some(tauri::Theme::Dark), - Some("light") => Some(tauri::Theme::Light), - _ => None, - }, - } -} - -fn read_desktop_theme_override() -> Option { - let settings = read_desktop_settings_json(); - - let use_system_theme = settings - .as_ref() - .and_then(|value| value.get("useSystemTheme")) - .and_then(|value| value.as_bool()); - - if matches!(use_system_theme, Some(true)) { - return None; - } - - let theme_mode = settings - .as_ref() - .and_then(|value| value.get("themeMode")) - .and_then(|value| value.as_str()); - - let theme_variant = settings - .as_ref() - .and_then(|value| value.get("themeVariant")) - .and_then(|value| value.as_str()); - - parse_theme_override(theme_mode, theme_variant) -} - -fn detect_desktop_lan_ipv4() -> Option { - let socket = UdpSocket::bind("0.0.0.0:0").ok()?; - socket.connect("8.8.8.8:80").ok()?; - let address = socket.local_addr().ok()?; - let ip = address.ip(); - - if ip.is_loopback() { - return None; - } - - match ip { - std::net::IpAddr::V4(ipv4) => Some(ipv4.to_string()), - std::net::IpAddr::V6(_) => None, - } -} - -/// Apply platform-specific window builder configuration. -fn apply_platform_window_config>( - builder: WebviewWindowBuilder<'_, tauri::Wry, M>, -) -> WebviewWindowBuilder<'_, tauri::Wry, M> { - #[cfg(target_os = "macos")] - let builder = builder - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .traffic_light_position(tauri::Position::Logical(tauri::LogicalPosition { - x: 17.0, - y: 26.0, - })); - - #[cfg(target_os = "windows")] - let builder = builder.additional_browser_args( - "--proxy-bypass-list=<-loopback> --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection", - ); - - builder -} - -#[tauri::command] -fn desktop_set_window_theme( - window: tauri::WebviewWindow, - theme_mode: Option, - theme_variant: Option, -) -> Result<(), String> { - let override_theme = parse_theme_override(theme_mode.as_deref(), theme_variant.as_deref()); - - window - .set_theme(override_theme) - .map_err(|error| format!("failed to set window theme: {error}"))?; - - Ok(()) -} - -#[tauri::command] -fn desktop_get_lan_address() -> Option { - detect_desktop_lan_ipv4() -} - -fn is_window_state_visible(app: &tauri::AppHandle, state: &DesktopWindowState) -> bool { - if state.width == 0 || state.height == 0 { - return false; - } - - let Ok(monitors) = app.available_monitors() else { - return true; - }; - if monitors.is_empty() { - return true; - } - - let left = state.x as f64; - let top = state.y as f64; - let right = left + state.width as f64; - let bottom = top + state.height as f64; - - for monitor in monitors { - let scale = monitor.scale_factor(); - if !scale.is_finite() || scale <= 0.0 { - continue; - } - - let position = monitor.position(); - let size = monitor.size(); - - let monitor_left = position.x as f64 / scale; - let monitor_top = position.y as f64 / scale; - let monitor_right = monitor_left + size.width as f64 / scale; - let monitor_bottom = monitor_top + size.height as f64 / scale; - - let overlap_width = right.min(monitor_right) - left.max(monitor_left); - let overlap_height = bottom.min(monitor_bottom) - top.max(monitor_top); - if overlap_width > 0.0 && overlap_height > 0.0 { - return true; - } - } - - false -} - -fn capture_window_state(window: &tauri::Window) -> Option { - let position = window.outer_position().ok()?; - let size = window.inner_size().ok()?; - let scale = window - .scale_factor() - .ok() - .filter(|value| value.is_finite() && *value > 0.0) - .unwrap_or(1.0); - - Some(DesktopWindowState { - x: (position.x as f64 / scale).round() as i32, - y: (position.y as f64 / scale).round() as i32, - width: (size.width as f64 / scale) - .round() - .max(MIN_WINDOW_WIDTH as f64) as u32, - height: (size.height as f64 / scale) - .round() - .max(MIN_WINDOW_HEIGHT as f64) as u32, - maximized: window.is_maximized().unwrap_or(false), - fullscreen: window.is_fullscreen().unwrap_or(false), - }) -} - -fn schedule_window_state_persist(window: tauri::Window, immediate: bool) { - if window.label() != "main" { - return; - } - - let app = window.app_handle().clone(); - let label = window.label().to_string(); - let revision = { - let Some(state) = app.try_state::() else { - return; - }; - let mut guard = state - .revisions - .lock() - .expect("window geometry debounce mutex"); - let next = guard.get(&label).copied().unwrap_or(0).saturating_add(1); - guard.insert(label.clone(), next); - next - }; - - tauri::async_runtime::spawn(async move { - if !immediate { - tokio::time::sleep(Duration::from_millis(WINDOW_STATE_DEBOUNCE_MS)).await; - } - - let is_latest = app - .try_state::() - .map(|state| { - state - .revisions - .lock() - .map(|guard| guard.get(&label).copied() == Some(revision)) - .unwrap_or(false) - }) - .unwrap_or(false); - if !is_latest { - return; - } - - let Some(snapshot) = capture_window_state(&window) else { - return; - }; - - if let Err(err) = write_desktop_window_state_to_disk(&snapshot) { - log::warn!("[desktop] failed to persist window geometry: {err}"); - } - }); -} - -/// Create a new window with a unique label, pointing at the given URL. -fn create_window( - app: &tauri::AppHandle, - url: &str, - local_origin: &str, - boot_outcome: Option<&DesktopBootOutcome>, - restore_geometry: bool, -) -> Result<()> { - let parsed = url::Url::parse(url).map_err(|err| anyhow!("Invalid URL: {err}"))?; - let label = next_window_label(app); - - let init_script = build_init_script(local_origin, boot_outcome); - - // Store the init script under this window's label so page reloads - // re-inject the correct boot outcome for this window. - if let Some(state) = app.try_state::() { - state - .scripts - .lock() - .expect("desktop ui injection mutex") - .insert(label.clone(), init_script.clone()); - *state - .local_origin - .lock() - .expect("desktop local origin mutex") = Some(local_origin.to_string()); - } - - let restored_state = if restore_geometry { - read_desktop_window_state_from_disk() - } else { - None - }; - - let mut builder = WebviewWindowBuilder::new(app, &label, WebviewUrl::External(parsed)) - .title("OpenChamber") - .inner_size(1280.0, 800.0) - .min_inner_size(MIN_WINDOW_WIDTH as f64, MIN_WINDOW_HEIGHT as f64) - .decorations(true) - .visible(false) - .initialization_script(&init_script); - - builder = apply_platform_window_config(builder); - - let apply_restored_state = restored_state - .as_ref() - .map(|state| is_window_state_visible(app, state)) - .unwrap_or(false); - - if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) { - let restored_width = state.width.max(MIN_RESTORE_WINDOW_WIDTH); - let restored_height = state.height.max(MIN_RESTORE_WINDOW_HEIGHT); - builder = builder - .inner_size(restored_width as f64, restored_height as f64) - .position(state.x as f64, state.y as f64); - } - - let window = builder.build()?; - let _ = window.set_theme(read_desktop_theme_override()); - disable_pinch_zoom(&window); - - if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) { - if state.maximized || state.fullscreen { - let _ = window.maximize(); - } - } - - let _ = window.show(); - let _ = window.set_focus(); - - Ok(()) -} - -fn create_startup_window(app: &tauri::AppHandle, restore_geometry: bool) -> Result<()> { - if app.get_webview_window("main").is_some() { - return Ok(()); - } - - let restored_state = if restore_geometry { - read_desktop_window_state_from_disk() - } else { - None - }; - - let splash_script = build_startup_splash_script(); - - let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into())) - .title("OpenChamber") - .inner_size(1280.0, 800.0) - .min_inner_size(MIN_WINDOW_WIDTH as f64, MIN_WINDOW_HEIGHT as f64) - .decorations(true) - .visible(true) - .initialization_script(&splash_script); - - builder = apply_platform_window_config(builder); - - let apply_restored_state = restored_state - .as_ref() - .map(|state| is_window_state_visible(app, state)) - .unwrap_or(false); - - if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) { - let restored_width = state.width.max(MIN_RESTORE_WINDOW_WIDTH); - let restored_height = state.height.max(MIN_RESTORE_WINDOW_HEIGHT); - builder = builder - .inner_size(restored_width as f64, restored_height as f64) - .position(state.x as f64, state.y as f64); - } - - let window = builder.build()?; - let _ = window.set_theme(read_desktop_theme_override()); - disable_pinch_zoom(&window); - - if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) { - if state.maximized || state.fullscreen { - let _ = window.maximize(); - } - } - - let _ = window.show(); - let _ = window.set_focus(); - - Ok(()) -} - -fn build_startup_splash_script() -> String { - let settings = fs::read_to_string(settings_file_path()) - .ok() - .and_then(|raw| serde_json::from_str::(&raw).ok()); - - let theme_mode = settings - .as_ref() - .and_then(|value| value.get("themeMode")) - .and_then(|value| value.as_str()) - .and_then(|value| match value.trim() { - "light" => Some("light"), - "dark" => Some("dark"), - "system" => Some("system"), - _ => None, - }); - - let use_system_theme = settings - .as_ref() - .and_then(|value| value.get("useSystemTheme")) - .and_then(|value| value.as_bool()) - .unwrap_or(true); - - let theme_variant = settings - .as_ref() - .and_then(|value| value.get("themeVariant")) - .and_then(|value| value.as_str()) - .and_then(|value| match value.trim() { - "light" => Some("light"), - "dark" => Some("dark"), - _ => None, - }); - - let effective_mode = theme_mode - .or_else(|| { - if use_system_theme { - Some("system") - } else { - None - } - }) - .or(theme_variant) - .unwrap_or("system"); - - let splash_bg_light = settings - .as_ref() - .and_then(|value| value.get("splashBgLight")) - .and_then(|value| value.as_str()) - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .unwrap_or(""); - let splash_fg_light = settings - .as_ref() - .and_then(|value| value.get("splashFgLight")) - .and_then(|value| value.as_str()) - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .unwrap_or(""); - let splash_bg_dark = settings - .as_ref() - .and_then(|value| value.get("splashBgDark")) - .and_then(|value| value.as_str()) - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .unwrap_or(""); - let splash_fg_dark = settings - .as_ref() - .and_then(|value| value.get("splashFgDark")) - .and_then(|value| value.as_str()) - .map(|value| value.trim()) - .filter(|value| !value.is_empty()) - .unwrap_or(""); - - let mode_json = serde_json::to_string(effective_mode).unwrap_or_else(|_| "\"system\"".into()); - let bg_light_json = serde_json::to_string(splash_bg_light).unwrap_or_else(|_| "\"\"".into()); - let fg_light_json = serde_json::to_string(splash_fg_light).unwrap_or_else(|_| "\"\"".into()); - let bg_dark_json = serde_json::to_string(splash_bg_dark).unwrap_or_else(|_| "\"\"".into()); - let fg_dark_json = serde_json::to_string(splash_fg_dark).unwrap_or_else(|_| "\"\"".into()); - - format!( - "(function(){{try{{var mode={mode_json};var bgLight={bg_light_json};var fgLight={fg_light_json};var bgDark={bg_dark_json};var fgDark={fg_dark_json};var root=document.documentElement;if(bgLight)root.style.setProperty('--splash-background-light',bgLight);if(fgLight)root.style.setProperty('--splash-stroke-light',fgLight);if(bgDark)root.style.setProperty('--splash-background-dark',bgDark);if(fgDark)root.style.setProperty('--splash-stroke-dark',fgDark);var prefersDark=false;try{{prefersDark=!!(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches);}}catch(_e){{}}var dark=mode==='dark'?true:(mode==='light'?false:prefersDark);root.setAttribute('data-splash-variant',dark?'dark':'light');root.style.setProperty('color-scheme',dark?'dark':'light');}}catch(_e){{}}}})();" - ) -} - -fn activate_main_window( - app: &tauri::AppHandle, - url: &str, - local_origin: &str, - boot_outcome: Option<&DesktopBootOutcome>, -) -> Result<()> { - let parsed = url::Url::parse(url).map_err(|err| anyhow!("Invalid URL: {err}"))?; - let init_script = build_init_script(local_origin, boot_outcome); - - if let Some(state) = app.try_state::() { - state - .scripts - .lock() - .expect("desktop ui injection mutex") - .insert("main".to_string(), init_script); - *state - .local_origin - .lock() - .expect("desktop local origin mutex") = Some(local_origin.to_string()); - } - - if let Some(window) = app.get_webview_window("main") { - window.navigate(parsed).map_err(|err| anyhow!(err.to_string()))?; - let _ = window.set_focus(); - return Ok(()); - } - - create_window(app, url, local_origin, boot_outcome, true) -} - -/// Open a new window pointed at the default host (local or configured default). -/// -/// Known multi-window limitations (acceptable for v1): -/// -/// - **localStorage conflicts**: Windows sharing the same origin (e.g. both on local) -/// share `localStorage`. Zustand `persist` middleware writes full state blobs on every -/// change with no cross-tab sync, so concurrent windows can overwrite each other's -/// persisted UI preferences, session selections, and model/agent choices. -/// Server-side data is unaffected. Scoping storage keys per window or adding -/// `storage` event listeners would fix this in a future iteration. -/// -/// - **Duplicate SSE connections**: Each window opens its own SSE connection to -/// `/api/global/event`, resulting in N connections for N windows. Each window -/// independently processes all events and may show duplicate toast notifications. -/// A SharedWorker, BroadcastChannel leader-election, or Rust-side SSE relay -/// would consolidate this in a future iteration. -/// -/// - **Startup race**: If called before the sidecar finishes starting (local_origin -/// not yet set), this function silently bails with a log warning. The user sees -/// no feedback from clicking the dock icon during the startup window (~0-20s). -fn open_new_window(app: &tauri::AppHandle) { - let local_origin = app - .try_state::() - .and_then(|state| { - state - .local_origin - .lock() - .expect("desktop local origin mutex") - .clone() - }); - - let Some(local_origin) = local_origin else { - log::warn!("[desktop] cannot open new window: local origin not yet known (sidecar may still be starting)"); - return; - }; - - // Resolve the URL the same way as initial setup: env override, then default host, else local. - let local_url = app - .try_state::() - .and_then(|state| state.url.lock().expect("sidecar url mutex").clone()) - .unwrap_or_else(|| local_origin.clone()); - - let local_ui_url = if cfg!(debug_assertions) { - // In dev mode, prefer the Vite dev server if it was used as local origin. - local_origin.clone() - } else { - local_url.clone() - }; - - let env_target = std::env::var("OPENCHAMBER_SERVER_URL") - .ok() - .and_then(|raw| normalize_server_url(&raw)) - .filter(|url| !same_server_url(url, &local_ui_url)); - - let cfg = read_desktop_hosts_config_from_disk(); - - let target_url = if let Some(ref env_url) = env_target { - env_url.clone() - } else if let Some(default_id) = cfg.default_host_id.as_deref() { - if default_id == LOCAL_HOST_ID { - local_ui_url.clone() - } else { - cfg.hosts - .iter() - .find(|h| h.id == default_id) - .map(|h| h.url.clone()) - .unwrap_or(local_ui_url.clone()) - } - } else { - local_ui_url.clone() - }; - - // Compute boot outcome for the new window (no probe yet for sync local case). - let boot_outcome = resolve_boot_outcome( - &cfg, - None, - true, - env_target.as_deref(), - ); - - // If the target is local, create the window immediately on this (main) thread. - if same_server_url(&target_url, &local_ui_url) { - if let Err(err) = create_window(app, &target_url, &local_origin, Some(&boot_outcome), false) { - log::error!("[desktop] failed to create new window: {err}"); - } - return; - } - - // For remote hosts, probe asynchronously then dispatch window creation - // back to the main thread via run_on_main_thread (required on macOS). - // Uses the same probe_with_retry policy as startup (soft + hard). - let handle = app.clone(); - let cfg_snapshot = cfg.clone(); - let env_target_snapshot = env_target.clone(); - tauri::async_runtime::spawn(async move { - let result = probe_with_retry(&target_url).await; - - let final_url = if result.navigable { - target_url - } else { - log::info!( - "[desktop] new window: default host ({}) probe returned non-navigable status, using local", - target_url - ); - local_ui_url - }; - - // Recompute boot outcome with actual probe result, using the - // same config/env snapshot that chose this window's target. - let final_boot_outcome = resolve_boot_outcome( - &cfg_snapshot, - result.probe.as_ref(), - true, - env_target_snapshot.as_deref(), - ); - - let local = local_origin; - let handle_clone = handle.clone(); - if let Err(err) = handle.run_on_main_thread(move || { - if let Err(err) = create_window(&handle_clone, &final_url, &local, Some(&final_boot_outcome), false) { - log::error!("[desktop] failed to create new window: {err}"); - } - }) { - log::error!("[desktop] failed to dispatch window creation to main thread: {err}"); - } - }); -} - -fn main() { - // Ensure localhost traffic never routes through a system/VPN proxy. - for key in ["NO_PROXY", "no_proxy"] { - let existing = env::var(key).unwrap_or_default(); - let loopback = ["127.0.0.1", "localhost", "::1"]; - let missing: Vec<&str> = loopback - .iter() - .filter(|addr| !existing.split(',').any(|part| part.trim() == **addr)) - .copied() - .collect(); - if !missing.is_empty() { - let merged = if existing.is_empty() { - missing.join(",") - } else { - format!("{},{}", existing, missing.join(",")) - }; - env::set_var(key, &merged); - } - } - - let log_builder = tauri_plugin_log::Builder::default() - .level(log::LevelFilter::Info) - .clear_targets() - .targets(if cfg!(debug_assertions) { - vec![ - tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), - tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview), - ] - } else { - vec![tauri_plugin_log::Target::new( - tauri_plugin_log::TargetKind::Stdout, - )] - }); - - let builder = tauri::Builder::default() - .manage(SidecarState::default()) - .manage(DesktopUiInjectionState::default()) - .manage(WindowFocusState::default()) - .manage(WindowGeometryDebounceState::default()) - .manage(DesktopSshManagerState::default()) - .manage(PendingUpdate(Mutex::new(None))) - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_notification::init()) - .plugin(tauri_plugin_updater::Builder::new().build()) - .plugin(log_builder.build()) - .on_page_load(|window, _payload| { - if let Some(state) = window.app_handle().try_state::() { - let label = window.label().to_string(); - if let Ok(guard) = state.scripts.lock() { - if let Some(script) = guard.get(&label) { - let _ = window.eval(script); - } - } - } - }) - .menu(|app| { - #[cfg(target_os = "macos")] - { - build_macos_menu(app) - } - - #[cfg(not(target_os = "macos"))] - { - tauri::menu::Menu::default(app) - } - }) - .on_menu_event(|app, event| { - #[cfg(target_os = "macos")] - { - let id = event.id().as_ref(); - - log::info!("[menu] click id={}", id); - - #[cfg(debug_assertions)] - { - let msg = serde_json::to_string(id).unwrap_or_else(|_| "\"(unserializable)\"".into()); - eval_in_focused_window(app, &format!("console.log('[menu] id=', {});", msg)); - } - - if id == MENU_ITEM_NEW_WINDOW_ID { - open_new_window(app); - return; - } - - if id == MENU_ITEM_CHECK_FOR_UPDATES_ID { - dispatch_check_for_updates(app); - return; - } - - if id == MENU_ITEM_REPORT_BUG_ID { - use tauri_plugin_shell::ShellExt; - #[allow(deprecated)] - { - let _ = app.shell().open(GITHUB_BUG_REPORT_URL, None); - } - return; - } - - if id == MENU_ITEM_REQUEST_FEATURE_ID { - use tauri_plugin_shell::ShellExt; - #[allow(deprecated)] - { - let _ = app.shell().open(GITHUB_FEATURE_REQUEST_URL, None); - } - return; - } - - if id == MENU_ITEM_JOIN_DISCORD_ID { - use tauri_plugin_shell::ShellExt; - #[allow(deprecated)] - { - let _ = app.shell().open(DISCORD_INVITE_URL, None); - } - return; - } - - if id == MENU_ITEM_ABOUT_ID { - dispatch_menu_action(app, "about"); - return; - } - if id == MENU_ITEM_SETTINGS_ID { - dispatch_menu_action(app, "settings"); - return; - } - if id == MENU_ITEM_COMMAND_PALETTE_ID { - dispatch_menu_action(app, "command-palette"); - return; - } - if id == MENU_ITEM_NEW_SESSION_ID { - dispatch_menu_action(app, "new-session"); - return; - } - if id == MENU_ITEM_WORKTREE_CREATOR_ID { - dispatch_menu_action(app, "new-worktree-session"); - return; - } - if id == MENU_ITEM_CHANGE_WORKSPACE_ID { - dispatch_menu_action(app, "change-workspace"); - return; - } - - if id == MENU_ITEM_OPEN_GIT_TAB_ID { - dispatch_menu_action(app, "open-git-tab"); - return; - } - if id == MENU_ITEM_OPEN_DIFF_TAB_ID { - dispatch_menu_action(app, "open-diff-tab"); - return; - } - - if id == MENU_ITEM_OPEN_FILES_TAB_ID { - dispatch_menu_action(app, "open-files-tab"); - return; - } - if id == MENU_ITEM_OPEN_TERMINAL_TAB_ID { - dispatch_menu_action(app, "open-terminal-tab"); - return; - } - if id == MENU_ITEM_COPY_ID { - dispatch_menu_action(app, "copy"); - return; - } - - if id == MENU_ITEM_THEME_LIGHT_ID { - dispatch_menu_action(app, "theme-light"); - return; - } - if id == MENU_ITEM_THEME_DARK_ID { - dispatch_menu_action(app, "theme-dark"); - return; - } - if id == MENU_ITEM_THEME_SYSTEM_ID { - dispatch_menu_action(app, "theme-system"); - return; - } - - if id == MENU_ITEM_TOGGLE_SIDEBAR_ID { - dispatch_menu_action(app, "toggle-sidebar"); - return; - } - if id == MENU_ITEM_TOGGLE_MEMORY_DEBUG_ID { - dispatch_menu_action(app, "toggle-memory-debug"); - return; - } - - if id == MENU_ITEM_HELP_DIALOG_ID { - dispatch_menu_action(app, "help-dialog"); - return; - } - if id == MENU_ITEM_DOWNLOAD_LOGS_ID { - dispatch_menu_action(app, "download-logs"); - return; - } - if id == MENU_ITEM_CLEAR_CACHE_ID { - let app = app.clone(); - tauri::async_runtime::spawn_blocking(move || { - let _ = crate::desktop_clear_cache(app); - }); - return; - } - if id == MENU_ITEM_QUIT_ID { - request_quit_with_confirmation(app); - return; - } - } - }) - .on_window_event(|window, event| { - let app = window.app_handle(); - let label = window.label().to_string(); - - if let tauri::WindowEvent::Focused(focused) = event { - if let Some(state) = app.try_state::() { - state.set_focused(&label, *focused); - } - } - - if let tauri::WindowEvent::Destroyed = event { - if let Some(state) = app.try_state::() { - state.remove_window(&label); - } - - if let Some(state) = app.try_state::() { - state - .scripts - .lock() - .expect("desktop ui injection mutex") - .remove(&label); - } - } - - if matches!(event, tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_)) { - schedule_window_state_persist(window.clone(), false); - } - - if let tauri::WindowEvent::CloseRequested { api, .. } = event { - schedule_window_state_persist(window.clone(), true); - - let remaining_visible = app - .webview_windows() - .values() - .filter(|w| w.is_visible().unwrap_or(false)) - .count(); - - if remaining_visible <= 1 { - api.prevent_close(); - let _ = window.hide(); - } - } - }) - .invoke_handler(tauri::generate_handler![ - desktop_notify, - desktop_check_for_updates, - desktop_download_and_install_update, - desktop_restart, - desktop_new_window, - desktop_new_window_at_url, - desktop_clear_cache, - desktop_open_path, - desktop_open_in_app, - desktop_filter_installed_apps, - desktop_get_installed_apps, - desktop_fetch_app_icons, - desktop_save_markdown_file, - desktop_hosts_get, - desktop_hosts_set, - desktop_host_probe, - desktop_set_window_theme, - desktop_get_lan_address, - remote_ssh::desktop_ssh_instances_get, - remote_ssh::desktop_ssh_instances_set, - remote_ssh::desktop_ssh_import_hosts, - remote_ssh::desktop_ssh_connect, - remote_ssh::desktop_ssh_disconnect, - remote_ssh::desktop_ssh_status, - remote_ssh::desktop_ssh_logs, - remote_ssh::desktop_ssh_logs_clear, - desktop_read_file, - ]) - .setup(|app| { - let handle = app.handle().clone(); - - if let Err(err) = create_startup_window(&handle, true) { - log::error!("[desktop] failed to create startup window: {err}"); - } - - tauri::async_runtime::spawn(async move { - // Helper: inject a fallback boot outcome when the local server - // cannot start, so the UI leaves the splash and shows - // chooser/recovery instead of hanging on a white screen. - let handle_for_fallback = handle.clone(); - let inject_startup_failure = |err: String| { - log::error!("[desktop] failed to start local server: {err}"); - let cfg = read_desktop_hosts_config_from_disk(); - let boot_outcome = compute_local_startup_failure_boot_outcome(&cfg); - let init_script = build_startup_failure_init_script(&boot_outcome); - if let Some(state) = handle_for_fallback.try_state::() - { - state - .scripts - .lock() - .expect("desktop ui injection mutex") - .insert("main".to_string(), init_script.clone()); - } - if let Some(window) = handle_for_fallback.get_webview_window("main") { - let _ = window.eval(&init_script); - } - }; - - let local_url = if cfg!(debug_assertions) { - let dev_url = "http://127.0.0.1:3901".to_string(); - if wait_for_health(&dev_url).await { - dev_url.to_string() - } else { - match spawn_local_server(&handle).await { - Ok(local) => local, - Err(err) => { - inject_startup_failure(err.to_string()); - return; - } - } - } - } else { - match spawn_local_server(&handle).await { - Ok(local) => local, - Err(err) => { - inject_startup_failure(err.to_string()); - return; - } - } - }; - - let local_ui_url = if cfg!(debug_assertions) { - let vite_url = "http://127.0.0.1:5173"; - if wait_for_health(vite_url).await { - vite_url.to_string() - } else { - log::warn!("[desktop] Vite dev server not ready, using local API UI at {local_url}"); - local_url.clone() - } - } else { - local_url.clone() - }; - - // Ensure local URL is always available to desktop commands, - // even when we are using the Vite dev server (no sidecar child). - if let Some(state) = handle.try_state::() { - *state.url.lock().expect("sidecar url mutex") = Some(local_url.clone()); - } - start_quit_risk_poller(local_url.clone()); - - let local_origin = url::Url::parse(&local_ui_url) - .ok() - .map(|u| u.origin().ascii_serialization()) - .unwrap_or_else(|| local_ui_url.clone()); - - // Selected host: env override first, then desktop default host, else local. - // If env override points to the local server, ignore it and use - // config-based resolution instead. - let env_target = std::env::var("OPENCHAMBER_SERVER_URL") - .ok() - .and_then(|raw| normalize_server_url(&raw)) - .filter(|url| !same_server_url(url, &local_ui_url)); - - let mut initial_url = env_target.as_deref().unwrap_or(&local_ui_url).to_string(); - - // Compute boot outcome and legacy-upgrade if needed. - let cfg = read_desktop_hosts_config_from_disk(); - - if env_target.is_none() { - if let Some(default_id) = cfg.default_host_id.as_deref() { - if default_id != LOCAL_HOST_ID { - if let Some(host) = cfg.hosts.iter().find(|h| h.id == default_id) { - initial_url = host.url.clone(); - } - } - } - } - - // If remote, probe and fall back to local if unreachable. - // Use the shared probe_with_retry policy (soft + hard). - let final_probe: Option = if !same_server_url(&initial_url, &local_ui_url) { - let result = probe_with_retry(&initial_url).await; - - if !result.navigable { - log::warn!( - "[desktop] startup host unreachable after retries ({}), falling back to local ({})", - initial_url, - local_ui_url - ); - initial_url = local_ui_url.clone(); - } - - result.probe - } else { - None - }; - - // Probe the local server to verify opencode is actually running. - // spawn_local_server only confirms the sidecar web server responded - // HTTP 200 — it does not check whether opencode CLI is ready. - let local_available = match wait_for_local_opencode_ready_with( - &local_url, - LOCAL_SIDECAR_HEALTH_TIMEOUT, - LOCAL_SIDECAR_HEALTH_POLL_INITIAL_INTERVAL, - LOCAL_SIDECAR_HEALTH_POLL_MAX_INTERVAL, - ) - .await - { - Some(probe) if matches!(probe.status.as_str(), "ok" | "auth") => { - log::info!("[desktop] local opencode verified (status={})", probe.status); - true - } - Some(probe) => { - log::warn!( - "[desktop] local server up but opencode not ready (status={}), treating as unavailable", - probe.status - ); - false - } - None => { - log::warn!("[desktop] local opencode probe failed, treating as unavailable"); - false - } - }; - - let boot_outcome = resolve_boot_outcome( - &cfg, - final_probe.as_ref(), - local_available, - env_target.as_deref(), - ); - - if let Err(err) = activate_main_window( - &handle, - &initial_url, - &local_origin, - Some(&boot_outcome), - ) { - log::error!("[desktop] failed to activate main window: {err}"); - } - }); - - Ok(()) - }) - ; - - let app = builder - .build(tauri::generate_context!()) - .expect("failed to build Tauri application"); - - install_macos_quit_confirmation_hook(); - - app.run(|app_handle, event| { - match event { - tauri::RunEvent::ExitRequested { api, .. } => { - use std::sync::atomic::Ordering; - if !QUIT_CONFIRMED.load(Ordering::SeqCst) { - api.prevent_exit(); - #[cfg(target_os = "macos")] - request_quit_with_confirmation(app_handle); - return; - } - if let Some(state) = app_handle.try_state::() { - state.shutdown_all(app_handle); - } - kill_sidecar(app_handle.clone()); - } - tauri::RunEvent::Exit => { - if let Some(state) = app_handle.try_state::() { - state.shutdown_all(app_handle); - } - kill_sidecar(app_handle.clone()); - } - #[cfg(target_os = "macos")] - tauri::RunEvent::Reopen { - has_visible_windows, - .. - } => { - if !has_visible_windows { - let windows = app_handle.webview_windows(); - let hidden = windows - .values() - .find(|w| !w.is_visible().unwrap_or(true)); - if let Some(w) = hidden { - let _ = w.show(); - let _ = w.set_focus(); - } else { - drop(windows); - open_new_window(app_handle); - } - } - } - _ => {} - } - }); -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }; - use std::time::{SystemTime, UNIX_EPOCH}; - - fn unique_settings_path(test_name: &str) -> PathBuf { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock drift") - .as_nanos(); - std::env::temp_dir().join(format!("openchamber-{test_name}-{nanos}-settings.json")) - } - - #[test] - fn sanitize_host_url_for_storage_keeps_query_params() { - let input = "https://example.com?coder_session_token=xxxxxx"; - let sanitized = sanitize_host_url_for_storage(input).expect("sanitized url"); - assert_eq!(sanitized, "https://example.com/?coder_session_token=xxxxxx"); - } - - #[test] - fn sanitize_host_url_for_storage_strips_fragment_and_keeps_query() { - let input = "https://example.com/workspace?coder_session_token=xxxxxx#ignored"; - let sanitized = sanitize_host_url_for_storage(input).expect("sanitized url"); - assert_eq!( - sanitized, - "https://example.com/workspace?coder_session_token=xxxxxx" - ); - } - - #[test] - fn write_and_read_hosts_config_preserves_query_params() { - let path = unique_settings_path("desktop-hosts-query"); - let config = DesktopHostsConfig { - hosts: vec![DesktopHost { - id: "remote-1".to_string(), - label: "Remote".to_string(), - url: "https://example.com?coder_session_token=xxxxxx".to_string(), - }], - default_host_id: Some("remote-1".to_string()), - initial_host_choice_completed: false, - }; - - write_desktop_hosts_config_to_path(&path, &config).expect("write config"); - let read_back = read_desktop_hosts_config_from_path(&path); - let _ = fs::remove_file(&path); - - assert_eq!(read_back.hosts.len(), 1); - assert_eq!( - read_back.hosts[0].url, - "https://example.com/?coder_session_token=xxxxxx" - ); - assert_eq!(read_back.default_host_id.as_deref(), Some("remote-1")); - } - - #[test] - fn read_hosts_config_defaults_initial_choice_flag_to_false() { - let path = unique_settings_path("desktop-hosts-default-flag"); - std::fs::write(&path, r#"{"desktopHosts":[],"desktopDefaultHostId":null}"#).unwrap(); - - let cfg = read_desktop_hosts_config_from_path(&path); - let _ = fs::remove_file(&path); - assert_eq!(cfg.initial_host_choice_completed, false); - } - - #[test] - fn write_and_read_hosts_config_preserves_initial_choice_flag() { - let path = unique_settings_path("desktop-hosts-preserve-flag"); - let cfg = DesktopHostsConfig { - hosts: vec![], - default_host_id: Some(LOCAL_HOST_ID.to_string()), - initial_host_choice_completed: true, - }; - - write_desktop_hosts_config_to_path(&path, &cfg).unwrap(); - let reread = read_desktop_hosts_config_from_path(&path); - let _ = fs::remove_file(&path); - - assert_eq!(reread.default_host_id.as_deref(), Some(LOCAL_HOST_ID)); - assert!(reread.initial_host_choice_completed); - } - - #[test] - fn omitted_initial_choice_flag_preserves_stored_true() { - let path = unique_settings_path("desktop-hosts-omit-preserves"); - - // Seed: write config with initialHostChoiceCompleted = true - let seed = DesktopHostsConfig { - hosts: vec![DesktopHost { - id: "remote-1".to_string(), - label: "Remote".to_string(), - url: "https://example.com".to_string(), - }], - default_host_id: Some("remote-1".to_string()), - initial_host_choice_completed: true, - }; - write_desktop_hosts_config_to_path(&path, &seed).unwrap(); - - // Call the production merge-and-write path with omitted field - let input = DesktopHostsConfigInput { - hosts: vec![], - default_host_id: Some("local".to_string()), - initial_host_choice_completed: None, - }; - write_desktop_hosts_config_input_to_path(&path, &input).unwrap(); - - let reread = read_desktop_hosts_config_from_path(&path); - let _ = fs::remove_file(&path); - - // The stored true must be preserved, not reset to false - assert!(reread.initial_host_choice_completed); - } - - // --- Task 2: probe validation tests --- - - /// Spawn a tiny HTTP server on a random port that responds with `status_code` - /// and `body`. Returns the base URL (e.g. `http://127.0.0.1:{port}`). - async fn spawn_test_http_server(status_code: u16, body: &str) -> String { - use tokio::net::TcpListener; - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind test server"); - let port = listener.local_addr().unwrap().port(); - let body_owned = body.to_string(); - - tokio::spawn(async move { - loop { - let (mut stream, _) = tokio::select! { - res = listener.accept() => { res.expect("accept") } - else => break, - }; - use tokio::io::AsyncWriteExt; - let response = format!( - "HTTP/1.1 {status_code} OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body_owned}", - body_owned.len() - ); - let _ = stream.write_all(response.as_bytes()).await; - } - }); - - format!("http://127.0.0.1:{port}") - } - - #[tokio::test] - async fn probe_returns_wrong_service_for_generic_http_200_health() { - let url = spawn_test_http_server(200, r#"{"status":"ok","uptime":42}"#).await; - // Give the server a moment to start listening - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - - let result = probe_host_with_timeout(&url, Duration::from_secs(2)) - .await - .expect("probe should not error"); - assert_eq!(result.status, "wrong-service"); - } - - #[tokio::test] - async fn probe_returns_ok_for_valid_openchamber_health_payload() { - let url = spawn_test_http_server(200, r#"{"openCodeRunning":true}"#).await; - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - - let result = probe_host_with_timeout(&url, Duration::from_secs(2)) - .await - .expect("probe should not error"); - assert_eq!(result.status, "ok"); - } - - #[tokio::test] - async fn probe_returns_auth_for_401_health() { - let url = spawn_test_http_server(401, r#"{"message":"unauthorized"}"#).await; - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - - let result = probe_host_with_timeout(&url, Duration::from_secs(2)) - .await - .expect("probe should not error"); - assert_eq!(result.status, "auth"); - } - - async fn spawn_flaky_openchamber_health_server() -> String { - use tokio::net::TcpListener; - - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind flaky test server"); - let port = listener.local_addr().unwrap().port(); - let request_count = Arc::new(AtomicUsize::new(0)); - - tokio::spawn({ - let request_count = Arc::clone(&request_count); - async move { - loop { - let (mut stream, _) = tokio::select! { - res = listener.accept() => { res.expect("accept") } - else => break, - }; - - let count = request_count.fetch_add(1, Ordering::SeqCst); - let body = if count == 0 { - r#"{"status":"ok","openCodeRunning":false,"isOpenCodeReady":false}"# - } else { - r#"{"status":"ok","openCodeRunning":true,"isOpenCodeReady":true}"# - }; - - use tokio::io::AsyncWriteExt; - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len() - ); - let _ = stream.write_all(response.as_bytes()).await; - } - } - }); - - format!("http://127.0.0.1:{port}") - } - - #[tokio::test] - async fn wait_for_local_opencode_ready_retries_until_health_payload_is_ready() { - let url = spawn_flaky_openchamber_health_server().await; - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - - let result = wait_for_local_opencode_ready_with( - &url, - Duration::from_millis(200), - Duration::from_millis(10), - Duration::from_millis(20), - ) - .await - .expect("probe result"); - - assert_eq!(result.status, "ok"); - } - - #[tokio::test] - async fn wait_for_local_opencode_ready_returns_last_probe_when_server_never_becomes_ready() { - let url = spawn_test_http_server( - 200, - r#"{"status":"ok","openCodeRunning":false,"isOpenCodeReady":false}"#, - ) - .await; - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - - let result = wait_for_local_opencode_ready_with( - &url, - Duration::from_millis(120), - Duration::from_millis(10), - Duration::from_millis(20), - ) - .await - .expect("probe result"); - - assert_eq!(result.status, "wrong-service"); - } - - // --- Task 3: boot outcome resolution tests --- - - fn make_config( - hosts: Vec<(&str, &str, &str)>, - default_host_id: Option<&str>, - initial_host_choice_completed: bool, - ) -> DesktopHostsConfig { - DesktopHostsConfig { - hosts: hosts - .into_iter() - .map(|(id, label, url)| DesktopHost { - id: id.to_string(), - label: label.to_string(), - url: url.to_string(), - }) - .collect(), - default_host_id: default_host_id.map(|s| s.to_string()), - initial_host_choice_completed, - } - } - - #[test] - fn resolve_boot_outcome_returns_first_launch_when_no_default_and_choice_not_completed() { - let cfg = make_config(vec![], None, false); - let probe: Option<&HostProbeResult> = None; - let outcome = resolve_boot_outcome(&cfg, probe, true, None); - assert_eq!(outcome.target, None); - assert_eq!(outcome.status, "not-configured"); - } - - #[test] - fn resolve_boot_outcome_returns_recovery_no_default_host_when_choice_completed_but_default_missing() { - let cfg = make_config( - vec![("remote-a", "Remote A", "https://a.test")], - None, - true, - ); - let probe: Option<&HostProbeResult> = None; - let outcome = resolve_boot_outcome(&cfg, probe, true, None); - assert_eq!(outcome.target, None); - assert_eq!(outcome.status, "not-configured"); - } - - #[test] - fn resolve_boot_outcome_returns_recovery_missing_default_host_when_default_id_has_no_matching_host() { - let cfg = make_config(vec![], Some("gone-1"), true); - let probe: Option<&HostProbeResult> = None; - let outcome = resolve_boot_outcome(&cfg, probe, true, None); - assert_eq!(outcome.target, Some("remote".to_string())); - assert_eq!(outcome.status, "missing"); - assert_eq!(outcome.host_id.as_deref(), Some("gone-1")); - } - - #[test] - fn resolve_boot_outcome_returns_main_local_when_default_is_local_and_available() { - let cfg = make_config(vec![], Some("local"), true); - let probe: Option<&HostProbeResult> = None; - let outcome = resolve_boot_outcome(&cfg, probe, true, None); - assert_eq!(outcome.target, Some("local".to_string())); - assert_eq!(outcome.status, "ok"); - } - - #[test] - fn resolve_boot_outcome_returns_recovery_local_unavailable_when_local_is_default_but_unavailable() { - let cfg = make_config(vec![], Some("local"), true); - let probe: Option<&HostProbeResult> = None; - let outcome = resolve_boot_outcome(&cfg, probe, false, None); - assert_eq!(outcome.target, Some("local".to_string())); - assert_eq!(outcome.status, "unreachable"); - } - - #[test] - fn resolve_boot_outcome_returns_main_remote_when_probe_is_ok() { - let cfg = make_config( - vec![("remote-a", "Remote A", "https://a.test")], - Some("remote-a"), - true, - ); - let probe = HostProbeResult { - status: "ok".to_string(), - latency_ms: 10, - }; - let outcome = resolve_boot_outcome(&cfg, Some(&probe), true, None); - assert_eq!(outcome.target, Some("remote".to_string())); - assert_eq!(outcome.status, "ok"); - assert_eq!(outcome.host_id.as_deref(), Some("remote-a")); - assert_eq!(outcome.url.as_deref(), Some("https://a.test")); - } - - #[test] - fn resolve_boot_outcome_returns_main_remote_when_probe_is_auth() { - let cfg = make_config( - vec![("remote-a", "Remote A", "https://a.test")], - Some("remote-a"), - true, - ); - let probe = HostProbeResult { - status: "auth".to_string(), - latency_ms: 10, - }; - let outcome = resolve_boot_outcome(&cfg, Some(&probe), true, None); - assert_eq!(outcome.target, Some("remote".to_string())); - assert_eq!(outcome.status, "ok"); - } - - #[test] - fn resolve_boot_outcome_returns_recovery_remote_unreachable_when_probe_fails() { - let cfg = make_config( - vec![("remote-a", "Remote A", "https://a.test")], - Some("remote-a"), - true, - ); - let probe = HostProbeResult { - status: "unreachable".to_string(), - latency_ms: 2000, - }; - let outcome = resolve_boot_outcome(&cfg, Some(&probe), true, None); - assert_eq!(outcome.target, Some("remote".to_string())); - assert_eq!(outcome.status, "unreachable"); - assert_eq!(outcome.host_id.as_deref(), Some("remote-a")); - } - - #[test] - fn resolve_boot_outcome_returns_recovery_remote_wrong_service_when_probe_says_wrong_service() { - let cfg = make_config( - vec![("remote-a", "Remote A", "https://a.test")], - Some("remote-a"), - true, - ); - let probe = HostProbeResult { - status: "wrong-service".to_string(), - latency_ms: 50, - }; - let outcome = resolve_boot_outcome(&cfg, Some(&probe), true, None); - assert_eq!(outcome.target, Some("remote".to_string())); - assert_eq!(outcome.status, "wrong-service"); - assert_eq!(outcome.host_id.as_deref(), Some("remote-a")); - } - - #[test] - fn resolve_boot_outcome_no_probe_but_remote_default_returns_unreachable() { - // Remote default but no probe result yet — treat as unreachable - // (probe hasn't happened yet, but user has already chosen a remote) - let cfg = make_config( - vec![("remote-a", "Remote A", "https://a.test")], - Some("remote-a"), - false, - ); - let probe: Option<&HostProbeResult> = None; - let outcome = resolve_boot_outcome(&cfg, probe, true, None); - assert_eq!(outcome.target, Some("remote".to_string())); - assert_eq!(outcome.status, "unreachable"); - assert_eq!(outcome.host_id.as_deref(), Some("remote-a")); - } - - // --- Startup failure fallback boot outcome tests --- - - #[test] - fn startup_failure_returns_recovery_local_unavailable_when_default_is_local() { - let cfg = make_config(vec![], Some("local"), true); - let outcome = compute_local_startup_failure_boot_outcome(&cfg); - assert_eq!(outcome.target, Some("local".to_string())); - assert_eq!(outcome.status, "unreachable"); - } - - #[test] - fn startup_failure_returns_first_launch_when_no_default_and_choice_not_completed() { - let cfg = make_config(vec![], None, false); - let outcome = compute_local_startup_failure_boot_outcome(&cfg); - assert_eq!(outcome.target, None); - assert_eq!(outcome.status, "not-configured"); - } - - #[test] - fn startup_failure_returns_recovery_no_default_host_when_choice_completed_but_no_default() { - let cfg = make_config(vec![], None, true); - let outcome = compute_local_startup_failure_boot_outcome(&cfg); - assert_eq!(outcome.target, None); - assert_eq!(outcome.status, "not-configured"); - } - - #[test] - fn startup_failure_never_returns_main_outcome() { - // When the local server fails to start, the fallback outcome must - // never be a "main-*" variant because the startup-failure path - // only injects globals into the already-open startup window — it - // does NOT navigate to a remote URL. A "main-*" outcome would - // gate splash dismissal on initialization and hang. - let cfg = make_config(vec![], None, false); - let outcome = compute_local_startup_failure_boot_outcome(&cfg); - assert!( - outcome.status != "ok", - "startup failure fallback must not return main-* outcome, got: {:?}", - outcome - ); - } - - #[test] - fn startup_failure_init_script_contains_boot_outcome_json() { - let cfg = make_config(vec![], Some("local"), true); - let outcome = compute_local_startup_failure_boot_outcome(&cfg); - let script = build_startup_failure_init_script(&outcome); - // The script must contain the serialized boot outcome JSON. - assert!( - script.contains(r#""target":"local""#) && script.contains(r#""status":"unreachable""#), - "init script should embed the structured boot outcome" - ); - // It must also set __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__ - assert!( - script.contains("__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__"), - "init script must set __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__" - ); - } -} diff --git a/packages/desktop/src-tauri/src/remote_ssh.rs b/packages/desktop/src-tauri/src/remote_ssh.rs deleted file mode 100644 index af37c8d6..00000000 --- a/packages/desktop/src-tauri/src/remote_ssh.rs +++ /dev/null @@ -1,3002 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::{ - collections::{HashMap, HashSet}, - fs, - io::Read, - net::{TcpListener, TcpStream}, - path::{Path, PathBuf}, - process::{Child, Command, Stdio}, - sync::{Arc, Mutex}, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; -use tauri::{AppHandle, Emitter, State}; - -const LOCAL_HOST_ID: &str = "local"; -const SSH_STATUS_EVENT: &str = "openchamber:ssh-instance-status"; -const DEFAULT_CONNECTION_TIMEOUT_SEC: u16 = 60; -const DEFAULT_LOCAL_BIND_HOST: &str = "127.0.0.1"; -const DEFAULT_CONTROL_PERSIST_SEC: u16 = 300; -const DEFAULT_READY_TIMEOUT_SEC: u64 = 30; -const DEFAULT_RECONNECT_MAX_ATTEMPTS: u32 = 5; -const MAX_LOG_LINES_PER_INSTANCE: usize = 1200; - -/// Monitor starts with fast polling and relaxes to steady-state after stabilization. -const MONITOR_INITIAL_POLL_SECS: u64 = 2; -const MONITOR_STEADY_POLL_SECS: u64 = 10; -/// Number of healthy ticks before switching from initial to steady-state polling. -const MONITOR_STABILIZE_TICKS: u32 = 5; - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshInstancesConfig { - pub instances: Vec, -} - -impl Default for DesktopSshInstancesConfig { - fn default() -> Self { - Self { - instances: Vec::new(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshParsedCommand { - pub destination: String, - pub args: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum DesktopSshRemoteMode { - Managed, - External, -} - -impl Default for DesktopSshRemoteMode { - fn default() -> Self { - Self::Managed - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum DesktopSshInstallMethod { - Npm, - Bun, - DownloadRelease, - UploadBundle, -} - -impl Default for DesktopSshInstallMethod { - fn default() -> Self { - Self::Bun - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshRemoteOpenchamberConfig { - #[serde(default)] - pub mode: DesktopSshRemoteMode, - #[serde(default = "default_true")] - pub keep_running: bool, - pub preferred_port: Option, - #[serde(default)] - pub install_method: DesktopSshInstallMethod, - #[serde(default)] - pub upload_bundle_over_ssh: bool, -} - -impl Default for DesktopSshRemoteOpenchamberConfig { - fn default() -> Self { - Self { - mode: DesktopSshRemoteMode::Managed, - keep_running: true, - preferred_port: None, - install_method: DesktopSshInstallMethod::Bun, - upload_bundle_over_ssh: false, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshLocalForwardConfig { - pub preferred_local_port: Option, - #[serde(default = "default_local_bind_host")] - pub bind_host: String, -} - -impl Default for DesktopSshLocalForwardConfig { - fn default() -> Self { - Self { - preferred_local_port: None, - bind_host: default_local_bind_host(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum DesktopSshSecretStore { - Never, - Settings, -} - -impl Default for DesktopSshSecretStore { - fn default() -> Self { - Self::Never - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshStoredSecret { - #[serde(default)] - pub enabled: bool, - pub value: Option, - #[serde(default)] - pub store: DesktopSshSecretStore, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshAuthConfig { - pub ssh_password: Option, - pub openchamber_password: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum DesktopSshPortForwardType { - Local, - Remote, - Dynamic, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshPortForward { - pub id: String, - #[serde(default = "default_true")] - pub enabled: bool, - #[serde(rename = "type")] - pub forward_type: DesktopSshPortForwardType, - pub local_host: Option, - pub local_port: Option, - pub remote_host: Option, - pub remote_port: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshInstance { - pub id: String, - pub nickname: Option, - pub ssh_command: String, - pub ssh_parsed: Option, - #[serde(default = "default_connection_timeout")] - pub connection_timeout_sec: u16, - #[serde(default)] - pub remote_openchamber: DesktopSshRemoteOpenchamberConfig, - #[serde(default)] - pub local_forward: DesktopSshLocalForwardConfig, - #[serde(default)] - pub auth: DesktopSshAuthConfig, - #[serde(default)] - pub port_forwards: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum DesktopSshPhase { - Idle, - ConfigResolved, - AuthCheck, - MasterConnecting, - RemoteProbe, - Installing, - Updating, - ServerDetecting, - ServerStarting, - Forwarding, - Ready, - Degraded, - Error, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshInstanceStatus { - pub id: String, - pub phase: DesktopSshPhase, - pub detail: Option, - pub local_url: Option, - pub local_port: Option, - pub remote_port: Option, - #[serde(default)] - pub started_by_us: bool, - #[serde(default)] - pub retry_attempt: u32, - #[serde(default)] - pub requires_user_action: bool, - pub updated_at_ms: u64, -} - -impl DesktopSshInstanceStatus { - fn idle(id: impl Into) -> Self { - Self { - id: id.into(), - phase: DesktopSshPhase::Idle, - detail: None, - local_url: None, - local_port: None, - remote_port: None, - started_by_us: false, - retry_attempt: 0, - requires_user_action: false, - updated_at_ms: now_millis(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DesktopSshImportCandidate { - pub host: String, - pub pattern: bool, - pub source: String, - pub ssh_command: String, -} - -#[derive(Default)] -struct DesktopSshManagerInner { - statuses: Mutex>, - logs: Mutex>>, - sessions: Mutex>, - connect_tasks: Mutex>>, - monitor_tasks: Mutex>>, - reconnect_attempts: Mutex>, - connect_attempts: Mutex>, -} - -struct SshSession { - instance: DesktopSshInstance, - parsed: DesktopSshParsedCommand, - session_dir: PathBuf, - control_path: PathBuf, - local_port: u16, - remote_port: u16, - started_by_us: bool, - master: Child, - master_detached: bool, - main_forward: Child, - main_forward_detached: bool, - extra_forwards: Vec, -} - -#[derive(Default)] -pub struct DesktopSshManagerState { - inner: Arc, -} - -#[derive(Clone, Debug, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RemoteSystemInfo { - openchamber_version: Option, - runtime: Option, - pid: Option, - started_at: Option, -} - -fn default_true() -> bool { - true -} - -fn default_connection_timeout() -> u16 { - DEFAULT_CONNECTION_TIMEOUT_SEC -} - -fn default_local_bind_host() -> String { - DEFAULT_LOCAL_BIND_HOST.to_string() -} - -fn now_millis() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0) -} - -fn settings_file_path() -> PathBuf { - if let Ok(dir) = std::env::var("OPENCHAMBER_DATA_DIR") { - if !dir.trim().is_empty() { - return PathBuf::from(dir.trim()).join("settings.json"); - } - } - let home = std::env::var("HOME").unwrap_or_default(); - PathBuf::from(home) - .join(".config") - .join("openchamber") - .join("settings.json") -} - -fn read_settings_root(path: &Path) -> Value { - let raw = fs::read_to_string(path).unwrap_or_default(); - let parsed = serde_json::from_str::(&raw).unwrap_or_else(|_| json!({})); - if parsed.is_object() { - parsed - } else { - json!({}) - } -} - -fn write_settings_root(path: &Path, root: &Value) -> Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - fs::write(path, serde_json::to_string_pretty(root)?)?; - Ok(()) -} - -fn build_display_label(instance: &DesktopSshInstance) -> String { - if let Some(nick) = instance - .nickname - .as_ref() - .map(|v| v.trim()) - .filter(|v| !v.is_empty()) - { - return nick.to_string(); - } - if let Some(parsed) = instance.ssh_parsed.as_ref() { - let destination = parsed.destination.trim(); - if !destination.is_empty() { - return destination.to_string(); - } - } - instance.id.clone() -} - -fn read_desktop_ssh_instances_from_path(path: &Path) -> DesktopSshInstancesConfig { - let root = read_settings_root(path); - let Some(items) = root - .get("desktopSshInstances") - .and_then(Value::as_array) - .cloned() - else { - return DesktopSshInstancesConfig::default(); - }; - - let mut instances = Vec::new(); - let mut seen = HashSet::new(); - for item in items { - let Ok(mut instance) = serde_json::from_value::(item) else { - continue; - }; - - let id = instance.id.trim().to_string(); - if id.is_empty() || id == LOCAL_HOST_ID || seen.contains(&id) { - continue; - } - instance.id = id.clone(); - instance.connection_timeout_sec = if instance.connection_timeout_sec == 0 { - DEFAULT_CONNECTION_TIMEOUT_SEC - } else { - instance.connection_timeout_sec - }; - if instance.local_forward.bind_host.trim().is_empty() { - instance.local_forward.bind_host = default_local_bind_host(); - } - if instance.ssh_parsed.is_none() { - if let Ok(parsed) = parse_ssh_command(&instance.ssh_command) { - instance.ssh_parsed = Some(parsed); - } - } - seen.insert(id); - instances.push(instance); - } - - DesktopSshInstancesConfig { instances } -} - -fn read_desktop_ssh_instances_from_disk() -> DesktopSshInstancesConfig { - read_desktop_ssh_instances_from_path(&settings_file_path()) -} - -fn sanitize_bind_host(raw: &str) -> String { - let trimmed = raw.trim(); - if trimmed.is_empty() { - return DEFAULT_LOCAL_BIND_HOST.to_string(); - } - match trimmed { - "127.0.0.1" | "localhost" | "0.0.0.0" => trimmed.to_string(), - _ => DEFAULT_LOCAL_BIND_HOST.to_string(), - } -} - -fn sanitize_forward(forward: &DesktopSshPortForward) -> Option { - let id = forward.id.trim().to_string(); - if id.is_empty() { - return None; - } - - let mut normalized = forward.clone(); - normalized.id = id; - normalized.local_host = normalized - .local_host - .as_ref() - .map(|v| sanitize_bind_host(v)) - .or_else(|| Some(DEFAULT_LOCAL_BIND_HOST.to_string())); - - match normalized.forward_type { - DesktopSshPortForwardType::Local => { - if normalized.local_port.is_none() || normalized.remote_port.is_none() { - return None; - } - if normalized - .remote_host - .as_ref() - .map(|v| v.trim()) - .unwrap_or("") - .is_empty() - { - normalized.remote_host = Some("127.0.0.1".to_string()); - } - } - DesktopSshPortForwardType::Remote => { - if normalized.local_port.is_none() || normalized.remote_port.is_none() { - return None; - } - if normalized - .remote_host - .as_ref() - .map(|v| v.trim()) - .unwrap_or("") - .is_empty() - { - normalized.remote_host = Some("127.0.0.1".to_string()); - } - if normalized - .local_host - .as_ref() - .map(|v| v.trim()) - .unwrap_or("") - .is_empty() - { - normalized.local_host = Some("127.0.0.1".to_string()); - } - } - DesktopSshPortForwardType::Dynamic => { - if normalized.local_port.is_none() { - return None; - } - normalized.remote_host = None; - normalized.remote_port = None; - } - } - - Some(normalized) -} - -fn sanitize_instance(mut instance: DesktopSshInstance) -> Result { - instance.id = instance.id.trim().to_string(); - if instance.id.is_empty() || instance.id == LOCAL_HOST_ID { - return Err(anyhow!("SSH instance id is required")); - } - instance.ssh_command = instance.ssh_command.trim().to_string(); - if instance.ssh_command.is_empty() { - return Err(anyhow!("SSH command is required")); - } - if instance.connection_timeout_sec == 0 { - instance.connection_timeout_sec = DEFAULT_CONNECTION_TIMEOUT_SEC; - } - instance.local_forward.bind_host = sanitize_bind_host(&instance.local_forward.bind_host); - let parsed = parse_ssh_command(&instance.ssh_command)?; - instance.ssh_parsed = Some(parsed); - - let mut seen = HashSet::new(); - let mut forwards = Vec::new(); - for forward in &instance.port_forwards { - let Some(normalized) = sanitize_forward(forward) else { - continue; - }; - if seen.contains(&normalized.id) { - continue; - } - seen.insert(normalized.id.clone()); - forwards.push(normalized); - } - instance.port_forwards = forwards; - - Ok(instance) -} - -fn sync_desktop_hosts_for_ssh( - root: &mut Value, - previous_ids: &HashSet, - instances: &[DesktopSshInstance], -) { - let next_ids: HashSet = instances.iter().map(|item| item.id.clone()).collect(); - - let mut hosts = root - .get("desktopHosts") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default(); - - hosts.retain(|entry| { - let id = entry - .get("id") - .and_then(Value::as_str) - .map(|value| value.trim()) - .unwrap_or(""); - if id.is_empty() { - return false; - } - !(previous_ids.contains(id) && !next_ids.contains(id)) - }); - - for instance in instances { - let label = build_display_label(instance); - let mut found = false; - for host in &mut hosts { - let host_id = host - .get("id") - .and_then(Value::as_str) - .map(|value| value.trim()) - .unwrap_or(""); - if host_id != instance.id { - continue; - } - if let Some(obj) = host.as_object_mut() { - obj.insert("id".to_string(), Value::String(instance.id.clone())); - obj.insert("label".to_string(), Value::String(label.clone())); - let should_set_default_url = obj - .get("url") - .and_then(Value::as_str) - .map(|value| value.trim().is_empty()) - .unwrap_or(true); - if should_set_default_url { - obj.insert( - "url".to_string(), - Value::String("http://127.0.0.1/".to_string()), - ); - } - } - found = true; - break; - } - - if !found { - hosts.push(json!({ - "id": instance.id, - "label": label, - "url": "http://127.0.0.1/" - })); - } - } - - root["desktopHosts"] = Value::Array(hosts); - - let default_id = root - .get("desktopDefaultHostId") - .and_then(Value::as_str) - .map(|value| value.trim().to_string()) - .unwrap_or_default(); - if !default_id.is_empty() - && previous_ids.contains(default_id.as_str()) - && !next_ids.contains(default_id.as_str()) - { - root["desktopDefaultHostId"] = Value::String(LOCAL_HOST_ID.to_string()); - } -} - -fn write_desktop_ssh_instances_to_path( - path: &Path, - config: DesktopSshInstancesConfig, -) -> Result { - let mut root = read_settings_root(path); - let previous = read_desktop_ssh_instances_from_path(path); - let previous_ids: HashSet = previous - .instances - .iter() - .map(|instance| instance.id.clone()) - .collect(); - - let mut seen = HashSet::new(); - let mut sanitized = Vec::new(); - - for instance in config.instances { - let normalized = sanitize_instance(instance)?; - if seen.contains(&normalized.id) { - continue; - } - seen.insert(normalized.id.clone()); - sanitized.push(normalized); - } - - sync_desktop_hosts_for_ssh(&mut root, &previous_ids, &sanitized); - root["desktopSshInstances"] = serde_json::to_value(&sanitized)?; - write_settings_root(path, &root)?; - - Ok(DesktopSshInstancesConfig { - instances: sanitized, - }) -} - -fn update_ssh_host_url(instance_id: &str, label: &str, local_url: &str) -> Result<()> { - let path = settings_file_path(); - let mut root = read_settings_root(&path); - let mut hosts = root - .get("desktopHosts") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default(); - - let mut found = false; - for host in &mut hosts { - let host_id = host - .get("id") - .and_then(Value::as_str) - .map(|value| value.trim()) - .unwrap_or(""); - if host_id != instance_id { - continue; - } - if let Some(obj) = host.as_object_mut() { - obj.insert("id".to_string(), Value::String(instance_id.to_string())); - obj.insert("label".to_string(), Value::String(label.to_string())); - obj.insert("url".to_string(), Value::String(local_url.to_string())); - found = true; - break; - } - } - - if !found { - hosts.push(json!({ - "id": instance_id, - "label": label, - "url": local_url - })); - } - - root["desktopHosts"] = Value::Array(hosts); - write_settings_root(&path, &root) -} - -fn persist_local_port_for_instance(instance_id: &str, local_port: u16) -> Result<()> { - let path = settings_file_path(); - let mut root = read_settings_root(&path); - let mut changed = false; - - if let Some(items) = root - .get_mut("desktopSshInstances") - .and_then(Value::as_array_mut) - { - for item in items { - let Some(id) = item.get("id").and_then(Value::as_str) else { - continue; - }; - if id.trim() != instance_id { - continue; - } - if item - .get("localForward") - .and_then(Value::as_object) - .is_none() - { - item["localForward"] = json!({}); - } - item["localForward"]["preferredLocalPort"] = Value::Number(local_port.into()); - changed = true; - break; - } - } - - if changed { - write_settings_root(&path, &root)?; - } - - Ok(()) -} - -fn split_shell_words(input: &str) -> Result> { - let mut tokens = Vec::new(); - let mut current = String::new(); - let mut chars = input.chars().peekable(); - let mut in_single = false; - let mut in_double = false; - - while let Some(ch) = chars.next() { - match ch { - '\\' if !in_single => { - if let Some(next) = chars.next() { - current.push(next); - } - } - '\'' if !in_double => { - in_single = !in_single; - } - '"' if !in_single => { - in_double = !in_double; - } - c if c.is_whitespace() && !in_single && !in_double => { - if !current.is_empty() { - tokens.push(std::mem::take(&mut current)); - } - } - _ => current.push(ch), - } - } - - if in_single || in_double { - return Err(anyhow!("Unclosed quote in SSH command")); - } - - if !current.is_empty() { - tokens.push(current); - } - - Ok(tokens) -} - -fn is_disallowed_primary_flag(token: &str) -> bool { - const DISALLOWED: [&str; 17] = [ - "-M", "-S", "-O", "-N", "-t", "-T", "-f", "-G", "-W", "-v", "-V", "-q", "-n", "-s", "-e", - "-E", "-g", - ]; - DISALLOWED.contains(&token) -} - -fn has_disallowed_o_option(value: &str) -> bool { - let lower = value.trim().to_ascii_lowercase(); - [ - "controlmaster", - "controlpath", - "controlpersist", - "batchmode", - "proxycommand", - ] - .iter() - .any(|prefix| lower.starts_with(prefix)) -} - -fn parse_ssh_command(raw: &str) -> Result { - let mut tokens = split_shell_words(raw)?; - if tokens.is_empty() { - return Err(anyhow!("SSH command is empty")); - } - - if tokens[0] == "ssh" { - tokens.remove(0); - } - - if tokens.is_empty() { - return Err(anyhow!("SSH command must include destination")); - } - - const ALLOWED_FLAGS: [&str; 11] = [ - "-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y", - ]; - const ALLOWED_WITH_VALUES: [&str; 14] = [ - "-B", "-b", "-c", "-D", "-F", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R", - ]; - - let mut destination: Option = None; - let mut args = Vec::new(); - let mut idx = 0usize; - - while idx < tokens.len() { - let token = tokens[idx].clone(); - if destination.is_some() { - return Err(anyhow!( - "SSH command has unsupported trailing argument: {token}" - )); - } - - if token.starts_with('-') { - if is_disallowed_primary_flag(token.as_str()) { - return Err(anyhow!("SSH option {token} is not allowed")); - } - - if ALLOWED_FLAGS.contains(&token.as_str()) { - args.push(token); - idx += 1; - continue; - } - - let mut matched = false; - for option in ALLOWED_WITH_VALUES { - if token == option { - if idx + 1 >= tokens.len() { - return Err(anyhow!("SSH option {option} requires a value")); - } - let value = tokens[idx + 1].clone(); - if option == "-o" && has_disallowed_o_option(&value) { - return Err(anyhow!("SSH option -o {value} is not allowed")); - } - args.push(token.clone()); - args.push(value); - idx += 2; - matched = true; - break; - } - - if token.starts_with(option) && token.len() > option.len() { - let value = token[option.len()..].to_string(); - if option == "-o" && has_disallowed_o_option(&value) { - return Err(anyhow!("SSH option -o {value} is not allowed")); - } - args.push(token.clone()); - idx += 1; - matched = true; - break; - } - } - - if !matched { - return Err(anyhow!("Unsupported SSH option: {token}")); - } - - continue; - } - - destination = Some(token); - idx += 1; - } - - let Some(destination) = destination - .map(|d| d.trim().to_string()) - .filter(|d| !d.is_empty()) - else { - return Err(anyhow!("SSH command must include destination")); - }; - - Ok(DesktopSshParsedCommand { destination, args }) -} - -fn shell_quote(value: &str) -> String { - let escaped = value.replace('\'', "'\\''"); - format!("'{escaped}'") -} - -fn run_output(command: &mut Command) -> Result<(i32, String, String)> { - let output = command - .output() - .with_context(|| format!("failed to execute command: {:?}", command))?; - - let code = output.status.code().unwrap_or(-1); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - Ok((code, stdout, stderr)) -} - -fn build_ssh_command( - parsed: &DesktopSshParsedCommand, - pre_destination_args: &[String], - remote_command: Option<&str>, -) -> Command { - let mut command = Command::new("ssh"); - command - .args(&parsed.args) - .args(pre_destination_args) - .arg(&parsed.destination); - if let Some(remote) = remote_command { - command.arg(remote); - } - command -} - -fn resolve_ssh_config(parsed: &DesktopSshParsedCommand) -> Result> { - let args = vec!["-G".to_string()]; - let mut command = build_ssh_command(parsed, &args, None); - let (code, stdout, stderr) = run_output(&mut command)?; - if code != 0 { - return Err(anyhow!(stderr.trim().to_string())); - } - - let mut resolved = HashMap::new(); - for line in stdout.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - let mut parts = trimmed.splitn(2, ' '); - let key = parts.next().unwrap_or_default().trim().to_ascii_lowercase(); - let value = parts.next().unwrap_or_default().trim(); - if key.is_empty() || value.is_empty() { - continue; - } - resolved.insert(key, value.to_string()); - } - Ok(resolved) -} - -fn ensure_session_dir(instance_id: &str) -> Result { - let base = settings_file_path() - .parent() - .map(Path::to_path_buf) - .unwrap_or_else(|| PathBuf::from("/tmp")) - .join("ssh") - .join(instance_id); - fs::create_dir_all(&base)?; - Ok(base) -} - -fn control_path_for_instance(_session_dir: &Path, instance_id: &str) -> PathBuf { - let hash = { - use std::hash::{Hash, Hasher}; - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - instance_id.hash(&mut hasher); - hasher.finish() - }; - std::env::temp_dir().join(format!("ocssh-{hash:x}.sock")) -} - -fn askpass_script_content() -> String { - let script = r#"#!/bin/bash -PROMPT="$1" - -if [[ -n "$OPENCHAMBER_SSH_ASKPASS_VALUE" ]]; then - if [[ "$PROMPT" == *"assword"* || "$PROMPT" == *"passphrase"* ]]; then - printf '%s\n' "$OPENCHAMBER_SSH_ASKPASS_VALUE" - exit 0 - fi -fi - -DEFAULT_ANSWER="" -HIDDEN_INPUT="true" - -if [[ "$PROMPT" == *"yes/no"* ]]; then - DEFAULT_ANSWER="yes" - HIDDEN_INPUT="false" -fi - -/usr/bin/osascript <<'APPLESCRIPT' "$PROMPT" "$DEFAULT_ANSWER" "$HIDDEN_INPUT" -on run argv - set promptText to item 1 of argv - set defaultAnswer to item 2 of argv - set hiddenInput to item 3 of argv - - try - if hiddenInput is "true" then - set response to display dialog promptText default answer defaultAnswer with hidden answer buttons {"Cancel", "OK"} default button "OK" - else - set response to display dialog promptText default answer defaultAnswer buttons {"Cancel", "OK"} default button "OK" - end if - return text returned of response - on error - error number -128 - end try -end run -APPLESCRIPT -"#; - script.to_string() -} - -fn write_askpass_script(path: &Path) -> Result<()> { - fs::write(path, askpass_script_content())?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perm = fs::metadata(path)?.permissions(); - perm.set_mode(0o700); - fs::set_permissions(path, perm)?; - } - Ok(()) -} - -fn spawn_master_process( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - askpass_path: &Path, - ssh_password: Option<&str>, -) -> Result { - let args = vec![ - "-o".to_string(), - "ControlMaster=yes".to_string(), - "-o".to_string(), - format!("ControlPath={}", control_path.display()), - "-o".to_string(), - format!("ControlPersist={DEFAULT_CONTROL_PERSIST_SEC}"), - "-N".to_string(), - ]; - let mut command = build_ssh_command(parsed, &args, None); - command - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", askpass_path) - .env("DISPLAY", "1"); - - if let Some(secret) = ssh_password.filter(|value| !value.trim().is_empty()) { - command.env("OPENCHAMBER_SSH_ASKPASS_VALUE", secret.trim()); - } - - command.spawn().with_context(|| { - format!( - "failed to start SSH ControlMaster for {}", - parsed.destination - ) - }) -} - -fn wait_for_master_ready( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - timeout_sec: u16, - master: &mut Child, -) -> Result<()> { - let deadline = std::time::Instant::now() + Duration::from_secs(timeout_sec as u64); - let mut poll_ms: u64 = 250; - while std::time::Instant::now() < deadline { - let args = vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", control_path.display()), - "-O".to_string(), - "check".to_string(), - ]; - - let mut check = build_ssh_command(parsed, &args, None); - let (code, _stdout, _stderr) = run_output(&mut check)?; - if code == 0 { - return Ok(()); - } - - if let Some(status) = master.try_wait().ok().flatten() { - let mut stderr = String::new(); - if let Some(mut stream) = master.stderr.take() { - let _ = stream.read_to_string(&mut stderr); - } - if stderr.trim().is_empty() { - return Err(anyhow!(format!( - "SSH master process exited before ready (status: {status})" - ))); - } - return Err(anyhow!(stderr.trim().to_string())); - } - - std::thread::sleep(Duration::from_millis(poll_ms)); - poll_ms = (poll_ms * 2).min(2000); - } - - Err(anyhow!("SSH ControlMaster connection timed out")) -} - -fn control_master_operation( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - op: &str, -) -> Result<(i32, String, String)> { - let args = vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", control_path.display()), - "-o".to_string(), - "BatchMode=yes".to_string(), - "-o".to_string(), - "ConnectTimeout=3".to_string(), - "-O".to_string(), - op.to_string(), - ]; - let mut command = build_ssh_command(parsed, &args, None); - run_output(&mut command) -} - -fn is_control_master_alive(parsed: &DesktopSshParsedCommand, control_path: &Path) -> bool { - control_master_operation(parsed, control_path, "check") - .map(|(code, _, _)| code == 0) - .unwrap_or(false) -} - -fn stop_control_master_best_effort(parsed: &DesktopSshParsedCommand, control_path: &Path) { - let _ = control_master_operation(parsed, control_path, "exit"); -} - -fn run_remote_command( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - script: &str, - timeout_sec: u16, -) -> Result { - let args = vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", control_path.display()), - "-o".to_string(), - format!("ConnectTimeout={timeout_sec}"), - "-T".to_string(), - ]; - let remote = format!("sh -lc {}", shell_quote(script)); - let mut command = build_ssh_command(parsed, &args, Some(&remote)); - let (code, stdout, stderr) = run_output(&mut command)?; - if code != 0 { - if stderr.trim().is_empty() { - return Err(anyhow!("Remote command failed")); - } - return Err(anyhow!(stderr.trim().to_string())); - } - Ok(stdout) -} - -fn remote_command_exists( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - command_name: &str, -) -> bool { - run_remote_command( - parsed, - control_path, - &format!( - "command -v {} >/dev/null 2>&1 && echo yes || echo no", - command_name - ), - DEFAULT_CONNECTION_TIMEOUT_SEC, - ) - .map(|output| output.trim() == "yes") - .unwrap_or(false) -} - -fn parse_version_token(raw: &str) -> Option { - for token in raw.split_whitespace() { - let mut candidate = token.trim().trim_start_matches('v').to_string(); - while candidate.ends_with(',') || candidate.ends_with(')') || candidate.ends_with('(') { - candidate.pop(); - } - let parts: Vec<&str> = candidate.split('.').collect(); - if parts.len() < 2 { - continue; - } - if parts - .iter() - .all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit())) - { - return Some(candidate); - } - } - None -} - -fn current_remote_openchamber_version( - parsed: &DesktopSshParsedCommand, - control_path: &Path, -) -> Option { - run_remote_command( - parsed, - control_path, - "openchamber --version 2>/dev/null || true", - DEFAULT_CONNECTION_TIMEOUT_SEC, - ) - .ok() - .and_then(|value| parse_version_token(&value)) -} - -fn install_openchamber_managed( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - version: &str, - preferred: &DesktopSshInstallMethod, -) -> Result<()> { - let has_bun = remote_command_exists(parsed, control_path, "bun"); - let has_npm = remote_command_exists(parsed, control_path, "npm"); - - let mut commands = Vec::new(); - - match preferred { - DesktopSshInstallMethod::Bun => { - if has_bun { - commands.push(format!("bun add -g @openchamber/web@{version}")); - } - if has_npm { - commands.push(format!("npm install -g @openchamber/web@{version}")); - } - } - DesktopSshInstallMethod::Npm => { - if has_npm { - commands.push(format!("npm install -g @openchamber/web@{version}")); - } - if has_bun { - commands.push(format!("bun add -g @openchamber/web@{version}")); - } - } - DesktopSshInstallMethod::DownloadRelease | DesktopSshInstallMethod::UploadBundle => { - if has_bun { - commands.push(format!("bun add -g @openchamber/web@{version}")); - } - if has_npm { - commands.push(format!("npm install -g @openchamber/web@{version}")); - } - } - } - - if commands.is_empty() { - return Err(anyhow!("Remote host has neither bun nor npm available")); - } - - let mut last_error: Option = None; - for command in commands { - match run_remote_command( - parsed, - control_path, - &command, - DEFAULT_CONNECTION_TIMEOUT_SEC, - ) { - Ok(_) => return Ok(()), - Err(err) => { - last_error = Some(err); - } - } - } - - Err(last_error.unwrap_or_else(|| anyhow!("Failed to install OpenChamber on remote host"))) -} - -fn parse_probe_status_line(line: Option<&str>, prefix: &str) -> Option { - let value = line?.strip_prefix(prefix)?.trim(); - value.parse::().ok() -} - -fn is_auth_http_status(status: u16) -> bool { - status == 401 || status == 403 -} - -fn is_liveness_http_status(status: u16) -> bool { - (200..=299).contains(&status) || is_auth_http_status(status) -} - -fn configured_openchamber_password(instance: &DesktopSshInstance) -> Option<&str> { - instance - .auth - .openchamber_password - .as_ref() - .and_then(|secret| { - if secret.enabled { - secret.value.as_deref() - } else { - None - } - }) - .map(str::trim) - .filter(|value| !value.is_empty()) -} - -fn probe_remote_system_info( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - port: u16, - openchamber_password: Option<&str>, -) -> Result { - let auth_payload = if let Some(password) = openchamber_password { - serde_json::to_string(&json!({ "password": password })).unwrap_or_else(|_| "{}".to_string()) - } else { - "{}".to_string() - }; - - let auth_enabled = if openchamber_password.is_some() { - "1" - } else { - "0" - }; - let script = format!( - "AUTH_STATUS=0; INFO_STATUS=0; HEALTH_STATUS=0; BODY_FILE=\"$(mktemp)\"; COOKIE_FILE=\"$(mktemp)\"; cleanup() {{ rm -f \"$BODY_FILE\" \"$COOKIE_FILE\"; }}; trap cleanup EXIT; if command -v curl >/dev/null 2>&1; then if [ \"{auth_enabled}\" = \"1\" ]; then AUTH_STATUS=\"$(curl -sS --max-time 3 -o /dev/null -w '%{{http_code}}' -c \"$COOKIE_FILE\" -H 'content-type: application/json' --data {auth_payload} http://127.0.0.1:{port}/auth/session || true)\"; if [ \"$AUTH_STATUS\" = \"200\" ]; then INFO_STATUS=\"$(curl -sS --max-time 3 -b \"$COOKIE_FILE\" -o \"$BODY_FILE\" -w '%{{http_code}}' http://127.0.0.1:{port}/api/system/info || true)\"; else INFO_STATUS=\"$(curl -sS --max-time 3 -o \"$BODY_FILE\" -w '%{{http_code}}' http://127.0.0.1:{port}/api/system/info || true)\"; fi; else INFO_STATUS=\"$(curl -sS --max-time 3 -o \"$BODY_FILE\" -w '%{{http_code}}' http://127.0.0.1:{port}/api/system/info || true)\"; fi; HEALTH_STATUS=\"$(curl -sS --max-time 3 -o /dev/null -w '%{{http_code}}' http://127.0.0.1:{port}/health || true)\"; elif command -v wget >/dev/null 2>&1; then wget -qO \"$BODY_FILE\" http://127.0.0.1:{port}/api/system/info >/dev/null 2>&1; if [ $? -eq 0 ]; then INFO_STATUS=200; fi; wget -qO- http://127.0.0.1:{port}/health >/dev/null 2>&1; if [ $? -eq 0 ]; then HEALTH_STATUS=200; fi; else exit 127; fi; printf 'INFO_STATUS=%s\\nAUTH_STATUS=%s\\nHEALTH_STATUS=%s\\n' \"$INFO_STATUS\" \"$AUTH_STATUS\" \"$HEALTH_STATUS\"; cat \"$BODY_FILE\" 2>/dev/null || true", - auth_payload = shell_quote(&auth_payload), - ); - let output = run_remote_command( - parsed, - control_path, - &script, - DEFAULT_CONNECTION_TIMEOUT_SEC, - )?; - - let mut lines = output.lines(); - let info_status = parse_probe_status_line(lines.next(), "INFO_STATUS=").unwrap_or(0); - let auth_status = parse_probe_status_line(lines.next(), "AUTH_STATUS=").unwrap_or(0); - let health_status = parse_probe_status_line(lines.next(), "HEALTH_STATUS=").unwrap_or(0); - let body = lines.collect::>().join("\n"); - - if is_liveness_http_status(info_status) { - if is_auth_http_status(info_status) { - if openchamber_password.is_some() && auth_status != 200 { - return Err(anyhow!(format!( - "Remote OpenChamber requires UI authentication and configured password was rejected (auth status {auth_status})" - ))); - } - - if is_liveness_http_status(health_status) { - return Ok(RemoteSystemInfo::default()); - } - - return Err(anyhow!( - "Remote OpenChamber requires UI authentication on /api/system/info; configure OpenChamber UI password" - )); - } - } else if is_liveness_http_status(health_status) { - return Ok(RemoteSystemInfo::default()); - } else { - return Err(anyhow!(format!( - "Remote OpenChamber probe failed (info status {info_status}, health status {health_status})" - ))); - } - - let mut info = serde_json::from_str::(&body).unwrap_or_default(); - if info.openchamber_version.is_none() { - if let Ok(value) = serde_json::from_str::(&body) { - info.openchamber_version = value - .get("openchamberVersion") - .and_then(Value::as_str) - .map(|v| v.to_string()); - info.runtime = value - .get("runtime") - .and_then(Value::as_str) - .map(|v| v.to_string()); - info.pid = value.get("pid").and_then(Value::as_u64); - info.started_at = value - .get("startedAt") - .and_then(Value::as_str) - .map(|v| v.to_string()); - } - } - Ok(info) -} - -fn remote_server_running( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - port: u16, - openchamber_password: Option<&str>, -) -> bool { - probe_remote_system_info(parsed, control_path, port, openchamber_password).is_ok() -} - -fn random_port_candidate(seed: &str) -> u16 { - use std::hash::{Hash, Hasher}; - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - seed.hash(&mut hasher); - now_millis().hash(&mut hasher); - let value = hasher.finish(); - let base = 20_000u16; - let span = 30_000u16; - base + ((value % span as u64) as u16) -} - -fn start_remote_server_managed( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - instance: &DesktopSshInstance, - desired_port: u16, -) -> Result { - let mut env_prefix = "OPENCHAMBER_RUNTIME=ssh-remote".to_string(); - if let Some(secret) = instance - .auth - .openchamber_password - .as_ref() - .and_then(|v| if v.enabled { v.value.clone() } else { None }) - .map(|v| v.trim().to_string()) - .filter(|v| !v.is_empty()) - { - env_prefix.push(' '); - env_prefix.push_str("OPENCHAMBER_UI_PASSWORD="); - env_prefix.push_str(&shell_quote(&secret)); - } - let script = format!( - "{env_prefix} openchamber serve --daemon --hostname 127.0.0.1 --port {desired_port}" - ); - let output = run_remote_command( - parsed, - control_path, - &script, - DEFAULT_CONNECTION_TIMEOUT_SEC, - )?; - - if let Some(port) = output - .split_whitespace() - .find_map(|token| token.parse::().ok()) - { - return Ok(port); - } - Ok(desired_port) -} - -fn stop_remote_server_best_effort( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - remote_port: u16, -) { - let script = format!( - "if command -v curl >/dev/null 2>&1; then curl -fsS -X POST http://127.0.0.1:{remote_port}/api/system/shutdown >/dev/null 2>&1 || true; elif command -v wget >/dev/null 2>&1; then wget -qO- --method=POST http://127.0.0.1:{remote_port}/api/system/shutdown >/dev/null 2>&1 || true; fi" - ); - let _ = run_remote_command( - parsed, - control_path, - &script, - DEFAULT_CONNECTION_TIMEOUT_SEC, - ); -} - -fn spawn_main_forward( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - bind_host: &str, - local_port: u16, - remote_port: u16, -) -> Result { - let args = vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", control_path.display()), - "-N".to_string(), - "-L".to_string(), - format!("{bind_host}:{local_port}:127.0.0.1:{remote_port}"), - ]; - let mut command = build_ssh_command(parsed, &args, None); - command - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .spawn() - .with_context(|| format!("Failed to start main SSH forward on local port {local_port}")) -} - -fn spawn_extra_forward( - parsed: &DesktopSshParsedCommand, - control_path: &Path, - forward: &DesktopSshPortForward, -) -> Result<()> { - let mut args = vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", control_path.display()), - "-O".to_string(), - "forward".to_string(), - ]; - - match forward.forward_type { - DesktopSshPortForwardType::Local => { - let local_host = forward - .local_host - .as_deref() - .filter(|v| !v.trim().is_empty()) - .unwrap_or("127.0.0.1"); - let local_port = forward - .local_port - .ok_or_else(|| anyhow!("Missing local port"))?; - let remote_host = forward - .remote_host - .as_deref() - .filter(|v| !v.trim().is_empty()) - .unwrap_or("127.0.0.1"); - let remote_port = forward - .remote_port - .ok_or_else(|| anyhow!("Missing remote port"))?; - args.push("-L".to_string()); - args.push(format!( - "{local_host}:{local_port}:{remote_host}:{remote_port}" - )); - } - DesktopSshPortForwardType::Remote => { - let remote_host = forward - .remote_host - .as_deref() - .filter(|v| !v.trim().is_empty()) - .unwrap_or("127.0.0.1"); - let remote_port = forward - .remote_port - .ok_or_else(|| anyhow!("Missing remote port"))?; - let local_host = forward - .local_host - .as_deref() - .filter(|v| !v.trim().is_empty()) - .unwrap_or("127.0.0.1"); - let local_port = forward - .local_port - .ok_or_else(|| anyhow!("Missing local port"))?; - args.push("-R".to_string()); - args.push(format!( - "{remote_host}:{remote_port}:{local_host}:{local_port}" - )); - } - DesktopSshPortForwardType::Dynamic => { - let local_host = forward - .local_host - .as_deref() - .filter(|v| !v.trim().is_empty()) - .unwrap_or("127.0.0.1"); - let local_port = forward - .local_port - .ok_or_else(|| anyhow!("Missing local port"))?; - args.push("-D".to_string()); - args.push(format!("{local_host}:{local_port}")); - } - } - - let mut command = build_ssh_command(parsed, &args, None); - let (code, stdout, stderr) = run_output(&mut command) - .with_context(|| format!("Failed to configure extra SSH forward {}", forward.id))?; - if code != 0 { - let detail = if stderr.trim().is_empty() { - stdout.trim() - } else { - stderr.trim() - }; - return Err(anyhow!(format!( - "Failed to configure extra SSH forward {}: {}", - forward.id, - if detail.is_empty() { - "unknown error" - } else { - detail - } - ))); - } - Ok(()) -} - -fn is_local_port_available(bind_host: &str, port: u16) -> bool { - TcpListener::bind(format!("{bind_host}:{port}")).is_ok() -} - -fn pick_unused_local_port() -> Result { - let listener = TcpListener::bind("127.0.0.1:0")?; - Ok(listener.local_addr()?.port()) -} - -fn is_local_tunnel_reachable(local_port: u16) -> bool { - let addr = format!("127.0.0.1:{local_port}"); - let Ok(parsed) = addr.parse() else { - return false; - }; - TcpStream::connect_timeout(&parsed, Duration::from_millis(500)).is_ok() -} - -fn wait_local_forward_ready(local_port: u16) -> Result<()> { - let deadline = std::time::Instant::now() + Duration::from_secs(DEFAULT_READY_TIMEOUT_SEC); - let addr: std::net::SocketAddr = format!("127.0.0.1:{local_port}").parse()?; - let mut poll_ms: u64 = 250; - while std::time::Instant::now() < deadline { - if let Ok(mut stream) = - TcpStream::connect_timeout(&addr, Duration::from_millis(1000)) - { - use std::io::{Read as IoRead, Write}; - let _ = stream.set_read_timeout(Some(Duration::from_millis(1000))); - let _ = stream.set_write_timeout(Some(Duration::from_millis(1000))); - let request = format!( - "GET /health HTTP/1.1\r\nHost: 127.0.0.1:{local_port}\r\nConnection: close\r\n\r\n" - ); - if stream.write_all(request.as_bytes()).is_ok() { - let mut buf = [0u8; 32]; - if let Ok(n) = stream.read(&mut buf) { - let head = std::str::from_utf8(&buf[..n]).unwrap_or(""); - // Match "HTTP/1.x 2xx" or "HTTP/1.x 401" - if head.starts_with("HTTP/1.") - && (head.contains(" 2") || head.contains(" 401")) - { - return Ok(()); - } - } - } - } - std::thread::sleep(Duration::from_millis(poll_ms)); - poll_ms = (poll_ms * 2).min(2000); - } - Err(anyhow!( - "Timed out waiting for forwarded OpenChamber health" - )) -} - -fn kill_child(child: &mut Child) { - let _ = child.kill(); - let _ = child.wait(); -} - -fn parse_ssh_config_candidates(path: &Path, source: &str) -> Vec { - let Ok(content) = fs::read_to_string(path) else { - return Vec::new(); - }; - let mut candidates = Vec::new(); - for line in content.lines() { - let trimmed = line.split('#').next().map(|part| part.trim()).unwrap_or(""); - if trimmed.is_empty() { - continue; - } - if trimmed.len() < 4 { - continue; - } - if !trimmed[..4].eq_ignore_ascii_case("host") { - continue; - } - - let rest = trimmed[4..].trim(); - if rest.is_empty() { - continue; - } - - for token in rest.split_whitespace() { - let host = token.trim(); - if host.is_empty() || host.starts_with('!') { - continue; - } - if host == "*" { - continue; - } - let pattern = host.contains('*') || host.contains('?'); - candidates.push(DesktopSshImportCandidate { - host: host.to_string(), - pattern, - source: source.to_string(), - ssh_command: format!("ssh {host}"), - }); - } - } - candidates -} - -impl DesktopSshManagerInner { - fn append_log_with_level(&self, id: &str, level: &str, message: impl Into) { - let line = format!("[{}] [{}] {}", now_millis(), level, message.into()); - let mut logs = self.logs.lock().expect("ssh logs mutex"); - let entry = logs.entry(id.to_string()).or_default(); - entry.push(line); - if entry.len() > MAX_LOG_LINES_PER_INSTANCE { - let overflow = entry.len() - MAX_LOG_LINES_PER_INSTANCE; - entry.drain(0..overflow); - } - } - - fn append_log(&self, id: &str, message: impl Into) { - self.append_log_with_level(id, "INFO", message); - } - - fn append_attempt_separator(&self, id: &str, connect_attempt: u32, retry_attempt: u32) { - let scope = if retry_attempt > 0 { - format!("retry {retry_attempt}") - } else { - "manual".to_string() - }; - self.append_log_with_level( - id, - "INFO", - format!("---------------- attempt #{connect_attempt} ({scope}) ----------------"), - ); - } - - fn logs_for_instance(&self, id: &str, limit: usize) -> Vec { - let logs = self.logs.lock().expect("ssh logs mutex"); - let mut lines = logs.get(id).cloned().unwrap_or_default(); - if limit > 0 && lines.len() > limit { - let keep_from = lines.len() - limit; - lines.drain(0..keep_from); - } - lines - } - - fn clear_logs_for_instance(&self, id: &str) { - self.logs.lock().expect("ssh logs mutex").remove(id); - } - - fn status_snapshot_for_instance(&self, id: &str) -> DesktopSshInstanceStatus { - self.statuses - .lock() - .expect("ssh status mutex") - .get(id) - .cloned() - .unwrap_or_else(|| DesktopSshInstanceStatus::idle(id)) - } - - fn set_status( - &self, - app: &AppHandle, - id: &str, - phase: DesktopSshPhase, - detail: Option, - local_url: Option, - local_port: Option, - remote_port: Option, - started_by_us: bool, - retry_attempt: u32, - requires_user_action: bool, - ) { - let level = if matches!(&phase, DesktopSshPhase::Error) { - "ERROR" - } else if matches!(&phase, DesktopSshPhase::Degraded) { - "WARN" - } else { - "INFO" - }; - - self.append_log_with_level( - id, - level, - format!( - "phase={} detail={} retry={} requires_user_action={}", - serde_json::to_string(&phase).unwrap_or_else(|_| "\"unknown\"".to_string()), - detail.as_deref().unwrap_or(""), - retry_attempt, - requires_user_action - ), - ); - - let status = DesktopSshInstanceStatus { - id: id.to_string(), - phase, - detail, - local_url, - local_port, - remote_port, - started_by_us, - retry_attempt, - requires_user_action, - updated_at_ms: now_millis(), - }; - - self.statuses - .lock() - .expect("ssh status mutex") - .insert(id.to_string(), status.clone()); - let _ = app.emit(SSH_STATUS_EVENT, status); - } - - fn clear_retry_attempt(&self, id: &str) { - self.reconnect_attempts - .lock() - .expect("ssh retry mutex") - .remove(id); - } - - fn next_retry_attempt(&self, id: &str) -> u32 { - let mut guard = self.reconnect_attempts.lock().expect("ssh retry mutex"); - let next = guard.get(id).copied().unwrap_or(0).saturating_add(1); - guard.insert(id.to_string(), next); - next - } - - fn current_retry_attempt(&self, id: &str) -> u32 { - self.reconnect_attempts - .lock() - .expect("ssh retry mutex") - .get(id) - .copied() - .unwrap_or(0) - } - - fn next_connect_attempt(&self, id: &str) -> u32 { - let mut guard = self - .connect_attempts - .lock() - .expect("ssh connect-attempt mutex"); - let next = guard.get(id).copied().unwrap_or(0).saturating_add(1); - guard.insert(id.to_string(), next); - next - } - - fn cancel_connect_task(&self, id: &str) { - if let Some(handle) = self - .connect_tasks - .lock() - .expect("ssh connect task mutex") - .remove(id) - { - handle.abort(); - } - } - - fn cancel_monitor_task(&self, id: &str) { - if let Some(handle) = self - .monitor_tasks - .lock() - .expect("ssh monitor task mutex") - .remove(id) - { - handle.abort(); - } - } - - fn session_is_alive(&self, id: &str) -> bool { - let mut sessions = self.sessions.lock().expect("ssh sessions mutex"); - let Some(session) = sessions.get_mut(id) else { - return false; - }; - - let mut main_anchor_alive = false; - - if !session.main_forward_detached { - if let Some(status) = session.main_forward.try_wait().ok().flatten() { - if status.success() { - session.main_forward_detached = true; - self.append_log_with_level( - id, - "INFO", - "Main tunnel helper exited after ControlMaster handoff", - ); - } else { - let mut stderr = String::new(); - if let Some(mut stream) = session.main_forward.stderr.take() { - let _ = stream.read_to_string(&mut stderr); - } - self.append_log_with_level( - id, - "WARN", - if stderr.trim().is_empty() { - format!("Existing main SSH forward is not running ({status})") - } else { - format!( - "Existing main SSH forward is not running ({status}): {}", - stderr.trim() - ) - }, - ); - return false; - } - } else { - main_anchor_alive = true; - } - } - - if main_anchor_alive { - return true; - } - - if session.master_detached { - if !is_control_master_alive(&session.parsed, &session.control_path) { - if is_local_tunnel_reachable(session.local_port) { - self.append_log_with_level( - id, - "WARN", - "SSH ControlMaster check failed but local tunnel is still reachable", - ); - return true; - } - self.append_log_with_level( - id, - "WARN", - "Existing SSH ControlMaster is not reachable", - ); - return false; - } - } else if let Some(status) = session.master.try_wait().ok().flatten() { - if status.success() && is_control_master_alive(&session.parsed, &session.control_path) { - session.master_detached = true; - self.append_log_with_level( - id, - "INFO", - "SSH ControlMaster transitioned to detached background mode", - ); - } else { - let mut stderr = String::new(); - if let Some(mut stream) = session.master.stderr.take() { - let _ = stream.read_to_string(&mut stderr); - } - self.append_log_with_level( - id, - "WARN", - if stderr.trim().is_empty() { - format!("Existing SSH ControlMaster is not running ({status})") - } else { - format!( - "Existing SSH ControlMaster is not running ({status}): {}", - stderr.trim() - ) - }, - ); - return false; - } - } - - true - } - - fn disconnect_internal(&self, app: &AppHandle, id: &str, report_idle: bool) { - self.cancel_connect_task(id); - self.cancel_monitor_task(id); - - if let Some(mut session) = self.sessions.lock().expect("ssh sessions mutex").remove(id) { - if session.started_by_us - && matches!( - session.instance.remote_openchamber.mode, - DesktopSshRemoteMode::Managed - ) - && !session.instance.remote_openchamber.keep_running - { - stop_remote_server_best_effort( - &session.parsed, - &session.control_path, - session.remote_port, - ); - } - - stop_control_master_best_effort(&session.parsed, &session.control_path); - - kill_child(&mut session.main_forward); - for child in &mut session.extra_forwards { - kill_child(child); - } - kill_child(&mut session.master); - - let _ = fs::remove_file(&session.control_path); - let _ = fs::remove_file(session.session_dir.join("askpass.sh")); - } - - self.clear_retry_attempt(id); - - if report_idle { - self.set_status( - app, - id, - DesktopSshPhase::Idle, - None, - None, - None, - None, - false, - 0, - false, - ); - } - } - - fn ensure_remote_server( - &self, - app: &AppHandle, - instance: &DesktopSshInstance, - parsed: &DesktopSshParsedCommand, - control_path: &Path, - ) -> Result<(u16, bool)> { - let app_version = app.package_info().version.to_string(); - - match instance.remote_openchamber.mode { - DesktopSshRemoteMode::External => { - let Some(port) = instance.remote_openchamber.preferred_port else { - return Err(anyhow!( - "External mode requires a preferred remote OpenChamber port" - )); - }; - self.set_status( - app, - &instance.id, - DesktopSshPhase::ServerDetecting, - Some("Probing external OpenChamber server".to_string()), - None, - None, - Some(port), - false, - 0, - false, - ); - probe_remote_system_info( - parsed, - control_path, - port, - configured_openchamber_password(instance), - ) - .map_err(|err| { - anyhow!(format!( - "External OpenChamber server probe failed on configured remote port: {err}" - )) - })?; - Ok((port, false)) - } - DesktopSshRemoteMode::Managed => { - self.set_status( - app, - &instance.id, - DesktopSshPhase::RemoteProbe, - Some("Checking remote OpenChamber installation".to_string()), - None, - None, - None, - false, - 0, - false, - ); - - let installed_version = current_remote_openchamber_version(parsed, control_path); - if installed_version.is_none() { - self.set_status( - app, - &instance.id, - DesktopSshPhase::Installing, - Some("Installing OpenChamber on remote host".to_string()), - None, - None, - None, - false, - 0, - false, - ); - install_openchamber_managed( - parsed, - control_path, - &app_version, - &instance.remote_openchamber.install_method, - )?; - } else if installed_version.as_deref() != Some(app_version.as_str()) { - self.set_status( - app, - &instance.id, - DesktopSshPhase::Updating, - Some(format!( - "Updating remote OpenChamber from {} to {}", - installed_version - .clone() - .unwrap_or_else(|| "unknown".to_string()), - app_version - )), - None, - None, - None, - false, - 0, - false, - ); - install_openchamber_managed( - parsed, - control_path, - &app_version, - &instance.remote_openchamber.install_method, - )?; - } - - self.set_status( - app, - &instance.id, - DesktopSshPhase::ServerDetecting, - Some("Detecting managed OpenChamber server".to_string()), - None, - None, - None, - false, - 0, - false, - ); - - let mut started_by_us = false; - let mut remote_port = instance.remote_openchamber.preferred_port; - - if let Some(port) = remote_port { - if !remote_server_running( - parsed, - control_path, - port, - configured_openchamber_password(instance), - ) { - remote_port = None; - } - } - - if remote_port.is_none() { - self.set_status( - app, - &instance.id, - DesktopSshPhase::ServerStarting, - Some("Starting managed OpenChamber server".to_string()), - None, - None, - None, - false, - 0, - false, - ); - let desired_port = instance - .remote_openchamber - .preferred_port - .unwrap_or_else(|| random_port_candidate(&instance.id)); - let started_port = - start_remote_server_managed(parsed, control_path, instance, desired_port)?; - remote_port = Some(started_port); - started_by_us = true; - } - - let Some(port) = remote_port else { - return Err(anyhow!("Failed to determine remote OpenChamber port")); - }; - - if !remote_server_running( - parsed, - control_path, - port, - configured_openchamber_password(instance), - ) { - return Err(anyhow!( - "Managed OpenChamber server failed to become reachable" - )); - } - - Ok((port, started_by_us)) - } - } - } - - fn connect_blocking( - self: &Arc, - app: &AppHandle, - instance: DesktopSshInstance, - ) -> Result<()> { - let id = instance.id.clone(); - self.set_status( - app, - &id, - DesktopSshPhase::ConfigResolved, - Some("Resolving SSH command".to_string()), - None, - None, - None, - false, - 0, - false, - ); - - let parsed = instance - .ssh_parsed - .clone() - .or_else(|| parse_ssh_command(&instance.ssh_command).ok()) - .ok_or_else(|| anyhow!("Invalid SSH command"))?; - - let _resolved = resolve_ssh_config(&parsed)?; - - self.set_status( - app, - &id, - DesktopSshPhase::AuthCheck, - Some("Checking SSH connectivity".to_string()), - None, - None, - None, - false, - 0, - false, - ); - - let session_dir = ensure_session_dir(&id)?; - let control_path = control_path_for_instance(&session_dir, &id); - let _ = fs::remove_file(&control_path); - let askpass_path = session_dir.join("askpass.sh"); - write_askpass_script(&askpass_path)?; - - self.set_status( - app, - &id, - DesktopSshPhase::MasterConnecting, - Some("Establishing SSH ControlMaster".to_string()), - None, - None, - None, - false, - 0, - false, - ); - - let mut master = spawn_master_process( - &parsed, - &control_path, - &askpass_path, - instance.auth.ssh_password.as_ref().and_then(|secret| { - if secret.enabled { - secret.value.as_deref() - } else { - None - } - }), - )?; - - if let Err(err) = wait_for_master_ready( - &parsed, - &control_path, - instance.connection_timeout_sec, - &mut master, - ) { - kill_child(&mut master); - return Err(err); - } - - self.set_status( - app, - &id, - DesktopSshPhase::RemoteProbe, - Some("Probing remote platform".to_string()), - None, - None, - None, - false, - 0, - false, - ); - - let remote_os = run_remote_command( - &parsed, - &control_path, - "uname -s", - instance.connection_timeout_sec, - )?; - - let remote_os = remote_os.trim().to_ascii_lowercase(); - if remote_os != "linux" && remote_os != "darwin" { - kill_child(&mut master); - return Err(anyhow!("Unsupported remote OS: {remote_os}")); - } - - let (remote_port, started_by_us) = - match self.ensure_remote_server(app, &instance, &parsed, &control_path) { - Ok(result) => result, - Err(err) => { - kill_child(&mut master); - return Err(err); - } - }; - - self.set_status( - app, - &id, - DesktopSshPhase::Forwarding, - Some("Setting up port forwards".to_string()), - None, - None, - Some(remote_port), - started_by_us, - 0, - false, - ); - - let bind_host = sanitize_bind_host(&instance.local_forward.bind_host); - let mut local_port = instance.local_forward.preferred_local_port.unwrap_or(0); - if local_port == 0 { - local_port = pick_unused_local_port()?; - } - if !is_local_port_available(&bind_host, local_port) { - local_port = pick_unused_local_port()?; - } - - let mut main_forward = - match spawn_main_forward(&parsed, &control_path, &bind_host, local_port, remote_port) { - Ok(child) => child, - Err(err) => { - kill_child(&mut master); - return Err(err); - } - }; - let mut main_forward_detached = false; - - std::thread::sleep(Duration::from_millis(250)); - if let Some(status) = main_forward.try_wait().ok().flatten() { - if status.success() { - main_forward_detached = true; - self.append_log_with_level( - &id, - "INFO", - "Main tunnel helper exited after ControlMaster handoff", - ); - } else { - let mut stderr = String::new(); - if let Some(mut stream) = main_forward.stderr.take() { - let _ = stream.read_to_string(&mut stderr); - } - kill_child(&mut master); - return Err(anyhow!(format!( - "Failed to start main port forward (status: {status}): {}", - stderr.trim() - ))); - } - } - - let mut extra_forwards = Vec::new(); - let mut extra_errors = Vec::new(); - for forward in instance - .port_forwards - .iter() - .filter(|forward| forward.enabled) - { - match spawn_extra_forward(&parsed, &control_path, forward) { - Ok(()) => { - if matches!(forward.forward_type, DesktopSshPortForwardType::Local) { - if let Some(local_port) = forward.local_port { - std::thread::sleep(Duration::from_millis(100)); - if !is_local_tunnel_reachable(local_port) { - extra_errors.push(format!( - "{}: local listener 127.0.0.1:{} is not reachable", - forward.id, local_port - )); - } - } - } - } - Err(err) => extra_errors.push(format!("{}: {}", forward.id, err)), - } - } - - if let Err(err) = wait_local_forward_ready(local_port) { - kill_child(&mut main_forward); - for child in &mut extra_forwards { - kill_child(child); - } - kill_child(&mut master); - return Err(err); - } - - let local_url = format!("http://127.0.0.1:{local_port}"); - let label = build_display_label(&instance); - let _ = update_ssh_host_url(&id, &label, &local_url); - if instance.local_forward.preferred_local_port != Some(local_port) { - let _ = persist_local_port_for_instance(&id, local_port); - } - - self.sessions.lock().expect("ssh sessions mutex").insert( - id.clone(), - SshSession { - instance: instance.clone(), - parsed, - session_dir, - control_path, - local_port, - remote_port, - started_by_us, - master, - master_detached: false, - main_forward, - main_forward_detached, - extra_forwards, - }, - ); - - self.clear_retry_attempt(&id); - self.set_status( - app, - &id, - DesktopSshPhase::Ready, - if extra_errors.is_empty() { - Some("SSH instance is ready".to_string()) - } else { - Some(format!( - "SSH instance is ready with forward warnings: {}", - extra_errors.join("; ") - )) - }, - Some(local_url), - Some(local_port), - Some(remote_port), - started_by_us, - 0, - false, - ); - - self.spawn_monitor(app.clone(), id); - Ok(()) - } - - fn spawn_monitor(self: &Arc, app: AppHandle, id: String) { - self.cancel_monitor_task(&id); - let inner = Arc::clone(self); - let id_for_task = id.clone(); - let handle = tauri::async_runtime::spawn(async move { - let mut healthy_ticks: u32 = 0; - loop { - let poll_secs = if healthy_ticks >= MONITOR_STABILIZE_TICKS { - MONITOR_STEADY_POLL_SECS - } else { - MONITOR_INITIAL_POLL_SECS - }; - tokio::time::sleep(Duration::from_secs(poll_secs)).await; - - let mut dropped_reason: Option = None; - let mut detached_notice: Option = None; - { - let mut sessions = inner.sessions.lock().expect("ssh sessions mutex"); - let Some(session) = sessions.get_mut(&id_for_task) else { - break; - }; - - let mut main_anchor_alive = false; - - if !session.main_forward_detached { - if let Some(status) = session.main_forward.try_wait().ok().flatten() { - if status.success() { - session.main_forward_detached = true; - detached_notice = Some( - "Main tunnel helper exited after ControlMaster handoff" - .to_string(), - ); - } else { - let mut stderr = String::new(); - if let Some(mut stream) = session.main_forward.stderr.take() { - let _ = stream.read_to_string(&mut stderr); - } - dropped_reason = Some(if stderr.trim().is_empty() { - format!("Main SSH forward exited ({status})") - } else { - format!("Main SSH forward exited ({status}): {}", stderr.trim()) - }); - } - } else { - main_anchor_alive = true; - } - } - - if dropped_reason.is_none() { - if main_anchor_alive { - if !session.master_detached { - if let Some(status) = session.master.try_wait().ok().flatten() { - if status.success() - && is_control_master_alive( - &session.parsed, - &session.control_path, - ) - { - session.master_detached = true; - if detached_notice.is_none() { - detached_notice = Some( - "SSH ControlMaster transitioned to detached background mode" - .to_string(), - ); - } - } else { - detached_notice = Some( - "SSH ControlMaster exited while main tunnel is still active" - .to_string(), - ); - } - } - } else if !is_control_master_alive( - &session.parsed, - &session.control_path, - ) { - detached_notice = Some( - "SSH ControlMaster is not reachable; main tunnel remains active" - .to_string(), - ); - } - } else if session.master_detached { - // Fast path: check local tunnel first (cheap TCP probe) - // before spawning an SSH subprocess for control master check. - if is_local_tunnel_reachable(session.local_port) { - // Tunnel is alive — skip the expensive SSH check entirely. - } else if !is_control_master_alive( - &session.parsed, - &session.control_path, - ) { - dropped_reason = - Some("SSH ControlMaster is not reachable".to_string()); - } else { - detached_notice = Some( - "Local tunnel unreachable but ControlMaster is alive" - .to_string(), - ); - } - } else if let Some(status) = session.master.try_wait().ok().flatten() { - if status.success() - && is_control_master_alive(&session.parsed, &session.control_path) - { - session.master_detached = true; - if detached_notice.is_none() { - detached_notice = Some( - "SSH ControlMaster transitioned to detached background mode" - .to_string(), - ); - } - } else { - let mut stderr = String::new(); - if let Some(mut stream) = session.master.stderr.take() { - let _ = stream.read_to_string(&mut stderr); - } - dropped_reason = Some(if stderr.trim().is_empty() { - format!("SSH ControlMaster exited ({status})") - } else { - format!( - "SSH ControlMaster exited ({status}): {}", - stderr.trim() - ) - }); - } - } - } - } - - if let Some(message) = detached_notice { - inner.append_log_with_level(&id_for_task, "INFO", message); - } - - if dropped_reason.is_none() { - healthy_ticks = healthy_ticks.saturating_add(1); - continue; - } - - let dropped_reason = - dropped_reason.unwrap_or_else(|| "SSH connection dropped".to_string()); - inner.append_log_with_level(&id_for_task, "WARN", dropped_reason.clone()); - - inner.disconnect_internal(&app, &id_for_task, false); - let attempt = inner.next_retry_attempt(&id_for_task); - - if attempt > DEFAULT_RECONNECT_MAX_ATTEMPTS { - inner.set_status( - &app, - &id_for_task, - DesktopSshPhase::Error, - Some(format!("{dropped_reason}. Retry limit reached")), - None, - None, - None, - false, - attempt, - true, - ); - break; - } - - inner.set_status( - &app, - &id_for_task, - DesktopSshPhase::Degraded, - Some(format!("{dropped_reason}. Reconnecting")), - None, - None, - None, - false, - attempt, - false, - ); - - let delay_ms = - (2u64.saturating_pow(attempt.saturating_sub(1))).saturating_mul(1000); - let jitter = (now_millis() % 700).saturating_add(100); - tokio::time::sleep(Duration::from_millis( - delay_ms.min(30_000).saturating_add(jitter), - )) - .await; - - if let Err(err) = inner.start_connect(app.clone(), id_for_task.clone()) { - inner.set_status( - &app, - &id_for_task, - DesktopSshPhase::Error, - Some(err), - None, - None, - None, - false, - attempt, - true, - ); - } - break; - } - - inner - .monitor_tasks - .lock() - .expect("ssh monitor task mutex") - .remove(&id_for_task); - }); - self.monitor_tasks - .lock() - .expect("ssh monitor task mutex") - .insert(id, handle); - } - - fn start_connect(self: &Arc, app: AppHandle, id: String) -> Result<(), String> { - let config = read_desktop_ssh_instances_from_disk(); - let Some(instance) = config.instances.into_iter().find(|item| item.id == id) else { - return Err("SSH instance not found".to_string()); - }; - - if self - .connect_tasks - .lock() - .expect("ssh connect task mutex") - .contains_key(&id) - { - self.append_log_with_level(&id, "INFO", "Connection already in progress"); - return Ok(()); - } - - if self.session_is_alive(&id) { - let snapshot = self.status_snapshot_for_instance(&id); - self.set_status( - &app, - &id, - DesktopSshPhase::Ready, - Some("SSH session already active".to_string()), - snapshot.local_url, - snapshot.local_port, - snapshot.remote_port, - snapshot.started_by_us, - snapshot.retry_attempt, - false, - ); - self.append_log_with_level( - &id, - "INFO", - "Connection already active; reusing existing SSH session", - ); - return Ok(()); - } - - let retry_attempt = self.current_retry_attempt(&id); - let connect_attempt = self.next_connect_attempt(&id); - self.append_attempt_separator(&id, connect_attempt, retry_attempt); - self.append_log(&id, "Starting SSH connection"); - self.disconnect_internal(&app, &id, false); - - let id_for_task = id.clone(); - let inner = Arc::clone(self); - let app_for_task = app.clone(); - let handle = tauri::async_runtime::spawn(async move { - let result = tauri::async_runtime::spawn_blocking({ - let inner = Arc::clone(&inner); - let app = app_for_task.clone(); - let instance = instance.clone(); - move || inner.connect_blocking(&app, instance) - }) - .await; - - match result { - Ok(Ok(())) => {} - Ok(Err(err)) => { - inner.set_status( - &app_for_task, - &id_for_task, - DesktopSshPhase::Error, - Some(err.to_string()), - None, - None, - None, - false, - 0, - true, - ); - inner.disconnect_internal(&app_for_task, &id_for_task, false); - } - Err(err) => { - inner.set_status( - &app_for_task, - &id_for_task, - DesktopSshPhase::Error, - Some(format!("SSH task failed: {err}")), - None, - None, - None, - false, - 0, - true, - ); - inner.disconnect_internal(&app_for_task, &id_for_task, false); - } - } - - inner - .connect_tasks - .lock() - .expect("ssh connect task mutex") - .remove(&id_for_task); - }); - - self.connect_tasks - .lock() - .expect("ssh connect task mutex") - .insert(id, handle); - - Ok(()) - } - - fn statuses_with_defaults(&self) -> Vec { - let config = read_desktop_ssh_instances_from_disk(); - let statuses = self.statuses.lock().expect("ssh status mutex"); - let mut result = Vec::new(); - - for instance in config.instances { - result.push( - statuses - .get(&instance.id) - .cloned() - .unwrap_or_else(|| DesktopSshInstanceStatus::idle(instance.id)), - ); - } - - result.sort_by(|a, b| a.id.cmp(&b.id)); - result - } -} - -#[tauri::command] -pub fn desktop_ssh_logs( - state: State<'_, DesktopSshManagerState>, - id: String, - limit: Option, -) -> Result, String> { - let id = id.trim().to_string(); - if id.is_empty() || id == LOCAL_HOST_ID { - return Err("SSH instance id is required".to_string()); - } - let cap = limit.unwrap_or(200).min(MAX_LOG_LINES_PER_INSTANCE); - Ok(state.inner.logs_for_instance(&id, cap)) -} - -#[tauri::command] -pub fn desktop_ssh_logs_clear( - state: State<'_, DesktopSshManagerState>, - id: String, -) -> Result<(), String> { - let id = id.trim().to_string(); - if id.is_empty() || id == LOCAL_HOST_ID { - return Err("SSH instance id is required".to_string()); - } - state.inner.clear_logs_for_instance(&id); - Ok(()) -} - -impl DesktopSshManagerState { - pub fn shutdown_all(&self, app: &AppHandle) { - let ids: Vec = self - .inner - .sessions - .lock() - .expect("ssh sessions mutex") - .keys() - .cloned() - .collect(); - for id in ids { - self.inner.disconnect_internal(app, &id, false); - } - - let connect_ids: Vec = self - .inner - .connect_tasks - .lock() - .expect("ssh connect task mutex") - .keys() - .cloned() - .collect(); - for id in connect_ids { - self.inner.cancel_connect_task(&id); - } - - let monitor_ids: Vec = self - .inner - .monitor_tasks - .lock() - .expect("ssh monitor task mutex") - .keys() - .cloned() - .collect(); - for id in monitor_ids { - self.inner.cancel_monitor_task(&id); - } - } -} - -#[tauri::command] -pub fn desktop_ssh_instances_get() -> Result { - Ok(read_desktop_ssh_instances_from_disk()) -} - -#[tauri::command] -pub fn desktop_ssh_instances_set(config: DesktopSshInstancesConfig) -> Result<(), String> { - write_desktop_ssh_instances_to_path(&settings_file_path(), config) - .map(|_| ()) - .map_err(|err| err.to_string()) -} - -#[tauri::command] -pub fn desktop_ssh_import_hosts() -> Result, String> { - let mut candidates = Vec::new(); - - if let Some(home) = std::env::var_os("HOME") { - let user_config = PathBuf::from(home).join(".ssh").join("config"); - candidates.extend(parse_ssh_config_candidates(&user_config, "user")); - } - candidates.extend(parse_ssh_config_candidates( - Path::new("/etc/ssh/ssh_config"), - "global", - )); - - let mut seen = HashSet::new(); - candidates.retain(|item| seen.insert(item.host.clone())); - candidates.sort_by(|a, b| a.host.cmp(&b.host)); - Ok(candidates) -} - -#[tauri::command] -pub fn desktop_ssh_connect( - app: AppHandle, - state: State<'_, DesktopSshManagerState>, - id: String, -) -> Result<(), String> { - let id = id.trim().to_string(); - if id.is_empty() || id == LOCAL_HOST_ID { - return Err("SSH instance id is required".to_string()); - } - state.inner.start_connect(app, id) -} - -#[tauri::command] -pub fn desktop_ssh_disconnect( - app: AppHandle, - state: State<'_, DesktopSshManagerState>, - id: String, -) -> Result<(), String> { - let id = id.trim().to_string(); - if id.is_empty() || id == LOCAL_HOST_ID { - return Err("SSH instance id is required".to_string()); - } - state.inner.disconnect_internal(&app, &id, true); - Ok(()) -} - -#[tauri::command] -pub fn desktop_ssh_status( - state: State<'_, DesktopSshManagerState>, - id: Option, -) -> Result, String> { - if let Some(instance_id) = id - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - { - return Ok(vec![state.inner.status_snapshot_for_instance(&instance_id)]); - } - - Ok(state.inner.statuses_with_defaults()) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn sample_instance(id: &str, command: &str) -> DesktopSshInstance { - DesktopSshInstance { - id: id.to_string(), - nickname: None, - ssh_command: command.to_string(), - ssh_parsed: None, - connection_timeout_sec: DEFAULT_CONNECTION_TIMEOUT_SEC, - remote_openchamber: DesktopSshRemoteOpenchamberConfig::default(), - local_forward: DesktopSshLocalForwardConfig::default(), - auth: DesktopSshAuthConfig::default(), - port_forwards: Vec::new(), - } - } - - #[test] - fn parse_ssh_command_accepts_supported_options() { - let parsed = parse_ssh_command( - "ssh -J jump.example.com -o StrictHostKeyChecking=accept-new user@example.com", - ) - .expect("parsed"); - assert_eq!(parsed.destination, "user@example.com"); - assert_eq!( - parsed.args, - vec![ - "-J".to_string(), - "jump.example.com".to_string(), - "-o".to_string(), - "StrictHostKeyChecking=accept-new".to_string(), - ] - ); - } - - #[test] - fn parse_ssh_command_rejects_disallowed_flags() { - let err = parse_ssh_command("ssh -M user@example.com") - .expect_err("should reject control master flag"); - assert!(err.to_string().contains("not allowed")); - } - - #[test] - fn parse_ssh_command_rejects_disallowed_controlpath_option() { - let err = parse_ssh_command("ssh -o ControlPath=/tmp/ssh.sock user@example.com") - .expect_err("should reject controlpath override"); - assert!(err.to_string().contains("not allowed")); - } - - #[test] - fn parse_ssh_command_keeps_ipv6_destination() { - let parsed = - parse_ssh_command("ssh user@[2001:db8::1]:2222").expect("parsed ipv6 destination"); - assert_eq!(parsed.destination, "user@[2001:db8::1]:2222"); - } - - #[test] - fn sync_desktop_hosts_removes_deleted_ssh_hosts() { - let mut root = json!({ - "desktopHosts": [ - {"id": "ssh-old", "label": "Old", "url": "http://127.0.0.1:1"}, - {"id": "http-1", "label": "HTTP", "url": "https://example.com"} - ], - "desktopDefaultHostId": "ssh-old" - }); - - let mut previous = HashSet::new(); - previous.insert("ssh-old".to_string()); - - let next = vec![sample_instance("ssh-new", "ssh user@example.com")]; - sync_desktop_hosts_for_ssh(&mut root, &previous, &next); - - let hosts = root - .get("desktopHosts") - .and_then(Value::as_array) - .expect("hosts array"); - assert_eq!(hosts.len(), 2); - assert!(hosts - .iter() - .any(|item| item.get("id") == Some(&Value::String("http-1".to_string())))); - assert!(hosts - .iter() - .any(|item| item.get("id") == Some(&Value::String("ssh-new".to_string())))); - assert_eq!( - root.get("desktopDefaultHostId").and_then(Value::as_str), - Some("local") - ); - } - - #[test] - fn parse_ssh_config_candidates_extracts_host_entries() { - let temp = - std::env::temp_dir().join(format!("openchamber-ssh-import-{}.txt", now_millis())); - fs::write( - &temp, - "\nHost prod\n HostName 10.0.0.1\nHost *.dev !skip\nHost *\n", - ) - .expect("write temp"); - - let candidates = parse_ssh_config_candidates(&temp, "user"); - let _ = fs::remove_file(&temp); - - assert!(candidates - .iter() - .any(|item| item.host == "prod" && !item.pattern)); - assert!(candidates - .iter() - .any(|item| item.host == "*.dev" && item.pattern)); - assert!(!candidates.iter().any(|item| item.host == "*")); - } - - #[test] - fn sanitize_instance_applies_defaults_and_parsed_command() { - let mut instance = sample_instance("ssh-1", "ssh user@example.com"); - instance.connection_timeout_sec = 0; - instance.local_forward.bind_host = "".to_string(); - - let normalized = sanitize_instance(instance).expect("sanitize instance"); - assert_eq!( - normalized.connection_timeout_sec, - DEFAULT_CONNECTION_TIMEOUT_SEC - ); - assert_eq!(normalized.local_forward.bind_host, "127.0.0.1"); - assert_eq!( - normalized.ssh_parsed.expect("parsed").destination, - "user@example.com" - ); - } - - #[test] - fn parse_probe_status_line_extracts_numeric_status() { - assert_eq!( - parse_probe_status_line(Some("INFO_STATUS=401"), "INFO_STATUS="), - Some(401) - ); - assert_eq!( - parse_probe_status_line(Some("INFO_STATUS=abc"), "INFO_STATUS="), - None - ); - assert_eq!( - parse_probe_status_line(Some("WRONG=200"), "INFO_STATUS="), - None - ); - } - - #[test] - fn liveness_status_accepts_success_and_auth_challenges() { - assert!(is_liveness_http_status(200)); - assert!(is_liveness_http_status(204)); - assert!(is_liveness_http_status(401)); - assert!(is_liveness_http_status(403)); - assert!(!is_liveness_http_status(500)); - assert!(!is_liveness_http_status(0)); - } -} diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json deleted file mode 100644 index b60ce1c9..00000000 --- a/packages/desktop/src-tauri/tauri.conf.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "../node_modules/@tauri-apps/cli/schema.json", - "productName": "OpenChamber", - "version": "1.12.0", - "identifier": "ai.opencode.openchamber", - "build": { - "beforeDevCommand": "node ./scripts/dev-web-server.mjs", - "beforeBuildCommand": "bun run build:sidecar", - "devUrl": "http://127.0.0.1:3901", - "frontendDist": "../noop-dist" - }, - "app": { - "windows": [ - { - "label": "main", - "create": false, - "title": "OpenChamber", - "width": 1280, - "height": 800, - "resizable": true, - "fullscreen": false, - "decorations": true, - "hiddenTitle": true, - "titleBarStyle": "Overlay", - "trafficLightPosition": { - "x": 17, - "y": 26 - }, - "dragDropEnabled": false, - "visible": false - } - ], - "security": { - "csp": null - }, - "withGlobalTauri": true, - "macOSPrivateApi": true - }, - "bundle": { - "active": true, - "externalBin": [ - "sidecars/openchamber-server" - ], - "resources": [ - "resources/web-dist/**/*" - ], - "icon": [ - "icons/icon.icns", - "icons/icon.png" - ], - "macOS": { - "exceptionDomain": "localhost", - "minimumSystemVersion": "13.0", - "signingIdentity": null, - "entitlements": "./entitlements.plist", - "infoPlist": "Info.plist", - "dmg": { - "appPosition": { - "x": 180, - "y": 170 - }, - "applicationFolderPosition": { - "x": 480, - "y": 170 - }, - "windowSize": { - "width": 660, - "height": 400 - } - } - }, - "createUpdaterArtifacts": true - }, - "plugins": { - "updater": { - "endpoints": [ - "https://github.com/btriapitsyn/openchamber/releases/latest/download/latest.json" - ], - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwRDMyQUQzNjNFRTc1ODIKUldTQ2RlNWoweXJUSUdpVWVWNm84R1pHamYzNVFhYWgyWmlpWFVzem5nUTlHd1dlRlNTV0FFc3IK" - } - } -} diff --git a/packages/desktop/src-tauri/tauri.dev.conf.json b/packages/desktop/src-tauri/tauri.dev.conf.json deleted file mode 100644 index 79324ff9..00000000 --- a/packages/desktop/src-tauri/tauri.dev.conf.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "../node_modules/@tauri-apps/cli/schema.json", - "bundle": { - "icon": [ - "icons/dev-icon.icns", - "icons/dev-icon.png" - ] - } -} diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 00630bb1..8bef3e6f 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -152,7 +152,6 @@ const LOCAL_DESKTOP_CLIENT_KIND = 'desktop-local'; const LOCAL_DESKTOP_CLIENT_DEDUPE_KEY = 'desktop-local'; const ENV_OVERRIDE_HOST_ID = '__env'; const CHANGELOG_URL = 'https://raw.githubusercontent.com/openchamber/openchamber/main/CHANGELOG.md'; -const UPDATE_METADATA_URL = 'https://github.com/openchamber/openchamber/releases/latest/download/latest.json'; const GITHUB_BUG_REPORT_URL = 'https://github.com/openchamber/openchamber/issues/new?template=bug_report.yml'; const GITHUB_FEATURE_REQUEST_URL = 'https://github.com/openchamber/openchamber/issues/new?template=feature_request.yml'; const DISCORD_INVITE_URL = 'https://discord.gg/ZYRSdnwwKA'; @@ -1681,7 +1680,6 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } backgroundColor: '#151313', frame: process.platform === 'win32' ? false : undefined, autoHideMenuBar: autoHidesNativeMenuBar, - // Tauri used an overlay title bar with explicit traffic-light placement. // Electron's hiddenInset adds its own extra inset, which leaves the controls // visibly lower than the app header. Use a plain hidden title bar instead. titleBarStyle: usesCustomTitleBar ? 'hidden' : 'default', @@ -1704,7 +1702,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } // sandbox must stay off: the preload uses contextBridge + ipcRenderer // from Electron's Node layer. contextIsolation + nodeIntegration:false // keep the renderer world walled off from Node. Do NOT flip to true — - // the preload would fail to load and __TAURI__ would go undefined. + // the preload would fail to load and the desktop bridge would be unavailable. sandbox: false, }, }; @@ -3028,9 +3026,8 @@ const handleInvoke = async (browserWindow, command, args = {}) => { } case 'desktop_set_vibrancy': { - // Vibrancy (macOS blur) is not supported in the Electron shell — the - // Tauri build used NSVisualEffectView via Tauri plugin, Electron has - // no equivalent for our titleBarStyle:'hidden' setup. Persist the + // Vibrancy (macOS blur) is not supported in the Electron shell for our + // titleBarStyle:'hidden' setup. Persist the // disabled state so settings UI reflects it; args.enabled is ignored. await mutateSettingsRoot((root) => { root.desktopVibrancy = false; @@ -3040,13 +3037,6 @@ const handleInvoke = async (browserWindow, command, args = {}) => { case 'desktop_check_for_updates': { const currentVersion = APP_VERSION; - let payload = null; - try { - const response = await fetch(UPDATE_METADATA_URL, { signal: AbortSignal.timeout(10_000) }); - payload = await response.json(); - } catch { - } - let updateResult = null; try { updateResult = await autoUpdater.checkForUpdates(); @@ -3056,14 +3046,12 @@ const handleInvoke = async (browserWindow, command, args = {}) => { const updateInfo = updateResult?.updateInfo; const nextVersion = (typeof updateInfo?.version === 'string' && updateInfo.version) || - (typeof payload?.version === 'string' && payload.version) || currentVersion; const available = compareSemver(nextVersion, currentVersion) > 0; const body = - (typeof payload?.notes === 'string' && payload.notes.trim() ? payload.notes : null) || (typeof updateInfo?.releaseNotes === 'string' && updateInfo.releaseNotes.trim() ? updateInfo.releaseNotes : null) || await parseRelevantChangelogNotes(currentVersion, nextVersion); - state.pendingUpdate = available ? { version: nextVersion, metadata: payload, electronUpdate: updateResult } : null; + state.pendingUpdate = available ? { version: nextVersion, electronUpdate: updateResult } : null; return { available, currentVersion, @@ -3071,7 +3059,7 @@ const handleInvoke = async (browserWindow, command, args = {}) => { body: body || null, date: (typeof updateInfo?.releaseDate === 'string' && updateInfo.releaseDate) || - (typeof payload?.pub_date === 'string' ? payload.pub_date : null), + null, }; } diff --git a/packages/electron/preload.mjs b/packages/electron/preload.mjs index e4e32d48..6a2fb9b3 100644 --- a/packages/electron/preload.mjs +++ b/packages/electron/preload.mjs @@ -24,8 +24,8 @@ const macosMajor = Number.parseInt(macosMajorRaw, 10); // Remote UIs still need it so isDesktopShell() returns true and the // window renders with desktop affordances (DesktopHostSwitcher, // title bar offsets, etc.). Expose unconditionally. -// - __TAURI__ is the IPC channel to the main process. The compatibility -// shim is exposed broadly, but privileged commands are gated in main.mjs. +// - __OPENCHAMBER_DESKTOP__ is the IPC channel to the main process. It is +// exposed broadly, but privileged commands are gated in main.mjs. // Local-only globals below stay limited to packaged UI / exact localOrigin. // Everything driven by localOrigin (home dir, macOS hints) also stays // local-only since it leaks info about the Electron host machine. @@ -136,21 +136,13 @@ ipcRenderer.on('openchamber:emit', (_evt, payload) => { dispatchNativeEvent(event, payload.detail); }); -// __TAURI__ is exposed on all pages; the main-process gate in +// The desktop bridge is exposed on all pages; the main-process gate in // ipcMain.handle('openchamber:invoke') decides per-command what is safe // for non-local callers (window/host-switcher ops yes, file/shell ops // no). See COMMANDS_SAFE_FOR_REMOTE in main.mjs. -contextBridge.exposeInMainWorld('__TAURI__', { - core: { - invoke: (cmd, args) => ipcRenderer.invoke('openchamber:invoke', cmd, args || {}), - }, - dialog: { - open: (options) => ipcRenderer.invoke('openchamber:dialog:open', options || {}), - }, - shell: { - open: (url) => ipcRenderer.invoke('openchamber:invoke', 'desktop_open_external_url', { url }), - }, - event: { - listen: async (event, handler) => addListener(event, handler), - }, +contextBridge.exposeInMainWorld('__OPENCHAMBER_DESKTOP__', { + invoke: (cmd, args) => ipcRenderer.invoke('openchamber:invoke', cmd, args || {}), + openDialog: (options) => ipcRenderer.invoke('openchamber:dialog:open', options || {}), + openExternal: (url) => ipcRenderer.invoke('openchamber:invoke', 'desktop_open_external_url', { url }), + listen: async (event, handler) => addListener(event, handler), }); diff --git a/packages/ui/package.json b/packages/ui/package.json index aa4b8983..30040f8f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -83,7 +83,6 @@ "devDependencies": { "@eslint/js": "^9.33.0", "@tailwindcss/postcss": "^4.0.0", - "@tauri-apps/api": "^2.10.1", "@types/node": "^24.3.1", "@types/prismjs": "^1.26.6", "@types/qrcode": "^1.5.5", diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index a8981772..fc096ec5 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -17,7 +17,7 @@ import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt'; import { useWindowTitle } from '@/hooks/useWindowTitle'; import { useConfigStore } from '@/stores/useConfigStore'; import { hasModifier } from '@/lib/utils'; -import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell, restartDesktopApp } from '@/lib/desktop'; +import { isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp } from '@/lib/desktop'; import { getInjectedBootOutcome, getBootInjectionStatus, @@ -758,7 +758,7 @@ function App({ apis }: AppProps) { const handleDesktopBootDismiss = React.useCallback(async () => { if (shouldRestartDesktopBootFlow({ - isTauriShell: isTauriShell(), + isDesktopShell: isDesktopShell(), isDesktopLocalOriginActive: isDesktopLocalOriginActive(), })) { await restartDesktopApp(); diff --git a/packages/ui/src/components/auth/SessionAuthGate.tsx b/packages/ui/src/components/auth/SessionAuthGate.tsx index 43037233..dfcc5e9e 100644 --- a/packages/ui/src/components/auth/SessionAuthGate.tsx +++ b/packages/ui/src/components/auth/SessionAuthGate.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui'; -import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop'; +import { invokeDesktop, isDesktopShell, isVSCodeRuntime } from '@/lib/desktop'; import { syncDesktopSettings, initializeAppearancePreferences } from '@/lib/persistence'; import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence'; import { DesktopHostSwitcherInline } from '@/components/desktop/DesktopHostSwitcher'; @@ -129,11 +129,7 @@ const issueDesktopClientTokenViaShell = async (password: string, trustDevice: bo if (!isDesktopShell() || typeof window === 'undefined') { return ''; } - const invoke = (window as unknown as { __TAURI__?: { core?: { invoke?: (cmd: string, args?: Record) => Promise } } }).__TAURI__?.core?.invoke; - if (typeof invoke !== 'function') { - return ''; - } - const response = await invoke('desktop_remote_password_login', { + const response = await invokeDesktop('desktop_remote_password_login', { url: getRuntimeApiBaseUrl(), password, trustDevice, diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 059c5d73..3d9b7a51 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -36,8 +36,7 @@ import { useCurrentSessionActivity } from '@/hooks/useSessionActivity'; import { toast } from '@/components/ui'; import { Button } from '@/components/ui/button'; // useMessageStore removed — messages now come from sync system -import { isTauriShell, isVSCodeRuntime } from '@/lib/desktop'; -import { runtimeFetch } from '@/lib/runtime-fetch'; +import { isVSCodeRuntime } from '@/lib/desktop'; import { isIMECompositionEvent } from '@/lib/ime'; import { StopIcon } from '@/components/icons/StopIcon'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; @@ -968,7 +967,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const suppressNextFileDropTextInsertTimeoutRef = React.useRef | null>(null); const pendingDroppedAbsolutePathsRef = React.useRef([]); const canAcceptDropRef = React.useRef(false); - const nativeDragInsideDropZoneRef = React.useRef(false); const mentionRef = React.useRef(null); const commandRef = React.useRef(null); const skillRef = React.useRef(null); @@ -3407,121 +3405,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo } }; - // Tauri desktop: handle native file drops via onDragDropEvent - React.useEffect(() => { - if (!isTauriShell()) return; - let cancelled = false; - let unlisten: (() => void) | null = null; - - void (async () => { - try { - const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow'); - const webviewWindow = getCurrentWebviewWindow(); - const removeListener = await webviewWindow.onDragDropEvent(async (event) => { - if (!canAcceptDropRef.current) return; - - const payload = (event as { payload?: unknown }).payload; - if (!payload || typeof payload !== 'object') return; - - const typed = payload as { type?: string; paths?: string[]; position?: { x?: number; y?: number } }; - const type = typed.type; - const x = typed.position?.x; - const y = typed.position?.y; - - // Check if drop is inside the chat input area - const zone = dropZoneRef.current; - let inZone: boolean | null = null; - if (zone && typeof x === 'number' && typeof y === 'number') { - const rect = zone.getBoundingClientRect(); - inZone = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; - // Handle retina displays where Tauri might report physical pixels - if (!inZone && window.devicePixelRatio > 1) { - const sx = x / window.devicePixelRatio; - const sy = y / window.devicePixelRatio; - inZone = sx >= rect.left && sx <= rect.right && sy >= rect.top && sy <= rect.bottom; - } - } - - if (type === 'enter' || type === 'over') { - if (inZone !== null) { - nativeDragInsideDropZoneRef.current = inZone; - } - setIsDragging(nativeDragInsideDropZoneRef.current); - return; - } - if (type === 'leave') { - nativeDragInsideDropZoneRef.current = false; - setIsDragging(false); - return; - } - if (type === 'drop') { - const shouldHandleDrop = inZone ?? nativeDragInsideDropZoneRef.current; - nativeDragInsideDropZoneRef.current = false; - setIsDragging(false); - if (!shouldHandleDrop) return; - - const paths = Array.isArray(typed.paths) - ? typed.paths.filter((p): p is string => typeof p === 'string') - : []; - if (paths.length === 0) return; - - for (const path of paths) { - try { - const normalizedPath = normalizeDroppedPath(path); - const fileName = normalizedPath.split(/[\\/]/).pop() || normalizedPath; - let file: File; - - // In Tauri shell, dropped paths are local machine paths. - // Read bytes via native command to avoid workspace-bound /api/fs/raw restrictions. - if (isTauriShell()) { - const { invoke } = await import('@tauri-apps/api/core'); - const result = await invoke<{ mime: string; base64: string }>('desktop_read_file', { path: normalizedPath }); - const byteCharacters = atob(result.base64); - const byteNumbers = new Array(byteCharacters.length); - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - const byteArray = new Uint8Array(byteNumbers); - const blob = new Blob([byteArray], { type: result.mime || 'application/octet-stream' }); - file = new File([blob], fileName, { type: result.mime || 'application/octet-stream' }); - } else { - const response = await runtimeFetch('/api/fs/raw', { query: { path: normalizedPath } }); - if (!response.ok) { - throw new Error(`Failed to read dropped file (${response.status})`); - } - const blob = await response.blob(); - file = new File([blob], fileName, { type: blob.type || 'application/octet-stream' }); - } - - await addAttachedFile(file); - } catch (error) { - console.error('Failed to attach dropped file:', path, error); - toast.error(t('chat.chatInput.toast.attachNamedFailed', { - name: path.split(/[\\/]/).pop() || t('chat.chatInput.fileFallback'), - })); - } - } - } - }); - - if (cancelled) { - removeListener(); - return; - } - unlisten = removeListener; - } catch (error) { - if (!cancelled) { - console.warn('Failed to register Tauri drag-drop listener:', error); - } - } - })(); - - return () => { - cancelled = true; - if (unlisten) unlisten(); - }; - }, [addAttachedFile, normalizeDroppedPath, t]); - const fileInputRef = React.useRef(null); const attachFiles = React.useCallback(async (files: FileList | File[]) => { diff --git a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx index 72a73685..8003c1b0 100644 --- a/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx +++ b/packages/ui/src/components/desktop/DesktopHostSwitcher.tsx @@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; import { toast } from '@/components/ui'; -import { isElectronShell, isTauriShell, isDesktopShell } from '@/lib/desktop'; +import { isElectronShell, isDesktopShell } from '@/lib/desktop'; import { Icon } from "@/components/icon/Icon"; import { useUIStore } from '@/stores/useUIStore'; import { useI18n } from '@/lib/i18n'; @@ -354,7 +354,7 @@ export function DesktopHostSwitcherDialog({ }, [allHosts, defaultHostId, t]); const persist = React.useCallback(async (nextHosts: DesktopHost[], nextDefaultHostId: string | null) => { - if (!isTauriShell()) return; + if (!isDesktopShell()) return; setIsSaving(true); setError(''); try { @@ -376,7 +376,7 @@ export function DesktopHostSwitcherDialog({ }, [onOpenChange, setSettingsDialogOpen, setSettingsPage]); const refresh = React.useCallback(async () => { - if (!isTauriShell()) return; + if (!isDesktopShell()) return; setIsLoading(true); setError(''); try { @@ -408,7 +408,7 @@ export function DesktopHostSwitcherDialog({ }, [t]); const probeAll = React.useCallback(async (hosts: DesktopHost[]) => { - if (!isTauriShell()) return; + if (!isDesktopShell()) return; setIsProbing(true); const nextProbingHostIds: Record = {}; for (const host of hosts) { @@ -458,7 +458,7 @@ export function DesktopHostSwitcherDialog({ }, [open, allHosts, probeAll]); React.useEffect(() => { - if (!open || !isTauriShell()) { + if (!open || !isDesktopShell()) { return; } let cancelled = false; @@ -512,7 +512,7 @@ export function DesktopHostSwitcherDialog({ const isSshHost = Boolean(sshHostIds[host.id]); - if (host.id !== LOCAL_HOST_ID && isSshHost && isTauriShell()) { + if (host.id !== LOCAL_HOST_ID && isSshHost && isDesktopShell()) { let existingStatus = sshStatusesById[host.id]; const latestStatus = await desktopSshStatus(host.id) .then((items) => items.find((item) => item.id === host.id) || null) @@ -596,7 +596,7 @@ export function DesktopHostSwitcherDialog({ } } - if (host.id !== LOCAL_HOST_ID && isTauriShell()) { + if (host.id !== LOCAL_HOST_ID && isDesktopShell()) { setSwitchingHostId(host.id); const probe = await desktopHostProbe(origin, { clientToken: host.clientToken || null }).catch((): HostProbeResult => ({ status: 'unreachable', latencyMs: 0 })); setStatusById((prev) => ({ @@ -705,7 +705,7 @@ export function DesktopHostSwitcherDialog({ error: null, }); - if (!hostId || hostId === LOCAL_HOST_ID || !isTauriShell()) { + if (!hostId || hostId === LOCAL_HOST_ID || !isDesktopShell()) { return; } @@ -721,7 +721,7 @@ export function DesktopHostSwitcherDialog({ }, [allHosts, handleSwitch, sshSwitchModal.hostId]); const connectSshHostInPlace = React.useCallback(async (host: DesktopHost) => { - if (!isTauriShell()) return; + if (!isDesktopShell()) return; setSwitchingHostId(host.id); try { await desktopSshConnect(host.id); @@ -750,7 +750,7 @@ export function DesktopHostSwitcherDialog({ return null; } - const tauriAvailable = isTauriShell(); + const desktopAvailable = isDesktopShell(); const content = ( <> @@ -772,7 +772,7 @@ export function DesktopHostSwitcherDialog({ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary' )} onClick={() => void probeAll(allHosts)} - disabled={!tauriAvailable || isLoading || isProbing} + disabled={!desktopAvailable || isLoading || isProbing} aria-label={t('desktopHostSwitcher.actions.refreshInstancesAria')} > @@ -805,7 +805,7 @@ export function DesktopHostSwitcherDialog({ size="sm" variant="ghost" onClick={() => void probeAll(allHosts)} - disabled={!tauriAvailable || isLoading || isProbing} + disabled={!desktopAvailable || isLoading || isProbing} > {t('desktopHostSwitcher.actions.refresh')} @@ -814,7 +814,7 @@ export function DesktopHostSwitcherDialog({ )} - {!tauriAvailable && ( + {!desktopAvailable && (
    {t('desktopHostSwitcher.state.limitedOnPage')} @@ -974,7 +974,7 @@ export function DesktopHostSwitcherDialog({
    - {tauriAvailable && editingId && editingId !== LOCAL_HOST_ID && ( + {desktopAvailable && editingId && editingId !== LOCAL_HOST_ID && (
    {t('desktopHostSwitcher.edit.title')}
    @@ -1202,7 +1202,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost }, [connectDefaultSshInstance, startupSshModal.hostId, startupSshModal.hostLabel]); React.useEffect(() => { - if (!isTauriShell()) return; + if (!isDesktopShell()) return; let cancelled = false; const run = async () => { diff --git a/packages/ui/src/components/desktop/OpenInAppButton.tsx b/packages/ui/src/components/desktop/OpenInAppButton.tsx index dc7ae966..24bccc12 100644 --- a/packages/ui/src/components/desktop/OpenInAppButton.tsx +++ b/packages/ui/src/components/desktop/OpenInAppButton.tsx @@ -10,7 +10,7 @@ import { toast } from '@/components/ui'; import { Icon } from "@/components/icon/Icon"; import { copyTextToClipboard } from '@/lib/clipboard'; import { cn } from '@/lib/utils'; -import { isDesktopLocalOriginActive, isTauriShell, openDesktopPath, openDesktopProjectInApp } from '@/lib/desktop'; +import { isDesktopLocalOriginActive, isDesktopShell, openDesktopPath, openDesktopProjectInApp } from '@/lib/desktop'; import { DEFAULT_OPEN_IN_APP_ID, OPEN_IN_APPS } from '@/lib/openInApps'; import { useOpenInAppsStore, type OpenInAppOption } from '@/stores/useOpenInAppsStore'; import { useI18n } from '@/lib/i18n'; @@ -87,7 +87,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps) initialize(); }, [initialize]); - const isDesktopLocal = isTauriShell() && isDesktopLocalOriginActive(); + const isDesktopLocal = isDesktopShell() && isDesktopLocalOriginActive(); const selectedApp = React.useMemo(() => { const known = availableApps.find((app) => app.id === selectedAppId) diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 7903aa89..dc02f2cc 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -1660,15 +1660,12 @@ export const Header: React.FC = ({ } let disposed = false; - let unlistenResize: (() => void) | null = null; const syncFullscreenState = async () => { try { - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - const currentWindow = getCurrentWindow(); - const fullscreen = await currentWindow.isFullscreen(); + const fullscreen = await invokeDesktop('desktop_is_window_fullscreen'); if (!disposed) { - setIsDesktopWindowFullscreen(fullscreen); + setIsDesktopWindowFullscreen(fullscreen === true); } } catch { if (!disposed) { @@ -1677,26 +1674,16 @@ export const Header: React.FC = ({ } }; - const attach = async () => { - try { - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - const currentWindow = getCurrentWindow(); - unlistenResize = await currentWindow.onResized(() => { - void syncFullscreenState(); - }); - } catch { - // Ignore listener setup failures; fallback state remains false. - } + const onResize = () => { + void syncFullscreenState(); }; void syncFullscreenState(); - void attach(); + window.addEventListener('openchamber:window-resized', onResize); return () => { disposed = true; - if (unlistenResize) { - unlistenResize(); - } + window.removeEventListener('openchamber:window-resized', onResize); }; }, [isDesktopApp, isMacPlatform]); diff --git a/packages/ui/src/components/onboarding/ChooserScreen.tsx b/packages/ui/src/components/onboarding/ChooserScreen.tsx index 88e19162..754aefc3 100644 --- a/packages/ui/src/components/onboarding/ChooserScreen.tsx +++ b/packages/ui/src/components/onboarding/ChooserScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { isDesktopShell, isTauriShell, startDesktopWindowDrag } from '@/lib/desktop'; +import { isDesktopShell, requestFileAccess, startDesktopWindowDrag } from '@/lib/desktop'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Icon } from "@/components/icon/Icon"; @@ -116,7 +116,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) { }, []); const persistFirstChoice = React.useCallback(async (choice: 'local' | 'remote') => { - if (!isTauriShell()) return; + if (!isDesktopApp) return; const config = await desktopHostsGet(); await desktopHostsSet({ @@ -124,14 +124,14 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) { ...(choice === 'local' ? { defaultHostId: 'local' } : {}), initialHostChoiceCompleted: true, }); - }, []); + }, [isDesktopApp]); const announceAvailable = React.useCallback(async () => { - if (isTauriShell()) { + if (isDesktopApp) { await persistFirstChoice('local'); } onCliAvailable?.(); - }, [onCliAvailable, persistFirstChoice]); + }, [isDesktopApp, onCliAvailable, persistFirstChoice]); // Background polling: while the local tab is visible, periodically check // whether the OpenCode CLI is reachable. As soon as it is, transition @@ -179,30 +179,23 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) { const handleBrowse = React.useCallback(async () => { if (typeof window === 'undefined') return; - if (!isDesktopApp || !isTauriShell()) return; - - const tauri = (window as unknown as { __TAURI__?: { dialog?: { open?: (opts: Record) => Promise } } }).__TAURI__; - if (!tauri?.dialog?.open) return; + if (!isDesktopApp) return; try { - const selected = await tauri.dialog.open({ - title: t('onboarding.localSetup.dialog.selectOpencodeBinary'), - multiple: false, - directory: false, - }); - if (typeof selected === 'string' && selected.trim().length > 0) { - setOpencodeBinary(selected.trim()); + const selected = await requestFileAccess(); + if (selected.success && selected.path && selected.path.trim().length > 0) { + setOpencodeBinary(selected.path.trim()); } } catch { // ignore } - }, [isDesktopApp, t]); + }, [isDesktopApp]); const handleApplyPath = React.useCallback(async () => { setIsApplyingPath(true); try { await updateDesktopSettings({ opencodeBinary: opencodeBinary.trim() }); - if (isTauriShell()) { + if (isDesktopApp) { await persistFirstChoice('local'); await restartDesktopApp(); return; @@ -211,7 +204,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) { } finally { setTimeout(() => setIsApplyingPath(false), 1000); } - }, [opencodeBinary, persistFirstChoice]); + }, [isDesktopApp, opencodeBinary, persistFirstChoice]); const handleCopy = React.useCallback(async () => { const result = await copyTextToClipboard(INSTALL_COMMAND); @@ -231,7 +224,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) { ? '/home/you/.bun/bin/opencode' : '/Users/you/.bun/bin/opencode'; - const showLocal = !isDesktopApp || !isTauriShell() || activeTab === 'local'; + const showLocal = !isDesktopApp || activeTab === 'local'; return (
    - {isDesktopApp && isTauriShell() && ( + {isDesktopApp && (
    diff --git a/packages/ui/src/components/onboarding/LocalSetupScreen.tsx b/packages/ui/src/components/onboarding/LocalSetupScreen.tsx index b04c4246..2d5f7434 100644 --- a/packages/ui/src/components/onboarding/LocalSetupScreen.tsx +++ b/packages/ui/src/components/onboarding/LocalSetupScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { isDesktopShell, isTauriShell } from '@/lib/desktop'; +import { isDesktopShell, requestFileAccess, startDesktopWindowDrag } from '@/lib/desktop'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Icon } from "@/components/icon/Icon"; @@ -122,14 +122,8 @@ export function LocalSetupScreen({ return; } if (e.button !== 0) return; - if (isDesktopApp && isTauriShell()) { - try { - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - const window = getCurrentWindow(); - await window.startDragging(); - } catch (error) { - console.error('Failed to start window dragging:', error); - } + if (isDesktopApp) { + await startDesktopWindowDrag(); } }, [isDesktopApp]); @@ -148,37 +142,28 @@ export function LocalSetupScreen({ if (typeof window === 'undefined') { return; } - if (!isDesktopApp || !isTauriShell()) { - return; - } - - const tauri = (window as unknown as { __TAURI__?: { dialog?: { open?: (opts: Record) => Promise } } }).__TAURI__; - if (!tauri?.dialog?.open) { + if (!isDesktopApp) { return; } try { - const selected = await tauri.dialog.open({ - title: t('onboarding.localSetup.dialog.selectOpencodeBinary'), - multiple: false, - directory: false, - }); - if (typeof selected === 'string' && selected.trim().length > 0) { - setOpencodeBinary(selected.trim()); + const selected = await requestFileAccess(); + if (selected.success && selected.path && selected.path.trim().length > 0) { + setOpencodeBinary(selected.path.trim()); } } catch { // ignore } - }, [isDesktopApp, t]); + }, [isDesktopApp]); const handleApplyPath = React.useCallback(async () => { setIsRetrying(true); try { await updateDesktopSettings({ opencodeBinary: opencodeBinary.trim() }); - // In desktop boot flow, always restart the entire Tauri app so Rust - // can re-evaluate the boot outcome with the updated binary path. - if (isTauriShell()) { + // In desktop boot flow, restart the app so the native host can + // re-evaluate the boot outcome with the updated binary path. + if (isDesktopApp) { await restartDesktopApp(); return; } @@ -187,7 +172,7 @@ export function LocalSetupScreen({ } finally { setTimeout(() => setIsRetrying(false), 1000); } - }, [opencodeBinary]); + }, [isDesktopApp, opencodeBinary]); const handleCopy = React.useCallback(async () => { const result = await copyTextToClipboard(INSTALL_COMMAND); @@ -321,7 +306,7 @@ export function LocalSetupScreen({ type="button" variant="secondary" onClick={handleBrowse} - disabled={isRetrying || !isDesktopApp || !isTauriShell()} + disabled={isRetrying || !isDesktopApp} > {t('onboarding.localSetup.actions.browse')} diff --git a/packages/ui/src/components/onboarding/RecoveryScreen.tsx b/packages/ui/src/components/onboarding/RecoveryScreen.tsx index 83fa4552..09a7af58 100644 --- a/packages/ui/src/components/onboarding/RecoveryScreen.tsx +++ b/packages/ui/src/components/onboarding/RecoveryScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { isTauriShell, restartDesktopApp } from '@/lib/desktop'; +import { isDesktopShell, restartDesktopApp } from '@/lib/desktop'; import { DesktopConnectionRecovery, type RecoveryVariant } from './DesktopConnectionRecovery'; import { RemoteConnectionForm } from './RemoteConnectionForm'; import { resolveRecoveryNextStep } from './desktopRecoveryRouting'; @@ -43,7 +43,7 @@ export function RecoveryScreen({ }: RecoveryScreenProps) { // Persist the user's first choice (local or remote) const persistFirstChoice = React.useCallback(async (choice: 'local' | 'remote') => { - if (!isTauriShell()) return; + if (!isDesktopShell()) return; const config = await desktopHostsGet(); await desktopHostsSet({ @@ -56,9 +56,9 @@ export function RecoveryScreen({ }, []); const handleRecoveryRetry = React.useCallback(async () => { - // In desktop boot flow, always restart the entire Tauri app so Rust - // can re-evaluate the boot outcome. - if (isTauriShell()) { + // In desktop boot flow, restart the app so the native host can + // re-evaluate the boot outcome. + if (isDesktopShell()) { await restartDesktopApp(); return; } @@ -77,7 +77,7 @@ export function RecoveryScreen({ // switch-default-to-local → persist local choice and restart await persistFirstChoice('local'); - if (isTauriShell()) { + if (isDesktopShell()) { await restartDesktopApp(); return; } @@ -105,7 +105,7 @@ export function RecoveryScreen({ isRecoveryMode={true} onSwitchToLocal={onSwitchToLocalFromRemote || (() => { persistFirstChoice('local').then(() => { - if (isTauriShell()) { + if (isDesktopShell()) { restartDesktopApp(); } else { onEnterLocalSetup?.(); diff --git a/packages/ui/src/components/onboarding/RemoteConnectionForm.tsx b/packages/ui/src/components/onboarding/RemoteConnectionForm.tsx index a17a91e9..161a58c4 100644 --- a/packages/ui/src/components/onboarding/RemoteConnectionForm.tsx +++ b/packages/ui/src/components/onboarding/RemoteConnectionForm.tsx @@ -8,7 +8,7 @@ import { } from '@/lib/desktopHosts'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { isTauriShell } from '@/lib/desktop'; +import { isDesktopShell, restartDesktopApp } from '@/lib/desktop'; import { useI18n } from '@/lib/i18n'; type ConnectionState = 'idle' | 'testing' | 'success' | 'error'; @@ -153,9 +153,8 @@ export function RemoteConnectionForm({ return; } - if (isTauriShell()) { - const tauri = (window as unknown as { __TAURI__?: { core?: { invoke?: (cmd: string, args?: Record) => Promise } } }).__TAURI__; - await tauri?.core?.invoke?.('desktop_restart'); + if (isDesktopShell()) { + await restartDesktopApp(); } } catch (err) { setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.failedToSaveConnection')); diff --git a/packages/ui/src/components/sections/openchamber/OpenCodeCliSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenCodeCliSettings.tsx index 7feaa686..6fa6a9d8 100644 --- a/packages/ui/src/components/sections/openchamber/OpenCodeCliSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenCodeCliSettings.tsx @@ -4,7 +4,7 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Icon } from "@/components/icon/Icon"; -import { isDesktopShell, isTauriShell } from '@/lib/desktop'; +import { isDesktopShell, requestFileAccess } from '@/lib/desktop'; import { updateDesktopSettings } from '@/lib/persistence'; import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore'; import { useUIStore } from '@/stores/useUIStore'; @@ -54,28 +54,19 @@ export const OpenCodeCliSettings: React.FC = () => { return; } - if (!isDesktopShell() || !isTauriShell()) { - return; - } - - const tauri = (window as unknown as { __TAURI__?: { dialog?: { open?: (opts: Record) => Promise } } }).__TAURI__; - if (!tauri?.dialog?.open) { + if (!isDesktopShell()) { return; } try { - const selected = await tauri.dialog.open({ - title: t('settings.openchamber.opencodeCli.dialog.selectBinaryTitle'), - multiple: false, - directory: false, - }); - if (typeof selected === 'string' && selected.trim().length > 0) { - setValue(selected.trim()); + const selected = await requestFileAccess(); + if (selected.success && selected.path && selected.path.trim().length > 0) { + setValue(selected.path.trim()); } } catch { // ignore } - }, [t]); + }, []); const handleSaveAndReload = React.useCallback(async () => { setIsSaving(true); @@ -130,7 +121,7 @@ export const OpenCodeCliSettings: React.FC = () => { variant="outline" size="xs" onClick={handleBrowse} - disabled={isLoading || isSaving || !isDesktopShell() || !isTauriShell()} + disabled={isLoading || isSaving || !isDesktopShell()} className="h-7 w-7 p-0" aria-label={t('settings.openchamber.opencodeCli.actions.browseAria')} title={t('settings.openchamber.opencodeCli.actions.browse')} diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index d951d3d3..baef3896 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -42,7 +42,6 @@ import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { EditorView } from '@codemirror/view'; import type { Extension } from '@codemirror/state'; -import { convertFileSrc } from '@tauri-apps/api/core'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useUIStore } from '@/stores/useUIStore'; import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore'; @@ -2629,7 +2628,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const srcPromise = files.readFileBinary ? files.readFileBinary(selectedFile.path, selectedFileReadOptions).then((result) => result.dataUrl) - : Promise.resolve(convertFileSrc(selectedFile.path, 'asset')); + : Promise.resolve(getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', { + path: selectedFile.path, + allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined, + })); await srcPromise .then((src) => { diff --git a/packages/ui/src/contexts/ThemeSystemContext.tsx b/packages/ui/src/contexts/ThemeSystemContext.tsx index 4bfa4f60..9db7bfa4 100644 --- a/packages/ui/src/contexts/ThemeSystemContext.tsx +++ b/packages/ui/src/contexts/ThemeSystemContext.tsx @@ -7,7 +7,7 @@ import React, { import { flushSync } from 'react-dom'; import type { Theme, ThemeMode } from '@/types/theme'; import type { DesktopSettings } from '@/lib/desktop'; -import { isDesktopLocalOriginActive, isTauriShell, isVSCodeRuntime } from '@/lib/desktop'; +import { isDesktopLocalOriginActive, isDesktopShell as detectDesktopShell, isVSCodeRuntime } from '@/lib/desktop'; import { setDesktopWindowTheme } from '@/lib/desktopNative'; import { CSSVariableGenerator } from '@/lib/theme/cssGenerator'; import { updateDesktopSettings } from '@/lib/persistence'; @@ -220,7 +220,7 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro }); const isVSCode = useMemo(() => isVSCodeRuntime(), []); const isLocalDesktopOrigin = useMemo(() => isDesktopLocalOriginActive(), []); - const isDesktopShell = useMemo(() => isTauriShell(), []); + const isDesktopShell = useMemo(() => detectDesktopShell(), []); const availableThemes = useMemo(() => { const merged: Theme[] = []; diff --git a/packages/ui/src/hooks/useMenuActions.ts b/packages/ui/src/hooks/useMenuActions.ts index e585e31a..d9bea5e7 100644 --- a/packages/ui/src/hooks/useMenuActions.ts +++ b/packages/ui/src/hooks/useMenuActions.ts @@ -56,17 +56,13 @@ const copyCurrentSelectionFallback = async (): Promise => { const MENU_ACTION_EVENT = 'openchamber:menu-action'; const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates'; -type TauriEventApi = { +type DesktopBridgeGlobal = { listen?: ( event: string, handler: (evt: { payload?: unknown }) => void ) => Promise<() => void>; }; -type TauriGlobal = { - event?: TauriEventApi; -}; - type MenuAction = | 'about' | 'settings' @@ -344,8 +340,8 @@ export const useMenuActions = ( React.useEffect(() => { if (typeof window === 'undefined') return; - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - const listen = tauri?.event?.listen; + const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__; + const listen = desktop?.listen; if (typeof listen !== 'function') return; let unlistenMenu: null | (() => void | Promise) = null; diff --git a/packages/ui/src/hooks/useRouter.ts b/packages/ui/src/hooks/useRouter.ts index e49f905e..c5896075 100644 --- a/packages/ui/src/hooks/useRouter.ts +++ b/packages/ui/src/hooks/useRouter.ts @@ -27,7 +27,7 @@ function isVSCodeContext(): boolean { * * Works in: * - Web: Full bidirectional sync - * - Desktop (Tauri): Full bidirectional sync + * - Desktop: Full bidirectional sync * - VS Code: State-only (no URL updates, reads initial params) */ export function useRouter(): void { diff --git a/packages/ui/src/hooks/useWindowTitle.ts b/packages/ui/src/hooks/useWindowTitle.ts index e05f0d38..87e64019 100644 --- a/packages/ui/src/hooks/useWindowTitle.ts +++ b/packages/ui/src/hooks/useWindowTitle.ts @@ -1,6 +1,6 @@ import React from 'react'; import { useProjectsStore } from '@/stores/useProjectsStore'; -import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell } from '@/lib/desktop'; +import { isDesktopLocalOriginActive, isDesktopShell } from '@/lib/desktop'; import { desktopHostsGet, getDesktopHostApiUrl, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts'; import { setDesktopWindowTitle } from '@/lib/desktopNative'; import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch'; @@ -110,7 +110,7 @@ export const useWindowTitle = () => { document.title = title; } - if (!isTauriShell()) { + if (!isDesktopShell()) { return; } diff --git a/packages/ui/src/lib/debug.ts b/packages/ui/src/lib/debug.ts index 7903e854..bafb8272 100644 --- a/packages/ui/src/lib/debug.ts +++ b/packages/ui/src/lib/debug.ts @@ -221,7 +221,7 @@ export const debugUtils = { })(); const runtimeApis = getRegisteredRuntimeAPIs(); - const isTauriShell = typeof window !== 'undefined' && Boolean((window as any).__TAURI__); + const isDesktopRuntime = typeof window !== 'undefined' && Boolean((window as { __OPENCHAMBER_ELECTRON__?: unknown }).__OPENCHAMBER_ELECTRON__); const safeJson = async (resp: Response) => { try { @@ -327,7 +327,7 @@ export const debugUtils = { const report = { runtime: { platform: runtimeApis?.runtime?.platform ?? null, - isDesktop: isTauriShell, + isDesktop: isDesktopRuntime, isVSCode: Boolean(runtimeApis?.runtime?.isVSCode), hasRuntimeApis: Boolean(runtimeApis), desktopServerOrigin: null, diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index bf52b185..a3143f08 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -185,19 +185,14 @@ export type DesktopSettings = { draftStarters?: DraftStarterRef[]; }; -type TauriGlobal = { - core?: { - invoke?: (cmd: string, args?: Record) => Promise; - }; - dialog?: { - open?: (options: Record) => Promise; - }; - event?: { - listen?: ( - event: string, - handler: (evt: { payload?: unknown }) => void, - ) => Promise<() => void>; - }; +type DesktopBridgeGlobal = { + invoke?: (cmd: string, args?: Record) => Promise; + openDialog?: (options: Record) => Promise; + openExternal?: (url: string) => Promise; + listen?: ( + event: string, + handler: (evt: { payload?: unknown }) => void, + ) => Promise<() => void>; }; type ElectronRuntimeGlobal = { @@ -209,27 +204,23 @@ const getElectronRuntime = (): ElectronRuntimeGlobal | null => { return (window as unknown as { __OPENCHAMBER_ELECTRON__?: ElectronRuntimeGlobal }).__OPENCHAMBER_ELECTRON__ ?? null; }; -export const isTauriShell = (): boolean => { - if (typeof window === 'undefined') return false; - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - return typeof tauri?.core?.invoke === 'function'; +const getDesktopBridge = (): DesktopBridgeGlobal | null => { + if (typeof window === 'undefined') return null; + return (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__ ?? null; }; export const isElectronShell = (): boolean => getElectronRuntime()?.runtime === 'electron'; export const hasDesktopInvoke = (): boolean => { - if (typeof window === 'undefined') return false; - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - return typeof tauri?.core?.invoke === 'function'; + return typeof getDesktopBridge()?.invoke === 'function'; }; export const canUseElectronDesktopIPC = (): boolean => isElectronShell() && hasDesktopInvoke(); export const invokeDesktop = async (command: string, args?: Record): Promise => { - if (typeof window === 'undefined') return null; - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - if (typeof tauri?.core?.invoke !== 'function') return null; - return tauri.core.invoke(command, args ?? {}) as Promise; + const bridge = getDesktopBridge(); + if (typeof bridge?.invoke !== 'function') return null; + return bridge.invoke(command, args ?? {}) as Promise; }; type LaunchAtLoginStatus = { @@ -367,18 +358,16 @@ export const isDesktopLocalOriginActive = (): boolean => { export const isDesktopShell = (): boolean => { if (typeof window === 'undefined') return false; - return isTauriShell() || isElectronShell(); + return isElectronShell(); }; export const startDesktopWindowDrag = async (): Promise => { - if (!isDesktopShell() || !isTauriShell()) { + if (!isDesktopShell()) { return false; } try { - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - const appWindow = getCurrentWindow(); - await appWindow.startDragging(); + await invokeDesktop('desktop_start_window_drag'); return true; } catch { return false; @@ -418,10 +407,9 @@ export const requestDirectoryAccess = async ( directoryPath: string ): Promise<{ success: boolean; path?: string; projectId?: string; error?: string }> => { // Desktop shell on local instance: use native folder picker. - if (isTauriShell() && isDesktopLocalOriginActive()) { + if (hasDesktopInvoke() && isDesktopLocalOriginActive()) { try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - const selected = await tauri?.dialog?.open?.({ + const selected = await getDesktopBridge()?.openDialog?.({ directory: true, multiple: false, title: 'Select Working Directory', @@ -431,7 +419,7 @@ export const requestDirectoryAccess = async ( } return { success: true, path: selected }; } catch (error) { - console.warn('Failed to request directory access (tauri)', error); + console.warn('Failed to request directory access', error); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } @@ -442,10 +430,9 @@ export const requestDirectoryAccess = async ( export const requestFileAccess = async ( options?: { filters?: Array<{ name: string; extensions: string[] }>; defaultPath?: string } ): Promise<{ success: boolean; path?: string; error?: string }> => { - if (isTauriShell() && isDesktopLocalOriginActive()) { + if (hasDesktopInvoke() && isDesktopLocalOriginActive()) { try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - const selected = await tauri?.dialog?.open?.({ + const selected = await getDesktopBridge()?.openDialog?.({ directory: false, multiple: false, title: 'Select File', @@ -457,7 +444,7 @@ export const requestFileAccess = async ( } return { success: true, path: selected }; } catch (error) { - console.warn('Failed to request file access (tauri)', error); + console.warn('Failed to request file access', error); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } @@ -482,10 +469,9 @@ export const stopAccessingDirectory = async ( export const sendAssistantCompletionNotification = async ( payload?: AssistantNotificationPayload ): Promise => { - if (isTauriShell()) { + if (hasDesktopInvoke()) { try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - await tauri?.core?.invoke?.('desktop_notify', { + await invokeDesktop('desktop_notify', { payload: { title: payload?.title, body: payload?.body, @@ -494,7 +480,7 @@ export const sendAssistantCompletionNotification = async ( }); return true; } catch (error) { - console.warn('Failed to send assistant completion notification (tauri)', error); + console.warn('Failed to send assistant completion notification', error); return false; } } @@ -503,16 +489,15 @@ export const sendAssistantCompletionNotification = async ( }; export const checkForDesktopUpdates = async (): Promise => { - if (!isTauriShell()) { + if (!hasDesktopInvoke()) { return null; } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - const info = await tauri?.core?.invoke?.('desktop_check_for_updates'); + const info = await invokeDesktop('desktop_check_for_updates'); return info as UpdateInfo; } catch (error) { - console.warn('Failed to check for updates (tauri)', error); + console.warn('Failed to check for updates', error); return null; } }; @@ -520,18 +505,18 @@ export const checkForDesktopUpdates = async (): Promise => { export const downloadDesktopUpdate = async ( onProgress?: (progress: UpdateProgress) => void ): Promise => { - if (!isTauriShell()) { + if (!hasDesktopInvoke()) { return false; } - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; + const bridge = getDesktopBridge(); let unlisten: null | (() => void | Promise) = null; let downloaded = 0; let total: number | undefined; try { - if (typeof onProgress === 'function' && tauri?.event?.listen) { - unlisten = await tauri.event.listen('openchamber:update-progress', (evt) => { + if (typeof onProgress === 'function' && bridge?.listen) { + unlisten = await bridge.listen('openchamber:update-progress', (evt) => { const payload = evt?.payload; if (!payload || typeof payload !== 'object') return; const data = payload as { event?: unknown; data?: unknown }; @@ -560,10 +545,10 @@ export const downloadDesktopUpdate = async ( }); } - await tauri?.core?.invoke?.('desktop_download_and_install_update'); + await invokeDesktop('desktop_download_and_install_update'); return true; } catch (error) { - console.warn('Failed to download update (tauri)', error); + console.warn('Failed to download update', error); return false; } finally { if (unlisten) { @@ -580,7 +565,7 @@ export const downloadDesktopUpdate = async ( }; export const restartToApplyUpdate = async (): Promise => { - if (!isTauriShell()) { + if (!hasDesktopInvoke()) { return false; } @@ -588,37 +573,35 @@ export const restartToApplyUpdate = async (): Promise => { }; export const restartDesktopApp = async (): Promise => { - if (!isTauriShell()) { + if (!hasDesktopInvoke()) { return false; } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - await tauri?.core?.invoke?.('desktop_restart'); + await invokeDesktop('desktop_restart'); return true; } catch (error) { - console.warn('Failed to restart desktop app (tauri)', error); + console.warn('Failed to restart desktop app', error); return false; } }; export const getDesktopLanAddress = async (): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return null; } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - const result = await tauri?.core?.invoke?.('desktop_get_lan_address'); + const result = await invokeDesktop('desktop_get_lan_address'); return typeof result === 'string' && result.trim().length > 0 ? result.trim() : null; } catch (error) { - console.warn('Failed to get desktop LAN address (tauri)', error); + console.warn('Failed to get desktop LAN address', error); return null; } }; export const openDesktopPath = async (path: string, app?: string | null): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return false; } @@ -628,20 +611,19 @@ export const openDesktopPath = async (path: string, app?: string | null): Promis } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - await tauri?.core?.invoke?.('desktop_open_path', { + await invokeDesktop('desktop_open_path', { path: trimmed, app: typeof app === 'string' && app.trim().length > 0 ? app.trim() : undefined, }); return true; } catch (error) { - console.warn('Failed to open path (tauri)', error); + console.warn('Failed to open path', error); return false; } }; export const revealDesktopPath = async (path: string): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return false; } @@ -651,8 +633,7 @@ export const revealDesktopPath = async (path: string): Promise => { } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - await tauri?.core?.invoke?.('desktop_reveal_path', { + await invokeDesktop('desktop_reveal_path', { path: trimmed, }); return true; @@ -665,7 +646,7 @@ export const saveDesktopMarkdownFile = async ( defaultFileName: string, content: string, ): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return null; } @@ -675,14 +656,13 @@ export const saveDesktopMarkdownFile = async ( } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - const result = await tauri?.core?.invoke?.('desktop_save_markdown_file', { + const result = await invokeDesktop('desktop_save_markdown_file', { defaultFileName: trimmedFileName, content, }); return typeof result === 'string' && result.trim().length > 0 ? result : null; } catch (error) { - console.warn('Failed to save markdown file (tauri)', error); + console.warn('Failed to save markdown file', error); return null; } }; @@ -692,7 +672,7 @@ export const openDesktopProjectInApp = async ( appId: string, appName: string, ): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return false; } @@ -705,8 +685,7 @@ export const openDesktopProjectInApp = async ( } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - await tauri?.core?.invoke?.('desktop_open_in_app', { + await invokeDesktop('desktop_open_in_app', { projectPath: trimmedProjectPath, appId: trimmedAppId, appName: trimmedAppName, @@ -723,7 +702,7 @@ export const openDesktopFileInApp = async ( appId: string, appName: string, ): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return false; } @@ -736,8 +715,7 @@ export const openDesktopFileInApp = async ( } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - await tauri?.core?.invoke?.('desktop_open_file_in_app', { + await invokeDesktop('desktop_open_file_in_app', { filePath: trimmedFilePath, appId: trimmedAppId, appName: trimmedAppName, @@ -750,7 +728,7 @@ export const openDesktopFileInApp = async ( }; export const filterInstalledDesktopApps = async (apps: string[]): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return []; } @@ -760,19 +738,18 @@ export const filterInstalledDesktopApps = async (apps: string[]): Promise('desktop_filter_installed_apps', { apps: candidate, }); return Array.isArray(result) ? result.filter((value) => typeof value === 'string') : []; } catch (error) { - console.warn('Failed to check installed apps (tauri)', error); + console.warn('Failed to check installed apps', error); return []; } }; export const fetchDesktopAppIcons = async (apps: string[]): Promise> => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return {}; } @@ -782,8 +759,7 @@ export const fetchDesktopAppIcons = async (apps: string[]): Promise('desktop_fetch_app_icons', { apps: candidate, }); if (!Array.isArray(result)) { @@ -798,7 +774,7 @@ export const fetchDesktopAppIcons = async (apps: string[]): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return { apps: [], success: false, hasCache: false, isCacheStale: false }; } @@ -829,8 +805,7 @@ export const fetchDesktopInstalledApps = async ( } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - const result = await tauri?.core?.invoke?.('desktop_get_installed_apps', { + const result = await invokeDesktop('desktop_get_installed_apps', { apps: candidate, force: force === true ? true : undefined, }); @@ -858,19 +833,18 @@ export const fetchDesktopInstalledApps = async ( isCacheStale: payload.isCacheStale === true, }; } catch (error) { - console.warn('Failed to fetch installed apps (tauri)', error); + console.warn('Failed to fetch installed apps', error); return { apps: [], success: false, hasCache: false, isCacheStale: false }; } }; export const clearDesktopCache = async (): Promise => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) { return false; } try { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - await tauri?.core?.invoke?.('desktop_clear_cache'); + await invokeDesktop('desktop_clear_cache'); return true; } catch (error) { console.warn('Failed to clear cache', error); diff --git a/packages/ui/src/lib/desktopBoot.test.ts b/packages/ui/src/lib/desktopBoot.test.ts index 8efb5590..9ab78c87 100644 --- a/packages/ui/src/lib/desktopBoot.test.ts +++ b/packages/ui/src/lib/desktopBoot.test.ts @@ -194,7 +194,7 @@ describe('shouldRestartDesktopBootFlow', () => { test('restarts the desktop app when boot UI is running in the startup window', () => { expect( shouldRestartDesktopBootFlow({ - isTauriShell: true, + isDesktopShell: true, isDesktopLocalOriginActive: false, }), ).toBe(true); @@ -203,16 +203,16 @@ describe('shouldRestartDesktopBootFlow', () => { test('does not restart when the local desktop origin is already active', () => { expect( shouldRestartDesktopBootFlow({ - isTauriShell: true, + isDesktopShell: true, isDesktopLocalOriginActive: true, }), ).toBe(false); }); - test('does not restart outside the tauri shell', () => { + test('does not restart outside the desktop shell', () => { expect( shouldRestartDesktopBootFlow({ - isTauriShell: false, + isDesktopShell: false, isDesktopLocalOriginActive: false, }), ).toBe(false); diff --git a/packages/ui/src/lib/desktopBoot.ts b/packages/ui/src/lib/desktopBoot.ts index ca9d2405..7c1d0880 100644 --- a/packages/ui/src/lib/desktopBoot.ts +++ b/packages/ui/src/lib/desktopBoot.ts @@ -222,14 +222,14 @@ export type InitialLoadingState = { }; export type DesktopBootFlowRestartInput = { - isTauriShell: boolean; + isDesktopShell: boolean; isDesktopLocalOriginActive: boolean; }; /** * Whether the initial loading screen can be dismissed. * - * Desktop shells must wait until a valid boot outcome is injected by Rust. + * Desktop shells must wait until a valid boot outcome is injected by the native host. * For non-main views (chooser, recovery), the splash can dismiss as soon as * the outcome is known — `isInitialized` is not required because OpenCode * may not be available in those flows. @@ -254,16 +254,16 @@ export function canDismissInitialLoading(state: InitialLoadingState): boolean { } /** - * Boot/recovery UI can render in the Tauri startup window before the local + * Boot/recovery UI can render in the desktop startup window before the local * desktop HTTP origin is active. In that state, same-origin reloads and - * `/api/*` requests cannot recover the app, so callers must restart Tauri. + * `/api/*` requests cannot recover the app, so callers must restart desktop. */ export function shouldRestartDesktopBootFlow(input: DesktopBootFlowRestartInput): boolean { - return input.isTauriShell && !input.isDesktopLocalOriginActive; + return input.isDesktopShell && !input.isDesktopLocalOriginActive; } /** - * Read the boot outcome injected by the Rust backend. + * Read the boot outcome injected by the native desktop host. * Returns `null` when not in desktop, when the outcome has not been set yet, * or when the injected payload is malformed. */ diff --git a/packages/ui/src/lib/desktopHosts.ts b/packages/ui/src/lib/desktopHosts.ts index 459d5d32..5dc39e96 100644 --- a/packages/ui/src/lib/desktopHosts.ts +++ b/packages/ui/src/lib/desktopHosts.ts @@ -1,12 +1,6 @@ -import { isTauriShell } from '@/lib/desktop'; +import { hasDesktopInvoke, invokeDesktop } from '@/lib/desktop'; -type TauriInvoke = (cmd: string, args?: Record) => Promise; - -type TauriGlobal = { - core?: { - invoke?: TauriInvoke; - }; -}; +type DesktopInvoke = (cmd: string, args?: Record) => Promise; export type DesktopHost = { id: string; @@ -176,10 +170,9 @@ export const getDesktopHostApiUrl = (host: DesktopHost): string => { return normalizeHostUrl(host.apiUrl || host.url) || host.apiUrl || host.url; }; -const getInvoke = (): TauriInvoke | null => { - if (!isTauriShell()) return null; - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - return typeof tauri?.core?.invoke === 'function' ? tauri.core.invoke : null; +const getInvoke = (): DesktopInvoke | null => { + if (!hasDesktopInvoke()) return null; + return (command, args) => invokeDesktop(command, args) as Promise; }; export const desktopHostsGet = async (): Promise => { diff --git a/packages/ui/src/lib/desktopNative.ts b/packages/ui/src/lib/desktopNative.ts index 62d3df1a..5b3be37f 100644 --- a/packages/ui/src/lib/desktopNative.ts +++ b/packages/ui/src/lib/desktopNative.ts @@ -1,30 +1,15 @@ -import { isDesktopShell } from '@/lib/desktop'; +import { hasDesktopInvoke, invokeDesktop, isDesktopShell } from '@/lib/desktop'; type InvokeArgs = Record; -const isElectronDesktop = (): boolean => { - return typeof window !== 'undefined' && Boolean((window as { __OPENCHAMBER_ELECTRON__?: unknown }).__OPENCHAMBER_ELECTRON__); -}; - -const getInvoke = () => { - if (typeof window === 'undefined') { - return null; - } - const tauri = (window as unknown as { - __TAURI__?: { core?: { invoke?: (cmd: string, args?: InvokeArgs) => Promise } }; - }).__TAURI__; - return typeof tauri?.core?.invoke === 'function' ? tauri.core.invoke : null; -}; - export const invokeDesktopCommand = async ( command: string, args?: InvokeArgs, ): Promise => { - const invoke = getInvoke(); - if (!invoke) { + if (!hasDesktopInvoke()) { throw new Error('Desktop runtime is not available'); } - return invoke(command, args) as Promise; + return invokeDesktop(command, args) as Promise; }; export const startDesktopWindowDrag = async (): Promise => { @@ -33,13 +18,7 @@ export const startDesktopWindowDrag = async (): Promise => { } try { - if (isElectronDesktop()) { - await invokeDesktopCommand('desktop_start_window_drag'); - return; - } - - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - await getCurrentWindow().startDragging(); + await invokeDesktopCommand('desktop_start_window_drag'); } catch { // ignore } @@ -51,11 +30,6 @@ export const isDesktopWindowFullscreen = async (): Promise => { } try { - if (!isElectronDesktop()) { - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - return await getCurrentWindow().isFullscreen(); - } - return Boolean(await invokeDesktopCommand('desktop_is_window_fullscreen')); } catch { return false; @@ -77,12 +51,6 @@ export const setDesktopWindowTitle = async (title: string): Promise => { } try { - if (!isElectronDesktop()) { - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - await getCurrentWindow().setTitle(title); - return; - } - await invokeDesktopCommand('desktop_set_window_title', { title }); } catch { // ignore @@ -98,12 +66,6 @@ export const setDesktopWindowTheme = async ( } try { - if (!isElectronDesktop()) { - const { invoke } = await import('@tauri-apps/api/core'); - await invoke('desktop_set_window_theme', { themeMode, themeVariant }); - return; - } - await invokeDesktopCommand('desktop_set_window_theme', { themeMode, themeVariant }); } catch { // ignore @@ -116,11 +78,6 @@ export const getDesktopAppVersion = async (): Promise => { } try { - if (!isElectronDesktop()) { - const { getVersion } = await import('@tauri-apps/api/app'); - return await getVersion(); - } - const version = await invokeDesktopCommand('desktop_get_app_version'); return typeof version === 'string' && version.trim().length > 0 ? version : null; } catch { @@ -146,17 +103,6 @@ export const listenDesktopNativeDragDrop = async ( return null; } - // Electron uses the renderer's native DOM drag/drop events instead of a - // separate webview drag listener. - if (isElectronDesktop()) { - return null; - } - - try { - const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow'); - const webviewWindow = getCurrentWebviewWindow(); - return await webviewWindow.onDragDropEvent(handler as never); - } catch { - return null; - } + void handler; + return null; }; diff --git a/packages/ui/src/lib/desktopSsh.ts b/packages/ui/src/lib/desktopSsh.ts index f094a689..a05f598f 100644 --- a/packages/ui/src/lib/desktopSsh.ts +++ b/packages/ui/src/lib/desktopSsh.ts @@ -1,17 +1,12 @@ -import { isTauriShell } from '@/lib/desktop'; +import { hasDesktopInvoke, invokeDesktop } from '@/lib/desktop'; -type TauriInvoke = (cmd: string, args?: Record) => Promise; +type DesktopInvoke = (cmd: string, args?: Record) => Promise; -type TauriGlobal = { - core?: { - invoke?: TauriInvoke; - }; - event?: { - listen?: ( - event: string, - handler: (evt: { payload?: unknown }) => void, - ) => Promise<() => void>; - }; +type DesktopBridgeGlobal = { + listen?: ( + event: string, + handler: (evt: { payload?: unknown }) => void, + ) => Promise<() => void>; }; export type DesktopSshRemoteMode = 'managed' | 'external'; @@ -126,10 +121,9 @@ const asStringArray = (value: unknown): string[] => { return value.filter((item): item is string => typeof item === 'string'); }; -const getInvoke = (): TauriInvoke | null => { - if (!isTauriShell()) return null; - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - return typeof tauri?.core?.invoke === 'function' ? tauri.core.invoke : null; +const getInvoke = (): DesktopInvoke | null => { + if (!hasDesktopInvoke()) return null; + return (command, args) => invokeDesktop(command, args) as Promise; }; const parseStoredSecret = (value: unknown): DesktopSshStoredSecret | undefined => { @@ -432,12 +426,12 @@ export const desktopSshLogsClear = async (id: string): Promise => { export const listenDesktopSshStatus = async ( listener: (status: DesktopSshInstanceStatus) => void, ): Promise<() => Promise> => { - if (!isTauriShell()) { + if (!hasDesktopInvoke()) { return async () => {}; } - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - const listen = tauri?.event?.listen; + const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__; + const listen = desktop?.listen; if (typeof listen !== 'function') { return async () => {}; } diff --git a/packages/ui/src/lib/device.ts b/packages/ui/src/lib/device.ts index b6b0b7c7..267c8c4b 100644 --- a/packages/ui/src/lib/device.ts +++ b/packages/ui/src/lib/device.ts @@ -45,7 +45,7 @@ const getNavigatorDeviceHints = (maxTouchPoints: number) => { }; const setRootDeviceAttributes = ( - isTauriShellRuntime: boolean, + isDesktopShellRuntime: boolean, deviceType: DeviceType, hasTouchInput: boolean, ) => { @@ -66,7 +66,7 @@ const setRootDeviceAttributes = ( : 'device-desktop' ); - if (isTauriShellRuntime) { + if (isDesktopShellRuntime) { root.classList.add('desktop-runtime'); root.style.setProperty('--is-mobile', '0'); root.style.setProperty('--device-type', 'desktop'); diff --git a/packages/ui/src/lib/ime.ts b/packages/ui/src/lib/ime.ts index ed2bb77e..55055905 100644 --- a/packages/ui/src/lib/ime.ts +++ b/packages/ui/src/lib/ime.ts @@ -5,10 +5,9 @@ import type React from 'react'; * Uses both `isComposing` and the `keyCode === 229` fallback. * * Note: `keyCode` is deprecated, but `229` remains a practical fallback for - * some WebKit-based environments (including Tauri WebView) where composition + * some WebKit-based environments where composition * events can be ordered unexpectedly. */ export const isIMECompositionEvent = (e: React.KeyboardEvent): boolean => { return e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229; }; - diff --git a/packages/ui/src/lib/preview/screenshot-capture.ts b/packages/ui/src/lib/preview/screenshot-capture.ts index eb99f0c3..a587a943 100644 --- a/packages/ui/src/lib/preview/screenshot-capture.ts +++ b/packages/ui/src/lib/preview/screenshot-capture.ts @@ -1,5 +1,6 @@ import { snapdom } from '@zumer/snapdom'; import { getFontEmbedCSS, toJpeg } from 'html-to-image'; +import { invokeDesktop } from '@/lib/desktop'; import { runtimeFetch } from '@/lib/runtime-fetch'; export type PreviewElementMetadata = { @@ -97,18 +98,16 @@ export const renderPreviewScreenshot = async ( iframe: HTMLIFrameElement, target: PreviewElementMetadata, ): Promise => { - const tauri = typeof window !== 'undefined' - ? (window as unknown as { __TAURI__?: { core?: { invoke?: (cmd: string, args?: Record) => Promise } } }).__TAURI__ - : undefined; - if (typeof tauri?.core?.invoke === 'function') { + if (typeof window !== 'undefined') { try { const rect = iframe.getBoundingClientRect(); - const capture = await tauri.core.invoke<{ mime: string; base64: string; width: number; height: number }>('desktop_capture_page_rect', { + const capture = await invokeDesktop<{ mime: string; base64: string; width: number; height: number }>('desktop_capture_page_rect', { x: rect.left, y: rect.top, width: rect.width, height: rect.height, }); + if (!capture) throw new Error('Desktop screenshot capture is not available'); const image = new Image(); await new Promise((resolve, reject) => { image.onload = () => resolve(); diff --git a/packages/ui/src/lib/router/index.ts b/packages/ui/src/lib/router/index.ts index 40571d88..896636c2 100644 --- a/packages/ui/src/lib/router/index.ts +++ b/packages/ui/src/lib/router/index.ts @@ -2,7 +2,7 @@ * Router module for URL-based navigation in OpenChamber. * * Provides bidirectional sync between URL query parameters and application state. - * Works across web, desktop (Tauri), and VS Code (state-only mode). + * Works across web, desktop, and VS Code (state-only mode). * * URL Schema: * - `?session=` - Navigate to specific session diff --git a/packages/ui/src/lib/shortcuts.ts b/packages/ui/src/lib/shortcuts.ts index 9f318ebd..2ebe9cc0 100644 --- a/packages/ui/src/lib/shortcuts.ts +++ b/packages/ui/src/lib/shortcuts.ts @@ -1,5 +1,5 @@ import { isMacOS } from '@/lib/utils'; -import { isTauriShell } from '@/lib/desktop'; +import { isDesktopShell } from '@/lib/desktop'; export type ShortcutModifier = 'mod' | 'shift' | 'alt' | 'option' | 'ctrl'; export type ShortcutKey = string; @@ -32,7 +32,7 @@ const MODIFIER_KEY_MAP: Record = { }; const DISPLAY_LABEL_MAP: Record = { - 'mod': isMacOS() && isTauriShell() ? '⌘' : 'Ctrl', + 'mod': isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl', 'shift': '⇧', 'alt': '⌥', 'option': '⌥', @@ -555,7 +555,7 @@ export function eventMatchesShortcut( const expectedShift = parsed.modifiers.has('shift'); const expectedAlt = parsed.modifiers.has('alt'); const expectedCtrl = parsed.modifiers.has('ctrl'); - const isDesktopMac = isMacOS() && isTauriShell(); + const isDesktopMac = isMacOS() && isDesktopShell(); const isMac = isMacOS(); const modMatches = isDesktopMac @@ -615,5 +615,5 @@ export function getShortcutLabel(id: string): string { } export function getModifierLabel(): string { - return isMacOS() && isTauriShell() ? '⌘' : 'Ctrl'; + return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl'; } diff --git a/packages/ui/src/lib/url.ts b/packages/ui/src/lib/url.ts index e052968f..0e688c97 100644 --- a/packages/ui/src/lib/url.ts +++ b/packages/ui/src/lib/url.ts @@ -1,15 +1,7 @@ import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; -/** - * Utility for opening external URLs with Tauri shell support. - * In desktop runtime, uses tauri.shell.open() for proper system browser handling. - * Falls back to window.open() for web runtime. - */ - -type TauriShell = { - shell?: { - open?: (url: string) => Promise; - }; +type DesktopBridgeGlobal = { + openExternal?: (url: string) => Promise; }; const parseUrlSafely = (value: string): URL | null => { @@ -90,7 +82,7 @@ export const extractLoopbackUrls = (text: string): string[] => { /** * Opens an external URL in the system browser. - * In Tauri desktop runtime, uses tauri.shell.open() for proper handling. + * In desktop runtime, uses the native shell for proper handling. * Falls back to window.open() for web runtime. * * @param url - The URL to open @@ -127,10 +119,10 @@ export const openExternalUrl = async (url: string): Promise => { } } - const tauri = (window as unknown as { __TAURI__?: TauriShell }).__TAURI__; - if (tauri?.shell?.open) { + const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__; + if (desktop?.openExternal) { try { - await tauri.shell.open(normalizedTarget); + await desktop.openExternal(normalizedTarget); return true; } catch { // Fall through to window.open diff --git a/packages/ui/src/lib/utils.ts b/packages/ui/src/lib/utils.ts index b856d146..fe7f1087 100644 --- a/packages/ui/src/lib/utils.ts +++ b/packages/ui/src/lib/utils.ts @@ -1,6 +1,6 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; -import { isTauriShell } from "@/lib/desktop"; +import { isDesktopShell } from "@/lib/desktop"; import { matchesFuzzyQuery } from "@/lib/search/fuzzySearch"; import type { I18nKey } from "@/lib/i18n"; @@ -31,19 +31,19 @@ export const getRevealLabelKey = (): I18nKey => { /** * Checks if the platform-appropriate modifier key is pressed. * On macOS desktop app: Cmd (metaKey), on other platforms or web: Ctrl (ctrlKey). - * Browser intercepts Cmd shortcuts, so we only use Cmd in Tauri desktop app. + * Browser intercepts Cmd shortcuts, so we only use Cmd in the desktop app. */ export const hasModifier = (e: KeyboardEvent | React.KeyboardEvent): boolean => { - return isMacOS() && isTauriShell() ? e.metaKey : e.ctrlKey; + return isMacOS() && isDesktopShell() ? e.metaKey : e.ctrlKey; }; /** * Returns the platform-appropriate modifier key label. * On macOS desktop app: "⌘", on other platforms or web: "Ctrl" - * Browser intercepts Cmd shortcuts, so we only show Cmd in Tauri desktop app. + * Browser intercepts Cmd shortcuts, so we only show Cmd in the desktop app. */ export const getModifierLabel = (): string => { - return isMacOS() && isTauriShell() ? '⌘' : 'Ctrl'; + return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl'; }; export const truncatePathMiddle = ( diff --git a/packages/ui/src/stores/useOpenInAppsStore.ts b/packages/ui/src/stores/useOpenInAppsStore.ts index 07ed395b..3fd4e2dd 100644 --- a/packages/ui/src/stores/useOpenInAppsStore.ts +++ b/packages/ui/src/stores/useOpenInAppsStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isTauriShell, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop'; +import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isDesktopShell, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop'; import { OPEN_IN_APPS, DEFAULT_OPEN_IN_APP_ID, OPEN_IN_ALWAYS_AVAILABLE_APP_IDS, getOpenInAppById, getPlatformOpenInApp, type OpenInApp } from '@/lib/openInApps'; import { updateDesktopSettings } from '@/lib/persistence'; @@ -86,7 +86,7 @@ export const useOpenInAppsStore = create()((set, get) => ({ }; const loadInstalledApps = async (force?: boolean) => { - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!isDesktopShell() || !isDesktopLocalOriginActive()) { return; } @@ -213,7 +213,7 @@ export const useOpenInAppsStore = create()((set, get) => ({ get().initialize(); } - if (!isTauriShell() || !isDesktopLocalOriginActive()) { + if (!isDesktopShell() || !isDesktopLocalOriginActive()) { return; } diff --git a/packages/ui/src/stores/useUpdateStore.ts b/packages/ui/src/stores/useUpdateStore.ts index 88d0f86d..97ca4766 100644 --- a/packages/ui/src/stores/useUpdateStore.ts +++ b/packages/ui/src/stores/useUpdateStore.ts @@ -8,7 +8,6 @@ import { restartToApplyUpdate, isDesktopLocalOriginActive, isElectronShell, - isTauriShell, isVSCodeRuntime, isWebRuntime, } from '@/lib/desktop'; @@ -83,7 +82,7 @@ function mapRuntimeParams(runtime: ClientRuntime): URLSearchParams { params.set('arch', detectArch()); params.set('platform', detectPlatform()); if (runtime === 'desktop') { - params.set('appType', isElectronShell() ? 'desktop-electron' : 'desktop-tauri'); + params.set('appType', 'desktop-electron'); params.set('instanceMode', isDesktopLocalOriginActive() ? 'local' : 'remote'); return params; } @@ -136,7 +135,7 @@ async function checkForWebUpdates(runtime: ClientRuntime, currentVersion?: strin } function detectRuntimeType(): 'desktop' | 'web' | 'vscode' | null { - if (isTauriShell()) { + if (isElectronShell()) { return 'desktop'; } if (isVSCodeRuntime()) return 'vscode'; diff --git a/packages/vscode/src/bridge-system-runtime.ts b/packages/vscode/src/bridge-system-runtime.ts index 9dc8e6ce..635fe497 100644 --- a/packages/vscode/src/bridge-system-runtime.ts +++ b/packages/vscode/src/bridge-system-runtime.ts @@ -51,8 +51,8 @@ const getOpenChamberConfigDir = (): string => { return path.join(os.homedir(), '.config', 'openchamber'); }; -const sanitizeInstallScope = (scope: string): 'desktop-tauri' | 'vscode' | 'web' => { - if (scope === 'desktop-tauri' || scope === 'vscode' || scope === 'web') return scope; +const sanitizeInstallScope = (scope: string): 'vscode' | 'web' => { + if (scope === 'vscode' || scope === 'web') return scope; return 'web'; }; diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 1b429779..d56e92a2 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -683,7 +683,7 @@ export async function activate(context: vscode.ExtensionContext) { sessionEditorProvider?.updateConnectionStatus(status, error); // Start/stop global event watcher based on connection status - // Mirrors web server and desktop Tauri behavior + // Mirrors web server and desktop behavior if (status === 'connected' && chatViewProvider && openCodeManager) { setChatViewProvider(chatViewProvider); void startGlobalEventWatcher(openCodeManager, chatViewProvider); diff --git a/packages/vscode/src/sessionActivityWatcher.ts b/packages/vscode/src/sessionActivityWatcher.ts index 3c0a2c42..61d4dbc2 100644 --- a/packages/vscode/src/sessionActivityWatcher.ts +++ b/packages/vscode/src/sessionActivityWatcher.ts @@ -1,7 +1,7 @@ import { createOpencodeClient } from '@opencode-ai/sdk/v2'; import type { OpenCodeManager } from './opencode'; -// Session activity tracking (mirrors web server and desktop Tauri behavior) +// Session activity tracking (mirrors web server and desktop behavior) type ActivityPhase = 'idle' | 'busy' | 'cooldown'; interface SessionActivity { diff --git a/packages/web/server/lib/notifications/emitter-runtime.js b/packages/web/server/lib/notifications/emitter-runtime.js index 67210e7a..ae302912 100644 --- a/packages/web/server/lib/notifications/emitter-runtime.js +++ b/packages/web/server/lib/notifications/emitter-runtime.js @@ -46,8 +46,7 @@ export const createNotificationEmitterRuntime = (dependencies) => { } try { - // stdout IPC: Tauri shell spawns this process as a sidecar and parses - // its stdout for the one-line `${prefix}{json}` protocol. + // stdout fallback for runtimes that parse the one-line `${prefix}{json}` protocol. process.stdout.write(`${desktopNotifyPrefix}${JSON.stringify(payload)}\n`); } catch { // ignore @@ -64,9 +63,9 @@ export const createNotificationEmitterRuntime = (dependencies) => { type: 'openchamber:notification', properties: { ...payload, - // Tell the UI whether the sidecar stdout notification channel is active. + // Tell the UI whether the stdout notification channel is active. // When true, the desktop UI should skip this SSE notification to avoid duplicates. - // When false (e.g. tauri dev), the UI must handle this SSE notification itself. + // When false, the UI must handle this SSE notification itself. desktopStdoutActive: desktopNotifyEnabled, }, }; diff --git a/packages/web/server/lib/package-manager.js b/packages/web/server/lib/package-manager.js index 4fb390f6..08efec9a 100644 --- a/packages/web/server/lib/package-manager.js +++ b/packages/web/server/lib/package-manager.js @@ -29,7 +29,7 @@ function getOpenChamberConfigDir() { } function sanitizeInstallScope(scope) { - if (scope === 'desktop-electron' || scope === 'desktop-tauri' || scope === 'vscode' || scope === 'web') return scope; + if (scope === 'desktop-electron' || scope === 'vscode' || scope === 'web') return scope; return 'web'; } @@ -65,7 +65,7 @@ function mapArch(value) { } function normalizeAppType(value) { - if (value === 'web' || value === 'desktop-electron' || value === 'desktop-tauri' || value === 'vscode') return value; + if (value === 'web' || value === 'desktop-electron' || value === 'vscode') return value; return 'web'; } diff --git a/packages/web/server/lib/package-manager.test.js b/packages/web/server/lib/package-manager.test.js index 3eef72e9..7edab852 100644 --- a/packages/web/server/lib/package-manager.test.js +++ b/packages/web/server/lib/package-manager.test.js @@ -115,27 +115,6 @@ describe('checkForUpdates', () => { expect(result.available).toBe(false); }); - it('does not cross-check desktop update claims against npm', async () => { - fetchMock - .when('api.openchamber.dev', { - ok: true, - json: async () => ({ - latestVersion: '1.10.0', - updateAvailable: true, - releaseNotes: '## [1.10.0] - 2026-05-01\n\n- Great new feature', - }), - }); - - const result = await checkForUpdates({ - appType: 'desktop-tauri', - currentVersion: '1.9.10', - }); - - expect(result.available).toBe(true); - expect(result.version).toBe('1.10.0'); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - it('accepts electron desktop update claims without npm cross-checking', async () => { fetchMock .when('api.openchamber.dev', { diff --git a/packages/web/src/api/notifications.ts b/packages/web/src/api/notifications.ts index 006e30ab..6dd2f814 100644 --- a/packages/web/src/api/notifications.ts +++ b/packages/web/src/api/notifications.ts @@ -184,13 +184,13 @@ const notifyWithDesktop = async (payload?: NotificationPayload): Promise ({ }, canNotify: () => { if (typeof window !== 'undefined') { - const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; - if (tauri?.core?.invoke) { + const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__; + if (desktop?.invoke) { return true; } } return typeof Notification !== 'undefined' ? Notification.permission === 'granted' : false; }, }); -type TauriGlobal = { - core?: { - invoke?: (cmd: string, args?: Record) => Promise; - }; +type DesktopBridgeGlobal = { + invoke?: (cmd: string, args?: Record) => Promise; }; diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs old mode 100644 new mode 100755 index b3dec753..ff621862 --- a/scripts/bump-version.mjs +++ b/scripts/bump-version.mjs @@ -10,15 +10,10 @@ const PACKAGES = [ 'package.json', 'packages/ui/package.json', 'packages/web/package.json', - 'packages/desktop/package.json', 'packages/electron/package.json', 'packages/vscode/package.json', ]; -const TAURI_CONF = 'packages/desktop/src-tauri/tauri.conf.json'; -const CARGO_TOML = 'packages/desktop/src-tauri/Cargo.toml'; -const CARGO_LOCK = 'packages/desktop/src-tauri/Cargo.lock'; - const newVersion = process.argv[2]; if (!newVersion || !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(newVersion)) { console.error('Usage: node scripts/bump-version.mjs '); @@ -29,7 +24,6 @@ if (!newVersion || !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(newVersion)) { console.log(`Bumping version to ${newVersion}\n`); -// Update package.json files for (const pkgPath of PACKAGES) { const fullPath = path.join(ROOT, pkgPath); const pkg = JSON.parse(fs.readFileSync(fullPath, 'utf8')); @@ -39,60 +33,4 @@ for (const pkgPath of PACKAGES) { console.log(` ${pkgPath}: ${oldVersion} -> ${newVersion}`); } -// Update tauri.conf.json -const tauriConfPath = path.join(ROOT, TAURI_CONF); -const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8')); -const oldTauriVersion = tauriConf.version; -tauriConf.version = newVersion; -fs.writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + '\n'); -console.log(` ${TAURI_CONF}: ${oldTauriVersion} -> ${newVersion}`); - -// Update Cargo.toml -const cargoPath = path.join(ROOT, CARGO_TOML); -let cargoContent = fs.readFileSync(cargoPath, 'utf8'); -const cargoMatch = cargoContent.match(/^version = "(.*)"/m); -const oldCargoVersion = cargoMatch ? cargoMatch[1] : 'unknown'; -cargoContent = cargoContent.replace( - /^version = ".*"$/m, - `version = "${newVersion}"` -); -fs.writeFileSync(cargoPath, cargoContent); -console.log(` ${CARGO_TOML}: ${oldCargoVersion} -> ${newVersion}`); - -// Update Cargo.lock for openchamber-desktop, if present -const cargoLockPath = path.join(ROOT, CARGO_LOCK); -if (fs.existsSync(cargoLockPath)) { - try { - let lockContent = fs.readFileSync(cargoLockPath, 'utf8'); - const anchor = 'name = "openchamber-desktop"'; - const anchorIndex = lockContent.indexOf(anchor); - if (anchorIndex !== -1) { - // find the next version line after the anchor - const verIndex = lockContent.indexOf('version', anchorIndex); - if (verIndex !== -1) { - const q1 = lockContent.indexOf('"', verIndex); - const q2 = lockContent.indexOf('"', q1 + 1); - const oldLockVersion = lockContent.substring(q1 + 1, q2); - lockContent = lockContent.substring(0, q1 + 1) + newVersion + lockContent.substring(q2); - fs.writeFileSync(cargoLockPath, lockContent); - console.log(` ${CARGO_LOCK}: ${oldLockVersion} -> ${newVersion}`); - } else { - console.warn(`Warning: could not locate version line in ${CARGO_LOCK}`); - } - } else { - console.warn(`Warning: could not find openchamber-desktop entry in ${CARGO_LOCK}`); - } - } catch (e) { - console.error(`Failed to update ${CARGO_LOCK}:`, e); - } -} else { - // No lock file to update; ignore gracefully - console.log(`Cargo.lock not found at ${CARGO_LOCK}, skipping lock update`); -} - -console.log(`\nVersion bumped to ${newVersion}`); -console.log('\nNext steps:'); -console.log(` git add -A`); -console.log(` git commit -m "release v${newVersion}"`); -console.log(` git tag v${newVersion}`); -console.log(` git push origin main --tags`); +console.log('\nVersion bump complete. Review changes, then commit and tag.'); diff --git a/tsconfig.json b/tsconfig.json index d74e8a23..d7898f2d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "references": [ { "path": "./packages/ui/tsconfig.json" }, { "path": "./packages/web/tsconfig.json" }, - { "path": "./packages/desktop/tsconfig.json" } + { "path": "./packages/electron/tsconfig.json" } ], "compilerOptions": { "baseUrl": ".", @@ -14,8 +14,7 @@ "resolveJsonModule": true, "paths": { "@openchamber/ui/*": ["./packages/ui/src/*"], - "@openchamber/web/*": ["./packages/web/src/*"], - "@openchamber/desktop/*": ["./packages/desktop/src/*"] + "@openchamber/web/*": ["./packages/web/src/*"] } } }