diff --git a/src/.github/workflows/build-macos-arm64-dmg.yml b/src/.github/workflows/build-macos-arm64-dmg.yml index 551ccca..71aeaa0 100644 --- a/src/.github/workflows/build-macos-arm64-dmg.yml +++ b/src/.github/workflows/build-macos-arm64-dmg.yml @@ -123,3 +123,79 @@ jobs: name: dmg-${{ inputs.macos_version }}-arm64 path: artifacts/*.dmg retention-days: 7 + + build-macos-dmg-arm64-electron: + name: Build Electron DMG (arm64, ${{ inputs.macos_version }}) + runs-on: ${{ inputs.macos_version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Setup bun + uses: oven-sh/setup-bun@v2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Install Apple Certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/electron-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 + security import $RUNNER_TEMP/certificate.p12 \ + -P "$APPLE_CERTIFICATE_PASSWORD" \ + -A -t cert -f pkcs12 \ + -k "$KEYCHAIN_PATH" + + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + - name: Build Electron app (arm64) + working-directory: packages/electron + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + ELECTRON_BUILDER_ARCH: arm64 + run: | + bun run build:web-assets + bun run bundle:main + bun run rebuild:native + ./node_modules/.bin/electron-builder --mac --arm64 --publish=never + + - name: Prepare DMG artifact + run: | + set -euo pipefail + mkdir -p artifacts + DMG_PATH="packages/electron/dist/*.dmg" + if ls $DMG_PATH 1> /dev/null 2>&1; then + DMG_FILE=$(ls $DMG_PATH | head -n 1) + DMG_NAME="OpenChamber_Electron_${{ inputs.macos_version }}_arm64.dmg" + cp "$DMG_FILE" "artifacts/$DMG_NAME" + else + echo "Error: DMG file not found at $DMG_PATH" + exit 1 + fi + + - name: Upload DMG artifact + uses: actions/upload-artifact@v4 + with: + name: dmg-electron-${{ inputs.macos_version }}-arm64 + path: artifacts/*.dmg + retention-days: 7 diff --git a/src/.github/workflows/release.yml b/src/.github/workflows/release.yml index f29c3c1..b1865b1 100644 --- a/src/.github/workflows/release.yml +++ b/src/.github/workflows/release.yml @@ -494,11 +494,174 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build-desktop-electron-macos: + needs: create-release + runs-on: macos-26 + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-apple-darwin + arch: arm64 + platform: darwin-aarch64 + - target: x86_64-apple-darwin + arch: x64 + platform: darwin-x86_64 + steps: + - uses: actions/checkout@v4 + + - name: Setup bun + uses: oven-sh/setup-bun@v2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Install Apple Certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/electron-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 + security import $RUNNER_TEMP/certificate.p12 \ + -P "$APPLE_CERTIFICATE_PASSWORD" \ + -A -t cert -f pkcs12 \ + -k "$KEYCHAIN_PATH" + + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + - name: Build Electron app + working-directory: packages/electron + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # rebuild-native.mjs reads this to target the right arch when + # cross-building (runner is arm64; x64 matrix needs the hint). + ELECTRON_BUILDER_ARCH: ${{ matrix.arch }} + run: | + bun run build:web-assets + bun run bundle:main + # npmRebuild=false in package.json, so electron-builder won't + # recompile native deps on its own — we must rebuild against the + # target Electron ABI before packaging, otherwise better-sqlite3/ + # node-pty/bun-pty crash on require inside the packaged app. + bun run rebuild:native + ./node_modules/.bin/electron-builder --mac --${{ matrix.arch }} --publish=never + + - name: Verify signature + entitlements + notarization + 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 + + echo "Verifying $APP_PATH" + codesign -vv --deep --strict "$APP_PATH" + + # Require hardened runtime + CS_INFO=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1) + echo "$CS_INFO" + if ! echo "$CS_INFO" | grep -q "flags=.*runtime"; then + echo "Error: hardened runtime flag missing" + exit 1 + fi + + # Require notary ticket stapled + xcrun stapler validate "$APP_PATH" + + ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true) + if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then + echo "Error: app sandbox entitlement is present" + exit 1 + fi + for key in \ + com.apple.security.cs.allow-jit \ + com.apple.security.cs.allow-unsigned-executable-memory \ + com.apple.security.cs.disable-library-validation + do + if ! echo "$ENTITLEMENTS" | grep -q "$key"; then + echo "Error: required entitlement missing: $key" + exit 1 + fi + done + + - name: Upload DMG / ZIP / blockmaps to release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.create-release.outputs.version }} + files: | + packages/electron/dist/*.dmg + packages/electron/dist/*.zip + packages/electron/dist/*.blockmap + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload per-arch latest-mac.yml for merge + uses: actions/upload-artifact@v4 + with: + name: latest-yml-${{ matrix.target }} + path: packages/electron/dist/latest-mac.yml + retention-days: 1 + + combine-electron-manifests: + needs: [create-release, build-desktop-electron-macos] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Download per-arch latest-mac.yml + uses: actions/download-artifact@v4 + with: + pattern: latest-yml-*-apple-darwin + path: artifacts + + - name: Finalize combined latest-mac.yml + env: + LATEST_YML_DIR: ${{ github.workspace }}/artifacts + GH_REPO: ${{ github.repository }} + OPENCHAMBER_VERSION: ${{ needs.create-release.outputs.version }} + run: node packages/electron/scripts/finalize-latest-yml.mjs + + - name: Upload combined latest-mac.yml to release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.create-release.outputs.version }} + files: ${{ runner.temp }}/latest-mac.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + finalize-release: - needs: [create-release, build-desktop-macos, publish-npm, combine-manifests] + needs: [create-release, build-desktop-macos, build-desktop-electron-macos, publish-npm, combine-manifests, combine-electron-manifests] runs-on: ubuntu-latest env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + DISCORD_UPDATE_ROLE_ID: ${{ secrets.DISCORD_UPDATE_ROLE_ID }} steps: - name: Publish release uses: softprops/action-gh-release@v2 @@ -514,11 +677,14 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ needs.create-release.outputs.version }} REPOSITORY: ${{ github.repository }} + UPDATE_ROLE_ID: ${{ env.DISCORD_UPDATE_ROLE_ID }} run: | node - <<'NODE' (async () => { const tag = `v${process.env.VERSION}`; const repo = process.env.REPOSITORY; + const rawRoleId = (process.env.UPDATE_ROLE_ID || '').trim(); + const updateRoleId = /^\d+$/.test(rawRoleId) ? rawRoleId : ''; const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${tag}`, { headers: { @@ -534,9 +700,18 @@ jobs: const release = await releaseRes.json(); const description = (release.body || `OpenChamber ${tag} released.`).slice(0, 4096); + const mention = updateRoleId ? `<@&${updateRoleId}>` : ''; const payload = { username: 'OpenChamber Releases', + ...(mention ? { content: mention } : {}), + ...(updateRoleId + ? { + allowed_mentions: { + roles: [updateRoleId], + }, + } + : {}), embeds: [ { title: release.name || `OpenChamber ${tag}`, diff --git a/src/.gitignore b/src/.gitignore index 0565d35..db16936 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -28,6 +28,7 @@ local-dev* *.sw? .opencode/plans/* .hive +docs/personal/* # Build outputs build/ diff --git a/src/.opencode/package-lock.json b/src/.opencode/package-lock.json new file mode 100644 index 0000000..fc32819 --- /dev/null +++ b/src/.opencode/package-lock.json @@ -0,0 +1,376 @@ +{ + "name": ".opencode", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@opencode-ai/plugin": "1.4.10" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.10.tgz", + "integrity": "sha512-35Za2LT2oNWnBoonmPjN1Z9PB4+ir2a6GbZ3nIZQQL/96mqzTRkT1FqUkQc3bdMmfT1R1rqOd5aMzkIXMqC7dA==", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.4.10", + "effect": "4.0.0-beta.48", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.100", + "@opentui/solid": ">=0.1.100" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.10.tgz", + "integrity": "sha512-Yaddcs/COp0hwiCxgobSZyDUN0nHgkEFL4bG0BQxwd52SGAysOr6A6L0ihfkuhVx0kbi9eXWgZk4ydNOrnur5w==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.48", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", + "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.6.0", + "find-my-way-ts": "^0.1.6", + "ini": "^6.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^1.11.9", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^13.0.0", + "yaml": "^2.8.3" + } + }, + "node_modules/fast-check": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.6.0.tgz", + "integrity": "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "license": "MIT" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "license": "Apache-2.0" + }, + "node_modules/msgpackr": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.9.tgz", + "integrity": "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/toml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", + "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/src/.opencode/skills/locale-ui-patterns/SKILL.md b/src/.opencode/skills/locale-ui-patterns/SKILL.md new file mode 100644 index 0000000..ff57255 --- /dev/null +++ b/src/.opencode/skills/locale-ui-patterns/SKILL.md @@ -0,0 +1,128 @@ +--- +name: locale-ui-patterns +description: Use when creating or modifying OpenChamber UI text, labels, buttons, placeholders, aria labels, empty states, toasts, dialogs, settings copy, navigation labels, or any user-facing strings. +--- + +# Locale UI Patterns + +## Core Rule + +User-facing UI text must go through `@/lib/i18n`; do not hardcode English strings in components. + +Use this skill for any React UI change that adds or edits visible text, accessible labels, placeholders, tooltips, toasts, dialogs, settings labels, navigation labels, or empty/error states. + +## Required Flow + +1. Add or reuse a key in `packages/ui/src/lib/i18n/messages/en.ts`. +2. Add the same key to every non-English dictionary in `packages/ui/src/lib/i18n/messages/`. +3. In components, call `const { t } = useI18n()` from `@/lib/i18n` and render `t('key')`. +4. For locale names or language picker labels, use `label(locale)` from `useI18n()`. +5. Keep locale state in `packages/ui/src/lib/i18n/*`; do not add locale fields to broad stores like `useUIStore`. +6. Do not remount the app to update language. Components must re-render through `useI18n()`. + +## Component Usage Rules + +- Import from `@/lib/i18n`, not deep files. +- Keep `t(...)` calls inside React render/hook scope so locale changes re-render text. +- Do not resolve translated text at module scope. +- For static option arrays, store `labelKey` / `descriptionKey`; resolve with `t(...)` inside the component. +- For non-React helpers, pass translated strings in from the component or pass `t` explicitly. + +## Key Style + +Use stable semantic keys, not English text as keys. + +Keys should describe location + UI role + meaning. They should not encode current copy wording. + +Use existing nearby naming when extending a surface. If no nearby pattern exists, choose a short path that mirrors the UI ownership. + +Namespaces like `layout.*`, `settings.*`, `chat.*`, `git.*`, `session.*`, `toast.*`, and `dialog.*` are examples, not a fixed exhaustive list. + +Good: +```ts +'settings.appearance.language.label': 'Language' +'layout.mainTab.chat': 'Chat' +'chat.input.placeholder': 'Ask OpenChamber...' +``` + +Bad: +```ts +'Language': 'Language' +'chatLabel': 'Chat' +'askOpenChamberDotDotDot': 'Ask OpenChamber...' +``` + +Avoid overly generic keys unless the text is truly global and context-independent. Prefer specific keys when button meaning can vary by surface. + +## Parameters + +Use `{name}` placeholders for dynamic values. + +```ts +'toast.language.changed': 'Language changed to {language}' +``` + +```tsx +t('toast.language.changed', { language: label(locale) }) +``` + +Do not pass grammar fragments as params. Never use params like `{suffix}`, `{plural}`, `{article}`, `{prefix}`, `{dateSuffix}`, or pieces of words/sentences. + +Bad: +```tsx +t('dialog.delete.description', { count, suffix: count === 1 ? '' : 's' }) +``` + +Good: +```tsx +count === 1 + ? t('dialog.delete.descriptionSingle', { count }) + : t('dialog.delete.descriptionPlural', { count }) +``` + +Plural/count-dependent text must use separate complete-message keys unless all supported locales can use one identical complete sentence. Placeholders are only for real values (`{count}`, `{name}`, `{path}`), not grammar. + +Optional clauses must also be complete-message keys. Do not build a sentence by injecting a translated phrase into another translated sentence. + +Bad: +```tsx +t('dialog.delete.description', { + dateLabel: date ? t('dialog.delete.dateSuffix', { date }) : '', +}) +``` + +Good: +```tsx +date + ? t('dialog.delete.descriptionWithDate', { count, date }) + : t('dialog.delete.description', { count }) +``` + +## What Counts As UI Text + +- Button and menu labels +- Settings labels and descriptions +- Placeholder text +- Tooltip content +- Dialog titles/descriptions/actions +- Toast title/description/action labels +- Empty/error/loading states +- `aria-label`, `title`, image `alt` text when user-facing + +## Exceptions + +Do not translate: + +- Product names: `OpenChamber`, `OpenCode`, `GitHub` +- Protocol/tool acronyms: `MCP`, `SSE`, `WebSocket`, `API` +- Model/provider names +- File paths, command names, environment variables +- User/generated content + +## Review Checklist + +- No new hardcoded user-facing English in changed UI files. +- Every new key exists in all dictionaries. +- No locale state added to broad/shared stores. +- No full app remount for locale changes. +- Locale switch preserves current UI state. diff --git a/src/AGENTS.md b/src/AGENTS.md index c601cac..35dd9de 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -1,88 +1,130 @@ # OpenChamber - AI Agent Reference (verified) ## Core purpose + OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an OpenCode server (local auto-start or remote URL). UI uses HTTP + SSE via `@opencode-ai/sdk`. ## Runtime architecture (IMPORTANT) -- `Desktop` is a thin Tauri shell that starts the web server sidecar and loads the web UI from `http://127.0.0.1:`. -- All backend logic lives in `packages/web/server/*` (and `packages/vscode/*` for the VS Code runtime). Desktop Rust is not a feature backend. -- Tauri is used only for stable native integrations: menu, dialog (open folder), notifications, updater, deep-links. + +- `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. +- All backend logic lives in `packages/web/server/*` (and `packages/vscode/*` for the VS Code runtime). The native shell is not a feature backend. +- The shell is used only for stable native integrations: menu, dialog (open folder), notifications, updater, deep-links, quit confirmation. + +### Desktop shell: Electron is the target, Tauri is legacy + +- **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. +- 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: both shells ship in the same GitHub release today (`.github/workflows/release.yml`). The one-shot Tauri → Electron auto-update migration is documented in `docs/TAURI_TO_ELECTRON_CUTOVER.md`; run that when the user decides to flip. +- After the cutover ships and stabilises, `packages/desktop/` is deleted; this note collapses back to "Desktop is Electron". ## Tech stack (source of truth: `package.json`, resolved: `bun.lock`) + - Runtime/tooling: Bun (`package.json` `packageManager`), Node >=20 (`package.json` `engines`) - UI: React, TypeScript, Vite, Tailwind v4 - State: Zustand (`packages/ui/src/stores/`) -- UI primitives: Radix UI (`package.json` deps), HeroUI (`package.json` deps), Remixicon (`package.json` deps) +- 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 (`package.json` deps) - Server: Express (`packages/web/server/index.js`) -- Desktop: Tauri v2 (`packages/desktop/src-tauri/`) +- Desktop (forward): Electron 41 (`packages/electron/`) +- Desktop (legacy, maintenance-only): Tauri v2 (`packages/desktop/src-tauri/`) - VS Code: extension + webview (`packages/vscode/`) ## Monorepo layout + Workspaces are `packages/*` (see `package.json`). + - Shared UI: `packages/ui` - Web app + server + CLI: `packages/web` -- Desktop app (Tauri): `packages/desktop` +- Desktop shell (Electron — forward): `packages/electron` +- Desktop shell (Tauri — legacy, maintenance-only): `packages/desktop` - VS Code extension: `packages/vscode` ## Documentation map + Before changing any mapped module, read its module documentation first. ### web + Web runtime and server implementation for OpenChamber. #### lib + Server-side integration modules used by API routes and runtime services. ##### quota + Quota provider registry, dispatch, and provider integrations for usage endpoints. + - Module docs: `packages/web/server/lib/quota/DOCUMENTATION.md` ##### git + Git repository operations for the web server runtime. + - Module docs: `packages/web/server/lib/git/DOCUMENTATION.md` ##### github + GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing. + - Module docs: `packages/web/server/lib/github/DOCUMENTATION.md` ##### opencode + OpenCode server integration utilities including config management, provider authentication, and UI authentication. + - Module docs: `packages/web/server/lib/opencode/DOCUMENTATION.md` ##### notifications + Notification message preparation utilities for system notifications, including text truncation and optional summarization. + - Module docs: `packages/web/server/lib/notifications/DOCUMENTATION.md` ##### terminal + WebSocket protocol utilities for terminal input handling including message normalization, control frame parsing, and rate limiting. + - Module docs: `packages/web/server/lib/terminal/DOCUMENTATION.md` ##### tts + Server-side text-to-speech services and summarization helpers for `/api/tts/*` endpoints. + - Module docs: `packages/web/server/lib/tts/DOCUMENTATION.md` ##### skills-catalog + Skills catalog management including discovery, installation, and configuration of agent skill packages. + - Module docs: `packages/web/server/lib/skills-catalog/DOCUMENTATION.md` ## Build / dev commands (verified) + All scripts are in `package.json`. + - Validate: `bun run type-check`, `bun run lint` - Build all: `bun run build` -- Desktop build: `bun run desktop: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`) ## Runtime entry points + - 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: Tauri entry `packages/desktop/src-tauri/src/main.rs` (spawns web server sidecar + loads web UI) -- Tauri backend: `packages/desktop/src-tauri/src/main.rs` +- 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` - VS Code extension host: `packages/vscode/src/extension.ts` - VS Code webview bootstrap: `packages/vscode/webview/main.tsx` ## OpenCode integration + - UI client wrapper: `packages/ui/src/lib/opencode/client.ts` (imports `@opencode-ai/sdk/v2`) - SSE hookup: `packages/ui/src/hooks/useEventStream.ts` - Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`) @@ -90,6 +132,7 @@ All scripts are in `package.json`. - External server support: Set `OPENCODE_HOST` (full base URL, e.g. `http://hostname:4096`) or `OPENCODE_PORT`, plus `OPENCODE_SKIP_START=true`, to connect to existing OpenCode instance ## Key UI patterns (reference files) + - Settings shell: `packages/ui/src/components/views/SettingsView.tsx` - Settings shared primitives: `packages/ui/src/components/sections/shared/` - Settings sections: `packages/ui/src/components/sections/` (incl `skills/`) @@ -98,27 +141,74 @@ All scripts are in `package.json`. - Terminal UI: `packages/ui/src/components/terminal/` (uses `ghostty-web`) ## External / system integrations (active) + - Git: `packages/ui/src/lib/gitApi.ts`, `packages/web/server/index.js` (`simple-git`) - Terminal PTY: `packages/web/server/index.js` (`bun-pty`/`node-pty`) - Skills catalog: `packages/web/server/lib/skills-catalog/`, UI: `packages/ui/src/components/sections/skills/` ## Agent constraints + - Do not modify `../opencode` (separate repo). - Do not run git/GitHub commands unless explicitly asked. -- Keep baseline green (run `bun run type-check`, `bun run lint`, `bun run build` before finalizing changes). +- Keep baseline green (run `bun run type-check`, `bun run lint` before finalizing changes). + +## Agent code of conduct + +- Prefer the smallest correct change. +- Preserve working behavior before improving structure. +- Do not add cleverness where a direct implementation is enough. +- Do not infer critical state from weak signals when a stronger source exists. +- Do not encode policy only in UI; enforce it in core logic. +- Do not hide data loss, partial failure, or fallback behavior. Make it explicit in code. +- Finish work end-to-end: implementation, verification, and cleanup. ## Development rules + - Keep diffs tight; avoid drive-by refactors. -- Backend changes: keep web/desktop/vscode runtimes consistent (if relevant). -- Follow local precedent; search nearby code first. -- TypeScript: avoid `any`/blind casts; keep ESLint/TS green. -- React: prefer function components + hooks; class only when needed (e.g. error boundaries). -- Control flow: avoid nested ternaries; prefer early returns + `if/else`/`switch`. -- Styling: Tailwind v4; typography via `packages/ui/src/lib/typography.ts`; theme vars via `packages/ui/src/lib/theme/`. -- Shared UI patterns: for "series of items + divider + series of items" layouts, use shared UI primitives instead of duplicating ad-hoc markup in feature components. -- Toasts: use custom toast wrapper from `@/components/ui` (backed by `packages/ui/src/components/ui/toast.ts`); do not import `sonner` directly in feature code. +- Follow local precedent; inspect nearby code before introducing new patterns. +- Backend changes: keep web, desktop, and VS Code behavior consistent when they share contracts. +- TypeScript: avoid `any`, blind casts, and shape guessing. +- React: prefer function components + hooks; use classes only when required. +- Control flow: prefer early returns and explicit branching over nested ternaries. +- Styling: Tailwind v4, typography via `packages/ui/src/lib/typography.ts`, theme vars via `packages/ui/src/lib/theme/`. +- Shared UI patterns: reuse shared primitives before introducing feature-local markup patterns. +- Toasts: use the wrapper from `@/components/ui`; do not import `sonner` directly in feature code. - No new deps unless asked. -- Never add secrets (`.env`, keys) or log sensitive data. +- Never add secrets or log sensitive data. + +## Architecture patterns + +### Thin entrypoints, focused modules + +- Keep orchestration entrypoints thin: `index.js`, bridge files, bootstrap files, provider roots. +- Move route, domain, and runtime logic into focused modules with clear ownership. +- Prefer dependency injection over hidden module coupling. +- Add or update module documentation when ownership changes. + +### Strong source of truth + +- Prefer deterministic state over heuristics. +- Use live server/session state for live activity. Do not let historical anomalies masquerade as current execution. +- If a fallback is necessary, scope it narrowly to the active entity and treat it as temporary. +- Restore derived UI state from authoritative records. Example: restore model or agent from the latest user message, not assistant-side guesses. + +### Live state vs historical state + +- Derive live UI behavior from live state channels, not persisted history. +- Use historical records to restore context, not to infer that work is still in progress. +- If live state is delayed, use the narrowest possible transient fallback and clear it as soon as authoritative state arrives. + +### Cross-runtime parity + +- If web defines a route or payload contract that shared UI depends on, keep VS Code and desktop parity where applicable. +- Shared behavior differences must be intentional and visible in code. +- Do not ship a web-only assumption into shared UI. + +### Partial-failure-safe flows + +- Cross-directory and multi-entity operations must tolerate partial failure. +- Prefer per-item results, rollback paths, or resumable cleanup over all-or-nothing assumptions. +- Never leave optimistic state or local caches stranded after failure. ## CLI Parity and Safety Policy (MANDATORY) @@ -158,6 +248,7 @@ are defined in the `clack-cli-patterns` skill and should not be duplicated here. When working on terminal CLI commands, prompts, or output formatting, agents **MUST** study the Clack CLI skill first. **Before starting terminal CLI work:** + ``` skill({ name: "clack-cli-patterns" }) ``` @@ -169,12 +260,127 @@ Scope: terminal CLI only (for example `packages/web/bin/*`). Do not apply this r When working on any UI components, styling, or visual changes, agents **MUST** study the theme system skill first. **Before starting any UI work:** + ``` skill({ name: "theme-system" }) ``` This skill contains all color tokens, semantic logic, decision tree, and usage patterns. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes. +## Performance rules (MANDATORY) + +These rules exist because violating them has caused measurable regressions (render cascades, memory bloat, UI jank). They apply to all UI and sync layer work. + +### Shared-store render discipline + +- **Treat common stores as render fanout boundaries.** An unnecessary reference change in shared state can re-render large parts of the app. +- **Do not put high-frequency state in broadly consumed stores.** Fast-changing state should live in narrow stores with narrow subscribers. +- **Update only the fields that changed.** Preserve references for untouched state branches. +- **Prefer leaf selectors over container selectors.** Subscribe to the smallest stable value that satisfies the component. +- **Isolate hot consumers.** If a value changes often and only a few components need it, move it to a narrower store or consume it in a memoized child. +- **Do not subscribe shell/layout components to broad live collections.** If a shell only needs one field, entity, or derived flag, subscribe to that instead of the whole collection. +- **Treat provider roots as global hot paths.** A top-level provider must not subscribe to high-frequency data unless the feature is actually enabled and the subscription is essential. + +### Zustand referential equality + +Zustand skips re-renders when a selector returns the same reference (`Object.is`). Every new object/array reference triggers a re-render in every subscriber. + +- **Never spread all state fields in an update.** Only create new references for fields that actually changed. A `message.part.delta` event should not clone `session`, `permission`, etc. +- **Select leaf values, not containers.** `useStore((s) => s.permission[sessionID])` is correct. `useStore((s) => s.permission)` subscribes to every permission change across all sessions. +- **Preserve references when merging.** If prepending older messages, keep existing message object references. Only add truly new items. Return the original array if nothing was added. +- **For derived collections, preserve item identity when presentation-relevant fields are unchanged.** Reuse previous item references for unchanged rows/items and move high-frequency live fields to narrow per-item selectors. + +### Store splitting + +A single store with N properties means every subscriber re-evaluates on every state change. Split stores by change frequency and subscriber set. + +- **Group state by how often it changes.** Streaming state (updated 60/sec) must not live with user preferences (updated on click). +- **Group state by who reads it.** If only 2 components need a value, it belongs in a store that only those 2 subscribe to. +- **Cross-store reads use `.getState()`.** Actions in one store that need another store call `useOtherStore.getState()` — imperative, no subscription. +- **Never add unrelated state to an existing store** just because it's convenient. Create a new store. + +### Event pipeline and SSE + +- **Gate expensive operations on the hot path.** During streaming, `message.part.delta` and `message.part.updated` fire ~60/sec. Any `findIndex`, `filter`, or iteration added to these handlers multiplies across every event. Gate behind a cheap boolean check first (e.g., check `next[0]` before scanning the array). +- **Skip no-op updates.** If an incoming event doesn't change the state (same role, same finish, same timestamps), return `false` from the reducer to avoid creating new references. +- **Coalesce by key.** Same-entity events (e.g., repeated `session.status` for the same session) should replace earlier ones in the queue, not accumulate. +- **Preserve event ordering semantics.** Reducers and queues must not let stale deltas or out-of-order events corrupt the latest state. +- **Do not widen live-activity fallbacks.** A fallback for delayed status should inspect only the current trailing entity, not arbitrary historical records. + +### Polling payload fidelity + +- **Do not let lightweight polling erase rich fields.** If light mode omits fields (e.g., `diffStats`), preserve previous rich data until a heavy follow-up fetch lands. +- **Use two-phase polling.** Run cheap change detection first; only run heavy status fetches for directories that actually changed. + +### Optimistic updates + +- **Use the shadow Map pattern.** Insert optimistic data into the store for instant UI, AND register it in a separate tracking Map. Cleanup happens deterministically via `mergeOptimisticPage` on the next data fetch — not via heuristics in the event reducer. +- **Pass client-generated IDs to the server.** Use the same ID format as the server (hex-encoded timestamps). Pass `messageID` to `promptAsync` so the server echoes back the same ID. This prevents duplicates and enables in-place replacement. +- **Rollback on error.** Remove the optimistic entry from both the store and the shadow Map. +- **Stabilize bridge callbacks.** When wiring hook callbacks into module-level refs, use stable ref wrappers so effects do not loop on changing function identities. + +### Session/input consistency + +- **Capture send config at queue time.** Queue items must include provider/model/agent/variant snapshot; do not re-resolve from mutable live state at send time. +- **Keep server-selected attachments sendable.** Preserve server-backed file selections in queue/submit flows and convert them to proper `file://` URLs before sending. +- **Do not let text input state repaint unrelated chrome.** Typing should not force unrelated controls, menus, indicators, or toolbars to re-render on every keystroke. +- **Extract slow-changing chrome from hot input paths.** If controls do not depend on the current text value, move them behind memoized boundaries with stable callbacks. + +### Bootstrap resilience + +- **Treat startup 502/503 as transient.** Retry bootstrap/session-list flows with bounded retries/intervals, especially in VS Code where API readiness can lag bridge startup. +- **Use polling recovery when failures are swallowed.** If an async loader resolves without throwing on failure, recover with interval retries gated by loaded-state checks. + +### Scroll and DOM + +- **Never use `await waitForFrames()` for scroll preservation.** Frames of visible scroll jump are unacceptable. Use `useLayoutEffect` to adjust scroll synchronously after React commits DOM — before the browser paints. +- **Capture scroll state before the state change, restore in layout effect.** The pattern: save `scrollHeight`/`scrollTop` into a ref before triggering the update, consume it in `useLayoutEffect` on the rendered output. +- **Do not let viewport resizes masquerade as content growth.** Viewport-height changes must not trigger the same scroll compensation logic used for actual content growth. +- **Disable or narrow native/browser scroll anchoring when custom scroll logic exists.** Browser anchoring and app-managed pinning/follow logic will fight and produce jiggle. +- **Autosize textareas without transient collapse on growth.** Avoid `height='auto'` shrink/expand cycles on every character when the content only grew; this creates visible layout bounce. + +### List ordering and view consistency + +- **Do not sort structural lists directly from high-churn live fields.** If live updates are frequent, sorting directly from them causes reorder thrash and wide rerender cascades. +- **If live recency is required, freeze order during high-frequency updates and apply a one-shot reorder only at an intentional lifecycle edge.** Choose the lifecycle edge explicitly instead of letting every intermediate update reshuffle the UI. +- **Use one ordering source for all views of the same data.** Different views of the same entities must derive from the same ranked list or rank map; do not let each surface re-derive ordering independently. +- **Do not mix global snapshots and local live snapshots without an explicit reconciliation policy.** If multiple data sources feed one view, define which fields win and how they merge. + +### Component isolation + +- **Extract high-frequency hook consumers into separate components.** If a hook re-evaluates 60/sec (e.g., streaming status), wrap its consumer in a `React.memo` child component so the parent doesn't re-render. +- **Use custom `React.memo` comparators for message rows.** Compare render-relevant fields (role, finish, parts count, part IDs) — not object references. + +### Caching and memory + +- **Cap in-memory caches with both count and byte limits.** Entry count alone doesn't prevent memory bloat from large files. Use dual-constraint LRU (e.g., 40 entries OR 20MB). +- **Set store session limits to match loaded data.** If bootstrap loads N sessions, set `limit >= N`. Otherwise the next SSE event triggers trimming that silently removes sessions. +- **Invalidate caches on mutations.** File content cache must clear entries on write, delete, rename. Prefetch cache must clear on session eviction. +- **Use TTLs to prevent redundant fetches.** If a session was fetched <15s ago, skip re-fetching — SSE events keep it current. + +### Directory context + +- **Never cache directory strings in closures.** Directory can change at any time (worktree switch). Read it dynamically from `opencodeClient.getDirectory()` at call time. +- **Pass directory hints when the source of truth isn't available yet.** Newly created sessions aren't in the sync store until SSE delivers them. Pass the known directory as a parameter instead of relying on lookup. + +## Regression-prevention checklist + +- When adding fallback logic, ask: can stale persisted data keep this path active forever? +- When deriving UI state, ask: is this live state, historical state, or inferred state? +- When adding store fields, ask: who reads this, how often does it change, and should it live elsewhere? +- When touching polling or bootstrap, ask: can a lighter payload erase richer existing data? +- When handling optimistic updates, ask: where is rollback, reconciliation, and duplicate prevention? +- When changing shared routes or state contracts, ask: what breaks in web, desktop, and VS Code? +- When fixing a bug with a heuristic, prefer narrowing the heuristic over widening it. + +## Validation expectations + +- Run `bun run type-check` and `bun run lint` before finalizing. +- For hot-path changes, verify behavior under streaming or repeated events, not just static render. +- For sync or startup changes, verify fresh load, retry/failure, and restart behavior. +- For session changes, verify create, stream, abort, permission, archive/delete, and revisit flows when relevant. + ## Recent changes + - Releases + high-level changes: `CHANGELOG.md` - Recent commits: `git log --oneline` (latest tags: `v1.4.6`, `v1.4.5`) diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 28d9265..a2e7c79 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -4,6 +4,106 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [1.9.9] - 2026-04-26 + +- UI/Localization: added a localization foundation with translated interface strings for Spanish, Brazilian Portuguese, Ukrainian, and Simplified Chinese. +- Settings/Appearance: added selectable interface and code fonts with 10 choices each. +- Chat/Workflow: added keyboard turn navigation, widened chat content, and introduced a local workspace review and summarize slash commands for faster review handoff. +- Chat/Mobile: improved mention and autocomplete behavior with complete results, clearer active-tab scoping, and less context-switching while drafting prompts. +- Chat/Tasks: todo list progress now updates live as task status changes, and task/model status hints are steadier during active runs (thanks to @Yabuku-xD). +- Files/Editor: added an "Open files in preview mode" setting and improved multi-file edit/diff safety so review flows stay cleaner (thanks to @daveotero). +- Reliability/Performance: improved cold start and streaming responsiveness with lazy-loaded heavy components, chunk-load recovery, lower re-render churn, and safer reconnect/local-stream recovery (thanks to @Yabuku-xD, @jwcrystal, @vhqtvn). +- Desktop/Web/Mobile: improved Electron update restart behavior, PWA service-worker notifications, mobile keyboard handling, and the Add Project panel flow (thanks to @Jovines, @vhqtvn). + +## [1.9.8] - 2026-04-22 + +- Sessions/Reliability: fixed parent-child session sync during reconnects and navigation, so status and progress stay aligned in complex session trees (thanks to @jwcrystal). +- Settings/Sync: settings updates now sync more reliably across clients, and sidebar session pagination is steadier in larger workspaces. +- Sessions/Folders: folder changes now persist through server-backed endpoints, improving consistency across environments and path setups. +- Notifications: permission notifications are now suppressed when auto-accept is enabled, reducing noise during trusted runs. +- Chat/Files: improved changed-files handling in chat and restored quick file-open flows from pending changes, so jump-to-edit stays fast (thanks to @jwcrystal). +- UI: improved bottom scroll shadow behavior and hide the tasks row when there is no active work for a cleaner conversation view. +- Reliability/Desktop: improved live event-stream recovery after transient stalls, wait briefly before failing chat actions during reconnects, and persist Electron server logs for easier disconnect debugging. +- Desktop/macOS: System color mode now tracks OS theme changes, traffic-light controls stay visible after dock restore, and update restart/changelog handling is more reliable. +- Chat/Commands: added `/summary` slash command for a non-destructive session summary - optional topic hint after the command focuses the output, and the prompt is customizable under Settings: Magic Prompts. + +## [1.9.7] - 2026-04-22 + +- Desktop: added an Electron desktop runtime in parallel with the current Tauri app, with Electron planned to become the default path in an upcoming release. +- Plans/Notes/Todos: added editable project plans from assistant messages, external plan upload, configurable planning magic prompts, and quicker note/todo handoff into new sessions or worktrees. +- Chat/Files: you can now drag files and folders from the file tree into chat, with improved `@folder` autocomplete for faster context building (thanks to @youfch). +- Sessions/UI: added bulk session selection in the sidebar and fixed pinned sessions so they persist reliably after reloads (thanks to @yart). +- Files/Git: added a file-change summary bar and auto-refresh for open files changed outside the app, improving review flow and keeping editors in sync (thanks to @jwcrystal). +- Git/Worktrees: improved branch/worktree reliability by allowing checkout with uncommitted changes, tightening worktree cache invalidation, and reducing incorrect remote prefetches (thanks to @jwcrystal, @jasonalsing). +- Settings/MCP: improved MCP auth flow with better remote-config support and clearer diagnostics, and aligned config resolution with OpenCode behavior for more predictable setup (thanks to @daveotero, @cyan). +- Reliability/Chat: hardened bootstrap and stream-connection recovery, preserved session/connect state more reliably, and reduced streaming UI churn for smoother long runs. +- Web/PWA: added install orientation controls and fixed loopback-origin handling for web push notifications in local setups (thanks to @vhqtvn, @yart). + +## [1.9.6] - 2026-04-17 + +- Reliability/Streaming: switched live message events to a WebSocket-first transport with SSE fallback, added response compression, and hardened proxy/compression handling so long runs stay smoother on slower or proxied networks (thanks to @geekifan, @jwcrystal). +- Sessions/Scheduled Tasks: added scheduled task creation and management with locale-aware scheduling, so recurring prompts run at the right local time without manual re-entry. +- Sessions/Worktrees: enforced session worktree isolation and tightened session-switch safety, reducing cross-worktree mix-ups when resuming chats or running Git actions (thanks to @jwcrystal). +- Files: added a full Go to Line workflow (toolbar + shortcut + dialog) and a new Copy Relative Path action, making in-editor navigation and path sharing much faster (thanks to @coldbrow). +- Files: file trees now auto-refresh when files change outside the app, so new, renamed, or updated files appear without manual reloads (thanks to @jwcrystal). +- Chat/Export: added export session as Markdown and improved empty-state/export behavior, making conversation handoff and documentation cleaner (thanks to @coldbrow). +- Chat/Requests: restored blocking request visibility in sub-sessions, scoped auto-approve to the active session tree, and reduced noisy auto-approved notifications during multi-session work. +- Desktop: added quick open and a LAN access toggle, plus safer quit behavior around scheduled tasks for smoother local-network and day-to-day desktop workflows (thanks to @An-jinu). +- Chat/Markdown: added LaTeX rendering support for clearer math and technical notation in messages (thanks to @ricautomation). +- Settings/Skills: skills are now sorted within groups so larger skill lists are easier to scan (thanks to @roctom). + +## [1.9.5] - 2026-04-14 + +- Security/Auth: added passkey sign-in for protected instances and new 1-week/30-day session expiration options, so teams can enforce stronger access controls with flexible login persistence (thanks to @daveotero, @pm0u). +- Voice: added OpenAI-compatible custom server support for both text-to-speech and speech-to-text, including configurable TTS model/pitch/volume and stricter custom URL validation for safer setup (thanks to @ablepharus). +- Chat/Tool Output: added an interactive tree viewer for structured outputs and fixed JSON quote rendering, making large payloads easier to inspect and copy accurately (thanks to @yaozhenghangma). +- Chat/Reliability: fixed question-tool content disappearing after refresh and hardened subagent/session recovery paths, reducing silent failures and stuck task states (thanks to @jwcrystal). +- Sync/Performance: optimized multi-session streaming with per-directory queues, event coalescing, and parts-gap recovery to keep live updates smoother under heavy activity (thanks to @jwcrystal). +- Sessions/UI: kept active sessions visible in Recent, auto-expanded parent groups when opening subagent sessions, and hid empty archived/folder sections for cleaner navigation (thanks to @jwcrystal). +- Git/UI: restored Git changes panel visibility and sidebar sync, so change review stays available and consistent while switching contexts (thanks to @jwcrystal). +- Desktop/Startup: delivered a more guided first-launch and smart recovery flow, plus startup and remote-window interaction fixes to reduce early-session friction (thanks to @jwcrystal). +- Usage: added Zhipu AI Coding Plan tracking and restored model-variant compatibility with older OpenCode runtimes for more reliable quota reporting and model selection (thanks to @cainiao1992, @Chi-square-test). + +## [1.9.4] - 2026-04-07 + +- Settings/Magic Prompts: added a dedicated Magic Prompts page with editable templates for commit/PR generation, PR and issue reviews, failed-check/comment analysis, and merge/cherry-pick conflict resolution. +- Chat/Performance: reduced streaming render churn across the app, so long responses stay smoother with less UI jitter during active runs. +- Chat/Scrolling: fixed jumpy follow behavior and restored stable bottom-resume/live-compaction updates, so staying on the latest output is more reliable. +- Reliability/Streaming: improved reconnect, retry, and directory-aware event routing to reduce stuck session/subagent states after transient disconnects (thanks to @jwcrystal, @daveotero). +- Chat/Tool Output: LSP diagnostics now render directly in tool output, making inline error review faster while iterating (thanks to @yulia-ivashko). +- Models: added defensive handling for missing model pricing/capability metadata so model controls fail less often with incomplete provider data (thanks to @Chi-square-test). +- Desktop/Performance: removed costly window translucency and reduced duplicate notification triggers for a cooler, less noisy desktop experience. +- Startup/Remote: restored remote provider startup behavior and tightened host/port detection to reduce false startup failures. +- Usage: refreshed MiniMax CN coding-plan quota data for more accurate usage reporting (thanks to @nzlov). + +## [1.9.3] - 2026-03-01 + +- Security/Chat: user messages now escape raw HTML by default, so pasted markup is shown safely as text instead of being interpreted by the renderer (thanks to @kalac2232). +- Desktop/Performance: reduced Tauri shell CPU/GPU overhead to keep the Desktop app cooler and smoother during longer sessions. +- Sessions/Drafts: draft chat config now stays synced with the selected draft target directory, reducing wrong-model or wrong-agent carryover when switching draft context (thanks to @hkay-dev). +- VSCode/Files: added file stat support in the extension bridge so markdown-related file checks resolve more reliably before opening or rendering (thanks to @geekifan). +- Chat/Models: added arrow-key navigation for thinking-mode selection in model controls, making keyboard model tuning faster during prompt setup (thanks to @daveotero). +- Files: added HTML preview support in the file viewer, so `.html` files can be inspected visually without leaving OpenChamber (thanks to @nguyenngothuong). +- Chat: improved error message readability with clearer styling and safer word-wrapping, so failures are easier to scan without layout breakage (thanks to @nguyenngothuong). +- Chat/JSON: added an interactive JSON tree viewer with collapse/expand controls and richer color cues for easier inspection of large structured outputs (thanks to @nguyenngothuong). +- Mobile/Settings: fixed lingering settings drawers and removed extra top spacing for a cleaner, less obstructed mobile layout (thanks to @Jovines). +- Git/Worktrees: fixed worktree detection and reset stale integration state when switching contexts, reducing wrong-target behavior in worktree flows (thanks to @jwcrystal). +- Desktop/Settings: window vibrancy now correctly controls macOS window transparency, and settings copy now clarifies when full transparency changes take effect. +- Reliability/Proxy: hardened OpenCode proxy header handling (including identity-encoding normalization, compression-header cleanup, hop-by-hop response-header stripping) and suppressed expected SSE close noise, improving stream stability and reducing false proxy errors (thanks to @jwcrystal, @Jovines, @JiwaniZakir, @shekohex). +- Reliability/Proxy: restored proxied chat event streaming so live responses continue working when OpenChamber is deployed behind a proxy. +- Terminal/Reliability: switched terminal transport to a pure WebSocket path with fallback handling, improving responsiveness and stability for interactive terminal sessions (thanks to @geekifan). +- Usage/Providers: added ZhipuAI quota tracking and fixed MiniMax coding-plan and GitHub Copilot overusage calculations for more accurate usage reporting (thanks to @kalac2232, @baruchvitorino, @ebrainte). + +## [1.9.2] - 2026-03-31 + +- Chat/Performance: rebuilt live session sync and streaming updates to cut render churn, reduce CPU spikes, and keep long-running chats smoother and more stable across runtimes. +- Worktrees/Multi-Run: added instant draft-first worktree creation and redesigned the multi-run launcher with a cleaner, faster flow for parallel runs. +- VSCode/UI: polished the extension chat and sidebar with improved spacing/tooltips, a resizable sessions pane, and better file-to-chat mention flows from Explorer. +- Models/Providers: improved custom provider model metadata loading and caching so model details stay more complete and consistent (thanks to @ZeppLu). +- CLI/Server: added `--foreground` for process-manager deployments, made managed server hostname configurable, and added an explicit `--host` option with safer localhost defaults (thanks to @colinmollenhour, @rapidrabbit76, @yulia-ivashko). +- Docker/Deployments: improved container defaults for broader compatibility, including UID 1000 user behavior, non-fatal SSH key generation, and better localhost detection in container networking (thanks to @yulia-ivashko). +- Web/PWA: fixed manifest behavior behind Cloudflare Access so install flows work more reliably in protected environments (thanks to @arthurfiorette). + ## [1.9.1] - 2026-03-20 - Sessions/UI: restored Project Notes access in the sidebar, polished notes/todo editing, and fixed project action overlap so project controls stay reachable for non-git directories. @@ -16,7 +116,6 @@ All notable changes to this project will be documented in this file. - Desktop: improved stale server-process cleanup on startup and fixed external link opening behavior for more predictable app interactions (thanks to @jwcrystal). - Usage: added MiniMax Weekly quota provider support for broader usage tracking coverage (thanks to @nzlov). - ## [1.9.0] - 2026-03-20 - UI/Navigation: delivered a major sidebar redesign with clearer hierarchy, unified action patterns, and improved session organization for better navigation through multiple projects (thanks to @yulia-ivashko). @@ -33,7 +132,6 @@ All notable changes to this project will be documented in this file. - Desktop/macOS: lowered the minimum supported macOS version to Ventura (13.0), expanding compatibility on older systems (thanks to @craigharman). - Updates/Reliability: unified update-check behavior across runtimes for more consistent update availability checks. - ## [1.8.7] - 2026-03-13 - CLI: fixed a startup regression in global npm/bun installs where wrapper or symlinked `openchamber` entrypoints could exit without output on commands like `--version` or `status`. @@ -41,8 +139,6 @@ All notable changes to this project will be documented in this file. - Windows/Web: daemon startup and Git operations no longer flash extra console windows, making background workflows less distracting (thanks to @SergioChan). - Deployment/Docker: improved `docker run` startup behavior and entrypoint handling so containerized installs start more reliably (thanks to @nzlov). - - ## [1.8.6] - 2026-03-13 - Tunnel/CLI: rebuilt tunnel workflows around clearer managed modes and provider-aware lifecycle commands, with safer startup checks, improved diagnostics, and cleaner CLI output for everyday remote access (thanks to @yulia-ivashko). @@ -68,7 +164,6 @@ All notable changes to this project will be documented in this file. - Tunnel/CLI: fixed one-time Cloudflare tunnel connect links in CLI output for `--try-cf-tunnel`, so remote collaborators can use the printed URL/QR flow successfully (thanks to @plfavreau). - Mobile/PWA: respected OS rotation lock by removing forced orientation behavior in the web app shell (thanks to @theluckystrike). - ## [1.8.4] - 2026-03-04 - Chat: added clickable file-path links in assistant messages (including line targeting), so you can jump from answer text straight to the exact file location (thanks to @yulia-ivashko). @@ -89,7 +184,6 @@ All notable changes to this project will be documented in this file. - UI: interactive controls now consistently show pointer cursors, improving click affordance and reducing ambiguous hover states (thanks to @KJdotIO). - Security/Reliability: hardened terminal auth, tightened skill-file path protections, and reduced sensitive request logging exposure for safer day-to-day usage (thanks to @yulia-ivashko). - ## [1.8.3] - 2026-03-02 - Chat: added user-message display controls for plain-text rendering and sticky headers, so you can tune readability to match your preferences. @@ -103,7 +197,6 @@ All notable changes to this project will be documented in this file. - Settings: reorganized chat display settings into a more compact two-column layout, so more new options are easier to navigate. - Mobile/UI: fixed session-title overflow in compact headers so running/unread indicators and actions remain visible (thanks to @iamhenry). - ## [1.8.2] - 2026-03-01 - Updates: hardened the self-update flow with safer release handling and fallback behavior, reducing failed or stuck updates. @@ -116,12 +209,10 @@ All notable changes to this project will be documented in this file. - Notifications/Voice: consolidated TTS and summarization service wiring for steadier text-to-speech and summary flows (thanks to @nelsonPires5). - Deployment: fixed Docker build/runtime issues for more reliable containerized setups (thanks to @nzlov). - ## [1.8.1] - 2026-02-28 - Web/Auth: fixed an issue where non-tunnel browser sessions could incorrectly show a tunnel-only lock screen; normal auth flow now appears unless a tunnel is actually active. - ## [1.8.0] - 2026-02-28 - Desktop: added SSH remote instance support with dedicated lifecycle and UX flows, so you can work against remote machines more reliably (thanks to @shekohex). @@ -147,7 +238,6 @@ All notable changes to this project will be documented in this file. - Usage: added MiniMax coding-plan quota provider support for broader usage tracking coverage (thanks to @nzlov). - Usage: added Ollama Cloud quota provider support for broader usage tracking coverage (thanks to @iamhenry). - ## [1.7.5] - 2026-02-25 - UI: moved projects into a dedicated sidebar rail and tightened the layout so switching projects and sessions feels faster. @@ -159,7 +249,6 @@ All notable changes to this project will be documented in this file. - Web: added `OPENCODE_HOST` support so you can connect directly to an external OpenCode server using a full base URL (thanks to @colinmollenhour). - Web/Mobile: fixed in-app update flow in containerized setups so updates apply correctly. - ## [1.7.4] - 2026-02-24 - Settings: redesigned the settings workspace with flatter, more consistent page layouts so configuration is faster to scan and edit. @@ -176,7 +265,6 @@ All notable changes to this project will be documented in this file. - Desktop: improved remote instance URL handling for more reliable host/query matching (thanks to @shekohex). - Files: added C, C++, and Go language support for syntax-aware rendering in code-heavy workflows (thanks to @fomenks). - ## [1.7.3] - 2026-02-21 - Settings: added customizable keyboard shortcuts for chat actions, panel toggles, and services, so you can better match OpenChamber to your workflow (thanks to @nelsonPires5). @@ -188,7 +276,6 @@ All notable changes to this project will be documented in this file. - Reliability: improved startup environment detection by capturing login-shell environment snapshots, reducing missing PATH/tool issues on launch. - Reliability: refactored OpenCode config/auth integration into domain modules for steadier provider auth and command loading flows (thanks to @nelsonPires5). - ## [1.7.2] - 2026-02-20 - Chat: question prompts now guide you to unanswered items before submit, making tool-question flows faster. @@ -198,7 +285,6 @@ All notable changes to this project will be documented in this file. - Settings: model variant options now refresh correctly in draft/new-session flows, avoiding stale selections. - Reliability: provider auth failures now show clearer re-auth guidance when tokens expire, making recovery faster (thanks to @yulia-ivashko). - ## [1.7.1] - 2026-02-18 - Chat: slash commands now follow server command semantics (including multiline arguments), so command behavior is more consistent with OpenCode CLI. @@ -210,17 +296,15 @@ All notable changes to this project will be documented in this file. - Mobile: fixed accidental abort right after tapping Send on touch devices, reducing interrupted responses (thanks to @shekohex). - Maintenance: removed deprecated GitHub Actions cloud runtime assets and docs to reduce setup confusion (thanks to @yulia-ivashko). - ## [1.7.0] - 2026-02-17 - Chat: improved live streaming with part-delta updates and smarter auto-follow scrolling, so long responses stay readable while they generate. - Chat: Mermaid diagrams now render inline in assistant messages, with quick copy/download actions for easier sharing. - UI: added a context overview panel with token usage, cost breakdown, and raw message inspection to make session debugging easier. - Sessions: project icon and color customizations now persist reliably across restarts. -**- Reliability: managed local OpenCode runtimes now use rotated secure auth and tighter lifecycle control across runtimes, reducing stale-process and reconnect issues (thanks to @yulia-ivashko).** + **- Reliability: managed local OpenCode runtimes now use rotated secure auth and tighter lifecycle control across runtimes, reducing stale-process and reconnect issues (thanks to @yulia-ivashko).** - Git/GitHub: improved backend reliability for repository and auth operations, helping branch and PR flows stay more predictable (thanks to @nelsonPires5). - ## [1.6.9] - 2026-02-16 - **UI: redesigned the workspace shell with a context panel, tabbed sidebars, and quicker navigation across chat, files, and reviews, so daily workflows feel more focused.** @@ -237,7 +321,6 @@ All notable changes to this project will be documented in this file. - Desktop: improved day-to-day polish with restored desktop window geometry and posiotion (thanks to @yulia-ivashko). - Mobile: fixes for small-screen editor, terminal, and layout overlap issues (thanks to @gsxdsm, @nelsonPires5). - ## [1.6.8] - 2026-02-12 - Chat: added drag-and-drop attachments with inline image previews, so sharing screenshots and files in prompts feels much faster and more reliable. @@ -249,7 +332,6 @@ All notable changes to this project will be documented in this file. - Desktop: fixed project selection in opened remote instances. - Desktop: fixed opened remote instances that use HTTP (helpful for instances under tunneling). - ## [1.6.7] - 2026-02-10 - Voice: added built-in voice input and read-aloud responses with multiple providers, so you can drive chats hands-free when typing is slower (thanks to @gsxdsm). @@ -277,7 +359,6 @@ All notable changes to this project will be documented in this file. - Mobile: fixed chat input layout issues on small screens (thanks to @nelsonPires5). - Reliability: fixed OpenCode auth pass-through and proxy env handling to reduce intermittent connection/auth issues (thanks to @gsxdsm). - ## [1.6.5] - 2026-02-6 - Settings: added an OpenCode CLI path override so you can point OpenChamber at a custom/local CLI install. @@ -290,7 +371,6 @@ All notable changes to this project will be documented in this file. - UI: added Vitesse Dark and Vitesse Light theme presets. - Reliability: improved OpenCode binary resolution and HOME-path handling across runtimes for steadier local startup. - ## [1.6.4] - 2026-02-5 - Desktop: switch between local and remote OpenChamber instances, plus a thinner runtime for better feature parity and fewer desktop-only quirks. @@ -304,7 +384,6 @@ All notable changes to this project will be documented in this file. - Web: fixed missing icon when installing the Android PWA (thanks to @nelsonPires5). - GitHub: PR description generation supports optional extra context for better summaries (thanks to @nelsonPires5). - ## [1.6.3] - 2026-02-2 - Web: improved server readiness check to use the `/global/health` endpoint for more reliable startup detection. @@ -312,7 +391,6 @@ All notable changes to this project will be documented in this file. - VSCode: improved server health check with the proper health API endpoint and increased timeout for steadier startup (thanks to @wienans). - Settings: dialog no longer persists open/closed state across app restarts. - ## [1.6.2] - 2026-02-1 - Usage: new multi-provider quota dashboard to monitor API usage across OpenAI, Google, and z.ai (thanks to @nelsonPires5). @@ -324,7 +402,6 @@ All notable changes to this project will be documented in this file. - Worktrees: workspace path now resolves correctly when using git worktrees (thanks to @nelsonPires5). - Projects: fixed directory creation outside workspace in the Add Project modal (thanks to @nelsonPires5). - ## [1.6.1] - 2026-01-30 - Chat: added Stop button to cancel generation mid-response. @@ -337,7 +414,6 @@ All notable changes to this project will be documented in this file. - Git: commit message generation now includes untracked files and handles git diff --no-index comparisons more reliably (thanks to @MrLYC). - Desktop: improved macOS window chrome and header spacing, including steadier traffic lights on older macOS versions (thanks to @yulia-ivashko). - ## [1.6.0] - 2026-01-29 - Chat: added message stall detection with automatic soft resync for more reliable message delivery. @@ -349,7 +425,6 @@ All notable changes to this project will be documented in this file. - Web: session activity tracking now works consistently across browser tabs. - Reliability: plans directory no longer errors when missing. - ## [1.5.9] - 2026-01-28 - Worktrees: migrated to Opencode SDK worktree implementation; sessions in worktrees are now completely isolated. @@ -360,7 +435,6 @@ All notable changes to this project will be documented in this file. - UI: Files, Diff, Git, and Terminal now follow the active session/worktree directory, including new-session drafts. - Web: plan lists no longer error when the plans directory is missing. - ## [1.5.8] - 2026-01-26 - Plans: new Plan/Build mode switching support with dedicated Plan content view with per-session context. @@ -373,14 +447,12 @@ All notable changes to this project will be documented in this file. - Activity: added a text-justification setting for activity summaries (thanks to @iyangdianfeng). - Reliability: file lists and message sends handle missing directories and transient errors more gracefully. - ## [1.5.7] - 2026-01-24 - GitHub: PR panel supports fork PR detection by branch name. - GitHub: Git tab PR panel can send failed checks/comments to chat with hidden context; added check details dialog with Actions step breakdown. - Web: GitHub auth flow fixes. - ## [1.5.6] - 2026-01-24 - GitHub: connect your account in Settings with device-flow auth to enable GitHub tools. @@ -389,7 +461,6 @@ All notable changes to this project will be documented in this file. - Git: manage pull requests in the Git view with AI-generated descriptions, status checks, ready-for-review, and merge actions. - Mobile: fixed CommandAutocomplete dropdown scrolling (thanks to @nelsonPires5). - ## [1.5.5] - 2026-01-23 - Navigation: URLs now sync the active session, tab, settings, and diff state for shareable links and reliable back/forward (thanks to @TaylorBeeston). @@ -398,7 +469,6 @@ All notable changes to this project will be documented in this file. - Web: push notifications no longer fire when a window is visible, avoiding duplicate alerts. - Web: improved push subscription handling across multiple windows for more reliable delivery. - ## [1.5.4] - 2026-01-22 - Chat: new Apply Patch tool UI with diff preview for patch-based edits. @@ -409,7 +479,6 @@ All notable changes to this project will be documented in this file. - Web: added Background notifications for PWA. - Reliability: connect to external OpenCode servers without auto-start and fixed subagent crashes (thanks to @TaylorBeeston). - ## [1.5.3] - 2026-01-20 - Files: edit files inline with syntax highlighting, draft protection, and save/discard flow. @@ -422,7 +491,6 @@ All notable changes to this project will be documented in this file. - Git: generated commit messages now auto-pick a gitmoji when enabled (thanks to @TheRealAshik). - Performance: faster filesystem/search operations and general stability improvements (thanks to @TheRealAshik). - ## [1.5.2] - 2026-01-17 - Sessions: added branch picker dialog to start new worktree sessions from local branches (thanks to @nilskroe). @@ -434,13 +502,11 @@ All notable changes to this project will be documented in this file. - VSCode: tuned layout breakpoint and server readiness timeout for steadier startup. - Reliability: improved OpenCode process cleanup to reduce orphaned servers. - ## [1.5.1] - 2026-01-16 - Desktop: fixed orphaned OpenCode processes not being cleaned up on restart or exit. - Opencode: fixed issue with reloading configuration was killing the app - ## [1.5.0] - 2026-01-16 - UI: added a new Files tab to browse workspace files directly from the interface. @@ -453,7 +519,6 @@ All notable changes to this project will be documented in this file. - Stability: fixed heartbeat race condition causing session stalls during long tasks (thanks to @tybradle). - Desktop: fixed commands for worktree setup access to PATH. - ## [1.4.9] - 2026-01-14 - VSCode: added session editor panel to view sessions alongside files. @@ -462,7 +527,6 @@ All notable changes to this project will be documented in this file. - Mobile: fixed iOS keyboard safe area padding for home indicator bar (thanks to @Jovines). - Upload: increased attachment size limit to 50MB with automatic image compression to 2048px for large files. - ## [1.4.8] - 2026-01-14 - Git Identities: added token-based authentication support with ~/.git-credentials discovery and import. @@ -474,13 +538,11 @@ All notable changes to this project will be documented in this file. - Reliability: improved project state preservation on validation failures (thanks to @vio1ator) and refined server health monitoring. - Stability: added graceful shutdown handling for the server process (thanks to @vio1ator). - ## [1.4.7] - 2026-01-10 - Skills: added ClawdHub integration as built-in market for skills. - Web: fixed issues in terminal - ## [1.4.6] - 2026-01-09 - VSCode/Web: switch opencode cli management to SDK. @@ -488,7 +550,6 @@ All notable changes to this project will be documented in this file. - Shortcuts: switched agent cycling shortcut from Shift + TAB to TAB again. - Chat: added question tool support with a rich UI for interaction. - ## [1.4.5] - 2026-01-08 - Chat: added support for model variants (thinking effort). @@ -499,7 +560,6 @@ All notable changes to this project will be documented in this file. - MCP: added ability to dynamically enabled/disabled configured MCP. - Web: refactored project adding UI with autocomplete. - ## [1.4.4] - 2026-01-08 - Agent Manager / Multi Run: select agent per worktree session (thanks to @wienans). @@ -515,7 +575,6 @@ All notable changes to this project will be documented in this file. - Tunnel: added QR code and password URL for Cloudflare tunnel (thanks to @martindonadieu). - Model selector: fixed dropdowns not responding to viewport size. - ## [1.4.3] - 2026-01-04 - VS Code extension: added Agent Manager panel to run the same prompt across up to 5 models in parallel (thanks to @wienans). @@ -523,7 +582,6 @@ All notable changes to this project will be documented in this file. - Added "Open subAgent session" button on task tool outputs to quickly navigate to child sessions (thanks to @aptdnfapt). - VS Code extension: improved activation reliability and error handling. - ## [1.4.2] - 2026-01-02 - Added timeline dialog (`/timeline` command or Cmd/Ctrl+T) for navigating, reverting, and forking from any point in the conversation (thanks to @aptdnfapt). @@ -532,7 +590,6 @@ All notable changes to this project will be documented in this file. - Desktop app: keyboard shortcuts now use Cmd on macOS and Ctrl on web/other platforms (thanks to @sakhnyuk). - Migrated to OpenCode SDK v2 with improved API types and streaming. - ## [1.4.1] - 2026-01-02 - Added the ability to select the same model multiple times in multi-agent runs for response comparison. @@ -545,7 +602,6 @@ All notable changes to this project will be documented in this file. - Terminal: improved terminal performance and stability by switching to the Ghostty-based terminal renderer, while keeping the existing terminal UX and per-directory sessions. - Terminal: fixed several issues with terminal session restore and rendering under heavy output, including switching directories and long-running TUI apps. - ## [1.4.0] - 2026-01-01 - Added the ability to run multiple agents from a single prompt, with each agent working in an isolated worktree. @@ -557,14 +613,12 @@ All notable changes to this project will be documented in this file. - Chat: now shows clearer error messages when agent messages fail. - Sidebar: improved readability for sticky headers with a dynamic background. - ## [1.3.9] - 2025-12-30 - - Added skills management to settings with the ability to create, edit, and delete skills (make sure you have the latest OpenCode version for skills support). +- Added skills management to settings with the ability to create, edit, and delete skills (make sure you have the latest OpenCode version for skills support). - Added Skills catalog functionality for discovering and installing skills from external sources. - VS Code extension: added right-click context menu with "Add to Context," "Explain," and "Improve Code" actions (thanks to @wienans). - ## [1.3.8] - 2025-12-29 - Added Intel Mac (x86_64) support for the desktop application (thanks to @rothnic). @@ -575,7 +629,6 @@ All notable changes to this project will be documented in this file. - Fixed scroll position persistence for active conversation turns across session switches. - Refactored Agents/Commands management with ability to configure project/user scopes. - ## [1.3.7] - 2025-12-28 - Redesigned Settings as a full-screen view with tabbed navigation. @@ -585,13 +638,11 @@ All notable changes to this project will be documented in this file. - Improved session activity status handling and message step completion logic. - Introduced enchanced VSCode extension settings with dynamic layout based on width. - ## [1.3.6] - 2025-12-27 - Added the ability to manage (connect/disconnect) providers in settings. - Adjusted auto-summarization visuals in chat. - ## [1.3.5] - 2025-12-26 - Added Nushell support for operations with Opencode CLI. @@ -603,14 +654,12 @@ All notable changes to this project will be documented in this file. - Added Discord links in the about section. - Added settings for choosing the default model/agent to start with in a new session. - ## [1.3.4] - 2025-12-25 - Diff view now loads reliably even with large files and slow networks. - Fixed getting diffs for worktree files. - VS Code extension: improved type checking and editor integration. - ## [1.3.3] - 2025-12-25 - Updated OpenCode SDK to 1.0.185 across all app versions. @@ -621,13 +670,11 @@ All notable changes to this project will be documented in this file. - Chat UI: improved turn grouping/activity rendering and fixed message metadata/agent selection propagation. - Chat UI: improved agent activity status behavior and reduced image thumbnail sizes for better readability. - ## [1.3.2] - 2025-12-22 - Fixed new bug session when switching directories - Updated Opencode SDK to the latest version - ## [1.3.1] - 2025-12-22 - New chats no longer create a session until you send your first message. @@ -635,7 +682,6 @@ All notable changes to this project will be documented in this file. - Fixed mobile and VSCode sessions handling - Updated app identity with new logo and icons across all platforms. - ## [1.3.0] - 2025-12-21 - Added revert functionality in chat for user messages. @@ -646,21 +692,18 @@ All notable changes to this project will be documented in this file. - Adjusted VSCode extension theme mapping and model selection view. - Polished file autocomplete experience. - ## [1.2.9] - 2025-12-20 - Session auto‑cleanup feature with configurable retention for each app version including VSCode extension. - Ability to update web package from mobile/PWA view in setting. - A lot of different optimization for a long sessions. - ## [1.2.8] - 2025-12-19 - Introduced update mechanism for web version that doesn't need any cli interaction. - Added installation script for web version with package managed detection. - Update and restart of web server now support automatic pick-up of previously set parameters like port or password. - ## [1.2.7] - 2025-12-19 - Comprehensive macOS native menu bar entries. @@ -668,14 +711,12 @@ All notable changes to this project will be documented in this file. - Improved theme consistency across dropdown menus, selects, and command palette. - Introduced keyboard shortcuts help menu and quick actions menu. - ## [1.2.6] - 2025-12-19 - Added write/create tool preview in permission cards with syntax highlighting. - More descriptive assistant status messages with tool-specific and varied idle phrases. - Polished Git view layout - ## [1.2.5] - 2025-12-19 - Polished chat expirience for longer session. @@ -685,13 +726,11 @@ All notable changes to this project will be documented in this file. - Fixed untracked files in new directories not showing individually. - Smoother session rename experience. - ## [1.2.4] - 2025-12-18 - MacOS app menu entries for Check for update and for creating bug/request in Help section. - For Mobile added settings, improved terminal scrolling, fixed app layout positioning. - ## [1.2.3] - 2025-12-17 - Added image preview support in Diff tab (shows original/modified images instead of base64 code). @@ -699,28 +738,24 @@ All notable changes to this project will be documented in this file. - Optimized git polling and background diff+syntax pre-warm for instant Diff tab open. - Optomized reloading unaffected diffs. - ## [1.2.2] - 2025-12-17 - Agent Task tool now renders progressively with live duration and completed sub-tools summary. - Unified markdown rendering between assistant messages and tool outputs. - Reduced markdown header sizes for better visual balance. - ## [1.2.1] - 2025-12-16 - Todo task tracking: collapsible status row showing AI's current task and progress. - Switched "Detailed" tool output mode to only open the 'task', 'edit', 'multiedit', 'write', 'bash' tools for better performance. - ## [1.2.0] - 2025-12-15 - Favorite & recent models for quick access in model selection. - Tool call expansion settings: collapsed, activity, or detailed modes. - Font size & spacing controls (50-200% scaling) in Appearance Settings. - Settings page access within VSCode extension. -Thanks to @theblazehen for contributing these features! - + Thanks to @theblazehen for contributing these features! ## [1.1.6] - 2025-12-15 @@ -728,27 +763,23 @@ Thanks to @theblazehen for contributing these features! - Improved mobile experience: simplified header, better diff file selector. - Redesigned password-protected session unlock screen. - ## [1.1.5] - 2025-12-15 - Enhanced file attachment features performance. - Added fuzzy search feature for file mentioning with @ in chat. - Optimized input area layout. - ## [1.1.4] - 2025-12-15 - Flexoki themes for Shiki syntax highlighting for consistency with the app color schema. - Enchanced VSCode extension theming with editor themes. - Fixed mobile view model/agent selection. - ## [1.1.3] - 2025-12-14 - Replaced Monaco diff editor with Pierre/diffs for better performance. - Added line wrap toggle in diff view with dynamic layout switching (auto-inline when narrow). - ## [1.1.2] - 2025-12-13 - Moved VS Code extension to activity bar (left sidebar). @@ -756,13 +787,11 @@ Thanks to @theblazehen for contributing these features! - Removed redundant VS Code commands. - Enhanced UserTextPart styling. - ## [1.1.1] - 2025-12-13 - Adjusted model/agent selection alignment. - Fixed user message rendering issues. - ## [1.1.0] - 2025-12-13 - Added assistant answer fork flow so users can start a new session from an assistant plan/response with inherited context. @@ -770,7 +799,6 @@ Thanks to @theblazehen for contributing these features! - Improved scroll performance with force flag and RAF placeholder. - Added git polling backoff optimization. - ## [1.0.9] - 2025-12-08 - Added directory picker on first launch to reduce macOS permission prompts. @@ -778,48 +806,40 @@ Thanks to @theblazehen for contributing these features! - Improved update dialog UI with inline version display. - Added macOS folder access usage descriptions. - ## [1.0.8] - 2025-12-08 - Added fallback detection for OpenCode CLI in ~/.opencode/bin. - Added window focus after app restart/update. - Adapted traffic lights position and corner radius for older macOS versions. - ## [1.0.7] - 2025-12-08 - Optimized Opencode binary detection. - Adjusted app update experience. - ## [1.0.6] - 2025-12-08 - Enhance shell environment detection. - ## [1.0.5] - 2025-12-07 - Fixed "Load older messages" incorrectly scrolling to bottom. - Fixed page refresh getting stuck on splash screen. - Disabled devtools and page refresh in production builds. - ## [1.0.4] - 2025-12-07 - Optimized desktop app start time - ## [1.0.3] - 2025-12-07 - Updated onboarding UI. - Updated sidebar styles. - ## [1.0.2] - 2025-12-07 - Updated MacOS window design to the latest one. - ## [1.0.1] - 2025-12-07 - Initial public release of OpenChamber web and desktop packages in a unified monorepo. diff --git a/src/README.md b/src/README.md index bbe6f9d..593c849 100644 --- a/src/README.md +++ b/src/README.md @@ -241,6 +241,11 @@ environment: Managed-local path note: `OPENCHAMBER_TUNNEL_CONFIG` must point to a path inside the container user home (`/home/openchamber/...`). If your Cloudflare config references a credentials JSON file, that file path must also be accessible inside the container (mount with `volumes`). +### Reverse proxy notes + +- For a complete reverse proxy setup guide, see [`docs/REVERSE_PROXY.md`](./docs/REVERSE_PROXY.md). +- Website docs source lives at `packages/docs/content/docs/reverse-proxy.mdx`. + ### Tunnel behavior notes - OpenChamber supports one active tunnel per running instance (port). diff --git a/src/bun.lock b/src/bun.lock index daaa0f7..7c0efe1 100644 --- a/src/bun.lock +++ b/src/bun.lock @@ -5,6 +5,7 @@ "": { "name": "openchamber-monorepo", "dependencies": { + "@base-ui/react": "^1.4.0", "@codemirror/autocomplete": "^6.20.0", "@codemirror/commands": "^6.10.1", "@codemirror/lang-cpp": "^6.0.3", @@ -19,11 +20,11 @@ "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.12.1", + "@codemirror/language": "6.12.2", "@codemirror/lint": "^6.9.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.13", + "@codemirror/view": "6.39.13", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@heroui/scroll-shadow": "^2.3.18", @@ -32,7 +33,7 @@ "@ibm/plex": "^6.4.1", "@lezer/highlight": "^1.2.3", "@octokit/rest": "^22.0.1", - "@opencode-ai/sdk": "^1.3.0", + "@opencode-ai/sdk": "^1.4.25", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -94,17 +95,33 @@ }, "packages/desktop": { "name": "@openchamber/desktop", - "version": "1.9.1", + "version": "1.9.8", "devDependencies": { "@tauri-apps/cli": "^2", "@types/node": "^24.3.1", "typescript": "~5.8.3", }, }, + "packages/electron": { + "name": "@openchamber/electron", + "version": "1.9.8", + "dependencies": { + "@openchamber/web": "workspace:*", + "electron-context-menu": "^4.1.2", + "electron-log": "^5.4.3", + "electron-updater": "^6.8.3", + }, + "devDependencies": { + "@electron/rebuild": "^3.7.0", + "electron": "^41.2.1", + "electron-builder": "^26.0.0", + }, + }, "packages/ui": { "name": "@openchamber/ui", - "version": "1.9.1", + "version": "1.9.8", "dependencies": { + "@base-ui/react": "^1.4.0", "@codemirror/autocomplete": "^6.20.0", "@codemirror/commands": "^6.10.1", "@codemirror/lang-cpp": "^6.0.3", @@ -119,13 +136,13 @@ "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.12.1", + "@codemirror/language": "6.12.2", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", "@codemirror/lint": "^6.9.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.13", + "@codemirror/view": "6.39.13", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -133,20 +150,10 @@ "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", "@lezer/highlight": "^1.2.3", - "@opencode-ai/sdk": "^1.3.0", + "@opencode-ai/sdk": "^1.4.25", "@pierre/diffs": "1.1.0-beta.13", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-toggle": "^1.1.10", - "@radix-ui/react-tooltip": "^1.2.8", "@remixicon/react": "^4.7.0", - "@streamdown/code": "^1.0.2", + "@simplewebauthn/browser": "13.3.0", "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", "beautiful-mermaid": "^1.1.3", @@ -154,12 +161,16 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "codemirror-lang-elixir": "^4.0.0", + "dompurify": "^3.2.7", "express": "^5.1.0", "fuse.js": "^7.1.0", "ghostty-web": "^0.4.0", "heic2any": "^0.0.4", "html-to-image": "^1.11.13", "http-proxy-middleware": "^3.0.5", + "katex": "^0.16.21", + "marked": "^17.0.3", + "morphdom": "^2.7.7", "motion": "^12.23.24", "next-themes": "^0.4.6", "prismjs": "^1.30.0", @@ -167,9 +178,11 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-syntax-highlighter": "^15.6.6", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", + "remend": "^1.2.1", "simple-git": "^3.28.0", "sonner": "^2.0.7", - "streamdown": "^2.2.0", "strip-json-comments": "^5.0.3", "tailwind-merge": "^3.3.1", "yaml": "^2.8.1", @@ -179,7 +192,7 @@ "devDependencies": { "@eslint/js": "^9.33.0", "@tailwindcss/postcss": "^4.0.0", - "@tauri-apps/api": "^2.9.0", + "@tauri-apps/api": "^2.10.1", "@types/node": "^24.3.1", "@types/prismjs": "^1.26.6", "@types/qrcode": "^1.5.5", @@ -205,10 +218,10 @@ }, "packages/vscode": { "name": "openchamber", - "version": "1.9.1", + "version": "1.9.8", "dependencies": { "@openchamber/ui": "workspace:*", - "@opencode-ai/sdk": "^1.3.0", + "@opencode-ai/sdk": "^1.4.25", "adm-zip": "^0.5.16", "jsonc-parser": "^3.3.1", "react": "^19.1.1", @@ -228,19 +241,42 @@ }, "packages/web": { "name": "@openchamber/web", - "version": "1.9.1", + "version": "1.9.8", "bin": { "openchamber": "./bin/cli.js", }, "dependencies": { "@clack/prompts": "^1.1.0", + "@octokit/rest": "^22.0.1", + "@opencode-ai/sdk": "^1.4.25", + "@simplewebauthn/server": "13.3.0", + "adm-zip": "^0.5.16", + "better-sqlite3": "^11.7.0", + "bun-pty": "^0.4.5", + "compression": "^1.8.1", + "cron-parser": "^4.9.0", + "express": "^5.1.0", + "http-proxy-middleware": "^3.0.5", + "jose": "^6.1.3", + "jsonc-parser": "^3.3.1", + "luxon": "^3.5.0", + "node-pty": "1.2.0-beta.12", + "openai": "^4.79.0", + "qrcode-terminal": "^0.12.0", + "reflect-metadata": "^0.2.2", + "simple-git": "^3.28.0", + "web-push": "^3.6.7", + "ws": "^8.18.3", + "yaml": "^2.8.1", + }, + "devDependencies": { + "@base-ui/react": "^1.4.0", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-go": "^6.0.1", + "@eslint/js": "^9.33.0", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", - "@octokit/rest": "^22.0.1", - "@opencode-ai/sdk": "^1.3.0", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -251,52 +287,38 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.8", "@remixicon/react": "^4.7.0", - "@types/react-syntax-highlighter": "^15.5.13", - "adm-zip": "^0.5.16", - "bun-pty": "^0.4.5", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "express": "^5.1.0", - "ghostty-web": "0.3.0", - "http-proxy-middleware": "^3.0.5", - "jose": "^6.1.3", - "jsonc-parser": "^3.3.1", - "next-themes": "^0.4.6", - "node-pty": "1.2.0-beta.12", - "openai": "^4.79.0", - "qrcode-terminal": "^0.12.0", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^15.6.6", - "remark-gfm": "^4.0.1", - "simple-git": "^3.28.0", - "sonner": "^2.0.7", - "strip-json-comments": "^5.0.3", - "tailwind-merge": "^3.3.1", - "web-push": "^3.6.7", - "ws": "^8.18.3", - "yaml": "^2.8.1", - "zustand": "^5.0.8", - }, - "devDependencies": { - "@eslint/js": "^9.33.0", + "@simplewebauthn/browser": "13.3.0", "@tailwindcss/postcss": "^4.0.0", "@types/adm-zip": "^0.5.7", "@types/node": "^24.3.1", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", + "@types/react-syntax-highlighter": "^15.5.13", + "@types/supertest": "^7.2.0", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", "concurrently": "^9.2.1", "cors": "^2.8.5", "cross-env": "^7.0.3", "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", + "ghostty-web": "0.3.0", "globals": "^16.3.0", + "next-themes": "^0.4.6", "nodemon": "^3.1.7", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-markdown": "^10.1.0", + "react-syntax-highlighter": "^15.6.6", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.7", + "strip-json-comments": "^5.0.3", + "supertest": "^7.2.2", + "tailwind-merge": "^3.3.1", "tailwindcss": "^4.0.0", "tsx": "^4.20.6", "tw-animate-css": "^1.3.8", @@ -304,7 +326,9 @@ "typescript-eslint": "^8.39.1", "vite": "^7.1.2", "vite-plugin-pwa": "^1.0.3", + "vitest": "^4.1.4", "workbox-window": "^7.4.0", + "zustand": "^5.0.8", }, }, }, @@ -313,6 +337,8 @@ "@codemirror/view": "6.39.13", }, "packages": { + "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.6", "", { "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA=="], @@ -519,7 +545,7 @@ "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], - "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], @@ -527,6 +553,10 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@base-ui/react": ["@base-ui/react@1.4.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.7", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-QcqdVbr/+ba2/RAKJIV1PV6S02Q5+r6a4Eym8ndBw+ZbBILkkmQAyRxXCg/pArrHnkrGeU8goe26aw0h6eE8pg=="], + + "@base-ui/utils": ["@base-ui/utils@0.2.7", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-nXYKhiL/0JafyJE8PfcflipGftOftlIwKd72rU15iZ1M5yqgg5J9P8NHU71GReDuXco5MJA/eVQqUT5WRqX9sA=="], + "@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="], "@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="], @@ -591,6 +621,10 @@ "@codemirror/view": ["@codemirror/view@6.39.13", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw=="], + "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], + + "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], @@ -599,6 +633,24 @@ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], + + "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], + + "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], + + "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"], + + "@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="], + + "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], + + "@electron/rebuild": ["@electron/rebuild@3.7.2", "", { "dependencies": { "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "fs-extra": "^10.0.0", "got": "^11.7.0", "node-abi": "^3.45.0", "node-api-version": "^0.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^6.0.5", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg=="], + + "@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], + + "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], @@ -669,13 +721,13 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], - "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.7", "", {}, "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w=="], @@ -691,6 +743,8 @@ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="], + "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], + "@heroui/react-rsc-utils": ["@heroui/react-rsc-utils@2.1.9", "", { "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-e77OEjNCmQxE9/pnLDDb93qWkX58/CcgIqdNAczT/zUP+a48NxGq2A2WRimvc1uviwaNL2StriE2DmyZPyYW7Q=="], "@heroui/react-utils": ["@heroui/react-utils@2.1.14", "", { "dependencies": { "@heroui/react-rsc-utils": "2.1.9", "@heroui/shared-utils": "2.1.12" }, "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-hhKklYKy9sRH52C9A8P0jWQ79W4MkIvOnKBIuxEMHhigjfracy0o0lMnAUdEsJni4oZKVJYqNGdQl+UVgcmeDA=="], @@ -707,6 +761,8 @@ "@heroui/use-data-scroll-overflow": ["@heroui/use-data-scroll-overflow@2.2.13", "", { "dependencies": { "@heroui/shared-utils": "2.1.12" }, "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-zboLXO1pgYdzMUahDcVt5jf+l1jAQ/D9dFqr7AxWLfn6tn7/EgY0f6xIrgWDgJnM0U3hKxVeY13pAeB4AFTqTw=="], + "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -729,6 +785,8 @@ "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -745,6 +803,8 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], + "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="], "@lezer/cpp": ["@lezer/cpp@1.1.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw=="], @@ -779,14 +839,26 @@ "@lezer/yaml": ["@lezer/yaml@1.0.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.0" } }, "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw=="], + "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], + + "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], + + "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="], + + "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="], + "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], "@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], @@ -813,14 +885,44 @@ "@openchamber/desktop": ["@openchamber/desktop@workspace:packages/desktop"], + "@openchamber/electron": ["@openchamber/electron@workspace:packages/electron"], + "@openchamber/ui": ["@openchamber/ui@workspace:packages/ui"], "@openchamber/web": ["@openchamber/web@workspace:packages/web"], - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.3.0", "", {}, "sha512-5WyYEpcV6Zk9otXOMIrvZRbJm1yxt/c8EXSBn1p6Sw1yagz8HRljkoUTJFxzD0x2+/6vAZItr3OrXDZfE+oA2g=="], + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.14.19", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-9sTGsi8/HlBBeaWfsUjdJ2yi/SqpRvqSld0IFXc3ldaPb1w1uIPvgCGzhlHYQtqatXxSaX5lTN7zpudMaE21aw=="], + + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], + + "@peculiar/asn1-android": ["@peculiar/asn1-android@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ=="], + + "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw=="], + + "@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w=="], + + "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g=="], + + "@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.6.1", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.1", "@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw=="], + + "@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw=="], + + "@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.6.1", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.1", "@peculiar/asn1-pfx": "^2.6.1", "@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw=="], + + "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="], + + "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA=="], + + "@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ=="], + + "@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="], "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -869,8 +971,6 @@ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], - "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], @@ -1025,14 +1125,22 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@simplewebauthn/browser": ["@simplewebauthn/browser@13.3.0", "", {}, "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ=="], + + "@simplewebauthn/server": ["@simplewebauthn/server@13.3.0", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.1", "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/x509": "^1.14.3" } }, "sha512-MLHYFrYG8/wK2i+86XMhiecK72nMaHKKt4bo+7Q1TbuG9iGjlSdfkPWKO5ZFE/BX+ygCJ7pr8H/AJeyAj1EaTQ=="], + + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], - "@streamdown/code": ["@streamdown/code@1.0.3", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-3Ym5TCLcGhrHY2qBaUVWpqNRtxnZvqh4Y5Qm/pTIKA4AmEWwAAoYjZnxG7mOsvOpWVWiDwETjUtchNL1XzQEAw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@surma/rollup-plugin-off-main-thread": ["@surma/rollup-plugin-off-main-thread@2.2.3", "", { "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", "magic-string": "^0.25.0", "string.prototype.matchall": "^4.0.6" } }, "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ=="], "@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="], + "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], @@ -1103,6 +1211,8 @@ "@textlint/types": ["@textlint/types@15.5.2", "", { "dependencies": { "@textlint/ast-node-types": "15.5.2" } }, "sha512-sJOrlVLLXp4/EZtiWKWq9y2fWyZlI8GP+24rnU5avtPWBIMm/1w97yzKrAqYF8czx2MqR391z5akhnfhj2f/AQ=="], + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + "@types/adm-zip": ["@types/adm-zip@0.5.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -1113,22 +1223,40 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/cookiejar": ["@types/cookiejar@2.1.5", "", {}, "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/dom-speech-recognition": ["@types/dom-speech-recognition@0.0.7", "", {}, "sha512-NjiUoJbBlKhyufNsMZLSp+pbPNtPAFnR738RCJvtZy/HVQ2TZjmqpMyaeOSMXgxdfZM60nt8QGbtfmQrJAH2sw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + + "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/methods": ["@types/methods@1.1.4", "", {}, "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/node": ["@types/node@24.10.15", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BgjLoRuSr0MTI5wA6gMw9Xy0sFudAaUuvrnjgGx9wZ522fYYLA5SYJ+1Y30vTcJEG+DRCyDHx/gzQVfofYzSdg=="], @@ -1137,6 +1265,8 @@ "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], + "@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], @@ -1149,14 +1279,24 @@ "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], + "@types/superagent": ["@types/superagent@8.1.9", "", { "dependencies": { "@types/cookiejar": "^2.1.5", "@types/methods": "^1.1.4", "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ=="], + + "@types/supertest": ["@types/supertest@7.2.0", "", { "dependencies": { "@types/methods": "^1.1.4", "@types/superagent": "^8.1.0" } }, "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], + "@types/vscode": ["@types/vscode@1.109.0", "", {}, "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw=="], + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], @@ -1183,6 +1323,20 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], + "@vitest/expect": ["@vitest/expect@4.1.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.5", "", { "dependencies": { "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.5", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g=="], + + "@vitest/runner": ["@vitest/runner@4.1.5", "", { "dependencies": { "@vitest/utils": "4.1.5", "pathe": "^2.0.3" } }, "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ=="], + + "@vitest/spy": ["@vitest/spy@4.1.5", "", {}, "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ=="], + + "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="], + "@vscode/vsce": ["@vscode/vsce@3.7.1", "", { "dependencies": { "@azure/identity": "^4.1.0", "@secretlint/node": "^10.1.2", "@secretlint/secretlint-formatter-sarif": "^10.1.2", "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", "@vscode/vsce-sign": "^2.0.0", "azure-devops-node-api": "^12.5.0", "chalk": "^4.1.2", "cheerio": "^1.0.0-rc.9", "cockatiel": "^3.1.2", "commander": "^12.1.0", "form-data": "^4.0.0", "glob": "^11.0.0", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", "secretlint": "^10.1.2", "semver": "^7.5.2", "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", "yauzl": "^2.3.1", "yazl": "^2.2.2" }, "optionalDependencies": { "keytar": "^7.7.0" }, "bin": { "vsce": "vsce" } }, "sha512-OTm2XdMt2YkpSn2Nx7z2EJtSuhRHsTPYsSK59hr3v8jRArK+2UEoju4Jumn1CmpgoBLGI6ReHLJ/czYltNUW3g=="], "@vscode/vsce-sign": ["@vscode/vsce-sign@2.0.9", "", { "optionalDependencies": { "@vscode/vsce-sign-alpine-arm64": "2.0.6", "@vscode/vsce-sign-alpine-x64": "2.0.6", "@vscode/vsce-sign-darwin-arm64": "2.0.6", "@vscode/vsce-sign-darwin-x64": "2.0.6", "@vscode/vsce-sign-linux-arm": "2.0.6", "@vscode/vsce-sign-linux-arm64": "2.0.6", "@vscode/vsce-sign-linux-x64": "2.0.6", "@vscode/vsce-sign-win32-arm64": "2.0.6", "@vscode/vsce-sign-win32-x64": "2.0.6" } }, "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g=="], @@ -1205,8 +1359,12 @@ "@vscode/vsce-sign-win32-x64": ["@vscode/vsce-sign-win32-x64@2.0.6", "", { "os": "win32", "cpu": "x64" }, "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], + "@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="], + "abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -1221,8 +1379,12 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1231,6 +1393,10 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], + + "app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], @@ -1239,12 +1405,22 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "asn1.js": ["asn1.js@5.4.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "safer-buffer": "^2.1.0" } }, "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA=="], + "asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="], + + "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], @@ -1277,10 +1453,14 @@ "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="], @@ -1289,6 +1469,8 @@ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], @@ -1305,12 +1487,22 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="], + + "builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], + "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], + + "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], + + "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -1325,6 +1517,8 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="], @@ -1341,14 +1535,28 @@ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], @@ -1371,10 +1579,18 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], + "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], + + "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], + + "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], + + "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], @@ -1389,12 +1605,22 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], + "core-js-compat": ["core-js-compat@3.48.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q=="], + "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], + + "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -1413,6 +1639,8 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -1433,6 +1661,10 @@ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + + "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], @@ -1447,24 +1679,42 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], + + "dmg-builder": ["dmg-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg=="], + + "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], "editions": ["editions@6.22.0", "", { "dependencies": { "version-range": "^4.15.0" } }, "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ=="], @@ -1473,14 +1723,36 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + "electron": ["electron@41.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-teeRThiYGTPKf/2yOW7zZA1bhb91KEQ4yLBPOg7GxpmnkLFLugKgQaAKOrCgdzwsXh/5mFIfmkm+4+wACJKwaA=="], + + "electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="], + + "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA=="], + + "electron-context-menu": ["electron-context-menu@4.1.2", "", { "dependencies": { "cli-truncate": "^4.0.0", "electron-dl": "^4.0.0", "electron-is-dev": "^3.0.1" } }, "sha512-9xYTUV0oRqKL50N9W71IrXNdVRB0LuBp3R1zkUdUc2wfIa2/QZwYYj5RLuO7Tn7ZSLVIaO3X6u+EIBK+cBvzrQ=="], + + "electron-dl": ["electron-dl@4.0.0", "", { "dependencies": { "ext-name": "^5.0.0", "pupa": "^3.1.0", "unused-filename": "^4.0.1" } }, "sha512-USiB9816d2JzKv0LiSbreRfTg5lDk3lWh0vlx/gugCO92ZIJkHVH0UM18EHvKeadErP6Xn4yiTphWzYfbA2Ong=="], + + "electron-is-dev": ["electron-is-dev@3.0.1", "", {}, "sha512-8TjjAh8Ec51hUi3o4TaU0mD3GMTOESi866oRNavj9A3IQJ7pmv+MJVmdZBFGw4GFT36X7bkqnuDNYvkQgvyI8Q=="], + + "electron-log": ["electron-log@5.4.3", "", {}, "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ=="], + + "electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], + "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="], + "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], + + "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], + "elkjs": ["elkjs@0.11.0", "", {}, "sha512-u4J8h9mwEDaYMqo0RYJpqNMFDoMK7f+pu4GjcV+N8jIC7TRdORgzkfSjTJemhqONFfH6fBI3wpysgWbhgVWIXw=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], @@ -1489,24 +1761,34 @@ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + "esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -1531,7 +1813,7 @@ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - "estree-walker": ["estree-walker@1.0.1", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -1543,10 +1825,22 @@ "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="], + + "ext-name": ["ext-name@5.0.0", "", { "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" } }, "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + + "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], + "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1557,6 +1851,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -1569,6 +1865,8 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -1597,6 +1895,8 @@ "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], @@ -1609,6 +1909,10 @@ "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1625,6 +1929,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -1633,6 +1939,8 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], @@ -1645,6 +1953,8 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], + "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -1653,6 +1963,8 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -1669,19 +1981,23 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + "hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="], - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - - "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], @@ -1703,6 +2019,8 @@ "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], @@ -1711,12 +2029,16 @@ "http-proxy-middleware": ["http-proxy-middleware@3.0.5", "", { "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", "http-proxy": "^1.18.1", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" } }, "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg=="], + "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], + "http_ece": ["http_ece@1.2.0", "", {}, "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], @@ -1731,8 +2053,14 @@ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], + "infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], @@ -1743,6 +2071,8 @@ "intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="], @@ -1787,6 +2117,10 @@ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], @@ -1821,6 +2155,8 @@ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], @@ -1831,6 +2167,8 @@ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "istextorbinary": ["istextorbinary@9.5.0", "", { "dependencies": { "binaryextensions": "^6.11.0", "editions": "^6.21.0", "textextensions": "^6.11.0" } }, "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw=="], @@ -1859,6 +2197,8 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + "json-with-bigint": ["json-with-bigint@3.5.7", "", {}, "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -1877,12 +2217,16 @@ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], + "keytar": ["keytar@7.9.0", "", { "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="], + "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -1921,10 +2265,14 @@ "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], @@ -1941,22 +2289,32 @@ "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], + "lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="], "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], + "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], "marked": ["marked@17.0.3", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], @@ -1975,6 +2333,8 @@ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], @@ -1997,6 +2357,8 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], @@ -2015,6 +2377,8 @@ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], @@ -2061,6 +2425,8 @@ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], @@ -2069,10 +2435,26 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="], + + "minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="], + + "minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], + "motion": ["motion@12.34.3", "", { "dependencies": { "framer-motion": "^12.34.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-xZIkBGO7v/Uvm+EyaqYd+9IpXu0sZqLywVlGdCFrrMiaO9JI4Kx51mO9KlHSWwll+gZUVY5OJsWgYI5FywJ/tw=="], "motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="], @@ -2089,7 +2471,7 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -2097,10 +2479,14 @@ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], + "node-pty": ["node-pty@1.2.0-beta.12", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-uExTCG/4VmSJa4+TjxFwPXv8BfacmfFEBL6JpxCMDghcwqzvD0yTcGmZ1fKOK6HY33tp0CelLblqTECJizc+Yw=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -2109,10 +2495,14 @@ "nodemon": ["nodemon@3.1.14", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw=="], + "nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="], + "normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -2123,10 +2513,16 @@ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + "on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], @@ -2139,8 +2535,12 @@ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], @@ -2171,6 +2571,8 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -2181,12 +2583,18 @@ "path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], @@ -2197,6 +2605,8 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -2205,6 +2615,16 @@ "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + "proc-log": ["proc-log@2.0.1", "", {}, "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + + "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="], + + "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -2217,6 +2637,12 @@ "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], + + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], "qrcode-terminal": ["qrcode-terminal@0.12.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="], @@ -2225,6 +2651,8 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -2253,12 +2681,16 @@ "read": ["read@1.0.7", "", { "dependencies": { "mute-stream": "~0.0.4" } }, "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ=="], + "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], + "read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "refractor": ["refractor@3.6.0", "", { "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.27.0" } }, "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA=="], @@ -2281,14 +2713,12 @@ "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], - - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - - "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], + "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], @@ -2305,14 +2735,30 @@ "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], + + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -2333,6 +2779,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], + "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -2341,8 +2789,12 @@ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], @@ -2373,7 +2825,9 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], @@ -2389,12 +2843,22 @@ "slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="], - "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "smob": ["smob@1.6.1", "", {}, "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g=="], + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + + "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "sort-keys": ["sort-keys@1.1.2", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg=="], + + "sort-keys-length": ["sort-keys-length@1.0.1", "", { "dependencies": { "sort-keys": "^1.0.0" } }, "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw=="], + "source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -2413,14 +2877,24 @@ "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + + "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "streamdown": ["streamdown@2.3.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.1", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-OqS3by/lt91lSicE8RQP2nTsYI6Q/dQgGP2vcyn9YesCmRHhNjswAuBAZA1z0F4+oBU3II/eV51LqjCqwTb1lw=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], @@ -2437,6 +2911,8 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-comments": ["strip-comments@2.0.1", "", {}, "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw=="], "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], @@ -2449,6 +2925,12 @@ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], + + "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.14.1" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="], + + "supertest": ["supertest@7.2.2", "", { "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", "superagent": "^10.3.0" } }, "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], @@ -2465,12 +2947,18 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], + "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], + "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], + "tempy": ["tempy@0.6.0", "", { "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw=="], "terminal-link": ["terminal-link@4.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^3.2.0" } }, "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA=="], @@ -2481,10 +2969,22 @@ "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], + "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], + + "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -2499,12 +2999,16 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], @@ -2555,12 +3059,20 @@ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + "unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="], + + "unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="], + "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], @@ -2573,6 +3085,8 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "unused-filename": ["unused-filename@4.0.1", "", { "dependencies": { "escape-string-regexp": "^5.0.0", "path-exists": "^5.0.0" } }, "sha512-ZX6U1J04K1FoSUeoX1OicAhw4d0aro2qo+L8RhJkiGTNtBNkd/Fi1Wxoc9HzcVu6HfOzm0si/N15JjxFmD1z6A=="], + "upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -2585,6 +3099,10 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], @@ -2593,6 +3111,8 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], + "version-range": ["version-range@4.15.0", "", {}, "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -2605,8 +3125,12 @@ "vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="], + "vitest": ["vitest@4.1.5", "", { "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", "@vitest/pretty-format": "4.1.5", "@vitest/runner": "4.1.5", "@vitest/snapshot": "4.1.5", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.5", "@vitest/browser-preview": "4.1.5", "@vitest/browser-webdriverio": "4.1.5", "@vitest/coverage-istanbul": "4.1.5", "@vitest/coverage-v8": "4.1.5", "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-push": ["web-push@3.6.7", "", { "dependencies": { "asn1.js": "^5.3.0", "http_ece": "1.2.0", "https-proxy-agent": "^7.0.0", "jws": "^4.0.0", "minimist": "^1.2.5" }, "bin": { "web-push": "src/cli.js" } }, "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A=="], @@ -2633,6 +3157,8 @@ "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "workbox-background-sync": ["workbox-background-sync@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w=="], @@ -2669,6 +3195,8 @@ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], @@ -2719,6 +3247,28 @@ "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + + "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@electron/node-gyp/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], + + "@electron/universal/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + + "@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "@electron/windows-sign/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -2727,6 +3277,14 @@ "@heroui/theme/tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + "@isaacs/fs-minipass/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "@npmcli/agent/socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "@openchamber/ui/ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -2753,6 +3311,8 @@ "@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@1.0.1", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="], + "@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "@rollup/pluginutils/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="], @@ -2779,40 +3339,90 @@ "@textlint/linter-formatter/pluralize": ["pluralize@2.0.0", "", {}, "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw=="], + "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@vscode/vsce/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], + + "app-builder-lib/@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="], + + "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + + "app-builder-lib/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "app-builder-lib/tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], + + "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "cacache/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "cacache/p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "dmg-builder/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "electron-builder/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + + "electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + + "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + "glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "globby/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + "hast-util-from-dom/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "hast-util-from-parse5/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], "hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="], @@ -2823,10 +3433,22 @@ "hastscript/space-separated-tokens": ["space-separated-tokens@1.1.5", "", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="], + "iconv-corefoundation/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], + + "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], + "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], "keytar/node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="], + "make-fetch-happen/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + + "make-fetch-happen/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "make-fetch-happen/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -2835,6 +3457,28 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "node-gyp/make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="], + + "node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], + + "node-gyp/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], + + "node-gyp/tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], + + "node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + "node-sarif-builder/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], "nodemon/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], @@ -2853,10 +3497,18 @@ "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "react-syntax-highlighter/@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "read-pkg/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], @@ -2865,24 +3517,58 @@ "refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="], + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + "source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "stringify-entities/character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "superagent/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "table/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "table/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "temp/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "unused-filename/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + "vite/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "workbox-build/@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "workbox-build/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "workbox-build/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], @@ -2897,6 +3583,14 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@electron/node-gyp/glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@rollup/plugin-node-resolve/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@secretlint/config-loader/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -2905,16 +3599,54 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "app-builder-lib/@electron/rebuild/node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="], + + "app-builder-lib/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "app-builder-lib/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "app-builder-lib/tar/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "app-builder-lib/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "app-builder-lib/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + + "cacache/glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "cli-truncate/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "hast-util-from-dom/hastscript/hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + "hast-util-from-parse5/hastscript/hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], "hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], + + "make-fetch-happen/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "make-fetch-happen/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "mdast-util-mdx-jsx/parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "mdast-util-mdx-jsx/parse-entities/character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], @@ -2927,6 +3659,28 @@ "mdast-util-mdx-jsx/parse-entities/is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "node-gyp/make-fetch-happen/cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="], + + "node-gyp/make-fetch-happen/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], + + "node-gyp/make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], + + "node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], + + "node-gyp/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "node-gyp/tar/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "node-gyp/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "node-gyp/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + "nodemon/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "nodemon/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], @@ -2949,6 +3703,8 @@ "table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "temp/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -3051,20 +3807,70 @@ "workbox-build/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@electron/node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "app-builder-lib/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "mdast-util-mdx-jsx/parse-entities/is-alphanumerical/is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + "node-gyp/make-fetch-happen/cacache/@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], + + "node-gyp/make-fetch-happen/cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + + "node-gyp/make-fetch-happen/cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "node-gyp/make-fetch-happen/cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "node-gyp/make-fetch-happen/cacache/minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + + "node-gyp/make-fetch-happen/cacache/unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="], + + "node-gyp/make-fetch-happen/minipass-fetch/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "nodemon/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "qrcode/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "node-gyp/make-fetch-happen/cacache/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "node-gyp/make-fetch-happen/cacache/unique-filename/unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="], + "qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], } } diff --git a/src/docs/REVERSE_PROXY.md b/src/docs/REVERSE_PROXY.md new file mode 100644 index 0000000..b0c368d --- /dev/null +++ b/src/docs/REVERSE_PROXY.md @@ -0,0 +1,337 @@ +# Reverse Proxy Setup + +Use this guide when running OpenChamber behind Nginx, Nginx Proxy Manager, Caddy, Cloudflare, or another reverse proxy. + +## Before you proxy it + +1. Confirm OpenChamber works directly first. +2. Open `http://:3000` or your custom port from the same network. +3. Only add the reverse proxy after the direct connection works. + +## What the proxy must support + +- WebSockets for live message transport: + - `/api/event/ws` + - `/api/global/event/ws` + - `/api/terminal/ws` +- SSE without buffering: + - `/api/event` + - `/api/global/event` + - `/api/notifications/stream` + - `/api/openchamber/events` + - `/api/terminal/:sessionId/stream` +- Large request bodies for attachments and file operations +- Long-lived read timeouts for live streams and terminal sessions + +## Rules that matter + +- Enable WebSocket proxying. +- Disable buffering on SSE routes. +- Disable gzip on the proxy if OpenChamber is already compressing responses. +- Keep compression enabled in only one layer. +- Forward normal proxy headers such as `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`. +- Increase body size limits if users upload files. + +## Quick checklist + +- OpenChamber reachable directly on LAN +- WebSockets enabled in the proxy +- SSE routes have buffering off +- `gzip off` on the proxy host, or proxy compression disabled another way +- `client_max_body_size` large enough for attachments +- `proxy_read_timeout` long enough for streams + +## Example: Nginx + +
+Show example config + +```nginx +client_max_body_size 50M; +client_body_buffer_size 50M; +proxy_request_buffering off; + +proxy_http_version 1.1; +proxy_set_header Connection ""; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; + +gzip off; + +location = /api/terminal/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location = /api/global/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location = /api/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location ~ ^/api/terminal/.+/stream$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location /api { + proxy_pass http://127.0.0.1:3000; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location / { + proxy_pass http://127.0.0.1:3000; +} +``` + +
+ +## Example: Nginx Proxy Manager + +
+Show Advanced tab example + +```nginx +client_max_body_size 50M; +client_body_buffer_size 50M; +proxy_request_buffering off; + +proxy_http_version 1.1; +proxy_set_header Connection ""; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; + +gzip off; + +location = /api/terminal/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/global/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/event { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/global/event { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/notifications/stream { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/openchamber/events { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location ~ ^/api/terminal/.+/stream$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location /api { + proxy_pass http://127.0.0.1:3000; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location / { + proxy_pass http://127.0.0.1:3000; +} +``` + +
+ +Also enable `Websockets Support` in Nginx Proxy Manager for this host. + +## Common failure signs + +### Page loads, but sending messages fails + +- WebSockets are not enabled in the proxy +- `/api/event/ws` or `/api/global/event/ws` is not passing through correctly + +### Notifications or live status do not update + +- one of the SSE routes is buffered or cached +- `X-Accel-Buffering "no"` is missing + +### File uploads fail + +- `client_max_body_size` is too small + +### Everything works locally, but breaks only behind the proxy + +- the proxy is compressing and buffering live traffic +- the proxy is missing WebSocket support + +## Example: Caddy + +
+Show example config + +```caddy +reverse_proxy 127.0.0.1:3000 { + # WebSocket support is automatic in Caddy + + # Flush SSE responses immediately + flush_interval -1 + + # Pass through Host and proxy headers + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + + # Increase timeouts for long-lived streams + transport http { + read_timeout 3600s + write_timeout 3600s + } +} +``` + +
+ +Caddy handles WebSocket upgrades automatically — no extra configuration needed. The `flush_interval -1` directive ensures SSE chunks are forwarded immediately without buffering. + +## CDN and double-compression warning + +If you place a CDN (such as Cloudflare) in front of your reverse proxy, be aware of double compression: + +- OpenChamber compresses HTTP responses with gzip (threshold 1 KB). +- Cloudflare and other CDNs also compress responses by default. +- This can cause double-compressed responses or incorrect `Content-Encoding` headers. + +To avoid this, disable compression at **one** layer: + +- **Cloudflare:** Rules → Compression → disable (or use "Passthrough" mode). +- **Nginx:** `gzip off` (already shown in the examples above). +- **Caddy:** Caddy does not re-compress by default if the upstream already sends compressed content. + +SSE streaming routes are excluded from compression by OpenChamber, but the CDN may still buffer them. Check your CDN documentation for how to disable buffering on SSE paths. diff --git a/src/docs/TAURI_TO_ELECTRON_CUTOVER.md b/src/docs/TAURI_TO_ELECTRON_CUTOVER.md new file mode 100644 index 0000000..28a21b5 --- /dev/null +++ b/src/docs/TAURI_TO_ELECTRON_CUTOVER.md @@ -0,0 +1,349 @@ +# 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. + +## 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` (minisign) | +| 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. + +## Release workflow changes + +The file to edit: `.github/workflows/release.yml`. + +Today it has 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 + +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 + +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: Install minisign + run: brew install minisign + + - name: Tar and sign Electron .app as Tauri update payload + env: + TAURI_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_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 + + # minisign needs the private key written to a file and a non-interactive + # password via -W (or env). The key in the secret is a minisign secret + # key block (base64-ish multi-line blob). Write to a file verbatim. + echo "$TAURI_KEY" > ../tauri-signing.key + echo "$TAURI_KEY_PASSWORD" | minisign -S -s ../tauri-signing.key \ + -m "$TARBALL" -W + + # Rename per platform so the release has distinct names for arm64/x64. + mv "$TARBALL" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz" + mv "${TARBALL}.minisig" "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 < @@ -114,7 +101,7 @@ - + diff --git a/src/packages/desktop/package.json b/src/packages/desktop/package.json index fa8e39a..8c9cce3 100644 --- a/src/packages/desktop/package.json +++ b/src/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@openchamber/desktop", - "version": "1.9.1", + "version": "1.9.9", "private": true, "type": "module", "desktopPrerequisites": [ diff --git a/src/packages/desktop/src-tauri/Cargo.lock b/src/packages/desktop/src-tauri/Cargo.lock index 97ff092..e6eeed6 100644 --- a/src/packages/desktop/src-tauri/Cargo.lock +++ b/src/packages/desktop/src-tauri/Cargo.lock @@ -14,7 +14,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "version_check", ] @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -92,10 +92,12 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ashpd" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" dependencies = [ + "async-fs", + "async-net", "enumflags2", "futures-channel", "futures-util", @@ -103,7 +105,6 @@ dependencies = [ "raw-window-handle", "serde", "serde_repr", - "tokio", "url", "wayland-backend", "wayland-client", @@ -137,9 +138,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -149,6 +150,17 @@ dependencies = [ "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" @@ -169,15 +181,26 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +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" @@ -204,7 +227,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -239,7 +262,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -289,6 +312,21 @@ 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" @@ -297,9 +335,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] @@ -325,22 +363,13 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2", ] [[package]] @@ -358,25 +387,26 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -402,17 +432,18 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byte-unit" -version = "5.1.6" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" dependencies = [ "rust_decimal", + "schemars 1.2.1", "serde", "utf8-width", ] @@ -441,9 +472,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -453,9 +484,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -466,7 +497,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cairo-sys-rs", "glib", "libc", @@ -487,9 +518,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -514,7 +545,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -524,14 +555,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.46" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -578,9 +609,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -641,11 +672,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation", "core-graphics-types", "foreign-types", @@ -658,7 +689,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation", "libc", ] @@ -723,6 +754,19 @@ dependencies = [ "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" @@ -730,7 +774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -740,14 +784,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -755,34 +799,33 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -796,7 +839,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -809,7 +852,28 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.110", + "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]] @@ -843,22 +907,16 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", + "bitflags 2.11.0", + "block2", "libc", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -869,23 +927,23 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "dlib" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading 0.8.9", + "libloading", ] [[package]] name = "dlopen2" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -895,13 +953,28 @@ dependencies = [ [[package]] name = "dlopen2_derive" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "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]] @@ -921,9 +994,9 @@ dependencies = [ [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -948,14 +1021,14 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "embed-resource" -version = "3.0.6" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "vswhom", "winreg", ] @@ -977,9 +1050,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -999,7 +1072,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1020,9 +1093,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -1096,27 +1169,26 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1128,6 +1200,18 @@ 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" @@ -1146,7 +1230,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1182,25 +1266,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", - "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1209,9 +1292,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1228,32 +1311,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -1262,7 +1345,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1397,9 +1479,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -1417,11 +1499,24 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "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" @@ -1460,7 +1555,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "futures-channel", "futures-core", "futures-executor", @@ -1488,7 +1583,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1567,7 +1662,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1581,9 +1676,18 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +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" @@ -1617,18 +1721,27 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] [[package]] -name = "http" -version = "1.3.1" +name = "html5ever" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +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", - "fnv", "itoa", ] @@ -1701,14 +1814,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.18" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1725,9 +1837,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1749,9 +1861,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", "png", @@ -1805,9 +1917,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1819,9 +1931,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1838,6 +1950,12 @@ dependencies = [ "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" @@ -1878,12 +1996,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -1899,15 +2017,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -1934,9 +2052,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "javascriptcore-rs" @@ -1970,7 +2088,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -1979,16 +2097,40 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +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.82" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2021,7 +2163,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "serde", "unicode-segmentation", ] @@ -2032,17 +2174,17 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", - "indexmap 2.12.0", - "selectors", + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.13.0", + "selectors 0.24.0", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libappindicator" @@ -2064,15 +2206,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading 0.7.4", + "libloading", "once_cell", ] [[package]] name = "libc" -version = "0.2.177" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -2084,32 +2226,23 @@ dependencies = [ "winapi", ] -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall", + "plain", + "redox_syscall 0.7.3", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2128,9 +2261,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ "value-bag", ] @@ -2149,13 +2282,13 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mac-notification-sys" -version = "0.6.9" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" dependencies = [ "cc", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", "time", ] @@ -2168,9 +2301,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "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]] @@ -2181,7 +2325,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -2192,9 +2336,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -2213,9 +2357,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minisign-verify" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" [[package]] name = "miniz_oxide" @@ -2229,9 +2373,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -2248,14 +2392,14 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.60.2", ] @@ -2265,8 +2409,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", - "jni-sys", + "bitflags 2.11.0", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -2286,7 +2430,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -2295,19 +2439,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nodrop" version = "0.1.14" @@ -2316,9 +2447,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "notify-rust" -version = "4.11.7" +version = "4.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" dependencies = [ "futures-lite", "log", @@ -2330,9 +2461,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-traits" @@ -2345,9 +2476,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2355,14 +2486,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -2374,27 +2505,11 @@ dependencies = [ "libc", ] -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - [[package]] name = "objc2" -version = "0.5.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -2406,41 +2521,11 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", - "libc", - "objc2 0.6.3", - "objc2-cloud-kit", - "objc2-core-data", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", - "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2-foundation", ] [[package]] @@ -2449,9 +2534,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -2460,48 +2545,13 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", "objc2-io-surface", ] -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2 0.6.3", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -2517,28 +2567,16 @@ dependencies = [ "cc", ] -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "libc", - "objc2 0.5.2", -] - [[package]] name = "objc2-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", + "bitflags 2.11.0", + "block2", "libc", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] @@ -2548,8 +2586,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", ] @@ -2559,45 +2597,20 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" dependencies = [ - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - [[package]] name = "objc2-osa-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-foundation", ] [[package]] @@ -2606,9 +2619,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", ] [[package]] @@ -2617,8 +2631,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", ] @@ -2628,10 +2642,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", ] [[package]] @@ -2640,21 +2654,21 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", - "objc2 0.6.3", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-javascript-core", "objc2-security", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" @@ -2670,12 +2684,15 @@ dependencies = [ [[package]] name = "openchamber-desktop" -version = "1.9.1" +version = "1.9.9" dependencies = [ "anyhow", "base64 0.22.1", "log", - "reqwest", + "objc2", + "objc2-web-kit", + "reqwest 0.12.28", + "rfd 0.15.4", "serde", "serde_json", "tauri", @@ -2687,9 +2704,14 @@ dependencies = [ "tauri-plugin-updater", "tokio", "url", - "window-vibrancy 0.7.1", ] +[[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" @@ -2722,12 +2744,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2779,7 +2801,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2826,6 +2848,17 @@ dependencies = [ "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" @@ -2846,6 +2879,16 @@ dependencies = [ "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" @@ -2876,6 +2919,16 @@ dependencies = [ "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" @@ -2900,7 +2953,20 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.110", + "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]] @@ -2927,14 +2993,23 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.1", + "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.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2944,9 +3019,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -2959,6 +3034,12 @@ 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" @@ -2966,7 +3047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.13.0", "quick-xml 0.38.4", "serde", "time", @@ -2999,6 +3080,12 @@ dependencies = [ "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" @@ -3029,6 +3116,16 @@ 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" @@ -3051,11 +3148,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.7", + "toml_edit 0.25.8+spec-1.1.0", ] [[package]] @@ -3090,9 +3187,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3135,6 +3232,15 @@ 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" @@ -3149,7 +3255,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -3157,9 +3263,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -3170,7 +3276,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -3192,9 +3298,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3205,6 +3311,12 @@ 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" @@ -3243,7 +3355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3273,7 +3385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3291,14 +3403,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -3333,7 +3445,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", +] + +[[package]] +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]] @@ -3342,9 +3463,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3364,14 +3485,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3381,9 +3502,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3392,9 +3513,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" @@ -3407,15 +3528,13 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", - "futures-channel", "futures-core", - "futures-util", "http", "http-body", "http-body-util", @@ -3435,6 +3554,44 @@ dependencies = [ "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", @@ -3444,7 +3601,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", ] [[package]] @@ -3454,22 +3610,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ "ashpd", - "block2 0.6.2", + "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 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3480,7 +3659,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3488,9 +3667,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" dependencies = [ "bitvec", "bytecheck", @@ -3506,9 +3685,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" dependencies = [ "proc-macro2", "quote", @@ -3517,9 +3696,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.39.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" dependencies = [ "arrayvec", "borsh", @@ -3529,13 +3708,14 @@ dependencies = [ "rkyv", "serde", "serde_json", + "wasm-bindgen", ] [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -3548,11 +3728,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -3561,9 +3741,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -3574,20 +3754,59 @@ dependencies = [ ] [[package]] -name = "rustls-pki-types" -version = "1.13.0" +name = "rustls-native-certs" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +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-webpki" -version = "0.103.8" +name = "rustls-platform-verifier" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +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", @@ -3602,9 +3821,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3615,6 +3834,15 @@ 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" @@ -3644,9 +3872,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3663,7 +3891,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3684,6 +3912,29 @@ 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" @@ -3691,14 +3942,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", - "derive_more", + "cssparser 0.29.6", + "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "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", ] @@ -3751,7 +4021,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3762,20 +4032,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3786,7 +4056,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3800,9 +4070,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" dependencies = [ "serde_core", ] @@ -3821,17 +4091,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -3840,14 +4110,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3869,7 +4139,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3882,6 +4152,15 @@ dependencies = [ "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" @@ -3933,18 +4212,19 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simdutf8" @@ -3960,15 +4240,15 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3978,34 +4258,34 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "softbuffer" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types", "js-sys", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", + "tracing", "wasm-bindgen", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4040,12 +4320,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "string_cache" version = "0.8.9" @@ -4059,6 +4333,18 @@ dependencies = [ "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" @@ -4071,6 +4357,18 @@ dependencies = [ "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" @@ -4107,9 +4405,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4133,7 +4431,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -4151,35 +4449,33 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.34.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", + "bitflags 2.11.0", + "block2", "core-foundation", "core-graphics", "crossbeam-channel", - "dispatch", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", "ndk-context", "ndk-sys", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "parking_lot", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -4197,7 +4493,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -4208,9 +4504,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -4225,9 +4521,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.4" +version = "2.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15524fc7959bfcaa051ba6d0b3fb1ef18e978de2176c7c6acb977f7fd14d35c7" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" dependencies = [ "anyhow", "bytes", @@ -4245,15 +4541,15 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4264,21 +4560,21 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tray-icon", "url", "webkit2gtk", "webview2-com", - "window-vibrancy 0.6.0", + "window-vibrancy", "windows", ] [[package]] name = "tauri-build" -version = "2.5.3" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" dependencies = [ "anyhow", "cargo_toml", @@ -4292,15 +4588,15 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.2" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" dependencies = [ "base64 0.22.1", "brotli", @@ -4314,9 +4610,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.110", + "syn 2.0.117", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "url", "uuid", @@ -4325,23 +4621,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.2" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.5.1" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" dependencies = [ "anyhow", "glob", @@ -4350,33 +4646,33 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-plugin-dialog" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" dependencies = [ "log", "raw-window-handle", - "rfd", + "rfd 0.16.0", "serde", "serde_json", "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.4.4" +version = "2.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" dependencies = [ "anyhow", "dunce", @@ -4389,30 +4685,30 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.17", - "toml 0.9.8", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", "url", ] [[package]] name = "tauri-plugin-log" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5709c792b8630290b5d9811a1f8fe983dd925fc87c7fc7f4923616458cd00b6" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" dependencies = [ "android_logger", "byte-unit", "fern", "log", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", "serde", "serde_json", "serde_repr", "swift-rs", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -4430,16 +4726,16 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "url", ] [[package]] name = "tauri-plugin-shell" -version = "2.3.3" +version = "2.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" dependencies = [ "encoding_rs", "log", @@ -4452,15 +4748,15 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] [[package]] name = "tauri-plugin-updater" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61" dependencies = [ "base64 0.22.1", "dirs", @@ -4472,7 +4768,8 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest", + "reqwest 0.13.2", + "rustls", "semver", "serde", "serde_json", @@ -4480,7 +4777,7 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "url", @@ -4490,23 +4787,23 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.9.2" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" dependencies = [ "cookie", "dpi", "gtk", "http", "jni", - "objc2 0.6.3", + "objc2", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webview2-com", @@ -4515,17 +4812,16 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.9.2" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7950f3bde6bcca6655bc5e76d3d6ec587ceb81032851ab4ddbe1f508bdea2729" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" dependencies = [ "gtk", "http", "jni", "log", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", "once_cell", "percent-encoding", "raw-window-handle", @@ -4542,9 +4838,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" dependencies = [ "anyhow", "brotli", @@ -4552,7 +4848,7 @@ dependencies = [ "ctor", "dunce", "glob", - "html5ever", + "html5ever 0.29.1", "http", "infer", "json-patch", @@ -4570,8 +4866,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.17", - "toml 0.9.8", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4586,7 +4882,7 @@ checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" dependencies = [ "dunce", "embed-resource", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -4596,19 +4892,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml 0.37.5", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "windows-version", ] [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4625,6 +4921,16 @@ dependencies = [ "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" @@ -4636,11 +4942,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4651,25 +4957,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -4677,22 +4983,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4710,9 +5016,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -4725,20 +5031,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", - "signal-hook-registry", "socket2", - "tracing", + "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" @@ -4751,9 +5067,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4776,17 +5092,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.13.0", "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned 1.1.0", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] @@ -4800,9 +5116,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +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", ] @@ -4813,7 +5138,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.13.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -4824,7 +5149,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.13.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -4833,36 +5158,36 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.25.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" dependencies = [ - "indexmap 2.12.0", - "toml_datetime 0.7.3", + "indexmap 2.13.0", + "toml_datetime 1.1.0+spec-1.1.0", "toml_parser", - "winnow 0.7.13", + "winnow 1.0.0", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ - "winnow 0.7.13", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -4875,11 +5200,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -4905,9 +5230,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -4916,43 +5241,43 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "tray-icon" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.60.2", ] @@ -4976,13 +5301,13 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -5028,15 +5353,21 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +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" @@ -5046,16 +5377,23 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +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" @@ -5076,9 +5414,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8-width" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" [[package]] name = "utf8_iter" @@ -5088,21 +5426,21 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] [[package]] name = "value-bag" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" [[package]] name = "version-compare" @@ -5169,18 +5507,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +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.105" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" dependencies = [ "cfg-if", "once_cell", @@ -5191,22 +5538,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5214,31 +5558,53 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" dependencies = [ "unicode-ident", ] [[package]] -name = "wasm-streams" -version = "0.4.2" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +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", @@ -5248,10 +5614,22 @@ dependencies = [ ] [[package]] -name = "wayland-backend" -version = "0.3.11" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +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", @@ -5263,11 +5641,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "rustix", "wayland-backend", "wayland-scanner", @@ -5275,11 +5653,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5287,20 +5665,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml 0.39.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", @@ -5309,9 +5687,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" dependencies = [ "js-sys", "wasm-bindgen", @@ -5328,10 +5706,22 @@ dependencies = [ ] [[package]] -name = "webkit2gtk" -version = "2.0.1" +name = "web_atoms" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +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", @@ -5353,9 +5743,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -5372,19 +5762,28 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "1.0.4" +name = "webpki-root-certs" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +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.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", @@ -5396,22 +5795,22 @@ dependencies = [ [[package]] name = "webview2-com-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "webview2-com-sys" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "windows-core 0.61.2", ] @@ -5453,30 +5852,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "raw-window-handle", "windows-sys 0.59.0", "windows-version", ] -[[package]] -name = "window-vibrancy" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010797bd7c40396fbc59d3105089fed0885fe267a0ef4a0a4646df54e28647f6" -dependencies = [ - "objc2 0.6.3", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "raw-window-handle", - "windows-sys 0.60.2", - "windows-version", -] - [[package]] name = "windows" version = "0.61.3" @@ -5544,7 +5928,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -5555,7 +5939,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -5876,9 +6260,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +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", ] @@ -5895,9 +6288,91 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +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" @@ -5907,30 +6382,29 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.53.5" +version = "0.54.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" dependencies = [ "base64 0.22.1", - "block2 0.6.2", + "block2", "cookie", "crossbeam-channel", "dirs", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http", "javascriptcore-rs", "jni", - "kuchikiki", "libc", "ndk", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -5939,7 +6413,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webkit2gtk-sys", @@ -6009,15 +6483,15 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "synstructure", ] [[package]] name = "zbus" -version = "5.12.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-executor", @@ -6033,16 +6507,16 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix", "serde", "serde_repr", - "tokio", "tracing", "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.13", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -6050,14 +6524,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.12.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -6065,34 +6539,33 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", - "winnow 0.7.13", + "winnow 0.7.15", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -6112,7 +6585,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "synstructure", ] @@ -6152,7 +6625,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -6163,47 +6636,53 @@ checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" dependencies = [ "arbitrary", "crc32fast", - "indexmap 2.12.0", + "indexmap 2.13.0", "memchr", ] [[package]] -name = "zvariant" -version = "5.8.0" +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +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.13", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.8.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.110", - "winnow 0.7.13", + "syn 2.0.117", + "winnow 0.7.15", ] diff --git a/src/packages/desktop/src-tauri/Cargo.toml b/src/packages/desktop/src-tauri/Cargo.toml index 3afbd71..de8e351 100644 --- a/src/packages/desktop/src-tauri/Cargo.toml +++ b/src/packages/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openchamber-desktop" -version = "1.9.1" +version = "1.9.9" edition = "2021" publish = false @@ -16,20 +16,27 @@ devtools = ["tauri/devtools"] anyhow = "1.0.86" base64 = "0.22.1" log = "0.4.28" -reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls", "blocking"] } +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.9.4", features = ["macos-private-api"] } -tauri-plugin-dialog = "2.4.2" -tauri-plugin-log = "2.7.1" -tauri-plugin-shell = "2.3.3" +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" -tokio = { version = "1.38", features = ["rt-multi-thread", "time"] } +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.3", features = [] } +tauri-build = { version = "2.5.6", features = [] } + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = true [target.'cfg(target_os = "macos")'.dependencies] -window-vibrancy = "0.7.1" +objc2 = "0.6" +objc2-web-kit = "0.3" +rfd = "0.15" diff --git a/src/packages/desktop/src-tauri/src/main.rs b/src/packages/desktop/src-tauri/src/main.rs index a5335de..16182c8 100644 --- a/src/packages/desktop/src-tauri/src/main.rs +++ b/src/packages/desktop/src-tauri/src/main.rs @@ -14,20 +14,31 @@ use std::{ path::{Path, PathBuf}, }; use std::{ - net::TcpListener, + net::{TcpListener, UdpSocket}, process::Command, sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{AtomicU32, AtomicU64, Ordering}, Mutex, }, time::Duration, }; -use tauri::utils::config::BackgroundThrottlingPolicy; 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")] -use window_vibrancy::{ - apply_vibrancy, clear_vibrancy, NSVisualEffectMaterial, -}; +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); @@ -104,6 +115,8 @@ 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")] +const MENU_ITEM_QUICK_OPEN_ID: &str = "menu_quick_open"; +#[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"; @@ -141,6 +154,8 @@ const MENU_ITEM_REQUEST_FEATURE_ID: &str = "menu_request_feature"; 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 = @@ -151,6 +166,198 @@ const GITHUB_FEATURE_REQUEST_URL: &str = #[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, @@ -190,6 +397,14 @@ fn build_macos_menu( Some("Cmd+K"), )?; + let quick_open = MenuItem::with_id( + app, + MENU_ITEM_QUICK_OPEN_ID, + "Quick Open…", + true, + Some("Cmd+P"), + )?; + let new_window = MenuItem::with_id( app, MENU_ITEM_NEW_WINDOW_ID, @@ -277,7 +492,7 @@ fn build_macos_menu( MENU_ITEM_TOGGLE_MEMORY_DEBUG_ID, "Toggle Memory Debug", true, - Some("Cmd+Shift+D"), + Some("CmdOrCtrl+Shift+D"), )?; let help_dialog = MenuItem::with_id( @@ -377,13 +592,20 @@ fn build_macos_menu( &PredefinedMenuItem::separator(app)?, &settings, &command_palette, + &quick_open, &PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::services(app, None)?, &PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::hide(app, None)?, &PredefinedMenuItem::hide_others(app, None)?, &PredefinedMenuItem::separator(app)?, - &PredefinedMenuItem::quit(app, None)?, + &MenuItem::with_id( + app, + MENU_ITEM_QUIT_ID, + format!("Quit {}", pkg_info.name), + true, + Some("Cmd+Q"), + )?, ], )?, &Submenu::with_items( @@ -1175,9 +1397,11 @@ fn is_app_bundle_installed(bundle_name: &str) -> bool { const SIDECAR_NAME: &str = "openchamber-server"; const SIDECAR_NOTIFY_PREFIX: &str = "[OpenChamberDesktopNotify] "; const HEALTH_TIMEOUT: Duration = Duration::from_secs(20); -const HEALTH_POLL_INTERVAL: Duration = Duration::from_millis(250); +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_INTERVAL: Duration = Duration::from_millis(100); +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); @@ -1190,20 +1414,46 @@ 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 the initialization script and local origin, shared across all windows. +/// 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 { - script: Mutex>, + /// 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>, - /// Host URLs that were probed unreachable (e.g. at startup). - /// `open_new_window` checks this to avoid opening windows at dead hosts. - unreachable_hosts: Mutex>, } /// Tracks the set of currently-focused window labels. @@ -1254,6 +1504,52 @@ struct DesktopHost { 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)] @@ -1334,13 +1630,14 @@ fn settings_file_path() -> PathBuf { .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 { - 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 + read_desktop_settings_json() .as_ref() .and_then(|v| v.get("desktopLocalPort")) .and_then(|v| v.as_u64()) @@ -1354,6 +1651,7 @@ fn read_desktop_local_port_from_disk() -> Option { } 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)?; @@ -1395,6 +1693,12 @@ fn read_desktop_hosts_config_from_path(path: &Path) -> DesktopHostsConfig { .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 { @@ -1420,6 +1724,7 @@ fn read_desktop_hosts_config_from_path(path: &Path) -> DesktopHostsConfig { DesktopHostsConfig { hosts, default_host_id: default_value, + initial_host_choice_completed, } } @@ -1438,6 +1743,7 @@ fn read_desktop_window_state_from_disk() -> Option { } 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)?; @@ -1458,10 +1764,6 @@ fn write_desktop_window_state_to_disk(state: &DesktopWindowState) -> Result<()> Ok(()) } -fn write_desktop_hosts_config_to_disk(config: &DesktopHostsConfig) -> Result<()> { - write_desktop_hosts_config_to_path(&settings_file_path(), config) -} - fn write_desktop_hosts_config_to_path(path: &Path, config: &DesktopHostsConfig) -> Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; @@ -1503,19 +1805,252 @@ fn write_desktop_hosts_config_to_path(path: &Path, config: &DesktopHostsConfig) 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(config: DesktopHostsConfig) -> Result<(), String> { - write_desktop_hosts_config_to_disk(&config).map_err(|err| err.to_string()) +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)] @@ -1562,9 +2097,129 @@ async fn probe_host_with_timeout(url: &str, timeout: Duration) -> Result 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 { - probe_host_with_timeout(&url, STARTUP_REMOTE_PROBE_SOFT_TIMEOUT).await + let result = probe_with_retry(&url).await; + result + .probe + .ok_or_else(|| "Probe failed".to_string()) } #[derive(Clone, Serialize)] @@ -1742,7 +2397,12 @@ fn maybe_show_sidecar_notification(app: &tauri::AppHandle, payload: SidecarNotif let _ = builder.show(); } -async fn wait_for_health_with(url: &str, timeout: Duration, poll_interval: Duration) -> bool { +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, @@ -1750,6 +2410,7 @@ async fn wait_for_health_with(url: &str, timeout: Duration, poll_interval: Durat 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 { @@ -1757,14 +2418,15 @@ async fn wait_for_health_with(url: &str, timeout: Duration, poll_interval: Durat return true; } } - tokio::time::sleep(poll_interval).await; + 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_INTERVAL).await + wait_for_health_with(url, HEALTH_TIMEOUT, HEALTH_POLL_INITIAL_INTERVAL, HEALTH_POLL_MAX_INTERVAL).await } fn kill_sidecar(app: tauri::AppHandle) { @@ -1774,16 +2436,28 @@ fn kill_sidecar(app: tauri::AppHandle) { let sidecar_url = state.url.lock().expect("sidecar url mutex").clone(); if let Some(url) = sidecar_url { - let shutdown_url = format!("{}/api/system/shutdown", url.trim_end_matches('/')); - if let Ok(client) = reqwest::blocking::Client::builder() - .no_proxy() - .timeout(Duration::from_millis(1500)) - .build() - { - if let Ok(resp) = client.post(shutdown_url).send() { - if resp.status().is_success() { - std::thread::sleep(Duration::from_millis(100)); - } + // 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)); } } } @@ -1875,27 +2549,10 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result { } }); + let desktop_settings = read_desktop_settings_json(); + let opencode_binary_from_settings: Option = (|| { - let data_dir = env::var("OPENCHAMBER_DATA_DIR") - .ok() - .and_then(|v| { - let t = v.trim().to_string(); - if t.is_empty() { - None - } else { - Some(PathBuf::from(t)) - } - }) - .or_else(|| { - resolved_home_dir_path - .as_ref() - .map(|home| home.join(".config").join("openchamber")) - }); - let data_dir = data_dir?; - let settings_path = data_dir.join("settings.json"); - let raw = fs::read_to_string(&settings_path).ok()?; - let json = serde_json::from_str::(&raw).ok()?; - let value = json.get("opencodeBinary")?.as_str()?.trim(); + let value = desktop_settings.as_ref()?.get("opencodeBinary")?.as_str()?.trim(); if value.is_empty() { return None; } @@ -1919,6 +2576,13 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result { 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() { @@ -1995,7 +2659,7 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result { .sidecar(SIDECAR_NAME) .map_err(|err| anyhow!("Failed to resolve sidecar '{SIDECAR_NAME}': {err}"))? .args(["--port", &port.to_string()]) - .env("OPENCHAMBER_HOST", "127.0.0.1") + .env("OPENCHAMBER_HOST", sidecar_bind_host) .env("OPENCHAMBER_DIST_DIR", dist_dir.clone()) .env("OPENCHAMBER_RUNTIME", "desktop") .env("OPENCHAMBER_DESKTOP_NOTIFY", "true") @@ -2068,7 +2732,8 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result { if !wait_for_health_with( &url, LOCAL_SIDECAR_HEALTH_TIMEOUT, - LOCAL_SIDECAR_HEALTH_POLL_INTERVAL, + LOCAL_SIDECAR_HEALTH_POLL_INITIAL_INTERVAL, + LOCAL_SIDECAR_HEALTH_POLL_MAX_INTERVAL, ) .await { @@ -2253,9 +2918,12 @@ fn desktop_new_window(app: tauri::AppHandle) -> Result<(), String> { /// Open a new window pointed at a specific URL (used by the host switcher UI). /// -/// IMPORTANT: Must remain synchronous -- see `desktop_new_window` doc comment. +/// 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] -fn desktop_new_window_at_url(app: tauri::AppHandle, url: String) -> Result<(), String> { +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() { @@ -2274,7 +2942,68 @@ fn desktop_new_window_at_url(app: tauri::AppHandle, url: String) -> Result<(), S }) .ok_or_else(|| "Local origin not yet known (sidecar may still be starting)".to_string())?; - create_window(&app, &url, &local_origin, false).map_err(|e| e.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. @@ -2334,6 +3063,45 @@ fn desktop_read_file(path: String) -> Result { }) } +#[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, @@ -2389,16 +3157,19 @@ fn macos_major_version() -> Option { /// 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) -> String { +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};}}catch(_e){{}}}})();" + "(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. @@ -2426,9 +3197,7 @@ fn parse_theme_override(theme_mode: Option<&str>, theme_variant: Option<&str>) - } fn read_desktop_theme_override() -> Option { - let settings = fs::read_to_string(settings_file_path()) - .ok() - .and_then(|raw| serde_json::from_str::(&raw).ok()); + let settings = read_desktop_settings_json(); let use_system_theme = settings .as_ref() @@ -2452,22 +3221,42 @@ fn read_desktop_theme_override() -> Option { parse_theme_override(theme_mode, theme_variant) } -#[cfg(target_os = "macos")] -fn apply_macos_window_vibrancy(window: &tauri::WebviewWindow) { - let _ = clear_vibrancy(window); +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 let Err(error) = apply_vibrancy( - window, - NSVisualEffectMaterial::Sidebar, - None, - None, - ) { - log::warn!("[desktop:vibrancy] Failed to apply macOS vibrancy: {error}"); + if ip.is_loopback() { + return None; + } + + match ip { + std::net::IpAddr::V4(ipv4) => Some(ipv4.to_string()), + std::net::IpAddr::V6(_) => None, } } -#[cfg(not(target_os = "macos"))] -fn apply_macos_window_vibrancy(_window: &tauri::WebviewWindow) {} +/// 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( @@ -2484,6 +3273,11 @@ fn desktop_set_window_theme( 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; @@ -2602,16 +3396,22 @@ 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); + let init_script = build_init_script(local_origin, boot_outcome); - // Store the init script and local origin so new windows and page reloads can reuse it. + // 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.script.lock().expect("desktop ui injection mutex") = Some(init_script.clone()); + state + .scripts + .lock() + .expect("desktop ui injection mutex") + .insert(label.clone(), init_script.clone()); *state .local_origin .lock() @@ -2630,8 +3430,9 @@ fn create_window( .min_inner_size(MIN_WINDOW_WIDTH as f64, MIN_WINDOW_HEIGHT as f64) .decorations(true) .visible(false) - .initialization_script(&init_script) - .background_throttling(BackgroundThrottlingPolicy::Disabled); + .initialization_script(&init_script); + + builder = apply_platform_window_config(builder); let apply_restored_state = restored_state .as_ref() @@ -2646,21 +3447,9 @@ fn create_window( .position(state.x as f64, state.y as f64); } - #[cfg(target_os = "macos")] - { - builder = builder - .transparent(true) - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .traffic_light_position(tauri::Position::Logical(tauri::LogicalPosition { - x: 17.0, - y: 26.0, - })); - } - let window = builder.build()?; let _ = window.set_theme(read_desktop_theme_override()); - apply_macos_window_vibrancy(&window); + disable_pinch_zoom(&window); if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) { if state.maximized || state.fullscreen { @@ -2693,8 +3482,9 @@ fn create_startup_window(app: &tauri::AppHandle, restore_geometry: bool) -> Resu .min_inner_size(MIN_WINDOW_WIDTH as f64, MIN_WINDOW_HEIGHT as f64) .decorations(true) .visible(true) - .initialization_script(&splash_script) - .background_throttling(BackgroundThrottlingPolicy::Disabled); + .initialization_script(&splash_script); + + builder = apply_platform_window_config(builder); let apply_restored_state = restored_state .as_ref() @@ -2709,21 +3499,9 @@ fn create_startup_window(app: &tauri::AppHandle, restore_geometry: bool) -> Resu .position(state.x as f64, state.y as f64); } - #[cfg(target_os = "macos")] - { - builder = builder - .transparent(true) - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .traffic_light_position(tauri::Position::Logical(tauri::LogicalPosition { - x: 17.0, - y: 26.0, - })); - } - let window = builder.build()?; let _ = window.set_theme(read_desktop_theme_override()); - apply_macos_window_vibrancy(&window); + disable_pinch_zoom(&window); if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) { if state.maximized || state.fullscreen { @@ -2820,12 +3598,21 @@ fn build_startup_splash_script() -> String { ) } -fn activate_main_window(app: &tauri::AppHandle, url: &str, local_origin: &str) -> Result<()> { +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); + let init_script = build_init_script(local_origin, boot_outcome); if let Some(state) = app.try_state::() { - *state.script.lock().expect("desktop ui injection mutex") = Some(init_script); + state + .scripts + .lock() + .expect("desktop ui injection mutex") + .insert("main".to_string(), init_script); *state .local_origin .lock() @@ -2838,7 +3625,7 @@ fn activate_main_window(app: &tauri::AppHandle, url: &str, local_origin: &str) - return Ok(()); } - create_window(app, url, local_origin, true) + create_window(app, url, local_origin, boot_outcome, true) } /// Open a new window pointed at the default host (local or configured default). @@ -2877,7 +3664,7 @@ fn open_new_window(app: &tauri::AppHandle) { return; }; - // Resolve the URL the same way as initial setup: default host or local. + // 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()) @@ -2890,52 +3677,118 @@ fn open_new_window(app: &tauri::AppHandle) { local_url.clone() }; - let mut target_url = local_ui_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(); - if let Some(default_id) = cfg.default_host_id { + + 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 { - target_url = local_ui_url.clone(); - } else if let Some(host) = cfg.hosts.into_iter().find(|h| h.id == default_id) { - target_url = host.url; + 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; } - // If this host was previously probed unreachable (e.g. at startup), fall back to local. - if target_url != local_ui_url { - let is_cached_unreachable = app - .try_state::() - .map(|state| { - state - .unreachable_hosts - .lock() - .expect("unreachable hosts mutex") - .contains(&target_url) - }) - .unwrap_or(false); + // 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; - if is_cached_unreachable { + let final_url = if result.navigable { + target_url + } else { log::info!( - "[desktop] new window: default host ({}) cached as unreachable, using local", + "[desktop] new window: default host ({}) probe returned non-navigable status, using local", target_url ); - target_url = local_ui_url; - } - } + local_ui_url + }; - if let Err(err) = create_window(app, &target_url, &local_origin, false) { - log::error!("[desktop] failed to create new window: {err}"); - } + // 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([ - tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), - tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview), - ]); + .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()) @@ -2951,8 +3804,9 @@ fn main() { .plugin(log_builder.build()) .on_page_load(|window, _payload| { if let Some(state) = window.app_handle().try_state::() { - if let Ok(guard) = state.script.lock() { - if let Some(script) = guard.as_ref() { + let label = window.label().to_string(); + if let Ok(guard) = state.scripts.lock() { + if let Some(script) = guard.get(&label) { let _ = window.eval(script); } } @@ -3031,6 +3885,10 @@ fn main() { dispatch_menu_action(app, "command-palette"); return; } + if id == MENU_ITEM_QUICK_OPEN_ID { + dispatch_menu_action(app, "quick-open"); + return; + } if id == MENU_ITEM_NEW_SESSION_ID { dispatch_menu_action(app, "new-session"); @@ -3104,6 +3962,10 @@ fn main() { }); return; } + if id == MENU_ITEM_QUIT_ID { + request_quit_with_confirmation(app); + return; + } } }) .on_window_event(|window, event| { @@ -3117,19 +3979,16 @@ fn main() { } if let tauri::WindowEvent::Destroyed = event { - // Clean up focus tracking for the destroyed window. if let Some(state) = app.try_state::() { state.remove_window(&label); } - // If this was the last window, kill the sidecar and exit. - let remaining = app.webview_windows().len(); - if remaining == 0 { - if let Some(state) = app.try_state::() { - state.shutdown_all(&app); - } - kill_sidecar(app.clone()); - app.exit(0); + if let Some(state) = app.try_state::() { + state + .scripts + .lock() + .expect("desktop ui injection mutex") + .remove(&label); } } @@ -3137,8 +3996,19 @@ fn main() { schedule_window_state_persist(window.clone(), false); } - if let tauri::WindowEvent::CloseRequested { .. } = event { + 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![ @@ -3154,10 +4024,12 @@ fn main() { 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, @@ -3176,6 +4048,28 @@ fn main() { } 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 { @@ -3184,7 +4078,7 @@ fn main() { match spawn_local_server(&handle).await { Ok(local) => local, Err(err) => { - log::error!("[desktop] failed to start local server: {err}"); + inject_startup_failure(err.to_string()); return; } } @@ -3193,7 +4087,7 @@ fn main() { match spawn_local_server(&handle).await { Ok(local) => local, Err(err) => { - log::error!("[desktop] failed to start local server: {err}"); + inject_startup_failure(err.to_string()); return; } } @@ -3216,6 +4110,7 @@ fn main() { 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() @@ -3223,66 +4118,88 @@ fn main() { .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)); + .and_then(|raw| normalize_server_url(&raw)) + .filter(|url| !same_server_url(url, &local_ui_url)); - let mut initial_url = env_target.unwrap_or_else(|| local_ui_url.clone()); + let mut initial_url = env_target.as_deref().unwrap_or(&local_ui_url).to_string(); - if initial_url == local_ui_url { - let cfg = read_desktop_hosts_config_from_disk(); - if let Some(default_id) = cfg.default_host_id { - if default_id == LOCAL_HOST_ID { - initial_url = local_ui_url.clone(); - } else if let Some(host) = cfg.hosts.into_iter().find(|h| h.id == default_id) { - initial_url = host.url; + // 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 initial_url != local_ui_url { - let failed_url = initial_url.clone(); - let soft_probe = - probe_host_with_timeout(&initial_url, STARTUP_REMOTE_PROBE_SOFT_TIMEOUT).await; + // 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; - let remote_reachable = match soft_probe { - Ok(probe) if probe.status != "unreachable" => true, - Ok(_) | Err(_) => { - log::warn!( - "[desktop] startup host slow/unreachable ({}), retrying with extended timeout", - initial_url - ); - - match probe_host_with_timeout( - &initial_url, - STARTUP_REMOTE_PROBE_HARD_TIMEOUT, - ) - .await - { - Ok(probe) if probe.status != "unreachable" => true, - Ok(_) | Err(_) => false, - } - } - }; - - if !remote_reachable { + 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(); - if let Some(state) = handle.try_state::() { - state - .unreachable_hosts - .lock() - .expect("unreachable hosts mutex") - .insert(failed_url); - } } - } - if let Err(err) = activate_main_window(&handle, &initial_url, &local_origin) { + 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}"); } }); @@ -3295,10 +4212,18 @@ fn main() { .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 { .. } => { - // Best-effort cleanup; never block shutdown. + 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); } @@ -3315,9 +4240,18 @@ fn main() { has_visible_windows, .. } => { - // macOS: clicking dock icon when no windows are open opens a new one. if !has_visible_windows { - open_new_window(app_handle); + 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); + } } } _ => {} @@ -3328,6 +4262,10 @@ fn main() { #[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 { @@ -3365,6 +4303,7 @@ mod tests { 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"); @@ -3378,4 +4317,412 @@ mod tests { ); 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/src/packages/desktop/src-tauri/src/remote_ssh.rs b/src/packages/desktop/src-tauri/src/remote_ssh.rs index 141519a..af37c8d 100644 --- a/src/packages/desktop/src-tauri/src/remote_ssh.rs +++ b/src/packages/desktop/src-tauri/src/remote_ssh.rs @@ -22,6 +22,12 @@ 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 { @@ -1027,6 +1033,7 @@ fn wait_for_master_ready( 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(), @@ -1056,7 +1063,8 @@ fn wait_for_master_ready( return Err(anyhow!(stderr.trim().to_string())); } - std::thread::sleep(Duration::from_millis(250)); + std::thread::sleep(Duration::from_millis(poll_ms)); + poll_ms = (poll_ms * 2).min(2000); } Err(anyhow!("SSH ControlMaster connection timed out")) @@ -1552,19 +1560,34 @@ fn is_local_tunnel_reachable(local_port: u16) -> bool { } fn wait_local_forward_ready(local_port: u16) -> Result<()> { - let client = reqwest::blocking::Client::builder() - .timeout(Duration::from_millis(1000)) - .no_proxy() - .build()?; let deadline = std::time::Instant::now() + Duration::from_secs(DEFAULT_READY_TIMEOUT_SEC); - let target = format!("http://127.0.0.1:{local_port}/health"); + 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(response) = client.get(&target).send() { - if response.status().is_success() || response.status().as_u16() == 401 { - return Ok(()); + 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(250)); + std::thread::sleep(Duration::from_millis(poll_ms)); + poll_ms = (poll_ms * 2).min(2000); } Err(anyhow!( "Timed out waiting for forwarded OpenChamber health" @@ -2353,8 +2376,14 @@ impl DesktopSshManagerInner { 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 { - tokio::time::sleep(Duration::from_secs(2)).await; + 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; @@ -2424,18 +2453,21 @@ impl DesktopSshManagerInner { ); } } else if session.master_detached { - if !is_control_master_alive(&session.parsed, &session.control_path) { - if is_local_tunnel_reachable(session.local_port) { - if detached_notice.is_none() { - detached_notice = Some( - "SSH ControlMaster check failed but local tunnel is still reachable" - .to_string(), - ); - } - } else { - dropped_reason = - Some("SSH ControlMaster is not reachable".to_string()); - } + // 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() @@ -2471,6 +2503,7 @@ impl DesktopSshManagerInner { } if dropped_reason.is_none() { + healthy_ticks = healthy_ticks.saturating_add(1); continue; } diff --git a/src/packages/desktop/src-tauri/tauri.conf.json b/src/packages/desktop/src-tauri/tauri.conf.json index 853ba53..2a57718 100644 --- a/src/packages/desktop/src-tauri/tauri.conf.json +++ b/src/packages/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/schema.json", "productName": "OpenChamber", - "version": "1.9.1", + "version": "1.9.9", "identifier": "ai.opencode.openchamber", "build": { "beforeDevCommand": "node ./scripts/dev-web-server.mjs", @@ -15,7 +15,6 @@ "label": "main", "create": false, "title": "OpenChamber", - "transparent": true, "width": 1280, "height": 800, "resizable": true, @@ -28,8 +27,7 @@ "y": 26 }, "dragDropEnabled": false, - "visible": false, - "backgroundThrottling": "disabled" + "visible": false } ], "security": { diff --git a/src/packages/docs/content/docs/reverse-proxy.mdx b/src/packages/docs/content/docs/reverse-proxy.mdx new file mode 100644 index 0000000..4085690 --- /dev/null +++ b/src/packages/docs/content/docs/reverse-proxy.mdx @@ -0,0 +1,347 @@ +--- +title: Reverse Proxy +description: Configure OpenChamber correctly behind Nginx, Nginx Proxy Manager, or another reverse proxy. +--- + +# Reverse Proxy + +Use this page if you run OpenChamber behind Nginx, Nginx Proxy Manager, Caddy, Cloudflare, or another reverse proxy. + +## Before you proxy it + +1. Confirm OpenChamber works directly first. +2. Open `http://:3000` or your custom port from the same network. +3. Only add the reverse proxy after the direct connection works. + +## What the proxy must support + +- WebSockets for live message transport: + - `/api/event/ws` + - `/api/global/event/ws` + - `/api/terminal/ws` +- SSE without buffering: + - `/api/event` + - `/api/global/event` + - `/api/notifications/stream` + - `/api/openchamber/events` + - `/api/terminal/:sessionId/stream` +- Large request bodies for attachments and file operations +- Long-lived read timeouts for live streams and terminal sessions + +## Rules that matter + +- Enable WebSocket proxying. +- Disable buffering on SSE routes. +- Disable gzip on the proxy if OpenChamber is already compressing responses. +- Keep compression enabled in only one layer. +- Forward normal proxy headers such as `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`. +- Increase body size limits if users upload files. + +## Quick checklist + +- OpenChamber reachable directly on LAN +- WebSockets enabled in the proxy +- SSE routes have buffering off +- `gzip off` on the proxy host, or proxy compression disabled another way +- `client_max_body_size` large enough for attachments +- `proxy_read_timeout` long enough for streams + +## Example: Nginx + +
+Show example config + +```nginx +client_max_body_size 50M; +client_body_buffer_size 50M; +proxy_request_buffering off; + +proxy_http_version 1.1; +proxy_set_header Connection ""; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; + +gzip off; + +location = /api/terminal/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location = /api/global/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location = /api/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location ~ ^/api/terminal/.+/stream$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location /api { + proxy_pass http://127.0.0.1:3000; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location / { + proxy_pass http://127.0.0.1:3000; +} +``` + +
+ +## Example: Nginx Proxy Manager + +
+Show Advanced tab example + +```nginx +client_max_body_size 50M; +client_body_buffer_size 50M; +proxy_request_buffering off; + +proxy_http_version 1.1; +proxy_set_header Connection ""; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; + +gzip off; + +location = /api/terminal/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/global/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/event { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/global/event { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/notifications/stream { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/openchamber/events { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location ~ ^/api/terminal/.+/stream$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location /api { + proxy_pass http://127.0.0.1:3000; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location / { + proxy_pass http://127.0.0.1:3000; +} +``` + +
+ +Also enable `Websockets Support` in Nginx Proxy Manager for this host. + +## Common failure signs + +### Page loads, but sending messages fails + +- WebSockets are not enabled in the proxy +- `/api/event/ws` or `/api/global/event/ws` is not passing through correctly + +### Notifications or live status do not update + +- one of the SSE routes is buffered or cached +- `X-Accel-Buffering "no"` is missing + +### File uploads fail + +- `client_max_body_size` is too small + +### Everything works locally, but breaks only behind the proxy + +- the proxy is compressing and buffering live traffic +- the proxy is missing WebSocket support + +## Example: Caddy + +
+Show example config + +```caddy +reverse_proxy 127.0.0.1:3000 { + # WebSocket support is automatic in Caddy + + # Flush SSE responses immediately + flush_interval -1 + + # Pass through Host and proxy headers + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + + # Increase timeouts for long-lived streams + transport http { + read_timeout 3600s + write_timeout 3600s + } +} +``` + +
+ +Caddy handles WebSocket upgrades automatically — no extra configuration needed. The `flush_interval -1` directive ensures SSE chunks are forwarded immediately without buffering. + +## CDN and double-compression warning + +If you place a CDN (such as Cloudflare) in front of your reverse proxy, be aware of double compression: + +- OpenChamber compresses HTTP responses with gzip (threshold 1 KB). +- Cloudflare and other CDNs also compress responses by default. +- This can cause double-compressed responses or incorrect `Content-Encoding` headers. + +To avoid this, disable compression at **one** layer: + +- **Cloudflare:** Rules → Compression → disable (or use "Passthrough" mode). +- **Nginx:** `gzip off` (already shown in the examples above). +- **Caddy:** Caddy does not re-compress by default if the upstream already sends compressed content. + +SSE streaming routes are excluded from compression by OpenChamber, but the CDN may still buffer them. Check your CDN documentation for how to disable buffering on SSE paths. + +## Related + +- [Tunnels](/tunnels/) +- [Troubleshooting](/troubleshooting/) diff --git a/src/packages/docs/sidebar.config.json b/src/packages/docs/sidebar.config.json index 2e3d8c4..1f1c4ae 100644 --- a/src/packages/docs/sidebar.config.json +++ b/src/packages/docs/sidebar.config.json @@ -18,6 +18,7 @@ { "label": "Help", "items": [ + { "label": "Reverse Proxy", "link": "/reverse-proxy/" }, { "label": "Troubleshooting", "link": "/troubleshooting/" } ] } diff --git a/src/packages/electron/.gitignore b/src/packages/electron/.gitignore new file mode 100644 index 0000000..2f8dd8d --- /dev/null +++ b/src/packages/electron/.gitignore @@ -0,0 +1,13 @@ +# Dependencies +node_modules/ + +# Electron build output +dist/ +dist-bundle/ + +# Generated packaging resources +resources/web-dist/ +resources/sidecar/ + +# OS-specific +.DS_Store diff --git a/src/packages/electron/main.mjs b/src/packages/electron/main.mjs new file mode 100644 index 0000000..d2b85e0 --- /dev/null +++ b/src/packages/electron/main.mjs @@ -0,0 +1,2372 @@ +import { app, BrowserWindow, dialog, ipcMain, Menu, nativeTheme, Notification, session, shell } from 'electron'; +import contextMenu from 'electron-context-menu'; +import log from 'electron-log/main.js'; +import dgram from 'node:dgram'; +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { execFile, spawn, spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; +import updaterPkg from 'electron-updater'; +import { ElectronSshManager } from './ssh-manager.mjs'; + +const execFileAsync = promisify(execFile); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const isDev = process.env.OPENCHAMBER_ELECTRON_DEV === '1' || !app.isPackaged; + +const DEEP_LINK_PROTOCOL = 'openchamber'; +const APP_USER_MODEL_ID = 'dev.openchamber.desktop'; + +if (!app.requestSingleInstanceLock()) { + app.exit(0); + process.exit(0); +} + +// Set the product name early so electron-log derives its log directory as +// ~/Library/Logs/OpenChamber/ (not ~/Library/Logs/@openchamber/electron/). +app.setName('OpenChamber'); +app.setAppUserModelId(APP_USER_MODEL_ID); +app.commandLine.appendSwitch('proxy-bypass-list', '<-loopback>'); + +try { + process.chdir(os.homedir()); +} catch { +} + +log.initialize(); +log.transports.file.maxSize = 5 * 1024 * 1024; +log.transports.file.level = 'info'; +log.transports.console.level = isDev ? 'debug' : 'warn'; + +// The in-process web server runs in this same Node process and uses plain +// `console.log/warn/error`. Without piping console through electron-log, +// that output never lands in ~/Library/Logs/OpenChamber/main.log and we +// can't diagnose issues (e.g. OpenCode lifecycle, SSE disconnects) after +// the fact. Route all console calls through electron-log so server-side +// diagnostics are persisted. +Object.assign(console, log.functions); + +const LOG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; +try { + const logPath = log.transports.file.getFile().path; + const logDir = path.dirname(logPath); + const cutoff = Date.now() - LOG_MAX_AGE_MS; + for (const entry of fs.readdirSync(logDir)) { + const candidate = path.join(logDir, entry); + try { + const info = fs.statSync(candidate); + if (info.isFile() && info.mtimeMs < cutoff) { + fs.unlinkSync(candidate); + } + } catch { + } + } +} catch { +} + +try { + if (!app.isDefaultProtocolClient(DEEP_LINK_PROTOCOL)) { + app.setAsDefaultProtocolClient(DEEP_LINK_PROTOCOL); + } +} catch (error) { + // log.* not yet initialized at this point; fall back to console. + console.warn('[electron] failed to register deep-link protocol:', error); +} + +const readAppMetadata = () => { + const candidates = [ + path.join(__dirname, 'package.json'), + path.join(__dirname, '..', 'package.json'), + path.join(app.getAppPath?.() || '', 'package.json'), + ].filter(Boolean); + for (const candidate of candidates) { + try { + const raw = fs.readFileSync(candidate, 'utf8'); + const parsed = JSON.parse(raw); + if (parsed?.name === '@openchamber/electron' && typeof parsed.version === 'string') { + return { name: parsed.name, version: parsed.version }; + } + } catch { + } + } + return { name: '@openchamber/electron', version: app.getVersion() }; +}; + +const APP_METADATA = readAppMetadata(); +const APP_VERSION = APP_METADATA.version; + +const DEFAULT_DESKTOP_PORT = 57123; +const MIN_WINDOW_WIDTH = 800; +const MIN_WINDOW_HEIGHT = 520; +const MIN_RESTORE_WINDOW_WIDTH = 900; +const MIN_RESTORE_WINDOW_HEIGHT = 560; +const LOCAL_HOST_ID = 'local'; +const ENV_OVERRIDE_HOST_ID = '__env'; +const CHANGELOG_URL = 'https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md'; +const UPDATE_METADATA_URL = 'https://github.com/btriapitsyn/openchamber/releases/latest/download/latest.json'; +const GITHUB_BUG_REPORT_URL = 'https://github.com/btriapitsyn/openchamber/issues/new?template=bug_report.yml'; +const GITHUB_FEATURE_REQUEST_URL = 'https://github.com/btriapitsyn/openchamber/issues/new?template=feature_request.yml'; +const DISCORD_INVITE_URL = 'https://discord.gg/ZYRSdnwwKA'; +const INSTALLED_APPS_CACHE_TTL_SECS = 60 * 60 * 24; +const INSTALLED_APPS_CACHE_FILE = 'discovered-apps.json'; + +const { autoUpdater } = updaterPkg; + +const state = { + serverHandle: null, + sidecarUrl: null, + localOrigin: null, + bootOutcome: null, + initScript: null, + mainWindow: null, + quitRequested: false, + quitConfirmed: false, + quitConfirmationPending: false, + installingUpdate: false, + quitRiskPollerStarted: false, + pendingUpdate: null, + unreachableHosts: new Set(), + windowCounter: 1, + focusedWindowIds: new Set(), + windowGeometryRevisions: new Map(), + sshStatuses: new Map(), + sshLogs: new Map(), +}; + +const QUIT_RISK_POLL_INTERVAL_MS = 5_000; +const quitRisk = { + hasActiveTunnel: false, + hasRunningScheduledTasks: false, + hasEnabledScheduledTasks: false, + runningScheduledTasksCount: 0, + enabledScheduledTasksCount: 0, +}; + +const shouldRequireQuitConfirmation = () => + quitRisk.hasActiveTunnel + || quitRisk.hasRunningScheduledTasks + || quitRisk.hasEnabledScheduledTasks; + +const quitConfirmationMessage = () => { + const reasons = []; + if (quitRisk.hasActiveTunnel) { + reasons.push('an active tunnel'); + } + if (quitRisk.runningScheduledTasksCount > 0) { + reasons.push(`${quitRisk.runningScheduledTasksCount} running scheduled task${quitRisk.runningScheduledTasksCount === 1 ? '' : 's'}`); + } + if (quitRisk.enabledScheduledTasksCount > 0) { + reasons.push(`${quitRisk.enabledScheduledTasksCount} enabled scheduled task${quitRisk.enabledScheduledTasksCount === 1 ? '' : 's'}`); + } + if (reasons.length === 0) { + return 'Background processes (sidecar, SSH sessions) will be stopped.'; + } + return `OpenChamber detected ${reasons.join(', ')}. Quitting now will stop sidecar/background processes and may interrupt pending work.`; +}; + +const prepareForQuit = ({ installingUpdate = false } = {}) => { + state.quitRequested = true; + state.quitConfirmed = true; + state.installingUpdate = installingUpdate; + state.quitConfirmationPending = false; + + if (state.mainWindow && !state.mainWindow.isDestroyed()) { + try { + debounceWindowStatePersist(state.mainWindow, true); + } catch { + } + } + + if (!installingUpdate) { + try { + killSidecar(); + } catch { + } + void sshManager.shutdownAll().catch(() => {}); + } +}; + +const performConfirmedQuit = () => { + if (state.quitConfirmed) return; + prepareForQuit(); + + // Safety net: force-exit if normal quit sequence stalls (e.g. background + // handles in electron-updater / fetch refs) after a short grace period. + const safety = setTimeout(() => { + app.exit(0); + }, 1500); + if (typeof safety?.unref === 'function') safety.unref(); + + app.quit(); +}; + +const requestQuitWithConfirmation = async () => { + if (!shouldRequireQuitConfirmation()) { + performConfirmedQuit(); + return; + } + + if (state.quitConfirmationPending) { + return; + } + state.quitConfirmationPending = true; + + const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed()); + const visible = windows.find((window) => window.isVisible()); + if (!visible) { + const hidden = windows.find((window) => !window.isVisible()); + if (hidden) { + hidden.show(); + hidden.focus(); + } + } + + try { + const result = await dialog.showMessageBox({ + type: 'warning', + title: 'Quit OpenChamber?', + message: 'Quit OpenChamber?', + detail: quitConfirmationMessage(), + buttons: ['Quit', 'Cancel'], + defaultId: 1, + cancelId: 1, + }); + state.quitConfirmationPending = false; + if (result.response === 0) { + performConfirmedQuit(); + } + } catch (error) { + state.quitConfirmationPending = false; + log.warn('[electron] quit confirmation dialog failed:', error); + } +}; + +const refreshQuitRiskFlags = async () => { + const base = typeof state.sidecarUrl === 'string' ? state.sidecarUrl.trim().replace(/\/$/, '') : ''; + if (!base) return; + + const scheduledUrl = `${base}/api/openchamber/scheduled-tasks/status`; + const tunnelUrl = `${base}/api/openchamber/tunnel/status`; + + const fetchJson = async (url) => { + try { + const response = await fetch(url, { signal: AbortSignal.timeout(2_000) }); + if (!response.ok) return null; + return await response.json(); + } catch { + return null; + } + }; + + const [scheduled, tunnel] = await Promise.all([fetchJson(scheduledUrl), fetchJson(tunnelUrl)]); + + if (scheduled && typeof scheduled === 'object') { + const enabledCount = Number(scheduled.enabledScheduledTasksCount ?? 0); + const runningCount = Number(scheduled.runningScheduledTasksCount ?? 0); + quitRisk.enabledScheduledTasksCount = Number.isFinite(enabledCount) ? enabledCount : 0; + quitRisk.runningScheduledTasksCount = Number.isFinite(runningCount) ? runningCount : 0; + quitRisk.hasEnabledScheduledTasks = Boolean(scheduled.hasEnabledScheduledTasks) || quitRisk.enabledScheduledTasksCount > 0; + quitRisk.hasRunningScheduledTasks = Boolean(scheduled.hasRunningScheduledTasks) || quitRisk.runningScheduledTasksCount > 0; + } + + if (tunnel && typeof tunnel === 'object') { + quitRisk.hasActiveTunnel = Boolean(tunnel.active); + } +}; + +const startQuitRiskPoller = () => { + if (process.platform !== 'darwin') return; + if (state.quitRiskPollerStarted) return; + state.quitRiskPollerStarted = true; + + const loop = async () => { + while (!state.quitConfirmed && !state.quitRequested) { + await refreshQuitRiskFlags(); + if (state.quitConfirmed || state.quitRequested) break; + await new Promise((resolve) => { + const timer = setTimeout(resolve, QUIT_RISK_POLL_INTERVAL_MS); + if (typeof timer?.unref === 'function') timer.unref(); + }); + } + }; + void loop(); +}; + +const settingsFilePath = () => { + if (typeof process.env.OPENCHAMBER_DATA_DIR === 'string' && process.env.OPENCHAMBER_DATA_DIR.trim()) { + return path.join(process.env.OPENCHAMBER_DATA_DIR.trim(), 'settings.json'); + } + return path.join(os.homedir(), '.config', 'openchamber', 'settings.json'); +}; + +const sshManager = new ElectronSshManager({ + settingsFilePath: settingsFilePath(), + appVersion: APP_VERSION, + emit: (event, detail) => emitToAllWindows(event, detail), +}); + +const readJsonFile = (filePath) => { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (error) { + if (error && error.code === 'ENOENT') return {}; + // Parse errors can happen if a concurrent writer just truncated the file + // and hasn't finished writing yet. Log loudly so we notice, then return + // {} as before. Writes are atomic (tmp + rename) so this race is rare. + log.warn?.('[electron] failed to read JSON file', filePath, error); + return {}; + } +}; + +const writeJsonFile = async (filePath, data) => { + await fsp.mkdir(path.dirname(filePath), { recursive: true }); + // Atomic: write to a temp file then rename. Readers never see a partial + // JSON file that could parse-error and get coerced to {}. + const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + await fsp.writeFile(tmp, JSON.stringify(data, null, 2)); + await fsp.rename(tmp, filePath); +}; + +const readSettingsRoot = () => { + const root = readJsonFile(settingsFilePath()); + return root && typeof root === 'object' && !Array.isArray(root) ? root : {}; +}; + +// Serializes read-modify-write of the settings file within this process. +// Multiple call sites (spawnLocalServer, writeDesktopHostsConfig, theme +// preference saves, ssh manager imports, etc.) would otherwise have their +// RMW pairs interleave across awaits, letting one writer's stale copy +// overwrite another writer's just-persisted changes. +let settingsMutationChain = Promise.resolve(); +const mutateSettingsRoot = (mutator) => { + const next = settingsMutationChain.then(async () => { + const current = readSettingsRoot(); + const result = await mutator(current); + const nextRoot = result ?? current; + await writeJsonFile(settingsFilePath(), nextRoot); + }); + // Keep the chain alive even if one mutator throws. + settingsMutationChain = next.catch(() => {}); + return next; +}; + +const writeSettingsRoot = async (root) => writeJsonFile(settingsFilePath(), root); + +const normalizeHostUrl = (raw) => { + const trimmed = typeof raw === 'string' ? raw.trim() : ''; + if (!trimmed) return null; + try { + const parsed = new URL(trimmed); + if (!['http:', 'https:'].includes(parsed.protocol)) return null; + parsed.hash = ''; + return parsed.toString(); + } catch { + return null; + } +}; + +const sanitizeHostUrlForStorage = (raw) => normalizeHostUrl(raw); + +const readDesktopHostsConfig = () => { + const root = readSettingsRoot(); + const hostsRaw = Array.isArray(root.desktopHosts) ? root.desktopHosts : []; + const hosts = hostsRaw + .map((entry) => { + const id = typeof entry?.id === 'string' ? entry.id.trim() : ''; + const url = sanitizeHostUrlForStorage(entry?.url); + if (!id || id === LOCAL_HOST_ID || !url) return null; + const label = typeof entry?.label === 'string' && entry.label.trim() ? entry.label.trim() : url; + return { id, label, url }; + }) + .filter(Boolean); + + return { + hosts, + defaultHostId: typeof root.desktopDefaultHostId === 'string' && root.desktopDefaultHostId.trim() + ? root.desktopDefaultHostId.trim() + : null, + initialHostChoiceCompleted: root.desktopInitialHostChoiceCompleted === true, + }; +}; + +const writeDesktopHostsConfig = async (config) => { + await mutateSettingsRoot((root) => { + root.desktopHosts = Array.isArray(config?.hosts) + ? config.hosts + .map((entry) => { + const id = typeof entry?.id === 'string' ? entry.id.trim() : ''; + const url = sanitizeHostUrlForStorage(entry?.url); + if (!id || id === LOCAL_HOST_ID || !url) return null; + return { + id, + label: typeof entry?.label === 'string' && entry.label.trim() ? entry.label.trim() : url, + url, + }; + }) + .filter(Boolean) + : []; + root.desktopDefaultHostId = typeof config?.defaultHostId === 'string' && config.defaultHostId.trim() + ? config.defaultHostId.trim() + : null; + if (typeof config?.initialHostChoiceCompleted === 'boolean') { + root.desktopInitialHostChoiceCompleted = config.initialHostChoiceCompleted; + } + }); +}; + +const readWindowState = () => { + const stateValue = readSettingsRoot().desktopWindowState; + return stateValue && typeof stateValue === 'object' ? stateValue : null; +}; + +const writeWindowState = async (browserWindow) => { + if (!browserWindow || browserWindow.isDestroyed()) return; + if (!state.mainWindow || browserWindow.id !== state.mainWindow.id) return; + + const bounds = browserWindow.getBounds(); + await mutateSettingsRoot((root) => { + root.desktopWindowState = { + x: bounds.x, + y: bounds.y, + width: Math.max(bounds.width, MIN_WINDOW_WIDTH), + height: Math.max(bounds.height, MIN_WINDOW_HEIGHT), + maximized: browserWindow.isMaximized(), + fullscreen: browserWindow.isFullScreen(), + }; + }); +}; + +const debounceWindowStatePersist = (browserWindow, immediate = false) => { + if (!browserWindow || browserWindow.isDestroyed()) return; + const key = String(browserWindow.id); + const revision = (state.windowGeometryRevisions.get(key) || 0) + 1; + state.windowGeometryRevisions.set(key, revision); + + const persist = async () => { + if (state.windowGeometryRevisions.get(key) !== revision) return; + await writeWindowState(browserWindow); + }; + + if (immediate) { + void persist(); + return; + } + + setTimeout(() => { + void persist(); + }, 300); +}; + +const buildHealthUrl = (url) => { + try { + const parsed = new URL(url); + parsed.pathname = `${parsed.pathname.replace(/\/$/, '') || ''}/health`; + return parsed.toString(); + } catch { + return null; + } +}; + +const probeHostWithTimeout = async (url, timeoutMs) => { + const healthUrl = buildHealthUrl(url); + if (!healthUrl) { + throw new Error('Invalid URL'); + } + + const started = Date.now(); + try { + const response = await fetch(healthUrl, { signal: AbortSignal.timeout(timeoutMs) }); + const status = response.status; + return { + status: status >= 200 && status < 300 ? 'ok' : (status === 401 || status === 403 ? 'auth' : 'unreachable'), + latencyMs: Date.now() - started, + }; + } catch { + return { status: 'unreachable', latencyMs: Date.now() - started }; + } +}; + +const waitForHealth = async (url, timeoutMs = 20_000, initialPollMs = 250, maxPollMs = 2000) => { + const deadline = Date.now() + timeoutMs; + let pollMs = initialPollMs; + while (Date.now() < deadline) { + try { + const response = await fetch(buildHealthUrl(url), { signal: AbortSignal.timeout(Math.min(pollMs * 4, 1500)) }); + if (response.ok) { + return true; + } + } catch { + } + await new Promise((resolve) => setTimeout(resolve, pollMs)); + pollMs = Math.min(pollMs * 2, maxPollMs); + } + return false; +}; + +const pickUnusedPort = async () => { + const net = await import('node:net'); + return await new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : 0; + server.close(() => resolve(port)); + }); + server.on('error', reject); + }); +}; + +const isPortFree = async (port) => { + if (!Number.isFinite(port) || port <= 0) return false; + const net = await import('node:net'); + return await new Promise((resolve) => { + const test = net.createServer(); + const done = (value) => { + try { test.close(); } catch {} + resolve(value); + }; + test.once('error', () => done(false)); + test.listen(port, '127.0.0.1', () => done(true)); + }); +}; + +// Return the LAN IPv4 of the interface that routes to the public internet. +// UDP "connect" is a kernel-side route lookup — no packet actually goes out — +// and it picks the same interface as a real outbound connection, which is what +// a phone on the same Wi-Fi needs to reach us. Falls back to scanning +// os.networkInterfaces() if the socket trick fails (e.g. no default route). +const detectLanIPv4Address = async () => { + const ip = await new Promise((resolve) => { + const socket = dgram.createSocket('udp4'); + const finish = (value) => { + try { socket.close(); } catch {} + resolve(value); + }; + socket.once('error', () => finish(null)); + try { + socket.connect(80, '8.8.8.8', (error) => { + if (error) return finish(null); + try { + const addr = socket.address(); + finish(addr && typeof addr.address === 'string' ? addr.address : null); + } catch { + finish(null); + } + }); + } catch { + finish(null); + } + }); + if (ip && ip !== '0.0.0.0' && !ip.startsWith('127.')) return ip; + + for (const entries of Object.values(os.networkInterfaces() || {})) { + for (const entry of entries || []) { + if (entry.family === 'IPv4' && !entry.internal && entry.address) { + return entry.address; + } + } + } + return null; +}; + +const buildLocalUrl = (port) => `http://127.0.0.1:${port}`; + +const resourceRoot = () => isDev ? path.join(__dirname, 'resources') : process.resourcesPath; +const resolveWebDistDir = () => path.join(resourceRoot(), 'web-dist'); + +const normalizeNotificationInput = (raw) => { + if (!raw || typeof raw !== 'object') return {}; + // UI IPC path wraps in { payload: {...} }; sidecar stdout path is flat. + if (raw.payload && typeof raw.payload === 'object') { + return { ...raw, ...raw.payload }; + } + return raw; +}; + +const isAnyWindowFocused = () => + BrowserWindow.getAllWindows().some( + (window) => !window.isDestroyed() && window.isFocused(), + ); + +const focusForegroundWindow = () => { + const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed()); + if (windows.length === 0) return; + const target = state.mainWindow && !state.mainWindow.isDestroyed() + ? state.mainWindow + : windows.find((window) => window.isVisible()) || windows[0]; + // macOS: bring the app to foreground FIRST. When the window is minimized + // to the Dock or hidden via Cmd+H, the app is in the background, and + // subsequent window.show/restore/focus calls won't pull it forward + // unless app.focus runs first. + if (process.platform === 'darwin') app.focus({ steal: true }); + if (target.isMinimized()) target.restore(); + target.show(); + target.focus(); + if (typeof target.moveTop === 'function') target.moveTop(); +}; + +// Keep references to live notifications so they aren't garbage-collected +// before the OS fires click/close. On macOS, losing the JS reference causes +// click events to silently stop firing after ~1 min. +// See https://blog.bloomca.me/2025/02/22/electron-mac-notifications +const activeNotifications = new Set(); + +const maybeShowNativeNotification = (rawInput) => { + const payload = normalizeNotificationInput(rawInput); + const requireHidden = Boolean(payload.requireHidden ?? payload.require_hidden); + + if (requireHidden && isAnyWindowFocused()) { + return; + } + + if (!Notification.isSupported()) { + return; + } + + const title = typeof payload.title === 'string' && payload.title.trim() + ? payload.title.trim() + : 'OpenChamber'; + const body = typeof payload.body === 'string' ? payload.body : ''; + const sessionId = typeof payload.sessionId === 'string' && payload.sessionId.trim() + ? payload.sessionId.trim() + : null; + + const notification = new Notification({ + title, + body, + silent: false, + ...(process.platform === 'darwin' ? { sound: 'Glass' } : {}), + }); + + activeNotifications.add(notification); + const release = () => { activeNotifications.delete(notification); }; + + notification.on('click', () => { + focusForegroundWindow(); + if (sessionId) { + emitToAllWindows('openchamber:open-session', { sessionId }); + } + release(); + }); + notification.on('close', release); + notification.on('failed', release); + + notification.show(); +}; + +const mapUpdaterProgressEvent = (payload) => ({ + event: payload.event, + data: payload.data, +}); + +const SHELL_ENV_TIMEOUT_MS = 5_000; +let cachedShellEnv = null; +let shellEnvProbed = false; + +const isNushell = (shell) => { + const name = path.basename(shell).toLowerCase(); + return name === 'nu' || name === 'nu.exe'; +}; + +const parseShellEnv = (buf) => { + const result = {}; + for (const line of buf.toString('utf8').split('\0')) { + if (!line) continue; + const idx = line.indexOf('='); + if (idx <= 0) continue; + result[line.slice(0, idx)] = line.slice(idx + 1); + } + return result; +}; + +const probeShellEnv = (shell, mode) => { + const result = spawnSync(shell, [mode, '-c', 'env -0'], { + stdio: ['ignore', 'pipe', 'ignore'], + timeout: SHELL_ENV_TIMEOUT_MS, + windowsHide: true, + }); + if (result.error || result.status !== 0) return null; + const env = parseShellEnv(result.stdout); + return Object.keys(env).length > 0 ? env : null; +}; + +// Finder-launched apps on macOS inherit a minimal PATH (no /opt/homebrew, mise, asdf, etc.). +// Probe the user's login shell once so the sidecar sees the same PATH / tool env as `$SHELL -il`. +const loadShellEnv = () => { + if (shellEnvProbed) return cachedShellEnv; + shellEnvProbed = true; + if (process.platform === 'win32') return null; + const shell = process.env.SHELL || '/bin/sh'; + if (isNushell(shell)) return null; + cachedShellEnv = probeShellEnv(shell, '-il') || probeShellEnv(shell, '-l'); + return cachedShellEnv; +}; + +// Merge the user's login-shell env (PATH, etc.) into this process before we +import { pathLooksUserConfigured, mergePathValues } from '@openchamber/web/server/lib/opencode/path-utils.js'; + +// import/start the server in-process. The server and its children (opencode +// CLI, git, etc.) inherit process.env directly now — there is no sidecar +// subprocess to hand a custom env to. +const inheritUserShellEnv = () => { + const shellEnv = loadShellEnv(); + if (!shellEnv) return; + + const homeDir = os.homedir(); + const currentPath = process.env.PATH || ''; + const currentPathLooksUserConfigured = pathLooksUserConfigured(currentPath, homeDir, ':'); + + for (const [key, value] of Object.entries(shellEnv)) { + if (key === 'PATH') continue; + if (typeof process.env[key] === 'undefined') { + process.env[key] = value; + } + } + + const shellPath = typeof shellEnv.PATH === 'string' ? shellEnv.PATH : ''; + if (!currentPathLooksUserConfigured && shellPath) { + process.env.PATH = mergePathValues(shellPath, currentPath, ':'); + } +}; + +const spawnLocalServer = async () => { + inheritUserShellEnv(); + + const settings = readSettingsRoot(); + const storedPort = Number.isFinite(settings.desktopLocalPort) ? settings.desktopLocalPort : null; + // When the user enables "Desktop Network Access" we bind on all interfaces + // so phones/tablets on the same Wi-Fi can reach the app. UI shows a clear + // warning and persists the flag via /api/config/settings. + const lanAccessEnabled = settings.desktopLanAccessEnabled === true; + const bindHost = lanAccessEnabled ? '0.0.0.0' : '127.0.0.1'; + + // Probe before starting the server — main() in the server module sets up a + // lot of global state before binding, and calling it twice after a listen + // failure would double-wire runtimes. Pick a known-free port in one shot. + const candidates = [storedPort, DEFAULT_DESKTOP_PORT].filter((v) => Number.isFinite(v) && v > 0); + let chosenPort = 0; + for (const candidate of candidates) { + if (await isPortFree(candidate)) { + chosenPort = candidate; + break; + } + } + if (chosenPort === 0) { + chosenPort = await pickUnusedPort(); + } + + // The server module reads ENV_DESKTOP_NOTIFY / OPENCHAMBER_DIST_DIR / + // OPENCHAMBER_RUNTIME at import time (top-level const), so these must be + // set before the first import. After this point, the same env is used by + // both the Electron main and the server running inside it. + process.env.OPENCHAMBER_HOST = bindHost; + process.env.OPENCHAMBER_DIST_DIR = resolveWebDistDir(); + process.env.OPENCHAMBER_RUNTIME = 'desktop'; + process.env.OPENCHAMBER_DESKTOP_NOTIFY = 'true'; + process.env.NO_PROXY = process.env.NO_PROXY || 'localhost,127.0.0.1'; + process.env.no_proxy = process.env.no_proxy || 'localhost,127.0.0.1'; + + const { startWebUiServer } = await import('@openchamber/web/server/index.js'); + + const handle = await startWebUiServer({ + port: chosenPort, + host: bindHost, + attachSignals: false, + exitOnShutdown: false, + onDesktopNotification: (payload) => maybeShowNativeNotification(payload), + }); + + const port = handle.getPort(); + const url = buildLocalUrl(port); + + state.serverHandle = handle; + state.sidecarUrl = url; + + await mutateSettingsRoot((root) => { + root.desktopLocalPort = port; + }); + + return url; +}; + +const killSidecar = () => { + if (state.serverHandle) { + try { + const result = state.serverHandle.stop({ exitProcess: false }); + if (result && typeof result.then === 'function') { + result.catch(() => {}); + } + } catch { + } + state.serverHandle = null; + } + state.sidecarUrl = null; +}; + +const macosMajorVersion = () => { + if (process.platform !== 'darwin') return 0; + const result = spawnSync('/usr/bin/sw_vers', ['-productVersion'], { encoding: 'utf8' }); + const raw = (result.stdout || '').trim(); + const [majorRaw, minorRaw] = raw.split('.'); + const major = Number.parseInt(majorRaw || '0', 10); + const minor = Number.parseInt(minorRaw || '0', 10); + return major === 10 ? minor : major; +}; + +const buildInitScript = (localOrigin, bootOutcome) => { + const home = JSON.stringify(os.homedir() || ''); + const local = JSON.stringify(localOrigin || ''); + const macVersion = macosMajorVersion(); + const outcome = JSON.stringify(bootOutcome ?? null); + return [ + '(function(){', + `try{window.__OPENCHAMBER_HOME__=${home};window.__OPENCHAMBER_MACOS_MAJOR__=${macVersion};window.__OPENCHAMBER_LOCAL_ORIGIN__=${local};var __oc_bo=${outcome};if(__oc_bo){window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__=__oc_bo;}}catch(_e){}`, + '}())', + ].join(''); +}; + +const computeBootOutcome = ({ envTargetUrl, probe, config, localAvailable }) => { + if (envTargetUrl) { + const status = probe && probe.status === 'unreachable' ? 'unreachable' : 'ok'; + return { target: 'remote', status, hostId: ENV_OVERRIDE_HOST_ID, url: envTargetUrl }; + } + + const defaultId = config.defaultHostId || ''; + if (!defaultId) { + return { target: null, status: 'not-configured' }; + } + + if (defaultId === LOCAL_HOST_ID) { + return localAvailable + ? { target: 'local', status: 'ok' } + : { target: 'local', status: 'unreachable' }; + } + + const host = config.hosts.find((entry) => entry.id === defaultId); + if (!host) { + return { target: 'remote', status: 'missing', hostId: defaultId }; + } + + const status = probe && probe.status === 'unreachable' ? 'unreachable' : 'ok'; + return { target: 'remote', status, hostId: host.id, url: host.url }; +}; + +const buildStartupSplashHtml = () => { + const settings = readSettingsRoot(); + const splashBgLight = typeof settings.splashBgLight === 'string' ? settings.splashBgLight.trim() : '#f5f5f4'; + const splashFgLight = typeof settings.splashFgLight === 'string' ? settings.splashFgLight.trim() : '#1c1917'; + const splashBgDark = typeof settings.splashBgDark === 'string' ? settings.splashBgDark.trim() : '#0c0a09'; + const splashFgDark = typeof settings.splashFgDark === 'string' ? settings.splashFgDark.trim() : '#fafaf9'; + + return ` + + + + + + + +
OpenChamber
+ + `; +}; + +const isBenignNavigationAbort = (error) => { + if (!error || typeof error !== 'object') { + return false; + } + + if (error.errno === -3) { + return true; + } + + const message = typeof error.message === 'string' ? error.message : ''; + return message.includes('ERR_ABORTED') || message.includes(' (-3) loading '); +}; + +const navigateWindow = async (browserWindow, url, { allowAbort = false } = {}) => { + try { + await browserWindow.loadURL(url); + } catch (error) { + if (allowAbort && isBenignNavigationAbort(error)) { + return; + } + throw error; + } +}; + +const emitToWindow = (browserWindow, event, detail) => { + if (!browserWindow || browserWindow.isDestroyed()) return; + browserWindow.webContents.send('openchamber:emit', { event, detail }); +}; + +const emitToAllWindows = (event, detail) => { + for (const browserWindow of BrowserWindow.getAllWindows()) { + emitToWindow(browserWindow, event, detail); + } +}; + +const pendingDeepLinks = []; + +const parseDeepLink = (raw) => { + if (typeof raw !== 'string') return null; + const trimmed = raw.trim(); + if (!trimmed) return null; + try { + const url = new URL(trimmed); + if (url.protocol !== `${DEEP_LINK_PROTOCOL}:`) return null; + const type = url.hostname; + if (!type) return null; + const segments = url.pathname.split('/').filter(Boolean); + const value = segments.length > 0 + ? decodeURIComponent(segments.join('/')) + : ''; + return { type, value }; + } catch { + return null; + } +}; + +const switchToHostById = async (rawId) => { + const id = typeof rawId === 'string' ? rawId.trim() : ''; + if (!id) return; + const config = readDesktopHostsConfig(); + let targetUrl = null; + if (id === LOCAL_HOST_ID) { + targetUrl = state.sidecarUrl || state.localOrigin; + } else { + const host = config.hosts.find((entry) => entry.id === id); + if (!host) { + log.warn('[electron] deep-link host not found:', id); + return; + } + targetUrl = host.url; + } + if (!targetUrl) { + log.warn('[electron] deep-link host has no target URL:', id); + return; + } + const bootOutcome = id === LOCAL_HOST_ID + ? { target: 'local', status: 'ok' } + : { target: 'remote', status: 'ok', hostId: id, url: targetUrl }; + log.info('[electron] switching to host', { id, bootOutcome }); + await activateMainWindow(targetUrl, state.localOrigin, bootOutcome); +}; + +const dispatchDeepLink = (link) => { + if (!link) return; + log.info('[electron] dispatching deep-link', { type: link.type, valueLen: link.value?.length || 0 }); + if (link.type === 'session' && link.value) { + emitToAllWindows('openchamber:open-session', { sessionId: link.value }); + return; + } + if (link.type === 'project' && link.value) { + emitToAllWindows('openchamber:open-project', { projectPath: link.value }); + return; + } + if (link.type === 'host' && link.value) { + void switchToHostById(link.value); + return; + } + log.warn('[electron] unknown deep-link action:', link.type); +}; + +const flushPendingDeepLinks = () => { + while (pendingDeepLinks.length > 0) { + dispatchDeepLink(pendingDeepLinks.shift()); + } +}; + +const isMainWindowReadyForDeepLink = () => + Boolean(state.mainWindow) + && !state.mainWindow.isDestroyed() + && !state.mainWindow.webContents.isLoading(); + +const handleDeepLinks = (urls) => { + for (const raw of urls) { + const parsed = parseDeepLink(raw); + if (!parsed) continue; + if (isMainWindowReadyForDeepLink()) { + dispatchDeepLink(parsed); + } else { + pendingDeepLinks.push(parsed); + } + } +}; + +const extractInitialDeepLinks = () => + process.argv.filter((arg) => typeof arg === 'string' && arg.startsWith(`${DEEP_LINK_PROTOCOL}://`)); + +const dispatchDomEventToWindow = (browserWindow, event, detail) => { + if (!browserWindow || browserWindow.isDestroyed()) return; + + const eventLiteral = JSON.stringify(event); + const script = detail === undefined + ? `window.dispatchEvent(new Event(${eventLiteral}));` + : `window.dispatchEvent(new CustomEvent(${eventLiteral}, { detail: ${JSON.stringify(detail)} }));`; + + void browserWindow.webContents.executeJavaScript(script, true).catch(() => {}); +}; + +const getMenuTargetWindow = () => { + const focused = BrowserWindow.getFocusedWindow(); + if (focused && !focused.isDestroyed()) return focused; + if (state.mainWindow && !state.mainWindow.isDestroyed()) return state.mainWindow; + const [firstWindow] = BrowserWindow.getAllWindows(); + return firstWindow && !firstWindow.isDestroyed() ? firstWindow : null; +}; + +const dispatchMenuAction = (action) => { + const target = getMenuTargetWindow(); + emitToWindow(target, 'openchamber:menu-action', action); + dispatchDomEventToWindow(target, 'openchamber:menu-action', action); +}; + +const dispatchCheckForUpdates = () => { + emitToAllWindows('openchamber:check-for-updates'); + for (const browserWindow of BrowserWindow.getAllWindows()) { + dispatchDomEventToWindow(browserWindow, 'openchamber:check-for-updates'); + } +}; + +const nextWindowLabel = () => { + const value = state.windowCounter++; + return value === 1 ? 'main' : `main-${value}`; +}; + +const readThemeSource = () => { + const settings = readSettingsRoot(); + // themeMode is the user's intent; themeVariant is only the resolved + // concrete appearance at persist time. When mode === 'system', we must + // follow the OS even if variant was saved as a specific value. + if (settings.themeMode === 'system' || settings.useSystemTheme === true) return 'system'; + if (settings.themeMode === 'light') return 'light'; + if (settings.themeMode === 'dark') return 'dark'; + if (settings.themeVariant === 'light') return 'light'; + if (settings.themeVariant === 'dark') return 'dark'; + return 'system'; +}; + +const createBrowserWindow = ({ label, restoreGeometry, url }) => { + const saved = restoreGeometry ? readWindowState() : null; + const useSaved = saved && typeof saved.width === 'number' && typeof saved.height === 'number'; + const desktopLocalOrigin = state.localOrigin || ''; + const desktopHome = os.homedir() || ''; + const desktopMacosMajor = String(macosMajorVersion()); + const options = { + title: 'OpenChamber', + width: useSaved ? Math.max(saved.width, MIN_RESTORE_WINDOW_WIDTH) : 1280, + height: useSaved ? Math.max(saved.height, MIN_RESTORE_WINDOW_HEIGHT) : 800, + minWidth: MIN_WINDOW_WIDTH, + minHeight: MIN_WINDOW_HEIGHT, + show: false, + backgroundColor: '#151313', + // 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: process.platform === 'darwin' ? 'hidden' : 'default', + trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 17 } : undefined, + webPreferences: { + additionalArguments: [ + `--openchamber-local-origin=${desktopLocalOrigin}`, + `--openchamber-home=${desktopHome}`, + `--openchamber-macos-major=${desktopMacosMajor}`, + `--openchamber-boot-outcome=${JSON.stringify(state.bootOutcome || null)}`, + ], + preload: isDev ? path.join(__dirname, 'preload.mjs') : path.join(app.getAppPath(), 'preload.mjs'), + backgroundThrottling: true, + contextIsolation: true, + nodeIntegration: false, + // 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. + sandbox: false, + }, + }; + + const browserWindow = new BrowserWindow(options); + browserWindow.__ocLabel = label || nextWindowLabel(); + + if (useSaved && Number.isFinite(saved.x) && Number.isFinite(saved.y)) { + browserWindow.setPosition(saved.x, saved.y); + } + + if (useSaved && saved.maximized) { + browserWindow.maximize(); + } + + browserWindow.on('focus', () => { + state.focusedWindowIds.add(browserWindow.id); + }); + browserWindow.on('blur', () => { + state.focusedWindowIds.delete(browserWindow.id); + }); + + // Traffic lights disappear during dock-restore animation when using + // titleBarStyle:'hidden' + custom trafficLightPosition. macOS caches a + // snapshot of the window at miniaturize time and plays it during the + // genie-restore animation. We re-assert button position on 'minimize' + // (before the snapshot) and 'restore'/'show'/'focus' to cover other + // transient reset states AppKit puts the buttons in. + if (process.platform === 'darwin') { + const refreshTrafficLights = () => { + if (browserWindow.isDestroyed()) return; + try { + browserWindow.setWindowButtonVisibility(true); + browserWindow.setTrafficLightPosition({ x: 16, y: 17 }); + } catch {} + }; + browserWindow.on('minimize', refreshTrafficLights); + browserWindow.on('restore', () => { + refreshTrafficLights(); + setTimeout(refreshTrafficLights, 250); + }); + browserWindow.on('show', refreshTrafficLights); + browserWindow.on('focus', refreshTrafficLights); + } + + browserWindow.on('resize', () => { + emitToWindow(browserWindow, 'openchamber:window-resized'); + debounceWindowStatePersist(browserWindow, false); + }); + browserWindow.on('move', () => { + debounceWindowStatePersist(browserWindow, false); + }); + browserWindow.on('close', (event) => { + if (process.platform === 'darwin' && !state.quitRequested) { + const remainingVisible = BrowserWindow.getAllWindows().filter( + (window) => !window.isDestroyed() && window.isVisible(), + ).length; + + if (remainingVisible <= 1) { + debounceWindowStatePersist(browserWindow, true); + event.preventDefault(); + browserWindow.hide(); + return; + } + } + + debounceWindowStatePersist(browserWindow, true); + }); + browserWindow.on('closed', () => { + state.focusedWindowIds.delete(browserWindow.id); + if (state.mainWindow && browserWindow.id === state.mainWindow.id) { + state.mainWindow = null; + } + if (BrowserWindow.getAllWindows().length === 0) { + if (!state.installingUpdate) { + killSidecar(); + } + if (process.platform !== 'darwin') { + app.quit(); + } + } + }); + + // Any navigation target that isn't our own UI (local server / configured + // desktop hosts) should open in the user's default browser, not spawn + // another Electron window loading arbitrary web content. + const isAllowedNavigationUrl = (raw) => { + try { + const url = new URL(raw); + if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'devtools:') return true; + if (url.protocol !== 'http:' && url.protocol !== 'https:') return false; + const hostname = url.hostname; + if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') return true; + if (state.localOrigin) { + try { + if (new URL(state.localOrigin).origin === url.origin) return true; + } catch { + } + } + const hosts = readDesktopHostsConfig()?.hosts || []; + for (const entry of hosts) { + if (typeof entry?.url !== 'string') continue; + try { + if (new URL(entry.url).origin === url.origin) return true; + } catch { + } + } + return false; + } catch { + return false; + } + }; + + browserWindow.webContents.setWindowOpenHandler(({ url }) => { + if (isAllowedNavigationUrl(url)) { + return { action: 'allow' }; + } + void shell.openExternal(url).catch(() => {}); + return { action: 'deny' }; + }); + + browserWindow.webContents.on('will-navigate', (event, url) => { + if (isAllowedNavigationUrl(url)) return; + event.preventDefault(); + void shell.openExternal(url).catch(() => {}); + }); + + browserWindow.webContents.setZoomFactor(1); + browserWindow.webContents.on('zoom-changed', () => { + browserWindow.webContents.setZoomFactor(1); + }); + + browserWindow.webContents.on('dom-ready', () => { + if (state.initScript) { + void browserWindow.webContents.executeJavaScript(state.initScript).catch(() => {}); + } + }); + + browserWindow.webContents.on('did-finish-load', () => { + browserWindow.webContents.setZoomFactor(1); + if (state.mainWindow && browserWindow.id === state.mainWindow.id && pendingDeepLinks.length > 0) { + const timer = setTimeout(flushPendingDeepLinks, 400); + if (typeof timer?.unref === 'function') timer.unref(); + } + }); + + browserWindow.once('ready-to-show', () => { + browserWindow.show(); + browserWindow.focus(); + }); + + if (url) { + void navigateWindow(browserWindow, url); + } else { + void navigateWindow( + browserWindow, + `data:text/html;charset=utf-8,${encodeURIComponent(buildStartupSplashHtml())}`, + { allowAbort: true }, + ); + } + + return browserWindow; +}; + +const activateMainWindow = async (url, localOrigin, bootOutcome) => { + state.localOrigin = localOrigin; + state.bootOutcome = bootOutcome ?? null; + state.initScript = buildInitScript(localOrigin, state.bootOutcome); + + const mainWindow = state.mainWindow; + if (mainWindow && !mainWindow.isDestroyed()) { + await navigateWindow(mainWindow, url, { allowAbort: true }); + mainWindow.show(); + mainWindow.focus(); + return mainWindow; + } + + state.mainWindow = createBrowserWindow({ + label: 'main', + restoreGeometry: true, + url, + }); + return state.mainWindow; +}; + +const createAdditionalWindow = async (url) => { + if (!state.localOrigin) { + return null; + } + const browserWindow = createBrowserWindow({ + label: nextWindowLabel(), + restoreGeometry: false, + url, + }); + return browserWindow; +}; + +const resolveInitialUrl = async () => { + const localUrl = isDev && await waitForHealth('http://127.0.0.1:3901', 5_000, 100) + ? 'http://127.0.0.1:3901' + : await spawnLocalServer(); + + const localUiUrl = isDev && await waitForHealth('http://127.0.0.1:5173', 8_000, 100) + ? 'http://127.0.0.1:5173' + : localUrl; + + state.sidecarUrl = localUrl; + const localAvailable = Boolean(localUrl); + + const localOrigin = new URL(localUiUrl).origin; + let initialUrl = localUiUrl; + let remoteProbe = null; + + const envTarget = normalizeHostUrl(process.env.OPENCHAMBER_SERVER_URL || ''); + const config = readDesktopHostsConfig(); + if (envTarget) { + initialUrl = envTarget; + } else if (config.defaultHostId && config.defaultHostId !== LOCAL_HOST_ID) { + const host = config.hosts.find((entry) => entry.id === config.defaultHostId); + if (host?.url) { + initialUrl = host.url; + } + } + + if (initialUrl !== localUiUrl) { + remoteProbe = await probeHostWithTimeout(initialUrl, 2_000); + if (remoteProbe.status === 'unreachable') { + remoteProbe = await probeHostWithTimeout(initialUrl, 10_000); + } + if (remoteProbe.status === 'unreachable') { + state.unreachableHosts.add(initialUrl); + initialUrl = localUiUrl; + } + } + + const bootOutcome = computeBootOutcome({ + envTargetUrl: envTarget || null, + probe: remoteProbe, + config, + localAvailable, + }); + + return { initialUrl, localOrigin, localUiUrl, bootOutcome }; +}; + +const compareSemver = (left, right) => { + const a = String(left || '').replace(/^v/, '').split('.').map((value) => Number.parseInt(value || '0', 10)); + const b = String(right || '').replace(/^v/, '').split('.').map((value) => Number.parseInt(value || '0', 10)); + const length = Math.max(a.length, b.length); + for (let index = 0; index < length; index += 1) { + const diff = (a[index] || 0) - (b[index] || 0); + if (diff !== 0) return diff; + } + return 0; +}; + +const parseGithubRepo = () => { + return { owner: 'btriapitsyn', repo: 'openchamber' }; +}; + +const setupAutoUpdater = () => { + if (!app.isPackaged) { + return; + } + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = false; + autoUpdater.allowPrerelease = false; + autoUpdater.fullChangelog = true; + autoUpdater.disableWebInstaller = false; + autoUpdater.logger = log; + + const { owner, repo } = parseGithubRepo(); + autoUpdater.setFeedURL({ + provider: 'github', + owner, + repo, + }); + + autoUpdater.on('download-progress', (progress) => { + emitToAllWindows('openchamber:update-progress', mapUpdaterProgressEvent({ + event: 'Progress', + data: { + chunkLength: Math.max(0, Math.round(progress.bytesPerSecond || 0)), + downloaded: Math.round(progress.transferred || 0), + total: Math.round(progress.total || 0), + }, + })); + }); + + autoUpdater.on('update-downloaded', (info) => { + log.info(`[electron] update-downloaded version=${info?.version || 'unknown'}`); + if (state.pendingUpdate) { + state.pendingUpdate.downloaded = true; + } + }); + + autoUpdater.on('error', (err) => { + log.error('[electron] autoUpdater error', err); + }); +}; + +const parseRelevantChangelogNotes = async (fromVersion, toVersion) => { + try { + const response = await fetch(CHANGELOG_URL, { signal: AbortSignal.timeout(10_000) }); + if (!response.ok) return null; + const changelog = await response.text(); + const sections = changelog.split(/^##\s+\[/m).slice(1); + const relevant = []; + for (const section of sections) { + const version = section.split(']')[0]; + if (compareSemver(version, fromVersion) > 0 && compareSemver(version, toVersion) <= 0) { + relevant.push(`## [${section}`.trim()); + } + } + return relevant.length > 0 ? relevant.join('\n\n') : null; + } catch { + return null; + } +}; + +const buildInstalledAppsCachePath = () => path.join(path.dirname(settingsFilePath()), INSTALLED_APPS_CACHE_FILE); + +// Async variants. sips + mdfind via spawnSync blocked the Electron main event +// loop for 2-3s on boot (22 OPEN_IN_APPS × ~200 ms each). Use execFile promises +// so each child-process wait yields to the loop and the UI stays responsive. +const pathExists = async (candidate) => { + try { + await fsp.access(candidate); + return true; + } catch { + return false; + } +}; + +const resolveAppBundlePath = async (appName) => { + if (process.platform !== 'darwin') return null; + const bundleName = appName.endsWith('.app') ? appName : `${appName}.app`; + const candidates = [ + `/Applications/${bundleName}`, + `/System/Applications/${bundleName}`, + `/System/Applications/Utilities/${bundleName}`, + path.join(os.homedir(), 'Applications', bundleName), + ]; + for (const candidate of candidates) { + if (await pathExists(candidate)) return candidate; + } + try { + const { stdout } = await execFileAsync('mdfind', ['-name', bundleName], { encoding: 'utf8' }); + const first = (stdout || '').split('\n').map((line) => line.trim()).find(Boolean); + return first || null; + } catch { + return null; + } +}; + +const isAppBundleInstalled = async (appName) => Boolean(await resolveAppBundlePath(appName)); + +const iconToDataUrl = async (iconPath, appName) => { + if (!iconPath || !(await pathExists(iconPath))) return null; + const safeName = String(appName || 'app').replace(/[^a-z0-9]/gi, '_'); + const tempPath = path.join(os.tmpdir(), `openchamber-icon-${safeName}-${Date.now()}.png`); + try { + await execFileAsync('sips', ['-s', 'format', 'png', '-Z', '32', iconPath, '--out', tempPath], { stdio: 'ignore' }); + } catch { + return null; + } + if (!(await pathExists(tempPath))) return null; + try { + const bytes = await fsp.readFile(tempPath); + return `data:image/png;base64,${bytes.toString('base64')}`; + } finally { + await fsp.rm(tempPath, { force: true }).catch(() => {}); + } +}; + +const resolveAppIconPath = async (appPath) => { + if (!appPath || !(await pathExists(appPath))) return null; + const resourcesPath = path.join(appPath, 'Contents', 'Resources'); + if (!(await pathExists(resourcesPath))) return null; + let entries; + try { + entries = await fsp.readdir(resourcesPath); + } catch { + return null; + } + const icon = entries.find((entry) => entry.toLowerCase().endsWith('.icns')); + return icon ? path.join(resourcesPath, icon) : null; +}; + +const buildInstalledApps = async (apps) => { + const seen = new Set(); + const names = apps + .map((raw) => String(raw || '').trim()) + .filter((raw) => raw && !seen.has(raw) && seen.add(raw)); + const results = []; + for (const name of names) { + const appPath = await resolveAppBundlePath(name); + if (!appPath) continue; + const iconDataUrl = await iconToDataUrl(await resolveAppIconPath(appPath), name); + results.push({ name, iconDataUrl }); + } + return results; +}; + +const parseSshConfigImports = () => { + const sshConfigPath = path.join(os.homedir(), '.ssh', 'config'); + if (!fs.existsSync(sshConfigPath)) return []; + const lines = fs.readFileSync(sshConfigPath, 'utf8').split(/\r?\n/); + const results = []; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#') || !trimmed.toLowerCase().startsWith('host ')) { + continue; + } + const hosts = trimmed.slice(5).trim().split(/\s+/).filter(Boolean); + for (const host of hosts) { + results.push({ + host, + pattern: /[*?]/.test(host), + source: sshConfigPath, + sshCommand: `ssh ${host}`, + }); + } + } + return results; +}; + +const readDesktopSshInstances = () => { + const root = readSettingsRoot(); + return { instances: Array.isArray(root.desktopSshInstances) ? root.desktopSshInstances : [] }; +}; + +const writeDesktopSshInstances = async (config) => { + const nextInstances = Array.isArray(config?.instances) ? config.instances : []; + await mutateSettingsRoot((root) => { + root.desktopSshInstances = nextInstances; + }); + return { instances: nextInstances }; +}; + +const updateHostUrlForSshInstance = async (id, label, localUrl) => { + const config = readDesktopHostsConfig(); + const nextHosts = config.hosts.filter((entry) => entry.id !== id); + nextHosts.push({ id, label, url: localUrl }); + await writeDesktopHostsConfig({ hosts: nextHosts, defaultHostId: config.defaultHostId }); +}; + +const JETBRAINS_APP_IDS = new Set([ + 'pycharm', + 'intellij', + 'webstorm', + 'phpstorm', + 'rider', + 'rustrover', + 'android-studio', +]); + +const CLI_BY_APP_ID = { + vscode: 'code', + cursor: 'cursor', + vscodium: 'codium', + windsurf: 'windsurf', + zed: 'zed', +}; + +const buildOpenProjectSpecs = ({ projectPath, appId, appName }) => { + if (appId === 'finder') { + return [{ program: 'open', args: [projectPath] }]; + } + + if (appId === 'terminal' || appId === 'iterm2' || appId === 'ghostty') { + return [{ program: 'open', args: ['-a', appName, projectPath] }]; + } + + const specs = []; + + const cli = CLI_BY_APP_ID[appId]; + if (cli) { + specs.push({ program: cli, args: ['-n', projectPath] }); + } + + if (JETBRAINS_APP_IDS.has(appId)) { + specs.push({ program: 'open', args: ['-na', appName, '--args', projectPath] }); + } + + specs.push({ program: 'open', args: ['-a', appName, projectPath] }); + return specs; +}; + +const buildOpenFileSpecs = ({ filePath, appId, appName }) => { + if (appId === 'finder') { + return [{ program: 'open', args: ['-R', filePath] }]; + } + + const parentDir = path.dirname(filePath); + if (appId === 'terminal' || appId === 'iterm2' || appId === 'ghostty') { + return [{ program: 'open', args: ['-a', appName, parentDir] }]; + } + + const specs = []; + + const cli = CLI_BY_APP_ID[appId]; + if (cli) { + specs.push({ program: cli, args: [filePath] }); + } + + specs.push({ program: 'open', args: ['-a', appName, filePath] }); + return specs; +}; + +const runSpecChain = (specs, appName) => { + const failures = []; + for (const spec of specs) { + const result = spawnSync(spec.program, spec.args, { stdio: 'ignore' }); + if (result.error) { + failures.push(`${spec.program}: ${result.error.message}`); + continue; + } + if (result.status === 0) { + return; + } + failures.push(`${spec.program} exited ${result.status}`); + } + throw new Error(`Failed to open in ${appName}: ${failures.join('; ')}`); +}; + +const handleInvoke = async (browserWindow, command, args = {}) => { + switch (command) { + case 'desktop_start_window_drag': + return null; + + case 'desktop_is_window_fullscreen': + return Boolean(browserWindow?.isFullScreen()); + + case 'desktop_set_window_title': + if (browserWindow && typeof args.title === 'string') { + browserWindow.setTitle(args.title); + } + return null; + + case 'desktop_get_app_version': + return APP_VERSION; + + case 'desktop_save_markdown_file': { + const defaultPath = typeof args.defaultFileName === 'string' ? args.defaultFileName.trim() : ''; + if (!defaultPath) { + throw new Error('Default file name is required'); + } + + const content = typeof args.content === 'string' ? args.content : ''; + const result = await dialog.showSaveDialog(browserWindow || undefined, { + defaultPath, + filters: [{ name: 'Markdown', extensions: ['md'] }], + }); + if (result.canceled || !result.filePath) { + return null; + } + + await fsp.writeFile(result.filePath, content, 'utf8'); + return result.filePath; + } + + case 'desktop_read_file': { + const rawPath = typeof args.path === 'string' ? args.path : ''; + if (!rawPath) throw new Error('Path is required'); + // Defense in depth behind the IPC origin gate: even our own UI (or a + // prompt-injected agent) can't read credential stores. Resolve the + // path, require it under $HOME or tmpdir, and refuse known secret dirs + // / dotfiles commonly holding keys. + const filePath = path.resolve(rawPath); + const home = os.homedir() || ''; + const tmp = os.tmpdir() || ''; + const underHome = home && (filePath === home || filePath.startsWith(home + path.sep)); + const underTmp = tmp && (filePath === tmp || filePath.startsWith(tmp + path.sep)); + if (!underHome && !underTmp) { + throw new Error('File is outside the allowed workspace'); + } + const DENIED_SEGMENTS = ['.ssh', '.aws', '.gnupg', '.gpg', '.config/gh', '.config/openchamber/credentials']; + const relFromHome = underHome ? filePath.slice(home.length + 1) : ''; + const relNormalized = relFromHome.split(path.sep).join('/'); + if (DENIED_SEGMENTS.some((segment) => relNormalized === segment || relNormalized.startsWith(`${segment}/`))) { + throw new Error('Access to this path is not allowed'); + } + const basename = path.basename(filePath).toLowerCase(); + if (basename === '.env' || basename.startsWith('.env.') || basename.endsWith('.pem') || basename.endsWith('.key')) { + throw new Error('Access to this path is not allowed'); + } + const stats = await fsp.stat(filePath); + if (stats.size > 50 * 1024 * 1024) { + throw new Error('File is too large. Maximum size is 50MB.'); + } + const bytes = await fsp.readFile(filePath); + const ext = path.extname(filePath).toLowerCase(); + const mime = ({ + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.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', + })[ext] || 'application/octet-stream'; + return { mime, base64: bytes.toString('base64'), size: bytes.length }; + } + + case 'desktop_notify': + maybeShowNativeNotification(args); + return null; + + case 'desktop_clear_cache': + await session.defaultSession.clearStorageData(); + for (const browserWindow of BrowserWindow.getAllWindows()) { + browserWindow.webContents.reload(); + } + return null; + + case 'desktop_open_path': { + const targetPath = typeof args.path === 'string' ? args.path.trim() : ''; + const appName = typeof args.app === 'string' ? args.app.trim() : ''; + if (!targetPath) throw new Error('Path is required'); + if (process.platform === 'darwin') { + const openArgs = appName ? ['-a', appName, targetPath] : [targetPath]; + spawn('open', openArgs, { detached: true, stdio: 'ignore' }).unref(); + return null; + } + await shell.openPath(targetPath); + return null; + } + + case 'desktop_open_external_url': { + const target = typeof args.url === 'string' ? args.url.trim() : ''; + if (!target) throw new Error('URL is required'); + + const parsed = new URL(target); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error('Only HTTP URLs can be opened externally'); + } + + await shell.openExternal(parsed.toString()); + return null; + } + + case 'desktop_reveal_path': { + const targetPath = typeof args.path === 'string' ? args.path.trim() : ''; + if (!targetPath) { + throw new Error('Path is required'); + } + + const stats = await fsp.stat(targetPath).catch(() => null); + if (stats?.isDirectory()) { + await shell.openPath(targetPath); + return null; + } + + shell.showItemInFolder(targetPath); + return null; + } + + case 'desktop_open_in_app': { + if (process.platform !== 'darwin') { + throw new Error('desktop_open_in_app is only supported on macOS'); + } + const projectPath = typeof args.projectPath === 'string' ? args.projectPath.trim() : ''; + const appId = typeof args.appId === 'string' ? args.appId.trim().toLowerCase() : ''; + const appName = typeof args.appName === 'string' ? args.appName.trim() : ''; + if (!projectPath || !appId || !appName) { + throw new Error('Project path, app id, and app name are required'); + } + runSpecChain(buildOpenProjectSpecs({ projectPath, appId, appName }), appName); + return null; + } + + case 'desktop_open_file_in_app': { + if (process.platform !== 'darwin') { + throw new Error('desktop_open_file_in_app is only supported on macOS'); + } + const filePath = typeof args.filePath === 'string' ? args.filePath.trim() : ''; + const appId = typeof args.appId === 'string' ? args.appId.trim().toLowerCase() : ''; + const appName = typeof args.appName === 'string' ? args.appName.trim() : ''; + if (!filePath || !appId || !appName) { + throw new Error('File path, app id, and app name are required'); + } + runSpecChain(buildOpenFileSpecs({ filePath, appId, appName }), appName); + return null; + } + + case 'desktop_filter_installed_apps': { + if (process.platform !== 'darwin') { + throw new Error('desktop_filter_installed_apps is only supported on macOS'); + } + if (!Array.isArray(args.apps)) return []; + const results = await Promise.all( + args.apps.map(async (appName) => (await isAppBundleInstalled(String(appName))) ? String(appName) : null) + ); + return results.filter(Boolean); + } + + case 'desktop_fetch_app_icons': { + if (process.platform !== 'darwin') { + throw new Error('desktop_fetch_app_icons is only supported on macOS'); + } + const names = Array.isArray(args.apps) ? args.apps : []; + const results = []; + for (const name of names) { + const appPath = await resolveAppBundlePath(String(name)); + if (!appPath) continue; + const dataUrl = await iconToDataUrl(await resolveAppIconPath(appPath), String(name)); + if (dataUrl) results.push({ app: String(name), dataUrl }); + } + return results; + } + + case 'desktop_get_installed_apps': { + if (process.platform !== 'darwin') { + throw new Error('desktop_get_installed_apps is only supported on macOS'); + } + const cachePath = buildInstalledAppsCachePath(); + const now = Math.floor(Date.now() / 1000); + let cache = null; + try { + cache = JSON.parse(await fsp.readFile(cachePath, 'utf8')); + } catch { + } + const cachedApps = Array.isArray(cache?.apps) ? cache.apps : []; + const hasCache = Boolean(cache); + const isCacheStale = !cache || (now - Number(cache.updatedAt || 0)) > INSTALLED_APPS_CACHE_TTL_SECS; + const refresh = async () => { + const apps = await buildInstalledApps(Array.isArray(args.apps) ? args.apps : []); + await fsp.mkdir(path.dirname(cachePath), { recursive: true }); + await fsp.writeFile(cachePath, JSON.stringify({ updatedAt: now, apps }, null, 2)); + emitToAllWindows('openchamber:installed-apps-updated', apps); + }; + if (!hasCache || isCacheStale || args.force === true) { + void refresh(); + } + return { apps: cachedApps, hasCache, isCacheStale }; + } + + case 'desktop_hosts_get': + return readDesktopHostsConfig(); + + case 'desktop_hosts_set': { + await writeDesktopHostsConfig(args.input || args.config || {}); + const updatedConfig = readDesktopHostsConfig(); + const envTarget = normalizeHostUrl(process.env.OPENCHAMBER_SERVER_URL || ''); + state.bootOutcome = computeBootOutcome({ + envTargetUrl: envTarget || null, + probe: null, + config: updatedConfig, + localAvailable: Boolean(state.sidecarUrl || state.localOrigin), + }); + state.initScript = buildInitScript(state.localOrigin, state.bootOutcome); + log.info('[electron] hosts config updated, recomputed bootOutcome', state.bootOutcome); + return null; + } + + case 'desktop_host_probe': + return probeHostWithTimeout(String(args.url || ''), 2_000); + + case 'desktop_set_window_theme': { + const mode = typeof args.themeMode === 'string' ? args.themeMode : ''; + const variant = typeof args.themeVariant === 'string' ? args.themeVariant : ''; + // Priority order: themeMode expresses the user's intent (including + // "follow OS"). Variant is just the resolved variant at send time; + // when mode === 'system' with variant === 'dark' (because OS is + // currently dark), we must still pin themeSource to 'system' so + // Chromium keeps reacting to OS theme changes. + if (mode === 'system') { + nativeTheme.themeSource = 'system'; + } else if (mode === 'light') { + nativeTheme.themeSource = 'light'; + } else if (mode === 'dark') { + nativeTheme.themeSource = 'dark'; + } else if (variant === 'light') { + nativeTheme.themeSource = 'light'; + } else if (variant === 'dark') { + nativeTheme.themeSource = 'dark'; + } else { + nativeTheme.themeSource = 'system'; + } + return null; + } + + 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 + // disabled state so settings UI reflects it; args.enabled is ignored. + await mutateSettingsRoot((root) => { + root.desktopVibrancy = false; + }); + return { enabled: false, requiresRestart: false }; + } + + 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(); + } catch { + } + + 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; + return { + available, + currentVersion, + version: available ? nextVersion : null, + body: body || null, + date: + (typeof updateInfo?.releaseDate === 'string' && updateInfo.releaseDate) || + (typeof payload?.pub_date === 'string' ? payload.pub_date : null), + }; + } + + case 'desktop_download_and_install_update': + if (!state.pendingUpdate) { + throw new Error('No pending update'); + } + emitToAllWindows('openchamber:update-progress', mapUpdaterProgressEvent({ + event: 'Started', + data: { + contentLength: null, + }, + })); + if (!state.pendingUpdate.electronUpdate) { + throw new Error('Electron updater metadata is not available for this build'); + } + if (!state.pendingUpdate.downloaded) { + await new Promise((resolve, reject) => { + let settled = false; + const cleanup = () => { + autoUpdater.off('update-downloaded', onDownloaded); + autoUpdater.off('error', onError); + }; + const finish = (callback, value) => { + if (settled) return; + settled = true; + cleanup(); + callback(value); + }; + const onDownloaded = () => finish(resolve, null); + const onError = (error) => finish(reject, error); + autoUpdater.on('update-downloaded', onDownloaded); + autoUpdater.on('error', onError); + Promise.resolve(autoUpdater.downloadUpdate()).catch((error) => finish(reject, error)); + }); + } + emitToAllWindows('openchamber:update-progress', mapUpdaterProgressEvent({ + event: 'Finished', + data: {}, + })); + return null; + + case 'desktop_restart': { + const applyUpdate = Boolean(state.pendingUpdate?.downloaded && app.isPackaged); + log.info(`[electron] desktop_restart applyUpdate=${applyUpdate} packaged=${app.isPackaged}`); + if (applyUpdate && process.platform === 'darwin' && typeof app.isInApplicationsFolder === 'function') { + try { + if (!app.isInApplicationsFolder()) { + throw new Error('Desktop update requires OpenChamber.app to be installed in /Applications'); + } + } catch (error) { + log.warn('[electron] desktop_restart blocked', error); + throw error; + } + } + if (applyUpdate) { + // Match the working updater pattern closely: only bypass the macOS + // hide-on-close / quit-confirmation guards, leave the rest of the + // updater-driven quit/install sequence alone. + state.quitRequested = true; + state.installingUpdate = true; + state.quitConfirmationPending = false; + if (state.mainWindow && !state.mainWindow.isDestroyed()) { + try { + debounceWindowStatePersist(state.mainWindow, true); + } catch { + } + } + } + // Defer so the IPC reply flushes before the app starts shutting down. + // Without this, quitAndInstall() can race with the renderer's pending + // invoke and the restart appears to do nothing from the UI side. + setImmediate(() => { + try { + if (applyUpdate) { + autoUpdater.quitAndInstall(); + } else { + app.relaunch(); + app.exit(0); + } + } catch (err) { + log.error('[electron] desktop_restart failed', err); + } + }); + return null; + } + + case 'desktop_get_lan_address': + return await detectLanIPv4Address(); + + case 'desktop_new_window': { + const config = readDesktopHostsConfig(); + const localUiUrl = state.sidecarUrl || state.localOrigin; + let targetUrl = localUiUrl; + if (config.defaultHostId && config.defaultHostId !== LOCAL_HOST_ID) { + const host = config.hosts.find((entry) => entry.id === config.defaultHostId); + if (host?.url && !state.unreachableHosts.has(host.url)) { + targetUrl = host.url; + } + } + await createAdditionalWindow(targetUrl); + return null; + } + + case 'desktop_new_window_at_url': { + const targetUrl = normalizeHostUrl(String(args.url || '')); + if (!targetUrl) { + throw new Error('Invalid URL'); + } + await createAdditionalWindow(targetUrl); + return null; + } + + case 'desktop_ssh_instances_get': + return sshManager.readInstances(); + + case 'desktop_ssh_instances_set': + await sshManager.setInstances(args.config || {}); + return null; + + case 'desktop_ssh_import_hosts': + return await sshManager.importHosts(); + + case 'desktop_ssh_connect': { + const id = String(args.id || '').trim(); + await sshManager.connect(id); + return null; + } + + case 'desktop_ssh_disconnect': { + const id = String(args.id || '').trim(); + await sshManager.disconnect(id); + return null; + } + + case 'desktop_ssh_status': { + const id = String(args.id || '').trim(); + return await sshManager.statusesWithDefaults(id || undefined); + } + + case 'desktop_ssh_logs': + return sshManager.logsForInstance(String(args.id || '').trim(), Number(args.limit) || 200); + + case 'desktop_ssh_logs_clear': + sshManager.clearLogsForInstance(String(args.id || '').trim()); + return null; + + default: + throw new Error(`Unknown desktop command: ${command}`); + } +}; + +const buildMacMenu = () => { + const dispatchAction = (action) => dispatchMenuAction(action); + const handleCopyAction = () => { + BrowserWindow.getFocusedWindow()?.webContents.copy(); + dispatchAction('copy'); + }; + + return Menu.buildFromTemplate([ + { + label: app.name, + submenu: [ + { role: 'about' }, + { + label: 'Check for Updates', + click: () => dispatchCheckForUpdates(), + }, + { type: 'separator' }, + { label: 'Settings', accelerator: 'Cmd+,', click: () => dispatchAction('settings') }, + { label: 'Command Palette', accelerator: 'Cmd+K', click: () => dispatchAction('command-palette') }, + { label: 'Quick Open…', accelerator: 'Cmd+P', click: () => dispatchAction('quick-open') }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { type: 'separator' }, + { role: 'quit' }, + ], + }, + { + label: 'File', + submenu: [ + { label: 'New Window', accelerator: 'Cmd+Shift+Alt+N', click: () => void handleInvoke(null, 'desktop_new_window') }, + { type: 'separator' }, + { label: 'New Session', accelerator: 'Cmd+N', click: () => dispatchAction('new-session') }, + { label: 'New Worktree', accelerator: 'Cmd+Shift+N', click: () => dispatchAction('new-worktree-session') }, + { type: 'separator' }, + { label: 'Add Workspace', click: () => dispatchAction('change-workspace') }, + { type: 'separator' }, + { role: 'close' }, + ], + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { label: 'Copy', accelerator: 'Cmd+C', click: () => handleCopyAction() }, + { role: 'paste' }, + { role: 'selectAll' }, + ], + }, + { + label: 'View', + submenu: [ + { label: 'Git', accelerator: 'Cmd+G', click: () => dispatchAction('open-git-tab') }, + { label: 'Diff', accelerator: 'Cmd+E', click: () => dispatchAction('open-diff-tab') }, + { label: 'Files', click: () => dispatchAction('open-files-tab') }, + { label: 'Terminal', accelerator: 'Cmd+T', click: () => dispatchAction('open-terminal-tab') }, + { type: 'separator' }, + { label: 'Light Theme', click: () => dispatchAction('theme-light') }, + { label: 'Dark Theme', click: () => dispatchAction('theme-dark') }, + { label: 'System Theme', click: () => dispatchAction('theme-system') }, + { type: 'separator' }, + { label: 'Toggle Session Sidebar', accelerator: 'Cmd+L', click: () => dispatchAction('toggle-sidebar') }, + { label: 'Toggle Memory Debug', accelerator: 'Cmd+Shift+D', click: () => dispatchAction('toggle-memory-debug') }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ], + }, + { + label: 'Window', + submenu: [ + { role: 'minimize' }, + { role: 'zoom' }, + { type: 'separator' }, + { role: 'close' }, + ], + }, + { + label: 'Help', + submenu: [ + { label: 'Keyboard Shortcuts', accelerator: 'Cmd+.', click: () => dispatchAction('help-dialog') }, + { label: 'Show Diagnostics', accelerator: 'Cmd+Shift+L', click: () => dispatchAction('download-logs') }, + { type: 'separator' }, + { label: 'Clear Cache', click: () => void handleInvoke(null, 'desktop_clear_cache') }, + { type: 'separator' }, + { label: 'Report a Bug', click: () => shell.openExternal(GITHUB_BUG_REPORT_URL) }, + { label: 'Request a Feature', click: () => shell.openExternal(GITHUB_FEATURE_REQUEST_URL) }, + { type: 'separator' }, + { label: 'Join Discord', click: () => shell.openExternal(DISCORD_INVITE_URL) }, + ], + }, + ]); +}; + +contextMenu({ + showInspectElement: isDev, + showSaveImageAs: true, + showCopyImage: true, + showCopyLink: true, +}); + +// All desktop_* IPC and dialog:open run with full Electron main privileges +// (fs access, shell.openPath, spawn, app.relaunch, …). The preload shim is +// injected into every webContents in the window, including remote hosts the +// user switches to via DesktopHostSwitcher. Without a gate, a malicious +// remote page could read arbitrary local files, open arbitrary apps, etc. +// +// Strategy: commands fall into two buckets by capability, not by origin. +// Window/host-switcher operations (probe a URL, open a new window, set +// title, read the hosts list) are safe for any renderer. Filesystem, +// shell.openPath, installed-app scans, app relaunch, and file dialogs +// are gated to local senders — even the user's own remote UI shouldn't +// need them, and a compromised remote can't use them either. +const isLocalSender = (webContents) => { + try { + const raw = typeof webContents?.getURL === 'function' ? webContents.getURL() : ''; + if (!raw) return false; + if (raw.startsWith('file://') || raw === 'about:blank') return true; + const url = new URL(raw); + if (url.protocol !== 'http:' && url.protocol !== 'https:') return false; + const hostname = url.hostname; + if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') return true; + if (state.localOrigin) { + try { + const allowed = new URL(state.localOrigin); + if (allowed.origin === url.origin) return true; + } catch { + } + } + return false; + } catch { + return false; + } +}; + +const COMMANDS_SAFE_FOR_REMOTE = new Set([ + 'desktop_hosts_get', + 'desktop_host_probe', + 'desktop_new_window', + 'desktop_new_window_at_url', + 'desktop_set_window_title', + 'desktop_set_window_theme', + 'desktop_is_window_fullscreen', + 'desktop_start_window_drag', + 'desktop_get_app_version', + 'desktop_get_lan_address', +]); + +ipcMain.handle('openchamber:invoke', async (event, command, args) => { + if (!isLocalSender(event.sender) && !COMMANDS_SAFE_FOR_REMOTE.has(command)) { + log.warn(`[ipc] rejected ${command} from non-local origin: ${event.sender?.getURL?.() || '(unknown)'}`); + throw new Error('IPC not available for this origin'); + } + const browserWindow = BrowserWindow.fromWebContents(event.sender); + return handleInvoke(browserWindow, command, args); +}); + +ipcMain.handle('openchamber:dialog:open', async (event, options) => { + // Native file dialogs expose absolute local paths; never grant to remote. + if (!isLocalSender(event.sender)) { + log.warn(`[ipc] rejected dialog:open from non-local origin: ${event.sender?.getURL?.() || '(unknown)'}`); + throw new Error('IPC not available for this origin'); + } + const browserWindow = BrowserWindow.fromWebContents(event.sender); + const result = await dialog.showOpenDialog(browserWindow || undefined, { + title: typeof options?.title === 'string' ? options.title : undefined, + filters: Array.isArray(options?.filters) + ? options.filters + .filter((filter) => filter && typeof filter === 'object') + .map((filter) => ({ + name: typeof filter.name === 'string' && filter.name.trim().length > 0 ? filter.name : 'Files', + extensions: Array.isArray(filter.extensions) + ? filter.extensions.filter((extension) => typeof extension === 'string' && extension.trim().length > 0) + : [], + })) + : undefined, + properties: [ + options?.directory ? 'openDirectory' : 'openFile', + options?.multiple ? 'multiSelections' : null, + 'createDirectory', + ].filter(Boolean), + }); + if (result.canceled) return null; + if (options?.multiple) return result.filePaths; + return result.filePaths[0] || null; +}); + +app.on('window-all-closed', () => { + if (process.platform === 'darwin' && !state.quitRequested) { + return; + } + + if (!state.installingUpdate) { + killSidecar(); + void sshManager.shutdownAll(); + } + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('before-quit', (event) => { + if (state.quitConfirmed || state.installingUpdate || process.platform !== 'darwin') { + state.quitRequested = true; + return; + } + event.preventDefault(); + void requestQuitWithConfirmation(); +}); + +app.on('second-instance', (_event, argv) => { + const urls = Array.isArray(argv) + ? argv.filter((arg) => typeof arg === 'string' && arg.startsWith(`${DEEP_LINK_PROTOCOL}://`)) + : []; + if (urls.length > 0) handleDeepLinks(urls); + focusForegroundWindow(); +}); + +app.on('open-url', (event, url) => { + event.preventDefault(); + handleDeepLinks([url]); +}); + +app.on('activate', async () => { + const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed()); + if (windows.length > 0) { + const visibleWindow = windows.find((window) => window.isVisible()); + const targetWindow = visibleWindow || state.mainWindow || windows[0]; + if (targetWindow.isMinimized()) targetWindow.restore(); + targetWindow.show(); + targetWindow.focus(); + return; + } + + if (state.localOrigin) { + const config = readDesktopHostsConfig(); + const localUiUrl = state.sidecarUrl || state.localOrigin; + const host = config.defaultHostId && config.defaultHostId !== LOCAL_HOST_ID + ? config.hosts.find((entry) => entry.id === config.defaultHostId) + : null; + const targetUrl = host?.url && !state.unreachableHosts.has(host.url) ? host.url : localUiUrl; + await createAdditionalWindow(targetUrl); + } +}); + +app.whenReady().then(async () => { + log.info('[electron] app starting', { + version: APP_VERSION, + packaged: app.isPackaged, + platform: process.platform, + arch: process.arch, + }); + nativeTheme.themeSource = readThemeSource(); + setupAutoUpdater(); + + if (process.platform === 'darwin') { + Menu.setApplicationMenu(buildMacMenu()); + } + + const initial = extractInitialDeepLinks(); + if (initial.length > 0) handleDeepLinks(initial); + + const { initialUrl, localOrigin, bootOutcome } = await resolveInitialUrl(); + await activateMainWindow(initialUrl, localOrigin, bootOutcome); + startQuitRiskPoller(); +}).catch((error) => { + log.error('[electron] startup failed:', error); + app.exit(1); +}); diff --git a/src/packages/electron/package.json b/src/packages/electron/package.json new file mode 100644 index 0000000..95080cc --- /dev/null +++ b/src/packages/electron/package.json @@ -0,0 +1,98 @@ +{ + "name": "@openchamber/electron", + "version": "1.9.9", + "private": true, + "description": "Electron desktop runtime for OpenChamber", + "author": "OpenChamber", + "type": "module", + "main": "./dist-bundle/main.mjs", + "dependencies": { + "@openchamber/web": "workspace:*", + "electron-context-menu": "^4.1.2", + "electron-log": "^5.4.3", + "electron-updater": "^6.8.3" + }, + "devDependencies": { + "@electron/rebuild": "^3.7.0", + "electron": "^41.2.1", + "electron-builder": "^26.0.0" + }, + "desktopPrerequisites": [ + "Electron runtime dependencies installed via bun install", + "Bun available for sidecar compilation", + "macOS build tools installed for notarized packaging" + ], + "scripts": { + "dev": "node ./scripts/electron-dev.mjs", + "build:web-assets": "node ./scripts/build-web-assets.mjs", + "build": "bun -e \"process.exit(0)\"", + "bundle:main": "bun ./scripts/bundle-main.mjs", + "rebuild:native": "node ./scripts/rebuild-native.mjs", + "package": "bun run build:web-assets && bun run bundle:main && bun run rebuild:native && electron-builder", + "finalize:latest-yml": "node ./scripts/finalize-latest-yml.mjs", + "type-check": "node --check ./main.mjs && node --check ./preload.mjs", + "lint": "node -e \"process.exit(0)\"" + }, + "build": { + "appId": "dev.openchamber.desktop", + "productName": "OpenChamber", + "files": [ + "dist-bundle/main.mjs", + "preload.mjs" + ], + "extraResources": [ + { + "from": "resources/web-dist", + "to": "web-dist" + } + ], + "directories": { + "buildResources": "resources/icons", + "output": "dist" + }, + "artifactName": "${productName}-${version}-${arch}.${ext}", + "npmRebuild": false, + "mac": { + "category": "public.app-category.developer-tools", + "icon": "resources/icons/icon.icns", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "resources/entitlements.mac.plist", + "entitlementsInherit": "resources/entitlements.mac.plist", + "notarize": true, + "target": [ + "dmg", + "zip" + ] + }, + "dmg": { + "sign": true, + "title": "${productName} ${version}", + "backgroundColor": "#FFFCF0", + "iconSize": 100, + "iconTextSize": 13, + "window": { + "width": 540, + "height": 340 + }, + "contents": [ + { + "x": 180, + "y": 140, + "type": "file" + }, + { + "x": 360, + "y": 140, + "type": "link", + "path": "/Applications" + } + ] + }, + "publish": { + "provider": "github", + "owner": "btriapitsyn", + "repo": "openchamber" + } + } +} diff --git a/src/packages/electron/preload.mjs b/src/packages/electron/preload.mjs new file mode 100644 index 0000000..2de3f08 --- /dev/null +++ b/src/packages/electron/preload.mjs @@ -0,0 +1,146 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +const eventListeners = new Map(); + +const readArgValue = (name) => { + const prefix = `${name}=`; + const entry = process.argv.find((value) => typeof value === 'string' && value.startsWith(prefix)); + if (!entry) { + return ''; + } + return entry.slice(prefix.length); +}; + +const localOrigin = readArgValue('--openchamber-local-origin'); +const homeDirectory = readArgValue('--openchamber-home'); +const macosMajorRaw = readArgValue('--openchamber-macos-major'); +const macosMajor = Number.parseInt(macosMajorRaw, 10); + +// Preload re-executes on every cross-origin navigation (we run with +// sandbox:false, per-document). Two separate concerns to balance: +// - __OPENCHAMBER_ELECTRON__ is a shell-identity flag (no capability). +// 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. Remote pages must +// not get it — otherwise any page loaded via DesktopHostSwitcher could +// read local files, open apps, relaunch, etc. Expose only on local +// pages (loopback / state.localOrigin / file:// for dev). +// Everything driven by localOrigin (home dir, macOS hints) also stays +// local-only since it leaks info about the Electron host machine. +const currentOrigin = (() => { + try { + return typeof location !== 'undefined' ? location.origin : ''; + } catch { + return ''; + } +})(); +const isLoopbackOrigin = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(currentOrigin); +const isLocalPage = currentOrigin === 'null' + || isLoopbackOrigin + || (localOrigin && currentOrigin === localOrigin); + +// Remote pages need __OPENCHAMBER_LOCAL_ORIGIN__ so the HostSwitcher knows +// the URL of the Local entry (isDesktopLocalOriginActive() falls back to +// window.location.origin otherwise — wrong on remote). Low risk: the value +// is just "http://127.0.0.1:" which is not exploitable without the +// IPC channel, and CORS on the local server prevents remote-origin fetches. +if (localOrigin) { + contextBridge.exposeInMainWorld('__OPENCHAMBER_LOCAL_ORIGIN__', localOrigin); +} + +// Home directory leaks the OS username — keep local-only. Remote pages +// operate on the REMOTE server's filesystem, local home is irrelevant +// (and would be misleading if consumed as a workspace hint). +if (isLocalPage && homeDirectory) { + contextBridge.exposeInMainWorld('__OPENCHAMBER_HOME__', homeDirectory); +} + +// macOS major version drives window chrome offsets (traffic lights) — UI +// presentation only, safe to expose. +if (Number.isFinite(macosMajor) && macosMajor > 0) { + contextBridge.exposeInMainWorld('__OPENCHAMBER_MACOS_MAJOR__', macosMajor); +} + +contextBridge.exposeInMainWorld('__OPENCHAMBER_ELECTRON__', { + runtime: 'electron', +}); + +// Note: bootOutcome must stay writable from the main world's initScript so +// re-navigations (host switch via deep link) can refresh it. contextBridge- +// exposed globals are read-only, which blocks that update — rely solely on +// the main-process initScript injection (dispatched on did-finish-load). + +const addListener = (event, handler) => { + const listeners = eventListeners.get(event) || new Set(); + listeners.add(handler); + eventListeners.set(event, listeners); + + return () => { + const current = eventListeners.get(event); + if (!current) { + return; + } + current.delete(handler); + if (current.size === 0) { + eventListeners.delete(event); + } + }; +}; + +const dispatchNativeEvent = (event, detail) => { + const listeners = eventListeners.get(event); + if (listeners) { + for (const listener of listeners) { + try { + listener({ payload: detail }); + } catch (error) { + console.error(`[electron:preload] listener failed for ${event}:`, error); + } + } + } + + try { + const domEvent = detail === undefined + ? new Event(event) + : new CustomEvent(event, { detail }); + window.dispatchEvent(domEvent); + } catch (error) { + console.error(`[electron:preload] failed to dispatch DOM event ${event}:`, error); + } +}; + +// Main-process events are read-only notifications (update progress, +// window focus, etc.) — safe to deliver to any page rendered in this +// webContents. The events themselves don't grant capability. +ipcRenderer.on('openchamber:emit', (_evt, payload) => { + if (!payload || typeof payload !== 'object') { + return; + } + + const event = typeof payload.event === 'string' ? payload.event : ''; + if (!event) { + return; + } + + dispatchNativeEvent(event, payload.detail); +}); + +// __TAURI__ 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), + }, +}); diff --git a/src/packages/electron/resources/entitlements.mac.plist b/src/packages/electron/resources/entitlements.mac.plist new file mode 100644 index 0000000..03649c4 --- /dev/null +++ b/src/packages/electron/resources/entitlements.mac.plist @@ -0,0 +1,35 @@ + + + + + + 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.cs.allow-dyld-environment-variables + + com.apple.security.device.audio-input + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + com.apple.security.inherit + + + diff --git a/src/packages/electron/resources/icons/app-icon.png b/src/packages/electron/resources/icons/app-icon.png new file mode 100644 index 0000000..c3f9a03 Binary files /dev/null and b/src/packages/electron/resources/icons/app-icon.png differ diff --git a/src/packages/electron/resources/icons/app-icon.svg b/src/packages/electron/resources/icons/app-icon.svg new file mode 100644 index 0000000..f271cda --- /dev/null +++ b/src/packages/electron/resources/icons/app-icon.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/packages/electron/resources/icons/dev-icon.icns b/src/packages/electron/resources/icons/dev-icon.icns new file mode 100644 index 0000000..04ac8d5 Binary files /dev/null and b/src/packages/electron/resources/icons/dev-icon.icns differ diff --git a/src/packages/electron/resources/icons/dev-icon.png b/src/packages/electron/resources/icons/dev-icon.png new file mode 100644 index 0000000..bab0ca2 Binary files /dev/null and b/src/packages/electron/resources/icons/dev-icon.png differ diff --git a/src/packages/electron/resources/icons/icon.icns b/src/packages/electron/resources/icons/icon.icns new file mode 100644 index 0000000..b6def08 Binary files /dev/null and b/src/packages/electron/resources/icons/icon.icns differ diff --git a/src/packages/electron/resources/icons/icon.png b/src/packages/electron/resources/icons/icon.png new file mode 100644 index 0000000..b9c1331 Binary files /dev/null and b/src/packages/electron/resources/icons/icon.png differ diff --git a/src/packages/electron/scripts/build-web-assets.mjs b/src/packages/electron/scripts/build-web-assets.mjs new file mode 100644 index 0000000..9cca352 --- /dev/null +++ b/src/packages/electron/scripts/build-web-assets.mjs @@ -0,0 +1,75 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const repoRoot = path.resolve(__dirname, '..', '..', '..'); +const webDir = path.join(repoRoot, 'packages', 'web'); +const electronDir = path.join(repoRoot, 'packages', 'electron'); + +const resourcesDir = path.join(electronDir, 'resources'); +const resourcesWebDistDir = path.join(resourcesDir, 'web-dist'); +const webDistDir = path.join(webDir, 'dist'); + +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(); + return resolved || 'bun'; +}; + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const removeDir = async (target) => { + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + await fs.rm(target, { recursive: true, force: true }); + return; + } catch (error) { + if (attempt === 4) throw error; + if (!['ENOTEMPTY', 'EBUSY', 'EPERM'].includes(error?.code)) throw error; + await sleep(100 * (attempt + 1)); + } + } +}; + +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 { + await fs.copyFile(from, to); + } + } +}; + +const bunExe = resolveBun(); + +console.log('[electron] building web UI dist...'); +run(bunExe, ['run', 'build'], webDir); + +console.log('[electron] staging packaged resources...'); +await fs.mkdir(resourcesDir, { recursive: true }); +const stagedWebDistDir = await fs.mkdtemp(path.join(resourcesDir, 'web-dist-staging-')); +await copyDir(webDistDir, stagedWebDistDir); +await removeDir(resourcesWebDistDir); +await fs.rename(stagedWebDistDir, resourcesWebDistDir); + +console.log(`[electron] web assets ready: ${resourcesWebDistDir}`); diff --git a/src/packages/electron/scripts/bundle-main.mjs b/src/packages/electron/scripts/bundle-main.mjs new file mode 100644 index 0000000..e8d06d3 --- /dev/null +++ b/src/packages/electron/scripts/bundle-main.mjs @@ -0,0 +1,44 @@ +/** + * Bundle main.mjs into a single file. Small electron-* helper deps are + * inlined; everything else — including the in-process web server + * (@openchamber/web) and native modules — stays external so it resolves + * from node_modules at runtime inside the packaged app. + * + * Why external matters: packages/web/server pulls in bun-pty, which has + * a top-level `import { dlopen } from "bun:ffi"`. If we inline it here, + * Node's ESM loader sees `bun:ffi` at package load time and crashes with + * ERR_UNSUPPORTED_ESM_URL_SCHEME before any runtime guard can skip it. + * Leaving @openchamber/web external means the conditional + * `if (isBunRuntime) await import('bun-pty')` stays dynamic and is never + * reached under Electron. + */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(__dirname, '..'); + +const result = await Bun.build({ + entrypoints: [path.join(root, 'main.mjs')], + outdir: path.join(root, 'dist-bundle'), + target: 'node', + format: 'esm', + external: [ + 'electron', + '@openchamber/web', + '@openchamber/web/*', + 'bun-pty', + 'node-pty', + 'better-sqlite3', + ], + minify: false, + sourcemap: 'none', + naming: '[name].mjs', +}); + +if (!result.success) { + for (const msg of result.logs) console.error(msg); + process.exit(1); +} + +console.log('[electron] main.mjs bundled -> dist-bundle/main.mjs'); diff --git a/src/packages/electron/scripts/electron-dev.mjs b/src/packages/electron/scripts/electron-dev.mjs new file mode 100644 index 0000000..46c2ce4 --- /dev/null +++ b/src/packages/electron/scripts/electron-dev.mjs @@ -0,0 +1,131 @@ +#!/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 electronDir = path.join(repoRoot, 'packages/electron'); + +function spawnProcess(command, args, options = {}) { + return spawn(command, args, { + cwd: repoRoot, + env: { ...process.env, OPENCHAMBER_ELECTRON_DEV: '1' }, + stdio: 'inherit', + detached: process.platform !== 'win32', + ...options, + }); +} + +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 devServer = spawnProcess('node', ['./scripts/dev-web-hmr.mjs'], { + env: { + ...process.env, + OPENCHAMBER_ELECTRON_DEV: '1', + OPENCHAMBER_HMR_UI_PORT: '5173', + OPENCHAMBER_HMR_API_PORT: '3901', + OPENCHAMBER_DISABLE_PWA_DEV: '1', + }, + }); + const electron = spawnProcess('npx', ['electron', './main.mjs'], { cwd: electronDir }); + + let cleaning = false; + const teardown = async (code) => { + if (cleaning) { + return; + } + cleaning = true; + + await Promise.all([stopChildTree(electron), stopChildTree(devServer)]); + process.exit(typeof code === 'number' ? code : 0); + }; + + const onChildExit = (label) => (code, signal) => { + if (code !== 0 || signal) { + console.warn(`[electron:dev] ${label} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}.`); + } + void teardown(code ?? 1); + }; + + devServer.on('exit', onChildExit('dev server')); + electron.on('exit', onChildExit('electron')); + devServer.on('error', (error) => { + console.error('[electron:dev] failed to start dev server:', error); + void teardown(1); + }); + electron.on('error', (error) => { + console.error('[electron:dev] failed to start electron:', error); + void teardown(1); + }); + + for (const [signal, exitCode] of Object.entries({ SIGINT: 130, SIGTERM: 143, SIGQUIT: 131, SIGHUP: 129 })) { + process.on(signal, () => { + void teardown(exitCode); + }); + } +} + +main().catch((error) => { + console.error('[electron:dev] unexpected error:', error); + process.exit(1); +}); diff --git a/src/packages/electron/scripts/finalize-latest-yml.mjs b/src/packages/electron/scripts/finalize-latest-yml.mjs new file mode 100644 index 0000000..68b2de1 --- /dev/null +++ b/src/packages/electron/scripts/finalize-latest-yml.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import path from 'node:path'; + +const dir = process.env.LATEST_YML_DIR; +const repo = process.env.GH_REPO; +const version = process.env.OPENCHAMBER_VERSION; + +if (!dir) throw new Error('LATEST_YML_DIR is required'); +if (!repo) throw new Error('GH_REPO is required'); +if (!version) throw new Error('OPENCHAMBER_VERSION is required'); + +const parse = (content) => { + const lines = content.split('\n'); + let releaseDate = ''; + let parsedVersion = ''; + const files = []; + let current; + + const flush = () => { + if (current?.url && current?.sha512 && current?.size) { + files.push(current); + } + current = undefined; + }; + + for (const line of lines) { + const trimmed = line.trim(); + const indented = line.startsWith(' ') || line.startsWith(' -'); + if (line.startsWith('version:')) { + parsedVersion = line.slice('version:'.length).trim(); + } else if (line.startsWith('releaseDate:')) { + releaseDate = line.slice('releaseDate:'.length).trim().replace(/^'|'$/g, ''); + } else if (trimmed.startsWith('- url:')) { + flush(); + current = { url: trimmed.slice('- url:'.length).trim() }; + } else if (indented && current && trimmed.startsWith('sha512:')) { + current.sha512 = trimmed.slice('sha512:'.length).trim(); + } else if (indented && current && trimmed.startsWith('size:')) { + current.size = Number(trimmed.slice('size:'.length).trim()); + } else if (indented && current && trimmed.startsWith('blockMapSize:')) { + current.blockMapSize = Number(trimmed.slice('blockMapSize:'.length).trim()); + } else if (!indented && current) { + flush(); + } + } + + flush(); + return { version: parsedVersion, releaseDate, files }; +}; + +const serialize = (data) => { + const lines = [`version: ${data.version}`, 'files:']; + for (const file of data.files) { + lines.push(` - url: ${file.url}`); + lines.push(` sha512: ${file.sha512}`); + lines.push(` size: ${file.size}`); + if (file.blockMapSize) { + lines.push(` blockMapSize: ${file.blockMapSize}`); + } + } + lines.push(`releaseDate: '${data.releaseDate}'`); + return `${lines.join('\n')}\n`; +}; + +const read = async (subdir, filename) => { + const filePath = path.join(dir, subdir, filename); + try { + return parse(await fs.readFile(filePath, 'utf8')); + } catch { + return null; + } +}; + +const output = {}; + +const winX64 = await read('latest-yml-x86_64-pc-windows-msvc', 'latest.yml'); +const winArm64 = await read('latest-yml-aarch64-pc-windows-msvc', 'latest.yml'); +if (winX64 || winArm64) { + const base = winArm64 || winX64; + output['latest.yml'] = serialize({ + version: base.version, + files: [...(winArm64?.files || []), ...(winX64?.files || [])], + releaseDate: base.releaseDate, + }); +} + +const linuxX64 = await read('latest-yml-x86_64-unknown-linux-gnu', 'latest-linux.yml'); +if (linuxX64) output['latest-linux.yml'] = serialize(linuxX64); + +const linuxArm64 = await read('latest-yml-aarch64-unknown-linux-gnu', 'latest-linux-arm64.yml'); +if (linuxArm64) output['latest-linux-arm64.yml'] = serialize(linuxArm64); + +const macX64 = await read('latest-yml-x86_64-apple-darwin', 'latest-mac.yml'); +const macArm64 = await read('latest-yml-aarch64-apple-darwin', 'latest-mac.yml'); +if (macX64 || macArm64) { + const base = macArm64 || macX64; + output['latest-mac.yml'] = serialize({ + version: base.version, + files: [...(macArm64?.files || []), ...(macX64?.files || [])], + releaseDate: base.releaseDate, + }); +} + +const tag = `v${version}`; +const tmp = process.env.RUNNER_TEMP || '/tmp'; +for (const [filename, content] of Object.entries(output)) { + const outputPath = path.join(tmp, filename); + await fs.writeFile(outputPath, content); + console.log(`prepared ${outputPath} for upload to ${repo} release ${tag}`); +} + +console.log('finalized latest yml files'); diff --git a/src/packages/electron/scripts/rebuild-native.mjs b/src/packages/electron/scripts/rebuild-native.mjs new file mode 100644 index 0000000..eb381df --- /dev/null +++ b/src/packages/electron/scripts/rebuild-native.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; +import { rebuild } from '@electron/rebuild'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const electronDir = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(electronDir, '..', '..'); +const require = createRequire(import.meta.url); + +const electronPkg = require('electron/package.json'); +const electronVersion = electronPkg.version; + +console.log(`[electron] rebuilding native modules against Electron ${electronVersion}...`); + +// Rebuild against the hoisted root node_modules (bun workspace layout). +// force=true re-links regardless of cached state; prebuild-install lookup is +// bypassed by @electron/rebuild in favor of direct node-gyp builds. +await rebuild({ + buildPath: repoRoot, + electronVersion, + force: true, + arch: process.env.ELECTRON_BUILDER_ARCH || process.arch, + onlyModules: ['better-sqlite3', 'node-pty', 'bun-pty'], +}); + +console.log('[electron] native modules rebuilt successfully'); diff --git a/src/packages/electron/ssh-manager.mjs b/src/packages/electron/ssh-manager.mjs new file mode 100644 index 0000000..e21ab19 --- /dev/null +++ b/src/packages/electron/ssh-manager.mjs @@ -0,0 +1,1245 @@ +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import net from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +const LOCAL_HOST_ID = 'local'; +const DEFAULT_CONNECTION_TIMEOUT_SEC = 60; +const DEFAULT_LOCAL_BIND_HOST = '127.0.0.1'; +const DEFAULT_CONTROL_PERSIST_SEC = 300; +const DEFAULT_READY_TIMEOUT_SEC = 30; +const DEFAULT_RECONNECT_MAX_ATTEMPTS = 5; +const MAX_LOG_LINES_PER_INSTANCE = 1200; + +const MONITOR_INITIAL_POLL_MS = 2000; +const MONITOR_STEADY_POLL_MS = 10000; +const MONITOR_STABILIZE_TICKS = 5; +const SSH_STATUS_EVENT = 'openchamber:ssh-instance-status'; + +const nowMillis = () => Date.now(); + +const shellQuote = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`; + +const hasGlobWildcard = (value) => /[*?]/.test(value); + +const expandSshIncludeToken = (token, baseDir) => { + const trimmed = String(token || '').trim(); + if (!trimmed) return []; + + const expandedHome = trimmed.startsWith('~/') + ? path.join(os.homedir(), trimmed.slice(2)) + : (trimmed === '~' ? os.homedir() : trimmed); + const resolved = path.isAbsolute(expandedHome) + ? expandedHome + : path.resolve(baseDir, expandedHome); + + if (!hasGlobWildcard(resolved)) { + return fs.existsSync(resolved) ? [resolved] : []; + } + + const dir = path.dirname(resolved); + const namePattern = path.basename(resolved); + if (hasGlobWildcard(dir) || !fs.existsSync(dir)) { + return []; + } + + const matcher = new RegExp(`^${namePattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*') + .replace(/\?/g, '.')}$`); + + try { + return fs.readdirSync(dir) + .filter((name) => matcher.test(name)) + .map((name) => path.join(dir, name)) + .filter((candidate) => fs.existsSync(candidate)) + .sort((left, right) => left.localeCompare(right)); + } catch { + return []; + } +}; + +const readJsonRoot = (settingsFilePath) => { + try { + const parsed = JSON.parse(fs.readFileSync(settingsFilePath, 'utf8')); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; + } catch { + return {}; + } +}; + +const writeJsonRoot = async (settingsFilePath, root) => { + await fsp.mkdir(path.dirname(settingsFilePath), { recursive: true }); + // Atomic write: concurrent readers (main.mjs, web server) would otherwise + // see partial JSON and readJsonRoot()'s catch would silently coerce to {}, + // causing the next read-modify-write to wipe the entire settings file. + const tmp = `${settingsFilePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + await fsp.writeFile(tmp, JSON.stringify(root, null, 2)); + await fsp.rename(tmp, settingsFilePath); +}; + +const defaultTrue = () => true; + +const sanitizeBindHost = (raw) => { + const trimmed = typeof raw === 'string' ? raw.trim() : ''; + if (!trimmed) return DEFAULT_LOCAL_BIND_HOST; + return ['127.0.0.1', 'localhost', '0.0.0.0'].includes(trimmed) ? trimmed : DEFAULT_LOCAL_BIND_HOST; +}; + +const splitShellWords = (input) => { + const tokens = []; + let current = ''; + let inSingle = false; + let inDouble = false; + const chars = [...String(input)]; + + for (let index = 0; index < chars.length; index += 1) { + const ch = chars[index]; + if (ch === '\\' && !inSingle) { + index += 1; + if (index < chars.length) current += chars[index]; + continue; + } + if (ch === '\'' && !inDouble) { + inSingle = !inSingle; + continue; + } + if (ch === '"' && !inSingle) { + inDouble = !inDouble; + continue; + } + if (/\s/.test(ch) && !inSingle && !inDouble) { + if (current) { + tokens.push(current); + current = ''; + } + continue; + } + current += ch; + } + + if (inSingle || inDouble) { + throw new Error('Unclosed quote in SSH command'); + } + if (current) tokens.push(current); + return tokens; +}; + +const isDisallowedPrimaryFlag = (token) => { + return ['-M', '-S', '-O', '-N', '-t', '-T', '-f', '-G', '-W', '-v', '-V', '-q', '-n', '-s', '-e', '-E', '-g'].includes(token); +}; + +const hasDisallowedOOption = (value) => { + const lower = String(value).trim().toLowerCase(); + return ['controlmaster', 'controlpath', 'controlpersist', 'batchmode', 'proxycommand'].some((prefix) => lower.startsWith(prefix)); +}; + +const parseSshCommand = (raw) => { + const tokens = splitShellWords(raw); + if (tokens.length === 0) { + throw new Error('SSH command is empty'); + } + + if (tokens[0] === 'ssh') { + tokens.shift(); + } + + if (tokens.length === 0) { + throw new Error('SSH command must include destination'); + } + + const allowedFlags = new Set(['-4', '-6', '-A', '-a', '-C', '-K', '-k', '-X', '-x', '-Y', '-y']); + const allowedWithValues = ['-B', '-b', '-c', '-D', '-F', '-I', '-i', '-J', '-l', '-m', '-o', '-P', '-p', '-R']; + + const args = []; + let destination = null; + for (let index = 0; index < tokens.length;) { + const token = tokens[index]; + if (destination) { + throw new Error(`SSH command has unsupported trailing argument: ${token}`); + } + + if (!token.startsWith('-')) { + destination = token.trim(); + index += 1; + continue; + } + + if (isDisallowedPrimaryFlag(token)) { + throw new Error(`SSH option ${token} is not allowed`); + } + + if (allowedFlags.has(token)) { + args.push(token); + index += 1; + continue; + } + + let matched = false; + for (const option of allowedWithValues) { + if (token === option) { + const value = tokens[index + 1]; + if (!value) { + throw new Error(`SSH option ${option} requires a value`); + } + if (option === '-o' && hasDisallowedOOption(value)) { + throw new Error(`SSH option -o ${value} is not allowed`); + } + args.push(token, value); + index += 2; + matched = true; + break; + } + + if (token.startsWith(option) && token.length > option.length) { + const value = token.slice(option.length); + if (option === '-o' && hasDisallowedOOption(value)) { + throw new Error(`SSH option -o ${value} is not allowed`); + } + args.push(token); + index += 1; + matched = true; + break; + } + } + + if (!matched) { + throw new Error(`Unsupported SSH option: ${token}`); + } + } + + if (!destination) { + throw new Error('SSH command must include destination'); + } + + return { destination, args }; +}; + +const runOutput = async (command, args, options = {}) => { + return await new Promise((resolve, reject) => { + const child = spawn(command, args, { + stdio: ['pipe', 'pipe', 'pipe'], + ...options, + }); + + let stdout = ''; + let stderr = ''; + child.stdout?.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.stderr?.on('data', (chunk) => { + stderr += chunk.toString(); + }); + child.on('error', reject); + child.on('close', (code) => { + resolve({ code: typeof code === 'number' ? code : -1, stdout, stderr }); + }); + }); +}; + +const buildSshArgs = (parsed, preDestinationArgs = [], remoteCommand = null) => { + const args = [...parsed.args, ...preDestinationArgs, parsed.destination]; + if (remoteCommand) args.push(remoteCommand); + return args; +}; + +const runRemoteCommand = async (parsed, controlPath, script, timeoutSec = DEFAULT_CONNECTION_TIMEOUT_SEC) => { + const args = buildSshArgs(parsed, [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-o', `ConnectTimeout=${timeoutSec}`, + '-T', + ], `sh -lc ${shellQuote(script)}`); + const { code, stdout, stderr } = await runOutput('ssh', args); + if (code !== 0) { + throw new Error((stderr || stdout || 'Remote command failed').trim()); + } + return stdout; +}; + +const controlMasterOperation = async (parsed, controlPath, op) => { + return await runOutput('ssh', buildSshArgs(parsed, [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-o', 'BatchMode=yes', + '-o', 'ConnectTimeout=3', + '-O', op, + ])); +}; + +const isControlMasterAlive = async (parsed, controlPath) => { + const { code } = await controlMasterOperation(parsed, controlPath, 'check'); + return code === 0; +}; + +const stopControlMasterBestEffort = async (parsed, controlPath) => { + try { + await controlMasterOperation(parsed, controlPath, 'exit'); + } catch { + } +}; + +const askpassScriptContent = () => `#!/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 + +if command -v osascript >/dev/null 2>&1; then + /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 + exit $? +fi + +printf '%s\\n' "$DEFAULT_ANSWER" +`; + +const writeAskpassScript = async (scriptPath) => { + await fsp.writeFile(scriptPath, askpassScriptContent(), { mode: 0o700 }); + await fsp.chmod(scriptPath, 0o700); +}; + +const randomPortCandidate = (seed) => { + let hash = 0; + const source = `${seed}:${Date.now()}`; + for (let index = 0; index < source.length; index += 1) { + hash = ((hash << 5) - hash + source.charCodeAt(index)) | 0; + } + const base = 20000; + const span = 30000; + return base + Math.abs(hash % span); +}; + +const pickUnusedLocalPort = async () => { + return await new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : 0; + server.close((error) => error ? reject(error) : resolve(port)); + }); + server.on('error', reject); + }); +}; + +const isLocalPortAvailable = async (bindHost, port) => { + return await new Promise((resolve) => { + const server = net.createServer(); + server.once('error', () => resolve(false)); + server.listen(port, bindHost, () => { + server.close(() => resolve(true)); + }); + }); +}; + +const isLocalTunnelReachable = async (localPort) => { + return await new Promise((resolve) => { + const socket = net.createConnection({ host: '127.0.0.1', port: localPort }); + const finish = (value) => { + socket.destroy(); + resolve(value); + }; + socket.once('connect', () => finish(true)); + socket.once('error', () => finish(false)); + socket.setTimeout(500, () => finish(false)); + }); +}; + +const waitLocalForwardReady = async (localPort) => { + const deadline = Date.now() + (DEFAULT_READY_TIMEOUT_SEC * 1000); + let pollMs = 250; + while (Date.now() < deadline) { + try { + const response = await fetch(`http://127.0.0.1:${localPort}/health`, { signal: AbortSignal.timeout(1000) }); + if (response.ok || response.status === 401) { + return; + } + } catch { + } + await new Promise((resolve) => setTimeout(resolve, pollMs)); + pollMs = Math.min(pollMs * 2, 2000); + } + throw new Error('Timed out waiting for forwarded OpenChamber health'); +}; + +const parseVersionToken = (raw) => { + for (const token of String(raw).split(/\s+/)) { + let candidate = token.trim().replace(/^v/, ''); + candidate = candidate.replace(/[,)]+$/g, ''); + const parts = candidate.split('.'); + if (parts.length >= 2 && parts.every((part) => /^\d+$/.test(part))) { + return candidate; + } + } + return null; +}; + +const parseProbeStatusLine = (line, prefix) => { + if (!line || !line.startsWith(prefix)) return null; + const value = Number.parseInt(line.slice(prefix.length).trim(), 10); + return Number.isFinite(value) ? value : null; +}; + +const isAuthHttpStatus = (status) => status === 401 || status === 403; +const isLivenessHttpStatus = (status) => (status >= 200 && status <= 299) || isAuthHttpStatus(status); + +export class ElectronSshManager { + constructor(options) { + this.settingsFilePath = options.settingsFilePath; + this.appVersion = options.appVersion; + this.emit = options.emit; + this.logs = new Map(); + this.statuses = new Map(); + this.sessions = new Map(); + this.monitorTimers = new Map(); + this.reconnectAttempts = new Map(); + this.connectAttempts = new Map(); + this.connecting = new Map(); + } + + appendLogWithLevel(id, level, message) { + const line = `[${nowMillis()}] [${level}] ${message}`; + const current = this.logs.get(id) || []; + current.push(line); + if (current.length > MAX_LOG_LINES_PER_INSTANCE) { + current.splice(0, current.length - MAX_LOG_LINES_PER_INSTANCE); + } + this.logs.set(id, current); + } + + appendLog(id, message) { + this.appendLogWithLevel(id, 'INFO', message); + } + + appendAttemptSeparator(id, connectAttempt, retryAttempt) { + const scope = retryAttempt > 0 ? `retry ${retryAttempt}` : 'manual'; + this.appendLogWithLevel(id, 'INFO', `---------------- attempt #${connectAttempt} (${scope}) ----------------`); + } + + statusSnapshotForInstance(id) { + return this.statuses.get(id) || { + id, + phase: 'idle', + detail: null, + localUrl: null, + localPort: null, + remotePort: null, + startedByUs: false, + retryAttempt: 0, + requiresUserAction: false, + updatedAtMs: nowMillis(), + }; + } + + setStatus(id, phase, detail = null, localUrl = null, localPort = null, remotePort = null, startedByUs = false, retryAttempt = 0, requiresUserAction = false) { + const level = phase === 'error' ? 'ERROR' : (phase === 'degraded' ? 'WARN' : 'INFO'); + this.appendLogWithLevel( + id, + level, + `phase=${JSON.stringify(phase)} detail=${detail || ''} retry=${retryAttempt} requires_user_action=${requiresUserAction}`, + ); + + const status = { + id, + phase, + detail, + localUrl, + localPort, + remotePort, + startedByUs, + retryAttempt, + requiresUserAction, + updatedAtMs: nowMillis(), + }; + this.statuses.set(id, status); + this.emit(SSH_STATUS_EVENT, status); + } + + clearRetryAttempt(id) { + this.reconnectAttempts.delete(id); + } + + nextRetryAttempt(id) { + const next = (this.reconnectAttempts.get(id) || 0) + 1; + this.reconnectAttempts.set(id, next); + return next; + } + + currentRetryAttempt(id) { + return this.reconnectAttempts.get(id) || 0; + } + + nextConnectAttempt(id) { + const next = (this.connectAttempts.get(id) || 0) + 1; + this.connectAttempts.set(id, next); + return next; + } + + logsForInstance(id, limit = 200) { + const lines = [...(this.logs.get(id) || [])]; + return limit > 0 && lines.length > limit ? lines.slice(-limit) : lines; + } + + clearLogsForInstance(id) { + this.logs.delete(id); + } + + parseSshConfigCandidates(filePath, source, visited = new Set()) { + const resolvedPath = path.resolve(filePath); + if (visited.has(resolvedPath) || !fs.existsSync(resolvedPath)) return []; + visited.add(resolvedPath); + + const content = fs.readFileSync(resolvedPath, 'utf8'); + const candidates = []; + const baseDir = path.dirname(resolvedPath); + for (const line of content.split(/\r?\n/)) { + const trimmed = (line.split('#')[0] || '').trim(); + if (!trimmed) continue; + + if (/^include(?:\s|$)/i.test(trimmed)) { + const includeExpr = trimmed.replace(/^include\s+/i, '').trim(); + if (!includeExpr) continue; + let includeTokens = []; + try { + includeTokens = splitShellWords(includeExpr); + } catch { + includeTokens = includeExpr.split(/\s+/).filter(Boolean); + } + for (const includeToken of includeTokens) { + const includePaths = expandSshIncludeToken(includeToken, baseDir); + for (const includePath of includePaths) { + candidates.push(...this.parseSshConfigCandidates(includePath, source, visited)); + } + } + continue; + } + + if (!/^host(?:\s|$)/i.test(trimmed)) continue; + const rest = trimmed.replace(/^host\s+/i, '').trim(); + if (!rest) continue; + for (const token of rest.split(/\s+/)) { + const host = token.trim(); + if (!host || host.startsWith('!') || host === '*') continue; + candidates.push({ + host, + pattern: /[*?]/.test(host), + source, + sshCommand: `ssh ${host}`, + }); + } + } + return candidates; + } + + async importHosts() { + const candidates = [ + ...this.parseSshConfigCandidates(path.join(os.homedir(), '.ssh', 'config'), 'user'), + ...this.parseSshConfigCandidates('/etc/ssh/ssh_config', 'global'), + ]; + const seen = new Set(); + return candidates + .filter((item) => !seen.has(item.host) && seen.add(item.host)) + .sort((left, right) => left.host.localeCompare(right.host)); + } + + readInstances() { + const root = readJsonRoot(this.settingsFilePath); + return { instances: Array.isArray(root.desktopSshInstances) ? root.desktopSshInstances : [] }; + } + + async setInstances(config) { + const root = readJsonRoot(this.settingsFilePath); + const instances = Array.isArray(config?.instances) ? config.instances.map((instance) => this.sanitizeInstance(instance)) : []; + root.desktopSshInstances = instances; + + const previousIds = new Set((Array.isArray(root.desktopHosts) ? root.desktopHosts : []).map((entry) => String(entry?.id || '').trim()).filter(Boolean)); + const hosts = Array.isArray(root.desktopHosts) ? root.desktopHosts.filter(Boolean) : []; + const nextIds = new Set(instances.map((instance) => instance.id)); + + const filteredHosts = hosts.filter((entry) => { + const id = String(entry?.id || '').trim(); + return id && !(previousIds.has(id) && !nextIds.has(id)); + }); + + for (const instance of instances) { + const label = instance.nickname?.trim() || instance.sshParsed?.destination || instance.id; + const existing = filteredHosts.find((entry) => entry?.id === instance.id); + if (existing) { + existing.label = label; + if (!existing.url || !String(existing.url).trim()) { + existing.url = 'http://127.0.0.1/'; + } + } else { + filteredHosts.push({ id: instance.id, label, url: 'http://127.0.0.1/' }); + } + } + + root.desktopHosts = filteredHosts; + if (typeof root.desktopDefaultHostId === 'string' && previousIds.has(root.desktopDefaultHostId) && !nextIds.has(root.desktopDefaultHostId)) { + root.desktopDefaultHostId = LOCAL_HOST_ID; + } + + await writeJsonRoot(this.settingsFilePath, root); + } + + sanitizeStoredSecret(secret) { + if (!secret || typeof secret !== 'object') return undefined; + return { + enabled: Boolean(secret.enabled), + store: secret.store === 'settings' ? 'settings' : 'never', + ...(typeof secret.value === 'string' && secret.value.trim() ? { value: secret.value } : {}), + }; + } + + sanitizeForward(forward) { + const id = typeof forward?.id === 'string' ? forward.id.trim() : ''; + if (!id) return null; + const type = forward?.type === 'remote' || forward?.type === 'dynamic' ? forward.type : 'local'; + const normalized = { + id, + enabled: forward?.enabled !== false, + type, + ...(forward?.localHost ? { localHost: sanitizeBindHost(forward.localHost) } : {}), + ...(Number.isFinite(forward?.localPort) ? { localPort: Number(forward.localPort) } : {}), + ...(forward?.remoteHost ? { remoteHost: String(forward.remoteHost).trim() || '127.0.0.1' } : {}), + ...(Number.isFinite(forward?.remotePort) ? { remotePort: Number(forward.remotePort) } : {}), + }; + + if (type === 'local' || type === 'remote') { + if (!normalized.localPort || !normalized.remotePort) return null; + normalized.remoteHost = normalized.remoteHost || '127.0.0.1'; + normalized.localHost = normalized.localHost || '127.0.0.1'; + } + if (type === 'dynamic' && !normalized.localPort) { + return null; + } + return normalized; + } + + sanitizeInstance(instance) { + const id = typeof instance?.id === 'string' ? instance.id.trim() : ''; + const sshCommand = typeof instance?.sshCommand === 'string' ? instance.sshCommand.trim() : ''; + if (!id || id === LOCAL_HOST_ID) { + throw new Error('SSH instance id is required'); + } + if (!sshCommand) { + throw new Error('SSH command is required'); + } + + const parsed = parseSshCommand(sshCommand); + const seen = new Set(); + const portForwards = Array.isArray(instance?.portForwards) + ? instance.portForwards + .map((forward) => this.sanitizeForward(forward)) + .filter((forward) => forward && !seen.has(forward.id) && seen.add(forward.id)) + : []; + + return { + id, + ...(typeof instance?.nickname === 'string' && instance.nickname.trim() ? { nickname: instance.nickname.trim() } : {}), + sshCommand, + sshParsed: parsed, + connectionTimeoutSec: Number.isFinite(instance?.connectionTimeoutSec) && Number(instance.connectionTimeoutSec) > 0 + ? Number(instance.connectionTimeoutSec) + : DEFAULT_CONNECTION_TIMEOUT_SEC, + remoteOpenchamber: { + mode: instance?.remoteOpenchamber?.mode === 'external' ? 'external' : 'managed', + keepRunning: instance?.remoteOpenchamber?.keepRunning !== false, + ...(Number.isFinite(instance?.remoteOpenchamber?.preferredPort) ? { preferredPort: Number(instance.remoteOpenchamber.preferredPort) } : {}), + installMethod: ['npm', 'bun', 'download_release', 'upload_bundle'].includes(instance?.remoteOpenchamber?.installMethod) + ? instance.remoteOpenchamber.installMethod + : 'bun', + uploadBundleOverSsh: Boolean(instance?.remoteOpenchamber?.uploadBundleOverSsh), + }, + localForward: { + bindHost: sanitizeBindHost(instance?.localForward?.bindHost), + ...(Number.isFinite(instance?.localForward?.preferredLocalPort) ? { preferredLocalPort: Number(instance.localForward.preferredLocalPort) } : {}), + }, + auth: { + ...(this.sanitizeStoredSecret(instance?.auth?.sshPassword) ? { sshPassword: this.sanitizeStoredSecret(instance.auth.sshPassword) } : {}), + ...(this.sanitizeStoredSecret(instance?.auth?.openchamberPassword) ? { openchamberPassword: this.sanitizeStoredSecret(instance.auth.openchamberPassword) } : {}), + }, + portForwards, + }; + } + + async updateHostUrl(instanceId, label, localUrl) { + const root = readJsonRoot(this.settingsFilePath); + const hosts = Array.isArray(root.desktopHosts) ? root.desktopHosts : []; + const existing = hosts.find((entry) => entry?.id === instanceId); + if (existing) { + existing.label = label; + existing.url = localUrl; + } else { + hosts.push({ id: instanceId, label, url: localUrl }); + } + root.desktopHosts = hosts; + await writeJsonRoot(this.settingsFilePath, root); + } + + async persistLocalPort(instanceId, localPort) { + const root = readJsonRoot(this.settingsFilePath); + const instances = Array.isArray(root.desktopSshInstances) ? root.desktopSshInstances : []; + for (const instance of instances) { + if (instance?.id !== instanceId) continue; + instance.localForward = instance.localForward && typeof instance.localForward === 'object' ? instance.localForward : {}; + instance.localForward.preferredLocalPort = localPort; + } + root.desktopSshInstances = instances; + await writeJsonRoot(this.settingsFilePath, root); + } + + async resolveSshConfig(parsed) { + const { code, stdout, stderr } = await runOutput('ssh', buildSshArgs(parsed, ['-G'])); + if (code !== 0) { + throw new Error(stderr.trim() || 'Failed to resolve SSH config'); + } + const map = new Map(); + for (const line of stdout.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed) continue; + const [key, ...rest] = trimmed.split(' '); + if (!key || rest.length === 0) continue; + map.set(key.toLowerCase(), rest.join(' ').trim()); + } + return map; + } + + ensureSessionDir(id) { + const base = path.join(path.dirname(this.settingsFilePath), 'ssh', id); + fs.mkdirSync(base, { recursive: true }); + return base; + } + + controlPathForInstance(id) { + let hash = 0; + for (const char of id) { + hash = ((hash << 5) - hash + char.charCodeAt(0)) | 0; + } + return path.join(os.tmpdir(), `ocssh-${Math.abs(hash).toString(16)}.sock`); + } + + async spawnMasterProcess(parsed, controlPath, askpassPath, sshPassword) { + const child = spawn('ssh', buildSshArgs(parsed, [ + '-o', 'ControlMaster=yes', + '-o', `ControlPath=${controlPath}`, + '-o', `ControlPersist=${DEFAULT_CONTROL_PERSIST_SEC}`, + '-N', + ]), { + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...process.env, + SSH_ASKPASS_REQUIRE: 'force', + SSH_ASKPASS: askpassPath, + DISPLAY: '1', + ...(sshPassword ? { OPENCHAMBER_SSH_ASKPASS_VALUE: sshPassword.trim() } : {}), + }, + }); + return child; + } + + async waitForMasterReady(parsed, controlPath, timeoutSec, master) { + const deadline = Date.now() + (timeoutSec * 1000); + let pollMs = 250; + while (Date.now() < deadline) { + const { code } = await runOutput('ssh', buildSshArgs(parsed, [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-O', 'check', + ])); + if (code === 0) return; + + const exited = master.exitCode; + if (typeof exited === 'number') { + throw new Error('SSH master process exited before ready'); + } + await new Promise((resolve) => setTimeout(resolve, pollMs)); + pollMs = Math.min(pollMs * 2, 2000); + } + throw new Error('SSH ControlMaster connection timed out'); + } + + configuredOpenChamberPassword(instance) { + const secret = instance?.auth?.openchamberPassword; + return secret?.enabled && typeof secret.value === 'string' && secret.value.trim() ? secret.value.trim() : null; + } + + async remoteCommandExists(parsed, controlPath, commandName) { + try { + const output = await runRemoteCommand(parsed, controlPath, `command -v ${commandName} >/dev/null 2>&1 && echo yes || echo no`); + return output.trim() === 'yes'; + } catch { + return false; + } + } + + async currentRemoteOpenChamberVersion(parsed, controlPath) { + try { + const output = await runRemoteCommand(parsed, controlPath, 'openchamber --version 2>/dev/null || true'); + return parseVersionToken(output); + } catch { + return null; + } + } + + async installOpenChamberManaged(parsed, controlPath, version, preferred) { + const hasBun = await this.remoteCommandExists(parsed, controlPath, 'bun'); + const hasNpm = await this.remoteCommandExists(parsed, controlPath, 'npm'); + const commands = []; + + if (preferred === 'bun') { + if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`); + if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`); + } else if (preferred === 'npm') { + if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`); + if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`); + } else { + if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`); + if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`); + } + + if (commands.length === 0) { + throw new Error('Remote host has neither bun nor npm available'); + } + + let lastError = null; + for (const command of commands) { + try { + await runRemoteCommand(parsed, controlPath, command); + return; + } catch (error) { + lastError = error; + } + } + throw lastError || new Error('Failed to install OpenChamber on remote host'); + } + + async probeRemoteSystemInfo(parsed, controlPath, port, openchamberPassword) { + const authPayload = openchamberPassword ? JSON.stringify({ password: openchamberPassword }) : '{}'; + const authEnabled = openchamberPassword ? '1' : '0'; + const script = `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 [ "${authEnabled}" = "1" ]; then AUTH_STATUS="$(curl -sS --max-time 3 -o /dev/null -w '%{http_code}' -c "$COOKIE_FILE" -H 'content-type: application/json' --data ${shellQuote(authPayload)} 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`; + const output = await runRemoteCommand(parsed, controlPath, script); + const lines = output.split(/\r?\n/); + const infoStatus = parseProbeStatusLine(lines[0], 'INFO_STATUS=') || 0; + const authStatus = parseProbeStatusLine(lines[1], 'AUTH_STATUS=') || 0; + const healthStatus = parseProbeStatusLine(lines[2], 'HEALTH_STATUS=') || 0; + const body = lines.slice(3).join('\n'); + + if (isLivenessHttpStatus(infoStatus)) { + if (isAuthHttpStatus(infoStatus)) { + if (openchamberPassword && authStatus !== 200) { + throw new Error(`Remote OpenChamber requires UI authentication and configured password was rejected (auth status ${authStatus})`); + } + if (isLivenessHttpStatus(healthStatus)) return {}; + throw new Error('Remote OpenChamber requires UI authentication on /api/system/info; configure OpenChamber UI password'); + } + } else if (isLivenessHttpStatus(healthStatus)) { + return {}; + } else { + throw new Error(`Remote OpenChamber probe failed (info status ${infoStatus}, health status ${healthStatus})`); + } + + try { + return JSON.parse(body); + } catch { + return {}; + } + } + + async remoteServerRunning(parsed, controlPath, port, openchamberPassword) { + try { + await this.probeRemoteSystemInfo(parsed, controlPath, port, openchamberPassword); + return true; + } catch { + return false; + } + } + + async startRemoteServerManaged(parsed, controlPath, instance, desiredPort) { + let envPrefix = 'OPENCHAMBER_RUNTIME=ssh-remote'; + const secret = this.configuredOpenChamberPassword(instance); + if (secret) { + envPrefix += ` OPENCHAMBER_UI_PASSWORD=${shellQuote(secret)}`; + } + const output = await runRemoteCommand(parsed, controlPath, `${envPrefix} openchamber serve --daemon --hostname 127.0.0.1 --port ${desiredPort}`); + const port = output.split(/\s+/).map((token) => Number.parseInt(token, 10)).find((value) => Number.isFinite(value)); + return port || desiredPort; + } + + async stopRemoteServerBestEffort(parsed, controlPath, remotePort) { + try { + await runRemoteCommand( + parsed, + controlPath, + `if command -v curl >/dev/null 2>&1; then curl -fsS -X POST http://127.0.0.1:${remotePort}/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:${remotePort}/api/system/shutdown >/dev/null 2>&1 || true; fi`, + ); + } catch { + } + } + + async spawnMainForward(parsed, controlPath, bindHost, localPort, remotePort) { + return spawn('ssh', buildSshArgs(parsed, [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-N', + '-L', `${bindHost}:${localPort}:127.0.0.1:${remotePort}`, + ]), { + stdio: ['ignore', 'ignore', 'pipe'], + }); + } + + async spawnExtraForward(parsed, controlPath, forward) { + const args = [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-O', 'forward', + ]; + if (forward.type === 'local') { + args.push('-L', `${forward.localHost || '127.0.0.1'}:${forward.localPort}:${forward.remoteHost || '127.0.0.1'}:${forward.remotePort}`); + } else if (forward.type === 'remote') { + args.push('-R', `${forward.remoteHost || '127.0.0.1'}:${forward.remotePort}:${forward.localHost || '127.0.0.1'}:${forward.localPort}`); + } else { + args.push('-D', `${forward.localHost || '127.0.0.1'}:${forward.localPort}`); + } + const { code, stdout, stderr } = await runOutput('ssh', buildSshArgs(parsed, args)); + if (code !== 0) { + throw new Error((stderr || stdout || `Failed to configure extra SSH forward ${forward.id}`).trim()); + } + } + + async ensureRemoteServer(instance, parsed, controlPath) { + if (instance.remoteOpenchamber.mode === 'external') { + if (!instance.remoteOpenchamber.preferredPort) { + throw new Error('External mode requires a preferred remote OpenChamber port'); + } + const port = instance.remoteOpenchamber.preferredPort; + this.setStatus(instance.id, 'server_detecting', 'Probing external OpenChamber server', null, null, port, false, 0, false); + await this.probeRemoteSystemInfo(parsed, controlPath, port, this.configuredOpenChamberPassword(instance)); + return { remotePort: port, startedByUs: false }; + } + + this.setStatus(instance.id, 'remote_probe', 'Checking remote OpenChamber installation'); + const installedVersion = await this.currentRemoteOpenChamberVersion(parsed, controlPath); + if (!installedVersion) { + this.setStatus(instance.id, 'installing', 'Installing OpenChamber on remote host'); + await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod); + } else if (installedVersion !== this.appVersion) { + this.setStatus(instance.id, 'updating', `Updating remote OpenChamber from ${installedVersion} to ${this.appVersion}`); + await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod); + } + + this.setStatus(instance.id, 'server_detecting', 'Detecting managed OpenChamber server'); + let remotePort = instance.remoteOpenchamber.preferredPort || null; + let startedByUs = false; + if (remotePort && !(await this.remoteServerRunning(parsed, controlPath, remotePort, this.configuredOpenChamberPassword(instance)))) { + remotePort = null; + } + if (!remotePort) { + this.setStatus(instance.id, 'server_starting', 'Starting managed OpenChamber server'); + const desiredPort = instance.remoteOpenchamber.preferredPort || randomPortCandidate(instance.id); + remotePort = await this.startRemoteServerManaged(parsed, controlPath, instance, desiredPort); + startedByUs = true; + } + if (!(await this.remoteServerRunning(parsed, controlPath, remotePort, this.configuredOpenChamberPassword(instance)))) { + throw new Error('Managed OpenChamber server failed to become reachable'); + } + return { remotePort, startedByUs }; + } + + async disconnectInternal(id, reportIdle) { + const timer = this.monitorTimers.get(id); + if (timer) { + clearTimeout(timer); + this.monitorTimers.delete(id); + } + + const session = this.sessions.get(id); + this.sessions.delete(id); + + if (session) { + if (session.startedByUs && session.instance.remoteOpenchamber.mode === 'managed' && !session.instance.remoteOpenchamber.keepRunning) { + await this.stopRemoteServerBestEffort(session.parsed, session.controlPath, session.remotePort); + } + await stopControlMasterBestEffort(session.parsed, session.controlPath); + for (const child of [session.mainForward, session.master]) { + try { + child.kill('SIGTERM'); + } catch { + } + } + try { + await fsp.rm(session.controlPath, { force: true }); + } catch { + } + try { + await fsp.rm(path.join(session.sessionDir, 'askpass.sh'), { force: true }); + } catch { + } + } + + this.clearRetryAttempt(id); + if (reportIdle) { + this.setStatus(id, 'idle', null, null, null, null, false, 0, false); + } + } + + async connectBlocking(instance) { + const id = instance.id; + this.setStatus(id, 'config_resolved', 'Resolving SSH command'); + const parsed = instance.sshParsed || parseSshCommand(instance.sshCommand); + await this.resolveSshConfig(parsed); + + this.setStatus(id, 'auth_check', 'Checking SSH connectivity'); + const sessionDir = this.ensureSessionDir(id); + const controlPath = this.controlPathForInstance(id); + try { await fsp.rm(controlPath, { force: true }); } catch {} + const askpassPath = path.join(sessionDir, 'askpass.sh'); + await writeAskpassScript(askpassPath); + + this.setStatus(id, 'master_connecting', 'Establishing SSH ControlMaster'); + const sshPassword = instance.auth?.sshPassword?.enabled ? instance.auth.sshPassword.value : null; + const master = await this.spawnMasterProcess(parsed, controlPath, askpassPath, sshPassword); + await this.waitForMasterReady(parsed, controlPath, instance.connectionTimeoutSec || DEFAULT_CONNECTION_TIMEOUT_SEC, master); + + this.setStatus(id, 'remote_probe', 'Probing remote platform'); + const remoteOs = (await runRemoteCommand(parsed, controlPath, 'uname -s', instance.connectionTimeoutSec || DEFAULT_CONNECTION_TIMEOUT_SEC)).trim().toLowerCase(); + if (!['linux', 'darwin'].includes(remoteOs)) { + master.kill('SIGTERM'); + throw new Error(`Unsupported remote OS: ${remoteOs}`); + } + + const { remotePort, startedByUs } = await this.ensureRemoteServer(instance, parsed, controlPath); + this.setStatus(id, 'forwarding', 'Setting up port forwards', null, null, remotePort, startedByUs, 0, false); + + const bindHost = sanitizeBindHost(instance.localForward?.bindHost); + let localPort = Number(instance.localForward?.preferredLocalPort) || 0; + if (!localPort) { + localPort = await pickUnusedLocalPort(); + } + if (!(await isLocalPortAvailable(bindHost, localPort))) { + localPort = await pickUnusedLocalPort(); + } + + const mainForward = await this.spawnMainForward(parsed, controlPath, bindHost, localPort, remotePort); + let mainForwardDetached = false; + await new Promise((resolve) => setTimeout(resolve, 250)); + if (typeof mainForward.exitCode === 'number') { + if (mainForward.exitCode === 0) { + mainForwardDetached = true; + this.appendLogWithLevel(id, 'INFO', 'Main tunnel helper exited after ControlMaster handoff'); + } else { + master.kill('SIGTERM'); + throw new Error(`Failed to start main port forward (status: ${mainForward.exitCode})`); + } + } + + const extraErrors = []; + for (const forward of instance.portForwards.filter((item) => item.enabled)) { + try { + await this.spawnExtraForward(parsed, controlPath, forward); + if (forward.type === 'local' && forward.localPort) { + await new Promise((resolve) => setTimeout(resolve, 100)); + if (!(await isLocalTunnelReachable(forward.localPort))) { + extraErrors.push(`${forward.id}: local listener 127.0.0.1:${forward.localPort} is not reachable`); + } + } + } catch (error) { + extraErrors.push(`${forward.id}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + await waitLocalForwardReady(localPort); + + const localUrl = `http://127.0.0.1:${localPort}`; + const label = instance.nickname?.trim() || parsed.destination || id; + await this.updateHostUrl(id, label, localUrl); + if (instance.localForward?.preferredLocalPort !== localPort) { + await this.persistLocalPort(id, localPort); + } + + this.sessions.set(id, { + instance, + parsed, + sessionDir, + controlPath, + localPort, + remotePort, + startedByUs, + master, + masterDetached: false, + mainForward, + mainForwardDetached, + }); + + this.clearRetryAttempt(id); + this.setStatus( + id, + 'ready', + extraErrors.length === 0 ? 'SSH instance is ready' : `SSH instance is ready with forward warnings: ${extraErrors.join('; ')}`, + localUrl, + localPort, + remotePort, + startedByUs, + 0, + false, + ); + this.spawnMonitor(id); + } + + spawnMonitor(id) { + const existing = this.monitorTimers.get(id); + if (existing) clearTimeout(existing); + let healthyTicks = 0; + const tick = async () => { + const session = this.sessions.get(id); + if (!session) { + this.monitorTimers.delete(id); + return; + } + + let droppedReason = null; + let detachedNotice = null; + + if (!session.mainForwardDetached) { + if (typeof session.mainForward.exitCode === 'number') { + if (session.mainForward.exitCode === 0) { + session.mainForwardDetached = true; + detachedNotice = 'Main tunnel helper exited after ControlMaster handoff'; + } else { + droppedReason = `Main SSH forward exited (${session.mainForward.exitCode})`; + } + } + } + + if (!droppedReason) { + if (session.mainForwardDetached) { + // Fast path: cheap TCP probe before expensive SSH subprocess + if (await isLocalTunnelReachable(session.localPort)) { + // Tunnel alive — skip SSH check + } else if (!await isControlMasterAlive(session.parsed, session.controlPath)) { + droppedReason = 'SSH ControlMaster is not reachable'; + } else { + detachedNotice = 'Local tunnel unreachable but ControlMaster is alive'; + } + } + } + + if (detachedNotice) { + this.appendLogWithLevel(id, 'INFO', detachedNotice); + } + if (!droppedReason) { + healthyTicks++; + const pollMs = healthyTicks >= MONITOR_STABILIZE_TICKS ? MONITOR_STEADY_POLL_MS : MONITOR_INITIAL_POLL_MS; + this.monitorTimers.set(id, setTimeout(tick, pollMs)); + return; + } + + this.appendLogWithLevel(id, 'WARN', droppedReason); + await this.disconnectInternal(id, false); + const attempt = this.nextRetryAttempt(id); + if (attempt > DEFAULT_RECONNECT_MAX_ATTEMPTS) { + this.setStatus(id, 'error', `${droppedReason}. Retry limit reached`, null, null, null, false, attempt, true); + return; + } + + this.setStatus(id, 'degraded', `${droppedReason}. Reconnecting`, null, null, null, false, attempt, false); + const delayMs = Math.min((2 ** Math.max(attempt - 1, 0)) * 1000 + (nowMillis() % 700) + 100, 30000); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + try { + await this.connect(id); + } catch (error) { + this.setStatus(id, 'error', error instanceof Error ? error.message : String(error), null, null, null, false, attempt, true); + } + }; + this.monitorTimers.set(id, setTimeout(tick, MONITOR_INITIAL_POLL_MS)); + } + + async connect(id) { + const trimmed = String(id || '').trim(); + if (!trimmed || trimmed === LOCAL_HOST_ID) { + throw new Error('SSH instance id is required'); + } + + if (this.connecting.has(trimmed)) { + this.appendLogWithLevel(trimmed, 'INFO', 'Connection already in progress'); + return this.connecting.get(trimmed); + } + + const instance = this.readInstances().instances.find((entry) => entry?.id === trimmed); + if (!instance) { + throw new Error('SSH instance not found'); + } + + const retryAttempt = this.currentRetryAttempt(trimmed); + const connectAttempt = this.nextConnectAttempt(trimmed); + this.appendAttemptSeparator(trimmed, connectAttempt, retryAttempt); + this.appendLog(trimmed, 'Starting SSH connection'); + await this.disconnectInternal(trimmed, false); + + const task = this.connectBlocking(this.sanitizeInstance(instance)) + .catch(async (error) => { + this.setStatus(trimmed, 'error', error instanceof Error ? error.message : String(error), null, null, null, false, 0, true); + await this.disconnectInternal(trimmed, false); + throw error; + }) + .finally(() => { + this.connecting.delete(trimmed); + }); + this.connecting.set(trimmed, task); + return task; + } + + async disconnect(id) { + const trimmed = String(id || '').trim(); + if (!trimmed || trimmed === LOCAL_HOST_ID) { + throw new Error('SSH instance id is required'); + } + await this.disconnectInternal(trimmed, true); + } + + async statusesWithDefaults(id) { + if (id) { + return [this.statusSnapshotForInstance(id)]; + } + return this.readInstances().instances + .map((instance) => this.statusSnapshotForInstance(instance.id)) + .sort((left, right) => left.id.localeCompare(right.id)); + } + + async shutdownAll() { + const ids = [...new Set([...this.sessions.keys(), ...this.connecting.keys(), ...this.monitorTimers.keys()])]; + for (const id of ids) { + await this.disconnectInternal(id, false); + } + } +} diff --git a/src/packages/ui/package.json b/src/packages/ui/package.json index c55e71c..a9a9f79 100644 --- a/src/packages/ui/package.json +++ b/src/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@openchamber/ui", - "version": "1.9.1", + "version": "1.9.9", "private": true, "type": "module", "main": "src/main.tsx", @@ -25,13 +25,13 @@ "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.12.1", + "@codemirror/language": "6.12.2", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", "@codemirror/lint": "^6.9.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.13", + "@codemirror/view": "6.39.13", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -39,20 +39,11 @@ "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", "@lezer/highlight": "^1.2.3", - "@opencode-ai/sdk": "^1.3.0", + "@opencode-ai/sdk": "^1.4.25", "@pierre/diffs": "1.1.0-beta.13", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-toggle": "^1.1.10", - "@radix-ui/react-tooltip": "^1.2.8", + "@base-ui/react": "^1.4.0", "@remixicon/react": "^4.7.0", - "@streamdown/code": "^1.0.2", + "@simplewebauthn/browser": "13.3.0", "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", "beautiful-mermaid": "^1.1.3", @@ -60,12 +51,18 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "codemirror-lang-elixir": "^4.0.0", + "dompurify": "^3.2.7", "express": "^5.1.0", "fuse.js": "^7.1.0", "ghostty-web": "^0.4.0", "heic2any": "^0.0.4", "html-to-image": "^1.11.13", "http-proxy-middleware": "^3.0.5", + "katex": "^0.16.21", + "marked": "^17.0.3", + "morphdom": "^2.7.7", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", "motion": "^12.23.24", "next-themes": "^0.4.6", "prismjs": "^1.30.0", @@ -73,9 +70,9 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-syntax-highlighter": "^15.6.6", + "remend": "^1.2.1", "simple-git": "^3.28.0", "sonner": "^2.0.7", - "streamdown": "^2.2.0", "strip-json-comments": "^5.0.3", "tailwind-merge": "^3.3.1", "yaml": "^2.8.1", @@ -85,7 +82,7 @@ "devDependencies": { "@eslint/js": "^9.33.0", "@tailwindcss/postcss": "^4.0.0", - "@tauri-apps/api": "^2.9.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/src/packages/ui/src/App.tsx b/src/packages/ui/src/App.tsx index 13875df..250a3a8 100644 --- a/src/packages/ui/src/App.tsx +++ b/src/packages/ui/src/App.tsx @@ -5,31 +5,45 @@ import { AgentManagerView } from '@/components/views/agent-manager'; import { ChatView } from '@/components/views'; import { FireworksProvider } from '@/contexts/FireworksContext'; import { Toaster } from '@/components/ui/sonner'; +import { Button } from '@/components/ui/button'; import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel'; +import { setStreamPerfEnabled } from '@/stores/utils/streamDebug'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; -import { useEventStream } from '@/hooks/useEventStream'; +// useEventStream removed — replaced by SyncProvider + SyncBridge import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'; import { useMenuActions } from '@/hooks/useMenuActions'; import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap'; -import { useServerSessionStatus } from '@/hooks/useServerSessionStatus'; import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup'; import { useQueuedMessageAutoSend } from '@/hooks/useQueuedMessageAutoSend'; import { useRouter } from '@/hooks/useRouter'; import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon'; import { usePwaManifestSync } from '@/hooks/usePwaManifestSync'; import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt'; +import { useWindowControlsOverlayLayout } from '@/hooks/useWindowControlsOverlayLayout'; import { useWindowTitle } from '@/hooks/useWindowTitle'; -import { useGitHubPrBackgroundTracking } from '@/hooks/useGitHubPrBackgroundTracking'; -import { GitPollingProvider } from '@/hooks/useGitPolling'; import { useConfigStore } from '@/stores/useConfigStore'; import { hasModifier } from '@/lib/utils'; -import { isDesktopLocalOriginActive, isDesktopShell } from '@/lib/desktop'; -import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen'; -import { useSessionStore } from '@/stores/useSessionStore'; +import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell, restartDesktopApp } from '@/lib/desktop'; +import { + getInjectedBootOutcome, + getBootInjectionStatus, + resolveDesktopBootView, + canDismissInitialLoading, + shouldRestartDesktopBootFlow, + type BootInjectionStatus, + type DesktopBootView, +} from '@/lib/desktopBoot'; +import type { RecoveryVariant } from '@/components/onboarding/DesktopConnectionRecovery'; +import { useSessionUIStore } from '@/sync/session-ui-store'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; +import { useProjectsStore } from '@/stores/useProjectsStore'; import { opencodeClient } from '@/lib/opencode/client'; +import { SyncProvider, useSessions } from '@/sync/sync-context'; +import { useSync } from '@/sync/use-sync'; +import { setOptimisticRefs } from '@/sync/session-actions'; import { useFontPreferences } from '@/hooks/useFontPreferences'; import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions'; +import { loadMonoFont, loadUiFont } from '@/lib/fontLoader'; import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay'; import { AboutDialog } from '@/components/ui/AboutDialog'; import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; @@ -37,15 +51,23 @@ import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { VoiceProvider } from '@/components/voice'; import { useUIStore } from '@/stores/useUIStore'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; +import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore'; import type { RuntimeAPIs } from '@/lib/api/types'; import { TooltipProvider } from '@/components/ui/tooltip'; +import { QuickOpenDialog } from '@/components/ui/QuickOpenDialog'; +import { McpOAuthCallbackPage } from '@/components/sections/mcp/McpOAuthCallbackPage'; +import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth'; +import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; +import { useI18n } from '@/lib/i18n'; -const CLI_MISSING_ERROR_REGEX = - /ENOENT|spawn\s+opencode|Unable\s+to\s+locate\s+the\s+opencode\s+CLI|OpenCode\s+CLI\s+not\s+found|opencode(\.exe)?\s+not\s+found|opencode(\.exe)?:\s*command\s+not\s+found|not\s+recognized\s+as\s+an\s+internal\s+or\s+external\s+command|env:\s*['"]?(node|bun)['"]?:\s*No\s+such\s+file\s+or\s+directory|(node|bun):\s*No\s+such\s+file\s+or\s+directory/i; -const CLI_ONBOARDING_HEALTH_POLL_MS = 1500; +// Lazy-loaded heavy views — loaded on demand to reduce initial bundle size. +const OnboardingScreen = lazyWithChunkRecovery(() => + import('@/components/onboarding/OnboardingScreen').then((m) => ({ default: m.OnboardingScreen })), +); const AboutDialogWrapper: React.FC = () => { - const { isAboutDialogOpen, setAboutDialogOpen } = useUIStore(); + const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen); + const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen); return ( { ); }; +const StartupInitializationRecovery: React.FC<{ + onRetry: () => void; + isRetrying: boolean; +}> = ({ onRetry, isRetrying }) => { + const { t } = useI18n(); + + return ( +
+
+
+

{t('startup.initRecovery.title')}

+

{t('startup.initRecovery.description')}

+
+ +
+
+ ); +}; + type AppProps = { apis: RuntimeAPIs; }; @@ -94,16 +137,80 @@ const readEmbeddedSessionChatConfig = (): EmbeddedSessionChatConfig | null => { }; }; +const isMcpOAuthCallbackPath = (): boolean => { + if (typeof window === 'undefined') { + return false; + } + + return window.location.pathname === MCP_OAUTH_CALLBACK_PATH; +}; + +const EmbeddedSessionSelectionGate: React.FC<{ + embeddedSessionChat: EmbeddedSessionChatConfig | null; + isVSCodeRuntime: boolean; +}> = ({ embeddedSessionChat, isVSCodeRuntime }) => { + const sessions = useSessions(); + const currentSessionId = useSessionUIStore((state) => state.currentSessionId); + const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); + + React.useEffect(() => { + if (!embeddedSessionChat || isVSCodeRuntime) { + return; + } + + if (currentSessionId === embeddedSessionChat.sessionId) { + return; + } + + if (!sessions.some((session) => session.id === embeddedSessionChat.sessionId)) { + return; + } + + void setCurrentSession(embeddedSessionChat.sessionId); + }, [currentSessionId, embeddedSessionChat, isVSCodeRuntime, sessions, setCurrentSession]); + + return null; +}; + +const SyncOptimisticBridge: React.FC = () => { + const sync = useSync(); + const addRef = React.useRef(sync.optimistic.add); + const removeRef = React.useRef(sync.optimistic.remove); + addRef.current = sync.optimistic.add; + removeRef.current = sync.optimistic.remove; + + React.useEffect(() => { + setOptimisticRefs( + (input) => addRef.current(input), + (input) => removeRef.current(input), + ); + }, []); + + return null; +}; + +function SyncAppEffects({ embeddedBackgroundWorkEnabled }: { + embeddedBackgroundWorkEnabled: boolean; +}) { + usePwaManifestSync(); + useWindowControlsOverlayLayout(); + useSessionAutoCleanup(embeddedBackgroundWorkEnabled); + useQueuedMessageAutoSend(embeddedBackgroundWorkEnabled); + useKeyboardShortcuts(); + + return ; +} + function App({ apis }: AppProps) { - const { initializeApp, isInitialized, isConnected } = useConfigStore(); + const initializeApp = useConfigStore((s) => s.initializeApp); + const isInitialized = useConfigStore((s) => s.isInitialized); + const isConnected = useConfigStore((s) => s.isConnected); const providersCount = useConfigStore((state) => state.providers.length); const agentsCount = useConfigStore((state) => state.agents.length); const loadProviders = useConfigStore((state) => state.loadProviders); const loadAgents = useConfigStore((state) => state.loadAgents); - const { error, clearError, loadSessions } = useSessionStore(); - const currentSessionId = useSessionStore((state) => state.currentSessionId); - const setCurrentSession = useSessionStore((state) => state.setCurrentSession); - const sessions = useSessionStore((state) => state.sessions); + const error = useSessionUIStore((s) => s.error); + const clearError = useSessionUIStore((s) => s.clearError); const currentDirectory = useDirectoryStore((state) => state.currentDirectory); const setDirectory = useDirectoryStore((state) => state.setDirectory); const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory); @@ -111,12 +218,33 @@ function App({ apis }: AppProps) { const { uiFont, monoFont } = useFontPreferences(); const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus); const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState(() => apis.runtime.isVSCode); - const [showCliOnboarding, setShowCliOnboarding] = React.useState(false); const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true); + const [initRetryExhausted, setInitRetryExhausted] = React.useState(false); + const [initRetryEpoch, setInitRetryEpoch] = React.useState(0); + const [manualInitRetrying, setManualInitRetrying] = React.useState(false); const isDesktopRuntime = React.useMemo(() => isDesktopShell(), []); + const setPlanModeEnabled = useFeatureFlagsStore((state) => state.setPlanModeEnabled); + const [bootInjectionStatus, setBootInjectionStatus] = React.useState(() => { + return getBootInjectionStatus(); + }); + const [bootView, setBootView] = React.useState(() => { + const outcome = getInjectedBootOutcome(); + return outcome !== null + ? resolveDesktopBootView({ isDesktopShell: true, bootOutcome: outcome }) + : null; + }); const appReadyDispatchedRef = React.useRef(false); + const initializationInFlightRef = React.useRef(false); const embeddedSessionChat = React.useMemo(() => readEmbeddedSessionChatConfig(), []); const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible; + const isMcpOAuthCallback = React.useMemo(() => isMcpOAuthCallbackPath(), []); + + React.useEffect(() => { + setStreamPerfEnabled(showMemoryDebug); + return () => { + setStreamPerfEnabled(false); + }; + }, [showMemoryDebug]); React.useEffect(() => { setIsVSCodeRuntime(apis.runtime.isVSCode); @@ -135,8 +263,6 @@ function App({ apis }: AppProps) { void refreshGitHubAuthStatus(apis.github, { force: true }); }, [apis.github, embeddedSessionChat, refreshGitHubAuthStatus]); - useGitHubPrBackgroundTracking(embeddedBackgroundWorkEnabled ? apis.github : undefined, apis.git); - React.useEffect(() => { if (typeof document === 'undefined') { return; @@ -144,6 +270,8 @@ function App({ apis }: AppProps) { const root = document.documentElement; const uiStack = UI_FONT_OPTION_MAP[uiFont]?.stack ?? UI_FONT_OPTION_MAP[DEFAULT_UI_FONT].stack; const monoStack = CODE_FONT_OPTION_MAP[monoFont]?.stack ?? CODE_FONT_OPTION_MAP[DEFAULT_MONO_FONT].stack; + void loadUiFont(uiFont); + void loadMonoFont(monoFont); root.style.setProperty('--font-sans', uiStack); root.style.setProperty('--font-heading', uiStack); @@ -157,25 +285,55 @@ function App({ apis }: AppProps) { } }, [uiFont, monoFont]); + const bootOutcomeKnown = bootInjectionStatus === 'valid'; + const bootViewIsMain = bootView?.screen === 'main'; + + // Splash dismissal: use the authoritative loading gate from desktopBoot. + // Desktop shells strictly require a valid boot outcome before dismissing. + // Non-main outcomes (chooser/recovery) can dismiss without waiting for init. React.useEffect(() => { - if (isInitialized) { - const hideInitialLoading = () => { - const loadingElement = document.getElementById('initial-loading'); - if (loadingElement) { - loadingElement.classList.add('fade-out'); - - setTimeout(() => { - loadingElement.remove(); - }, 300); - } - }; - - const timer = setTimeout(hideInitialLoading, 150); - return () => clearTimeout(timer); + if (!canDismissInitialLoading({ + isDesktopShell: isDesktopRuntime, + isInitialized, + bootOutcomeKnown, + bootViewIsMain, + })) { + return; } - }, [isInitialized]); + const timer = setTimeout(() => { + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement) { + loadingElement.classList.add('fade-out'); + setTimeout(() => { + loadingElement.remove(); + }, 300); + } + }, 150); + + return () => clearTimeout(timer); + }, [isDesktopRuntime, isInitialized, bootOutcomeKnown, bootViewIsMain]); + + // Deterministic malformed handling: update splash text so the user + // sees a specific error instead of a generic spinner, but do NOT + // dismiss the splash (that only happens on a valid outcome). React.useEffect(() => { + if (!isDesktopRuntime || bootInjectionStatus !== 'malformed') { + return; + } + + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement) { + loadingElement.textContent = 'Desktop startup failed — please restart the app.'; + } + }, [isDesktopRuntime, bootInjectionStatus]); + + // Non-desktop fallback: remove splash after 5 seconds even if init stalls. + React.useEffect(() => { + if (isDesktopRuntime) { + return; + } + const fallbackTimer = setTimeout(() => { const loadingElement = document.getElementById('initial-loading'); if (loadingElement && !isInitialized) { @@ -187,7 +345,29 @@ function App({ apis }: AppProps) { }, 5000); return () => clearTimeout(fallbackTimer); - }, [isInitialized]); + }, [isDesktopRuntime, isInitialized]); + + React.useEffect(() => { + let cancelled = false; + + const run = async () => { + const res = await fetch('/health', { method: 'GET' }).catch(() => null); + if (!res || !res.ok || cancelled) return; + const data = (await res.json().catch(() => null)) as null | { + planModeExperimentalEnabled?: unknown; + }; + if (!data || cancelled) return; + const raw = data.planModeExperimentalEnabled; + const enabled = raw === true || raw === 1 || raw === '1' || raw === 'true'; + setPlanModeEnabled(enabled); + }; + + void run(); + + return () => { + cancelled = true; + }; + }, [setPlanModeEnabled]); React.useEffect(() => { const init = async () => { @@ -196,76 +376,138 @@ function App({ apis }: AppProps) { if (isVSCodeRuntime) { return; } - await initializeApp(); + if (initializationInFlightRef.current) { + return; + } + initializationInFlightRef.current = true; + try { + await initializeApp(); + } finally { + initializationInFlightRef.current = false; + } }; init(); }, [initializeApp, isVSCodeRuntime]); - const startupRecoveryInProgressRef = React.useRef(false); - const startupRecoveryLastAttemptRef = React.useRef(0); - React.useEffect(() => { - if (isVSCodeRuntime) { - return; - } - if (!isConnected) { - return; - } - if (providersCount > 0 && agentsCount > 0) { - return; - } - if (startupRecoveryInProgressRef.current) { - return; - } + if (isVSCodeRuntime || isInitialized) return; - const now = Date.now(); - if (now - startupRecoveryLastAttemptRef.current < 750) { - return; - } + let active = true; + let retryTimer: ReturnType | undefined; + let retryCount = 0; + const MAX_RETRIES = 10; + const BASE_DELAY_MS = 1000; - startupRecoveryLastAttemptRef.current = now; - startupRecoveryInProgressRef.current = true; - - const repair = async () => { - try { - if (providersCount === 0) { - await loadProviders(); - } - if (agentsCount === 0) { - await loadAgents(); - } - } catch { - // Keep UI responsive; we'll retry on next cycle. - } finally { - startupRecoveryInProgressRef.current = false; + const retryInitialization = async () => { + if (!active) return; + if (retryCount >= MAX_RETRIES) { + setInitRetryExhausted(true); + return; } + const state = useConfigStore.getState(); + if (state.isInitialized) { + setInitRetryExhausted(false); + return; + } + if (initializationInFlightRef.current) { + retryTimer = setTimeout(retryInitialization, BASE_DELAY_MS); + return; + } + + retryCount += 1; + initializationInFlightRef.current = true; + try { + await state.initializeApp(); + } finally { + initializationInFlightRef.current = false; + } + + const next = useConfigStore.getState(); + if (!active) return; + if (next.isInitialized) { + setInitRetryExhausted(false); + return; + } + if (retryCount >= MAX_RETRIES) { + setInitRetryExhausted(true); + return; + } + const delay = Math.min(BASE_DELAY_MS * Math.pow(2, retryCount - 1), 16000); + retryTimer = setTimeout(retryInitialization, delay); }; - void repair(); - }, [agentsCount, isConnected, isVSCodeRuntime, loadAgents, loadProviders, providersCount]); + retryTimer = setTimeout(retryInitialization, BASE_DELAY_MS); + + return () => { + active = false; + if (retryTimer) clearTimeout(retryTimer); + }; + }, [initRetryEpoch, isInitialized, isVSCodeRuntime]); + + React.useEffect(() => { + if (isInitialized) { + setInitRetryExhausted(false); + } + }, [isInitialized]); + + React.useEffect(() => { + if (!initRetryExhausted) return; + + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement) { + loadingElement.classList.add('fade-out'); + setTimeout(() => { + loadingElement.remove(); + }, 300); + } + }, [initRetryExhausted]); + + // Startup recovery: poll until providers AND agents are loaded. + // loadProviders/loadAgents resolve normally even on failure (errors swallowed), + // so a reactive effect can't detect failure — we need an interval. + React.useEffect(() => { + if (isVSCodeRuntime || !isConnected) return; + if (providersCount > 0 && agentsCount > 0) return; + + let active = true; + let retries = 0; + const MAX_RETRIES = 15; + const attempt = async () => { + const state = useConfigStore.getState(); + if (state.providers.length > 0 && state.agents.length > 0) return; + try { + if (state.providers.length === 0) await loadProviders(); + if (useConfigStore.getState().agents.length === 0) await loadAgents(); + } catch { /* retry next interval */ } + }; + + void attempt(); + const id = setInterval(() => { + if (!active) return; + if (++retries >= MAX_RETRIES) { clearInterval(id); return; } + void attempt(); + }, 2000); + return () => { active = false; clearInterval(id); }; + }, [isConnected, isVSCodeRuntime, loadAgents, loadProviders, providersCount, agentsCount]); React.useEffect(() => { if (isSwitchingDirectory) { return; } - const syncDirectoryAndSessions = async () => { - // VS Code runtime loads sessions via VSCodeLayout bootstrap to avoid startup races. - if (isVSCodeRuntime) { - return; - } + // VS Code runtime loads sessions via VSCodeLayout bootstrap to avoid startup races. + if (isVSCodeRuntime) { + return; + } - if (!isConnected) { - return; - } - opencodeClient.setDirectory(currentDirectory); + if (!isConnected) { + return; + } + opencodeClient.setDirectory(currentDirectory); - await loadSessions(); - }; - - syncDirectoryAndSessions(); - }, [currentDirectory, isSwitchingDirectory, loadSessions, isConnected, isVSCodeRuntime]); + // Session loading is handled by the sync system's bootstrap — no manual loadSessions needed. + }, [currentDirectory, isSwitchingDirectory, isConnected, isVSCodeRuntime]); React.useEffect(() => { if (!embeddedSessionChat || typeof window === 'undefined') { @@ -317,22 +559,6 @@ function App({ apis }: AppProps) { setDirectory(embeddedSessionChat.directory, { showOverlay: false }); }, [currentDirectory, embeddedSessionChat, isVSCodeRuntime, setDirectory]); - React.useEffect(() => { - if (!embeddedSessionChat || isVSCodeRuntime) { - return; - } - - if (currentSessionId === embeddedSessionChat.sessionId) { - return; - } - - if (!sessions.some((session) => session.id === embeddedSessionChat.sessionId)) { - return; - } - - void setCurrentSession(embeddedSessionChat.sessionId); - }, [currentSessionId, embeddedSessionChat, isVSCodeRuntime, sessions, setCurrentSession]); - React.useEffect(() => { if (!embeddedSessionChat || typeof window === 'undefined') { return; @@ -356,6 +582,40 @@ function App({ apis }: AppProps) { }; }, [embeddedSessionChat]); + React.useEffect(() => { + if (typeof window === 'undefined') return; + + const handler = (event: Event) => { + const detail = (event as CustomEvent<{ sessionId?: string }>).detail; + const sessionId = typeof detail?.sessionId === 'string' ? detail.sessionId.trim() : ''; + if (!sessionId) return; + void useSessionUIStore.getState().setCurrentSession(sessionId); + }; + + window.addEventListener('openchamber:open-session', handler as EventListener); + return () => window.removeEventListener('openchamber:open-session', handler as EventListener); + }, []); + + React.useEffect(() => { + if (typeof window === 'undefined') return; + + const handler = (event: Event) => { + const detail = (event as CustomEvent<{ projectPath?: string }>).detail; + const projectPath = typeof detail?.projectPath === 'string' ? detail.projectPath.trim() : ''; + if (!projectPath) return; + const projectsStore = useProjectsStore.getState(); + const existing = projectsStore.projects.find((project) => project.path === projectPath); + if (existing) { + projectsStore.setActiveProject(existing.id); + } else { + projectsStore.addProject(projectPath); + } + }; + + window.addEventListener('openchamber:open-project', handler as EventListener); + return () => window.removeEventListener('openchamber:open-project', handler as EventListener); + }, []); + React.useEffect(() => { if (typeof window === 'undefined') return; if (!isInitialized || isSwitchingDirectory) return; @@ -365,22 +625,17 @@ function App({ apis }: AppProps) { window.dispatchEvent(new Event('openchamber:app-ready')); }, [isInitialized, isSwitchingDirectory]); - useEventStream({ enabled: embeddedBackgroundWorkEnabled }); + // useEventStream replaced by SyncProvider + SyncBridge - // Server-authoritative session status polling - // Replaces SSE-dependent status updates with reliable HTTP polling - useServerSessionStatus({ enabled: embeddedBackgroundWorkEnabled }); + // Session attention now handled by notification-store via SSE events (session.idle/session.error) usePushVisibilityBeacon({ enabled: embeddedBackgroundWorkEnabled }); - usePwaManifestSync(); usePwaInstallPrompt(); useWindowTitle(); useRouter(); - useKeyboardShortcuts(); - const handleToggleMemoryDebug = React.useCallback(() => { setShowMemoryDebug(prev => !prev); }, []); @@ -388,8 +643,6 @@ function App({ apis }: AppProps) { useMenuActions(handleToggleMemoryDebug); useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled }); - useSessionAutoCleanup({ enabled: embeddedBackgroundWorkEnabled }); - useQueuedMessageAutoSend({ enabled: embeddedBackgroundWorkEnabled }); React.useEffect(() => { if (embeddedSessionChat) { @@ -397,14 +650,19 @@ function App({ apis }: AppProps) { } const handleKeyDown = (e: KeyboardEvent) => { - if (hasModifier(e) && e.shiftKey && e.key === 'D') { + const isDebugShortcut = hasModifier(e) + && e.shiftKey + && !e.altKey + && (e.code === 'KeyD' || e.key.toLowerCase() === 'd'); + + if (isDebugShortcut) { e.preventDefault(); setShowMemoryDebug(prev => !prev); } }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); + window.addEventListener('keydown', handleKeyDown, true); + return () => window.removeEventListener('keydown', handleKeyDown, true); }, [embeddedSessionChat]); React.useEffect(() => { @@ -418,58 +676,141 @@ function App({ apis }: AppProps) { } }, [clearError, embeddedSessionChat, error]); + // Poll for the injected boot outcome until it becomes available (desktop only). + // The Rust backend sets window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__ once the + // sidecar reaches a stable state. We poll with exponential backoff to handle + // potential race conditions during startup and config writes. React.useEffect(() => { - if (embeddedSessionChat) { - return; - } - - if (!isDesktopShell() || !isDesktopLocalOriginActive()) { + if (!isDesktopRuntime || bootInjectionStatus !== 'not-injected') { return; } let cancelled = false; - const run = async () => { - const res = await fetch('/health', { method: 'GET' }).catch(() => null); - if (!res || !res.ok || cancelled) return; - const data = (await res.json().catch(() => null)) as null | { - openCodeRunning?: unknown; - isOpenCodeReady?: unknown; - opencodeBinaryResolved?: unknown; - lastOpenCodeError?: unknown; - }; - if (!data || cancelled) return; - const openCodeRunning = data.openCodeRunning === true; - const isOpenCodeReady = data.isOpenCodeReady === true; - const resolvedBinary = typeof data.opencodeBinaryResolved === 'string' ? data.opencodeBinaryResolved.trim() : ''; - const hasResolvedBinary = resolvedBinary.length > 0; - const err = typeof data.lastOpenCodeError === 'string' ? data.lastOpenCodeError : ''; - const cliMissing = - !openCodeRunning && - (CLI_MISSING_ERROR_REGEX.test(err) || (!hasResolvedBinary && !isOpenCodeReady)); - setShowCliOnboarding(cliMissing); + let attempts = 0; + const BASE_INTERVAL = 200; + const MAX_INTERVAL = 2000; + const MAX_ATTEMPTS = 50; // 10 seconds total (200ms * 50 with exponential backoff cap) + + const pollWithBackoff = () => { + if (cancelled) return; + + attempts++; + const status = getBootInjectionStatus(); + + if (status !== 'not-injected') { + cancelled = true; + setBootInjectionStatus(status); + + if (status === 'valid') { + const outcome = getInjectedBootOutcome(); + if (outcome) { + setBootView(resolveDesktopBootView({ isDesktopShell: true, bootOutcome: outcome })); + } + } + // If status is 'malformed', we keep the splash visible with error text + // handled by the separate useEffect below + return; + } + + // Exponential backoff with cap + const nextInterval = Math.min(BASE_INTERVAL * Math.pow(1.1, attempts), MAX_INTERVAL); + + if (attempts >= MAX_ATTEMPTS) { + // Max attempts reached - keep polling but show error + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement && !loadingElement.textContent?.includes('taking longer')) { + loadingElement.textContent = 'Desktop startup is taking longer than expected...'; + } + } + + window.setTimeout(pollWithBackoff, nextInterval); }; - void run(); - const interval = window.setInterval(() => { - void run(); - }, CLI_ONBOARDING_HEALTH_POLL_MS); + // Start polling + window.setTimeout(pollWithBackoff, BASE_INTERVAL); return () => { cancelled = true; - window.clearInterval(interval); }; - }, [embeddedSessionChat]); + }, [isDesktopRuntime, bootInjectionStatus]); + + const handleDesktopBootDismiss = React.useCallback(async () => { + if (shouldRestartDesktopBootFlow({ + isTauriShell: isTauriShell(), + isDesktopLocalOriginActive: isDesktopLocalOriginActive(), + })) { + await restartDesktopApp(); + return; + } - const handleCliAvailable = React.useCallback(() => { - setShowCliOnboarding(false); window.location.reload(); }, []); - if (showCliOnboarding) { + const handleManualInitRetry = React.useCallback(async () => { + if (manualInitRetrying || initializationInFlightRef.current) return; + + setInitRetryExhausted(false); + setManualInitRetrying(true); + initializationInFlightRef.current = true; + try { + await useConfigStore.getState().initializeApp(); + } finally { + initializationInFlightRef.current = false; + setManualInitRetrying(false); + } + + if (!useConfigStore.getState().isInitialized) { + setInitRetryEpoch((value) => value + 1); + } + }, [manualInitRetrying]); + + // Map boot outcome kind to recovery variant + const mapBootViewToRecoveryVariant = (view: DesktopBootView): RecoveryVariant | undefined => { + if (view.screen === 'recovery') { + return view.variant; + } + return undefined; + }; + + // Desktop boot view routing. + // When the boot outcome resolves to a non-main screen (chooser, recovery), + // render OnboardingScreen with appropriate mode/variant. + if (isDesktopRuntime && bootView && bootView.screen !== 'main') { + // First-launch chooser + if (bootView.screen === 'chooser') { + return ( + +
+ }> + { + // Switch to remote tab - handled internally by OnboardingScreen + }} + /> + +
+
+ ); + } + + // Recovery screens + const recoveryVariant = mapBootViewToRecoveryVariant(bootView); + const hostUrl = bootView.screen === 'recovery' && 'url' in bootView ? bootView.url : undefined; + return (
- + }> + +
); @@ -478,14 +819,37 @@ function App({ apis }: AppProps) { if (embeddedSessionChat) { return ( - - -
- - -
-
-
+ + + +
+ + + + +
+
+
+
+
+ ); + } + + if (isMcpOAuthCallback) { + return ( + + + + ); + } + + if (initRetryExhausted && !isInitialized && !isVSCodeRuntime && !embeddedSessionChat) { + return ( + + { void handleManualInitRetry(); }} + isRetrying={manualInitRetrying} + /> ); } @@ -493,62 +857,79 @@ function App({ apis }: AppProps) { // VS Code runtime - simplified layout without git/terminal views if (isVSCodeRuntime) { // Check if this is the Agent Manager panel - const panelType = typeof window !== 'undefined' - ? (window as { __OPENCHAMBER_PANEL_TYPE__?: 'chat' | 'agentManager' }).__OPENCHAMBER_PANEL_TYPE__ + const panelType = typeof window !== 'undefined' + ? (window as { __OPENCHAMBER_PANEL_TYPE__?: 'chat' | 'agentManager' }).__OPENCHAMBER_PANEL_TYPE__ : 'chat'; - + if (panelType === 'agentManager') { return ( - - -
- - -
-
-
-
- ); - } - - return ( - - - + +
- + +
-
-
+ + +
+ ); + } + + return ( + + + + + +
+ + + +
+
+
+
+
); } + // Always mount the full provider tree to avoid remounts when isInitialized + // flips from false → true. FireworksProvider and VoiceProvider are lightweight + // shells; their heavy children are only activated when actually needed. + const isBootShell = !isInitialized && !isDesktopRuntime; + return ( - - + +
+ - - - {showMemoryDebug && ( - setShowMemoryDebug(false)} /> + {!isBootShell && ( + <> + + + + {showMemoryDebug && ( + setShowMemoryDebug(false)} /> + )} + )}
-
-
+ +
); } diff --git a/src/packages/ui/src/assets/icons/file-types/json.svg b/src/packages/ui/src/assets/icons/file-types/json.svg index 2590b94..9200a60 100644 --- a/src/packages/ui/src/assets/icons/file-types/json.svg +++ b/src/packages/ui/src/assets/icons/file-types/json.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/packages/ui/src/assets/icons/file-types/sprite.svg b/src/packages/ui/src/assets/icons/file-types/sprite.svg index c46adad..fcfbfb0 100644 --- a/src/packages/ui/src/assets/icons/file-types/sprite.svg +++ b/src/packages/ui/src/assets/icons/file-types/sprite.svg @@ -4533,10 +4533,10 @@ - - + + - + diff --git a/src/packages/ui/src/components/auth/SessionAuthGate.tsx b/src/packages/ui/src/components/auth/SessionAuthGate.tsx index 4fcbcad..779a8f5 100644 --- a/src/packages/ui/src/components/auth/SessionAuthGate.tsx +++ b/src/packages/ui/src/components/auth/SessionAuthGate.tsx @@ -1,14 +1,28 @@ import React from 'react'; import { RiLockLine, RiLockUnlockLine, RiLoader4Line } from '@remixicon/react'; +import { browserSupportsWebAuthn } from '@simplewebauthn/browser'; 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 { syncDesktopSettings, initializeAppearancePreferences } from '@/lib/persistence'; import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence'; import { DesktopHostSwitcherInline } from '@/components/desktop/DesktopHostSwitcher'; import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; +import { useI18n } from '@/lib/i18n'; +import { + authenticateWithPasskey, + cancelPasskeyCeremony, + defaultPasskeyStatus, + fetchPasskeyStatus, + isPasskeyCeremonyAbort, + type PasskeyStatus, + registerCurrentDevicePasskey, +} from '@/lib/passkeys'; const STATUS_CHECK_ENDPOINT = '/auth/session'; +const TRUST_DEVICE_STORAGE_KEY = 'openchamber.uiAuth.trustDevice'; const fetchSessionStatus = async (): Promise => { console.log('[Frontend Auth] Checking session status...'); @@ -23,7 +37,14 @@ const fetchSessionStatus = async (): Promise => { return response; }; -const submitPassword = async (password: string): Promise => { +const readStoredTrustDevice = (): boolean => { + if (typeof window === 'undefined') { + return false; + } + return window.localStorage.getItem(TRUST_DEVICE_STORAGE_KEY) === 'true'; +}; + +const submitPassword = async (password: string, trustDevice: boolean): Promise => { console.log('[Frontend Auth] Submitting password...'); const response = await fetch(STATUS_CHECK_ENDPOINT, { method: 'POST', @@ -32,7 +53,7 @@ const submitPassword = async (password: string): Promise => { 'Content-Type': 'application/json', Accept: 'application/json', }, - body: JSON.stringify({ password }), + body: JSON.stringify({ password, trustDevice }), }); console.log('[Frontend Auth] Password submit response:', response.status, response.statusText); return response; @@ -64,11 +85,12 @@ const AuthShell: React.FC<{ children: React.ReactNode }> = ({ children }) => ( const LoadingScreen: React.FC = () => (
- +
); const ErrorScreen: React.FC = ({ onRetry, errorType = 'network', retryAfter }) => { + const { t } = useI18n(); const isRateLimit = errorType === 'rate-limit'; const minutes = retryAfter ? Math.ceil(retryAfter / 60) : 1; @@ -77,16 +99,18 @@ const ErrorScreen: React.FC = ({ onRetry, errorType = 'network

- {isRateLimit ? 'Too many attempts' : 'Unable to reach server'} + {isRateLimit ? t('sessionAuth.error.rateLimitTitle') : t('sessionAuth.error.networkTitle')}

{isRateLimit - ? `Please wait ${minutes} minute${minutes > 1 ? 's' : ''} before trying again.` - : "We couldn't verify the UI session. Check that the service is running and try again."} + ? (minutes > 1 + ? t('sessionAuth.error.rateLimitDescriptionPlural', { minutes }) + : t('sessionAuth.error.rateLimitDescriptionSingle', { minutes })) + : t('sessionAuth.error.networkDescription')}

@@ -106,6 +130,7 @@ interface ErrorScreenProps { } export const SessionAuthGate: React.FC = ({ children }) => { + const { t } = useI18n(); const vscodeRuntime = React.useMemo(() => isVSCodeRuntime(), []); const skipAuth = vscodeRuntime; const showHostSwitcher = React.useMemo(() => isDesktopShell() && !vscodeRuntime, [vscodeRuntime]); @@ -115,9 +140,66 @@ export const SessionAuthGate: React.FC = ({ children }) => const [errorMessage, setErrorMessage] = React.useState(''); const [retryAfter, setRetryAfter] = React.useState(undefined); const [isTunnelLocked, setIsTunnelLocked] = React.useState(false); + const [passkeyStatus, setPasskeyStatus] = React.useState(defaultPasskeyStatus); + const [supportsPasskeys, setSupportsPasskeys] = React.useState(false); + const [isPasskeyBusy, setIsPasskeyBusy] = React.useState(false); + const [trustDevice, setTrustDevice] = React.useState(() => readStoredTrustDevice()); + const [activePasskeyAction, setActivePasskeyAction] = React.useState<'auth' | 'register' | null>(null); const passwordInputRef = React.useRef(null); const hasResyncedRef = React.useRef(skipAuth); + React.useEffect(() => { + if (typeof window === 'undefined') { + return; + } + window.localStorage.setItem(TRUST_DEVICE_STORAGE_KEY, trustDevice ? 'true' : 'false'); + }, [trustDevice]); + + const refreshPasskeyStatus = React.useCallback(async () => { + if (skipAuth) { + return defaultPasskeyStatus; + } + + try { + const nextStatus = await fetchPasskeyStatus(); + setPasskeyStatus(nextStatus); + return nextStatus; + } catch { + setPasskeyStatus(defaultPasskeyStatus); + return defaultPasskeyStatus; + } + }, [skipAuth]); + + React.useEffect(() => { + let cancelled = false; + + if (skipAuth) { + return; + } + + void (async () => { + try { + if (!window.isSecureContext || !browserSupportsWebAuthn()) { + if (!cancelled) { + setSupportsPasskeys(false); + } + return; + } + if (!cancelled) { + setSupportsPasskeys(true); + } + } catch { + if (!cancelled) { + setSupportsPasskeys(false); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [skipAuth]); + const checkStatus = React.useCallback(async () => { if (skipAuth) { console.log('[Frontend Auth] VSCode runtime, skipping auth'); @@ -125,16 +207,12 @@ export const SessionAuthGate: React.FC = ({ children }) => return; } - // 检查 cookie 是否存在 - const cookies = document.cookie; - const hasAccessToken = cookies.includes('oc_ui_session='); - const hasRefreshToken = cookies.includes('oc_ui_refresh='); - console.log('[Frontend Auth] Cookies check - access:', hasAccessToken, 'refresh:', hasRefreshToken); - console.log('[Frontend Auth] All cookies:', cookies.split(';').map(c => c.trim().split('=')[0])); - setState((prev) => (prev === 'authenticated' ? prev : 'pending')); try { - const response = await fetchSessionStatus(); + const [response, latestPasskeyStatus] = await Promise.all([ + fetchSessionStatus(), + refreshPasskeyStatus(), + ]); const responseText = await response.text(); console.log('[Frontend Auth] Raw response:', response.status, responseText); @@ -158,6 +236,7 @@ export const SessionAuthGate: React.FC = ({ children }) => console.warn('[Frontend Auth] Debug info:', data.debug); } setIsTunnelLocked(data.tunnelLocked === true); + setPasskeyStatus(latestPasskeyStatus); setState('locked'); setRetryAfter(undefined); return; @@ -182,7 +261,7 @@ export const SessionAuthGate: React.FC = ({ children }) => setState('error'); setIsTunnelLocked(false); } - }, [skipAuth]); + }, [refreshPasskeyStatus, skipAuth]); React.useEffect(() => { if (skipAuth) { @@ -220,6 +299,28 @@ export const SessionAuthGate: React.FC = ({ children }) => const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); + await handlePasswordUnlock(false); + }; + + const registerPasskeyForCurrentSession = React.useCallback(async () => { + setActivePasskeyAction('register'); + setIsPasskeyBusy(true); + try { + await registerCurrentDevicePasskey(); + } finally { + setActivePasskeyAction(null); + setIsPasskeyBusy(false); + } + await refreshPasskeyStatus(); + }, [refreshPasskeyStatus]); + + const cancelActivePasskey = React.useCallback(() => { + cancelPasskeyCeremony(); + setActivePasskeyAction(null); + setIsPasskeyBusy(false); + }, []); + + const handlePasswordUnlock = React.useCallback(async (enrollPasskey: boolean) => { if (isTunnelLocked) { return; } @@ -227,28 +328,43 @@ export const SessionAuthGate: React.FC = ({ children }) => return; } + if (isPasskeyBusy) { + cancelActivePasskey(); + } + setIsSubmitting(true); setErrorMessage(''); try { - const response = await submitPassword(password); + const response = await submitPassword(password, trustDevice); if (response.ok) { console.log('[Frontend Auth] Login successful'); - // 检查登录后 cookie 是否被设置 - const cookies = document.cookie; - const hasAccessToken = cookies.includes('oc_ui_session='); - const hasRefreshToken = cookies.includes('oc_ui_refresh='); - console.log('[Frontend Auth] After login - access:', hasAccessToken, 'refresh:', hasRefreshToken); - console.log('[Frontend Auth] All cookies after login:', cookies.split(';').map(c => c.trim().split('=')[0]).filter(Boolean)); setPassword(''); setIsTunnelLocked(false); + if (enrollPasskey && supportsPasskeys) { + try { + await registerPasskeyForCurrentSession(); + toast.success(t('sessionAuth.toast.passkeyAdded')); + setState('authenticated'); + return; + } catch (error) { + if (isPasskeyCeremonyAbort(error)) { + toast.message(t('sessionAuth.toast.passkeySetupCanceled')); + } else { + const message = error instanceof Error ? error.message : t('sessionAuth.error.passkeySetupFailed'); + toast.error(message); + } + setState('authenticated'); + return; + } + } setState('authenticated'); return; } if (response.status === 401) { console.warn('[Frontend Auth] Login failed: Invalid password'); - setErrorMessage('Incorrect password. Try again.'); + setErrorMessage(t('sessionAuth.error.incorrectPassword')); setIsTunnelLocked(false); setState('locked'); return; @@ -264,18 +380,86 @@ export const SessionAuthGate: React.FC = ({ children }) => } console.error('[Frontend Auth] Login failed: Unexpected response', response.status); - setErrorMessage('Unexpected response from server.'); + setErrorMessage(t('sessionAuth.error.unexpectedResponse')); setIsTunnelLocked(false); setState('error'); } catch (error) { console.warn('Failed to submit UI password:', error); - setErrorMessage('Network error. Check connection and retry.'); + setErrorMessage(t('sessionAuth.error.networkRetry')); setIsTunnelLocked(false); setState('error'); } finally { setIsSubmitting(false); } - }; + }, [cancelActivePasskey, isPasskeyBusy, isSubmitting, isTunnelLocked, password, registerPasskeyForCurrentSession, supportsPasskeys, t, trustDevice]); + + const handlePasskeyUnlock = React.useCallback(async () => { + if (isSubmitting || !supportsPasskeys) { + return; + } + + if (isPasskeyBusy) { + cancelActivePasskey(); + return; + } + + setIsPasskeyBusy(true); + setActivePasskeyAction('auth'); + setErrorMessage(''); + + try { + await authenticateWithPasskey(trustDevice); + + setPassword(''); + setState('authenticated'); + } catch (error) { + if (isPasskeyCeremonyAbort(error)) { + setErrorMessage(''); + } else { + const message = error instanceof Error ? error.message : t('sessionAuth.error.passkeySignInCanceled'); + setErrorMessage(message); + } + } finally { + setActivePasskeyAction(null); + setIsPasskeyBusy(false); + } + }, [cancelActivePasskey, isPasskeyBusy, isSubmitting, supportsPasskeys, t, trustDevice]); + + const handlePasskeySetupOnly = React.useCallback(async () => { + if (isSubmitting || isTunnelLocked || !supportsPasskeys) { + return; + } + + if (isPasskeyBusy) { + cancelActivePasskey(); + return; + } + + if (state !== 'authenticated') { + if (!password) { + setErrorMessage(t('sessionAuth.error.enterPasswordForPasskey')); + return; + } + await handlePasswordUnlock(true); + return; + } + + setErrorMessage(''); + try { + await registerPasskeyForCurrentSession(); + toast.success(t('sessionAuth.toast.passkeyAdded')); + } catch (error) { + if (isPasskeyCeremonyAbort(error)) { + toast.message(t('sessionAuth.toast.passkeySetupCanceled')); + return; + } + const message = error instanceof Error ? error.message : t('sessionAuth.error.passkeySetupFailed'); + toast.error(message); + } + }, [cancelActivePasskey, handlePasswordUnlock, isPasskeyBusy, isSubmitting, isTunnelLocked, password, registerPasskeyForCurrentSession, state, supportsPasskeys, t]); + + const canOfferPasskeySetup = supportsPasskeys && passkeyStatus.enabled; + const canUsePasskey = canOfferPasskeySetup && passkeyStatus.hasPasskeys; if (state === 'pending') { return ; @@ -295,17 +479,35 @@ export const SessionAuthGate: React.FC = ({ children }) =>

- {isTunnelLocked ? 'Tunnel access required' : 'Unlock OpenChamber'} + {isTunnelLocked ? t('sessionAuth.locked.tunnelTitle') : t('sessionAuth.locked.unlockTitle')}

{isTunnelLocked - ? 'Open this tunnel using the one-time connect link from the desktop app.' - : 'This session is password-protected.'} + ? t('sessionAuth.locked.tunnelDescription') + : t('sessionAuth.locked.passwordDescription')}

{!isTunnelLocked && ( -
+ + {canUsePasskey && ( + + )}
@@ -314,7 +516,7 @@ export const SessionAuthGate: React.FC = ({ children }) => ref={passwordInputRef} type="password" autoComplete="current-password" - placeholder="Enter password" + placeholder={t('sessionAuth.password.placeholder')} value={password} onChange={(event) => { setPassword(event.target.value); @@ -332,7 +534,7 @@ export const SessionAuthGate: React.FC = ({ children }) => type="submit" size="icon" disabled={!password || isSubmitting} - aria-label={isSubmitting ? 'Unlocking' : 'Unlock'} + aria-label={isSubmitting ? t('sessionAuth.actions.unlockingAria') : t('sessionAuth.actions.unlockAria')} > {isSubmitting ? ( @@ -341,6 +543,45 @@ export const SessionAuthGate: React.FC = ({ children }) => )}
+ {canOfferPasskeySetup ? ( +
+ + +
+ ) : ( + + )} {errorMessage && (

{errorMessage} @@ -353,7 +594,7 @@ export const SessionAuthGate: React.FC = ({ children }) =>

- Use Local if remote is unreachable. + {t('sessionAuth.locked.hostSwitcherHint')}

)} diff --git a/src/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx b/src/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx index 3a1aa39..98cc461 100644 --- a/src/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx +++ b/src/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx @@ -3,6 +3,7 @@ import { cn, fuzzyMatch } from '@/lib/utils'; import { useConfigStore } from '@/stores/useConfigStore'; import { useAgentsStore, isAgentBuiltIn, type AgentWithExtras } from '@/stores/useAgentsStore'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; +import { useI18n } from '@/lib/i18n'; interface AgentInfo { name: string; @@ -40,13 +41,15 @@ export const AgentMentionAutocomplete = React.forwardRef { + const { t } = useI18n(); const containerRef = React.useRef(null); const [selectedIndex, setSelectedIndex] = React.useState(0); const [agents, setAgents] = React.useState([]); const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); const ignoreTabClickRef = React.useRef(false); - const { getVisibleAgents } = useConfigStore(); - const { agents: agentsWithMetadata, loadAgents } = useAgentsStore(); + const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents); + const agentsWithMetadata = useAgentsStore((state) => state.agents); + const loadAgents = useAgentsStore((state) => state.loadAgents); React.useEffect(() => { if (agentsWithMetadata.length === 0) { @@ -156,7 +159,7 @@ export const AgentMentionAutocomplete = React.forwardRef#{agent.name} {isSystem ? ( - system + {t('chat.agentMentionAutocomplete.badge.system')} ) : agent.scope ? ( ([ + { id: 'commands' as const, label: t('chat.autocomplete.tabs.commands') }, + { id: 'agents' as const, label: t('chat.autocomplete.tabs.agents') }, + { id: 'files' as const, label: t('chat.autocomplete.tabs.files') }, + ]), [t]); + return (
- {([ - { id: 'commands' as const, label: 'Commands' }, - { id: 'agents' as const, label: 'Agents' }, - { id: 'files' as const, label: 'Files' }, - ]).map((tab) => ( + {tabs.map((tab) => (
); diff --git a/src/packages/ui/src/components/chat/ChangedFilesList.tsx b/src/packages/ui/src/components/chat/ChangedFilesList.tsx new file mode 100644 index 0000000..bc8f670 --- /dev/null +++ b/src/packages/ui/src/components/chat/ChangedFilesList.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; +import { type ChangedFileEntry, getDisplayPath, getFileStats } from './changedFiles'; +import { useI18n } from '@/lib/i18n'; + +interface ChangedFilesListProps { + files: ChangedFileEntry[]; + currentDirectory: string; + onOpenFile: (file: ChangedFileEntry) => void; +} + +export const ChangedFilesList: React.FC = ({ files, currentDirectory, onOpenFile }) => { + const { t } = useI18n(); + return ( + <> +
+ {t('chat.changedFiles.title')} + {files.length} +
+ +
+ {files.map((file, index) => { + const { fileName, dirPart } = getDisplayPath(file, currentDirectory); + const stats = getFileStats(file); + + return ( + + ); + })} +
+ + ); +}; diff --git a/src/packages/ui/src/components/chat/ChatContainer.tsx b/src/packages/ui/src/components/chat/ChatContainer.tsx index d072445..ff3401a 100644 --- a/src/packages/ui/src/components/chat/ChatContainer.tsx +++ b/src/packages/ui/src/components/chat/ChatContainer.tsx @@ -1,23 +1,23 @@ import React from 'react'; import { RiArrowLeftLine } from '@remixicon/react'; -import { useShallow } from 'zustand/react/shallow'; -import type { Message, Part } from '@opencode-ai/sdk/v2'; +import type { Message, Part, Session } from '@opencode-ai/sdk/v2'; import { ChatInput } from './ChatInput'; -import { useSessionStore } from '@/stores/useSessionStore'; import { useUIStore } from '@/stores/useUIStore'; import { Skeleton } from '@/components/ui/skeleton'; import ChatEmptyState from './ChatEmptyState'; import MessageList, { type MessageListHandle } from './MessageList'; +import { PermissionCard } from './PermissionCard'; +import { QuestionCard } from './QuestionCard'; +import { StatusRowContainer } from './StatusRowContainer'; import ScrollToBottomButton from './components/ScrollToBottomButton'; import { ScrollShadow } from '@/components/ui/ScrollShadow'; -import { useChatScrollManager } from '@/hooks/useChatScrollManager'; +import { useChatScrollManager, type AnimationHandlers, type ContentChangeReason } from '@/hooks/useChatScrollManager'; import { useChatTimelineController } from './hooks/useChatTimelineController'; import { useChatTurnNavigation } from './hooks/useChatTurnNavigation'; import { useDeviceInfo } from '@/lib/device'; import { Button } from '@/components/ui/button'; import { OverlayScrollbar } from '@/components/ui/OverlayScrollbar'; -import { TimelineDialog } from './TimelineDialog'; import type { PermissionRequest } from '@/types/permission'; import type { QuestionRequest } from '@/types/question'; import { cn } from '@/lib/utils'; @@ -26,11 +26,99 @@ import { flattenBlockingRequests, } from './lib/blockingRequests'; +// New sync system imports +import { useSessionUIStore } from '@/sync/session-ui-store'; +import { useViewportStore } from '@/sync/viewport-store'; +import { useStreamingStore } from '@/sync/streaming'; +import { + useSessionMessageCount, + useSessionMessageRecords, + useSessions, + useDirectorySync, + useSessionStatus, +} from '@/sync/sync-context'; +import { useSync } from '@/sync/use-sync'; +import { usePlanDetection } from '@/hooks/usePlanDetection'; +import { getAllSyncSessions } from '@/sync/sync-refs'; +import { useI18n } from '@/lib/i18n'; + const EMPTY_MESSAGES: Array<{ info: Message; parts: Part[] }> = []; const EMPTY_PERMISSIONS: PermissionRequest[] = []; const EMPTY_QUESTIONS: QuestionRequest[] = []; const IDLE_SESSION_STATUS = { type: 'idle' as const }; const SESSION_RESELECTED_EVENT = 'openchamber:session-reselected'; +const DEFAULT_RETRY_MESSAGE = 'Quota limit reached. Retrying automatically.'; +const CHAT_SCROLL_STYLE = { + overflowAnchor: 'none', + overscrollBehavior: 'contain', + overscrollBehaviorY: 'contain', +} as const; +const CHAT_NAVIGATION_IGNORED_TARGET_SELECTOR = [ + 'a[href]', + 'button', + 'input', + 'select', + 'textarea', + '[contenteditable="true"]', + '[role="button"]', + '[role="combobox"]', + '[role="dialog"]', + '[role="listbox"]', + '[role="menu"]', + '[role="menuitem"]', + '[role="option"]', + '[role="textbox"]', + '[data-radix-popper-content-wrapper]', +].join(','); +type SessionMessageRecord = { info: Message; parts: Part[] }; + +const isHTMLElement = (target: EventTarget | null): target is HTMLElement => { + return target instanceof HTMLElement; +}; + +const shouldIgnoreChatNavigationTarget = (target: EventTarget | null): boolean => { + if (!isHTMLElement(target)) { + return false; + } + + return Boolean(target.closest(CHAT_NAVIGATION_IGNORED_TARGET_SELECTOR)); +}; + +const shouldIgnoreChatNavigationForFocus = (activeElement: Element | null, scrollContainer: HTMLElement | null): boolean => { + if (typeof document === 'undefined') { + return true; + } + + if (!activeElement || activeElement === document.body || activeElement === document.documentElement) { + return true; + } + + if (shouldIgnoreChatNavigationTarget(activeElement)) { + return true; + } + + return !scrollContainer?.contains(activeElement); +}; + +const hasBlockingChatOverlay = (): boolean => { + const { + isAboutDialogOpen, + isCommandPaletteOpen, + isHelpDialogOpen, + isImagePreviewOpen, + isMultiRunLauncherOpen, + isSessionSwitcherOpen, + isSettingsDialogOpen, + } = useUIStore.getState(); + + return isAboutDialogOpen + || isCommandPaletteOpen + || isHelpDialogOpen + || isImagePreviewOpen + || isMultiRunLauncherOpen + || isSessionSwitcherOpen + || isSettingsDialogOpen; +}; type HydratingToolSkeletonRow = { id: string; @@ -38,6 +126,162 @@ type HydratingToolSkeletonRow = { detailWidth: string; }; +type ChatViewportProps = { + currentSessionId: string; + isDesktopExpandedInput: boolean; + isMobile: boolean; + stickyUserHeader: boolean; + scrollRef: React.RefObject; + messageListRef: React.RefObject; + turnStart: number; + pendingRevealWork: boolean; + renderedMessages: SessionMessageRecord[]; + hasMoreAboveTurns: boolean; + isLoadingOlder: boolean; + sessionIsWorking: boolean; + streamingMessageId: string | null; + activeStreamingPhase: import('./message/types').StreamPhase | null; + retryOverlay: { + sessionId: string; + message: string; + confirmedAt?: number; + fallbackTimestamp?: number; + } | null; + handleMessageContentChange: (reason?: ContentChangeReason) => void; + getAnimationHandlers: (messageId: string) => AnimationHandlers; + handleLoadOlder: () => void; + scrollToBottom: (options?: { instant?: boolean; force?: boolean }) => void; + sessionQuestions: QuestionRequest[]; + sessionPermissions: PermissionRequest[]; + isProgrammaticFollowActive: boolean; +}; + +const ChatViewport = React.memo(({ + currentSessionId, + isDesktopExpandedInput, + isMobile, + stickyUserHeader, + scrollRef, + messageListRef, + turnStart, + pendingRevealWork, + renderedMessages, + hasMoreAboveTurns, + isLoadingOlder, + sessionIsWorking, + streamingMessageId, + activeStreamingPhase, + retryOverlay, + handleMessageContentChange, + getAnimationHandlers, + handleLoadOlder, + scrollToBottom, + sessionQuestions, + sessionPermissions, + isProgrammaticFollowActive, +}: ChatViewportProps) => { + const focusScrollContainer = React.useCallback((event: React.MouseEvent) => { + if (event.defaultPrevented || shouldIgnoreChatNavigationTarget(event.target)) { + return; + } + + if (typeof window !== 'undefined' && window.getSelection()?.type === 'Range') { + return; + } + + scrollRef.current?.focus({ preventScroll: true }); + }, [scrollRef]); + + return ( +
+
+ +
+ + {(sessionQuestions.length > 0 || sessionPermissions.length > 0) && ( +
+ {sessionQuestions.map((question) => ( + + ))} + {sessionPermissions.map((permission) => ( + + ))} +
+ )} + +
+ +
+ + + + +
+
+ ); +}, (prev, next) => { + return prev.currentSessionId === next.currentSessionId + && prev.isDesktopExpandedInput === next.isDesktopExpandedInput + && prev.isMobile === next.isMobile + && prev.stickyUserHeader === next.stickyUserHeader + && prev.scrollRef === next.scrollRef + && prev.messageListRef === next.messageListRef + && prev.turnStart === next.turnStart + && prev.pendingRevealWork === next.pendingRevealWork + && prev.renderedMessages === next.renderedMessages + && prev.hasMoreAboveTurns === next.hasMoreAboveTurns + && prev.isLoadingOlder === next.isLoadingOlder + && prev.sessionIsWorking === next.sessionIsWorking + && prev.streamingMessageId === next.streamingMessageId + && prev.activeStreamingPhase === next.activeStreamingPhase + && prev.retryOverlay === next.retryOverlay + && prev.handleMessageContentChange === next.handleMessageContentChange + && prev.getAnimationHandlers === next.getAnimationHandlers + && prev.handleLoadOlder === next.handleLoadOlder + && prev.scrollToBottom === next.scrollToBottom + && prev.sessionQuestions === next.sessionQuestions + && prev.sessionPermissions === next.sessionPermissions + && prev.isProgrammaticFollowActive === next.isProgrammaticFollowActive; +}); + +ChatViewport.displayName = 'ChatViewport'; + const HYDRATING_SKELETON_ITEMS: Array<{ id: number; toolRows: HydratingToolSkeletonRow[]; @@ -71,101 +315,179 @@ const HYDRATING_SKELETON_ITEMS: Array<{ ]; export const ChatContainer: React.FC = () => { - const { - currentSessionId, - loadMessages, - loadMoreMessages, - updateViewportAnchor, - openNewSessionDraft, - setCurrentSession, - newSessionDraft, - } = useSessionStore( - useShallow((state) => ({ - currentSessionId: state.currentSessionId, - loadMessages: state.loadMessages, - loadMoreMessages: state.loadMoreMessages, - updateViewportAnchor: state.updateViewportAnchor, - openNewSessionDraft: state.openNewSessionDraft, - setCurrentSession: state.setCurrentSession, - newSessionDraft: state.newSessionDraft, - })) + const { t } = useI18n(); + // Session UI state + const currentSessionId = useSessionUIStore((s) => s.currentSessionId); + const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft); + const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession); + const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft); + const updateViewportAnchor = useViewportStore((s) => s.updateViewportAnchor); + const isSyncing = useViewportStore((s) => s.isSyncing); + const sessionMemoryStateMap = useViewportStore((s) => s.sessionMemoryState); + + // Sync actions + const sync = useSync(); + const loadMessages = React.useCallback( + (sessionId: string) => sync.syncSession(sessionId), + [sync], + ); + const loadMoreMessages = React.useCallback( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (sessionId: string, _direction: 'up' | 'down') => sync.loadMore(sessionId), + [sync], ); - const { isSyncing, messageStreamStates, sessionMemoryStateMap } = useSessionStore( - useShallow((state) => ({ - isSyncing: state.isSyncing, - messageStreamStates: state.messageStreamStates, - sessionMemoryStateMap: state.sessionMemoryState, - })) - ); + // UI store + const isExpandedInput = useUIStore((state) => state.isExpandedInput); + const stickyUserHeader = useUIStore((state) => state.stickyUserHeader); + const chatRenderMode = useUIStore((state) => state.chatRenderMode); - const { - isTimelineDialogOpen, - setTimelineDialogOpen, - isExpandedInput, - stickyUserHeader, - chatRenderMode, - } = useUIStore(); - - const sessionMessages = useSessionStore( + // Streaming state + const streamingMessageId = useStreamingStore( React.useCallback( - (state) => (currentSessionId ? state.messages.get(currentSessionId) ?? EMPTY_MESSAGES : EMPTY_MESSAGES), - [currentSessionId] - ) + (s) => (currentSessionId ? s.streamingMessageIds.get(currentSessionId) ?? null : null), + [currentSessionId], + ), + ); + const activeStreamingPhase = useStreamingStore( + React.useCallback( + (s) => { + if (!streamingMessageId) return null; + return s.messageStreamStates.get(streamingMessageId)?.phase ?? null; + }, + [streamingMessageId], + ), + ); + const sessionMessageCount = useSessionMessageCount(currentSessionId ?? ''); + const hasLoadedSessionMessages = useDirectorySync( + React.useCallback( + (state) => (currentSessionId ? state.message[currentSessionId] !== undefined : false), + [currentSessionId], + ), + ); + // Messages from sync system + const sessionMessageRecords = useSessionMessageRecords(currentSessionId ?? ''); + const sessionMessages = currentSessionId ? sessionMessageRecords : EMPTY_MESSAGES; + + // Sessions from sync system + const sessions = useSessions(); + + // Plan detection - watches messages for plan creation and signals store + usePlanDetection(currentSessionId ?? ''); + + // Session status from sync system + const sessionStatusForCurrent = useSessionStatus(currentSessionId ?? '') ?? IDLE_SESSION_STATUS; + + // Permissions & questions from sync system + const allPermissions = useDirectorySync( + React.useCallback((s) => s.permission ?? {}, []), + ); + const allQuestions = useDirectorySync( + React.useCallback((s) => s.question ?? {}, []), ); - const sessions = useSessionStore((state) => state.sessions); + // Convert Record → Map for blockingRequests helpers + const permissionsMap = React.useMemo(() => { + const m = new Map(); + for (const [k, v] of Object.entries(allPermissions)) m.set(k, v as PermissionRequest[]); + return m; + }, [allPermissions]); - const blockingRequestState = useSessionStore( - useShallow((state) => ({ - sessions: state.sessions, - permissions: state.permissions, - questions: state.questions, - })) - ); + const questionsMap = React.useMemo(() => { + const m = new Map(); + for (const [k, v] of Object.entries(allQuestions)) m.set(k, v as QuestionRequest[]); + return m; + }, [allQuestions]); const scopedSessionIds = React.useMemo( () => collectVisibleSessionIdsForBlockingRequests( - blockingRequestState.sessions.map((session) => ({ id: session.id, parentID: session.parentID })), + sessions.map((session) => ({ id: session.id, parentID: session.parentID })), currentSessionId, ), - [blockingRequestState.sessions, currentSessionId] + [sessions, currentSessionId], ); const sessionPermissions = React.useMemo(() => { if (scopedSessionIds.length === 0) return EMPTY_PERMISSIONS; - return flattenBlockingRequests(blockingRequestState.permissions, scopedSessionIds); - }, [blockingRequestState.permissions, scopedSessionIds]); + return flattenBlockingRequests(permissionsMap, scopedSessionIds); + }, [permissionsMap, scopedSessionIds]); const sessionQuestions = React.useMemo(() => { if (scopedSessionIds.length === 0) return EMPTY_QUESTIONS; - return flattenBlockingRequests(blockingRequestState.questions, scopedSessionIds); - }, [blockingRequestState.questions, scopedSessionIds]); + return flattenBlockingRequests(questionsMap, scopedSessionIds); + }, [questionsMap, scopedSessionIds]); + const sessionIsWorking = React.useMemo(() => { + if (!currentSessionId || sessionPermissions.length > 0 || sessionQuestions.length > 0) { + return false; + } - const historyMeta = useSessionStore( - React.useCallback( - (state) => (currentSessionId ? state.sessionHistoryMeta.get(currentSessionId) ?? null : null), - [currentSessionId] - ) - ); + if (streamingMessageId || activeStreamingPhase) { + return true; + } - const streamingMessageId = useSessionStore( - React.useCallback( - (state) => (currentSessionId ? state.streamingMessageIds.get(currentSessionId) ?? null : null), - [currentSessionId] - ) - ); + const statusType = sessionStatusForCurrent.type ?? 'idle'; + if (statusType === 'busy' || statusType === 'retry') { + return true; + } - const sessionStatusForCurrent = useSessionStore( - React.useCallback( - (state) => (currentSessionId ? state.sessionStatus?.get(currentSessionId) ?? IDLE_SESSION_STATUS : IDLE_SESSION_STATUS), - [currentSessionId] - ) - ); + const lastMessage = sessionMessages[sessionMessages.length - 1]?.info as Message | undefined; + return Boolean( + lastMessage + && lastMessage.role === 'assistant' + && typeof (lastMessage as { time?: { completed?: number } }).time?.completed !== 'number', + ); + }, [activeStreamingPhase, currentSessionId, sessionMessages, sessionPermissions.length, sessionQuestions.length, sessionStatusForCurrent.type, streamingMessageId]); + const activeRetryStatus = React.useMemo(() => { + if (!currentSessionId || sessionStatusForCurrent.type !== 'retry') { + return null; + } - const hasSessionMessagesEntry = useSessionStore( - React.useCallback((state) => (currentSessionId ? state.messages.has(currentSessionId) : false), [currentSessionId]) - ); + const rawMessage = typeof (sessionStatusForCurrent as { message?: string }).message === 'string' + ? (((sessionStatusForCurrent as { message?: string }).message) ?? '').trim() + : ''; + + return { + sessionId: currentSessionId, + message: rawMessage || DEFAULT_RETRY_MESSAGE, + confirmedAt: (sessionStatusForCurrent as { confirmedAt?: number }).confirmedAt, + }; + }, [currentSessionId, sessionStatusForCurrent]); + const [retryFallbackTimestamp, setRetryFallbackTimestamp] = React.useState(0); + const retryFallbackSessionRef = React.useRef(null); + + React.useEffect(() => { + if (!activeRetryStatus || typeof activeRetryStatus.confirmedAt === 'number') { + retryFallbackSessionRef.current = null; + setRetryFallbackTimestamp(0); + return; + } + + if (retryFallbackSessionRef.current !== activeRetryStatus.sessionId) { + retryFallbackSessionRef.current = activeRetryStatus.sessionId; + setRetryFallbackTimestamp(Date.now()); + } + }, [activeRetryStatus]); + + const retryOverlay = React.useMemo(() => { + if (!activeRetryStatus) { + return null; + } + + return { + ...activeRetryStatus, + fallbackTimestamp: retryFallbackTimestamp, + }; + }, [activeRetryStatus, retryFallbackTimestamp]); + + // History metadata — use sync's hasMore/isLoading + const historyMeta = React.useMemo(() => { + if (!currentSessionId) return null; + return { + limit: sessionMessages.length, + complete: !sync.hasMore(currentSessionId), + loading: sync.isLoading(currentSessionId), + }; + }, [currentSessionId, sessionMessages.length, sync]); const { isMobile } = useDeviceInfo(); const draftOpen = Boolean(newSessionDraft?.open); @@ -173,24 +495,19 @@ export const ChatContainer: React.FC = () => { const messageListRef = React.useRef(null); const parentSession = React.useMemo(() => { - if (!currentSessionId) { - return null; - } - + if (!currentSessionId) return null; const current = sessions.find((session) => session.id === currentSessionId); const parentID = current?.parentID; - if (!parentID) { - return null; - } - - return sessions.find((session) => session.id === parentID) ?? null; + if (!parentID) return null; + return sessions.find((session) => session.id === parentID) + ?? getAllSyncSessions().find((session) => session.id === parentID) + ?? null; }, [currentSessionId, sessions]); const handleReturnToParentSession = React.useCallback(() => { - if (!parentSession) { - return; - } - void setCurrentSession(parentSession.id); + if (!parentSession) return; + const parentDirectory = (parentSession as Session & { directory?: string | null }).directory ?? null; + setCurrentSession(parentSession.id, parentDirectory); }, [parentSession, setCurrentSession]); const returnToParentButton = parentSession ? ( @@ -200,11 +517,13 @@ export const ChatContainer: React.FC = () => { size="xs" onClick={handleReturnToParentSession} className="absolute left-3 top-3 z-20 !font-normal bg-[var(--surface-background)]/95" - aria-label="Return to parent session" - title={parentSession.title?.trim() ? `Return to: ${parentSession.title}` : 'Return to parent session'} + aria-label={t('chat.container.returnToParent.aria')} + title={parentSession.title?.trim() + ? t('chat.container.returnToParent.titleNamed', { title: parentSession.title }) + : t('chat.container.returnToParent.title')} > - Parent + {t('chat.container.returnToParent.label')} ) : null; @@ -219,83 +538,146 @@ export const ChatContainer: React.FC = () => { }, [sessionPermissions, sessionQuestions]); const activeTurnChangeRef = React.useRef<(turnId: string | null) => void>(() => {}); + const handleActiveTurnChange = React.useCallback((turnId: string | null) => { + activeTurnChangeRef.current(turnId); + }, []); const { scrollRef, handleMessageContentChange, getAnimationHandlers, + prepareForBottomResume, scrollToBottom, - releasePinnedScroll, isPinned, isOverflowing, isProgrammaticFollowActive, } = useChatScrollManager({ currentSessionId, - sessionMessages, - streamingMessageId, + sessionMessageCount, + sessionIsWorking, sessionMemoryState: sessionMemoryStateMap, updateViewportAnchor, isSyncing, isMobile, chatRenderMode, - messageStreamStates, sessionPermissions: sessionBlockingCards, - onActiveTurnChange: (turnId) => { - activeTurnChangeRef.current(turnId); - }, + onActiveTurnChange: handleActiveTurnChange, }); + const viewportMessages = sessionMessages; + const timelineController = useChatTimelineController({ sessionId: currentSessionId, - messages: sessionMessages, + messages: viewportMessages, historyMeta, scrollRef, messageListRef, loadMoreMessages, + prepareForBottomResume, scrollToBottom, isPinned, isOverflowing, }); - const { resumeToBottomInstant } = timelineController; + const { loadEarlier, resumeToBottomInstant } = timelineController; + + const runLatestInstantResume = React.useCallback(async () => { + if (!currentSessionId) { + scrollToBottom({ instant: true, force: true }); + return; + } + await resumeToBottomInstant(); + }, [currentSessionId, resumeToBottomInstant, scrollToBottom]); + + const resumeToLatestInstant = React.useCallback(() => { + void runLatestInstantResume(); + }, [runLatestInstantResume]); React.useEffect(() => { activeTurnChangeRef.current = timelineController.handleActiveTurnChange; }, [timelineController.handleActiveTurnChange]); + React.useEffect(() => { + if (sessionPermissions.length === 0 && sessionQuestions.length === 0) { + return; + } + handleMessageContentChange('permission'); + }, [handleMessageContentChange, sessionPermissions, sessionQuestions]); + + const handleLoadOlder = React.useCallback(() => { + void loadEarlier(); + }, [loadEarlier]); + const navigation = useChatTurnNavigation({ sessionId: currentSessionId, turnIds: timelineController.turnIds, activeTurnId: timelineController.activeTurnId, scrollToTurn: timelineController.scrollToTurn, scrollToMessage: timelineController.scrollToMessage, - resumeToBottom: timelineController.resumeToBottom, + resumeToBottom: timelineController.resumeToBottomInstant, }); React.useEffect(() => { - if (typeof window === 'undefined' || !currentSessionId) { + if (typeof window === 'undefined' || !currentSessionId || isDesktopExpandedInput) { return; } - const handleSessionReselected = (event: Event) => { - const customEvent = event as CustomEvent; - if (customEvent.detail !== currentSessionId) { + const handleChatTurnKeyDown = (event: KeyboardEvent) => { + if (event.defaultPrevented || event.isComposing) { return; } - resumeToBottomInstant(); + if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { + return; + } + + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { + return; + } + + const { activeMainTab } = useUIStore.getState(); + if (activeMainTab !== 'chat' || hasBlockingChatOverlay()) { + return; + } + + const scrollContainer = scrollRef.current; + if (shouldIgnoreChatNavigationForFocus(document.activeElement, scrollContainer)) { + return; + } + + if (shouldIgnoreChatNavigationTarget(event.target)) { + return; + } + + event.preventDefault(); + const offset = event.key === 'ArrowUp' ? -1 : 1; + void navigation.scrollByTurnOffset(offset, { resumePastEnd: false }); + }; + + window.addEventListener('keydown', handleChatTurnKeyDown); + return () => { + window.removeEventListener('keydown', handleChatTurnKeyDown); + }; + }, [currentSessionId, isDesktopExpandedInput, navigation, scrollRef]); + + React.useEffect(() => { + if (typeof window === 'undefined' || !currentSessionId) return; + + const handleSessionReselected = (event: Event) => { + const customEvent = event as CustomEvent; + if (customEvent.detail !== currentSessionId) return; + if (isPinned || !isOverflowing || isProgrammaticFollowActive) return; + void resumeToBottomInstant(); }; window.addEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener); return () => { window.removeEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener); }; - }, [currentSessionId, resumeToBottomInstant]); + }, [currentSessionId, isOverflowing, isPinned, isProgrammaticFollowActive, resumeToBottomInstant]); React.useLayoutEffect(() => { const container = scrollRef.current; - if (!container) { - return; - } + if (!container) return; const updateChatScrollHeight = () => { container.style.setProperty('--chat-scroll-height', `${container.clientHeight}px`); @@ -329,37 +711,56 @@ export const ChatContainer: React.FC = () => { }; }, [currentSessionId, isDesktopExpandedInput, scrollRef]); - const hasHistoryMetadata = React.useMemo(() => { - return Boolean(historyMeta); - }, [historyMeta]); + const lastScrolledSessionRef = React.useRef(null); const isSessionHydrating = Boolean(currentSessionId) - && (!hasSessionMessagesEntry || !hasHistoryMetadata || historyMeta?.loading === true); + && !hasLoadedSessionMessages; React.useEffect(() => { if (!currentSessionId) { return; } - const hasSessionMessages = hasSessionMessagesEntry; - if (hasSessionMessages && hasHistoryMetadata) { + if (lastScrolledSessionRef.current === currentSessionId) { return; } + const hasHashTarget = typeof window !== 'undefined' && window.location.hash.length > 0; + if (hasHashTarget) { + lastScrolledSessionRef.current = currentSessionId; + return; + } + + lastScrolledSessionRef.current = currentSessionId; + + if (typeof window === 'undefined') { + resumeToLatestInstant(); + return; + } + + window.requestAnimationFrame(() => { + resumeToLatestInstant(); + }); + }, [currentSessionId, resumeToLatestInstant]); + + React.useEffect(() => { + if (!currentSessionId) return; + if (hasLoadedSessionMessages) return; + const load = async () => { await loadMessages(currentSessionId).finally(() => { const statusType = sessionStatusForCurrent.type ?? 'idle'; const isActivePhase = statusType === 'busy' || statusType === 'retry'; const hasHashTarget = typeof window !== 'undefined' && window.location.hash.length > 0; - const shouldSkipScroll = (isActivePhase && isPinned) || hasHashTarget; + const shouldSkipScroll = hasHashTarget || (isActivePhase && isPinned); if (!shouldSkipScroll) { if (typeof window === 'undefined') { - scrollToBottom({ instant: true }); + resumeToLatestInstant(); } else { window.requestAnimationFrame(() => { - scrollToBottom({ instant: true }); + resumeToLatestInstant(); }); } } @@ -367,41 +768,35 @@ export const ChatContainer: React.FC = () => { }; void load(); - }, [currentSessionId, hasHistoryMetadata, hasSessionMessagesEntry, isPinned, loadMessages, scrollToBottom, sessionMessages.length, sessionStatusForCurrent.type]); + }, [currentSessionId, hasLoadedSessionMessages, isPinned, loadMessages, resumeToLatestInstant, sessionStatusForCurrent.type]); - if (!currentSessionId && !draftOpen) { - return ( -
- -
- ); - } + if (!currentSessionId && !draftOpen) { + return ( +
+ +
+ ); + } - if (!currentSessionId && draftOpen) { - return ( -
- {!isDesktopExpandedInput ? ( -
- -
- ) : null} + if (!currentSessionId && draftOpen) { + return ( +
+ {!isDesktopExpandedInput ? ( +
+ +
+ ) : null}
- -
-
+ isDesktopExpandedInput + ? 'flex-1 min-h-0 bg-background' + : 'bg-background' + )} + > + +
+
); } @@ -409,23 +804,20 @@ export const ChatContainer: React.FC = () => { return null; } - if (isSessionHydrating && sessionMessages.length === 0 && !streamingMessageId) { - return ( -
- {returnToParentButton} -
+ {returnToParentButton} +
-
+
{HYDRATING_SKELETON_ITEMS.map((item) => (
@@ -457,26 +849,23 @@ export const ChatContainer: React.FC = () => {
- -
+ isDesktopExpandedInput + ? 'flex-1 min-h-0 bg-background' + : 'bg-background' + )} + > + +
); } - if (sessionMessages.length === 0 && !streamingMessageId) { - return ( -
- {returnToParentButton} -
+ {returnToParentButton} +
{
- -
+ isDesktopExpandedInput + ? 'flex-1 min-h-0 bg-background' + : 'bg-background' + )} + > + +
); } - return ( -
- {returnToParentButton} -
-
- -
- { - void timelineController.loadEarlier(); - }} - scrollToBottom={scrollToBottom} - scrollRef={scrollRef} - /> -
-
- -
-
+ return ( +
+ {returnToParentButton} +
- {!isDesktopExpandedInput && sessionMessages.length > 0 && ( - + {!isDesktopExpandedInput && sessionMessages.length > 0 && ( + )} - +
- - { - releasePinnedScroll(); - return navigation.scrollToMessageId(messageId, { behavior: 'smooth', updateHash: false }); - }} - onScrollByTurnOffset={(offset) => { - releasePinnedScroll(); - void navigation.scrollByTurnOffset(offset); - }} - onResumeToLatest={navigation.resumeToLatest} - />
); }; diff --git a/src/packages/ui/src/components/chat/ChatEmptyState.tsx b/src/packages/ui/src/components/chat/ChatEmptyState.tsx index 00c30ee..e5ad6e9 100644 --- a/src/packages/ui/src/components/chat/ChatEmptyState.tsx +++ b/src/packages/ui/src/components/chat/ChatEmptyState.tsx @@ -1,45 +1,29 @@ import React from 'react'; import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; -import { TextLoop } from '@/components/ui/TextLoop'; import { useThemeSystem } from '@/contexts/useThemeSystem'; - -const phrases = [ - "Fix the failing tests", - "Refactor this to be more readable", - "Add form validation", - "Optimize this function", - "Write tests for this", - "Explain how this works", - "Add a new feature", - "Help me debug this", - "Review my code", - "Simplify this logic", - "Add error handling", - "Create a new component", - "Update the documentation", - "Find the bug here", - "Improve performance", - "Add type definitions", -]; +import { useGlobalSyncStore } from '@/sync/global-sync-store'; +import { useI18n } from '@/lib/i18n'; const ChatEmptyState: React.FC = () => { + const { t } = useI18n(); const { currentTheme } = useThemeSystem(); + const initError = useGlobalSyncStore((s) => s.error); - // Use theme's muted foreground for secondary text const textColor = currentTheme?.colors?.surface?.mutedForeground || 'var(--muted-foreground)'; return (
- - - {phrases.map((phrase) => ( - "{phrase}…" - ))} - + + {initError ? ( +
+ {t('chat.emptyState.opencodeUnreachable')} + + {initError.message} + +
+ ) : ( + {t('chat.emptyState.startNewChat')} + )}
); }; diff --git a/src/packages/ui/src/components/chat/ChatErrorBoundary.tsx b/src/packages/ui/src/components/chat/ChatErrorBoundary.tsx index c2fc26a..29d3e12 100644 --- a/src/packages/ui/src/components/chat/ChatErrorBoundary.tsx +++ b/src/packages/ui/src/components/chat/ChatErrorBoundary.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { RiChat3Line, RiRestartLine } from '@remixicon/react'; import { Button } from '../ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { useI18n } from '@/lib/i18n'; interface ChatErrorBoundaryState { hasError: boolean; @@ -14,8 +15,21 @@ interface ChatErrorBoundaryProps { sessionId?: string; } -export class ChatErrorBoundary extends React.Component { - constructor(props: ChatErrorBoundaryProps) { +interface ChatErrorBoundaryTexts { + title: string; + description: string; + sessionLabel: string; + detailsSummary: string; + resetAction: string; + persistentHint: string; +} + +interface ChatErrorBoundaryViewProps extends ChatErrorBoundaryProps { + texts: ChatErrorBoundaryTexts; +} + +class ChatErrorBoundaryView extends React.Component { + constructor(props: ChatErrorBoundaryViewProps) { super(props); this.state = { hasError: false }; } @@ -44,24 +58,24 @@ export class ChatErrorBoundary extends React.Component - Chat Error + {this.props.texts.title}

- The chat interface encountered an error. This might be due to a temporary network issue or corrupted message data. + {this.props.texts.description}

{this.props.sessionId && (
- Session: {this.props.sessionId} + {this.props.texts.sessionLabel}: {this.props.sessionId}
)} {this.state.error && (
- Error details -
+                  {this.props.texts.detailsSummary}
+                  
                     {this.state.error.toString()}
                   
@@ -70,12 +84,12 @@ export class ChatErrorBoundary extends React.Component
- If the problem persists, try refreshing the page. + {this.props.texts.persistentHint}
@@ -86,3 +100,20 @@ export class ChatErrorBoundary extends React.Component + ); +} diff --git a/src/packages/ui/src/components/chat/ChatInput.tsx b/src/packages/ui/src/components/chat/ChatInput.tsx index dd1526d..24b724d 100644 --- a/src/packages/ui/src/components/chat/ChatInput.tsx +++ b/src/packages/ui/src/components/chat/ChatInput.tsx @@ -16,37 +16,40 @@ import { RiSendPlane2Line, } from '@remixicon/react'; import { BrowserVoiceButton } from '@/components/voice'; -import { useSessionStore } from '@/stores/useSessionStore'; -import { useSessionStore as useSessionManagementStore } from '@/stores/sessionStore'; +// sessionStore removed — currentSessionId comes from useSessionUIStore import { useConfigStore } from '@/stores/useConfigStore'; import { useUIStore } from '@/stores/useUIStore'; import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore'; +import { useSessionUIStore } from '@/sync/session-ui-store'; +import { useSelectionStore } from '@/sync/selection-store'; +import { useInputStore } from '@/sync/input-store'; import type { AttachedFile } from '@/stores/types/sessionTypes'; +import * as sessionActions from '@/sync/session-actions'; +import { useUserMessageHistory } from '@/sync/sync-context'; import { useInlineCommentDraftStore, type InlineCommentDraft } from '@/stores/useInlineCommentDraftStore'; import { appendInlineComments } from '@/lib/messages/inlineComments'; +import { renderMagicPrompt } from '@/lib/magicPrompts'; import { AttachedFilesList } from './FileAttachment'; import { QueuedMessageChips } from './QueuedMessageChips'; import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete'; -import { CommandAutocomplete, type CommandAutocompleteHandle } from './CommandAutocomplete'; +import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from './CommandAutocomplete'; import { SkillAutocomplete, type SkillAutocompleteHandle } from './SkillAutocomplete'; import { cn, formatDirectoryName, isMacOS } from '@/lib/utils'; import { ModelControls } from './ModelControls'; -import { UnifiedControlsDrawer } from './UnifiedControlsDrawer'; import { parseAgentMentions } from '@/lib/messages/agentMentions'; import { StatusRow } from './StatusRow'; +import { PendingChangesBar } from './PendingChangesBar'; import { MobileAgentButton } from './MobileAgentButton'; import { MobileModelButton } from './MobileModelButton'; import { MobileSessionStatusBar } from './MobileSessionStatusBar'; -import { useAssistantStatus } from '@/hooks/useAssistantStatus'; import { useCurrentSessionActivity } from '@/hooks/useSessionActivity'; import { toast } from '@/components/ui'; -import { useFileStore } from '@/stores/fileStore'; -import { useMessageStore } from '@/stores/messageStore'; +// useMessageStore removed — messages now come from sync system import { isTauriShell, isVSCodeRuntime } from '@/lib/desktop'; import { isIMECompositionEvent } from '@/lib/ime'; import { StopIcon } from '@/components/icons/StopIcon'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import type { MobileControlsPanel } from './mobileControlsUtils'; +import { getCycledPrimaryAgentName, type MobileControlsPanel } from './mobileControlsUtils'; import { DropdownMenu, DropdownMenuContent, @@ -61,10 +64,14 @@ import { useChatSearchDirectory } from '@/hooks/useChatSearchDirectory'; import { opencodeClient } from '@/lib/opencode/client'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, getProjectIconImageUrl } from '@/lib/projectMeta'; -import { useGitBranches, useGitStore } from '@/stores/useGitStore'; +import { useGitBranches, useGitStore, useIsGitRepo } from '@/stores/useGitStore'; +import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { createWorktreeDraft } from '@/lib/worktreeSessionCreator'; +import { buildSessionTargetOptions } from '@/sync/session-worktree-contract'; import { usePermissionStore } from '@/stores/permissionStore'; +import { extractGitChangedFiles } from './changedFiles'; +import { useI18n } from '@/lib/i18n'; const MAX_VISIBLE_TEXTAREA_LINES = 8; const EMPTY_QUEUE: QueuedMessage[] = []; @@ -81,6 +88,28 @@ const VS_CODE_DROP_DATA_TYPES = [ const FILE_URI_PREFIX = 'file://'; +const encodeFilePath = (filepath: string): string => { + let normalized = filepath.replace(/\\/g, '/'); + if (/^[A-Za-z]:/.test(normalized)) { + normalized = `/${normalized}`; + } + return normalized + .split('/') + .map((segment, index) => { + if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment; + return encodeURIComponent(segment); + }) + .join('/'); +}; + +const toServerFileUrl = (filepath: string): string => { + const normalized = filepath.replace(/\\/g, '/').trim(); + if (normalized.toLowerCase().startsWith(FILE_URI_PREFIX)) { + return normalized; + } + return `file://${encodeFilePath(normalized)}`; +}; + const isLikelyAbsolutePath = (value: string): boolean => ( value.startsWith('/') || value.startsWith('\\\\') @@ -198,6 +227,373 @@ const getProjectIconColor = (projectColor?: string | null): string | undefined = return PROJECT_COLOR_MAP[projectColor] ?? undefined; }; +const MemoModelControls = React.memo(ModelControls); +const MemoBrowserVoiceButton = React.memo(BrowserVoiceButton); +const MemoMobileAgentButton = React.memo(MobileAgentButton); +const MemoMobileModelButton = React.memo(MobileModelButton); +const MemoStatusRow = React.memo(StatusRow); + +type ComposerAttachmentControlsProps = { + isMobile: boolean; + isVSCode: boolean; + footerIconButtonClass: string; + iconSizeClass: string; + fileInputRef: React.RefObject; + handleLocalFileSelect: (event: React.ChangeEvent) => void | Promise; + handlePickLocalFiles: () => void; + handleOpenCommandMenu: () => void; + openIssuePicker: () => void; + openPrPicker: () => void; + onOpenSettings?: () => void; +}; + +const ComposerAttachmentControls = React.memo(function ComposerAttachmentControls(props: ComposerAttachmentControlsProps) { + const { t } = useI18n(); + const { + isMobile, + isVSCode, + footerIconButtonClass, + iconSizeClass, + fileInputRef, + handleLocalFileSelect, + handlePickLocalFiles, + handleOpenCommandMenu, + openIssuePicker, + openPrPicker, + onOpenSettings, + } = props; + + return ( +
+ {isMobile ? ( + + ) : null} + + +
+ {isVSCode ? ( + + ) : ( + + + + + + { + requestAnimationFrame(handlePickLocalFiles); + }} + > + + {t('chat.chatInput.actions.attachFiles')} + + { + requestAnimationFrame(openIssuePicker); + }} + > + + {t('chat.chatInput.actions.linkGithubIssue')} + + { + requestAnimationFrame(openPrPicker); + }} + > + + {t('chat.chatInput.actions.linkGithubPr')} + + + + )} +
+ + {onOpenSettings ? ( + + ) : null} +
+ ); +}, (prev, next) => ( + prev.isMobile === next.isMobile + && prev.isVSCode === next.isVSCode + && prev.footerIconButtonClass === next.footerIconButtonClass + && prev.iconSizeClass === next.iconSizeClass + && prev.onOpenSettings === next.onOpenSettings +)); + +type PermissionAutoAcceptButtonProps = { + footerIconButtonClass: string; + iconSizeClass: string; + permissionScopeSessionId: string | null; + permissionAutoAcceptEnabled: boolean; + handlePermissionAutoAcceptToggle: () => void; + withTooltip?: boolean; +}; + +const PermissionAutoAcceptButton = React.memo(function PermissionAutoAcceptButton(props: PermissionAutoAcceptButtonProps) { + const { t } = useI18n(); + const { + footerIconButtonClass, + iconSizeClass, + permissionScopeSessionId, + permissionAutoAcceptEnabled, + handlePermissionAutoAcceptToggle, + withTooltip = false, + } = props; + + const ariaLabel = permissionAutoAcceptEnabled + ? t('chat.chatInput.permissionAutoAccept.disable') + : t('chat.chatInput.permissionAutoAccept.enable'); + const tooltipLabel = permissionAutoAcceptEnabled + ? t('chat.chatInput.permissionAutoAccept.on') + : t('chat.chatInput.permissionAutoAccept.off'); + + const button = ( + + ); + + if (!withTooltip) { + return button; + } + + return ( + + + {button} + + + {tooltipLabel} + + + ); +}); + +type FocusModeButtonProps = { + footerIconButtonClass: string; + iconSizeClass: string; + isExpandedInput: boolean; + onToggle: () => void; +}; + +const FocusModeButton = React.memo(function FocusModeButton(props: FocusModeButtonProps) { + const { footerIconButtonClass, iconSizeClass, isExpandedInput, onToggle } = props; + const { t } = useI18n(); + + return ( + + + + + +
+ {t('chat.chatInput.focusMode.label')} + + {isMacOS() ? '⌘⇧E' : 'Ctrl+Shift+E'} + +
+
+
+ ); +}); + +type ComposerActionButtonsProps = { + isMobile: boolean; + footerIconButtonClass: string; + sendIconSizeClass: string; + stopIconSizeClass: string; + canSend: boolean; + canAbort: boolean; + hasContent: boolean; + currentSessionId: string | null; + newSessionDraftOpen: boolean; + onPrimaryAction: () => void; + onQueueMessage: () => void; + onAbort: () => void; +}; + +const ComposerActionButtons = React.memo(function ComposerActionButtons(props: ComposerActionButtonsProps) { + const { + isMobile, + footerIconButtonClass, + sendIconSizeClass, + stopIconSizeClass, + canSend, + canAbort, + hasContent, + currentSessionId, + newSessionDraftOpen, + onPrimaryAction, + onQueueMessage, + onAbort, + } = props; + const { t } = useI18n(); + + const sendButton = ( + + ); + + if (!canAbort) { + return sendButton; + } + + return ( +
+ {hasContent ? ( + + ) : null} + +
+ ); +}, (prev, next) => ( + prev.isMobile === next.isMobile + && prev.footerIconButtonClass === next.footerIconButtonClass + && prev.sendIconSizeClass === next.sendIconSizeClass + && prev.stopIconSizeClass === next.stopIconSizeClass + && prev.canSend === next.canSend + && prev.canAbort === next.canAbort + && prev.hasContent === next.hasContent + && prev.currentSessionId === next.currentSessionId + && prev.newSessionDraftOpen === next.newSessionDraftOpen +)); + const appendWithLineBreaks = (base: string, next: string): string => { const separator = !base ? '' @@ -266,14 +662,46 @@ const saveStoredDraft = (sessionId: string | null, draft: string): void => { } }; -export const ChatInput: React.FC = ({ onOpenSettings, scrollToBottom }) => { +// Per-session confirmed mentions key — tracks which @mentions are confirmed (blue) vs plain text +const getConfirmedMentionsKey = (sessionId: string | null): string => + `openchamber_chat_confirmed_mentions_${sessionId ?? 'new'}`; + +const saveConfirmedMentions = (sessionId: string | null, mentions: Set): void => { + try { + if (mentions.size > 0) { + localStorage.setItem(getConfirmedMentionsKey(sessionId), JSON.stringify([...mentions])); + } else { + localStorage.removeItem(getConfirmedMentionsKey(sessionId)); + } + } catch { + // Ignore localStorage errors + } +}; + +const loadConfirmedMentions = (sessionId: string | null): Set => { + try { + const raw = localStorage.getItem(getConfirmedMentionsKey(sessionId)); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + return new Set(parsed.filter((v): v is string => typeof v === 'string')); + } + } + } catch { + // Ignore localStorage errors + } + return new Set(); +}; + +const ChatInputComponent: React.FC = ({ onOpenSettings, scrollToBottom }) => { + const { t } = useI18n(); // Track if we restored a draft on mount (for text selection) const initialDraftRef = React.useRef(null); // Track initial session ID (captured at mount time for draft restoration) const initialSessionIdRef = React.useRef(null); const [message, setMessage] = React.useState(() => { // Read per-session draft at mount time using the current session from the store - const sessionId = useSessionStore.getState().currentSessionId; + const sessionId = useSessionUIStore.getState().currentSessionId; initialSessionIdRef.current = sessionId; const draft = getStoredDraft(sessionId); if (draft) { @@ -281,8 +709,14 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } return draft; }); + // Restore confirmed mentions from localStorage on mount + const confirmedMentionsRef = React.useRef>(loadConfirmedMentions(initialSessionIdRef.current)); + // Helper: check if a mention path looks like a file/folder (has path separators, extension, or was explicitly confirmed) + const isConfirmedFilePath = (text: string): boolean => + text.includes('/') || text.includes('\\') || text.includes('.') || confirmedMentionsRef.current.has(text); const [inputMode, setInputMode] = React.useState<'normal' | 'shell'>('normal'); const [isDragging, setIsDragging] = React.useState(false); + const [isInternalDrag, setIsInternalDrag] = React.useState(false); const [showFileMention, setShowFileMention] = React.useState(false); const [mentionQuery, setMentionQuery] = React.useState(''); const [showCommandAutocomplete, setShowCommandAutocomplete] = React.useState(false); @@ -291,13 +725,15 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const [showSkillAutocomplete, setShowSkillAutocomplete] = React.useState(false); const [skillQuery, setSkillQuery] = React.useState(''); const [textareaSize, setTextareaSize] = React.useState<{ height: number; maxHeight: number } | null>(null); - const [mobileControlsOpen, setMobileControlsOpen] = React.useState(false); const [mobileControlsPanel, setMobileControlsPanel] = React.useState(null); // Message history navigation state (up/down arrow to recall previous messages) const [historyIndex, setHistoryIndex] = React.useState(-1); // -1 = not browsing, 0+ = index from most recent const [draftMessage, setDraftMessage] = React.useState(''); // Preserves input when entering history mode const textareaRef = React.useRef(null); + const cursorPosRef = React.useRef(0); + const previousMessageLengthRef = React.useRef(message.length); const dropZoneRef = React.useRef(null); + const dragEnterCountRef = React.useRef(0); const suppressNextFileDropTextInsertRef = React.useRef(false); const suppressNextFileDropTextInsertTimeoutRef = React.useRef | null>(null); const pendingDroppedAbsolutePathsRef = React.useRef([]); @@ -313,54 +749,77 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const lastPersistedDraftRef = React.useRef>(new Map()); const currentSessionIdForDraftRef = React.useRef(null); - const sendMessage = useSessionStore((state) => state.sendMessage); - const currentSessionId = useSessionStore((state) => state.currentSessionId); - const newSessionDraft = useSessionStore((state) => state.newSessionDraft); + // TODO: port sendMessage to session-actions (complex — creates sessions, handles attachments, etc.) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sendMessage = React.useRef((...args: any[]) => + Promise.resolve((useSessionUIStore.getState().sendMessage as (...a: unknown[]) => unknown)(...args)), + ).current; + const currentSessionId = useSessionUIStore((s) => s.currentSessionId); + const currentDirectory = useDirectoryStore((s) => s.currentDirectory); + const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft); const newSessionDraftOpen = Boolean(newSessionDraft?.open); - const setNewSessionDraftTarget = useSessionStore((state) => state.setNewSessionDraftTarget); - const availableWorktreesByProject = useSessionStore((state) => state.availableWorktreesByProject); - const abortCurrentOperation = useSessionStore((state) => state.abortCurrentOperation); - const acknowledgeSessionAbort = useSessionStore((state) => state.acknowledgeSessionAbort); - const abortPromptSessionId = useSessionStore((state) => state.abortPromptSessionId); - const clearAbortPrompt = useSessionStore((state) => state.clearAbortPrompt); - const attachedFiles = useSessionStore((state) => state.attachedFiles); - const addAttachedFile = useSessionStore((state) => state.addAttachedFile); - const clearAttachedFiles = useSessionStore((state) => state.clearAttachedFiles); - const saveSessionAgentSelection = useSessionStore((state) => state.saveSessionAgentSelection); - const consumePendingInputText = useSessionStore((state) => state.consumePendingInputText); - const setPendingInputText = useSessionStore((state) => state.setPendingInputText); - const pendingInputText = useSessionStore((state) => state.pendingInputText); - const consumePendingSyntheticParts = useSessionStore((state) => state.consumePendingSyntheticParts); - const currentManagementSessionId = useSessionManagementStore((state) => state.currentSessionId); + const setNewSessionDraftTarget = useSessionUIStore((s) => s.setNewSessionDraftTarget); + const availableWorktreesByProject = useSessionUIStore((s) => s.availableWorktreesByProject); + const abortPromptSessionId = useSessionUIStore((s) => s.abortPromptSessionId); + const clearAbortPrompt = useSessionUIStore((s) => s.clearAbortPrompt); + const attachedFiles = useInputStore((s) => s.attachedFiles); + const addAttachedFile = useInputStore((s) => s.addAttachedFile); + const clearAttachedFiles = useInputStore((s) => s.clearAttachedFiles); + const saveSessionAgentSelection = useSelectionStore((s) => s.saveSessionAgentSelection); + const consumePendingInputText = useInputStore((s) => s.consumePendingInputText); + const setPendingInputText = useInputStore((s) => s.setPendingInputText); + const pendingInputText = useInputStore((s) => s.pendingInputText); + const consumePendingSyntheticParts = useInputStore((s) => s.consumePendingSyntheticParts); + const acknowledgeSessionAbort = useSessionUIStore((s) => s.acknowledgeSessionAbort); + const abortCurrentOperation = React.useCallback( + (sessionIdOverride?: string) => sessionActions.abortCurrentOperation(sessionIdOverride ?? currentSessionId ?? ''), + [currentSessionId], + ); + const currentManagementSessionId = currentSessionId; const projects = useProjectsStore((state) => state.projects); const activeProjectId = useProjectsStore((state) => state.activeProjectId); const setActiveProjectIdOnly = useProjectsStore((state) => state.setActiveProjectIdOnly); - const { currentProviderId, currentModelId, currentVariant, currentAgentName, setAgent, getVisibleAgents } = useConfigStore(); + const currentProviderId = useConfigStore((state) => state.currentProviderId); + const currentModelId = useConfigStore((state) => state.currentModelId); + const currentVariant = useConfigStore((state) => state.currentVariant); + const currentAgentName = useConfigStore((state) => state.currentAgentName); + const setAgent = useConfigStore((state) => state.setAgent); + const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents); const agents = getVisibleAgents(); - const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]); - const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen, cornerRadius, persistChatDraft, inputSpellcheckEnabled, isExpandedInput, setExpandedInput } = useUIStore(); - const { working } = useAssistantStatus(); + const isMobile = useUIStore((state) => state.isMobile); + const inputBarOffset = useUIStore((state) => state.inputBarOffset); + const persistChatDraft = useUIStore((state) => state.persistChatDraft); + const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled); + const isExpandedInput = useUIStore((state) => state.isExpandedInput); + const setExpandedInput = useUIStore((state) => state.setExpandedInput); const { git: runtimeGit } = useRuntimeAPIs(); const { currentTheme } = useThemeSystem(); const chatSearchDirectory = useChatSearchDirectory(); + const isGitRepo = useIsGitRepo(currentDirectory); + const currentGitStatus = useGitStore((state) => + currentDirectory ? state.directories.get(currentDirectory)?.status ?? null : null, + ); const [showAbortStatus, setShowAbortStatus] = React.useState(false); - const [textareaScrollTop, setTextareaScrollTop] = React.useState(0); const setSessionAutoAccept = usePermissionStore((state) => state.setSessionAutoAccept); + const composerHighlightRef = React.useRef(null); const isDesktopExpanded = isExpandedInput && !isMobile; - const chatInputRadius = 'var(--radius-lg)'; + const chatInputRadius = 'var(--radius-xl)'; - const sendableAttachedFiles = React.useMemo( - () => attachedFiles.filter((file) => file.source !== 'server'), - [attachedFiles], + const sendableAttachedFiles = attachedFiles; + + const knownAgentNames = React.useMemo( + () => new Set(agents.map((agent) => agent.name.toLowerCase())), + [agents] ); + const knownAgentNamesRef = React.useRef(knownAgentNames); + knownAgentNamesRef.current = knownAgentNames; const hasInlineMentionForHighlight = React.useMemo(() => { if (!message || !message.includes('@') || inputMode === 'shell') { return false; } - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); const mentionRegex = /@([^\s]+)/g; let match: RegExpExecArray | null; while ((match = mentionRegex.exec(message)) !== null) { @@ -376,12 +835,12 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (knownAgentNames.has(mentionPath.toLowerCase())) { return true; } - if (mentionPath.includes('/') || mentionPath.includes('\\') || mentionPath.includes('.')) { + if (isConfirmedFilePath(mentionPath)) { return true; } } return false; - }, [agents, inputMode, message]); + }, [inputMode, message, knownAgentNames]); const highlightedComposerContent = React.useMemo(() => { if (!hasInlineMentionForHighlight) { @@ -389,7 +848,6 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } const parts: Array<{ text: string; mentionKind: 'none' | 'file' | 'agent' }> = []; - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); const mentionRegex = /@([^\s]+)/g; let lastIndex = 0; let match: RegExpExecArray | null; @@ -405,7 +863,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const isFileMention = isBoundary && mention.length > 0 && !knownAgentNames.has(mention.toLowerCase()) - && (mention.includes('/') || mention.includes('\\') || mention.includes('.')); + && isConfirmedFilePath(mention); if (start > lastIndex) { parts.push({ text: message.slice(lastIndex, start), mentionKind: 'none' }); @@ -422,12 +880,16 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } return parts; - }, [agents, hasInlineMentionForHighlight, message]); + }, [hasInlineMentionForHighlight, message, knownAgentNames]); const sanitizeAttachmentsForSend = React.useCallback( (files: AttachedFile[] | undefined): AttachedFile[] => (files ?? []) - .filter((file) => file.source !== 'server') - .map((file) => ({ ...file })), + .map((file) => ({ + ...file, + dataUrl: file.source === 'server' && file.serverPath + ? toServerFileUrl(file.serverPath) + : file.dataUrl, + })), [], ); @@ -438,7 +900,6 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const clientDirectory = opencodeClient.getDirectory() || ''; const root = (chatSearchDirectory || clientDirectory).replace(/\\/g, '/').replace(/\/+$/, ''); - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); const seenPaths = new Set(); const attachments: AttachedFile[] = []; @@ -461,11 +922,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo continue; } - if (knownAgentNames.has(mentionPath.toLowerCase())) { + if (knownAgentNamesRef.current.has(mentionPath.toLowerCase())) { continue; } - const looksLikeFilePath = mentionPath.includes('/') || mentionPath.includes('\\') || mentionPath.includes('.'); + const looksLikeFilePath = isConfirmedFilePath(mentionPath); if (!looksLikeFilePath) { continue; } @@ -498,7 +959,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo filename, mimeType: 'text/plain', size: 0, - dataUrl: normalizedServerPath, + dataUrl: toServerFileUrl(normalizedServerPath), source: 'server', serverPath: normalizedServerPath, }); @@ -508,7 +969,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo sanitizedText: rawText, attachments, }; - }, [agents, chatSearchDirectory]); + }, [chatSearchDirectory]); const [autocompleteOverlayPosition, setAutocompleteOverlayPosition] = React.useState(null); const abortTimeoutRef = React.useRef | null>(null); const prevWasAbortedRef = React.useRef(false); @@ -563,29 +1024,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const consumeDrafts = useInlineCommentDraftStore((state) => state.consumeDrafts); const hasDrafts = draftCount > 0; - // User message history for up/down arrow navigation - // Get raw messages from store (stable reference) - const sessionMessages = useMessageStore( - React.useCallback( - (state) => (currentSessionId ? state.messages.get(currentSessionId) : undefined), - [currentSessionId] - ) - ); - // Derive user message history with useMemo to avoid infinite re-renders - const userMessageHistory = React.useMemo(() => { - if (!sessionMessages) return []; - return sessionMessages - .filter((m) => m.info.role === 'user') - .map((m) => { - const textPart = m.parts.find((p) => p.type === 'text'); - if (textPart && 'text' in textPart) { - return String(textPart.text); - } - return ''; - }) - .filter((text) => text.length > 0) - .reverse(); // Most recent first - }, [sessionMessages]); + // User message history for up/down arrow navigation. + // Keep this on a narrow hook instead of full session message records. + const userMessageHistory = useUserMessageHistory(currentSessionId ?? ""); // Keep messageRef in sync with message state React.useEffect(() => { @@ -604,6 +1045,15 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } saveStoredDraft(sessionId, draft); + // Only persist confirmed mentions that are actually present in the draft text + const activeMentions = new Set(); + for (const mention of confirmedMentionsRef.current) { + if (draft.includes(`@${mention}`)) { + activeMentions.add(mention); + } + } + confirmedMentionsRef.current = activeMentions; + saveConfirmedMentions(sessionId, activeMentions); lastPersistedDraftRef.current.set(key, draft); }, []); @@ -656,6 +1106,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo // Restore draft for the session we're entering const newDraft = getStoredDraft(currentSessionId); setMessage(newDraft); + confirmedMentionsRef.current = loadConfirmedMentions(currentSessionId); if (newDraft) { requestAnimationFrame(() => { textareaRef.current?.select(); @@ -664,6 +1115,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } else { // Persist disabled: clear input without saving setMessage(''); + confirmedMentionsRef.current = new Set(); } } }, [clearPendingDraftPersist, currentSessionId, persistChatDraft, persistDraftImmediately]); @@ -723,98 +1175,16 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo // Session activity for queue availability and controls const { phase: sessionPhase } = useCurrentSessionActivity(); - const handleTextareaPointerDownCapture = React.useCallback((event: React.PointerEvent) => { - if (!isMobile) { - return; - } - - if (event.pointerType !== 'touch') { - return; - } - - const textarea = textareaRef.current; - if (!textarea) { - return; - } - - if (document.activeElement === textarea) { - return; - } - - // Prevent iOS from scrolling the page to reveal the input. - event.preventDefault(); - event.stopPropagation(); - - const scroller = document.scrollingElement; - if (scroller && scroller.scrollTop !== 0) { - scroller.scrollTop = 0; - } - if (window.scrollY !== 0) { - window.scrollTo(0, 0); - } - - try { - textarea.focus({ preventScroll: true }); - } catch { - textarea.focus(); - } - - const len = textarea.value.length; - try { - textarea.setSelectionRange(len, len); - } catch { - // ignored - } - }, [isMobile]); - - const handleOpenMobileControls = React.useCallback(() => { - if (!isMobile) { - return; - } - - if (mobileControlsOpen) { - setMobileControlsOpen(false); - return; - } - - setMobileControlsPanel(null); - - if (isKeyboardOpen) { - textareaRef.current?.blur(); - requestAnimationFrame(() => { - setMobileControlsOpen(true); - }); - return; - } - - setMobileControlsOpen(true); - }, [isMobile, isKeyboardOpen, mobileControlsOpen]); - - const handleCloseMobileControls = React.useCallback(() => { - setMobileControlsOpen(false); - }, []); - const handleOpenMobilePanel = React.useCallback((panel: MobileControlsPanel) => { if (!isMobile) { return; } - setMobileControlsOpen(false); textareaRef.current?.blur(); requestAnimationFrame(() => { setMobileControlsPanel(panel); }); }, [isMobile]); - const handleReturnToUnifiedControls = React.useCallback(() => { - if (!isMobile) { - return; - } - setMobileControlsPanel(null); - requestAnimationFrame(() => { - setMobileControlsOpen(true); - }); - }, [isMobile]); - // Consume pending input text (e.g., from revert action) React.useEffect(() => { if (pendingInputText !== null) { @@ -839,11 +1209,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } }, [pendingInputText, consumePendingInputText]); - const hasContent = message.trim() || sendableAttachedFiles.length > 0 || hasDrafts; + const hasContent = message.trim().length > 0 || sendableAttachedFiles.length > 0 || hasDrafts; const hasQueuedMessages = queuedMessages.length > 0; const canSend = hasContent || hasQueuedMessages; - const canAbort = working.isWorking; + const canAbort = sessionPhase !== 'idle'; // Keep a ref to handleSubmit so callbacks don't depend on it. type SubmitOptions = { @@ -866,9 +1236,18 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo addToQueue(currentSessionId, { content: messageToQueue, attachments: attachmentsToQueue.length > 0 ? attachmentsToQueue : undefined, + sendConfig: currentProviderId && currentModelId ? { + providerID: currentProviderId, + modelID: currentModelId, + agent: currentAgentName ?? undefined, + variant: currentVariant ?? undefined, + } : undefined, }); // Clear input and attachments + // Note: confirmedMentionsRef is NOT cleared here because queued messages + // are processed later in handleSubmit which reads the ref via extractInlineFileMentions. + // The ref is cleared in handleSubmit after all queued messages are sent. setMessage(''); if (attachmentsToQueue.length > 0) { clearAttachedFiles(); @@ -877,7 +1256,30 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (!isMobile) { textareaRef.current?.focus(); } - }, [hasContent, currentSessionId, message, sendableAttachedFiles, sanitizeAttachmentsForSend, addToQueue, clearAttachedFiles, isMobile, consumeDrafts]); + }, [hasContent, currentSessionId, message, sendableAttachedFiles, sanitizeAttachmentsForSend, addToQueue, clearAttachedFiles, isMobile, consumeDrafts, currentProviderId, currentModelId, currentAgentName, currentVariant]); + + const handleQueuedMessageEdit = React.useCallback((content: string) => { + setMessage(content); + setTimeout(() => { + textareaRef.current?.focus(); + }, 0); + }, []); + + const handleOpenAgentPanel = React.useCallback(() => { + setMobileControlsPanel('agent'); + }, []); + + const handleToggleExpandedInput = React.useCallback(() => { + setExpandedInput(!isExpandedInput); + }, [isExpandedInput, setExpandedInput]); + + const openIssuePicker = React.useCallback(() => { + setIssuePickerOpen(true); + }, []); + + const openPrPicker = React.useCallback(() => { + setPrPickerOpen(true); + }, []); const handleSubmit = async (options?: SubmitOptions) => { const queuedOnly = options?.queuedOnly ?? false; @@ -888,9 +1290,6 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo return; } - // Re-pin and scroll to bottom when sending - scrollToBottom?.({ instant: true, force: true }); - if (!currentProviderId || !currentModelId) { console.warn('Cannot send message: provider or model not selected'); return; @@ -1012,8 +1411,10 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } if (!queuedOnly) { setMessage(''); + confirmedMentionsRef.current.clear(); // Clear per-session draft on submit saveStoredDraft(currentSessionId, ''); + saveConfirmedMentions(currentSessionId, confirmedMentionsRef.current); // Reset message history navigation state setHistoryIndex(-1); setDraftMessage(''); @@ -1037,25 +1438,82 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo .split(/\s+/)[0] ?.toLowerCase(); - // NEW: /undo - revert to last message (populates input with reverted message text) if (commandName === 'undo' && currentSessionId) { - await useSessionStore.getState().handleSlashUndo(currentSessionId); - // Don't clear message - pendingInputText will populate it with reverted message + await useSessionUIStore.getState().handleSlashUndo(currentSessionId); scrollToBottom?.({ instant: true, force: true }); - return; // Don't send to assistant + return; } - // NEW: /redo - unrevert or partial redo (populates input with message text) else if (commandName === 'redo' && currentSessionId) { - await useSessionStore.getState().handleSlashRedo(currentSessionId); - // Don't clear message - pendingInputText will populate it + await useSessionUIStore.getState().handleSlashRedo(currentSessionId); scrollToBottom?.({ instant: true, force: true }); - return; // Don't send to assistant + return; } - // NEW: /timeline - open timeline dialog - else if (commandName === 'timeline' && currentSessionId) { - setTimelineDialogOpen(true); - setMessage(''); - return; // Don't send to assistant + else if (commandName === 'compact' && currentSessionId) { + try { + await sessionActions.waitForConnectionOrThrow(); + const { opencodeClient } = await import('@/lib/opencode/client'); + const sdk = opencodeClient.getSdkClient(); + const configState = useConfigStore.getState(); + await sdk.session.summarize({ + sessionID: currentSessionId, + modelID: configState.currentModelId || '', + providerID: configState.currentProviderId || '', + }); + } catch (error) { + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.compactFailed')); + } + return; + } + else if (commandName === 'summary' && currentSessionId) { + try { + await sessionActions.waitForConnectionOrThrow(); + // Everything after `/summary ` is an optional topic hint + // the user wants the summary focused on. + const topic = normalizedCommand.replace(/^\/summary\b/i, '').trim(); + const topicLine = topic ? ` focused on: ${topic}` : ''; + const topicBlock = topic + ? `The user asked you to focus this summary on: ${topic}. Prioritize that topic; mention unrelated threads only in passing.` + : ''; + const visibleText = await renderMagicPrompt('session.summary.visible', { topic_line: topicLine }); + const instructionsText = await renderMagicPrompt('session.summary.instructions', { topic_block: topicBlock }); + await sendMessage( + visibleText, + currentProviderId, + currentModelId, + currentAgentName, + [], + agentMentionName, + [{ text: instructionsText, synthetic: true }], + currentVariant, + inputMode, + ); + scrollToBottom?.({ instant: true, force: true }); + } catch (error) { + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.summaryFailed')); + } + return; + } + else if (commandName === 'workspace-review' && (currentSessionId || newSessionDraftOpen)) { + try { + await sessionActions.waitForConnectionOrThrow(); + const visibleText = await renderMagicPrompt('session.review.visible'); + const instructionsText = await renderMagicPrompt('session.review.instructions'); + await sendMessage( + visibleText, + currentProviderId, + currentModelId, + currentAgentName, + [], + agentMentionName, + [{ text: instructionsText, synthetic: true }], + currentVariant, + inputMode, + ); + scrollToBottom?.({ instant: true, force: true }); + } catch (error) { + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.reviewFailed')); + } + return; } } @@ -1065,7 +1523,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo ...additionalParts.flatMap(p => p.attachments ?? []), ]; - void sendMessage( + const sendPromise = sendMessage( primaryText, currentProviderId, currentModelId, @@ -1075,7 +1533,17 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo additionalParts.length > 0 ? additionalParts : undefined, currentVariant, inputMode - ).then(() => { + ); + + if (typeof window === 'undefined') { + scrollToBottom?.({ instant: true, force: true }); + } else { + window.requestAnimationFrame(() => { + scrollToBottom?.({ instant: true, force: true }); + }); + } + + void sendPromise.then(() => { // Clear linked issue after successful message send if (linkedIssue) { setLinkedIssue(null); @@ -1106,25 +1574,25 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo normalized === 'failed to send message'; if (normalized.includes('payload too large') || normalized.includes('413') || normalized.includes('entity too large')) { - toast.error('Attachments are too large to send. Please try reducing the number or size of images.'); + toast.error(t('chat.chatInput.toast.attachmentsTooLarge')); if (allAttachments.length > 0) { - useFileStore.setState({ attachedFiles: allAttachments }); + useInputStore.setState({ attachedFiles: allAttachments }); } return; } if (isSoftNetworkError) { if (allAttachments.length > 0) { - useFileStore.setState({ attachedFiles: allAttachments }); - toast.error('Failed to send attachments. Try fewer files or smaller images.'); + useInputStore.setState({ attachedFiles: allAttachments }); + toast.error(t('chat.chatInput.toast.sendAttachmentsFailed')); } return; } if (allAttachments.length > 0) { - useFileStore.setState({ attachedFiles: allAttachments }); + useInputStore.setState({ attachedFiles: allAttachments }); } - toast.error(rawMessage || 'Message failed to send. Attachments restored.'); + toast.error(rawMessage || t('chat.chatInput.toast.messageSendFailed')); }); if (!isMobile) { @@ -1182,10 +1650,13 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } const token = message.slice(tokenStart, tokenEnd); + const mentionContent = token.slice(1); const looksLikeFileMention = FILE_MENTION_TOKEN.test(token) - && (token.includes('/') || token.includes('\\') || token.includes('.')); + && !knownAgentNamesRef.current.has(mentionContent.toLowerCase()) + && isConfirmedFilePath(mentionContent); if (looksLikeFileMention) { + confirmedMentionsRef.current.delete(mentionContent); const removeUntil = message[tokenEnd] === ' ' ? tokenEnd + 1 : tokenEnd; const nextMessage = `${message.slice(0, tokenStart)}${message.slice(removeUntil)}`; e.preventDefault(); @@ -1461,33 +1932,37 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }, [abortCurrentOperation, clearAbortPrompt, currentSessionId, startAbortIndicator]); const handleCycleAgent = React.useCallback(() => { - if (primaryAgents.length <= 1) return; + const nextAgentName = getCycledPrimaryAgentName(agents, currentAgentName); + if (!nextAgentName) return; - const currentIndex = primaryAgents.findIndex(agent => agent.name === currentAgentName); - const nextIndex = (currentIndex + 1) % primaryAgents.length; - const nextAgent = primaryAgents[nextIndex]; - - setAgent(nextAgent.name); + setAgent(nextAgentName); if (currentSessionId) { - saveSessionAgentSelection(currentSessionId, nextAgent.name); + saveSessionAgentSelection(currentSessionId, nextAgentName); } - }, [primaryAgents, currentAgentName, currentSessionId, setAgent, saveSessionAgentSelection]); + }, [agents, currentAgentName, currentSessionId, setAgent, saveSessionAgentSelection]); - const adjustTextareaHeight = React.useCallback(() => { + const adjustTextareaHeight = React.useCallback((options?: { allowShrink?: boolean }) => { const textarea = textareaRef.current; if (!textarea) { return; } + const previousScrollTop = textarea.scrollTop; + if (isDesktopExpanded) { textarea.style.height = '100%'; textarea.style.maxHeight = 'none'; setTextareaSize(null); + if (textarea.scrollTop !== previousScrollTop) { + textarea.scrollTop = previousScrollTop; + } return; } - textarea.style.height = 'auto'; + if (options?.allowShrink ?? true) { + textarea.style.height = 'auto'; + } const view = textarea.ownerDocument?.defaultView; const computedStyle = view ? view.getComputedStyle(textarea) : null; @@ -1506,6 +1981,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo textarea.style.height = `${nextHeight}px`; textarea.style.maxHeight = `${maxHeight}px`; + if (textarea.scrollTop !== previousScrollTop) { + textarea.scrollTop = previousScrollTop; + } setTextareaSize((prev) => { if (prev && prev.height === nextHeight && prev.maxHeight === maxHeight) { @@ -1516,7 +1994,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }, [isDesktopExpanded]); React.useLayoutEffect(() => { - adjustTextareaHeight(); + const allowShrink = message.length < previousMessageLengthRef.current; + previousMessageLengthRef.current = message.length; + adjustTextareaHeight({ allowShrink }); }, [adjustTextareaHeight, message, isMobile]); const updateAutocompleteState = React.useCallback((value: string, cursorPosition: number) => { @@ -1575,7 +2055,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const isWordBoundary = !charBefore || /\s/.test(charBefore); if (isWordBoundary && !textAfterAt.includes(' ') && !textAfterAt.includes('\n')) { setMentionQuery(textAfterAt); - setAutocompleteTab('agents'); + setAutocompleteTab((current) => current === 'files' ? 'files' : 'agents'); setShowFileMention(true); } else { setShowFileMention(false); @@ -1618,22 +2098,31 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo // ignored } } + const cursorPosition = textarea?.selectionStart ?? message.length; + const textBeforeCursor = message.substring(0, cursorPosition); + const lastAtSymbol = textBeforeCursor.lastIndexOf('@'); + const nextMentionQuery = lastAtSymbol !== -1 + ? textBeforeCursor.substring(lastAtSymbol + 1).replace(/[\s\n].*$/, '') + : ''; + setAutocompleteTab(tab); setCommandQuery(''); - setMentionQuery(''); if (tab === 'commands') { + setMentionQuery(''); applyAutocompletePrefix('/'); } if (tab === 'agents') { + setMentionQuery(nextMentionQuery); applyAutocompletePrefix('@'); } if (tab === 'files') { + setMentionQuery(nextMentionQuery); applyAutocompletePrefix('@'); } setShowSkillAutocomplete(false); setShowCommandAutocomplete(tab === 'commands'); setShowFileMention(tab === 'agents' || tab === 'files'); - }, [applyAutocompletePrefix, isMobile, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]); + }, [applyAutocompletePrefix, isMobile, message, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]); const handleOpenCommandMenu = React.useCallback(() => { if (!isMobile) { @@ -1807,10 +2296,10 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo await addAttachedFile(file); } catch (error) { console.error('Clipboard image attach failed', error); - toast.error(error instanceof Error ? error.message : 'Failed to attach image from clipboard'); + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.clipboardAttachFailed')); } } - }, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection]); + }, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection, t]); const handleFileSelect = (file: { name: string; path: string; relativePath?: string }) => { @@ -1822,6 +2311,8 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo ? file.relativePath.trim() : (toProjectRelativeMentionPath(file.path) || file.name); + confirmedMentionsRef.current.add(mentionPath); + if (lastAtSymbol !== -1) { const newMessage = message.substring(0, lastAtSymbol) + @@ -1936,7 +2427,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo textareaRef.current?.focus(); }; - const handleCommandSelect = (command: { name: string; description?: string; agent?: string; model?: string }) => { + const handleCommandSelect = (command: CommandInfo) => { setMessage(`/${command.name} `); @@ -1975,7 +2466,6 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo React.useEffect(() => { if (!isMobile) { - setMobileControlsOpen(false); setMobileControlsPanel(null); } }, [isMobile]); @@ -1999,6 +2489,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (lowerTypes.includes('files')) return true; if (lowerTypes.includes('text/uri-list')) return true; if (lowerTypes.includes('codefiles')) return true; + if (lowerTypes.includes('application/x-openchamber-file-path')) return true; if (lowerTypes.some((type) => type.includes('vnd.code.tree'))) return true; } @@ -2098,20 +2589,25 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const addVSCodeDroppedUrisAsMentions = React.useCallback((uris: string[]) => { if (uris.length === 0) return; - const mentions = Array.from(new Set(uris + const paths = uris .map((entry) => normalizeDroppedPath(entry)) .map((entry) => toProjectRelativeMentionPath(entry)) .map((entry) => entry.trim().replace(/^\.\//, '')) - .filter((entry) => entry.length > 0) - .map((entry) => `@${entry}`))); + .filter((entry) => entry.length > 0); + + for (const p of paths) { + confirmedMentionsRef.current.add(p); + } + + const mentions = Array.from(new Set(paths.map((entry) => `@${entry}`))); if (mentions.length === 0) { return; } setPendingInputText(mentions.join(' '), 'append-inline'); - toast.success(`Added ${mentions.length} file mention${mentions.length > 1 ? 's' : ''}`); - }, [normalizeDroppedPath, setPendingInputText, toProjectRelativeMentionPath]); + toast.success(t('chat.chatInput.toast.addedFileMentions', { count: mentions.length })); + }, [normalizeDroppedPath, setPendingInputText, t, toProjectRelativeMentionPath]); const handleDragEnter = (e: React.DragEvent) => { if (!hasDraggedFiles(e.dataTransfer)) { @@ -2119,6 +2615,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } e.preventDefault(); e.stopPropagation(); + dragEnterCountRef.current++; + const isInternal = e.dataTransfer.types?.includes('application/x-openchamber-file-path') ?? false; + if (isInternal !== isInternalDrag) { + setIsInternalDrag(isInternal); + } if ((currentSessionId || newSessionDraftOpen) && !isDragging) { setIsDragging(true); } @@ -2139,13 +2640,24 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - if (e.currentTarget === e.target) { + dragEnterCountRef.current--; + if (dragEnterCountRef.current <= 0) { + dragEnterCountRef.current = 0; setIsDragging(false); + setIsInternalDrag(false); clearDropTextSuppression(); } }; + const handleDragEnd = () => { + dragEnterCountRef.current = 0; + setIsDragging(false); + setIsInternalDrag(false); + clearDropTextSuppression(); + }; + const handleDrop = async (e: React.DragEvent) => { + dragEnterCountRef.current = 0; const draggedFiles = hasDraggedFiles(e.dataTransfer); if (!draggedFiles) { clearDropTextSuppression(); @@ -2157,6 +2669,37 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (!currentSessionId && !newSessionDraftOpen) return; + // Internal drag: file tree → chat input (relative path as @mention) + const internalPath = e.dataTransfer.getData('application/x-openchamber-file-path'); + if (internalPath && internalPath !== '.') { + confirmedMentionsRef.current.add(internalPath); + const mention = `@${internalPath}`; + const textarea = textareaRef.current; + const currentMessage = messageRef.current; + if (textarea) { + const pos = textarea.selectionStart ?? cursorPosRef.current; + const end = textarea.selectionEnd ?? pos; + const before = currentMessage.slice(0, pos); + const after = currentMessage.slice(end); + const needSpaceBefore = before.length > 0 && !/\s$/.test(before); + const needSpaceAfter = after.length > 0 && !/^\s/.test(after); + const insert = `${needSpaceBefore ? ' ' : ''}${mention}${needSpaceAfter ? ' ' : ''}`; + const nextMessage = `${before}${insert}${after}`; + setMessage(nextMessage); + requestAnimationFrame(() => { + const cursorPos = pos + insert.length; + textarea.selectionStart = cursorPos; + textarea.selectionEnd = cursorPos; + cursorPosRef.current = cursorPos; + textarea.focus(); + }); + } else { + setMessage((prev) => appendInlineText(prev, mention)); + } + clearDropTextSuppression(); + return; + } + const files = collectDroppedFiles(e.dataTransfer); if (files.length === 0 && isVSCodeRuntime()) { @@ -2179,7 +2722,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo await addAttachedFile(file); } catch (error) { console.error('File attach failed', error); - toast.error(error instanceof Error ? error.message : 'Failed to attach file'); + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.attachFileFailed')); } } } @@ -2187,15 +2730,15 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }; const handleDropCapture = (e: React.DragEvent) => { - if (!isVSCodeRuntime()) { - return; - } if (!hasDraggedFiles(e.dataTransfer)) { return; } - suppressNextFileDropTextInsertRef.current = true; - scheduleDropTextSuppressionExpiry(); + // Prevent native textarea drop text insertion for all runtimes e.preventDefault(); + if (isVSCodeRuntime()) { + suppressNextFileDropTextInsertRef.current = true; + scheduleDropTextSuppressionExpiry(); + } }; // Tauri desktop: handle native file drops via onDragDropEvent @@ -2287,7 +2830,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo await addAttachedFile(file); } catch (error) { console.error('Failed to attach dropped file:', path, error); - toast.error(`Failed to attach ${path.split(/[\\/]/).pop() || 'file'}`); + toast.error(t('chat.chatInput.toast.attachNamedFailed', { + name: path.split(/[\\/]/).pop() || t('chat.chatInput.fileFallback'), + })); } } } @@ -2309,7 +2854,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo cancelled = true; if (unlisten) unlisten(); }; - }, [addAttachedFile, normalizeDroppedPath]); + }, [addAttachedFile, normalizeDroppedPath, t]); const fileInputRef = React.useRef(null); @@ -2321,10 +2866,10 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo await addAttachedFile(file); } catch (error) { console.error('File attach failed', error); - toast.error(error instanceof Error ? error.message : 'Failed to attach file'); + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.attachFileFailed')); } } - }, [addAttachedFile]); + }, [addAttachedFile, t]); const handleVSCodePickFiles = React.useCallback(async () => { try { @@ -2337,7 +2882,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const summary = skipped .map((s: { name?: string; reason?: string }) => `${s?.name || 'file'}: ${s?.reason || 'skipped'}`) .join('\n'); - toast.error(`Some files were skipped:\n${summary}`); + toast.error(t('chat.chatInput.toast.someFilesSkipped', { summary })); } const asFiles = picked @@ -2366,9 +2911,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } } catch (error) { console.error('VS Code file pick failed', error); - toast.error(error instanceof Error ? error.message : 'Failed to pick files in VS Code'); + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.vscodePickFailed')); } - }, [attachFiles]); + }, [attachFiles, t]); const handlePickLocalFiles = React.useCallback(() => { if (isVSCodeRuntime()) { @@ -2442,6 +2987,8 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }; }, [fetchBranches, runtimeGit, selectedDraftProject, selectedDraftProjectBranches?.all, selectedDraftProjectPath, showDraftTargetSelectors]); + const selectedDraftProjectCurrentBranch = selectedDraftProjectBranches?.current?.trim() ?? ''; + const projectRootBranchOption = React.useMemo(() => { if (!selectedDraftProject) { return null; @@ -2450,25 +2997,20 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (!value) { return null; } - const projectRootBranch = selectedDraftProjectBranches?.current?.trim() ?? ''; - if (!projectRootBranch) { + if (!selectedDraftProjectCurrentBranch) { return null; } return { value, - label: projectRootBranch, + label: selectedDraftProjectCurrentBranch, }; - }, [selectedDraftProject, selectedDraftProjectBranches]); + }, [selectedDraftProject, selectedDraftProjectCurrentBranch]); const worktreeBranchOptions = React.useMemo(() => { if (!selectedDraftProject) { - return [] as Array<{ value: string; label: string }>; + return []; } - const seen = new Set(); - const options: Array<{ value: string; label: string }> = []; - const rootValue = projectRootBranchOption?.value ?? null; - const worktrees = (() => { if (!selectedDraftProjectPath) { return []; @@ -2478,23 +3020,13 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo ?? []; })(); - worktrees - .slice() - .sort((a, b) => a.branch.localeCompare(b.branch)) - .forEach((worktree) => { - const normalizedValue = normalizePath(worktree.path); - if (!normalizedValue || normalizedValue === rootValue || seen.has(normalizedValue)) { - return; - } - seen.add(normalizedValue); - options.push({ - value: normalizedValue, - label: worktree.branch?.trim() || formatDirectoryName(worktree.path), - }); - }); - - return options; - }, [availableWorktreesByProject, projectRootBranchOption?.value, selectedDraftProject, selectedDraftProjectPath]); + return buildSessionTargetOptions({ + projectRoot: normalizePath(selectedDraftProject.path) ?? '', + rootBranch: selectedDraftProjectCurrentBranch, + worktrees, + pendingBootstrapDirectory: newSessionDraft?.bootstrapPendingDirectory ?? null, + }); + }, [availableWorktreesByProject, newSessionDraft?.bootstrapPendingDirectory, selectedDraftProject, selectedDraftProjectCurrentBranch, selectedDraftProjectPath]); const selectedDraftDirectory = React.useMemo( () => normalizePath(newSessionDraft?.bootstrapPendingDirectory ?? null) @@ -2543,6 +3075,13 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo return draftBranchItems.find((item) => item.value === selectedValue)?.label ?? formatDirectoryName(selectedValue); }, [draftBranchItems, selectedDraftDirectory]); + const hasPendingChanges = React.useMemo(() => { + if (isGitRepo !== true || !currentGitStatus || currentGitStatus.isClean) { + return false; + } + return extractGitChangedFiles(currentGitStatus.files, currentGitStatus.diffStats, currentDirectory).length > 0; + }, [currentDirectory, currentGitStatus, isGitRepo]); + const selectedDraftBranchIsKnown = React.useMemo(() => { if (!selectedDraftDirectory) { return true; @@ -2560,7 +3099,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (!selectedDraftDirectory || !selectedDraftBranchIsKnown) { return; } - useSessionStore.getState().setDraftPreserveDirectoryOverride(false); + useSessionUIStore.getState().setDraftPreserveDirectoryOverride(false); }, [newSessionDraft?.open, newSessionDraft?.preserveDirectoryOverride, selectedDraftBranchIsKnown, selectedDraftDirectory]); const shouldShowDraftBranchSelector = React.useMemo(() => { @@ -2574,7 +3113,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }, [isDiscoveringDraftBranches, projectRootBranchOption, worktreeBranchOptions.length]); const handleDraftProjectChange = React.useCallback((projectId: string) => { - const draft = useSessionStore.getState().newSessionDraft; + const draft = useSessionUIStore.getState().newSessionDraft; if (draft?.pendingWorktreeRequestId || draft?.bootstrapPendingDirectory || draft?.preserveDirectoryOverride) { return; } @@ -2592,7 +3131,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }, [activeProjectId, projects, setActiveProjectIdOnly, setNewSessionDraftTarget]); const handleDraftDirectoryChange = React.useCallback((directory: string) => { - const draft = useSessionStore.getState().newSessionDraft; + const draft = useSessionUIStore.getState().newSessionDraft; if (draft?.pendingWorktreeRequestId || draft?.bootstrapPendingDirectory || draft?.preserveDirectoryOverride) { return; } @@ -2678,252 +3217,18 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const handlePermissionAutoAcceptToggle = React.useCallback(() => { if (!permissionScopeSessionId) { - toast.error('Open a session first'); + toast.error(t('chat.chatInput.toast.openSessionFirst')); return; } const nextEnabled = !permissionAutoAcceptEnabled; setSessionAutoAccept(permissionScopeSessionId, nextEnabled).catch(() => { - toast.error('Failed to toggle permission auto-accept'); + toast.error(t('chat.chatInput.toast.togglePermissionAutoAcceptFailed')); }); - }, [permissionAutoAcceptEnabled, permissionScopeSessionId, setSessionAutoAccept]); - - const permissionAutoAcceptAriaLabel = permissionAutoAcceptEnabled - ? 'Disable permission auto-accept' - : 'Enable permission auto-accept'; - const permissionAutoAcceptTooltipLabel = permissionAutoAcceptEnabled - ? 'Permission auto-accept: on' - : 'Permission auto-accept: off'; - - const permissionAutoAcceptButton = ( - - ); - - const permissionAutoAcceptButtonWithTooltip = ( - - - {permissionAutoAcceptButton} - - - {permissionAutoAcceptTooltipLabel} - - - ); - - // Send button - respects queue mode setting - const sendButton = ( - - ); - - // Queue button for adding message to queue while working - const queueButton = ( - - ); - - // Stop button replaces send button when working - const stopButton = ( - - ); - - // Action buttons area: either send button, or stop (+ optional queue button floating above) - const actionButtons = canAbort ? ( -
- {hasContent && queueButton} - {stopButton} -
- ) : ( - sendButton - ); - - const attachmentMenu = ( - <> - - -
- {isVSCode ? ( - - ) : ( - - - - - - { - requestAnimationFrame(() => handlePickLocalFiles()); - }} - > - - Attach files - - { - requestAnimationFrame(() => { - setIssuePickerOpen(true); - }); - }} - > - - Link GitHub Issue - - { - requestAnimationFrame(() => { - setPrPickerOpen(true); - }); - }} - > - - Link GitHub PR - - - - )} -
- - ); - - const settingsButton = onOpenSettings ? ( - - ) : null; - - const attachmentsControls = ( -
- {isMobile ? ( - - ) : null} - {attachmentMenu} - {settingsButton} -
- ); - - const workingStatusText = working.statusText; + }, [permissionAutoAcceptEnabled, permissionScopeSessionId, setSessionAutoAccept, t]); React.useEffect(() => { - const pendingAbortBanner = Boolean(working.wasAborted); + const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId; if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) { startAbortIndicator(); if (currentSessionId) { @@ -2932,11 +3237,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } prevWasAbortedRef.current = pendingAbortBanner; }, [ + abortPromptSessionId, acknowledgeSessionAbort, currentSessionId, showAbortStatus, startAbortIndicator, - working.wasAborted, ]); React.useEffect(() => { @@ -2955,20 +3260,14 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo className={cn( "relative pt-0 pb-4", isDesktopExpanded && 'flex h-full min-h-0 flex-col pt-4', - isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area" + isMobile && 'bottom-safe-area' )} - data-keyboard-avoid="true" - style={isMobile && inputBarOffset > 0 && !isKeyboardOpen ? { marginBottom: `${inputBarOffset}px` } : undefined} + style={isMobile && inputBarOffset > 0 ? { marginBottom: `${inputBarOffset}px` } : undefined} > -
+
{ - setMessage(content); - setTimeout(() => { - textareaRef.current?.focus(); - }, 0); - }} + onEditMessage={handleQueuedMessageEdit} /> {hasDrafts && (
@@ -2979,7 +3278,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo borderColor: currentTheme?.colors?.interactive?.border, }} > - Review comments: + {t('chat.chatInput.reviewComments')} {draftCount} @@ -3005,7 +3304,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo #{linkedIssue.number} {linkedIssue.author && ( - by {linkedIssue.author.login} + {t('chat.chatInput.linked.byAuthor', { author: linkedIssue.author.login })} )} @@ -3018,7 +3317,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors" - aria-label="Open issue in browser" + aria-label={t('chat.chatInput.linked.issue.openInBrowserAria')} > @@ -3028,7 +3327,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo setLinkedIssue(null); }} className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer" - aria-label="Remove linked issue" + aria-label={t('chat.chatInput.linked.issue.removeAria')} > @@ -3051,9 +3350,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo /> )} - PR #{linkedPr.number} + {t('chat.chatInput.linked.pr.number', { number: linkedPr.number })} {linkedPr.author && ( - by {linkedPr.author.login} + {t('chat.chatInput.linked.byAuthor', { author: linkedPr.author.login })} )} @@ -3069,7 +3368,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors" - aria-label="Open pull request in browser" + aria-label={t('chat.chatInput.linked.pr.openInBrowserAria')} > @@ -3079,7 +3378,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo setLinkedPr(null); }} className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer" - aria-label="Remove linked pull request" + aria-label={t('chat.chatInput.linked.pr.removeAria')} > @@ -3087,17 +3386,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo
)} - } /> {showDraftTargetSelectors && selectedDraftProject ? (
@@ -3107,7 +3400,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo > {renderProjectLabelWithIcon(selectedDraftProject)} @@ -3129,16 +3422,16 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo > - {selectedDraftBranchLabel ?? 'Branch'} + {selectedDraftBranchLabel ?? t('chat.chatInput.branch')} {projectRootBranchOption ? ( - Project root + {t('chat.chatInput.projectRoot')} {projectRootBranchOption.label} @@ -3147,19 +3440,19 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo {projectRootBranchOption ? : null}
- Worktrees + {t('chat.chatInput.worktrees')}
{worktreeBranchOptions.map((option) => ( - {option.label} + {option.pending ? '⏳ ' : ''}{option.label} ))}
@@ -3194,22 +3487,25 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} + onDragEnd={handleDragEnd} > {isDragging && ( -
+
-

Drop files here to attach

+

+ {isInternalDrag ? t('chat.chatInput.drop.insertMention') : t('chat.chatInput.drop.attachFiles')} +

)} @@ -3291,7 +3587,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo : 'pt-4 pb-2', inputMode === 'shell' ? 'font-mono' : 'typography-markdown md:typography-ui-label', )} - style={{ transform: `translateY(-${textareaScrollTop}px)` }} + ref={composerHighlightRef} > {highlightedComposerContent.map((part, index) => ( = ({ onOpenSettings, scrollToBo
)}