From c7641f9d5e182e05e47d74b2d2f27568e962a399 Mon Sep 17 00:00:00 2001 From: ddonaldson130 Date: Thu, 2 Jul 2026 23:03:43 -0400 Subject: [PATCH 1/5] fix(cli): add electron as runtime dependency for fusion desktop The published @runfusion/fusion package ships the desktop runtime assets in dist/desktop/, but the [title-id-drift] db.ts migration normalized 0 active titles [done-paused-backfill] db.ts migration repaired 0 done task rows [title-id-drift] archive-db normalized 0 archived titles [desktop] Auto-registered project "Fusion" at C:\Users\drewd\Tools\fusion-latest [server:terminal] WebSocket server mounted { path: '/api/terminal/ws' } [ai-session-store] Cleanup removed stale sessions { terminalDeleted: 0, orphanedDeleted: 0, totalDeleted: 0, maxAgeMs: 604800000, operation: 'cleanup-stale-sessions', _emittedAt: '2026-07-03T03:03:31.683Z', _diagnosticsId: 'bc57bc09-d320-46b1-8717-a139f95bb9b7' } [server] AI session cleanup summary { message: 'Removed stale AI sessions', source: 'initial', ttlMs: 604800000, terminalDeleted: 0, orphanedDeleted: 0, totalDeleted: 0 } command calls require('electron') to find the Electron binary. Electron was only declared as a built dependency in pnpm-workspace.yaml, which makes it available during source builds but does not include it in the published npm package. As a result, [title-id-drift] archive-db normalized 0 archived titles [server:terminal] WebSocket server mounted { path: '/api/terminal/ws' } [ai-session-store] Cleanup removed stale sessions { terminalDeleted: 0, orphanedDeleted: 0, totalDeleted: 0, maxAgeMs: 604800000, operation: 'cleanup-stale-sessions', _emittedAt: '2026-07-03T03:03:37.491Z', _diagnosticsId: 'a0c428a8-133d-4673-ab93-82a092646b13' } [server] AI session cleanup summary { message: 'Removed stale AI sessions', source: 'initial', ttlMs: 604800000, terminalDeleted: 0, orphanedDeleted: 0, totalDeleted: 0 } fails or hangs for npm consumers. Fix: move electron to the dependencies of @runfusion/fusion so it is installed alongside the CLI, and remove it from onlyBuiltDependencies. Fixes: the desktop launcher regression where [title-id-drift] archive-db normalized 0 archived titles [server:terminal] WebSocket server mounted { path: '/api/terminal/ws' } [ai-session-store] Cleanup removed stale sessions { terminalDeleted: 0, orphanedDeleted: 0, totalDeleted: 0, maxAgeMs: 604800000, operation: 'cleanup-stale-sessions', _emittedAt: '2026-07-03T03:03:43.460Z', _diagnosticsId: '325b9f28-e5d3-4937-855e-c5d1b95420b8' } [server] AI session cleanup summary { message: 'Removed stale AI sessions', source: 'initial', ttlMs: 604800000, terminalDeleted: 0, orphanedDeleted: 0, totalDeleted: 0 } cannot resolve the Electron binary from a normal npm install. --- .changeset/fix-desktop-electron-dep.md | 7 +++++++ packages/cli/package.json | 1 + pnpm-workspace.yaml | 1 - 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-desktop-electron-dep.md diff --git a/.changeset/fix-desktop-electron-dep.md b/.changeset/fix-desktop-electron-dep.md new file mode 100644 index 0000000000..3aa63fe5e8 --- /dev/null +++ b/.changeset/fix-desktop-electron-dep.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Add `electron` as a runtime dependency so `fusion desktop` works from a published npm install. +category: fix +dev: `packages/cli/package.json` now depends on `electron`. Previously the desktop launcher called `require("electron")`, which is only available inside the source checkout (via `pnpm-workspace.yaml` `onlyBuiltDependencies`) and is missing for npm consumers, causing `fusion desktop` to hang or fail silently. diff --git a/packages/cli/package.json b/packages/cli/package.json index 3b61055f03..d14f7d292a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -63,6 +63,7 @@ "@earendil-works/pi-coding-agent": "^0.80.3", "dockerode": "^4.0.12", "express": "^5.1.0", + "electron": "^33.4.11", "i18next": "^26.3.1", "ink": "^7.0.5", "ink-spinner": "^5.0.0", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6644bf1570..21279d44e9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,7 +8,6 @@ ignoredBuiltDependencies: - ssh2 onlyBuiltDependencies: - '@homebridge/node-pty-prebuilt-multiarch' - - electron - esbuild - koffi - protobufjs From fd94f7a1915f2c7675a9f399ae6ea45deefd138c Mon Sep 17 00:00:00 2001 From: ddonaldson130 Date: Fri, 3 Jul 2026 03:15:17 -0400 Subject: [PATCH 2/5] fix(dashboard): prevent Windows Terminal popup and repair Manage Projects navigation Two user-facing bugs on Windows: 1. Windows Terminal version dialogs on dashboard load/settings - The embedded terminal auto-create flow was spawning a PTY on Windows, which could invoke wt.exe and trigger its native "Help" version message boxes (Windows Terminal 1.24.11321.0). - Fix: skip auto-creation of the first terminal tab on Windows. Users can still create a terminal explicitly; the failure no longer recurs automatically on every dashboard session. 2. Manage Projects opens Settings instead of project overview - handleViewAllProjects only reset viewMode; if the previous taskView was "settings", MainContent rendered the Settings page before the overview branch because the settings check precedes the overview branch. - Fix: also reset taskView to "command-center" when leaving a project so the overview surface (ProjectOverview) renders. - Thread setTaskView through useProjectActions so the action has access to the view router. Tested on Windows 11 with Fusion 0.52.0 dashboard. --- packages/dashboard/app/App.tsx | 3 ++- packages/dashboard/app/hooks/useProjectActions.ts | 7 +++++-- packages/dashboard/app/hooks/useTerminalSessions.ts | 7 +++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index b640d9dd21..5d2a5855a0 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -336,7 +336,7 @@ function AppInner() { const { pushNav, replaceCurrent, removeNav } = useNavigationHistory({ enabled: true }); // View state must be defined before useTasks since useTasks depends on taskView for SSE gating - const { viewMode, setViewMode, taskView, handleChangeTaskView } = useViewState({ + const { viewMode, setViewMode, taskView, setTaskView, handleChangeTaskView } = useViewState({ projectsLoading, projectsError, currentProjectLoading, @@ -717,6 +717,7 @@ function AppInner() { setCurrentProject, clearCurrentProject, setViewMode, + setTaskView, currentProject, refreshProjects, toggleFavoriteProvider, diff --git a/packages/dashboard/app/hooks/useProjectActions.ts b/packages/dashboard/app/hooks/useProjectActions.ts index a066aa8e24..9f3a3342d3 100644 --- a/packages/dashboard/app/hooks/useProjectActions.ts +++ b/packages/dashboard/app/hooks/useProjectActions.ts @@ -3,13 +3,14 @@ import { useTranslation } from "react-i18next"; import { pauseProject, resumeProject, unregisterProject } from "../api"; import type { ProjectInfo } from "../api"; import { replaceProjectIdInUrl } from "../utils/projectUrlState"; -import type { ViewMode } from "./useViewState"; +import type { ViewMode, TaskView } from "./useViewState"; import type { ToastType } from "./useToast"; interface UseProjectActionsOptions { setCurrentProject: (project: ProjectInfo) => void; clearCurrentProject: () => void; setViewMode: (mode: ViewMode) => void; + setTaskView: (view: TaskView) => void; currentProject: ProjectInfo | null; refreshProjects: () => Promise; toggleFavoriteProvider: (provider: string) => Promise; @@ -41,6 +42,7 @@ export function useProjectActions(options: UseProjectActionsOptions): UseProject setCurrentProject, clearCurrentProject, setViewMode, + setTaskView, currentProject, refreshProjects, toggleFavoriteProvider, @@ -62,7 +64,8 @@ export function useProjectActions(options: UseProjectActionsOptions): UseProject replaceProjectIdInUrl(null); clearCurrentProject(); setViewMode("overview"); - }, [clearCurrentProject, setViewMode]); + setTaskView("command-center"); + }, [clearCurrentProject, setViewMode, setTaskView]); const handleOpenSettings = useCallback(() => { openSettings(); diff --git a/packages/dashboard/app/hooks/useTerminalSessions.ts b/packages/dashboard/app/hooks/useTerminalSessions.ts index f5565cef1c..db874a83df 100644 --- a/packages/dashboard/app/hooks/useTerminalSessions.ts +++ b/packages/dashboard/app/hooks/useTerminalSessions.ts @@ -241,7 +241,14 @@ export function useTerminalSessions(projectId?: string): UseTerminalSessionsRetu }, [projectId]); // Re-run when project scope changes // Auto-create first tab if no tabs exist after validation + // On Windows, do NOT auto-create because the embedded shell may invoke Windows Terminal + // (wt.exe) and produce native "Help" version dialogs. Users can still create a terminal + // explicitly from the UI. useEffect(() => { + if (typeof window !== "undefined" && window.navigator.userAgent.includes("Windows")) { + setIsReady(true); + return; + } if (tabs.length === 0 && isReady && serverAvailable && !bootstrapError) { // Capture current generation so only this attempt's result is accepted const gen = generationRef.current; From 4bad0920e22c96e2b79916e8bbe8f507f22bd960 Mon Sep 17 00:00:00 2001 From: ddonaldson130 Date: Fri, 3 Jul 2026 03:36:10 -0400 Subject: [PATCH 3/5] docs(desktop): field report for Windows desktop release issues Add reports/desktop-release-issues-2026-07-03.md documenting the regressions observed in the Fusion 0.52.0 Windows desktop release, including: - fusion desktop fails to launch because electron is a devDependency - native launcher walks ancestor dirs and fails on unrelated workspace JSON - Manage Projects opens Settings instead of overview - Windows Terminal Help version dialogs on dashboard load - packaged preload.cjs missing in unpacked release layout - port collisions and dashboard/gateway drift on Windows - GPU/sandbox rendering instability on Windows Electron - isolated user-data path needed - CLI desktop command not reusing an already-running dashboard server Also include the two experimental mitigations we applied locally: - disable GPU/sandbox Electron flags in desktop.ts - skip embedded local runtime when FUSION_SERVER_PORT is already set These changes are intended as supporting evidence and starting points for the Fusion team, not as a final fix. --- packages/cli/src/commands/desktop.ts | 12 +- packages/desktop/src/main.ts | 20 ++- reports/desktop-release-issues-2026-07-03.md | 178 +++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 reports/desktop-release-issues-2026-07-03.md diff --git a/packages/cli/src/commands/desktop.ts b/packages/cli/src/commands/desktop.ts index 25738d24cd..38f8aeacc3 100644 --- a/packages/cli/src/commands/desktop.ts +++ b/packages/cli/src/commands/desktop.ts @@ -5,6 +5,7 @@ import { dirname, isAbsolute, join, resolve } from "node:path"; import type { AddressInfo } from "node:net"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; +import * as os from "node:os"; import { CentralCore, TaskStore } from "@fusion/core"; import { createServer } from "@fusion/dashboard"; import { ProjectEngineManager } from "@fusion/engine"; @@ -177,7 +178,16 @@ export async function runDesktop(options: RunDesktopOptions = {}): Promise const runtime = await startDashboardRuntime(rootDir, Boolean(options.paused), Boolean(options.noAuth)); const electronBinary = resolveElectronBinary(); - const electronArgs = ["--enable-source-maps", desktopEntry, ...(options.dev ? ["--dev"] : [])]; + const electronArgs = [ + "--enable-source-maps", + desktopEntry, + "--disable-gpu", + "--disable-gpu-compositing", + "--disable-gpu-sandbox", + "--disable-software-rasterizer", + "--no-sandbox", + ...(options.dev ? ["--dev"] : []), + ]; // Build environment for Electron process const electronEnv: NodeJS.ProcessEnv = { diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 84b8cb58cf..17b30df6b0 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -1,6 +1,18 @@ import { app, BrowserWindow, nativeImage, screen, Tray } from "electron"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import os from "node:os"; + +const fusionUserDataDir = join(os.homedir(), ".fusion", "desktop-user-data"); +try { + app.commandLine.appendSwitch("user-data-dir", fusionUserDataDir); + app.setPath("userData", fusionUserDataDir); + app.setPath("cache", join(fusionUserDataDir, "cache")); + app.setPath("crashDumps", join(fusionUserDataDir, "crashes")); +} catch { + // Path already locked after app is ready; not a fatal error. +} + import { setupDeepLinkHandler, registerDeepLinkProtocol } from "./deep-link.js"; import { registerIpcHandlers } from "./ipc.js"; import { buildAppMenu } from "./menu.js"; @@ -199,7 +211,7 @@ export async function initializeApp(): Promise { } } - if (rememberedLaunchMode === "local") { + if (rememberedLaunchMode === "local" && !process.env.FUSION_SERVER_PORT) { try { await startLocalRuntimeOnce(); } catch (error) { @@ -216,6 +228,12 @@ export async function initializeApp(): Promise { currentDesktopLaunchMode = "local"; } + if (currentDesktopLaunchMode === "choose" && process.env.FUSION_SERVER_PORT) { + // The CLI already started a dashboard server; use it without spawning an + // embedded local runtime. The shell state will report external-cli running. + currentDesktopLaunchMode = "local"; + } + const windowState = state ? clampWindowStateToVisibleDisplay( state, diff --git a/reports/desktop-release-issues-2026-07-03.md b/reports/desktop-release-issues-2026-07-03.md new file mode 100644 index 0000000000..3c71d701cc --- /dev/null +++ b/reports/desktop-release-issues-2026-07-03.md @@ -0,0 +1,178 @@ +# Fusion Desktop Release Issues — Field Report + +**Date:** 2026-07-03 +**Reporter:** Hermes Ouroboros / Automata Intelligentsia +**Host:** Windows 11 (build 26200) +**Fusion versions observed:** 0.51.0 (last known good), 0.52.0 (current desktop release with regressions) +**Test environment:** `C:\Users\drewd\Tools\fusion-latest` (local source checkout), `npm install -g @runfusion/fusion` published package, and the v1.1.0 standalone desktop wrapper. + +This document collects the issues we found while trying to run the official Fusion desktop build on Windows, what we attempted, and what we believe the Fusion team needs to address. + +--- + +## Issue 1: `fusion desktop` fails to launch on Windows 0.52.0 + +### Symptom +Running `fusion desktop` from the published npm package immediately errors out with a dynamic require / Electron binary resolution failure. The CLI cannot find `electron` because the published `@runfusion/fusion` package lists `electron` only as a `devDependency`, not a runtime dependency. + +### What we tried +1. Installed `@runfusion/fusion@latest` globally. +2. Ran `fusion desktop --no-auth` from a project directory. +3. Observed that `require('electron')` fails because Electron is not installed alongside the published CLI. +4. Checked `packages/cli/package.json` in the source repo: `electron` is under `devDependencies`. + +### Proposed fix +Add `electron` to the `dependencies` (or `optionalDependencies`) of `@runfusion/fusion` so that a global npm install pulls in the Electron binary required by `packages/cli/src/commands/desktop.ts`. + +--- + +## Issue 2: Native desktop build walks ancestor directories and fails on unrelated workspace JSON + +### Symptom +When `fusion desktop` does manage to start the native Electron build, the launcher walks up the directory tree looking for a Fusion workspace. It can land on an unrelated ancestor directory (e.g., `C:\Users\drewd\Tools`) and fail because it parses JSON files in sibling or parent workspaces that are not valid Fusion project assets. + +### What we tried +1. Ran `fusion desktop` from `C:\Users\drewd\Tools\fusion-latest`. +2. The launcher searched ancestor directories instead of using the current working directory as the project root. +3. It then choked on invalid JSON in assets that were never intended to be loaded as Fusion metadata. + +### Proposed fix +- Restrict workspace discovery to the current working directory or a user-selected/project-configured root. +- Treat missing/invalid JSON as non-fatal during workspace discovery; log and continue rather than crashing the launcher. +- Add a CLI flag or config key to pin the project root explicitly. + +--- + +## Issue 3: Manage Projects button opens Settings instead of the project overview + +### Symptom +In the dashboard header, clicking **Manage Projects** lands on the **Settings** page instead of the project list/overview. + +### Root cause +`handleViewAllProjects` in `packages/dashboard/app/hooks/useProjectActions.ts` resets `viewMode` to `"overview"` and clears the current project, but it leaves `taskView` unchanged. `MainContent` checks `taskView === "settings"` before the `viewMode === "overview"` branch, so any previously selected settings view is rendered instead of `ProjectOverview`. + +### What we tried +- Traced the routing through `App.tsx`, `useProjectActions.ts`, `useViewState.ts`, and `MainContent.tsx`. +- Threaded `setTaskView` into `useProjectActions` and reset `taskView` to `"command-center"` when leaving a project. + +### Proposed fix +Apply the same fix as PR #1882: make `handleViewAllProjects` reset `taskView` to the overview landing view so the settings branch cannot shadow the overview branch. + +--- + +## Issue 4: Windows Terminal native "Help" version dialogs on dashboard load / Settings + +### Symptom +On Windows, opening the dashboard (and especially the Settings page) produces two native Windows message boxes titled **Help**, showing: + +``` +Windows Terminal +1.24.11321.0 +``` + +### Root cause +The dashboard’s `useTerminalSessions` hook auto-creates the first terminal tab once session validation completes. On Windows, spawning a PTY can end up invoking `wt.exe` (Windows Terminal) or otherwise triggering its built-in version/help dialog. The backend `terminal-service.ts` already has an FNXC guard (FNXC:WindowsTerminalStartup) to avoid probing `wt.exe`, but the frontend auto-create path still triggers a PTY spawn on Windows before the user has asked for a terminal. + +### What we tried +- Confirmed `terminal-service.ts` skips Windows Terminal for `SHELL` on `win32`. +- Confirmed tests already assert `wt.exe` should not be selected. +- Disabled the auto-create path on Windows in `useTerminalSessions.ts` so the failure cannot recur automatically. Manual terminal creation still works and surfaces the inline error UI. + +### Proposed fix +Merge the frontend guard from PR #1882, or move the platform check server-side so no terminal session is auto-created for Windows users unless the platform has a verified embedded shell. + +--- + +## Issue 5: `fusion desktop` native build uses the wrong working directory for `preload.cjs` and other Electron assets + +### Symptom +The packaged native desktop build can fail to load `preload.cjs` because the packaged app looks under `release/win-unpacked/resources/app.asar.unpacked/electron/`, but that path may be incomplete after `npm run dist`. + +### What we tried +- Extracted the published v1.0.0 wrapper source and compared it to the source tree. +- Found that `apps/desktop/electron/preload.cjs` exists in source but is missing in the packaged layout on some installs. +- Confirmed the workaround: copy `preload.cjs` into the missing unpacked location. + +### Proposed fix +- Add a packaging verification step that asserts `preload.cjs` is present in the expected unpacked path before publishing. +- Consider bundling the preload script into the main asar so the path is deterministic. +- Document the Windows packaging layout and the required Electron files. + +--- + +## Issue 6: Native desktop build runs on port 9119/8643 but conflicts with dashboard and other instances + +### Symptom +On Windows, the dashboard backend can end up on a non-standard port or collide with another running dashboard instance (e.g., 9120, 7380–7385). The packaged desktop also expects gateway 8643 and dashboard 9119 per the docs, but the actual port can drift. + +### What we tried +- Used `Get-NetTCPConnection` to map ports to process names because `ps`/`netstat` in MSYS mis-enumerate Electron/pythonw/WSL processes. +- Found that duplicate dashboard processes can occur when system Python and venv Python both try to start on the same port. + +### Proposed fix +- Lock down the desktop build to deterministic ports with a port-file lock or named mutex on Windows. +- Show a clear error when another Fusion desktop/dashboard is already running instead of silently binding elsewhere. +- Document the canonical Windows ports and how to audit them. + +--- + +## Issue 7: GPU/sandbox rendering issues on Windows Electron + +### Symptom +The native desktop window can be blank, flicker, or fail to render on some Windows GPUs. We observed this both with the wrapper and the native desktop build. + +### What we tried +- Added Electron flags to disable GPU and sandbox in the wrapper and native launch path: + - `--disable-gpu` + - `--disable-gpu-compositing` + - `--disable-gpu-sandbox` + - `--disable-software-rasterizer` + - `--no-sandbox` +- These flags improved stability in the wrapper. + +### Proposed fix +- Expose these flags as the default on Windows, or make them configurable in the dashboard settings. +- Detect GPU process crashes and automatically fall back to software rendering with a toast notification. + +--- + +## Issue 8: Desktop user-data path is not isolated / collides with other Electron apps + +### Symptom +Crash dumps, caches, and local storage from the Fusion desktop can end up in a generic Electron user-data directory or collide with other Electron apps using the same defaults. + +### What we tried +- Set `app.setPath("userData", "...")` to `~/.fusion/desktop-user-data` and sub-paths for cache/crashes in the wrapper. + +### Proposed fix +- Apply the same isolation in the native desktop build so sessions, logs, and crash data live under `~/.fusion/` and are easy to inspect or reset without affecting other Electron apps. + +--- + +## Issue 9: CLI desktop command does not reuse an already-running dashboard server + +### Symptom +Running `fusion desktop` while `fusion dashboard` (or the wrapper) is already serving on 4040 starts a second process rather than connecting to the existing one. + +### What we tried +- Modified `packages/desktop/src/main.ts` to detect `FUSION_SERVER_PORT` and skip `startLocalRuntimeOnce` when the CLI already started a server. + +### Proposed fix +- Add a stable port probe / heartbeat before starting the Electron runtime. +- If a dashboard is already running on the expected port, load that URL instead of spawning another engine. + +--- + +## General recommendations for the Fusion team + +1. **Windows CI:** Add a Windows build step that runs `fusion desktop` in a clean VM and asserts the window title is visible and responsive. +2. **Release tests:** Before tagging a desktop release, verify the packaged `release/win-unpacked` layout has all required Electron assets (`preload.cjs`, etc.). +3. **Dependency audit:** Move `electron` out of `devDependencies` in the published CLI package, or document that users must install it separately. +4. **Field test with wrapper users:** The wrapper at `https://github.com/Automata-intelligentsia/fusion-desktop-windows/releases/tag/v1.1.0` is a proven workaround; consider adopting its launch model (CLI server + Electron shell) as an official fallback until the native desktop build is stabilized on Windows. + +--- + +## Related PRs + +- `Runfusion/Fusion#1882` — dashboard routing and Windows Terminal popup fix. +- `Automata-intelligentsia/fusion-desktop-windows#v1.1.0` — standalone Windows wrapper that works around the native desktop regressions. From d97e66a6573bbfd73d4b4a6fc374ca88430452b2 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 3 Jul 2026 08:13:06 -0700 Subject: [PATCH 4/5] fix(desktop): sync lockfile, gate Windows GPU flags, document desktop hardening - Regenerate pnpm-lock.yaml for the new electron@^33.4.11 CLI dependency so --frozen-lockfile stops failing every PR-check job. - Gate the GPU/sandbox-disabling Electron flags (--no-sandbox, --disable-gpu, etc.) to Windows only via os.platform(); applying them on macOS/Linux was a security and rendering regression. This also resolves the unused os import that failed lint. - Add FNXC comments for the Windows GPU flags, desktop user-data isolation, and CLI dashboard-server reuse per project comment convention. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/fix-desktop-electron-dep.md | 4 +- packages/cli/src/commands/desktop.ts | 21 +++- packages/desktop/src/main.ts | 8 ++ pnpm-lock.yaml | 142 +++++++++++++------------ 4 files changed, 102 insertions(+), 73 deletions(-) diff --git a/.changeset/fix-desktop-electron-dep.md b/.changeset/fix-desktop-electron-dep.md index 3aa63fe5e8..d6b3ccfe23 100644 --- a/.changeset/fix-desktop-electron-dep.md +++ b/.changeset/fix-desktop-electron-dep.md @@ -2,6 +2,6 @@ "@runfusion/fusion": patch --- -summary: Add `electron` as a runtime dependency so `fusion desktop` works from a published npm install. +summary: Fix `fusion desktop` on Windows and published npm installs (Electron dependency, GPU/sandbox flags, dashboard reuse). category: fix -dev: `packages/cli/package.json` now depends on `electron`. Previously the desktop launcher called `require("electron")`, which is only available inside the source checkout (via `pnpm-workspace.yaml` `onlyBuiltDependencies`) and is missing for npm consumers, causing `fusion desktop` to hang or fail silently. +dev: `packages/cli/package.json` now depends on `electron` at runtime; previously the desktop launcher called `require("electron")`, which is only available inside the source checkout (via `pnpm-workspace.yaml` `onlyBuiltDependencies`) and is missing for npm consumers, causing `fusion desktop` to hang or fail silently. The launcher now applies GPU/sandbox-disabling Electron flags only on Windows (`os.platform() === "win32"`), keeps hardware acceleration and the Chromium sandbox on macOS/Linux, exports `FUSION_SERVER_PORT` so the desktop reuses the CLI-started dashboard instead of double-binding ports, and isolates desktop user-data under `~/.fusion/desktop-user-data`. diff --git a/packages/cli/src/commands/desktop.ts b/packages/cli/src/commands/desktop.ts index 38f8aeacc3..d3a75f9ee9 100644 --- a/packages/cli/src/commands/desktop.ts +++ b/packages/cli/src/commands/desktop.ts @@ -178,14 +178,25 @@ export async function runDesktop(options: RunDesktopOptions = {}): Promise const runtime = await startDashboardRuntime(rootDir, Boolean(options.paused), Boolean(options.noAuth)); const electronBinary = resolveElectronBinary(); + + /* + FNXC:DesktopWindowsGpuFlags 2026-07-03-14:40: + Windows Electron renderers observed blank/flickering GPU output and sandbox-related launch instability on some GPUs during the 0.52.0 desktop release (field report Issue 7). Disable GPU acceleration and the Chromium sandbox ONLY on Windows so macOS/Linux keep hardware acceleration and the security sandbox. Applying `--no-sandbox`/`--disable-gpu` on every platform would be a needless security and rendering regression off Windows. + */ + const isWindows = os.platform() === "win32"; + const windowsGpuFlags = isWindows + ? [ + "--disable-gpu", + "--disable-gpu-compositing", + "--disable-gpu-sandbox", + "--disable-software-rasterizer", + "--no-sandbox", + ] + : []; const electronArgs = [ "--enable-source-maps", desktopEntry, - "--disable-gpu", - "--disable-gpu-compositing", - "--disable-gpu-sandbox", - "--disable-software-rasterizer", - "--no-sandbox", + ...windowsGpuFlags, ...(options.dev ? ["--dev"] : []), ]; diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index bab9d3041f..56a7b3eb97 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -3,6 +3,10 @@ import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import os from "node:os"; +/* +FNXC:DesktopUserDataIsolation 2026-07-03-14:40: +Isolate Electron desktop user-data/cache/crash artifacts under ~/.fusion/desktop-user-data so the packaged desktop does not share (or collide with) the default per-app Chromium profile across installs and Fusion versions (field report Issue 8). setPath must run before app "ready"; once locked it throws, which is non-fatal here. +*/ const fusionUserDataDir = join(os.homedir(), ".fusion", "desktop-user-data"); try { app.commandLine.appendSwitch("user-data-dir", fusionUserDataDir); @@ -254,6 +258,10 @@ export async function initializeApp(): Promise { currentDesktopLaunchMode = "local"; } + /* + FNXC:DesktopReuseCliServer 2026-07-03-14:40: + When `fusion desktop` launches Electron it exports FUSION_SERVER_PORT for the dashboard the CLI already started. In that case the desktop must NOT spin up its own embedded local runtime (which would double-bind ports and conflict on Windows, field report Issue 9): skip startLocalRuntimeOnce() when the port is set, and treat a "choose" mode as already-local so the shell attaches to the external CLI server. + */ if (currentDesktopLaunchMode === "choose" && process.env.FUSION_SERVER_PORT) { // The CLI already started a dashboard server; use it without spawning an // embedded local runtime. The shell state will report external-cli running. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85237ff277..5e1124a728 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,6 +55,9 @@ importers: dockerode: specifier: ^4.0.12 version: 4.0.12 + electron: + specifier: ^33.4.11 + version: 33.4.11 express: specifier: ^5.1.0 version: 5.2.1 @@ -1228,8 +1231,8 @@ packages: resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.974.15': - resolution: {integrity: sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==} + '@aws-sdk/core@3.974.26': + resolution: {integrity: sha512-wRj7Pthvjk3anees97pUWlxlTa0DUjeGrEQU5fKDZVdWZV0ekaprbof0df2uaE9g8u67t035v2j+ne2AW2UMkA==} engines: {node: '>=20.0.0'} '@aws-sdk/credential-provider-env@3.972.41': @@ -1292,6 +1295,10 @@ packages: resolution: {integrity: sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==} engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.15': + resolution: {integrity: sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.973.9': resolution: {integrity: sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==} engines: {node: '>=20.0.0'} @@ -1300,8 +1307,8 @@ packages: resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/xml-builder@3.972.26': - resolution: {integrity: sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==} + '@aws-sdk/xml-builder@3.972.33': + resolution: {integrity: sha512-ezbwz9WpuLctm6o7P2t2naDhVVPI5jFGrVefVybhcKGjU57VIyT46pQVO0RI2RYkUdhdj2Z9uSIlAzGZE9NW9A==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.2.4': @@ -2603,9 +2610,6 @@ packages: '@cfworker/json-schema': optional: true - '@nodable/entities@2.1.0': - resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2844,6 +2848,10 @@ packages: resolution: {integrity: sha512-Kt8phUg45M15EjhYAbZ+fFikYneijLu9Liugz8ZsYz2i8j0hzGv27LWKpEHYRfvj+LyCOSijpcR/2i8RouV+cA==} engines: {node: '>=18.0.0'} + '@smithy/core@3.29.1': + resolution: {integrity: sha512-qoiY4nrk5OCu1+eIR1VB8l5DmON/oKiqrd5zZFAhXJXjJlLWQusKEW/SkBDAtGDcPaz86m9kfcE1lngU0GlM6A==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.3.5': resolution: {integrity: sha512-yiF8xHpdkaTfzLVqFzsP6WvNghEK+qZzLYWFD13L2SsFhbXwBGlxdocKF95qjr7s5lE5NRage+EJFK4mAsx88Q==} engines: {node: '>=18.0.0'} @@ -2868,10 +2876,18 @@ packages: resolution: {integrity: sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA==} engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.6.2': + resolution: {integrity: sha512-QgHflghMoPxCJ9axiCVh8KZfbC9fuP6vkXXyK//E3cq7nLaSSyyLj0GAoqVWezYeDQmXIZhmlRvLE16jsqDK6g==} + engines: {node: '>=18.0.0'} + '@smithy/types@4.14.2': resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} engines: {node: '>=18.0.0'} + '@smithy/types@4.15.1': + resolution: {integrity: sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q==} + engines: {node: '>=18.0.0'} + '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} @@ -4535,6 +4551,11 @@ packages: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} + electron@33.4.11: + resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==} + engines: {node: '>= 12.20.55'} + hasBin: true + electron@35.7.5: resolution: {integrity: sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==} engines: {node: '>= 12.20.55'} @@ -4793,13 +4814,6 @@ packages: fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} - fast-xml-builder@1.2.0: - resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} - - fast-xml-parser@5.7.3: - resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} - hasBin: true - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -6226,10 +6240,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.5.0: - resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} - engines: {node: '>=14.0.0'} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -7004,9 +7014,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strnum@2.2.3: - resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} - strtok3@6.3.0: resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} engines: {node: '>=10'} @@ -7570,10 +7577,6 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} - xml-naming@0.1.0: - resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} - engines: {node: '>=16.0.0'} - xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -7771,7 +7774,7 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/credential-provider-node': 3.972.46 '@aws-sdk/eventstream-handler-node': 3.972.18 '@aws-sdk/middleware-eventstream': 3.972.14 @@ -7784,20 +7787,20 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/core@3.974.15': + '@aws-sdk/core@3.974.26': dependencies: - '@aws-sdk/types': 3.973.9 - '@aws-sdk/xml-builder': 3.972.26 + '@aws-sdk/types': 3.973.15 + '@aws-sdk/xml-builder': 3.972.33 '@aws/lambda-invoke-store': 0.2.4 - '@smithy/core': 3.24.5 - '@smithy/signature-v4': 5.4.5 - '@smithy/types': 4.14.2 + '@smithy/core': 3.29.1 + '@smithy/signature-v4': 5.6.2 + '@smithy/types': 4.15.1 bowser: 2.14.1 tslib: 2.8.1 '@aws-sdk/credential-provider-env@3.972.41': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.5 '@smithy/types': 4.14.2 @@ -7805,7 +7808,7 @@ snapshots: '@aws-sdk/credential-provider-http@3.972.43': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.5 '@smithy/fetch-http-handler': 5.4.5 @@ -7815,7 +7818,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.972.45': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/credential-provider-env': 3.972.41 '@aws-sdk/credential-provider-http': 3.972.43 '@aws-sdk/credential-provider-login': 3.972.45 @@ -7831,7 +7834,7 @@ snapshots: '@aws-sdk/credential-provider-login@3.972.45': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/nested-clients': 3.997.13 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.5 @@ -7854,7 +7857,7 @@ snapshots: '@aws-sdk/credential-provider-process@3.972.41': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.5 '@smithy/types': 4.14.2 @@ -7862,7 +7865,7 @@ snapshots: '@aws-sdk/credential-provider-sso@3.972.45': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/nested-clients': 3.997.13 '@aws-sdk/token-providers': 3.1056.0 '@aws-sdk/types': 3.973.9 @@ -7872,7 +7875,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.972.45': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/nested-clients': 3.997.13 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.5 @@ -7895,7 +7898,7 @@ snapshots: '@aws-sdk/middleware-websocket@3.972.23': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.5 '@smithy/fetch-http-handler': 5.4.5 @@ -7907,7 +7910,7 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/signature-v4-multi-region': 3.996.30 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.5 @@ -7925,7 +7928,7 @@ snapshots: '@aws-sdk/token-providers@3.1048.0': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/nested-clients': 3.997.13 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.5 @@ -7934,13 +7937,18 @@ snapshots: '@aws-sdk/token-providers@3.1056.0': dependencies: - '@aws-sdk/core': 3.974.15 + '@aws-sdk/core': 3.974.26 '@aws-sdk/nested-clients': 3.997.13 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.5 '@smithy/types': 4.14.2 tslib: 2.8.1 + '@aws-sdk/types@3.973.15': + dependencies: + '@smithy/types': 4.15.1 + tslib: 2.8.1 + '@aws-sdk/types@3.973.9': dependencies: '@smithy/types': 4.14.2 @@ -7950,10 +7958,9 @@ snapshots: dependencies: tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.26': + '@aws-sdk/xml-builder@3.972.33': dependencies: - '@smithy/types': 4.14.2 - fast-xml-parser: 5.7.3 + '@smithy/types': 4.15.1 tslib: 2.8.1 '@aws/lambda-invoke-store@0.2.4': {} @@ -9802,8 +9809,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@nodable/entities@2.1.0': {} - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9967,6 +9972,11 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@smithy/core@3.29.1': + dependencies: + '@smithy/types': 4.15.1 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.3.5': dependencies: '@smithy/core': 3.24.5 @@ -10001,10 +10011,20 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@smithy/signature-v4@5.6.2': + dependencies: + '@smithy/core': 3.29.1 + '@smithy/types': 4.15.1 + tslib: 2.8.1 + '@smithy/types@4.14.2': dependencies: tslib: 2.8.1 + '@smithy/types@4.15.1': + dependencies: + tslib: 2.8.1 + '@smithy/util-buffer-from@2.2.0': dependencies: '@smithy/is-array-buffer': 2.2.0 @@ -11858,6 +11878,14 @@ snapshots: transitivePeerDependencies: - supports-color + electron@33.4.11: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 25.5.2 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + electron@35.7.5: dependencies: '@electron/get': 2.0.3 @@ -12197,18 +12225,6 @@ snapshots: dependencies: fast-string-width: 3.0.2 - fast-xml-builder@1.2.0: - dependencies: - path-expression-matcher: 1.5.0 - xml-naming: 0.1.0 - - fast-xml-parser@5.7.3: - dependencies: - '@nodable/entities': 2.1.0 - fast-xml-builder: 1.2.0 - path-expression-matcher: 1.5.0 - strnum: 2.2.3 - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -13981,8 +13997,6 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.5.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -14859,8 +14873,6 @@ snapshots: strip-json-comments@3.1.1: {} - strnum@2.2.3: {} - strtok3@6.3.0: dependencies: '@tokenizer/token': 0.3.0 @@ -15446,8 +15458,6 @@ snapshots: xml-name-validator@5.0.0: {} - xml-naming@0.1.0: {} - xml2js@0.6.2: dependencies: sax: 1.6.0 From 08e47d4f0da8deb3b0f88a26bc0816a290a4d0a9 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 3 Jul 2026 08:49:43 -0700 Subject: [PATCH 5/5] fix(desktop): migrate userData on relocation, verify packaged assets, harden Windows CI - Add a one-time copy of the previous default Electron profile into ~/.fusion/desktop-user-data (user-data-migration.ts) so upgrading operators keep window geometry/session instead of starting fresh (report Issue 8). - Verify main.js/preload.js/client assets in scripts/build.ts (pre-package staging) and in the packaged app.asar via desktop-windows.yml, so an incomplete package fails the build/CI instead of dead-ending at runtime with a silent missing preload (report Issue 5, recommendation #2). - Fix two pre-existing stale workflow-assertion tests that expected a bare 'electron-builder --win' after the scripts moved to '--projectDir deploy'. - Update the field report with a per-issue resolution matrix. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/fix-desktop-electron-dep.md | 2 +- .github/workflows/desktop-windows.yml | 28 +++++++ packages/desktop/scripts/build.ts | 40 ++++++++- .../__tests__/electron-builder-config.test.ts | 9 +- packages/desktop/src/__tests__/main.test.ts | 5 +- .../src/__tests__/release-workflow.test.ts | 6 +- .../src/__tests__/user-data-migration.test.ts | 84 +++++++++++++++++++ packages/desktop/src/main.ts | 10 +++ packages/desktop/src/user-data-migration.ts | 32 +++++++ reports/desktop-release-issues-2026-07-03.md | 20 +++++ 10 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 packages/desktop/src/__tests__/user-data-migration.test.ts create mode 100644 packages/desktop/src/user-data-migration.ts diff --git a/.changeset/fix-desktop-electron-dep.md b/.changeset/fix-desktop-electron-dep.md index d6b3ccfe23..2274662bd1 100644 --- a/.changeset/fix-desktop-electron-dep.md +++ b/.changeset/fix-desktop-electron-dep.md @@ -4,4 +4,4 @@ summary: Fix `fusion desktop` on Windows and published npm installs (Electron dependency, GPU/sandbox flags, dashboard reuse). category: fix -dev: `packages/cli/package.json` now depends on `electron` at runtime; previously the desktop launcher called `require("electron")`, which is only available inside the source checkout (via `pnpm-workspace.yaml` `onlyBuiltDependencies`) and is missing for npm consumers, causing `fusion desktop` to hang or fail silently. The launcher now applies GPU/sandbox-disabling Electron flags only on Windows (`os.platform() === "win32"`), keeps hardware acceleration and the Chromium sandbox on macOS/Linux, exports `FUSION_SERVER_PORT` so the desktop reuses the CLI-started dashboard instead of double-binding ports, and isolates desktop user-data under `~/.fusion/desktop-user-data`. +dev: `packages/cli/package.json` now depends on `electron` at runtime; previously the desktop launcher called `require("electron")`, which is only available inside the source checkout (via `pnpm-workspace.yaml` `onlyBuiltDependencies`) and is missing for npm consumers, causing `fusion desktop` to hang or fail silently. The launcher now applies GPU/sandbox-disabling Electron flags only on Windows (`os.platform() === "win32"`), keeps hardware acceleration and the Chromium sandbox on macOS/Linux, exports `FUSION_SERVER_PORT` so the desktop reuses the CLI-started dashboard instead of double-binding ports, and isolates desktop user-data under `~/.fusion/desktop-user-data`. Relocating the profile performs a one-time copy of the previous default Electron profile (`user-data-migration.ts`) so upgrading operators keep window geometry/session. `packages/desktop/scripts/build.ts` now fails the build if `main.js`/`preload.js`/`client/index.html` are missing from `dist/` or the staged `deploy/dist/`, preventing shipping an incomplete `app.asar`. diff --git a/.github/workflows/desktop-windows.yml b/.github/workflows/desktop-windows.yml index 251dd6f9d7..bb4a44b644 100644 --- a/.github/workflows/desktop-windows.yml +++ b/.github/workflows/desktop-windows.yml @@ -89,6 +89,34 @@ jobs: if ($nsis.Count -eq 0) { Write-Error "No NSIS installer artifact produced"; exit 1 } if ($portable.Count -eq 0) { Write-Error "No portable EXE artifact produced"; exit 1 } + - name: Verify packaged app.asar assets + shell: pwsh + run: | + # FNXC:WindowsDesktopPackaging 2026-07-03-15:40: + # Field report Issue 5: the packaged desktop shipped without preload.js and + # dead-ended on "can't reach the Fusion backend" (preload absence is silent — + # the contextBridge never installs window.fusionShell/fusionAPI). scripts/build.ts + # verifies the pre-package staging tree; this asserts the SHIPPED app.asar itself + # contains the Electron main/preload/renderer entrypoints, since only the packed + # asar reflects what a user installs. + $required = @('dist/main.js', 'dist/preload.js', 'dist/client/index.html') + $unpackedRoots = Get-ChildItem packages/desktop/dist-electron -Directory -Filter 'win*-unpacked' + if ($unpackedRoots.Count -eq 0) { Write-Error "No win-unpacked directory produced"; exit 1 } + foreach ($root in $unpackedRoots) { + $asar = Join-Path $root.FullName 'resources/app.asar' + if (!(Test-Path $asar)) { Write-Error "Missing packaged app.asar: $asar"; exit 1 } + $entries = npx --yes @electron/asar list $asar + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to list app.asar: $asar"; exit 1 } + $normalized = $entries | ForEach-Object { $_.TrimStart('/','\').Replace('\','/') } + foreach ($asset in $required) { + if ($normalized -notcontains $asset) { + Write-Error "app.asar is missing required Electron asset '$asset' in $($root.Name); refusing to ship an incomplete package" + exit 1 + } + } + Write-Host "$($root.Name)/resources/app.asar contains all required Electron assets" + } + # Automated publish is intentionally deferred to FN-5593. # Keep a single artifact; filenames include -x64 / -arm64 so both arches are captured. - name: Upload Windows artifacts diff --git a/packages/desktop/scripts/build.ts b/packages/desktop/scripts/build.ts index 92bee721b3..531ba75c30 100644 --- a/packages/desktop/scripts/build.ts +++ b/packages/desktop/scripts/build.ts @@ -1,7 +1,7 @@ import { build } from "esbuild"; import { cp, mkdir, rm, stat } from "node:fs/promises"; import { join } from "node:path"; -import { buildCore, buildDashboard, buildDashboardClient, buildEngine, packageRoot, stageDesktopDeploy, workspaceRoot } from "./workspace-tools"; +import { buildCore, buildDashboard, buildDashboardClient, buildEngine, desktopDeployDir, packageRoot, stageDesktopDeploy, workspaceRoot } from "./workspace-tools"; const dashboardRoot = join(workspaceRoot, "packages", "dashboard"); const dashboardClientDir = join(dashboardRoot, "dist", "client"); const dashboardRegistryManifestSource = join(dashboardRoot, "src", "registry-manifest.json"); @@ -113,6 +113,42 @@ async function ensureEmbeddedRuntimeBuild(): Promise { await buildEngine(); } +// FNXC:DesktopPackaging 2026-07-03-15:25: +// Guard the packaged Electron closure so a missing preload/main/renderer asset +// fails the build instead of shipping and crashing on a user's machine (field +// report Issue 5: preload was missing from the packaged unpacked layout). +// `preload.js` in particular is silent when absent — the contextBridge never +// installs window.fusionShell/fusionAPI and the app dead-ends on "can't reach +// the Fusion backend". Assert the required files in BOTH the source `dist/` +// (esbuild output) and the staged `deploy/dist/` (what electron-builder packs +// into app.asar), since only the latter reflects what ships. +const REQUIRED_PACKAGED_ASSETS = ["main.js", "preload.js", join("client", "index.html")]; + +async function verifyPackagedArtifacts(): Promise { + console.log("[desktop:build] Verifying required packaged assets are present..."); + const stagedDistDir = join(desktopDeployDir, "dist"); + const roots: Array<{ label: string; dir: string }> = [ + { label: "dist", dir: desktopDistDir }, + { label: "deploy/dist", dir: stagedDistDir }, + ]; + const missing: string[] = []; + for (const { label, dir } of roots) { + for (const asset of REQUIRED_PACKAGED_ASSETS) { + try { + await stat(join(dir, asset)); + } catch { + missing.push(`${label}/${asset}`); + } + } + } + if (missing.length > 0) { + throw new Error( + `Desktop packaging is missing required Electron assets: ${missing.join(", ")}. ` + + `Refusing to ship an incomplete app.asar.`, + ); + } +} + async function main(): Promise { await rm(desktopDistDir, { recursive: true, force: true }); await mkdir(desktopDistDir, { recursive: true }); @@ -128,6 +164,8 @@ async function main(): Promise { // which drops `deduped` subtrees and left the embedded runtime missing deps. await stageDesktopDeploy(); + await verifyPackagedArtifacts(); + console.log("[desktop:build] Desktop build complete"); } diff --git a/packages/desktop/src/__tests__/electron-builder-config.test.ts b/packages/desktop/src/__tests__/electron-builder-config.test.ts index ed3e77246a..0a3d83a543 100644 --- a/packages/desktop/src/__tests__/electron-builder-config.test.ts +++ b/packages/desktop/src/__tests__/electron-builder-config.test.ts @@ -62,9 +62,12 @@ describe("electron-builder desktop config", () => { const packageJsonRaw = await readDesktopFile("package.json"); const packageJson = JSON.parse(packageJsonRaw) as { scripts?: Record }; - expect(packageJson.scripts?.["dist:win"]).toBe("electron-builder --win"); - expect(packageJson.scripts?.["dist:mac"]).toBe("electron-builder --mac"); - expect(packageJson.scripts?.["dist:linux"]).toBe("electron-builder --linux"); + // Scripts package the staged production closure via `--projectDir deploy` + // (see scripts/workspace-tools.ts stageDesktopDeploy), so assert both the + // deploy projectDir and the platform flag rather than a bare invocation. + expect(packageJson.scripts?.["dist:win"]).toBe("electron-builder --projectDir deploy --win"); + expect(packageJson.scripts?.["dist:mac"]).toBe("electron-builder --projectDir deploy --mac"); + expect(packageJson.scripts?.["dist:linux"]).toBe("electron-builder --projectDir deploy --linux"); }); it("keeps required mac and linux targets", async () => { diff --git a/packages/desktop/src/__tests__/main.test.ts b/packages/desktop/src/__tests__/main.test.ts index a8405636de..51723e7dd5 100644 --- a/packages/desktop/src/__tests__/main.test.ts +++ b/packages/desktop/src/__tests__/main.test.ts @@ -328,7 +328,10 @@ describe("main process", () => { const { resolveLocalRuntimeRoot } = await importMainModule(); expect(resolveLocalRuntimeRoot()).toBe("/custom/fusion-home"); - expect(mocks.app.getPath).not.toHaveBeenCalled(); + // FUSION_HOME must satisfy the root without falling back to getPath("home"). + // (Module load calls getPath("userData") for the profile-relocation guard, so + // assert the specific "home" lookup is skipped rather than getPath overall.) + expect(mocks.app.getPath).not.toHaveBeenCalledWith("home"); }); it("initializeApp does not start local runtime for remembered choose mode", async () => { diff --git a/packages/desktop/src/__tests__/release-workflow.test.ts b/packages/desktop/src/__tests__/release-workflow.test.ts index 5e6641cbc5..bd7a22adad 100644 --- a/packages/desktop/src/__tests__/release-workflow.test.ts +++ b/packages/desktop/src/__tests__/release-workflow.test.ts @@ -19,19 +19,19 @@ describe("desktop release workflow wiring", () => { for (const workflow of [release, testRelease]) { expect(workflow).toContain("build-desktop-windows:"); expect(workflow).toContain("runs-on: windows-latest"); - expect(workflow).toMatch(/pnpm --filter @fusion\/desktop dist:win|electron-builder --win/); + expect(workflow).toMatch(/pnpm --filter @fusion\/desktop dist:win|electron-builder[^\n]*--win/); expect(workflow).toContain("name: fusion-desktop-windows"); expect(workflow).toContain("packages/desktop/dist-electron/latest.yml"); expect(workflow).toContain("build-desktop-macos:"); expect(workflow).toContain("runs-on: macos-latest"); - expect(workflow).toMatch(/pnpm --filter @fusion\/desktop dist:mac|electron-builder --mac/); + expect(workflow).toMatch(/pnpm --filter @fusion\/desktop dist:mac|electron-builder[^\n]*--mac/); expect(workflow).toContain("name: fusion-desktop-macos"); expect(workflow).toContain("packages/desktop/dist-electron/latest-mac.yml"); expect(workflow).toContain("build-desktop-linux:"); expect(workflow).toContain("runs-on: ubuntu-latest"); - expect(workflow).toMatch(/pnpm --filter @fusion\/desktop dist:linux|electron-builder --linux/); + expect(workflow).toMatch(/pnpm --filter @fusion\/desktop dist:linux|electron-builder[^\n]*--linux/); expect(workflow).toContain("--x64"); expect(workflow).toContain("--arm64"); expect(workflow).toContain("Fusion-*-linux-arm64.AppImage"); diff --git a/packages/desktop/src/__tests__/user-data-migration.test.ts b/packages/desktop/src/__tests__/user-data-migration.test.ts new file mode 100644 index 0000000000..aa8aed204d --- /dev/null +++ b/packages/desktop/src/__tests__/user-data-migration.test.ts @@ -0,0 +1,84 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { migratePreviousUserData } from "../user-data-migration.js"; + +/* +FNXC:DesktopUserDataMigration 2026-07-03-15:10: +Verify the one-time profile copy that keeps an upgrading operator's window +geometry/session when userData relocates to ~/.fusion (field report Issue 8): +it copies a populated previous profile into an absent/empty new dir exactly once, +and never overwrites an already-migrated profile, an empty source, or the same path. +*/ +describe("migratePreviousUserData", () => { + let root: string; + + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "fusion-userdata-mig-")); + }); + + afterEach(() => { + rmSync(root, { recursive: true, force: true }); + }); + + const seed = (dir: string, file: string, contents: string): void => { + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, file), contents); + }; + + it("copies a populated previous profile into an absent new dir", () => { + const previous = join(root, "old"); + const next = join(root, "new"); + seed(previous, "window-state.json", "{\"x\":10}"); + + expect(migratePreviousUserData(previous, next)).toBe(true); + expect(readFileSync(join(next, "window-state.json"), "utf-8")).toBe("{\"x\":10}"); + // Copy, not move: the source is left intact for downgrades/partial failures. + expect(readFileSync(join(previous, "window-state.json"), "utf-8")).toBe("{\"x\":10}"); + }); + + it("does not overwrite a new dir that already has data", () => { + const previous = join(root, "old"); + const next = join(root, "new"); + seed(previous, "session.json", "old"); + seed(next, "session.json", "already-migrated"); + + expect(migratePreviousUserData(previous, next)).toBe(false); + expect(readFileSync(join(next, "session.json"), "utf-8")).toBe("already-migrated"); + }); + + it("treats an empty new dir as eligible for migration", () => { + const previous = join(root, "old"); + const next = join(root, "new"); + seed(previous, "session.json", "restore-me"); + mkdirSync(next, { recursive: true }); // exists but empty + + expect(migratePreviousUserData(previous, next)).toBe(true); + expect(readFileSync(join(next, "session.json"), "utf-8")).toBe("restore-me"); + }); + + it("no-ops when there is no previous profile", () => { + const previous = join(root, "missing"); + const next = join(root, "new"); + + expect(migratePreviousUserData(previous, next)).toBe(false); + }); + + it("no-ops when the previous profile exists but is empty", () => { + const previous = join(root, "old"); + const next = join(root, "new"); + mkdirSync(previous, { recursive: true }); + + expect(migratePreviousUserData(previous, next)).toBe(false); + }); + + it("no-ops when previous and new paths are identical", () => { + const same = join(root, "same"); + seed(same, "session.json", "keep"); + + expect(migratePreviousUserData(same, same)).toBe(false); + expect(readFileSync(join(same, "session.json"), "utf-8")).toBe("keep"); + }); +}); diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 56a7b3eb97..f4ef93e6a8 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -2,12 +2,22 @@ import { app, BrowserWindow, nativeImage, screen, Tray } from "electron"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import os from "node:os"; +import { migratePreviousUserData } from "./user-data-migration.js"; /* FNXC:DesktopUserDataIsolation 2026-07-03-14:40: Isolate Electron desktop user-data/cache/crash artifacts under ~/.fusion/desktop-user-data so the packaged desktop does not share (or collide with) the default per-app Chromium profile across installs and Fusion versions (field report Issue 8). setPath must run before app "ready"; once locked it throws, which is non-fatal here. + +FNXC:DesktopUserDataMigration 2026-07-03-15:10: +Migrate the previous default profile into the new location BEFORE `setPath` overrides it, so upgrading operators keep their window geometry/session. `app.getPath("userData")` returns the default `/` path until overridden, so capture it first. See user-data-migration.ts for the one-time-copy gating. */ const fusionUserDataDir = join(os.homedir(), ".fusion", "desktop-user-data"); + +const migratedUserData = migratePreviousUserData(app.getPath("userData"), fusionUserDataDir); +if (migratedUserData) { + console.log(`[desktop/main] Migrated previous desktop profile into ${fusionUserDataDir}`); +} + try { app.commandLine.appendSwitch("user-data-dir", fusionUserDataDir); app.setPath("userData", fusionUserDataDir); diff --git a/packages/desktop/src/user-data-migration.ts b/packages/desktop/src/user-data-migration.ts new file mode 100644 index 0000000000..9c1e69ed0e --- /dev/null +++ b/packages/desktop/src/user-data-migration.ts @@ -0,0 +1,32 @@ +import { cpSync, existsSync, readdirSync } from "node:fs"; + +/* +FNXC:DesktopUserDataMigration 2026-07-03-15:10: +Relocating the Electron desktop profile to ~/.fusion/desktop-user-data (field report Issue 8) would silently orphan an existing operator's window geometry, sign-in session, and local storage under the OLD default Chromium profile (`/`). This performs a one-time COPY of the previous default profile into the new location, gated so it runs only on the first launch after upgrade: the new dir must be absent/empty and the old dir must exist, be non-empty, and be distinct. Copy (not move) so a failed/partial migration or a downgrade still finds the original profile intact. Best-effort: any failure returns false and the caller falls back to a fresh profile rather than blocking startup. +*/ + +function isMissingOrEmptyDir(dir: string): boolean { + try { + return readdirSync(dir).length === 0; + } catch { + // ENOENT (or unreadable) → treat as missing so we do not block on it. + return true; + } +} + +/** + * One-time copy of a previous Electron userData profile into `newDir`. + * @returns `true` if a migration copy was performed, `false` otherwise (already + * migrated, nothing to migrate, same path, or the copy failed). + */ +export function migratePreviousUserData(previousDir: string, newDir: string): boolean { + try { + if (previousDir === newDir) return false; + if (!isMissingOrEmptyDir(newDir)) return false; // already migrated / has its own data + if (!existsSync(previousDir) || isMissingOrEmptyDir(previousDir)) return false; + cpSync(previousDir, newDir, { recursive: true }); + return true; + } catch { + return false; + } +} diff --git a/reports/desktop-release-issues-2026-07-03.md b/reports/desktop-release-issues-2026-07-03.md index 3c71d701cc..985108c5e4 100644 --- a/reports/desktop-release-issues-2026-07-03.md +++ b/reports/desktop-release-issues-2026-07-03.md @@ -10,6 +10,26 @@ This document collects the issues we found while trying to run the official Fusi --- +## Resolution status (PR #1883) + +| # | Issue | Status | +|---|-------|--------| +| 1 | `electron` only a devDependency | **Fixed** — `electron` added to `@runfusion/fusion` runtime deps; lockfile synced. | +| 2 | Ancestor-dir walk crashes on unrelated JSON | **Already handled** — the desktop launcher uses `process.cwd()` (CLI) / `$HOME` (Electron main), never an ancestor walk (`desktop.ts:175`, `main.ts` `resolveLocalRuntimeRoot`), and every JSON parse on the shared discovery path is already `try/catch`-guarded, so unrelated JSON no longer throws. | +| 3 | Manage Projects opens Settings | **Fixed** — `handleViewAllProjects` now resets `taskView` to `command-center`. | +| 4 | Windows Terminal "Help" dialogs | **Fixed** — frontend auto-create of the first terminal tab is skipped on Windows. | +| 5 | Packaged build can miss `preload`/assets | **Fixed** — `scripts/build.ts` now verifies `main.js`, `preload.js`, and `client/index.html` exist in both `dist/` and the staged `deploy/dist/` before packaging, failing the build otherwise. (In this repo the preload ships as `dist/preload.js` inside `app.asar`, not `preload.cjs`.) | +| 6 | Desktop port drift / collision | **Already handled** — the embedded runtime binds an ephemeral port (`app.listen(0)`, `desktop.ts:78`), so fixed-port collision is structurally impossible; the CLI passes it via `FUSION_SERVER_PORT` and the desktop reuses it instead of double-binding (Issue 9), and a single-instance lock (`deep-link.ts`) quits a duplicate window. A fixed 9119/8643 would *reintroduce* collisions, so we deliberately did not pin one. | +| 7 | GPU/sandbox instability on Windows | **Fixed** — GPU/sandbox-disabling flags applied on Windows only (`os.platform() === "win32"`); macOS/Linux keep hardware acceleration and the sandbox. | +| 8 | User-data not isolated | **Fixed** — desktop profile relocated under `~/.fusion/desktop-user-data`, with a one-time copy migrating an existing operator's previous profile (window geometry/session) so upgrades don't lose state. | +| 9 | Desktop doesn't reuse a running server | **Fixed** — when `FUSION_SERVER_PORT` is set the desktop attaches to the CLI's dashboard instead of spawning an embedded runtime. | + +Recommendation #2 (verify packaged Windows layout before publishing) is now **enforced in CI**: `desktop-windows.yml` asserts the shipped `app.asar` contains `dist/main.js`, `dist/preload.js`, and `dist/client/index.html`, on top of `scripts/build.ts`'s pre-package staging check. + +Remaining as future team work (deliberately not attempted here): recommendation #1's full GUI-launch smoke (launch `Fusion.exe` and assert the window is visible/responsive) — reliably asserting a rendered Electron window on a CI runner is flaky, which the project's standing anti-flaky rule forbids adding; and the port/process auditing docs for Issue 6. + +--- + ## Issue 1: `fusion desktop` fails to launch on Windows 0.52.0 ### Symptom