diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index a35f125b466..be838b93067 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -13,9 +13,11 @@ import { import { makeAppSetup } from "lib/electron-app/factories/app/setup"; import { handleAuthCallback, + loadToken, parseAuthDeepLink, } from "lib/trpc/routers/auth/utils/auth-functions"; import { applyShellEnvToProcess } from "lib/trpc/routers/workspaces/utils/shell-env"; +import { env as mainEnv } from "main/env.main"; import { DEFAULT_CONFIRM_ON_QUIT, PLATFORM, @@ -358,6 +360,14 @@ if (!gotTheLock) { // before the tray initializes, so it shows accurate status immediately. await getHostServiceCoordinator().discoverAll(); + if (IS_DEV) { + getHostServiceCoordinator().enableDevReload(async () => { + const { token } = await loadToken(); + if (!token) return null; + return { authToken: token, cloudApiUrl: mainEnv.NEXT_PUBLIC_API_URL }; + }); + } + await makeAppSetup(() => MainWindow()); setupAutoUpdater(); initTray(); diff --git a/apps/desktop/src/main/lib/host-service-coordinator.ts b/apps/desktop/src/main/lib/host-service-coordinator.ts index efb0df8db4b..a39c1898dde 100644 --- a/apps/desktop/src/main/lib/host-service-coordinator.ts +++ b/apps/desktop/src/main/lib/host-service-coordinator.ts @@ -1,6 +1,7 @@ import * as childProcess from "node:child_process"; import { randomBytes } from "node:crypto"; import { EventEmitter } from "node:events"; +import * as fs from "node:fs"; import { createServer } from "node:net"; import path from "node:path"; import { settings } from "@superset/local-db"; @@ -103,6 +104,7 @@ export class HostServiceCoordinator extends EventEmitter { >(); private scriptPath = path.join(__dirname, "host-service.js"); private machineId = getHashedDeviceId(); + private devReloadWatcher: fs.FSWatcher | null = null; async start( organizationId: string, @@ -222,6 +224,92 @@ export class HostServiceCoordinator extends EventEmitter { ); } + /** + * Dev-only: watch the built host-service bundle and restart running + * instances when it changes. Gives a fast edit→reload loop for code + * under packages/host-service and src/main/host-service without + * restarting Electron. In-memory host-service state (PTYs, watchers, + * chat streams) is torn down on each reload — this is not true HMR. + */ + enableDevReload( + configProvider: () => Promise, + ): () => void { + if (this.devReloadWatcher) return () => {}; + + const scriptDir = path.dirname(this.scriptPath); + const scriptFile = path.basename(this.scriptPath); + let debounce: ReturnType | null = null; + let reloading = false; + + const waitForStableBundle = async (): Promise => { + const deadline = Date.now() + 5_000; + let lastSize = -1; + let stableSince = 0; + while (Date.now() < deadline) { + try { + const stat = fs.statSync(this.scriptPath); + if (stat.size > 0 && stat.size === lastSize) { + if (Date.now() - stableSince >= 150) return true; + } else { + lastSize = stat.size; + stableSince = Date.now(); + } + } catch { + lastSize = -1; + stableSince = 0; + } + await new Promise((r) => setTimeout(r, 50)); + } + return false; + }; + + const trigger = () => { + if (debounce) clearTimeout(debounce); + debounce = setTimeout(() => { + void (async () => { + if (reloading) return; + if (this.getActiveOrganizationIds().length === 0) return; + reloading = true; + try { + const ready = await waitForStableBundle(); + if (!ready) { + console.warn( + "[host-service] bundle did not stabilize, skipping reload", + ); + return; + } + const config = await configProvider(); + if (!config) return; + console.log( + "[host-service] bundle changed, restarting running instances", + ); + await this.restartAll(config); + } catch (error) { + console.error("[host-service] dev reload failed:", error); + } finally { + reloading = false; + } + })(); + }, 250); + }; + + try { + this.devReloadWatcher = fs.watch(scriptDir, (_event, filename) => { + if (filename && filename !== scriptFile) return; + trigger(); + }); + } catch (error) { + console.error("[host-service] failed to enable dev reload:", error); + return () => {}; + } + + return () => { + if (debounce) clearTimeout(debounce); + this.devReloadWatcher?.close(); + this.devReloadWatcher = null; + }; + } + // ── Adoption ────────────────────────────────────────────────────── private async tryAdopt(organizationId: string): Promise { diff --git a/bun.lock b/bun.lock index 7b1ddd00d7e..cde4c6c4a76 100644 --- a/bun.lock +++ b/bun.lock @@ -110,7 +110,7 @@ }, "apps/desktop": { "name": "@superset/desktop", - "version": "1.5.1", + "version": "1.5.3", "dependencies": { "@ai-sdk/anthropic": "^3.0.43", "@ai-sdk/openai": "3.0.36",