feat(desktop): Connection → Change Launch Mode menu item

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>
This commit is contained in:
gsxdsm
2026-05-17 22:16:27 -07:00
parent 3cae2a30b9
commit 75a712771e
7 changed files with 64 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Add a "Connection → Change Launch Mode…" menu item to the Fusion desktop app so users can switch between Run Locally and Connect to Remote after the initial chooser. It resets the persisted desktop mode, stops the embedded runtime, and reloads the dashboard so the launch gate re-prompts.

View File

@@ -30,7 +30,7 @@
.desktop-launch-gate__panel p {
margin: 0 0 1.25rem;
color: var(--text-secondary, #9ca3af);
color: var(--text-muted, #9ca3af);
font-size: 0.9375rem;
line-height: 1.5;
}

View File

@@ -117,6 +117,27 @@ export function DesktopLaunchGate({ children }: PropsWithChildren) {
};
}, []);
useEffect(() => {
const shell = getFusionShell();
if (!shell?.onResetDesktopModeRequest || !shell.resetDesktopMode) {
return;
}
return shell.onResetDesktopModeRequest(() => {
void (async () => {
try {
await shell.resetDesktopMode?.();
} catch {
// Reset best-effort; we still reload to give the user a fresh
// chooser even if the IPC failed.
}
const url = new URL(window.location.href);
url.searchParams.delete("serverBaseUrl");
url.searchParams.delete("shellMode");
window.location.replace(url.toString());
})();
});
}, []);
if (phase.kind === "loading" || phase.kind === "starting-local") {
const message = phase.kind === "loading" ? "Loading Fusion…" : phase.message;
return (

View File

@@ -45,6 +45,8 @@ export interface FusionShellApi {
deleteProfile(profileId: string): Promise<void>;
setActiveProfile(profileId: string | null): Promise<ShellConnectionState>;
setDesktopMode(mode: "local" | "remote"): Promise<ShellConnectionState>;
resetDesktopMode?(): Promise<ShellConnectionState>;
onResetDesktopModeRequest?(callback: () => void): () => void;
startQrScan(): Promise<{ serverUrl: string; authToken?: string | null }>;
openConnectionManager(): Promise<void>;
subscribe(listener: (state: ShellConnectionState) => void): () => void;

View File

@@ -174,6 +174,16 @@ export function registerIpcHandlers(mainWindow: BrowserWindow, tray: Tray, optio
return emitShellState(mainWindow, options.getRuntimeStatus);
});
ipcMain.handle("shell:resetDesktopMode", async () => {
const settings = await readShellSettings();
settings.desktopMode = null;
settings.hasCompletedModeSelection = false;
await writeShellSettings(settings);
// Stop the embedded runtime so a subsequent local choice spins up fresh.
await options.stopLocalRuntime?.().catch(() => undefined);
return emitShellState(mainWindow, options.getRuntimeStatus);
});
ipcMain.handle("shell:startQrScan", async () => {
throw new Error("QR scanning is not available in desktop shell");
});

View File

@@ -10,6 +10,24 @@ export interface AppMenuOptions {
appName: string;
}
function buildConnectionSubmenu(options: AppMenuOptions): MenuItemConstructorOptions {
const { mainWindow } = options;
return {
label: "Connection",
submenu: [
{
label: "Change Launch Mode…",
click: () => {
// The renderer-side gate listens for this and calls
// shell.resetDesktopMode() before reloading without the cached
// serverBaseUrl query param.
mainWindow.webContents.send("shell:reset-desktop-mode-request");
},
},
],
};
}
function buildAppSubmenu(options: AppMenuOptions): MenuItemConstructorOptions {
return {
label: options.appName,
@@ -219,6 +237,7 @@ export function buildMenuTemplate(options: AppMenuOptions): MenuItemConstructorO
const template: MenuItemConstructorOptions[] = [
buildEditSubmenu(),
buildViewSubmenu(options),
buildConnectionSubmenu(options),
buildWindowSubmenu(isMac),
buildHelpSubmenu(),
];

View File

@@ -114,6 +114,12 @@ const fusionShell = {
getDesktopModeState: (): Promise<{ isFirstRun: boolean; desktopMode: "local" | "remote" | null }> =>
ipcRenderer.invoke("shell:getDesktopModeState"),
setDesktopMode: (mode: "local" | "remote"): Promise<ShellConnectionState> => ipcRenderer.invoke("shell:setDesktopMode", mode),
resetDesktopMode: (): Promise<ShellConnectionState> => ipcRenderer.invoke("shell:resetDesktopMode"),
onResetDesktopModeRequest: (callback: () => void): (() => void) => {
const handler = () => callback();
ipcRenderer.on("shell:reset-desktop-mode-request", handler);
return () => ipcRenderer.removeListener("shell:reset-desktop-mode-request", handler);
},
startQrScan: (): Promise<{ serverUrl: string; authToken?: string | null }> => ipcRenderer.invoke("shell:startQrScan"),
openConnectionManager: (): Promise<void> => ipcRenderer.invoke("shell:openConnectionManager"),
subscribe: (listener: (state: ShellConnectionState) => void): (() => void) => {