Skip to content

feat: post agent alerts to the system's notification center (macOS) - #634

Open
abhijit-s wants to merge 5 commits into
herdrdev:masterfrom
abhijit-s:feat/system-notifications-upstream
Open

abhijit-s wants to merge 5 commits into
herdrdev:masterfrom
abhijit-s:feat/system-notifications-upstream

Conversation

@abhijit-s

Copy link
Copy Markdown
Contributor

TL;DR: closes #120. An agent that needs you, hit an error or finished also reaches macOS Notification Center, not only the in-app toast. A system_notifications setting picks when: background (default; only while Herdr isn't the key window), always or off. Clicking a notification brings Herdr forward and focuses that agent. My fork has carried this since 2026-11-03.

Why

What changes

  • Alerts: the three toast alerts also post a notification through GPUI's show_system_notification: " needs you", " hit an error", " finished".
  • Body: the agent's own 7501 msg when it sent one (for example "approve Bash: make"), above "space / tab".
  • One per agent: each notification is tagged herdr-agent:<address>, so a newer alert for the same agent replaces the older one. It is dismissed once you look at the agent, it starts working again, or it goes away while Herdr runs.
  • Never while you're looking: no notification for the agent you're looking at (alert::should_notify).
  • Click: activates Herdr and runs agent.focus for that agent, as clicking the toast does.
  • Dock: dock_bounce: true bounces the Dock icon for "needs you" while Herdr is in the background (request_attention). Off by default.
  • error is its own alert: a status of error alerts like "needs you" does, so a stopped agent is told apart from one waiting on a question.
  • Test notification: Settings > Alerts has "Send a test notification" (the app.test_notification command). It posts one right away, whatever system_notifications says, so you can see how alerts look and, on macOS, answer the permission question while you're there.
  • Clicks that start Herdr: a click on a notification from an earlier run is kept until the window opens and that agent is listed, then goes to it. If the agent is gone by then, nothing happens.
  • Leftovers: when Herdr starts, notifications an earlier run left for live agents go away, except for an agent still blocked or in error.
  • Settings window: under Alerts, a "System notifications" choice, a "Send a test notification" button, and on macOS a "Bounce the Dock icon" toggle.
  • Dev builds stay silent: the vendored GPUI posts nothing unless the app runs from a bundle (its NSInternalInconsistencyException guard).
  • Files: src/alert.rs (new), src/model.rs, src/window.rs, src/settings.rs, src/main.rs, src/commands.rs, defaults/frame/settings/main.js, defaults/AGENTS.md, docs/commands.md.

Choices worth a look

  • app.test_notification ignores system_notifications, so the button works even when alerts are off. That also means a script or agent can post one with alerts off. If you'd rather not expose that, Settings could post it directly instead of through a command.
  • The first-notification drop is GPUI's: its macOS backend posts without waiting for the permission answer (vendor/gpui-pre-macos/src/system_notifications.rs). The test button is a workaround on Herdr's side; the real fix belongs in GPUI.

Not in this pull request

  • alert_command (run a program instead of the notification, with HERDR_AGENT_* in its environment, like the herdr TUI's and tuios's notify adapters): my fork has it, but it's a separate surface, so I'd send it as its own PR if you want it.
  • Linux and Windows (asked for in macOS: add native Notification Center alerts when agents finish or need input #120): tested on macOS only. On Linux, GPUI posts through XDG notifications, so it may work, but I haven't tried it. On Windows, GPUI needs an app identity (set_app_identity) that Herdr doesn't set yet, so an unpackaged build likely posts nothing.
  • Dock badge (suggested by @shellscape): not included; a natural follow-up using the same "seen" state.

Tests

  • Unit: alert::tests::notifies_only_when_away_unless_always, alert::tests::the_notification_says_what_the_toast_says_tagged_by_agent, alert::tests::the_agents_own_message_goes_over_the_place, alert::tests::an_error_is_told_apart_from_blocked, settings::tests::system_notifications_parse_and_stay_out_of_settings_json_by_default, settings::tests::the_guide_explains_every_setting (both new keys documented), model::tests::alerts_follow_herdr (error cases added).
  • Window: window::tests::agent_alerts_reach_the_notification_center (runs window::tests::system_notifications in its own process) drives the core's listing through GPUI's test platform:
    • nothing posts while Herdr is in front in background, nothing when off, nothing for the agent on screen;
    • the tag, title and msg-over-"space / tab" body;
    • dismissal when the agent works again or goes away;
    • one notification per agent, and error with its own title;
    • a click goes to the agent, including a click made before the window opened;
    • a click on an agent that's gone does nothing;
    • the first listing takes down leftovers but keeps a waiting agent's;
    • app.test_notification posts whatever the setting says.
  • just check passes on this branch (bun 41/41, cargo 332 passed, 2 ignored). Clippy adds no warnings in the code this branch changes.
  • Permission: Herdr.app never asked because it never posted a notification. With this branch, GPUI's macOS backend calls UNUserNotificationCenter.requestAuthorization (alert and sound) the first time Herdr posts one (vendor/gpui-pre-macos/src/system_notifications.rs, show → request_authorization), and Herdr then appears in System Settings → Notifications. GPUI posts that first notification without waiting for the answer, so the alert that brings up the question may not show. "Send a test notification" in Settings > Alerts lets you answer macOS while you're there, before any agent's alert depends on it. There's no prompt with system_notifications: "off" until you press the button, and none from a build not run as an app bundle.

Alerts only showed as toasts inside Herdr, so with Herdr behind other apps or on another Space an agent could finish or wait for approval unnoticed (herdrdev#120).

An agent that needs you, hit an error or finished now also posts a system notification through GPUI's show_system_notification, titled as the toast, with the agent's own 7501 msg (when it gave one) above "space / tab" as the body. Each is tagged herdr-agent:<address>, so a newer alert for the same agent replaces the older one, and it is dismissed once the agent is looked at, starts working again or goes away. Clicking one activates Herdr and runs agent.focus, as clicking the toast does.

system_notifications in settings.json says when: background (default, only while Herdr's window isn't key), always, or off; never for the agent on screen. dock_bounce: true bounces the Dock icon for an agent that needs you while Herdr is in the background. Both are in Settings > Alerts and documented in defaults/AGENTS.md.

A status of error is its own alert (model::Alert::Error, "<name> hit an error"), rung, bounced and toasted as "needs you" is, so a stopped agent reads differently from one waiting on a question.

The settings mod's appearance picker becomes a general choicePicker so the new choice reuses it.
…ers swept

macOS asks for notification permission the first time Herdr posts one, and GPUI posts it before the answer comes back, so the first agent alert was likely lost to the question. Settings > Alerts gets "Send a test notification" (the new app.test_notification command), which posts one with its own herdr-test tag whatever system_notifications says, so the person answers macOS while they are there (herdrdev#120 asked for this button).

A click on a notification could start Herdr, or come before the core listed that agent, and was then lost or failed as "no running terminal". The click handler is now set up in main before any window opens (a click without the window waits in a global for it), and an agent not listed yet is shown at the next listing that has agents, or dropped silently if it is gone by then. An empty listing (no core yet, or nothing running) decides neither.

Notifications are dismissed by tag, so the first listing also takes down what an earlier run left for live agents, except those still blocked or in error, which stay until looked at as usual. A run can't list what is delivered, so one left for an agent that ended while Herdr was closed stays in Notification Center.

alert() asks the window whether it is key (is_window_active) instead of the 400 ms-polled window_active, should_notify loses its looking parameter (alert() already returns early for an agent on screen), the Dock bounce row shows only on macOS, and the settings.json test moves to settings.rs beside the others.
@guillaume86

Copy link
Copy Markdown
Contributor

Tried this on Windows 11, merged in my fork with my patch for #611 (feat/windows-attention):

  • Notifications post once the app has an AppUserModelID. With HERDR_APP_ID set, set_app_identity runs at startup, and a blocked agent produced "Claude Code needs you", with the agent's message and "space / tab" as the body, under the app's name. Without an identity I'd expect nothing, as you wrote, but I haven't tried that.
  • dock_bounce already works on Windows: request_attention blinks the taskbar button once and leaves it orange until the window is focused, as Teams does. Only the Settings toggle is macOS-only; bc06487 shows it on Windows as "Highlight the taskbar button".

Not tried: clicking a notification, and the leftovers sweep at startup.

@abhijit-s

Copy link
Copy Markdown
Contributor Author

Thanks @guillaume86, that's the first Windows run of this.

@abhijit-s

Copy link
Copy Markdown
Contributor Author

@ogulcancelik since #364 (b721ded), a layout can name its own notifications mod, and that mod gets Herdr's own alerts as well as every notify(). Mods also see the window's focus (#350). So most of this PR could live in a mod instead: when to post (background, always, off), one notification per agent, and the Settings rows.

What a mod still can't do is post a native notification as Herdr. exec("osascript", ...) posts as Script Editor, can't replace or withdraw an earlier notification, and a click on it can't reach Herdr to focus the agent. That part is what #312 asks for.

So, whichever you prefer:

  • Close this PR, and I'll keep the alert rules in my own toast mod.
  • Shrink it to the part a mod can't do: a mod API that posts a system notification with a tag (a newer one with the same tag replaces it), withdraws it by tag, and runs a command such as agent.focus when it's clicked. No settings keys and no alert policy in the app.

No rush, and either is fine with me.

guillaume86 added a commit to guillaume86/herdr-app that referenced this pull request Nov 13, 2026
setBadge (herdrdev#519) was a no-op off macOS. On Windows it now draws the label
as a red disc (GDI text as a coverage mask, centred on its ink, shrunk to
fit) and sets it as the main window's taskbar overlay
(ITaskbarList3::SetOverlayIcon), like Teams and Outlook. Only a changed
label reaches the taskbar, so a mod can call it on every render; the
TaskbarButtonCreated message (window subclass) puts it back when the
button is created late or Explorer restarts. A minimized window doesn't
render, so the docs tell mods to call it from a timer too.

dock_bounce (herdrdev#634) already calls request_attention on every system; on
Windows that leaves the taskbar button orange until the window is
focused. Settings > Alerts now shows the toggle there too, as
"Highlight the taskbar button".

The windows crate is a direct dependency at the version and features
gpui-pre-windows already builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011KbiYtTeMm6Nixmi99Vw8S
…ications-upstream

# Conflicts:
#	src/settings.rs
@shellscape

Copy link
Copy Markdown
Contributor

@ogulcancelik please consider adopting the "Shrink it to the part a mod can't do" option. More than reasonable for mods to tackle this, if we can have mods access the necessary bits they currently cannot.

…ications-upstream

# Conflicts:
#	src/main.rs

This branch has not been deployed

No deployments
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.

macOS: add native Notification Center alerts when agents finish or need input

4 participants