diff --git a/src/desktop-electron/app.ts b/src/desktop-electron/app.ts index 388b863..2195f8e 100644 --- a/src/desktop-electron/app.ts +++ b/src/desktop-electron/app.ts @@ -1,9 +1,57 @@ import { app } from "electron"; +import path from "node:path"; import { registerElectronDesktopBridge } from "./main"; import { resolveDesktopStartUrl } from "./startup"; import { findRepoRoot } from "../installer-core/repo"; +export interface DesktopStartupWindow { + show(): void; + focus(): void; +} + +export interface DesktopStartupApp { + dock?: { + show(): void; + }; + focus(options?: { steal?: boolean }): void; +} + +export function focusDesktopWindow(window: DesktopStartupWindow, desktopApp: DesktopStartupApp = app): void { + try { + desktopApp.dock?.show(); + } catch { + // Ignore dock activation failures on non-macOS runtimes. + } + + window.show(); + window.focus(); + + try { + desktopApp.focus({ steal: true }); + } catch { + desktopApp.focus(); + } +} + +export function shouldAutoStartDesktopApp( + argv: string[] = process.argv, + filename: string = __filename, + electronRuntime: string | undefined = process.versions?.electron, +): boolean { + if (!electronRuntime) { + return false; + } + + const entryArg = argv[1]; + if (!entryArg) { + return false; + } + + return path.resolve(entryArg) === path.resolve(filename); +} + export async function startElectronDesktopApp(): Promise { + await app.whenReady(); const repoRoot = findRepoRoot(__dirname); const startUrl = resolveDesktopStartUrl(repoRoot, process.env); const desktopBridge = await registerElectronDesktopBridge({ @@ -11,15 +59,20 @@ export async function startElectronDesktopApp(): Promise { startUrl, }); - await desktopBridge.createWindow(); + const window = await desktopBridge.createWindow(); + focusDesktopWindow(window); app.on("window-all-closed", async () => { await desktopBridge.dispose(); app.quit(); }); + + app.on("activate", () => { + focusDesktopWindow(window); + }); } -if (require.main === module) { +if (shouldAutoStartDesktopApp()) { void startElectronDesktopApp().catch((error) => { console.error("[desktop] Electron startup failed.", error); app.quit(); diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index f3a4b3c..fc5c69d 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -38,6 +38,8 @@ declare module "electron" { export class BrowserWindow { constructor(options?: BrowserWindowConstructorOptions); loadURL(url: string): Promise; + show(): void; + focus(): void; webContents: WebContents; } @@ -52,6 +54,10 @@ declare module "electron" { on(event: "window-all-closed" | "activate", listener: () => void): void; getVersion(): string; isPackaged: boolean; + focus(options?: { steal?: boolean }): void; + dock?: { + show(): void; + }; quit(): void; }; diff --git a/tests/installer/desktop-preview-startup.test.ts b/tests/installer/desktop-preview-startup.test.ts index eee5e81..e18d50b 100644 --- a/tests/installer/desktop-preview-startup.test.ts +++ b/tests/installer/desktop-preview-startup.test.ts @@ -4,6 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { installDesktopLoadDiagnostics, resolveDesktopStartUrl } from "../../src/desktop-electron/startup"; +import { focusDesktopWindow, shouldAutoStartDesktopApp } from "../../src/desktop-electron/app"; interface FakeWebContents { on( @@ -84,3 +85,37 @@ test("desktop load diagnostics present a readable fallback when renderer load fa assert.match(decoded, /file:\/\/\/broken\/index\.html/); assert.match(errors.join("\n"), /ERR_FILE_NOT_FOUND/); }); + +test("desktop startup explicitly surfaces the first Electron window", () => { + const calls: string[] = []; + const fakeWindow = { + show() { + calls.push("window.show"); + }, + focus() { + calls.push("window.focus"); + }, + }; + const fakeApp = { + dock: { + show() { + calls.push("dock.show"); + }, + }, + focus(options?: { steal?: boolean }) { + calls.push(`app.focus:${options?.steal === true ? "steal" : "default"}`); + }, + }; + + focusDesktopWindow(fakeWindow, fakeApp); + + assert.deepEqual(calls, ["dock.show", "window.show", "window.focus", "app.focus:steal"]); +}); + +test("desktop app entrypoint auto-starts only for Electron script execution", () => { + const filename = "/workspace/dist/src/desktop-electron/app.js"; + + assert.equal(shouldAutoStartDesktopApp(["/electron", filename], filename, "35.1.4"), true); + assert.equal(shouldAutoStartDesktopApp(["/node", "/workspace/test-runner.js"], filename, undefined), false); + assert.equal(shouldAutoStartDesktopApp(["/electron"], filename, "35.1.4"), false); +});