feat: show notification badge on app icon#2645
Conversation
Move palette notification lookup and badge count logic into the task notifications store so both the command palette and app badge sync use the same source of truth.
Greptile SummaryThis PR adds a native app icon badge that reflects the current task notification count, synced reactively from the renderer via MobX and IPC. It extracts notification logic into a shared
Confidence Score: 5/5Safe to merge — the badge sync is additive and non-destructive, the main-process service is well-isolated, and the shared notification logic is used consistently by both the badge and the command palette. The change introduces a new reactive side-effect (badge sync) and extracts existing notification logic into a shared module. The deduplication concern raised in the prior review has been resolved by removing the force option entirely. Badge count and palette items use the same granularity (conversation-level for the active task, task-level for all others) so they stay in sync. Startup/quit badge lifecycle is handled correctly via initialize() and the before-quit finally block before app.exit(0). No files require special attention.
|
| Filename | Overview |
|---|---|
| apps/emdash-desktop/src/renderer/features/tasks/stores/task-notifications.ts | Extracted notification logic shared by badge and palette; badge and palette use the same granularity (conversation-level for current task, task-level otherwise) so they stay in sync. |
| apps/emdash-desktop/src/renderer/app/notification-badge-sync.tsx | Null-rendering component that wires a MobX reaction to the IPC badge call; reaction lifecycle is correctly tied to component mount/unmount via useEffect. |
| apps/emdash-desktop/src/main/core/app/app-badge-service.ts | New service wrapping app.setBadgeCount with deduplication; clean and straightforward, previous concerns about a dead force option have been resolved. |
| apps/emdash-desktop/src/main/core/app/controller.ts | Adds setNotificationBadgeCount IPC handler; types are inferred from the router shape, no separate type definition needed. |
| apps/emdash-desktop/src/main/index.ts | Badge service initialized on app ready and cleared inside the before-quit finally block before app.exit(0), ensuring the badge is cleared at shutdown. |
| apps/emdash-desktop/src/renderer/App.tsx | Mounts NotificationBadgeSync inside the auth-gated workspace tree, so the badge is only synced when the user is logged in. |
| apps/emdash-desktop/src/renderer/features/command-palette/palette-notifications-group.tsx | Palette now delegates to the shared getTaskNotificationItems helper, removing duplicated traversal logic. |
| apps/emdash-desktop/src/main/core/tasks/task-service.ts | Formatting-only change adding braces to a single-line if; no logic change. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Nav as Navigation State (MobX)
participant Sync as NotificationBadgeSync
participant Store as task-notifications.ts
participant IPC as rpc.app
participant Badge as AppBadgeService
participant OS as app.setBadgeCount
Note over Sync: useEffect mounts reaction (fireImmediately: true)
Nav->>Sync: observable change (currentViewId / taskId)
Sync->>Store: getVisibleTaskNotificationCount(projectId, taskId)
Store->>Store: "iterate projects/tasks, skip archived,<br/>current task = conversation-level count,<br/>other tasks = +1 per task"
Store-->>Sync: count (number)
Sync->>IPC: setNotificationBadgeCount(count)
IPC->>Badge: setVisibleNotificationCount(count)
Badge->>Badge: "setCount - deduplicate<br/>(skip if count === unreadCount)"
Badge->>OS: app.setBadgeCount(count)
Note over Badge: On before-quit: appBadgeService.clear() then app.exit(0)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Nav as Navigation State (MobX)
participant Sync as NotificationBadgeSync
participant Store as task-notifications.ts
participant IPC as rpc.app
participant Badge as AppBadgeService
participant OS as app.setBadgeCount
Note over Sync: useEffect mounts reaction (fireImmediately: true)
Nav->>Sync: observable change (currentViewId / taskId)
Sync->>Store: getVisibleTaskNotificationCount(projectId, taskId)
Store->>Store: "iterate projects/tasks, skip archived,<br/>current task = conversation-level count,<br/>other tasks = +1 per task"
Store-->>Sync: count (number)
Sync->>IPC: setNotificationBadgeCount(count)
IPC->>Badge: setVisibleNotificationCount(count)
Badge->>Badge: "setCount - deduplicate<br/>(skip if count === unreadCount)"
Badge->>OS: app.setBadgeCount(count)
Note over Badge: On before-quit: appBadgeService.clear() then app.exit(0)
Reviews (2): Last reviewed commit: "fix(app): align notification badge count" | Re-trigger Greptile
Description
Screenshot/Recording (if applicable)
https://streamable.com/fx54bk
Checklist
messages and, when possible, the PR title