Skip to content

feat(calendar): show macOS reminders in the calendar agenda - #525

Open
AndrewDi wants to merge 7 commits into
mactools-app:mainfrom
AndrewDi:feat/calendar-reminders
Open

AndrewDi wants to merge 7 commits into
mactools-app:mainfrom
AndrewDi:feat/calendar-reminders

Conversation

@AndrewDi

@AndrewDi AndrewDi commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Calendar widget now reads macOS reminders from EventKit and shows them in the month grid, the recent agenda, and the day popover, next to calendar events.

  • Fetches reminders with a due date (both incomplete and completed).

  • Completed reminders are marked with a filled checkmark, strikethrough title, and a dimmed month-grid dot; incomplete items sort before completed ones.

  • New permission card 系统提醒事项 / System Reminders requests Reminders access and opens Privacy_Reminders when denied.

  • New settings toggle 显示提醒事项 / Show Reminders (default on) to include reminders in the agenda.

  • Adds NSRemindersUsageDescription / NSRemindersFullAccessUsageDescription to the host Info.plist.

  • Updates plugin privacy metadata (dataObserved) and regenerates the website plugin data.

  • Events and reminders that have only a start time (zero-duration events, reminders with only a due time) now show the single start time instead of a synthetic one-minute range such as 09:00-09:01.

Notes / known limitations

  • Recurring reminders are shown once using the master reminder's due date; recurrence occurrences are not expanded.
  • Reminders without a due date are not displayed.

Testing

  • CalendarComponentViewModelTests, CalendarEventGrouperTests, CalendarSettingsStoreTests pass.
  • make validate-changelog, make validate-generated-plugin-data, make script-tests pass.
  • Localization audit passes for all 12 supported locales.
  • New tests cover start-only events and reminders rendering a single start time.

@AndrewDi
AndrewDi force-pushed the feat/calendar-reminders branch from 1c52705 to ad180ea Compare November 6, 2026 07:26
@ggbond268

Copy link
Copy Markdown
Collaborator

Thanks for adding Reminders support! I recommend addressing these three items before merging:

  1. Host compatibility: The permission request depends on the new Reminders privacy descriptions in the host app, but minHostVersion is still 2.0.1. Older compatible hosts do not include these keys. Please require the first host release that includes them and add an app changelog entry for this requirement.

  2. Repeated full-library reads: Each date-range request fetches all reminders, then converts and filters them on the main thread. Cancelling the refresh task also leaves the underlying EventKit fetch running. With a large reminder library or rapid month switching, this could affect the host's responsiveness. Please reuse a cached snapshot, refresh it when EventKit data changes, and properly cancel superseded fetches. This is a performance risk identified from the code path, not a measured freeze.

  3. Preference backup: I reproduced that turning off “Show Reminders,” exporting preferences, and restoring them into a fresh store leaves the setting enabled. The setter also skips the normal preference-change notification. Please include this setting in PortablePreferencesStore, follow the existing save/notification pattern, and apply its restored value to the view model.

The existing 27 Calendar tests passed. Thanks for following up on these!

@AndrewDi
AndrewDi force-pushed the feat/calendar-reminders branch from ad180ea to 6cedba6 Compare November 6, 2026 12:24
@AndrewDi

AndrewDi commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I've addressed all three items:

  1. Host compatibility: minHostVersion is now 2.2.4, the first host release that includes the Reminders usage descriptions (NSRemindersUsageDescription / NSRemindersFullAccessUsageDescription). Added an app changelog fragment documenting the host requirement.

  2. Repeated full-library reads: CalendarEventService now keeps a cached [CalendarEventInput] snapshot and filters date-range requests from it instead of fetching all reminders every time. The snapshot is rebuilt only when EventKit data changes (.EKEventStoreChanged) or on the first read after becoming stale, and refreshReminders() cancels the superseded fetchReminders request via cancelFetchRequest with a generation token so a late result never overwrites a newer snapshot.

  3. Preference backup: settings.shows-reminders is now part of the PortablePreferencesStore fields, setShowsReminders follows the existing commit + onPersistentPreferencesChange pattern, and restorePortablePreferences applies the restored value to the view model via configureAgenda(..., showsReminders:).

Added/updated tests covering the cached-snapshot reuse across month navigation, the .EKEventStoreChanged snapshot refresh, and the portable backup/restore of the toggle.

@ggbond268

Copy link
Copy Markdown
Collaborator

Thanks for the update! I confirmed the “Show Reminders” preference now survives backup and restore, and changing it notifies the host. Three issues remain in 6cedba6.

  1. Stale reminders after reopening the widget. In a widget test, I renamed a reminder while the widget was hidden. It still showed the old title when reopened. Please mark the snapshot stale when the widget is hidden and reload it on the next open, while keeping it cached when browsing months.
  2. A cancelled fetch leaves its task suspended. EventKit skips the completion callback after cancellation, but that callback is the only place that resumes the waiting task. Please have cancellation and completion share a path that resumes each continuation exactly once.
  3. An older Nightly still passes the version check. Nightly 89.1 reports 2.2.4, but its host lacks the Reminders privacy descriptions. Please check the host’s usage description before requesting access and prompt the user to update if it’s missing. Alternatively, require a new host version that includes the descriptions. Nightly 89.1 release

Fetch incomplete and completed reminders with due dates from EventKit and
merge them into the month grid, agenda list, and day popover. Completed
reminders are marked with a filled checkmark, strikethrough, and dimmed
dot. Adds a reminders permission card, a show-reminders toggle, and the
required Reminders usage descriptions.
Require host 2.2.4 (the first release with the Reminders usage
descriptions) and document the host requirement in an app changelog.
Cache the reminder library as a Sendable snapshot and filter date ranges
from it, refreshing the snapshot only when EventKit data changes and
cancelling superseded fetches. Include the show-reminders toggle in the
portable preferences backup and apply the restored value to the view
model.
- Mark the reminder snapshot stale when the widget closes so reopening
  refetches instead of reusing a possibly-stale cache.
- Cancel a superseded reminder fetch by sharing a continuation that is
  resumed exactly once through either completion or cancellation, since
  EventKit skips its callback after cancelFetchRequest.
- Reject Reminders access on hosts that lack the privacy usage
  description and prompt for an app update instead.
@AndrewDi
AndrewDi force-pushed the feat/calendar-reminders branch from 6cedba6 to 5d4378a Compare November 7, 2026 08:40
@AndrewDi

AndrewDi commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the second review! All three items are addressed in the latest commits.

1. Hidden widget must refresh the reminder snapshot on reopen
CalendarComponentViewModel.stop() now calls eventService.invalidateReminderSnapshot(), which bumps the fetch generation and marks the snapshot stale. The next open refetches from EventKit instead of reusing a possibly-stale cache, while browsing within an open month still filters the cached snapshot.

2. Cancellation must not leave the waiting task hanging
EventKit skips the completion callback after cancelFetchRequest, so a superseded fetch previously left the withCheckedContinuation suspended forever. Added ReminderFetchWaiter, a lock-guarded continuation that is resumed exactly once through either complete or cancel; cancellation resumes the waiter with an empty box and the generation check discards the result.

3. Reject Reminders access when the host lacks the privacy description
CalendarEventService now checks the host bundle for NSRemindersFullAccessUsageDescription. When missing, requestReminderAccess() returns a denied result with an update prompt, the permission card shows permission.reminders.hostUpdateRequired, and the action handler no longer opens the system privacy pane. minHostVersion is 2.2.4 with an app changelog entry.

Added tests for items 1 and 3; the suite compiles and all local validations (changelog, localization audit, generated plugin data, backup inventory, script tests) pass.

Events and reminders that have only a start time (zero-duration events,
reminders with only a due time) rendered as a synthetic one-minute range
such as 09:00-09:01. Show the single start time instead.
The assertions compared against a literal 09:00 string, which fails under
12-hour locales where the renderer emits '9:00 AM'. Assert on the range
separator instead and add a ranged-event control to keep the check
meaningful.

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.

2 participants