diff --git a/.changeset/windows-close-to-tray.md b/.changeset/windows-close-to-tray.md new file mode 100644 index 0000000000..74d71b7802 --- /dev/null +++ b/.changeset/windows-close-to-tray.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: The Windows desktop close dialog now offers Minimize to tray, keeping Fusion and the embedded PostgreSQL running in the background. +category: feature +dev: "win32 close dialog gains a Minimize to tray default alongside Exit-and-stop-PostgreSQL / Exit-leave-it-running (or a two-button variant when no embedded runtime is active); tray click restores the window. OS session end still skips dialogs and performs the full stop." diff --git a/packages/desktop/src/__tests__/main.test.ts b/packages/desktop/src/__tests__/main.test.ts index b10108edf3..ba77698ce1 100644 --- a/packages/desktop/src/__tests__/main.test.ts +++ b/packages/desktop/src/__tests__/main.test.ts @@ -121,6 +121,7 @@ const mocks = vi.hoisted(() => { Menu, nativeImage, shell, + dialog: { showMessageBoxSync: vi.fn(() => 1) }, screen, browserWindowInstance, browserWindowHandlers, @@ -133,6 +134,7 @@ const mocks = vi.hoisted(() => { vi.mock("electron", () => ({ app: mocks.app, + dialog: mocks.dialog, BrowserWindow: mocks.BrowserWindow, ipcMain: mocks.ipcMain, Tray: mocks.Tray, @@ -496,8 +498,14 @@ describe("main process", () => { expect(mocks.browserWindowInstance.on).toHaveBeenCalledWith("closed", expect.any(Function)); }); - it("windows window close saves state and allows quit cleanup instead of hiding", async () => { + /* + FNXC:DesktopClosePolicy 2026-07-18-06:40: + Windows close now asks: Minimize to tray keeps the app (and embedded + PostgreSQL) running in the background; Exit performs the full shutdown. + */ + it("windows window close with Exit chosen saves state and allows quit cleanup instead of hiding", async () => { mockPlatform("win32"); + mocks.dialog.showMessageBoxSync.mockReturnValue(1); const { initializeApp, run } = await importMainModule(); await initializeApp(); @@ -518,6 +526,25 @@ describe("main process", () => { expect(mainDeps.stopLocal).toHaveBeenCalledTimes(1); }); + it("windows window close with Minimize to tray hides instead of quitting", async () => { + mockPlatform("win32"); + mocks.dialog.showMessageBoxSync.mockReturnValue(0); + const { initializeApp, run } = await importMainModule(); + + await initializeApp(); + run(); + const closeHandler = mocks.browserWindowHandlers.get("close") as + | ((event: { preventDefault: () => void }) => void) + | undefined; + const event = { preventDefault: vi.fn() }; + + closeHandler?.(event); + + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(mocks.browserWindowInstance.hide).toHaveBeenCalledTimes(1); + expect(mocks.app.quit).not.toHaveBeenCalled(); + }); + it("macOS window close hides to tray without quitting", async () => { mockPlatform("darwin"); const { initializeApp, run } = await importMainModule(); diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index d1b2b101f3..445284ad1f 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -271,19 +271,51 @@ export function createMainWindow(state?: WindowState, launchTargetUrl?: string): force-killed, skipping all teardown. Electron's app "session-end" event flags that case; the default (full stop) then applies. */ + /* + FNXC:DesktopClosePolicy 2026-07-18-06:40: + Operator request: Windows needs a close-to-tray background option like + macOS/Linux. The close dialog now offers it alongside the exit choices; + "Minimize to tray" keeps Fusion (and the embedded PostgreSQL) running in + the background, restorable from the tray icon. OS session end still skips + every dialog and performs the full stop. + */ + if (osSessionEnding) { + return; + } const runtimeStatus = localRuntimeManager?.getStatus(); - if (!osSessionEnding && runtimeStatus?.state === "running" && runtimeStatus.source === "embedded-local") { + if (runtimeStatus?.state === "running" && runtimeStatus.source === "embedded-local") { const choice = dialog.showMessageBoxSync(window, { type: "question", title: "Fusion", - message: "Also shut down the embedded PostgreSQL server?", - detail: "Other Fusion processes (like the fn CLI) can keep using it if you leave it running. It will be reused on the next start either way.", - buttons: ["Shut down PostgreSQL", "Leave it running"], + message: "Close Fusion?", + detail: "Minimize to tray keeps Fusion and the embedded PostgreSQL server running in the background. If you exit, other Fusion processes (like the fn CLI) can keep using PostgreSQL only if you leave it running.", + buttons: ["Minimize to tray", "Exit and stop PostgreSQL", "Exit, leave PostgreSQL running"], defaultId: 0, cancelId: 0, noLink: true, }); - keepEmbeddedPostgresOnQuit = choice === 1; + if (choice === 0) { + event.preventDefault(); + window.hide(); + return; + } + keepEmbeddedPostgresOnQuit = choice === 2; + return; + } + const plainChoice = dialog.showMessageBoxSync(window, { + type: "question", + title: "Fusion", + message: "Close Fusion?", + detail: "Minimize to tray keeps Fusion running in the background; restore it from the tray icon.", + buttons: ["Minimize to tray", "Exit"], + defaultId: 0, + cancelId: 0, + noLink: true, + }); + if (plainChoice === 0) { + event.preventDefault(); + window.hide(); + return; } return; }