diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs index a418fb49cb5..0148b3cbafd 100644 --- a/config/electron-builder.config.cjs +++ b/config/electron-builder.config.cjs @@ -159,7 +159,7 @@ module.exports = { if (context.electronPlatformName === 'darwin') { await signMacComputerUseHelper(join(resourcesDir, 'Orca Computer Use.app'), context.packager) await signMacNotificationStatusHelper( - join(resourcesDir, 'orca-notification-status'), + join(resourcesDir, '..', 'MacOS', 'orca-notification-status'), context.packager ) } @@ -251,11 +251,16 @@ module.exports = { from: 'native/computer-use-macos/.build/release/Orca Computer Use.app', to: 'Orca Computer Use.app' }, + featureWallResources + ], + // Why: the notification-status helper must execute from Contents/MacOS — + // on macOS 26 UNUserNotificationCenter aborts (bundleProxyForCurrentProcess + // is nil) for executables launched out of Contents/Resources (#7929). + extraFiles: [ { from: 'native/notification-status-macos/.build/release/orca-notification-status', - to: 'orca-notification-status' - }, - featureWallResources + to: 'MacOS/orca-notification-status' + } ], target: [ { diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs index 46298b376df..850b8810f9b 100644 --- a/config/scripts/electron-builder-config.test.mjs +++ b/config/scripts/electron-builder-config.test.mjs @@ -63,6 +63,22 @@ describe('electron-builder config', () => { ) }) + // Why: on macOS 26 UNUserNotificationCenter aborts for executables launched + // from Contents/Resources, so the helper must ship in Contents/MacOS (#7929). + it('ships the mac notification-status helper in Contents/MacOS, not Resources', () => { + expect(electronBuilderConfig.mac.extraFiles).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + from: 'native/notification-status-macos/.build/release/orca-notification-status', + to: 'MacOS/orca-notification-status' + }) + ]) + ) + expect(electronBuilderConfig.mac.extraResources).not.toEqual( + expect.arrayContaining([expect.objectContaining({ to: 'orca-notification-status' })]) + ) + }) + it('unpacks the compiled CommonJS boundary with CLI runtime files', () => { expect(electronBuilderConfig.asarUnpack).toEqual( expect.arrayContaining(['out/package.json', 'out/cli/**', 'out/shared/**']) diff --git a/src/main/ipc/notification-authorization-status.ts b/src/main/ipc/notification-authorization-status.ts index 358d0effce9..bdaba67efae 100644 --- a/src/main/ipc/notification-authorization-status.ts +++ b/src/main/ipc/notification-authorization-status.ts @@ -12,10 +12,11 @@ let cachedHelperPath: string | null | undefined /** * Resolves the bundled notification-status helper binary. * - * Why: the helper must live inside the app bundle — NSBundle resolves the - * process's bundle by walking up from the executable path, and macOS keys - * notification records to that identity. Both dev copies and packaged builds - * place it next to the Electron executable in Contents/MacOS. + * Why: the helper must live in Contents/MacOS next to the Electron executable + * — NSBundle resolves the process's bundle by walking up from the executable + * path, macOS keys notification records to that identity, and macOS 26 aborts + * UNUserNotificationCenter for executables run from Contents/Resources + * (#7929). Dev copies and packaged builds (extraFiles) both place it there. */ function resolveHelperPath(): string | null { if (cachedHelperPath !== undefined) { @@ -25,14 +26,8 @@ function resolveHelperPath(): string | null { cachedHelperPath = null return cachedHelperPath } - // Dev copies place the helper next to the Electron executable; packaged - // builds ship it via extraResources. Both are inside the .app, which is - // what NSBundle resolution requires. - const candidates = [ - join(dirname(process.execPath), HELPER_EXECUTABLE), - ...(process.resourcesPath ? [join(process.resourcesPath, HELPER_EXECUTABLE)] : []) - ] - cachedHelperPath = candidates.find((candidate) => existsSync(candidate)) ?? null + const candidate = join(dirname(process.execPath), HELPER_EXECUTABLE) + cachedHelperPath = existsSync(candidate) ? candidate : null return cachedHelperPath }