Skip to content

feat: Add app badge count for unread tasks#1723

Merged
arnestrickmann merged 1 commit intogeneralaction:mainfrom
homebysix:ej/app-badge-count
Apr 15, 2026
Merged

feat: Add app badge count for unread tasks#1723
arnestrickmann merged 1 commit intogeneralaction:mainfrom
homebysix:ej/app-badge-count

Conversation

@homebysix
Copy link
Copy Markdown
Contributor

@homebysix homebysix commented Apr 14, 2026

Summary

Adds a native OS badge to the app icon showing how many tasks have unread activity. The count matches the sidebar's blue-dot indicators. Controlled by a new "App badge" toggle in notification settings, off by default.

Bridges the renderer's existing AgentStatusStore unread state to app.setBadgeCount() via a fire-and-forget IPC call. Adds getUnreadCount() and onUnreadCountChange() to the store, deriving the count from the existing unreadById map.

Fixes

Resolves #1722

Snapshot

image

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • This change requires a documentation update

Mandatory Tasks

  • I have self-reviewed the code

Checklist

  • I have read the contributing guide
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked if my PR needs changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works

Summary by CodeRabbit

Release Notes

  • New Features

    • Added "App badge" notification setting to enable display of unread count on the OS application badge.
    • Unread badge count automatically synchronizes across the application based on message status when the feature is enabled.
  • Tests

    • Comprehensive test coverage for unread count aggregation, listener notifications, subscription management, and badge synchronization behavior.

Show the native OS badge on the app icon with the count of tasks
that have unread activity. Controlled by a new "App badge" toggle
in notification settings, off by default.
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 14, 2026

@homebysix is attempting to deploy a commit to the General Action Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

This pull request implements an app badge count feature that displays the unread task count on the native OS dock/taskbar icon. The feature adds settings infrastructure, IPC bridge functionality, unread count tracking enhancements, and UI components to enable and synchronize the badge display across the renderer and main processes.

Changes

Cohort / File(s) Summary
IPC Bridge & Type Definitions
src/main/ipc/appIpc.ts, src/main/preload.ts, src/renderer/types/electron-api.d.ts
Added IPC listener for app:set-badge-count that forwards badge count to app.setBadgeCount() with error suppression; exposed setBadgeCount(count: number) renderer API; added TypeScript definitions for the new method.
Settings Infrastructure
src/main/settings.ts, src/renderer/components/NotificationSettingsCard.tsx
Extended AppSettings.notifications with new boolean appBadge field (default false); added UI toggle in notification settings card tied to normalized settings.
Unread Tracking & Store
src/renderer/lib/agentStatusStore.ts
Enhanced store with global unread listener management; added getUnreadCount() to aggregate unread state and onUnreadCountChange(listener) for subscription; updated setUnread to notify all registered global listeners on state changes.
Badge Synchronization
src/renderer/views/Workspace.tsx
Added useEffect that syncs OS badge count whenever unread count changes or notification settings toggle appBadge, conditional on both enabled and appBadge flags.
Test Updates
src/test/main/appIpc.openIn.test.ts, src/test/main/settings.test.ts, src/test/renderer/agentStatusStore.test.ts
Extended mocks to support IPC on handler; updated settings tests to include appBadge field; added comprehensive tests for unread count aggregation, listener triggering, and unsubscribe behavior.

Sequence Diagram

sequenceDiagram
    actor User
    participant Settings as Settings UI
    participant Store as agentStatusStore
    participant Workspace as Workspace Component
    participant IPC as electronAPI/ipcMain
    participant Main as Main Process

    User->>Settings: Toggle "App badge" setting
    Settings->>Store: updateSettings({appBadge: true})
    
    Note over Store: Task status changes<br/>(waiting/complete/error)
    Store->>Store: setUnread(taskId, true)
    Store->>Workspace: notify unread listeners
    
    Workspace->>Workspace: sync(): compute badge count<br/>from getUnreadCount()
    Workspace->>IPC: setBadgeCount(count)
    IPC->>Main: ipcMain.on('app:set-badge-count')
    Main->>Main: app.setBadgeCount(count)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A badge upon the dock now glows,
Counting tasks the app well knows,
Unread whispers, blue and bright,
Now at glance, a native sight!
With toggles blessed in settings fair,
Emdash badges everywhere! 🎫

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature: adding an app badge count to display unread tasks, which is the primary objective of this changeset.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #1722: native OS badge via app.setBadgeCount(), opt-in toggle in notification settings (off by default), unread count tracking via AgentStatusStore, and per-task clearing on mark-as-read.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the app badge count feature. The modifications span IPC communication, settings management, store updates, UI controls, and type definitions—all necessary for the feature requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/renderer/views/Workspace.tsx (1)

103-110: Skip unread subscription when app badge is disabled.

This effect still subscribes and sends 0 updates while disabled; short-circuiting avoids unnecessary IPC traffic.

♻️ Proposed refinement
   useEffect(() => {
     const enabled =
       (settings?.notifications?.enabled ?? true) && (settings?.notifications?.appBadge ?? false);
-    const sync = () =>
-      window.electronAPI.setBadgeCount(enabled ? agentStatusStore.getUnreadCount() : 0);
-    sync();
-    return agentStatusStore.onUnreadCountChange(sync);
+    if (!enabled) {
+      window.electronAPI.setBadgeCount(0);
+      return;
+    }
+    const sync = () => window.electronAPI.setBadgeCount(agentStatusStore.getUnreadCount());
+    sync();
+    return agentStatusStore.onUnreadCountChange(sync);
   }, [settings?.notifications?.enabled, settings?.notifications?.appBadge]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/views/Workspace.tsx` around lines 103 - 110, The effect
currently always subscribes and calls setBadgeCount(0) when app badge is
disabled; update the useEffect so it computes enabled from settings and
short-circuits when the appBadge is false: if not enabled, call
window.electronAPI.setBadgeCount(0) once and return without calling
agentStatusStore.onUnreadCountChange, otherwise set up the sync function and
return the subscription. Refer to useEffect, the local sync function,
agentStatusStore.onUnreadCountChange and window.electronAPI.setBadgeCount when
making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/ipc/appIpc.ts`:
- Around line 203-207: The IPC handler for 'app:set-badge-count' currently
trusts the renderer-provided count; coerce and validate that input to a finite
non-negative integer before comparing against lastBadgeCount and calling
app.setBadgeCount. Inside the ipcMain.on callback (the handler that receives the
count parameter), convert the incoming value to a number, verify
Number.isFinite, floor it (or use Math.trunc) and clamp to zero for negatives,
treat invalid/non-finite inputs as 0 (or ignore), then use this normalized value
for the dedupe check against lastBadgeCount and for the subsequent
app.setBadgeCount call (update lastBadgeCount only after successful
normalization).

---

Nitpick comments:
In `@src/renderer/views/Workspace.tsx`:
- Around line 103-110: The effect currently always subscribes and calls
setBadgeCount(0) when app badge is disabled; update the useEffect so it computes
enabled from settings and short-circuits when the appBadge is false: if not
enabled, call window.electronAPI.setBadgeCount(0) once and return without
calling agentStatusStore.onUnreadCountChange, otherwise set up the sync function
and return the subscription. Refer to useEffect, the local sync function,
agentStatusStore.onUnreadCountChange and window.electronAPI.setBadgeCount when
making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5143a901-8941-4693-ad6c-06228a822763

📥 Commits

Reviewing files that changed from the base of the PR and between d1b00e0 and 033a690.

📒 Files selected for processing (10)
  • src/main/ipc/appIpc.ts
  • src/main/preload.ts
  • src/main/settings.ts
  • src/renderer/components/NotificationSettingsCard.tsx
  • src/renderer/lib/agentStatusStore.ts
  • src/renderer/types/electron-api.d.ts
  • src/renderer/views/Workspace.tsx
  • src/test/main/appIpc.openIn.test.ts
  • src/test/main/settings.test.ts
  • src/test/renderer/agentStatusStore.test.ts

Comment thread src/main/ipc/appIpc.ts
Comment on lines +203 to +207
ipcMain.on('app:set-badge-count', (_event, count: number) => {
if (count === lastBadgeCount) return;
lastBadgeCount = count;
try {
app.setBadgeCount(count);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate and normalize badge-count IPC input in main process.

Line 203 currently trusts renderer input. Please coerce to a finite non-negative integer before dedupe/update to avoid invalid values and unstable dedupe behavior.

🔧 Proposed hardening patch
-  ipcMain.on('app:set-badge-count', (_event, count: number) => {
+  ipcMain.on('app:set-badge-count', (_event, rawCount: unknown) => {
+    const count =
+      typeof rawCount === 'number' && Number.isFinite(rawCount)
+        ? Math.max(0, Math.floor(rawCount))
+        : 0;
     if (count === lastBadgeCount) return;
     lastBadgeCount = count;
     try {
       app.setBadgeCount(count);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ipcMain.on('app:set-badge-count', (_event, count: number) => {
if (count === lastBadgeCount) return;
lastBadgeCount = count;
try {
app.setBadgeCount(count);
ipcMain.on('app:set-badge-count', (_event, rawCount: unknown) => {
const count =
typeof rawCount === 'number' && Number.isFinite(rawCount)
? Math.max(0, Math.floor(rawCount))
: 0;
if (count === lastBadgeCount) return;
lastBadgeCount = count;
try {
app.setBadgeCount(count);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/appIpc.ts` around lines 203 - 207, The IPC handler for
'app:set-badge-count' currently trusts the renderer-provided count; coerce and
validate that input to a finite non-negative integer before comparing against
lastBadgeCount and calling app.setBadgeCount. Inside the ipcMain.on callback
(the handler that receives the count parameter), convert the incoming value to a
number, verify Number.isFinite, floor it (or use Math.trunc) and clamp to zero
for negatives, treat invalid/non-finite inputs as 0 (or ignore), then use this
normalized value for the dedupe check against lastBadgeCount and for the
subsequent app.setBadgeCount call (update lastBadgeCount only after successful
normalization).

@arnestrickmann
Copy link
Copy Markdown
Contributor

Hi @homebysix,
Thanks for the PR and for your contribution. Great addition!

I replied to your issue a few minutes ago, if you haven't seen it #1722

@arnestrickmann
Copy link
Copy Markdown
Contributor

As said, we will include this in the new version soon as well. @homebysix

But also merging this here to try it out and for all the users of the current version:))

Thanks again!

@arnestrickmann arnestrickmann merged commit a9256f1 into generalaction:main Apr 15, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat]: App badge count for unread tasks

2 participants