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:
5
.changeset/desktop-change-launch-mode-menu.md
Normal file
5
.changeset/desktop-change-launch-mode-menu.md
Normal 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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user