From becedda8bcf04af061d43153c1de9ef66520ccad Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 31 May 2026 07:19:31 -0700 Subject: [PATCH] perf: move unread dock badge cleanup to app root --- src/renderer/src/App.tsx | 7 +-- .../src/hooks/useUnreadDockBadge.test.ts | 45 +++++++++++++++++++ src/renderer/src/hooks/useUnreadDockBadge.ts | 22 +++++---- 3 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 src/renderer/src/hooks/useUnreadDockBadge.test.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index fa2791f3970..e65ab274a4a 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -262,7 +262,7 @@ function applyRemoteWorkspacePatchStatus( } function App(): React.JSX.Element { - useUnreadDockBadge() + const clearUnreadDockBadge = useUnreadDockBadge() useRadixBodyPointerEventsRecovery() useWebSessionTabsSync() const [floatingTerminalOpen, setFloatingTerminalOpen] = useState(false) @@ -374,12 +374,13 @@ function App(): React.JSX.Element { const setAppRootNode = useCallback( (node: HTMLDivElement | null): void => { - // Why: return-focus frames are only valid while the App root is mounted. + // Why: these best-effort App chrome cleanups share the App root lifetime. if (!node) { cancelFloatingTerminalReturnFocusFrame() + clearUnreadDockBadge() } }, - [cancelFloatingTerminalReturnFocusFrame] + [cancelFloatingTerminalReturnFocusFrame, clearUnreadDockBadge] ) const rememberFloatingTerminalReturnFocus = useCallback((): void => { diff --git a/src/renderer/src/hooks/useUnreadDockBadge.test.ts b/src/renderer/src/hooks/useUnreadDockBadge.test.ts new file mode 100644 index 00000000000..0129741468a --- /dev/null +++ b/src/renderer/src/hooks/useUnreadDockBadge.test.ts @@ -0,0 +1,45 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/unread-badge-count', () => ({ + getUnreadBadgeCount: vi.fn() +})) + +vi.mock('@/store', () => ({ + useAppStore: vi.fn() +})) + +import { clearUnreadDockBadgeCount } from './useUnreadDockBadge' + +describe('clearUnreadDockBadgeCount', () => { + let setUnreadDockBadgeCount: ReturnType + + beforeEach(() => { + setUnreadDockBadgeCount = vi.fn().mockResolvedValue(undefined) + vi.stubGlobal('window', { + api: { + app: { + setUnreadDockBadgeCount + } + } + }) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('clears the app badge', () => { + clearUnreadDockBadgeCount() + + expect(setUnreadDockBadgeCount).toHaveBeenCalledWith(0) + }) + + it('treats badge clearing as best-effort', async () => { + setUnreadDockBadgeCount.mockRejectedValueOnce(new Error('dock unavailable')) + + clearUnreadDockBadgeCount() + await Promise.resolve() + + expect(setUnreadDockBadgeCount).toHaveBeenCalledWith(0) + }) +}) diff --git a/src/renderer/src/hooks/useUnreadDockBadge.ts b/src/renderer/src/hooks/useUnreadDockBadge.ts index 1f6674a9824..25925708365 100644 --- a/src/renderer/src/hooks/useUnreadDockBadge.ts +++ b/src/renderer/src/hooks/useUnreadDockBadge.ts @@ -2,7 +2,17 @@ import { useEffect } from 'react' import { getUnreadBadgeCount } from '@/lib/unread-badge-count' import { useAppStore } from '@/store' -export function useUnreadDockBadge(): void { +function setUnreadDockBadgeCountBestEffort(count: number): void { + void window.api.app.setUnreadDockBadgeCount(count).catch(() => { + // Dock sync is best-effort chrome; stale badge state should not affect app use. + }) +} + +export function clearUnreadDockBadgeCount(): void { + setUnreadDockBadgeCountBestEffort(0) +} + +export function useUnreadDockBadge(): typeof clearUnreadDockBadgeCount { const unreadCount = useAppStore((state) => getUnreadBadgeCount({ worktreesByRepo: state.worktreesByRepo, @@ -12,14 +22,8 @@ export function useUnreadDockBadge(): void { ) useEffect(() => { - void window.api.app.setUnreadDockBadgeCount(unreadCount).catch(() => { - // Dock sync is best-effort chrome; stale badge state should not affect app use. - }) + setUnreadDockBadgeCountBestEffort(unreadCount) }, [unreadCount]) - useEffect(() => { - return () => { - void window.api.app.setUnreadDockBadgeCount(0).catch(() => {}) - } - }, []) + return clearUnreadDockBadgeCount }