fix: address engine startup review feedback
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
Start the AI engine by default in `pnpm local`, keep dashboard `--dev` engine-on unless `--no-engine` is passed, start desktop local runtimes with engines, and show dashboard instructions when Fusion is launched without an engine.
|
||||
|
||||
@@ -2229,6 +2229,15 @@ describe("runDashboard — --no-engine mode", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not start engine components when dev mode is launched with noEngine", async () => {
|
||||
const { TriageProcessor, TaskExecutor, Scheduler } = await import("@fusion/engine");
|
||||
await runDashboard(0, { open: false, dev: true, noEngine: true });
|
||||
|
||||
expect(TriageProcessor).not.toHaveBeenCalled();
|
||||
expect(TaskExecutor).not.toHaveBeenCalled();
|
||||
expect(Scheduler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows 'AI engine: ✓ active' when not in dev mode", async () => {
|
||||
await runDashboard(0, { open: false });
|
||||
|
||||
|
||||
@@ -330,7 +330,7 @@ function SystemPanel({ state, isFocused }: { state: DashboardState; isFocused: b
|
||||
</Box>
|
||||
<Box flexDirection="row" gap={1} flexShrink={0}>
|
||||
<Text dimColor>Engine</Text>
|
||||
{info.engineMode === "dev" && <Text color="yellow">dev</Text>}
|
||||
{info.engineMode === "no-engine" && <Text color="yellow">no-engine</Text>}
|
||||
{info.engineMode === "paused" && <Text color="yellow">paused</Text>}
|
||||
{info.engineMode === "active" && <Text color="green">active</Text>}
|
||||
</Box>
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface SystemInfo {
|
||||
authEnabled: boolean;
|
||||
authToken?: string;
|
||||
tokenizedUrl?: string;
|
||||
engineMode: "dev" | "active" | "paused";
|
||||
engineMode: "no-engine" | "active" | "paused";
|
||||
fileWatcher: boolean;
|
||||
startTimeMs: number;
|
||||
startupDurationMs?: number;
|
||||
|
||||
@@ -2380,7 +2380,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
if (isTTY && tui) {
|
||||
// Determine engine mode
|
||||
const settings = await store.getSettings();
|
||||
const engineMode = noEngine ? "dev" : settings.enginePaused ? "paused" : "active";
|
||||
const engineMode = noEngine ? "no-engine" : settings.enginePaused ? "paused" : "active";
|
||||
const startupDurationMs = Date.now() - dashboardStartedAt;
|
||||
|
||||
const systemInfo: SystemInfo = {
|
||||
@@ -2905,7 +2905,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
tui.log(`Dashboard started at ${baseUrl}`);
|
||||
if (engineMode === "active") {
|
||||
tui.log("AI engine active");
|
||||
} else if (engineMode === "dev") {
|
||||
} else if (engineMode === "no-engine") {
|
||||
tui.log("AI engine disabled (--no-engine)");
|
||||
} else {
|
||||
tui.log("AI engine paused");
|
||||
|
||||
@@ -50,71 +50,74 @@ async function buildDesktopArtifacts(rootDir: string): Promise<void> {
|
||||
|
||||
async function startDashboardRuntime(rootDir: string, paused: boolean): Promise<DashboardRuntime> {
|
||||
const store = new TaskStore(rootDir);
|
||||
await store.init();
|
||||
await store.watch();
|
||||
|
||||
if (paused) {
|
||||
await store.updateSettings({ enginePaused: true });
|
||||
}
|
||||
|
||||
/*
|
||||
* FNXC:DesktopRuntime 2026-06-20-23:39:
|
||||
* Desktop local mode must start the same project engine lifecycle as CLI dashboard mode; a desktop window without engines leaves users with a live dashboard that cannot execute tasks.
|
||||
*/
|
||||
const centralCore = new CentralCore();
|
||||
await centralCore.init();
|
||||
const cwdRegistered = await ensureCwdProjectRegistered({
|
||||
cwd: rootDir,
|
||||
central: centralCore,
|
||||
logPrefix: "desktop",
|
||||
autoRegister: true,
|
||||
});
|
||||
const engineManager = new ProjectEngineManager(centralCore);
|
||||
await engineManager.startAll();
|
||||
engineManager.startReconciliation();
|
||||
const cwdEngine = cwdRegistered
|
||||
? await engineManager.ensureEngine(cwdRegistered.id).catch((err) => {
|
||||
console.warn(`[desktop] Failed to warm cwd project engine: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const app = createServer(store, {
|
||||
engine: cwdEngine,
|
||||
engineManager,
|
||||
centralCore,
|
||||
onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),
|
||||
});
|
||||
const server = app.listen(0);
|
||||
|
||||
let server: import("node:http").Server | null = null;
|
||||
let engineManager: ProjectEngineManager | undefined;
|
||||
let centralCore: CentralCore | undefined;
|
||||
try {
|
||||
await store.init();
|
||||
await store.watch();
|
||||
|
||||
if (paused) {
|
||||
await store.updateSettings({ enginePaused: true });
|
||||
}
|
||||
|
||||
/*
|
||||
* FNXC:DesktopRuntime 2026-06-20-23:39:
|
||||
* Desktop local mode must start the same project engine lifecycle as CLI dashboard mode; a desktop window without engines leaves users with a live dashboard that cannot execute tasks.
|
||||
*/
|
||||
centralCore = new CentralCore();
|
||||
await centralCore.init();
|
||||
const cwdRegistered = await ensureCwdProjectRegistered({
|
||||
cwd: rootDir,
|
||||
central: centralCore,
|
||||
logPrefix: "desktop",
|
||||
autoRegister: true,
|
||||
});
|
||||
engineManager = new ProjectEngineManager(centralCore);
|
||||
await engineManager.startAll();
|
||||
engineManager.startReconciliation();
|
||||
const cwdEngine = cwdRegistered
|
||||
? await engineManager.ensureEngine(cwdRegistered.id).catch((err) => {
|
||||
console.warn(`[desktop] Failed to warm cwd project engine: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const app = createServer(store, {
|
||||
engine: cwdEngine,
|
||||
engineManager,
|
||||
centralCore,
|
||||
onProjectFirstAccessed: (projectId: string) => engineManager?.onProjectAccessed(projectId),
|
||||
});
|
||||
server = app.listen(0);
|
||||
|
||||
await Promise.race([
|
||||
once(server, "listening"),
|
||||
once(server, "error").then(([error]) => {
|
||||
throw error;
|
||||
}),
|
||||
]);
|
||||
const address = server.address() as AddressInfo | null;
|
||||
if (!address?.port) {
|
||||
throw new Error("Failed to determine dashboard server port");
|
||||
}
|
||||
|
||||
return {
|
||||
store,
|
||||
server,
|
||||
port: address.port,
|
||||
engineManager,
|
||||
centralCore,
|
||||
};
|
||||
} catch (error) {
|
||||
await engineManager.stopAll().catch(() => undefined);
|
||||
await centralCore.close?.().catch(() => undefined);
|
||||
if (server) {
|
||||
await new Promise<void>((resolve) => server?.close(() => resolve()));
|
||||
}
|
||||
await engineManager?.stopAll().catch(() => undefined);
|
||||
await centralCore?.close?.().catch(() => undefined);
|
||||
store.close();
|
||||
throw error;
|
||||
}
|
||||
|
||||
const address = server.address() as AddressInfo | null;
|
||||
if (!address?.port) {
|
||||
server.close();
|
||||
store.close();
|
||||
throw new Error("Failed to determine dashboard server port");
|
||||
}
|
||||
|
||||
return {
|
||||
store,
|
||||
server,
|
||||
port: address.port,
|
||||
engineManager,
|
||||
centralCore,
|
||||
};
|
||||
}
|
||||
|
||||
async function closeDashboardRuntime(runtime: DashboardRuntime): Promise<void> {
|
||||
|
||||
@@ -25,11 +25,10 @@ export function EngineUnavailableBanner({ isVisible }: EngineUnavailableBannerPr
|
||||
<p className="engine-unavailable-banner__body">
|
||||
<Trans
|
||||
i18nKey="app:engineUnavailable.body"
|
||||
defaults="This dashboard can display project data, but task automation will not run until you restart Fusion with the engine. Stop this server and run <sourceCmd>pnpm local</sourceCmd> from a source checkout, or <cliCmd>fn dashboard</cliCmd> from an installed CLI. On older source checkouts, use <legacyCmd>pnpm local -- --engine</legacyCmd>."
|
||||
defaults="This dashboard can display project data, but task automation will not run until you restart Fusion with the engine. Stop this server and run <sourceCmd>pnpm local</sourceCmd> from a source checkout, or <cliCmd>fn dashboard</cliCmd> from an installed CLI."
|
||||
components={{
|
||||
sourceCmd: <code />,
|
||||
cliCmd: <code />,
|
||||
legacyCmd: <code />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
@@ -742,7 +742,6 @@ describe("FN-4250 FileBrowserProvider coverage", () => {
|
||||
expect(await screen.findByText("AI engine is not running")).toBeInTheDocument();
|
||||
expect(screen.getByText("pnpm local")).toBeInTheDocument();
|
||||
expect(screen.getByText("fn dashboard")).toBeInTheDocument();
|
||||
expect(screen.getByText("pnpm local -- --engine")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show engine restart instructions when health reports an engine", async () => {
|
||||
@@ -758,6 +757,32 @@ describe("FN-4250 FileBrowserProvider coverage", () => {
|
||||
expect(screen.queryByText("AI engine is not running")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show engine restart instructions when an older health payload omits engine status", async () => {
|
||||
vi.mocked(fetchDashboardHealth).mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
version: "1.0.0",
|
||||
uptime: 1,
|
||||
database: {
|
||||
healthy: true,
|
||||
corruptionDetected: false,
|
||||
corruptionErrors: [],
|
||||
lastCheckedAt: null,
|
||||
isRunning: false,
|
||||
},
|
||||
taskIdIntegrity: { status: "ok", checkedAt: "2026-05-12T00:00:00.000Z", anomalies: [], recommendedAction: null },
|
||||
});
|
||||
mockProjectsState.loading = false;
|
||||
mockProjectsState.projects = [
|
||||
{ id: DEFAULT_PROJECT_ID, name: "Test Project", path: "/test", status: "active", isolationMode: "in-process", createdAt: "", updatedAt: "" },
|
||||
];
|
||||
mockCurrentProjectState.loading = false;
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(fetchDashboardHealth).toHaveBeenCalled());
|
||||
expect(screen.queryByText("AI engine is not running")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows engine restart instructions on mobile when health reports a UI-only dashboard", async () => {
|
||||
vi.mocked(fetchDashboardHealth).mockResolvedValueOnce({
|
||||
status: "ok",
|
||||
@@ -786,7 +811,6 @@ describe("FN-4250 FileBrowserProvider coverage", () => {
|
||||
|
||||
expect(await screen.findByText("AI engine is not running")).toBeInTheDocument();
|
||||
expect(screen.getByText("fn dashboard")).toBeInTheDocument();
|
||||
expect(screen.getByText("pnpm local -- --engine")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("FN-4779: renders app shell immediately when project data is ready", () => {
|
||||
|
||||
@@ -38,6 +38,10 @@ const mocks = vi.hoisted(() => {
|
||||
const centralCore = {
|
||||
init: vi.fn(async () => undefined),
|
||||
close: vi.fn(async () => undefined),
|
||||
getProjectByPath: vi.fn(async () => ({ id: "project-1", name: "Repo", path: "/repo", status: "active" })),
|
||||
listProjects: vi.fn(async () => []),
|
||||
registerProject: vi.fn(async ({ path, name }: { path: string; name: string }) => ({ id: "project-1", name, path, status: "initializing" })),
|
||||
updateProject: vi.fn(async (id: string, patch: Record<string, unknown>) => ({ id, name: "Repo", path: "/repo", status: patch.status ?? "active" })),
|
||||
};
|
||||
const engine = { id: "engine-1" };
|
||||
const engineMap = new Map([["project-1", engine]]);
|
||||
@@ -46,6 +50,7 @@ const mocks = vi.hoisted(() => {
|
||||
startReconciliation: vi.fn(),
|
||||
stopAll: vi.fn(async () => undefined),
|
||||
getAllEngines: vi.fn(() => engineMap),
|
||||
ensureEngine: vi.fn(async () => engine),
|
||||
onProjectAccessed: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -97,6 +102,8 @@ describe("DesktopLocalServerManager", () => {
|
||||
expect(manager.getPort()).toBe(4545);
|
||||
expect(manager.getState().status).toBe("ready");
|
||||
expect(mocks.engineManager.startAll).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.centralCore.getProjectByPath).toHaveBeenCalledWith("/repo");
|
||||
expect(mocks.engineManager.ensureEngine).toHaveBeenCalledWith("project-1");
|
||||
expect(mocks.createServer).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
@@ -130,6 +137,37 @@ describe("DesktopLocalServerManager", () => {
|
||||
expect(manager.getState()).toMatchObject({ status: "error", error: "init failed" });
|
||||
});
|
||||
|
||||
it("cleans up engine and central core when server creation fails", async () => {
|
||||
mocks.createServer.mockImplementationOnce(() => {
|
||||
throw new Error("server failed");
|
||||
});
|
||||
const { DesktopLocalServerManager } = await import("../local-server.ts");
|
||||
const manager = new DesktopLocalServerManager("/repo");
|
||||
|
||||
await expect(manager.start()).rejects.toThrow("server failed");
|
||||
|
||||
expect(mocks.engineManager.stopAll).toHaveBeenCalled();
|
||||
expect(mocks.centralCore.close).toHaveBeenCalled();
|
||||
expect(mocks.store.close).toHaveBeenCalled();
|
||||
expect(manager.getState()).toMatchObject({ status: "error", error: "server failed" });
|
||||
});
|
||||
|
||||
it("registers an active runtime-root project when no projects exist", async () => {
|
||||
mocks.centralCore.getProjectByPath.mockResolvedValueOnce(undefined);
|
||||
const { DesktopLocalServerManager } = await import("../local-server.ts");
|
||||
const manager = new DesktopLocalServerManager("/repo");
|
||||
|
||||
await manager.start();
|
||||
|
||||
expect(mocks.centralCore.registerProject).toHaveBeenCalledWith({
|
||||
path: "/repo",
|
||||
name: "repo",
|
||||
isolationMode: "in-process",
|
||||
});
|
||||
expect(mocks.centralCore.updateProject).toHaveBeenCalledWith("project-1", { status: "active" });
|
||||
expect(mocks.engineManager.ensureEngine).toHaveBeenCalledWith("project-1");
|
||||
});
|
||||
|
||||
it("returns existing runtime when start is called twice", async () => {
|
||||
const { DesktopLocalServerManager } = await import("../local-server.ts");
|
||||
const manager = new DesktopLocalServerManager("/repo");
|
||||
|
||||
@@ -32,6 +32,7 @@ const mocks = vi.hoisted(() => {
|
||||
focus: vi.fn(),
|
||||
hide: vi.fn(),
|
||||
maximize: vi.fn(),
|
||||
webContents: { reload: vi.fn() },
|
||||
};
|
||||
|
||||
const BrowserWindow = vi.fn(function () {
|
||||
@@ -386,6 +387,21 @@ describe("main process", () => {
|
||||
expect(mainDeps.saveDesktopLaunchMode).toHaveBeenCalledWith("remote");
|
||||
});
|
||||
|
||||
it("does not persist local menu mode when local runtime startup fails", async () => {
|
||||
mainDeps.startLocal.mockRejectedValueOnce(new Error("boom"));
|
||||
const { initializeApp, getCurrentDesktopLaunchMode } = await importMainModule();
|
||||
|
||||
await initializeApp();
|
||||
|
||||
const menuOptions = mainDeps.buildAppMenu.mock.calls[0]?.[0] as
|
||||
| { onStartLocalRuntime?: () => Promise<void> }
|
||||
| undefined;
|
||||
await expect(menuOptions?.onStartLocalRuntime?.()).rejects.toThrow("boom");
|
||||
|
||||
expect(mainDeps.saveDesktopLaunchMode).not.toHaveBeenCalledWith("local");
|
||||
expect(getCurrentDesktopLaunchMode()).toBe("choose");
|
||||
});
|
||||
|
||||
it("createMainWindow registers close and closed handlers", async () => {
|
||||
const { createMainWindow } = await importMainModule();
|
||||
|
||||
|
||||
26
packages/desktop/src/engine-runtime.ts
Normal file
26
packages/desktop/src/engine-runtime.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { basename } from "node:path";
|
||||
|
||||
import type { CentralCore, RegisteredProject } from "@fusion/core";
|
||||
|
||||
/*
|
||||
* FNXC:DesktopRuntime 2026-06-21-02:04:
|
||||
* Desktop local mode starts engines by default, so the embedded server should prefer the project represented by the desktop runtime root instead of whichever registered engine happens to be first. The runtime root may be a home directory, so this path must not call helpers that initialize Git repositories as a side effect.
|
||||
*/
|
||||
export async function ensureDesktopRuntimeProject(centralCore: CentralCore, rootDir: string): Promise<RegisteredProject> {
|
||||
const existing = await centralCore.getProjectByPath(rootDir);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const projects = await centralCore.listProjects();
|
||||
if (projects.length > 0) {
|
||||
return projects[0]!;
|
||||
}
|
||||
|
||||
const registered = await centralCore.registerProject({
|
||||
path: rootDir,
|
||||
name: basename(rootDir) || "Fusion Desktop",
|
||||
isolationMode: "in-process",
|
||||
});
|
||||
return centralCore.updateProject(registered.id, { status: "active" });
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { once } from "node:events";
|
||||
import type { Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
|
||||
import { ensureDesktopRuntimeProject } from "./engine-runtime.js";
|
||||
|
||||
export type RuntimeSource = "embedded-local" | "external-cli" | "none";
|
||||
export type RuntimeState = "stopped" | "starting" | "running" | "error";
|
||||
|
||||
@@ -41,7 +43,7 @@ async function createStoreDefault(rootDir: string): Promise<TaskStoreLike> {
|
||||
return new TaskStore(rootDir) as TaskStoreLike;
|
||||
}
|
||||
|
||||
async function createDashboardServerDefault(store: TaskStoreLike, _rootDir: string): Promise<{ server: Server; cleanup: RuntimeCleanup }> {
|
||||
async function createDashboardServerDefault(store: TaskStoreLike, rootDir: string): Promise<{ server: Server; cleanup: RuntimeCleanup }> {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const { createServer } = await import("@fusion/dashboard");
|
||||
const { ProjectEngineManager } = await import("@fusion/engine");
|
||||
@@ -51,25 +53,33 @@ async function createDashboardServerDefault(store: TaskStoreLike, _rootDir: stri
|
||||
* Embedded desktop local mode should be an executable Fusion node, not a dashboard-only shell. Start all registered project engines and pass the manager to the API server so project-scoped routes can start newly accessed engines.
|
||||
*/
|
||||
const centralCore = new CentralCore();
|
||||
await centralCore.init();
|
||||
const engineManager = new ProjectEngineManager(centralCore);
|
||||
await engineManager.startAll();
|
||||
engineManager.startReconciliation();
|
||||
const primaryEngine = [...engineManager.getAllEngines().values()][0];
|
||||
const app = createServer(store as never, {
|
||||
engine: primaryEngine,
|
||||
engineManager,
|
||||
centralCore,
|
||||
onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),
|
||||
});
|
||||
|
||||
return {
|
||||
server: app.listen(0),
|
||||
cleanup: async () => {
|
||||
await engineManager.stopAll();
|
||||
await centralCore.close?.();
|
||||
},
|
||||
const cleanup = async () => {
|
||||
await engineManager.stopAll();
|
||||
await centralCore.close?.();
|
||||
};
|
||||
|
||||
try {
|
||||
await centralCore.init();
|
||||
const rootProject = await ensureDesktopRuntimeProject(centralCore, rootDir);
|
||||
await engineManager.startAll();
|
||||
engineManager.startReconciliation();
|
||||
const primaryEngine = await engineManager.ensureEngine(rootProject.id);
|
||||
const app = createServer(store as never, {
|
||||
engine: primaryEngine,
|
||||
engineManager,
|
||||
centralCore,
|
||||
onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),
|
||||
});
|
||||
|
||||
return {
|
||||
server: app.listen(0),
|
||||
cleanup,
|
||||
};
|
||||
} catch (error) {
|
||||
await cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function parsePort(raw: string | undefined): number | undefined {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { once } from "node:events";
|
||||
import type { Server } from "node:http";
|
||||
|
||||
import { ensureDesktopRuntimeProject } from "./engine-runtime.js";
|
||||
|
||||
type TaskStoreLike = {
|
||||
init(): Promise<void>;
|
||||
watch(): Promise<void>;
|
||||
@@ -61,11 +64,16 @@ export class DesktopLocalServerManager {
|
||||
* This legacy desktop local server path still needs to launch project engines so every embedded desktop server follows the same executable-by-default contract.
|
||||
*/
|
||||
const centralCore = new CentralCore();
|
||||
await centralCore.init();
|
||||
const engineManager = new ProjectEngineManager(centralCore);
|
||||
cleanup = async () => {
|
||||
await engineManager.stopAll();
|
||||
await centralCore.close?.();
|
||||
};
|
||||
await centralCore.init();
|
||||
const rootProject = await ensureDesktopRuntimeProject(centralCore, this.rootDir);
|
||||
await engineManager.startAll();
|
||||
engineManager.startReconciliation();
|
||||
const primaryEngine = [...engineManager.getAllEngines().values()][0];
|
||||
const primaryEngine = await engineManager.ensureEngine(rootProject.id);
|
||||
const app = createServer(store as never, {
|
||||
engine: primaryEngine,
|
||||
engineManager,
|
||||
@@ -73,10 +81,6 @@ export class DesktopLocalServerManager {
|
||||
onProjectFirstAccessed: (projectId: string) => engineManager.onProjectAccessed(projectId),
|
||||
});
|
||||
server = app.listen(0);
|
||||
cleanup = async () => {
|
||||
await engineManager.stopAll();
|
||||
await centralCore.close?.();
|
||||
};
|
||||
|
||||
await Promise.race([
|
||||
once(server, "listening"),
|
||||
|
||||
@@ -238,12 +238,16 @@ export async function initializeApp(): Promise<void> {
|
||||
onChangeLaunchMode: async () => {
|
||||
await resetLaunchModeAndReload(createdWindow);
|
||||
},
|
||||
/*
|
||||
* FNXC:DesktopRuntimeMode 2026-06-21-02:04:
|
||||
* Menu-driven local/remote switching must only persist local mode after the embedded runtime starts successfully; shutdown stops the local server without rewriting the launch-mode preference.
|
||||
*/
|
||||
onStartLocalRuntime: async () => {
|
||||
if (!localRuntimeManager) return;
|
||||
currentRemoteLaunch = null;
|
||||
currentDesktopLaunchMode = "local";
|
||||
localRuntimeStartupAttempted = false;
|
||||
await startLocalRuntimeOnce();
|
||||
currentRemoteLaunch = null;
|
||||
currentDesktopLaunchMode = "local";
|
||||
await saveDesktopLaunchMode("local");
|
||||
createdWindow.webContents.reload();
|
||||
},
|
||||
|
||||
@@ -23,6 +23,10 @@ function runMenuAction(label: string, action: (() => Promise<void> | void) | und
|
||||
}
|
||||
|
||||
function buildConnectionSubmenu(options: AppMenuOptions): MenuItemConstructorOptions {
|
||||
/*
|
||||
* FNXC:DesktopConnectionMenu 2026-06-21-02:04:
|
||||
* Desktop users need menu-level controls to start the embedded local server, shut it down, or connect to a remote server without returning to first-run setup.
|
||||
*/
|
||||
return {
|
||||
label: "Connection",
|
||||
submenu: [
|
||||
|
||||
Reference in New Issue
Block a user