# @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: ```bash 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) ```bash 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`) ```typescript 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() ``` ## IPC Channel Reference `src/ipc.ts` registers the renderer ↔ main process bridge used by `window.fusionAPI`. ### Renderer → Main (`ipcRenderer.invoke`) | Channel | Direction | Parameters | Returns | |---|---|---|---| | `window:minimize` | renderer → main | none | `Promise` | | `window:maximize` | renderer → main | none | `Promise` (new maximized state) | | `window:close` | renderer → main | none | `Promise` | | `window:isMaximized` | renderer → main | none | `Promise` | | `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` | | `tray:updateStatus` | renderer → main | `status: "running" \| "paused" \| "stopped"` | `Promise` | | `native:showExportDialog` | renderer → main | none | `Promise` | | `native:showImportDialog` | renderer → main | none | `Promise` | ### 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 | ## 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 API (`window.fusionAPI`) `src/preload.ts` exposes a safe, context-isolated bridge: - 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)` All preload typings are declared in `src/types.d.ts` (`FusionAPI`, `SystemInfo`, `UpdateCheckResult`, `DeepLinkResult`). ## Module Integration Overview ```text 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` | | `showImportSettingsDialog` | `(parentWindow?) => Promise` | | `showDesktopNotification` | `(title, body, options?) => void` | | `setupAutoUpdater` | `(mainWindow?) => void` | | `loadWindowState` | `() => Promise` | | `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.