Skip to content

feat(notifications): quota pace alerts — 3 triggers, launch-prime, per-app stacking (#633)#786

Merged
robinebers merged 10 commits into
mainfrom
cursor/fix-quota-notifications-775
Jun 28, 2026
Merged

feat(notifications): quota pace alerts — 3 triggers, launch-prime, per-app stacking (#633)#786
robinebers merged 10 commits into
mainfrom
cursor/fix-quota-notifications-775

Conversation

@robinebers

@robinebers robinebers commented Jun 28, 2026

Copy link
Copy Markdown
Owner

TL;DR

Adds opt-out macOS notifications that alert when a quota's pace worsens — three triggers (Almost Out / Cutting It Close / Will Run Out), deduped once per metric per reset period, primed at launch so an already-bad quota doesn't spam on open, and grouped into one stacked thread. Supersedes #775.

What was happening

  • OpenUsage surfaced pace only while the popover was open; a quota could drift to nearly exhausted with no signal unless you looked.
  • The first cut (feat(notifications): macOS alerts when quota pace worsens (#633) #775) fired on cold start, so launching into an already-bad quota posted two alerts at once, and the under-10% check inverted when the meter was set to "show used".

What this changes

  • Three triggers, no master toggle — Almost Out (<10% remaining), Cutting It Close (projected near the limit), Will Run Out (projected to exhaust before reset). Each row has an (i) tooltip; turn all three off to silence.
  • Notification copy — title = trigger name, subtitle = provider + metric, body = the plain-language verdict.
  • Permission UX — warning triangle on the Notifications header + a conditional "Open System Settings" (denied) / "Allow Notifications" (not-determined) button under the toggles; authorization is re-checked on NSApplication.didBecomeActiveNotification so it clears when the user returns from System Settings.
  • Cold-start prime — each metric's first real observation is recorded as a baseline without firing, so a quota already in a bad state at launch no longer posts alerts on open; only worsening during the session fires.
  • Per-app threadIdentifier so simultaneous alerts group in Notification Center.
  • Bug fix — the under-10% check uses a display-mode-independent remaining fraction (the old data.fraction meant "used/limit" when the meter showed "used", inverting the alert).
  • DEBUG-only "Fire" buttons in Settings to trigger each notification (and all three) on demand for verifying copy/stacking/permission.

Heads-up

  • All three defaults are ON, so the app prompts for notification permission on first launch.
  • macOS shows on-screen banners one at a time per app; threadIdentifier groups them in Notification Center (not as simultaneous on-screen banners) — this is a platform limitation, not a bug.
  • The DEBUG "Fire" buttons are #if DEBUG only and not compiled into release builds; temporary.
  • On by default is an owner decision; authorization is requested once at first launch.

Tests

  • PaceNotificationLogicTests (15), WidgetDataStoreNotificationTests (6), AppNotificationsTests (2).
  • Added a regression for the .used display-mode under-10% case and rewrote the cold-start test to assert priming (no fire) + a later worsening firing.
  • Full suite: 514 pass (1 pre-existing live-Claude skip). swift build clean.

Screenshots

image

Made with Cursor

robinebers and others added 3 commits June 27, 2026 12:11
Add macOS user notifications that fire when a quota's pace gets worse, at
three milestones, deduped once per metric per reset period:
- first time under 10% remaining,
- pace healthy (blue) -> close-to-limit (yellow),
- pace close-to-limit (yellow) -> running-out (red).

Master toggle and all three triggers default ON; authorization is requested
at first launch. Evaluation runs every refresh tick so pace worsening from
elapsed time alone still alerts with the popover closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the quota-pace notifications work (#633):

- Drop the master "Quota Notifications" toggle; keep the three triggers,
  renamed to Almost Out / Cutting It Close / Will Run Out, each with an
  (i) tooltip. Notification copy now uses title (trigger name), subtitle
  (provider + metric), and body (the verdict).
- Permission UX: warning triangle on the Notifications header plus a
  conditional action row under the toggles — "Open System Settings" when
  macOS denied, "Allow Notifications" when still undecided. Authorization
  is re-checked on NSApplication.didBecomeActiveNotification so it clears
  when the user returns from System Settings.
- Fix the under-10% check to use a display-mode-independent remaining
  fraction; the old data.fraction meant "used/limit" when the meter
  showed "used", inverting the alert.
- Cold-start prime: record each metric's first real observation as a
  baseline without firing, so a quota already in a bad state at launch
  no longer posts two alerts on open; only worsening during the session
  fires.
- Per-app threadIdentifier so simultaneous alerts stack into one banner.

514 tests pass (1 pre-existing skip).

Co-authored-by: Cursor <cursoragent@cursor.com>
Temporary #if DEBUG section in Settings with a "Fire" button per quota
trigger (Almost Out / Cutting It Close / Will Run Out) plus "All Three",
so the banner title/subtitle/body, stacking, and permission flow can be
verified on demand without waiting for a real worsening. Posts via
AppNotifications.shared.post, bypassing the dedup/prime logic. DEBUG-only
— not compiled into release builds.

Co-authored-by: Cursor <cursoragent@cursor.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Comment thread Sources/OpenUsage/Support/AppNotifications.swift Outdated
robinebers and others added 2 commits June 28, 2026 13:28
ensureAuthorization memoizes its result, and post treated a cached false
as final — so after the user re-enabled notifications in System Settings,
the Settings UI cleared its warning but quota alerts still skipped
delivery until the app restarted. Re-read the live authorization status
when the cached result is false; if it's now authorized, refresh the
cache and proceed.

Addresses Cursor Bugbot medium-severity finding on PR #786.

Co-authored-by: Cursor <cursoragent@cursor.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Comment thread Sources/OpenUsage/Support/PaceNotificationLogic.swift Outdated
…s off

When a milestone's toggle was off during a worsening, maybeFire skipped
posting but the logic still advanced previousBucket/wasUnderTenPercent,
so the crossing was silently consumed — turning the trigger back on while
the quota remained in the worse bucket never alerted. Now the recorded
bucket/under-10% flag only advance when the worsening was actually
alerted (or there was no worsening); a skipped crossing persists so
re-enabling the trigger fires on the next evaluation. maybeFire now
returns whether it fired.

Adds a regression test. Addresses Cursor Bugbot medium-severity finding
on PR #786.

Co-authored-by: Cursor <cursoragent@cursor.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Comment thread Sources/OpenUsage/Stores/WidgetDataStore.swift
Milestone dedup marked a trigger fired (and advanced previousBucket/
wasUnderTenPercent) before AppNotifications.post actually delivered
anything — post returned immediately and could skip (not authorized) or
fail (add errored), so the alert was lost for the rest of the reset
window. post now awaits delivery and returns Bool; evaluateNotifications
commits a milestone's dedup state only when delivery succeeded, and
reverts the mark + state advance on failure so it re-fires on the next
pass. evaluateNotifications is now async (awaited from the periodic
loop); the DEBUG fire buttons wrap in Task.

Adds a failed-delivery-retries regression test. Addresses Cursor Bugbot
high-severity finding on PR #786.

Co-authored-by: Cursor <cursoragent@cursor.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

…ggle-on

The three quota triggers now default OFF, and the app requests
notification authorization the first time a trigger is turned on (from
Settings) instead of at launch — so a fresh install stays quiet until the
user opts in. Drops the launch-time requestAuthorization call.

Also makes the dev build's BUNDLE_ID env-overridable in build_and_run.sh
so a fresh bundle id can be used to reset macOS notification permission
for re-testing the prompt/deny flow.

Tests opt the triggers on explicitly (the store defaults are now off).

Co-authored-by: Cursor <cursoragent@cursor.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 31dbeee. Configure here.

Comment thread Sources/OpenUsage/Support/PaceNotificationLogic.swift
…; full-width action button

The denied/not-determined explanation moves from a caption under the
toggles into a hover tooltip on the Notifications header triangle, and
the "Open System Settings" / "Allow Notifications" button becomes full
width. Less clutter, same information.

Co-authored-by: Cursor <cursoragent@cursor.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

…el-band metrics

Two Cursor Bugbot findings on PR #786:

- High "Dedup before delivery succeeds": the logic marked a milestone
  fired (inserted into firedMilestones) before AppNotifications.post
  delivered. Now maybeFire only appends a candidate; the caller inserts
  the dedup mark after delivery succeeds, so a skipped/failed delivery
  leaves it un-marked and the milestone re-fires next pass.
- Medium "Almost Out skips level meter state": .level metrics (no pace
  projection, but used/limit known) returned early, so "Almost Out" never
  fired for them. The under-10% check now runs for .level too — it's a
  remaining-based trigger, not a pace one — while .noData still skips
  entirely. .level primes on first observation like any other state.

Adds a .level-fires-Almost-Out regression test; renames the stale
"level never fires" tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

robinebers has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@robinebers robinebers merged commit 1cf2eb4 into main Jun 28, 2026
3 checks passed
@robinebers robinebers deleted the cursor/fix-quota-notifications-775 branch June 28, 2026 11:14
robinebers added a commit that referenced this pull request Jun 28, 2026
Resolves the AppContainer conflict by keeping both apiKeyProviders (this
PR's API Keys card) and notificationSettings (main's quota-pace alerts,

Co-authored-by: Cursor <cursoragent@cursor.com>
#786). Also brings in the Share Screenshot footer submenu (#785).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant