Skip to content

feat: list notifications and act on one through Notification Center's custom actions - #74

Merged
iamnbutler merged 1 commit into
mainfrom
feat/notifications
Oct 17, 2026
Merged

iamnbutler merged 1 commit into
mainfrom
feat/notifications

Conversation

@iamnbutler

Copy link
Copy Markdown
Contributor

Adds desktop_notifications (read) and desktop_notification_action (act). There are now 44 tools.

Built against the known macOS 11–15 Notification Center structure. The macOS 27 structure is unverified. The screen was locked for this whole PR, so no AX survey or live check has run. Both are listed in #42. If 27 is structured differently, we'll adjust then.

Model assumed (macOS 11–15)

  • Banners and alerts are windows of NotificationCenter (com.apple.notificationcenterui), and so is the open panel.
  • Entries advertise AXCustomActions as Name:<name>\nTarget:…\nSelector:… descriptors: Close, Show, Options, and the app's own actions.
  • Group stacks expand on AXPress.
  • Subroles are AXNotificationCenter*.

Peekaboo: peekaboo-notification-actions.patch (last in the stack)

  • (a) Listing. DetachedAXObservationWorker sets attributes.customActions to the custom action names, newline-joined. It does this only when the traversed process's executable is exactly /System/Library/CoreServices/NotificationCenter.app/Contents/MacOS/NotificationCenter (proc_pidpath). Bounds:
    • at most 8 names
    • each at most 64 characters
    • no control characters
    • a name advertised twice is never listed
  • (b) Performing. performActionWithOutcome accepts AXCustomAction:<name> and runs element-action's usual checks: snapshot, exact snapshot element, process generation, and the mutation target. It also refuses before dispatch unless both of these hold:
    • the receipt process is NotificationCenter, through an injectable customActionDescriptors seam
    • the element lists exactly that name
      It performs the one matching descriptor and reports the requested name as actionName, because the Bridge .performAction receipt requires result.actionName == expected.
  • No Bridge operation is added, so the inventory counts and tests are unchanged.
  • Fixture tests (NotificationCenterCustomActionsTests): parsing, bounds, duplicates, the reserved form, isValidActionName, the non-NC process gate, performing the listed descriptor, and refusing before dispatch outside NC or for an unlisted or ambiguous name. Run with a narrow filter together with ActionInputDriverTests, whose fixture now records action names: 93 tests passed.

Client

  • Native actions.swift: element-action also accepts AXCustomAction:<name>, but only when the snapshot element's customActions lists that name.
  • src/notifications.ts, the read side:
    • Query NC by bundle ID, then inspect each of its on-screen windows (at most 8) in accessibility mode.
    • Entries are elements with customActions or an AXNotificationCenter* subrole.
    • Each entry has: id = snapshot/element, kind, app, title, subtitle, body, time, description, raw texts, stack, and actions.
    • Fields come from text identifiers when present (fields_from: identifiers); otherwise from text order plus the app name at the start of the description (fields_from: order).
    • system_alerts lists UserNotificationCenter and SecurityAgent windows for desktop_dialog. They are never inspected or acted on.
  • The action:
    • open is the element click.
    • close runs AXCustomAction:Close.
    • Any other name runs AXCustomAction:<name>.
    • A stale or missing snapshot or element is refused as "Notification gone; read again with desktop_notifications."
    • After the action, notifications are read again. The result is completed only when the entry (same window, kind, texts and description as in the last read) is gone; otherwise it is unknown.
    • It does not read again before acting, because a new inspection would replace the snapshot the id names.
  • Tests: test/notifications.test.mjs with fixtures.
  • Docs: docs/tools.md and the patch README.

Checks

  • npm run check and npm test (14) pass. Native and the signed dev app build.
  • While locked, the dev app runs desktop_notifications. It finds NC (pid 1299) and its 3 widget windows, which report capture-unavailable errors, and it lists the 5 UserNotificationCenter alerts under system_alerts without touching them.
  • Live checks are pending (screen locked) and listed in Run the live checks deferred while the screen was locked #42.

@iamnbutler
iamnbutler merged commit 5f17583 into main Oct 17, 2026
2 checks passed
@iamnbutler
iamnbutler deleted the feat/notifications branch October 17, 2026 05:29
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.

1 participant