From dd5edd144f7969abcd9c76f89baad92295b116df Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Wed, 29 Jul 2026 22:50:43 +0200 Subject: [PATCH 1/3] fix(macos): launch as an agent app so no dock tile is ever created Gitify shipped without LSUIElement, so the app started as a regular dock app and relied on electron-menubar's runtime `app.dock.hide()` to transform it into an accessory app. Measured on macOS 26, that leaves a 150-3000ms window where the dock tile genuinely exists, and macOS can silently drop the transform, stranding the icon until the next launch (#3069). Declaring LSUIElement makes the process start as an accessory app, so there is no tile and no transform to lose. --- electron-builder.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/electron-builder.js b/electron-builder.js index 8e3ae5a2e..ad0b989a5 100644 --- a/electron-builder.js +++ b/electron-builder.js @@ -24,6 +24,14 @@ const config = { mac: { category: 'public.app-category.developer-tools', icon: 'assets/images/app-icon.icns', + extendInfo: { + // Launch as a macOS agent app so no dock tile is ever created. Without + // this the app starts as a regular dock app and `app.dock.hide()` has to + // transform it after the fact, leaving a ~300ms window where the icon is + // real; macOS can silently drop that transform and strand the icon for + // the whole session, which only a restart clears (#3069). + LSUIElement: true, + }, identity: 'Adam Setch (5KD23H9729)', type: 'distribution', notarize: false, // Handle notarization in afterSign.js From 335c04445404b53e59b9ab695cf6a00ff24a995d Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Wed, 29 Jul 2026 22:56:09 +0200 Subject: [PATCH 2/3] chore: remove comments --- electron-builder.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/electron-builder.js b/electron-builder.js index ad0b989a5..dc20ce5d4 100644 --- a/electron-builder.js +++ b/electron-builder.js @@ -25,11 +25,6 @@ const config = { category: 'public.app-category.developer-tools', icon: 'assets/images/app-icon.icns', extendInfo: { - // Launch as a macOS agent app so no dock tile is ever created. Without - // this the app starts as a regular dock app and `app.dock.hide()` has to - // transform it after the fact, leaving a ~300ms window where the icon is - // real; macOS can silently drop that transform and strand the icon for - // the whole session, which only a restart clears (#3069). LSUIElement: true, }, identity: 'Adam Setch (5KD23H9729)', From dfe44963fbedda2b072eb9aa64685401e49785e2 Mon Sep 17 00:00:00 2001 From: Afonso Jorge Ramos Date: Wed, 29 Jul 2026 23:09:25 +0200 Subject: [PATCH 3/3] refactor(macos): drop dialog-close dock re-hides made redundant by agent app launch --- src/main/lifecycle/first-run.test.ts | 24 +----------------------- src/main/lifecycle/first-run.ts | 4 ---- src/main/updater.test.ts | 10 ++-------- src/main/updater.ts | 9 +-------- 4 files changed, 4 insertions(+), 43 deletions(-) diff --git a/src/main/lifecycle/first-run.test.ts b/src/main/lifecycle/first-run.test.ts index cef342395..c95d09c79 100644 --- a/src/main/lifecycle/first-run.test.ts +++ b/src/main/lifecycle/first-run.test.ts @@ -19,7 +19,6 @@ vi.mock('node:fs', () => ({ const moveToApplicationsFolderMock = vi.fn(); const isInApplicationsFolderMock = vi.fn(() => false); const getPathMock = vi.fn(() => '/User/Data'); -const dockHideMock = vi.fn(); const showMessageBoxMock = vi.fn(async () => ({ response: 0, @@ -31,15 +30,7 @@ vi.mock('electron', () => ({ getPath: () => getPathMock(), isInApplicationsFolder: () => isInApplicationsFolderMock(), moveToApplicationsFolder: () => moveToApplicationsFolderMock(), - dock: { - hide: () => dockHideMock(), - }, - } satisfies Pick< - Electron.App, - 'getPath' | 'isInApplicationsFolder' | 'moveToApplicationsFolder' - > & { - dock: { hide: () => void }; - }, + } satisfies Pick, dialog: { showMessageBox: () => showMessageBoxMock(), } satisfies Pick, @@ -134,19 +125,6 @@ describe('main/lifecycle/first-run', () => { expect(moveToApplicationsFolderMock).not.toHaveBeenCalled(); }); - it('re-hides the dock icon after the prompt closes (#3069)', async () => { - existsSyncMock.mockReturnValueOnce(false); - existsSyncMock.mockReturnValueOnce(false); - showMessageBoxMock.mockResolvedValueOnce({ - response: 1, - checkboxChecked: false, - }); - - await onFirstRunMaybe(); - - expect(dockHideMock).toHaveBeenCalled(); - }); - it('does not prompt on non-macOS', async () => { mac = false; existsSyncMock.mockReturnValueOnce(false); diff --git a/src/main/lifecycle/first-run.ts b/src/main/lifecycle/first-run.ts index 570fd32a0..585a86f0f 100644 --- a/src/main/lifecycle/first-run.ts +++ b/src/main/lifecycle/first-run.ts @@ -38,10 +38,6 @@ async function promptMoveToApplicationsFolder() { message: 'Move to Applications Folder?', }); - // Parentless dialogs pull a dock-hidden (accessory) app into the Dock and - // macOS does not restore the accessory policy on close; re-hide (#3069). - app.dock?.hide(); - if (response === 0) { app.moveToApplicationsFolder(); } diff --git a/src/main/updater.test.ts b/src/main/updater.test.ts index a583d8c47..ce4586d22 100644 --- a/src/main/updater.test.ts +++ b/src/main/updater.test.ts @@ -1,4 +1,4 @@ -import { app, dialog } from 'electron'; +import { dialog } from 'electron'; import type { Menubar } from 'electron-menubar'; import { APPLICATION } from '../shared/constants'; @@ -45,11 +45,6 @@ vi.mock('electron', () => { } } return { - app: { - dock: { - hide: vi.fn(), - }, - }, dialog: { showMessageBox: vi.fn(), } satisfies Pick, @@ -145,7 +140,7 @@ describe('main/updater.ts', () => { expect(autoUpdater.quitAndInstall).toHaveBeenCalled(); }); - it('re-hides the dock icon when user dismisses the dialog (#3069)', async () => { + it('does not install when user dismisses the dialog', async () => { vi.mocked(dialog.showMessageBox).mockResolvedValue({ response: 1, // "Later" button index checkboxChecked: false, @@ -159,7 +154,6 @@ describe('main/updater.ts', () => { await Promise.resolve(); expect(autoUpdater.quitAndInstall).not.toHaveBeenCalled(); - expect(app.dock?.hide).toHaveBeenCalled(); }); }); diff --git a/src/main/updater.ts b/src/main/updater.ts index a0c27a180..214014d84 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -1,4 +1,4 @@ -import { app, dialog, type MessageBoxOptions } from 'electron'; +import { dialog, type MessageBoxOptions } from 'electron'; import type { Menubar } from 'electron-menubar'; import { autoUpdater } from 'electron-updater'; @@ -200,14 +200,7 @@ export default class AppUpdater { dialog.showMessageBox(dialogOpts).then((returnValue) => { if (returnValue.response === 0) { autoUpdater.quitAndInstall(); - return; } - - // Presenting a parentless dialog forces a dock-hidden (accessory) macOS - // app to activate into the Dock, and the accessory policy is not - // restored when the dialog closes. Re-assert it so the dock icon - // disappears again (#3069). - app.dock?.hide(); }); } }