diff --git a/.changeset/fix-desktop-electron-dep.md b/.changeset/fix-desktop-electron-dep.md new file mode 100644 index 0000000000..2274662bd1 --- /dev/null +++ b/.changeset/fix-desktop-electron-dep.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +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`. 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/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/packages/cli/src/commands/desktop.ts b/packages/cli/src/commands/desktop.ts index 25738d24cd..d3a75f9ee9 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,27 @@ 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"] : [])]; + + /* + 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, + ...windowsGpuFlags, + ...(options.dev ? ["--dev"] : []), + ]; // Build environment for Electron process const electronEnv: NodeJS.ProcessEnv = { diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index aec7296195..3d23005db5 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -337,7 +337,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, @@ -718,6 +718,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; 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 75f3bc2524..f4ef93e6a8 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -1,6 +1,32 @@ 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); + 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"; @@ -225,7 +251,7 @@ export async function initializeApp(): Promise { } } - if (rememberedLaunchMode === "local") { + if (rememberedLaunchMode === "local" && !process.env.FUSION_SERVER_PORT) { try { await startLocalRuntimeOnce(); } catch (error) { @@ -242,6 +268,16 @@ 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. + currentDesktopLaunchMode = "local"; + } + const windowState = state ? clampWindowStateToVisibleDisplay( state, 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/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 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 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..985108c5e4 --- /dev/null +++ b/reports/desktop-release-issues-2026-07-03.md @@ -0,0 +1,198 @@ +# 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. + +--- + +## 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 +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.