diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b64c134b46..91772e4a33 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -111,6 +111,7 @@ ], "category": "public.app-category.developer-tools", "extendInfo": { + "LSUIElement": true, "CFBundleAllowMixedLocalizations": true, "CFBundleDevelopmentRegion": "en", "CFBundleLocalizations": [ diff --git a/apps/desktop/scripts/verify-workdsh-carrier.ts b/apps/desktop/scripts/verify-workdsh-carrier.ts index 627c899496..711e404159 100644 --- a/apps/desktop/scripts/verify-workdsh-carrier.ts +++ b/apps/desktop/scripts/verify-workdsh-carrier.ts @@ -20,6 +20,11 @@ export function normalizeAsarEntry(entry: string): string { } export async function afterPack(context: PackContext): Promise { + if (context.electronPlatformName === 'darwin') { + const plist = join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents', 'Info.plist') + const nativeRole = spawnSync('/usr/libexec/PlistBuddy', ['-c', 'Print :LSUIElement', plist], { encoding: 'utf8' }) + if (nativeRole.status !== 0 || nativeRole.stdout.trim() !== 'true') throw new Error('macOS carrier must start as a native UIElement before browser worker JavaScript runs') + } const resources = context.electronPlatformName === 'darwin' ? join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents', 'Resources') : join(context.appOutDir, 'resources') diff --git a/apps/desktop/src/workdsh-main.ts b/apps/desktop/src/workdsh-main.ts index d33ce8845a..1e5c3b19bb 100644 --- a/apps/desktop/src/workdsh-main.ts +++ b/apps/desktop/src/workdsh-main.ts @@ -451,6 +451,9 @@ const worker = browserWorkerRequest() if (worker !== undefined) { startBrowserWorker(worker) } else { +// The native bundle starts without a Dock icon so spawned browser workers cannot +// flash one before JavaScript runs. Only the primary application becomes foreground. +if (process.platform === 'darwin') app.setActivationPolicy('regular') const desktopUserData = process.env.WORKDSH_DESKTOP_USER_DATA if (desktopUserData) { if (!isAbsolute(desktopUserData)) throw new Error('Desktop user data override must be absolute') diff --git a/apps/desktop/tests/connection-lifecycle.spec.ts b/apps/desktop/tests/connection-lifecycle.spec.ts index 9fdc994dca..af69ae313c 100644 --- a/apps/desktop/tests/connection-lifecycle.spec.ts +++ b/apps/desktop/tests/connection-lifecycle.spec.ts @@ -30,7 +30,7 @@ vi.mock('electron', async () => { return { BrowserWindow: Window, ipcMain: { handle: (name: string, fn: any) => { state.handlers[name] = fn } }, - app: Object.assign(new EventEmitter(), { setName: vi.fn(), requestSingleInstanceLock: () => true, whenReady: async () => {}, quit: vi.fn(), relaunch: vi.fn(), getPath: () => state.userData }), + app: Object.assign(new EventEmitter(), { setActivationPolicy: vi.fn(), setName: vi.fn(), requestSingleInstanceLock: () => true, whenReady: async () => {}, quit: vi.fn(), relaunch: vi.fn(), getPath: () => state.userData }), shell: { openExternal: vi.fn() }, dialog: { showErrorBox: vi.fn(), showOpenDialog: vi.fn(async () => ({ canceled: true, filePaths: [] })) }, Menu: { buildFromTemplate: (menu: any[]) => { state.menu = menu; return menu }, setApplicationMenu: vi.fn() }, } @@ -73,6 +73,9 @@ it('enforces the packaged backend, uses explicitly installed enterprise identity }) try { await import('../src/workdsh-main.ts') + const { app: startupApp } = await import('electron') + if (process.platform === 'darwin') expect(startupApp.setActivationPolicy).toHaveBeenCalledWith('regular') + else expect(startupApp.setActivationPolicy).not.toHaveBeenCalled() await vi.waitFor(() => expect(state.windows).toHaveLength(1)) const entry = state.windows[0] expect(decodeURIComponent(entry.url)).toContain('

https://company.test

')