From 81352c824896325a654273b612b196e3415e5d2b Mon Sep 17 00:00:00 2001 From: MocA-Love Date: Sat, 25 Apr 2026 02:28:23 +0900 Subject: [PATCH 1/6] feat(desktop): Windows x64 build and runtime support (#273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 最初の Windows 対応。Windows 10/11 x64 で app が起動し、 ワークスペース/ターミナル/チャットなどのコア機能が動く状態を作る。 agent hook と一部の macOS 固有統合は Windows では skip して known limitation とし、follow-up PR で段階的に PowerShell 実装を追加する。 ## 起動基盤 (upstream #2100 を fork 用に再実装) - scripts/postinstall.sh -> scripts/postinstall.mjs (fork の \$CI 早期 exit ロジックを維持、Windows では install:deps を非致命に) - electron-builder.ts: npmRebuild を Windows で無効化 (VS Build Tools 無しでの node-gyp 失敗を回避)、NSIS Desktop/ Start Menu ショートカットを追加 - electron.vite.config.ts: rollup banner で require("electron") 前に ELECTRON_RUN_AS_NODE をクリア、renderer に stripCrossOriginPlugin を追加 (ASAR file:// の CORS 問題でブラック画面になるのを防止) - copy-native-modules.ts: Windows で Bun が作る directory junction を recursive rmSync で削除するよう分岐 - factories/app/setup.ts: Windows でも GPU hardware acceleration を disable (ドライバ互換によるブラック画面防止) - window-loader.ts: Windows production は superset-app:// カスタム プロトコル経由でロード (file:// だと ES dynamic import が破綻する) - main/index.ts: superset-app scheme の privileged 登録 + protocol handler、api.superset.sh / PostHog / Sentry への outbound CORS バイパス - main/windows/main.ts: Windows は titleBarOverlay で閉じる/最小化 ボタンを描画、nativeTheme で dark/light 色追従、renderer console の warning/error を stdout に forward - vite/helpers.ts: defineEnv の ?? を || に変更 (空文字列 CI secret フォールバック)、stripCrossOriginPlugin を export ## macOS 固有機能の Windows fallback - play-sound.ts: Windows で PowerShell の System.Media.SoundPlayer (WAV) / MediaPlayer (他) 経由の再生分岐を追加 - ScriptsEditor.tsx: file input の accept に .ps1/.cmd/.bat を追加 - auto-updater は既に IS_FORK path で GitHub API 経由のチェックが動く ため Windows でも更新通知が出る (変更なし) ## agent-setup / host-service - agent-setup/index.ts: Windows では wrapper/hook setup を skip。 bash 専用の notify / copilot / cursor / gemini / codex テンプレートを PowerShell に完全移植する作業は follow-up PR - host-service/.../setup-terminal.ts + runtime/teardown/teardown.ts: Windows では setup.ps1 / .cmd / .bat (resp. teardown) を探して powershell.exe -File または cmd.exe /c で spawn。POSIX は従来どおり setup.sh / teardown.sh - providers/git/.../askpass.ts: Windows では findstr ベースの .cmd askpass ヘルパを生成 ## CI - .github/workflows/build-desktop.yml: windows-latest で NSIS x64 インストーラをビルドする build-windows job を追加。 git config --global core.longpaths true、Git Bash で fork 既存の bash スクリプトを動かし、bun run build:browser-mcp で superset-browser-mcp.exe を生成、electron-builder --win で .exe + .exe.blockmap + latest.yml を artifact にアップロード ## Follow-up (本 PR の対象外) - notify / copilot / cursor / gemini / codex hook テンプレートの PowerShell 版 - Windows での agent wrapper (PATH 注入, sleep-inhibitor) - Windows 11 の Mica / Acrylic vibrancy fallback - Windows NSIS auto-update 設定 (Squirrel.Windows 対応) - dev mode の deep-link 登録 (Windows レジストリ) - BUILDING.md / README の Windows 手順追記 Refs: #273 --- .github/workflows/build-desktop.yml | 126 ++++++++++++++++++ apps/desktop/electron-builder.ts | 9 +- apps/desktop/electron.vite.config.ts | 7 + apps/desktop/scripts/copy-native-modules.ts | 17 ++- .../lib/electron-app/factories/app/setup.ts | 4 +- apps/desktop/src/lib/window-loader.ts | 12 +- apps/desktop/src/main/index.ts | 58 ++++++++ .../desktop/src/main/lib/agent-setup/index.ts | 13 ++ apps/desktop/src/main/lib/play-sound.ts | 27 +++- apps/desktop/src/main/windows/main.ts | 44 +++++- .../ScriptsEditor/ScriptsEditor.tsx | 2 +- apps/desktop/vite/helpers.ts | 24 +++- package.json | 2 +- .../git/CloudGitCredentialProvider/askpass.ts | 15 +++ .../src/runtime/teardown/teardown.ts | 58 +++++++- .../shared/setup-terminal.ts | 77 ++++++++--- plans/20260425-windows-desktop-support.md | 115 ++++++++++++++++ scripts/postinstall.mjs | 56 ++++++++ scripts/postinstall.sh | 23 ---- 19 files changed, 626 insertions(+), 63 deletions(-) create mode 100644 plans/20260425-windows-desktop-support.md create mode 100644 scripts/postinstall.mjs delete mode 100755 scripts/postinstall.sh diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 6d0f5733b41..3d23672f022 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -263,3 +263,129 @@ jobs: path: apps/desktop/release/*-linux.yml retention-days: ${{ inputs.artifact_retention_days }} if-no-files-found: error + + build-windows: + name: Build - Windows (x64) + runs-on: windows-latest + environment: production + + defaults: + run: + # bash (Git Bash) is bundled on windows-latest runners and is used so + # the cross-platform scripts behave like they do on macOS/Linux. + shell: bash + + steps: + - name: Enable git long paths + shell: pwsh + run: git config --global core.longpaths true + + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Setup Bun + id: setup-bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version-file: .bun-version + + - name: Cache dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-revision }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-revision }}- + + - name: Install dependencies + run: bun install --frozen --ignore-scripts + + - name: Install desktop native dependencies + working-directory: apps/desktop + run: bun run install:deps + + - name: Set version suffix + if: inputs.version_suffix != '' + working-directory: apps/desktop + run: | + CURRENT_VERSION=$(node -p "require('./package.json').version") + NEW_VERSION="${CURRENT_VERSION}${{ inputs.version_suffix }}" + echo "Setting version to: $NEW_VERSION" + node -e " + const fs = require('fs'); + const pkg = require('./package.json'); + pkg.version = '$NEW_VERSION'; + fs.writeFileSync('./package.json', JSON.stringify(pkg, null, '\t') + '\n'); + " + echo "Updated package.json version to $NEW_VERSION" + + - name: Clean dev folder + working-directory: apps/desktop + run: bun run clean:dev + + - name: Generate file icons + working-directory: apps/desktop + run: bun run generate:icons + + - name: Compile app with electron-vite + working-directory: apps/desktop + env: + NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} + NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }} + NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }} + NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} + NEXT_PUBLIC_DOCS_URL: ${{ secrets.NEXT_PUBLIC_DOCS_URL }} + NEXT_PUBLIC_STREAMS_URL: ${{ secrets.NEXT_PUBLIC_STREAMS_URL }} + NEXT_PUBLIC_ELECTRIC_URL: ${{ secrets.NEXT_PUBLIC_ELECTRIC_URL }} + SENTRY_DSN_DESKTOP: ${{ secrets.SENTRY_DSN_DESKTOP }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + RELAY_URL: ${{ secrets.RELAY_URL }} + SUPERSET_WORKSPACE_NAME: superset + run: bun run compile:app + + - name: Build superset-browser-mcp binary + # Bun doesn't auto-run lifecycle hooks, so the prebuild step that emits + # Resources/resources/superset-browser-mcp/superset-browser-mcp.exe + # has to be invoked explicitly. + working-directory: apps/desktop + run: bun run build:browser-mcp + + - name: Build Electron app (NSIS) + working-directory: apps/desktop + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + run: bun run package -- --publish never --config ${{ inputs.electron_builder_config }} --win + + - name: Verify Windows installer + update manifest exist + working-directory: apps/desktop + run: | + ls -la release + test -n "$(ls -1 release/*.exe 2>/dev/null)" || { + echo "::error::No NSIS .exe generated in apps/desktop/release" + exit 1 + } + test -f release/latest.yml || { + echo "::error::latest.yml (auto-update manifest) missing" + exit 1 + } + + - name: Upload NSIS installer artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ inputs.artifact_prefix }}-win-x64-nsis + path: | + apps/desktop/release/*.exe + apps/desktop/release/*.exe.blockmap + retention-days: ${{ inputs.artifact_retention_days }} + if-no-files-found: error + + - name: Upload Windows auto-update manifest + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ inputs.artifact_prefix }}-win-x64-update-manifest + path: apps/desktop/release/latest.yml + retention-days: ${{ inputs.artifact_retention_days }} + if-no-files-found: error diff --git a/apps/desktop/electron-builder.ts b/apps/desktop/electron-builder.ts index bcab6a2f253..655f30a3961 100644 --- a/apps/desktop/electron-builder.ts +++ b/apps/desktop/electron-builder.ts @@ -96,8 +96,10 @@ const config: Configuration = { "!**/.DS_Store", ], - // Rebuild native modules for Electron's Node.js version - npmRebuild: true, + // Rebuild native modules for Electron's Node.js version. + // Disabled on Windows — native modules are materialized by install:deps + + // copy:native-modules, and node-gyp fails without Visual Studio Build Tools. + npmRebuild: process.platform !== "win32", // macOS DMG // NOTE: dmgbuild 1.2.0 は size = (sum(app files) + 128MB) を割り当てるが、 @@ -173,6 +175,9 @@ const config: Configuration = { nsis: { oneClick: false, allowToChangeInstallationDirectory: true, + createDesktopShortcut: true, + createStartMenuShortcut: true, + shortcutName: productName, }, }; diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 3637b123c23..310eff05ec9 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -15,6 +15,7 @@ import { defineEnv, devPath, htmlEnvTransformPlugin, + stripCrossOriginPlugin, } from "./vite/helpers"; // override: true ensures .env values take precedence over inherited env vars @@ -124,6 +125,11 @@ export default defineConfig({ }, output: { dir: resolve(devPath, "main"), + // VS Code and other Electron hosts set ELECTRON_RUN_AS_NODE=1 on + // their child process env; leaving it set puts Electron into plain + // Node mode and the app never opens a window. Clear it before any + // require("electron") call — must be the very first statement. + banner: "delete process.env.ELECTRON_RUN_AS_NODE;", }, external: ["electron", ...mainExternalizedDependencies], plugins: [sentryPlugin].filter(Boolean), @@ -253,6 +259,7 @@ export default defineConfig({ }), reactPlugin(), htmlEnvTransformPlugin(), + stripCrossOriginPlugin(), ], worker: { diff --git a/apps/desktop/scripts/copy-native-modules.ts b/apps/desktop/scripts/copy-native-modules.ts index c730c9de9ac..6008a77da06 100644 --- a/apps/desktop/scripts/copy-native-modules.ts +++ b/apps/desktop/scripts/copy-native-modules.ts @@ -122,8 +122,13 @@ function copyModuleIfSymlink( console.log(` ${moduleName}: symlink -> replacing with real files`); console.log(` Real path: ${realPath}`); - // Remove the symlink - rmSync(modulePath); + // Windows: Bun materializes direct deps as directory junctions instead of + // file symlinks; rmSync needs { recursive: true } to remove them. + if (process.platform === "win32") { + rmSync(modulePath, { recursive: true, force: true }); + } else { + rmSync(modulePath); + } // Copy the actual files cpSync(realPath, modulePath, { recursive: true }); @@ -275,7 +280,13 @@ function copyDependencyForPackage( const nestedStats = lstatSync(nestedDependencyPath); if (nestedStats.isSymbolicLink()) { const realPath = realpathSync(nestedDependencyPath); - rmSync(nestedDependencyPath); + // Windows: bun materializes these as directory junctions, not + // file symlinks; rmSync needs { recursive: true } to remove them. + if (process.platform === "win32") { + rmSync(nestedDependencyPath, { recursive: true, force: true }); + } else { + rmSync(nestedDependencyPath); + } cpSync(realPath, nestedDependencyPath, { recursive: true, }); diff --git a/apps/desktop/src/lib/electron-app/factories/app/setup.ts b/apps/desktop/src/lib/electron-app/factories/app/setup.ts index 67385353019..fae84025cc6 100644 --- a/apps/desktop/src/lib/electron-app/factories/app/setup.ts +++ b/apps/desktop/src/lib/electron-app/factories/app/setup.ts @@ -59,7 +59,9 @@ export async function makeAppSetup( return window; } -PLATFORM.IS_LINUX && app.disableHardwareAcceleration(); +// Disable GPU hardware acceleration on Linux and Windows to prevent black/blank +// screens caused by driver incompatibilities with Chromium's compositor. +(PLATFORM.IS_LINUX || PLATFORM.IS_WINDOWS) && app.disableHardwareAcceleration(); // macOS Sequoia+: occluded window throttling can corrupt GPU compositor layers if (PLATFORM.IS_MAC) { diff --git a/apps/desktop/src/lib/window-loader.ts b/apps/desktop/src/lib/window-loader.ts index 1b01eb63c66..e116fa54edd 100644 --- a/apps/desktop/src/lib/window-loader.ts +++ b/apps/desktop/src/lib/window-loader.ts @@ -24,9 +24,17 @@ export function registerRoute(props: { const url = `http://localhost:${env.DESKTOP_VITE_PORT}/#/`; console.log("[window-loader] Loading development URL:", url); props.browserWindow.loadURL(url); + } else if (process.platform === "win32") { + // Production (Windows): file:// breaks ES module dynamic imports + // (code-split route chunks) in Electron on Windows. The custom + // superset-app:// protocol — registered in main/index.ts — serves the + // same renderer files with proper module support so lazy routes load. + const url = "superset-app://app/index.html#/"; + console.log("[window-loader] Loading custom protocol URL:", url); + props.browserWindow.loadURL(url); } else { - // Production: load from file with hash routing - // TanStack Router uses hash-based routing, so we always start at #/ + // Production (macOS / Linux): load from file with hash routing. + // TanStack Router uses hash-based routing, so we always start at #/. console.log("[window-loader] Loading file:", props.htmlFile); props.browserWindow.loadFile(props.htmlFile, { hash: "/" }); } diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 6fa586e9c18..295c8d11c21 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -598,6 +598,17 @@ protocol.registerSchemesAsPrivileged([ supportFetchAPI: true, }, }, + { + // Windows production loader uses this scheme so ES module dynamic imports + // (code-split route chunks) work — file:// breaks them on Windows. + scheme: "superset-app", + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: true, + }, + }, ]); const gotTheLock = app.requestSingleInstanceLock(); @@ -766,6 +777,53 @@ if (!gotTheLock) { .fromPartition("persist:superset") .protocol.handle("superset-workspace-media", workspaceMediaHandler); + // Windows production: serve renderer bundle through a custom protocol so + // ES module dynamic imports work (file:// breaks them on Windows). + if (PLATFORM.IS_WINDOWS && !IS_DEV) { + const rendererDir = path.join(__dirname, "../renderer"); + const appProtocolHandler = (request: Request) => { + let urlPath = new URL(request.url).pathname; + if (urlPath.startsWith("/")) urlPath = urlPath.slice(1); + const filePath = path.join(rendererDir, urlPath); + return net.fetch(pathToFileURL(filePath).toString()); + }; + protocol.handle("superset-app", appProtocolHandler); + session + .fromPartition("persist:superset") + .protocol.handle("superset-app", appProtocolHandler); + + // API server's CORS policy doesn't allow the custom scheme origin. + // Rewrite the outgoing Origin and echo back access-control-allow-origin + // so API / PostHog / Sentry calls succeed from superset-app://app. + const appSession = session.fromPartition("persist:superset"); + const apiHost = new URL( + process.env.NEXT_PUBLIC_API_URL || "https://api.superset.sh", + ).host; + const corsTargets = [ + `https://${apiHost}/*`, + "https://*.posthog.com/*", + "https://*.sentry.io/*", + ]; + appSession.webRequest.onBeforeSendHeaders( + { urls: corsTargets }, + (details, callback) => { + if (details.requestHeaders.Origin === "superset-app://app") { + delete details.requestHeaders.Origin; + } + callback({ requestHeaders: details.requestHeaders }); + }, + ); + appSession.webRequest.onHeadersReceived( + { urls: [`https://${apiHost}/*`] }, + (details, callback) => { + const headers = details.responseHeaders ?? {}; + headers["access-control-allow-origin"] = ["superset-app://app"]; + headers["access-control-allow-credentials"] = ["true"]; + callback({ responseHeaders: headers }); + }, + ); + } + ensureProjectIconsDir(); setWorkspaceDockIcon(); initSentry(); diff --git a/apps/desktop/src/main/lib/agent-setup/index.ts b/apps/desktop/src/main/lib/agent-setup/index.ts index 3b8ddd33324..2fd35c30a44 100644 --- a/apps/desktop/src/main/lib/agent-setup/index.ts +++ b/apps/desktop/src/main/lib/agent-setup/index.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import { PLATFORM } from "shared/constants"; import { setupDesktopAgentCapabilities } from "./desktop-agent-setup"; import { BASH_DIR, @@ -16,6 +17,18 @@ import { } from "./shell-wrappers"; export function setupAgentHooks(): void { + // Windows: the current wrappers + notify / copilot / cursor / gemini / + // codex hooks are bash-only. Skipping setup keeps the app usable on + // Windows — agents run from the system PATH without Superset wrappers, so + // agent-side notifications and PATH injection are disabled until a native + // PowerShell implementation lands. Tracked in issue #273. + if (PLATFORM.IS_WINDOWS) { + console.log( + "[agent-setup] Skipping agent hook setup on Windows (not yet implemented)", + ); + return; + } + console.log("[agent-setup] Initializing agent hooks..."); fs.mkdirSync(BIN_DIR, { recursive: true }); diff --git a/apps/desktop/src/main/lib/play-sound.ts b/apps/desktop/src/main/lib/play-sound.ts index 17fdf7a5dcc..9471be49f2e 100644 --- a/apps/desktop/src/main/lib/play-sound.ts +++ b/apps/desktop/src/main/lib/play-sound.ts @@ -12,8 +12,11 @@ interface PlaySoundCallbacks { * Plays a sound file at the given volume using platform-specific commands. * Returns the primary ChildProcess, or null if playback was skipped. * - * On macOS, volume is controlled via afplay -v (0.0-1.0). - * On Linux, volume is controlled via paplay --volume (0-65536), with aplay fallback. + * - macOS: afplay -v (0.0-1.0) + * - Linux: paplay --volume (0-65536), with aplay fallback + * - Windows: PowerShell + System.Media.SoundPlayer (WAV) or MediaPlayer (other). + * System.Media.SoundPlayer doesn't support volume control, so the requested + * volume is honored only as a mute toggle (volume === 0 → skip playback). */ export function playSoundFile( soundPath: string, @@ -33,6 +36,26 @@ export function playSoundFile( ); } + if (process.platform === "win32") { + if (volume === 0) { + callbacks?.onComplete?.(); + return null; + } + // PowerShell arguments are single-quoted to avoid shell injection; any + // single quote in the path is escaped per PowerShell conventions. + const escapedPath = soundPath.replace(/'/g, "''"); + const isWav = /\.wav$/i.test(soundPath); + const script = isWav + ? `$p = New-Object Media.SoundPlayer '${escapedPath}'; $p.PlaySync()` + : `Add-Type -AssemblyName presentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([System.Uri]::new('${escapedPath}')); $p.Volume = ${volumeDecimal}; $p.Play(); Start-Sleep -Milliseconds 500; while ($p.NaturalDuration.HasTimeSpan -and $p.Position -lt $p.NaturalDuration.TimeSpan) { Start-Sleep -Milliseconds 200 }`; + return execFile( + "powershell.exe", + ["-NoProfile", "-NonInteractive", "-Command", script], + { windowsHide: true }, + () => callbacks?.onComplete?.(), + ); + } + // Linux: paplay --volume accepts 0-65536 (65536 = 100%) const paVolume = Math.round(volumeDecimal * 65536); return execFile( diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index ad5d2cda604..7104bc1b736 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -333,7 +333,20 @@ export async function MainWindow() { autoHideMenuBar: true, frame: false, titleBarStyle: "hidden", - trafficLightPosition: { x: 16, y: 16 }, + // Windows has no traffic-light controls; use the Electron overlay so the + // built-in minimize/maximize/close buttons render on top of the custom + // title bar. macOS keeps the familiar red/yellow/green indent. + ...(PLATFORM.IS_WINDOWS + ? { + titleBarOverlay: { + color: nativeTheme.shouldUseDarkColors ? "#1e1e1e" : "#ffffff", + symbolColor: nativeTheme.shouldUseDarkColors + ? "#ffffff" + : "#000000", + height: 35, + }, + } + : { trafficLightPosition: { x: 16, y: 16 } }), webPreferences: { preload: join(__dirname, "../preload/index.js"), webviewTag: true, @@ -353,6 +366,35 @@ export async function MainWindow() { window.webContents.setBackgroundThrottling(false); } + // Windows: forward renderer warnings/errors to the main process stdout so + // black-screen-style startup failures show up in the Electron log rather + // than being trapped inside the DevTools that the user cannot open. + if (PLATFORM.IS_WINDOWS) { + window.webContents.on( + "console-message", + (_event, level, message, line, sourceId) => { + if (level < 2) return; + const levelStr = + ["verbose", "info", "warning", "error"][level] ?? "unknown"; + const source = sourceId ? ` (${sourceId}:${line})` : ""; + const formatted = `[renderer:${levelStr}] ${message}${source}`; + if (level === 3) console.error(formatted); + else console.warn(formatted); + }, + ); + + // Keep the title-bar overlay contrast aligned with the OS theme — it is + // a Windows-only API so the call is safely gated. + nativeTheme.on("updated", () => { + if (window.isDestroyed()) return; + window.setTitleBarOverlay?.({ + color: nativeTheme.shouldUseDarkColors ? "#1e1e1e" : "#ffffff", + symbolColor: nativeTheme.shouldUseDarkColors ? "#ffffff" : "#000000", + height: 35, + }); + }); + } + if (ipcHandler) { ipcHandler.attachWindow(window); } else { diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/ScriptsEditor/ScriptsEditor.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/ScriptsEditor/ScriptsEditor.tsx index d22a8090fdd..31e6ede9b1f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/ScriptsEditor/ScriptsEditor.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/ScriptsEditor/ScriptsEditor.tsx @@ -155,7 +155,7 @@ function ScriptTextarea({ diff --git a/apps/desktop/vite/helpers.ts b/apps/desktop/vite/helpers.ts index b2552e2dba6..84a5d8a87b1 100644 --- a/apps/desktop/vite/helpers.ts +++ b/apps/desktop/vite/helpers.ts @@ -20,7 +20,10 @@ export function defineEnv( value: string | undefined, fallback?: string, ): string { - return JSON.stringify(value ?? fallback); + // `||` instead of `??` so empty strings from unresolved CI secrets fall + // back to the default (matters when a CI workflow references a missing + // env var and injects the empty string). + return JSON.stringify(value || fallback); } const RESOURCES_TO_COPY = [ @@ -66,6 +69,25 @@ export function copyResourcesPlugin(): Plugin { }; } +/** + * Strips the `crossorigin` attribute that Vite injects on