Move the host-agnostic bundled-plugin auto-install logic (manifest loading, entry-path
resolution, install/update/enable flow) out of the CLI package into @fusion/core so the
desktop embedded runtime can auto-install bundled runtime plugins without depending on
the CLI package; the CLI module becomes a thin adapter that supplies its own bundle-dir
resolution to the shared helper.
- Add packages/core/src/plugins/bundled-plugin-install.ts with the shared, host-agnostic
ensureBundledPluginInstalled / ensureBundledDependencyGraphPluginInstalled /
ensureBundledCursorRuntimePluginInstalled implementation and BUNDLED_PLUGIN_IDS/
isBundledPluginId/resolvePluginEntryPath, exported from @fusion/core's index.
- Slim packages/cli/src/plugins/bundled-plugin-install.ts to a CLI-specific
candidate-bundle-dir resolver that delegates to @fusion/core and re-exports the same
public surface dashboard.ts/serve.ts/daemon.ts already depend on.
- Remove the now-redundant packages/cli/src/plugins/__tests__/resolve-plugin-entry-path-sync.test.ts
(coverage moved with the implementation to @fusion/core).
- Add packages/desktop/src/bundled-plugin-dirs.ts to resolve each bundled plugin's staged
package directory via import.meta.resolve, mirroring the CLI's dist/plugins/<id> resolver.
- Wire local-runtime.ts and local-server.ts to call ensureBundledPluginInstalled before
loadAllPlugins() and expose a lazy-install callback for PUT /api/plugins/:id/settings,
mirroring the CLI dashboard command's startup auto-install pass.
- Update docs/PLUGIN_AUTHORING.md to describe the shared bundled-plugin-install location.
Files changed:
docs/PLUGIN_AUTHORING.md | 11 +
.../__tests__/bundled-plugin-install.test.ts | 619 ++-------------------
.../resolve-plugin-entry-path-sync.test.ts | 97 ----
packages/cli/src/plugins/bundled-plugin-install.ts | 250 +--------
packages/core/src/index.ts | 8 +
.../__tests__/bundled-plugin-install.test.ts | 391 +++++++++++++
.../core/src/plugins/bundled-plugin-install.ts | 186 +++++++
.../src/__tests__/bundled-plugin-dirs.test.ts | 59 ++
.../desktop/src/__tests__/local-runtime.test.ts | 183 +++++-
.../desktop/src/__tests__/local-server.test.ts | 96 +++-
packages/desktop/src/bundled-plugin-dirs.ts | 61 ++
packages/desktop/src/local-runtime.ts | 66 ++-
packages/desktop/src/local-server.ts | 36 +-
13 files changed, 1171 insertions(+), 892 deletions(-)
Fusion-Task-Id: FN-7637
Fusion-Task-Lineage: 953c5b82-a079-4600-b3af-45c974cd5014
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the desktop app's plugin subsystem, which was never wired into createServer(), breaking Settings > Plugins Browse registry and plugin install.
- local-runtime.ts: construct a PluginStore + PluginLoader (mirroring the CLI dashboard command), load enabled plugins, run plugin schema-init hooks, and pass pluginStore/pluginLoader/pluginRunner into createServer()
- local-server.ts: apply the same wiring to the legacy desktop local server path for consistency
- Both paths fail soft: a broken plugin subsystem (e.g. corrupt manifest) is logged/traced but no longer blocks embedded dashboard startup
- Extend local-runtime.test.ts and local-server.test.ts to cover plugin wiring and the fail-soft path
- Add changeset (patch) documenting the fix
Files changed:
.changeset/fn-7623-desktop-plugin-wiring.md | 7 ++
.../desktop/src/__tests__/local-runtime.test.ts | 123 ++++++++++++++++++++-
.../desktop/src/__tests__/local-server.test.ts | 69 +++++++++++-
packages/desktop/src/local-runtime.ts | 50 ++++++++-
packages/desktop/src/local-server.ts | 41 ++++++-
5 files changed, 286 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-7623
Fusion-Task-Lineage: c6f291fb-e6aa-4ac1-a3f3-4189fc831c60
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Adds bounded internal retry to the desktop embedded-start path so a transient first-attempt failure self-heals before the operator ever sees the "Couldn't start local Fusion" error screen.
- LocalRuntimeManager.startEmbedded() now retries startEmbeddedAttempt() up to startupRetries (default 3) total attempts with a startupRetryDelayMs (default 150ms) delay between attempts, both overridable via constructor options for deterministic zero-delay tests.
- status.state stays "starting" across retried attempts; only the final attempt's real error sets state "error" and is thrown/surfaced, so genuine failures still report their real message unchanged.
- Only affects the embedded-start path (never external-cli or already-running paths).
- Adds regression tests covering retry-then-success, exhausted-retries-surfaces-final-error, and status transitions across attempts.
Files changed:
.../desktop/src/__tests__/local-runtime.test.ts | 158 ++++++++++++++++++++-
packages/desktop/src/local-runtime.ts | 76 +++++++++-
2 files changed, 228 insertions(+), 6 deletions(-)
Fusion-Task-Id: FN-7617
Fusion-Task-Lineage: 2baf90f0-052c-42ff-a5b1-bc0b802d0ddc
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
First-run project setup surfaced a blocking "Agent with this name already exists" error when creating
the default CEO. The default first agent can be created from more than one first-run surface (the
unified ModelOnboarding agent step and the project-setup SetupWizard sub-flow), and agent names are
unique per store, so the second create returned 409. The step's goal — a first agent exists — was
already satisfied, so both onboarding surfaces now treat a name collision as success and advance
instead of blocking. The user still creates the agent; they just aren't punished for the flow offering
it twice.
Separately, the desktop header "Desktop local mode / Switch server" button did nothing: main relays
the click as a `shell:open-connection-manager` IPC (webContents.send), but the preload never forwarded
it to the `window` DOM event ShellContext listens for, so the signal was dropped. Add the preload
bridge so the button opens NativeShellConnectionManager (Local/Remote toggle + remote-server profiles),
letting operators switch between running locally, connecting to a remote server, or a different remote.
Regression tests: SetupWizard advances on an already-exists agent; preload dispatches the window event
on the IPC.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
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.
createServer's modelRegistry is optional and, when absent, the /api/models endpoint returns an
empty list — so after connecting a provider (e.g. Anthropic) the onboarding model picker showed
"no models". The desktop wired authStorage but no ModelRegistry.
Add a shared createFusionModelRegistry(authStorage) factory to @fusion/engine (which already depends
on @earendil-works/pi-coding-agent) so non-CLI hosts don't need that dep directly, and pass its
result as modelRegistry from both desktop server paths. Verified against a clean embedded server:
/api/models now returns 24 Claude models instead of an empty list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
local-server now imports createFusionAuthStorage from @fusion/engine; add it to the test's engine
mock so the startup path resolves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop embedded server called createServer WITHOUT an authStorage, so GET /api/auth/status
returned 500 "Authentication is not configured". The dashboard's first-run onboarding hook
(useAuthOnboarding -> fetchAuthStatus) catches that failure silently and never opens the model
onboarding wizard — so the desktop skipped straight to project creation with no AI/GitHub setup,
regardless of onboarding-completion flags (which is why clearing them did nothing). Providers also
couldn't be authenticated at all.
Wire createFusionAuthStorage() (the same storage the CLI dashboard/serve commands use) into both the
primary (local-runtime) and legacy (local-server) desktop server paths. Verified against a clean
embedded server: /api/auth/status now returns 200 with the provider list instead of 500. (Full
API-key provider wrapping stays CLI-only for now; OAuth + CLI providers are available on desktop.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- DesktopLaunchGate: the chooser onPick("local") path still called the removed
applyServerBaseUrl helper (broke Typecheck and the desktop local chooser flow) —
call navigateToLocalRuntimeOrigin. Also re-read shell.getState() immediately before the
self-healing start check so a stale mount snapshot doesn't fire a redundant
setDesktopMode("local") on normal boots (Greptile).
- register-fn-binary-routes: on Windows the npm install runs under a shell, so a timeout's
child.kill only stopped cmd.exe and left npm.cmd/node running — kill the whole process tree
via taskkill /T on win32 (CodeRabbit).
- local-server.test: align with the new resolver contract — assert the runtime never
auto-registers the root project and starts engine-less when no projects exist (CodeRabbit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop embedded runtime auto-registered its runtime root (the user's HOME directory) as a
project on first launch, and bare `fusion` / `fn` / `fn dashboard` / `fusion dashboard`
auto-registered the CWD as a project. Both silently created a "cwd-mode" project the operator
never chose and dropped them onto a board for it.
- Desktop: replace ensureDesktopRuntimeProject (which registered home) with
resolveDesktopRuntimePrimaryProject, which only PICKS an already-registered project as the
primary engine target and registers nothing. With no projects the server starts engine-less
(createServer's engine is optional) and the dashboard shows its onboarding empty state.
Applied to both the primary (local-runtime) and legacy (local-server) desktop server paths.
- CLI dashboard command: ensureCwdProjectRegistered now runs with autoRegister:false, so it uses
the CWD project only if already registered, else starts with none and the dashboard onboards.
(serve/daemon keep their existing --no-auto-register flag; the CLI `desktop` launcher unchanged.)
Verified: with zero projects the embedded server starts, /api/health -> 200, /api/projects -> [],
/ serves the client. Unit test asserts resolveDesktopRuntimePrimaryProject registers nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Desktop startup used two independent persisted sources of truth that could
disagree: desktop-launch-mode.json decided whether main STARTS the embedded
local runtime, while shell-connections.json (desktopMode) decided whether the
renderer launch gate WAITS for it. shell:setDesktopMode persists shell settings
before the fallible startLocalRuntimeOnce()/saveDesktopLaunchMode(), so a first
"local" selection whose runtime start failed or was interrupted left
shell=local / launch-mode=choose permanently. Every subsequent launch then sat
at "Starting local Fusion runtime…" polling a runtime nobody started, timing out
after 30s.
Fix (defense in depth):
- initializeApp reconciles: a completed shell "local" selection is authoritative;
it heals the launch-mode file and starts the runtime.
- onDesktopModeChange/onDesktopLaunchModeChange persist launch-mode BEFORE the
fallible start so an interrupted start cannot re-create the desync.
- DesktopLaunchGate no longer assumes main started the runtime; if it is not
running/starting it actively (re)starts via setDesktopMode("local") before polling.
- Add env-gated startup trace (FUSION_STARTUP_TRACE) so packaged builds, which
otherwise log nothing, can diagnose this class of stall.
Regression tests assert the invariant (split-brain -> runtime starts + file heals;
agreement-on-choose -> no start). flushPromises now drains via a macrotask so
run()-based tests observe a fully-initialized app regardless of async chain length.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds macOS code signing and notarization infrastructure (FN-5609), introducing hardened runtime entitlements, electron-builder config for signed builds, and signing-enabled release workflows, with tests validating the configuration and docs covering the signing flow.
Fusion-Task-Id: FN-5609
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5609
Adds Linux ARM64 as a target architecture to the desktop release pipeline, including dual-arch release and test-release workflows, electron-builder configuration updates, and corresponding tests and documentation.
Fusion-Task-Id: FN-5608
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5608
Add multi-platform updater feed configuration for the desktop app, including Windows, Mac, and Linux feed files with a collector, wired into the release and test-release workflows, plus corresponding tests and documentation.
Fusion-Task-Id: FN-5607
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5607
Implements Linux GPG signing for the release pipeline by adding a `sign-linux.sh` helper, wiring it into the release and test-release workflows, including `.asc` signature files in release collectors, and documenting the full signing workflow in CODE_SIGNING.md with a note in the desktop README.
Fusion-Task-Id: FN-5605
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5605
Added GitHub Actions workflows for macOS and Linux desktop release artifacts (`release.yml` and `test-release.yml`), wired in new `dist` scripts across platforms, and extended test coverage for electron-builder config and release workflow logic, with updated documentation for the new artifacts.
Fusion-Task-Id: FN-5603
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5603
Added Windows ARM64 support to the desktop build pipeline, introducing separate target architecture arrays for x64 and ARM64, configuring electron-builder to produce artifacts for both platforms, and adding tests to assert the correct architecture names.
Fusion-Task-Id: FN-5594
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5594
Adds a Windows desktop build job to the release pipeline, wires the desktop artifacts into both release and test-release workflows, includes workflow shape assertions in tests, and documents the Windows release artifacts in the desktop README.
Fusion-Task-Id: FN-5593
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5593
Adds Windows desktop packaging support for Fusion, including a new GitHub Actions workflow for building Windows desktop targets, matching build scripts in the root and desktop package, a test for electron-builder configuration, and documentation of the packaging path.
Fusion-Task-Id: FN-5587
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fusion-Task-Id: FN-5587
The console.log breadcrumbs added while diagnosing the menu also
caused an uncaught "write EPIPE" when the parent terminal pipe was
already closed by the time the user clicked Change Launch Mode.
Now that the flow is verified working, remove the chatter and keep
only the actual error-path logging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The recently added "Change Launch Mode…" menu wiring made main.ts pass
an extra onChangeLaunchMode callback to buildAppMenu(). The
main-integration test still asserted the old exact-shape {mainWindow,
appName} payload, so it failed once concurrent test load made
ordering matter. Switched to expect.objectContaining so future
buildAppMenu props don't break this test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the renderer-event approach (which silently failed when the
renderer-side listener wasn't yet registered) with an onChangeLaunchMode
callback wired through AppMenuOptions. The callback runs
resetLaunchModeAndReload in main: writes shell settings to clear the
chosen mode, stops the embedded runtime, then navigates the window
directly to the renderer entrypoint with no cached query params so the
launch gate re-prompts.
Also surface the same flow from the dashboard's BackendConnectionErrorPage:
when running inside the desktop shell, the "Can't reach the Fusion backend"
page now offers a "Change Launch Mode…" button alongside Retry, so a user
who chose a broken backend isn't stuck without the Electron menubar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The close handler hides the BrowserWindow instead of destroying it, so
once closed, subsequent dock or Finder activations were no-ops because
the activate handler only created a new window when mainWindow === null.
Show + focus the existing window if it's hidden so relaunch behaves as
expected. Also add console.log breadcrumbs in DesktopLaunchGate so we
can diagnose why the chooser sometimes appears not to render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Connection" submenu with a "Change Launch Mode…" item that lets
users switch between Run Locally and Connect to Remote after the initial
chooser. The menu sends shell:reset-desktop-mode-request to the renderer;
the dashboard's DesktopLaunchGate listens, calls the new
shell:resetDesktopMode IPC (clears hasCompletedModeSelection, stops the
embedded runtime), strips the cached serverBaseUrl/shellMode query params,
and reloads so the gate re-prompts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In packaged Electron builds, `process.argv[1]` is undefined (Electron loads
the main script via package.json `main`, not via argv), so the bottom-of-file
guard never invoked `run()` and the app started without creating a window.
Also build the dashboard client with `--base ./` so its `file://`-loaded
index.html resolves `./assets/*` from inside the asar instead of the
filesystem root, which was producing a blank white window.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>