Files
fusion/packages/desktop
Fusion 43dd04860c feat(FN-3471): apply tokenized sizing and focus styles to component CSS
Merged CSS changes that apply tokenized sizing and focus styles to InsightsView and DesktopModeChooser components, with the majority of changes in InsightsView.css.

Fusion-Task-Id: FN-3471
2026-05-05 00:21:21 -07:00
..
2026-05-04 15:54:42 -07:00
2026-05-04 15:54:42 -07:00

@fusion/desktop

Electron desktop shell for Fusion.

This package provides a native Electron wrapper around the existing Fusion dashboard web UI. The desktop shell presents native desktop affordances including a system tray and application menu, with an embedded renderer for production deployments.

Running the Desktop Shell

Hot-reload development workflow

Run a single command from the workspace root:

pnpm --filter @fusion/desktop dev

This command now orchestrates the full desktop dev loop:

  1. Bundles Electron main.ts and preload.ts to packages/desktop/dist
  2. Starts the dashboard Vite renderer dev server (@fusion/dashboard dev:serve)
  3. Waits for renderer readiness
  4. Launches Electron with --dev and live renderer reload

By default it uses http://localhost:5173. Override with FUSION_DASHBOARD_URL.

Production-style desktop launch (from CLI)

fn desktop

fn desktop builds desktop artifacts, starts an embedded dashboard server on an ephemeral port, and launches Electron with embedded renderer assets.

Useful flags:

  • fn desktop --dev — use dev renderer URL (FUSION_DASHBOARD_URL or http://localhost:5173)
  • fn desktop --paused — start with engine paused

Renderer Architecture

The desktop uses a dual-mode renderer strategy:

Production Mode (default)

  • Loads embedded dashboard assets from dist/client/ (bundled at build time)
  • Uses window.loadFile() to load dist/client/index.html
  • Renderer connects to the embedded API server via IPC (getServerPort())

Development Mode (--dev or NODE_ENV=development)

  • Loads renderer from FUSION_DASHBOARD_URL (defaults to http://localhost:5173)
  • Uses window.loadURL() for live reload support
  • Renderer connects to the dev API server

Renderer Resolution (src/renderer.ts)

isDevelopmentMode()  // Checks NODE_ENV or --dev flag
isUrlRenderer()     // true in dev mode, false in production
getRendererUrl()     // Returns URL or file:// path
getRendererFilePath() // Returns absolute file path for loadFile()

First-run Shell Onboarding (Desktop)

Desktop boots through a shell-owned mode chooser before mounting the dashboard app when the user has not completed mode selection yet.

  • First run choice: users choose Local Fusion (bundled runtime) or Remote connection path.
  • Mode contract: desktopMode is "local" | "remote" | null and hasCompletedModeSelection determines whether the renderer treats startup as first-run. IPC also exposes a renderer-safe { isFirstRun, desktopMode } shape via shell:getDesktopModeState.
  • Desktop mode restore: after selection, mode is persisted and reused on relaunch.
  • Remote profiles: multiple saved profiles are supported (name, serverUrl, optional authToken) and can be managed/switched later from the dashboard header connection UI.
  • Storage boundary: shell connection state is stored only in desktop-local app data at app.getPath("userData")/shell-connections.json and is not written to .fusion/config.json or dashboard project storage keys.

Production vs dev bootstrap behavior

  • Production (fn desktop): renderer mounts DesktopShellBootstrap, which resolves shell mode via preload/IPC and either renders the chooser or mounts the dashboard shell. In remote mode, the dashboard shell opens the native connection onboarding/manager flow instead of the local runtime path.
  • Dev (pnpm --filter @fusion/desktop dev): same mode bootstrap flow runs; only the renderer source (Vite URL vs bundled file) changes.

IPC Channel Reference

src/ipc.ts registers renderer ↔ main process bridges used by window.electronAPI (desktop renderer transport/window controls) and window.fusionShell (shared shell connection contract for dashboard code).

Renderer → Main (ipcRenderer.invoke)

Channel Direction Parameters Returns
window:minimize renderer → main none Promise<void>
window:maximize renderer → main none Promise<boolean> (new maximized state)
window:close renderer → main none Promise<void>
window:isMaximized renderer → main none Promise<boolean>
app:getSystemInfo renderer → main none Promise<{ platform; arch; electronVersion; nodeVersion; appVersion; }>
app:checkForUpdates renderer → main none Promise<{ status: "checking" } | { status: "error"; error: string }>
app:getServerPort renderer → main none Promise<number | undefined>
tray:updateStatus renderer → main status: "running" | "paused" | "stopped" Promise<void>
native:showExportDialog renderer → main none Promise<string | null>
native:showImportDialog renderer → main none Promise<string | null>

Main → Renderer Events (ipcRenderer.on)

Channel Direction Payload
deep-link main → renderer DeepLinkResult ({ type, id, raw })
update-available main → renderer update info object (includes version)
update-downloaded main → renderer no payload is currently forwarded by preload

Local Bundled Runtime Lifecycle

Desktop local mode uses an in-process runtime manager (src/local-server.ts) that mirrors the CLI desktop server pattern:

  • creates TaskStore, calls init() and watch()
  • creates the dashboard server with createServer(store)
  • listens on an ephemeral port (0, never 4040)
  • reports idle | starting | ready | error local runtime state via window.fusionShell
  • starts automatically when desktop mode is local, stops on remote switch and app shutdown

Main Process Lifecycle

src/main.ts orchestrates module startup in this order:

  1. loadWindowState()
  2. createMainWindow(state)
  3. buildAppMenu({ mainWindow, appName: "Fusion" })
  4. setupTray(mainWindow, tray)
  5. registerIpcHandlers(mainWindow, tray)
  6. registerDeepLinkProtocol()
  7. setupDeepLinkHandler(mainWindow)
  8. setupAutoUpdater(mainWindow)
  9. mainWindow.maximize() when restored state was maximized

Window state and close-to-tray behavior

  • Startup restores width/height from persisted state (fallback: DEFAULT_WINDOW_STATE).
  • Position (x, y) is restored only when both values are present.
  • On window close:
    • state is saved via saveWindowState(mainWindow)
    • if app is not quitting, close is prevented and the window hides to tray
    • if app is quitting, close proceeds normally

Quit cleanup

  • before-quit sets app.isQuitting = true
  • Tray instance is destroyed (tray.destroy())
  • mainWindow is nulled on closed for clean re-creation on macOS activate

Preload APIs (window.electronAPI and window.fusionShell)

src/preload.ts exposes safe, context-isolated bridges:

  • window.electronAPI
    • Window control: minimize(), maximize(), close(), isMaximized()
    • App/system: getSystemInfo(), checkForUpdates(), getServerPort()
    • Tray: updateTrayStatus(status)
    • Native dialogs: showExportDialog(), showImportDialog()
    • Event subscriptions (return unsubscribe functions):
      • onDeepLink(callback)
      • onUpdateAvailable(callback)
      • onUpdateDownloaded(callback)
  • window.fusionShell
    • getState(), listProfiles(), saveProfile(), deleteProfile()
    • setActiveProfile(), setDesktopMode()
    • startQrScan(), openConnectionManager(), subscribe(listener)
  • window.fusionAPI remains as a backward-compatible alias of window.electronAPI.

All preload typings are declared in src/types.d.ts.

Module Integration Overview

renderer (window.fusionAPI)
        │
        ▼
   preload.ts (contextBridge)
        │
        ▼
     ipc.ts handlers ───────────► native.ts (dialogs, updater, window state)
        │
        ├────────────────────────► tray.ts (status + tray menu wiring)
        │
        └────────────────────────► main.ts lifecycle orchestration
                                      ├─ menu.ts (application menu)
                                      └─ deep-link.ts (fusion:// protocol + routing)

System Tray

  • Left-clicking the tray icon toggles the main window visibility.
  • Right-click context menu includes:
    • Show/Hide Window (contextual based on visibility)
    • Pause/Resume Engine (status toggle placeholder; IPC wiring lands in FN-1076)
    • Quit Fusion
  • Tray tooltip reflects engine status:
    • Fusion — Running
    • Fusion — Paused
    • Fusion — Stopped
  • Tray icon is generated from the Fusion four-dot logo.

Application Menu

The desktop shell installs a native menu with standard shortcuts.

  • macOS: App, Edit, View, Window, and Help menus.
  • Windows/Linux: Edit, View, Window, and Help (no App menu).
  • Keyboard shortcuts use Electron CmdOrCtrl accelerators for cross-platform behavior.
  • View menu includes reload, force reload, dev tools toggle, and zoom controls.

Native Integrations

src/native.ts provides desktop-native utilities used by the Electron main process:

  • Settings file dialogs
    • showExportSettingsDialog(parentWindow?) opens a save dialog for JSON exports using a default filename like fusion-settings-YYYY-MM-DD-HHmmss.json.
    • showImportSettingsDialog(parentWindow?) opens a single-file JSON picker.
  • Desktop notifications
    • showDesktopNotification(title, body, options?) wraps Electron Notification with support checks and optional click callback wiring.
  • Auto-updater integration
    • setupAutoUpdater(mainWindow?) configures electron-updater, checks for updates, and relays update-available / update-downloaded events to the renderer via IPC.
    • Failures are logged and treated as non-fatal (important for unsigned/local dev builds).
  • Window state persistence
    • loadWindowState() reads window-state.json from app.getPath("userData").
    • saveWindowState(mainWindow) writes bounds/maximized state atomically (.tmp + rename).
    • DEFAULT_WINDOW_STATE is the fallback (1280x900, not maximized).

Deep Linking

src/deep-link.ts implements fusion:// protocol support.

Supported URL patterns

  • fusion://task/FN-123 → task deep link
  • fusion://project/my-app → project deep link
  • fusion://task/FN-123/extra → extra segments are ignored
  • fusion://project/my%20app → ID is URL-decoded

Invalid or unsupported URLs (wrong scheme, missing host, unknown host) are ignored.

Single-instance behavior and platform differences

  • setupDeepLinkHandler(mainWindow) owns app.requestSingleInstanceLock().
  • If no lock is granted, the app quits to avoid duplicate instances.
  • macOS: listens to open-url events.
  • Windows/Linux: listens to second-instance args and extracts fusion:// URLs.
  • Valid parsed deep links are forwarded to the renderer as mainWindow.webContents.send("deep-link", result).

Cross-Task API Contract (FN-1075 → FN-1076)

FN-1076 depends on these exact exports and names.

src/native.ts

Export Type
showExportSettingsDialog (parentWindow?) => Promise<string | null>
showImportSettingsDialog (parentWindow?) => Promise<string | null>
showDesktopNotification (title, body, options?) => void
setupAutoUpdater (mainWindow?) => void
loadWindowState () => Promise<WindowState | null>
saveWindowState (mainWindow) => void
DEFAULT_WINDOW_STATE WindowState
WindowState interface

src/deep-link.ts

Export Type
registerDeepLinkProtocol () => void
parseDeepLink (url: string) => DeepLinkResult | null
handleDeepLink (mainWindow, url: string) => void
setupDeepLinkHandler (mainWindow) => void
DeepLinkResult interface

Tray Icons

Tray icons are generated from packages/dashboard/app/public/logo.svg.

  • Script: pnpm --filter @fusion/desktop generate:icons
  • Package-local equivalent (from packages/desktop): pnpm generate:icons
  • Generated outputs are committed under src/icons/:
    • tray-16.png
    • tray-32.png
    • tray-48.png

Scripts

  • pnpm --filter @fusion/desktop dev — hot-reload workflow (main/preload bundle + dashboard Vite dev server + Electron)
  • pnpm --filter @fusion/desktop build — production desktop build (dashboard client build + main/preload bundle + asset copy)
  • pnpm --filter @fusion/desktop test — run Vitest suite
  • pnpm --filter @fusion/desktop typecheck — run TypeScript checks without emitting files
  • pnpm --filter @fusion/desktop generate:icons — regenerate tray icon PNG assets from the dashboard logo SVG
  • pnpm --filter @fusion/desktop pack — generate unpacked artifacts via electron-builder (--dir)
  • pnpm --filter @fusion/desktop dist — generate installable desktop artifacts via electron-builder

Packaging

Desktop packaging is configured in electron-builder.yml.

  • Output directory: packages/desktop/dist-electron
  • Targets: macOS (dmg, zip), Windows (nsis, portable), Linux (AppImage, deb, tar.gz)
  • Deep link protocol: fusion://
  • Publish provider: GitHub (gsxdsm/fusion)

Run pnpm --filter @fusion/desktop build before pack/dist to ensure dist/ assets are up to date.

Environment

  • FUSION_DASHBOARD_URL — override the default dashboard URL in development mode (http://localhost:5173)
  • FUSION_SERVER_PORT — internal: port for embedded API server (set by CLI)
  • FUSION_ELECTRON_BINARY — path to Electron binary (for testing)

Build Pipeline

Development Build (pnpm --filter @fusion/desktop dev)

  1. Bundle main.ts and preload.ts with esbuild
  2. Start dashboard Vite dev server
  3. Launch Electron with --dev flag

Production Build (pnpm --filter @fusion/desktop build)

  1. Build dashboard client to packages/dashboard/dist/client/
  2. Bundle main.ts and preload.ts with esbuild
  3. Copy dashboard client to packages/desktop/dist/client/

CLI Launch (fn desktop)

  1. Build desktop artifacts (unless --dev)
  2. Start embedded API server on ephemeral port
  3. Launch Electron:
    • Production: Uses embedded renderer assets, getServerPort() for API connection
    • Development (--dev): Uses FUSION_DASHBOARD_URL for live reload

Desktop Shell UI Components

  • src/renderer/components/DesktopWrapper.tsx wraps the dashboard app for Electron-only chrome.
  • src/renderer/components/TitleBar.tsx implements a custom frameless title bar with Fusion branding, drag region behavior, and window controls (minimize/maximize/close).
  • The title bar styling lives in src/renderer/components/TitleBar.css and uses dashboard theme tokens (--surface, --border, --text, etc.).

Desktop Hooks

Reusable renderer hooks in src/renderer/hooks/ expose Electron runtime capabilities:

  • useElectron() — runtime detection + typed electronAPI access
  • useAutoUpdate() — update-available subscription + install trigger
  • useDeepLink() — deep-link subscription and fusion://task/... / fusion://project/... parsing

Renderer Entrypoint

  • src/renderer/index.html mirrors dashboard theme initialization logic with Electron-safe defaults.
  • src/renderer/index.tsx mounts the dashboard app in StrictMode and wraps it in DesktopWrapper.
  • Unlike the web dashboard entry (packages/dashboard/app/main.tsx), this renderer entry does not register service workers and is intended for desktop-only bootstrapping.