diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index e333b1d3..0cc03021 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -134,22 +134,6 @@ jobs: exit 1 fi - - name: Load release notes - id: release_notes - shell: bash - run: | - NOTES_FILE="release-notes/v${{ steps.version.outputs.value }}.md" - if [ ! -f "$NOTES_FILE" ]; then - echo "::error::Missing release notes file: $NOTES_FILE" - exit 1 - fi - { - echo 'body<<__RELEASE_NOTES__' - cat "$NOTES_FILE" - echo - echo '__RELEASE_NOTES__' - } >> "$GITHUB_OUTPUT" - - name: Install Linux dependencies if: contains(matrix.platform, 'ubuntu') run: | @@ -210,6 +194,69 @@ jobs: if: matrix.smoke_platform == 'macos' run: bun run test:package-smoke --platform macos --package-kind release --artifacts-dir desktop/build-artifacts/electron --require-macos-gatekeeper + - name: Validate matrix release asset set + shell: bash + working-directory: desktop/build-artifacts/electron + env: + APP_VERSION: ${{ steps.version.outputs.value }} + run: | + case "${{ matrix.label }}" in + macOS-ARM64) + expected=( + "Claude-Code-Haha-${APP_VERSION}-mac-arm64.dmg" + "Claude-Code-Haha-${APP_VERSION}-mac-arm64.dmg.blockmap" + "Claude-Code-Haha-${APP_VERSION}-mac-arm64.zip" + "Claude-Code-Haha-${APP_VERSION}-mac-arm64.zip.blockmap" + "latest-mac.yml" + ) + ;; + macOS-x64) + expected=( + "Claude-Code-Haha-${APP_VERSION}-mac-x64.dmg" + "Claude-Code-Haha-${APP_VERSION}-mac-x64.dmg.blockmap" + "Claude-Code-Haha-${APP_VERSION}-mac-x64.zip" + "Claude-Code-Haha-${APP_VERSION}-mac-x64.zip.blockmap" + "latest-mac.yml" + ) + ;; + Linux-x64) + expected=( + "Claude-Code-Haha-${APP_VERSION}-linux-x64.AppImage" + "Claude-Code-Haha-${APP_VERSION}-linux-x64.AppImage.blockmap" + "Claude-Code-Haha-${APP_VERSION}-linux-x64.deb" + "latest-linux.yml" + ) + ;; + Linux-ARM64) + expected=( + "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage" + "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage.blockmap" + "Claude-Code-Haha-${APP_VERSION}-linux-arm64.deb" + "latest-linux.yml" + ) + ;; + Windows-x64) + expected=( + "Claude-Code-Haha-${APP_VERSION}-win-x64.exe" + "Claude-Code-Haha-${APP_VERSION}-win-x64.exe.blockmap" + "latest.yml" + ) + ;; + *) + echo "::error::No expected asset list for matrix label ${{ matrix.label }}" + exit 1 + ;; + esac + + missing=() + for file in "${expected[@]}"; do + [ -f "$file" ] || missing+=("$file") + done + if [ "${#missing[@]}" -gt 0 ]; then + printf '::error::Missing release assets for %s: %s\n' "${{ matrix.label }}" "${missing[*]}" + exit 1 + fi + - name: Namespace update metadata assets shell: bash working-directory: desktop/build-artifacts/electron @@ -226,15 +273,11 @@ jobs: path: desktop/build-artifacts/electron/latest*.yml if-no-files-found: ignore - - name: Upload artifacts - uses: softprops/action-gh-release@v2 + - name: Upload release artifacts for final publish + uses: actions/upload-artifact@v4 with: - tag_name: v${{ steps.version.outputs.value }} - name: Claude Code Haha v${{ steps.version.outputs.value }} - body: ${{ steps.release_notes.outputs.body }} - draft: ${{ github.event_name == 'workflow_dispatch' && inputs.draft || false }} - prerelease: false - files: | + name: desktop-release-artifacts-${{ matrix.label }} + path: | desktop/build-artifacts/electron/*.dmg desktop/build-artifacts/electron/*.zip desktop/build-artifacts/electron/*.exe @@ -242,8 +285,9 @@ jobs: desktop/build-artifacts/electron/*.deb desktop/build-artifacts/electron/*.blockmap desktop/build-artifacts/electron/*.yml + if-no-files-found: error - publish-update-metadata: + publish-release: needs: build runs-on: ubuntu-latest @@ -258,6 +302,32 @@ jobs: VERSION=$(node -p "require('./desktop/package.json').version") echo "value=$VERSION" >> "$GITHUB_OUTPUT" + - name: Validate tag matches version + if: github.event_name == 'push' + shell: bash + run: | + EXPECTED_TAG="v${{ steps.version.outputs.value }}" + if [ "${GITHUB_REF_NAME}" != "$EXPECTED_TAG" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match app version ${EXPECTED_TAG}" + exit 1 + fi + + - name: Load release notes + id: release_notes + shell: bash + run: | + NOTES_FILE="release-notes/v${{ steps.version.outputs.value }}.md" + if [ ! -f "$NOTES_FILE" ]; then + echo "::error::Missing release notes file: $NOTES_FILE" + exit 1 + fi + { + echo 'body<<__RELEASE_NOTES__' + cat "$NOTES_FILE" + echo + echo '__RELEASE_NOTES__' + } >> "$GITHUB_OUTPUT" + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -266,6 +336,46 @@ jobs: - name: Install root dependencies run: bun install + - name: Download release artifacts + uses: actions/download-artifact@v4 + with: + pattern: desktop-release-artifacts-* + path: artifacts/release-assets + merge-multiple: true + + - name: Validate complete release asset set + shell: bash + env: + APP_VERSION: ${{ steps.version.outputs.value }} + run: | + expected=( + "Claude-Code-Haha-${APP_VERSION}-mac-arm64.dmg" + "Claude-Code-Haha-${APP_VERSION}-mac-arm64.dmg.blockmap" + "Claude-Code-Haha-${APP_VERSION}-mac-arm64.zip" + "Claude-Code-Haha-${APP_VERSION}-mac-arm64.zip.blockmap" + "Claude-Code-Haha-${APP_VERSION}-mac-x64.dmg" + "Claude-Code-Haha-${APP_VERSION}-mac-x64.dmg.blockmap" + "Claude-Code-Haha-${APP_VERSION}-mac-x64.zip" + "Claude-Code-Haha-${APP_VERSION}-mac-x64.zip.blockmap" + "Claude-Code-Haha-${APP_VERSION}-linux-x64.AppImage" + "Claude-Code-Haha-${APP_VERSION}-linux-x64.AppImage.blockmap" + "Claude-Code-Haha-${APP_VERSION}-linux-x64.deb" + "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage" + "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage.blockmap" + "Claude-Code-Haha-${APP_VERSION}-linux-arm64.deb" + "Claude-Code-Haha-${APP_VERSION}-win-x64.exe" + "Claude-Code-Haha-${APP_VERSION}-win-x64.exe.blockmap" + ) + + missing=() + for file in "${expected[@]}"; do + [ -f "artifacts/release-assets/$file" ] || missing+=("$file") + done + if [ "${#missing[@]}" -gt 0 ]; then + printf '::error::Missing complete release assets: %s\n' "${missing[*]}" + exit 1 + fi + - name: Download update metadata artifacts uses: actions/download-artifact@v4 with: @@ -276,10 +386,38 @@ jobs: - name: Merge standard update metadata run: bun run scripts/release-update-metadata.ts --metadata-dir artifacts/update-metadata --out-dir artifacts/update-metadata-standard - - name: Upload standard update metadata + - name: Validate standard update metadata set + shell: bash + run: | + expected=( + "latest-mac.yml" + "latest-linux.yml" + "latest-linux-arm64.yml" + "latest.yml" + ) + missing=() + for file in "${expected[@]}"; do + [ -f "artifacts/update-metadata-standard/$file" ] || missing+=("$file") + done + if [ "${#missing[@]}" -gt 0 ]; then + printf '::error::Missing standard update metadata: %s\n' "${missing[*]}" + exit 1 + fi + + - name: Publish complete GitHub release uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.version.outputs.value }} + name: Claude Code Haha v${{ steps.version.outputs.value }} + body: ${{ steps.release_notes.outputs.body }} draft: ${{ github.event_name == 'workflow_dispatch' && inputs.draft || false }} prerelease: false - files: artifacts/update-metadata-standard/*.yml + fail_on_unmatched_files: true + files: | + artifacts/release-assets/**/*.dmg + artifacts/release-assets/**/*.zip + artifacts/release-assets/**/*.exe + artifacts/release-assets/**/*.AppImage + artifacts/release-assets/**/*.deb + artifacts/release-assets/**/*.blockmap + artifacts/update-metadata-standard/*.yml diff --git a/desktop/bun.lock b/desktop/bun.lock index 8ffd27ee..e66645f8 100644 --- a/desktop/bun.lock +++ b/desktop/bun.lock @@ -44,15 +44,6 @@ "vite": "^8.0.10", "vitest": "^3.0.4", }, - "optionalDependencies": { - "@tauri-apps/api": "^2.3.0", - "@tauri-apps/cli": "^2.3.1", - "@tauri-apps/plugin-dialog": "^2.2.1", - "@tauri-apps/plugin-notification": "^2.2.1", - "@tauri-apps/plugin-process": "^2.2.1", - "@tauri-apps/plugin-shell": "^2.2.1", - "@tauri-apps/plugin-updater": "^2.2.1", - }, }, }, "packages": { @@ -378,42 +369,6 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.2.2.tgz", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="], - "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "https://registry.npmmirror.com/@tauri-apps/api/-/api-2.10.1.tgz", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="], - - "@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli/-/cli-2.10.1.tgz", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="], - - "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ=="], - - "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw=="], - - "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w=="], - - "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA=="], - - "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg=="], - - "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw=="], - - "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw=="], - - "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ=="], - - "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg=="], - - "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw=="], - - "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="], - - "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.7.0", "https://registry.npmmirror.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.0.tgz", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw=="], - - "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "https://registry.npmmirror.com/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="], - - "@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.1", "https://registry.npmmirror.com/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="], - - "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "https://registry.npmmirror.com/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="], - - "@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.10.1", "https://registry.npmmirror.com/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA=="], - "@testing-library/dom": ["@testing-library/dom@10.4.1", "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], diff --git a/desktop/package.json b/desktop/package.json index b4987a3a..6c7cc496 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -13,7 +13,7 @@ "node_modules/node-pty/**", "src-tauri/binaries/**" ], - "artifactName": "Claude-Code-Haha-${version}-${arch}.${ext}", + "artifactName": "Claude-Code-Haha-${version}-${os}-${arch}.${ext}", "npmRebuild": false, "directories": { "output": "build-artifacts/electron" @@ -73,7 +73,6 @@ "electron:package:dir": "bun run clean:electron-output && bun run electron:build && bunx electron-builder --dir --publish never", "check:electron": "node ./node_modules/typescript/bin/tsc -p electron/tsconfig.json && bun test electron && bun run build:electron", "preview": "vite preview", - "tauri": "tauri", "test": "vitest", "test:ui": "vitest --ui", "lint": "node ./node_modules/typescript/bin/tsc --noEmit" @@ -117,14 +116,5 @@ "typescript": "^5.9.3", "vite": "^8.0.10", "vitest": "^3.0.4" - }, - "optionalDependencies": { - "@tauri-apps/api": "^2.3.0", - "@tauri-apps/cli": "^2.3.1", - "@tauri-apps/plugin-dialog": "^2.2.1", - "@tauri-apps/plugin-notification": "^2.2.1", - "@tauri-apps/plugin-process": "^2.2.1", - "@tauri-apps/plugin-shell": "^2.2.1", - "@tauri-apps/plugin-updater": "^2.2.1" } } diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index 63a7ad30..217ebdbe 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -9,6 +9,7 @@ import { useUpdateStore } from '../stores/updateStore' import type { SavedProvider } from '../types/provider' import type { ProviderPreset } from '../types/providerPreset' import type { AppMode, ChatSendBehavior, ThemeMode, UpdateProxySettings } from '../types/settings' +import { browserHost } from '../lib/desktopHost/browserHost' const MOCK_DELETE_PROVIDER = vi.fn() const MOCK_GET_SETTINGS = vi.fn() @@ -122,6 +123,39 @@ vi.mock('../components/chat/CodeViewer', () => ({ CodeViewer: ({ code }: { code: string }) =>
{code}
, })) +function installElectronDesktopHost() { + window.desktopHost = { + ...browserHost, + kind: 'electron', + isDesktop: true, + capabilities: { + ...browserHost.capabilities, + appMode: true, + dialogs: true, + notifications: true, + shell: true, + updates: true, + zoom: true, + }, + app: { + getVersion: vi.fn().mockResolvedValue('0.3.2'), + }, + dialogs: { + ...browserHost.dialogs, + open: vi.fn((options) => tauriDialogMock.open(options)), + }, + shell: { + ...browserHost.shell, + open: vi.fn().mockResolvedValue(undefined), + }, + appMode: { + ...browserHost.appMode, + prepareRestart: vi.fn(() => tauriCoreMock.invoke('prepare_for_app_mode_restart')), + restart: vi.fn(() => tauriProcessMock.relaunch()), + }, + } +} + describe('Settings > General tab', () => { beforeEach(() => { vi.useRealTimers() @@ -143,6 +177,8 @@ describe('Settings > General tab', () => { tauriProcessMock.relaunch.mockReset() tauriProcessMock.relaunch.mockResolvedValue(undefined) delete (window as unknown as { __TAURI_INTERNALS__?: object }).__TAURI_INTERNALS__ + delete (window as unknown as { __TAURI__?: object }).__TAURI__ + installElectronDesktopHost() MOCK_GET_SETTINGS.mockResolvedValue({}) MOCK_UPDATE_SETTINGS.mockResolvedValue({}) providerStoreState.providers = [] @@ -408,9 +444,6 @@ describe('Settings > General tab', () => { }) it('keeps data storage at the bottom of General settings', () => { - const tauriWindow = window as unknown as { __TAURI_INTERNALS__?: object } - tauriWindow.__TAURI_INTERNALS__ = {} - render() fireEvent.click(screen.getByText('General')) @@ -423,9 +456,6 @@ describe('Settings > General tab', () => { }) it('lets desktop users choose a portable data directory and relaunch immediately', async () => { - const tauriWindow = window as unknown as { __TAURI_INTERNALS__?: object } - tauriWindow.__TAURI_INTERNALS__ = {} - render() fireEvent.click(screen.getByText('General')) @@ -447,8 +477,6 @@ describe('Settings > General tab', () => { }) it('switches back to the system directory without deleting portable data', async () => { - const tauriWindow = window as unknown as { __TAURI_INTERNALS__?: object } - tauriWindow.__TAURI_INTERNALS__ = {} useSettingsStore.setState({ appMode: { mode: 'portable', @@ -475,9 +503,6 @@ describe('Settings > General tab', () => { }) it('validates portable directory input and lets users reset to the app-side folder', async () => { - const tauriWindow = window as unknown as { __TAURI_INTERNALS__?: object } - tauriWindow.__TAURI_INTERNALS__ = {} - render() fireEvent.click(screen.getByText('General')) @@ -493,8 +518,6 @@ describe('Settings > General tab', () => { }) it('shows folder picker failures as an inline storage error', async () => { - const tauriWindow = window as unknown as { __TAURI_INTERNALS__?: object } - tauriWindow.__TAURI_INTERNALS__ = {} tauriDialogMock.open.mockRejectedValueOnce(new Error('dialog unavailable')) render() @@ -506,8 +529,6 @@ describe('Settings > General tab', () => { }) it('treats external CLAUDE_CONFIG_DIR as the controlling data source', async () => { - const tauriWindow = window as unknown as { __TAURI_INTERNALS__?: object } - tauriWindow.__TAURI_INTERNALS__ = {} useSettingsStore.setState({ appMode: { mode: 'portable', @@ -533,9 +554,6 @@ describe('Settings > General tab', () => { }) it('keeps mode switch confirmation cancelable before restart starts', async () => { - const tauriWindow = window as unknown as { __TAURI_INTERNALS__?: object } - tauriWindow.__TAURI_INTERNALS__ = {} - render() fireEvent.click(screen.getByText('General')) @@ -551,8 +569,6 @@ describe('Settings > General tab', () => { }) it('shows restart preparation failures without relaunching', async () => { - const tauriWindow = window as unknown as { __TAURI_INTERNALS__?: object } - tauriWindow.__TAURI_INTERNALS__ = {} tauriCoreMock.invoke.mockRejectedValueOnce(new Error('restart preparation failed')) render() @@ -566,8 +582,6 @@ describe('Settings > General tab', () => { }) it('shows the saved restart-required state inside the storage section', () => { - const tauriWindow = window as unknown as { __TAURI_INTERNALS__?: object } - tauriWindow.__TAURI_INTERNALS__ = {} useSettingsStore.setState({ appModeRequiresRestart: true }) render() diff --git a/desktop/src/components/chat/ChatInput.test.tsx b/desktop/src/components/chat/ChatInput.test.tsx index 9e39ec2d..a482110b 100644 --- a/desktop/src/components/chat/ChatInput.test.tsx +++ b/desktop/src/components/chat/ChatInput.test.tsx @@ -61,19 +61,6 @@ vi.mock('../../api/websocket', () => ({ }, })) -vi.mock('@tauri-apps/plugin-dialog', () => ({ - open: mocks.dialogOpen, -})) - -vi.mock('@tauri-apps/api/webview', () => ({ - getCurrentWebview: () => ({ - onDragDropEvent: vi.fn(async (handler: (event: { payload: unknown }) => void) => { - mocks.webviewDragHandlers.push(handler) - return mocks.webviewUnlisten - }), - }), -})) - vi.mock('../../hooks/useMobileViewport', () => ({ useMobileViewport: () => viewportMocks.isMobile, })) @@ -92,6 +79,7 @@ import { useSessionStore } from '../../stores/sessionStore' import { useSettingsStore } from '../../stores/settingsStore' import { useTabStore } from '../../stores/tabStore' import { useWorkspaceChatContextStore } from '../../stores/workspaceChatContextStore' +import { browserHost } from '../../lib/desktopHost/browserHost' function okRepositoryContext() { return { @@ -134,9 +122,33 @@ describe('ChatInput file mentions', () => { const initialTabState = useTabStore.getInitialState() const initialWorkspaceContextState = useWorkspaceChatContextStore.getInitialState() + const installElectronFileHost = () => { + window.desktopHost = { + ...browserHost, + kind: 'electron', + isDesktop: true, + capabilities: { + ...browserHost.capabilities, + dialogs: true, + }, + dialogs: { + ...browserHost.dialogs, + open: mocks.dialogOpen, + }, + webview: { + ...browserHost.webview, + onDragDropEvent: async (handler) => { + mocks.webviewDragHandlers.push(handler as (event: { payload: unknown }) => void) + return mocks.webviewUnlisten + }, + }, + } + } + beforeEach(() => { vi.clearAllMocks() mocks.webviewDragHandlers.length = 0 + Reflect.deleteProperty(window, 'desktopHost') delete (window as Window & { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ viewportMocks.isMobile = false useSettingsStore.setState({ locale: 'en' }) @@ -785,10 +797,7 @@ describe('ChatInput file mentions', () => { }) it('uses native desktop file paths instead of inlining selected files', async () => { - Object.defineProperty(window, '__TAURI_INTERNALS__', { - configurable: true, - value: {}, - }) + installElectronFileHost() mocks.dialogOpen.mockResolvedValueOnce([ '/Users/nanmi/tmp/large-a.log', 'C:\\Users\\Nanmi\\Desktop\\large-b.zip', @@ -832,10 +841,7 @@ describe('ChatInput file mentions', () => { }) it('accepts native desktop file drops on the active session composer as path-only attachments', async () => { - Object.defineProperty(window, '__TAURI_INTERNALS__', { - configurable: true, - value: {}, - }) + installElectronFileHost() render() diff --git a/desktop/src/components/chat/useComposerFileDrop.ts b/desktop/src/components/chat/useComposerFileDrop.ts index 16b6b71b..ba6216eb 100644 --- a/desktop/src/components/chat/useComposerFileDrop.ts +++ b/desktop/src/components/chat/useComposerFileDrop.ts @@ -7,20 +7,20 @@ import { type ComposerAttachment, } from '../../lib/composerAttachments' -type TauriDropPosition = { +type DesktopDropPosition = { x: number y: number } -type TauriDragDropPayload = - | { type: 'enter'; paths: string[]; position: TauriDropPosition } - | { type: 'over'; position: TauriDropPosition } - | { type: 'drop'; paths: string[]; position: TauriDropPosition } +type DesktopDragDropPayload = + | { type: 'enter'; paths: string[]; position: DesktopDropPosition } + | { type: 'over'; position: DesktopDropPosition } + | { type: 'drop'; paths: string[]; position: DesktopDropPosition } | { type: 'leave' } | { type: 'cancel' } -type TauriDragDropEvent = { - payload: TauriDragDropPayload +type DesktopDragDropEvent = { + payload: DesktopDragDropPayload } type UseComposerFileDropOptions = { @@ -30,7 +30,7 @@ type UseComposerFileDropOptions = { onError?: (error: unknown) => void } -function isPointInsideElement(element: HTMLElement | null, position: TauriDropPosition): boolean { +function isPointInsideElement(element: HTMLElement | null, position: DesktopDropPosition): boolean { if (!element) return false const rect = element.getBoundingClientRect() return ( @@ -76,7 +76,7 @@ export function useComposerFileDrop({ .onDragDropEvent((event) => { if (disposed) return - const payload = (event as TauriDragDropEvent).payload + const payload = (event as DesktopDragDropEvent).payload if (payload.type === 'cancel' || payload.type === 'leave') { dragDepthRef.current = 0 setIsDragActive(false) diff --git a/desktop/src/components/layout/Sidebar.test.tsx b/desktop/src/components/layout/Sidebar.test.tsx index f80b585a..5fe96033 100644 --- a/desktop/src/components/layout/Sidebar.test.tsx +++ b/desktop/src/components/layout/Sidebar.test.tsx @@ -254,7 +254,7 @@ describe('Sidebar', () => { { sessionId: 'session-new-1', title: 'New Session', type: 'session', status: 'idle' }, ]) expect(useTabStore.getState().activeTabId).toBe('session-new-1') - expect(screen.getByRole('complementary')).not.toHaveAttribute('data-tauri-drag-region') + expect(screen.getByRole('complementary')).not.toHaveAttribute('data-desktop-drag-region') }) it('groups sessions by project and expands overflow rows', () => { diff --git a/desktop/src/components/layout/TabBar.test.tsx b/desktop/src/components/layout/TabBar.test.tsx index 866e290a..1b8ad014 100644 --- a/desktop/src/components/layout/TabBar.test.tsx +++ b/desktop/src/components/layout/TabBar.test.tsx @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import '@testing-library/jest-dom' import type { PerSessionState } from '../../stores/chatStore' import type { ChatState } from '../../types/chat' +import { browserHost } from '../../lib/desktopHost/browserHost' const startDraggingMock = vi.hoisted(() => vi.fn(() => Promise.resolve())) const getCurrentWindowMock = vi.hoisted(() => vi.fn(() => ({ @@ -98,6 +99,22 @@ vi.mock('./WindowControls', () => ({ })) describe('TabBar', () => { + const installElectronDesktopHost = () => { + window.desktopHost = { + ...browserHost, + kind: 'electron', + isDesktop: true, + capabilities: { + ...browserHost.capabilities, + windowControls: true, + }, + window: { + ...browserHost.window, + startDragging: startDraggingMock, + }, + } + } + beforeEach(() => { class ResizeObserverMock { constructor(_callback: ResizeObserverCallback) {} @@ -113,10 +130,8 @@ describe('TabBar', () => { value: ResizeObserverMock, }) - Object.defineProperty(window, '__TAURI__', { - configurable: true, - value: {}, - }) + Reflect.deleteProperty(window, '__TAURI__') + installElectronDesktopHost() Object.defineProperty(window.HTMLElement.prototype, 'scrollIntoView', { configurable: true, @@ -157,7 +172,8 @@ describe('TabBar', () => { useTerminalPanelStore.setState(useTerminalPanelStore.getInitialState(), true) useBrowserPanelStore.setState(useBrowserPanelStore.getInitialState(), true) - delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__ + Reflect.deleteProperty(window, 'desktopHost') + Reflect.deleteProperty(window, '__TAURI__') }) it('scrolls the active tab into view when the active tab changes', async () => { @@ -291,8 +307,8 @@ describe('TabBar', () => { render() }) - expect(screen.getByTestId('tab-bar')).not.toHaveAttribute('data-tauri-drag-region') - expect(screen.getByTestId('tab-bar-drag-gutter')).toHaveAttribute('data-tauri-drag-region') + expect(screen.getByTestId('tab-bar')).not.toHaveAttribute('data-desktop-drag-region') + expect(screen.getByTestId('tab-bar-drag-gutter')).toHaveAttribute('data-desktop-drag-region') }) it('keeps the desktop tab strip at a roomier titlebar height', async () => { @@ -452,7 +468,7 @@ describe('TabBar', () => { }) it('hides the open-project control outside the desktop shell', async () => { - delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__ + Reflect.deleteProperty(window, 'desktopHost') const { TabBar } = await import('./TabBar') const { useTabStore } = await import('../../stores/tabStore') diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index 4ea1bdd4..90f13ef4 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -407,7 +407,7 @@ export function TabBar() { {isDesktopRuntime && (