feat(desktop): reminders notifications, snooze, overlay, and inbox view mode#1093
Conversation
2af5d46 to
55989a3
Compare
Reminders v6 — UI screenshotsVisual walkthrough of the four UI changes in this PR, captured via the v6 Playwright screenshot spec ( Snooze dropdownSnooze menu open on a fired reminder, showing the defer-time presets (In 30m / 1h / 3h / Tomorrow 9am / Next Mon 9am / Custom). Blue-tint overlayThe Inbox Reminders viewThe inbox Reminders view — the v6 relocation target after the sidebar nav teardown — grouping reminders into OVERDUE and TODAY, each row with done / snooze / dismiss actions. Reminder surfacing as dueThe reminder surfacing as due — the watermark-guarded fire path fixed in this PR. The actual fire-on-due notification is an OS-native |
7284d09 to
14ce20c
Compare
Builds four reminder surfaces on a shared react-query spine: - A single useRemindersQuery keyed by pubkey, invalidated in all four mutation paths (create/snooze/complete/cancel), so the badge, channel overlay, panel, and fire-on-due detection can never disagree. - App-level watermark fire-on-due: a persisted lastReminderCheck timestamp, firing pending reminders that newly cross not_before since the watermark, coalesced into one toast, seeded to now on first launch so history is not replayed. Pure predicates live in lib/reminderFilters.ts so the highest-risk detection logic is unit-testable without react-query. - Slack-style snooze dropdown over a shared TIME_PRESETS module with a future-time validator guarding the custom date/time surface. - Blue-tint overlay on channel messages that have an active reminder. - Reminders relocated into the inbox as a ?view=reminders mode rather than a standalone route: it renders RemindersPanel rows directly and never enters the FeedItem selection model, so the detail-pane/selectedItemId collision is dissolved by construction. /reminders redirects to /?view=reminders to preserve history and bookmarks; the Bell sidebar item is removed. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… advance Fold two advisory review MINORs. groupReminders guarded on `!r.notBefore`, dropping epoch-0 timestamps, while isDue/dueSince use `!== undefined` — align all three so the lib reads consistently (epoch 0 is unreachable; no behavior change). Document that the watermark advances even when the toast is suppressed so a future reader does not mistake the no-backlog-replay design for a bug. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
On mount, useRemindersQuery is still loading (data === undefined) so remindersRef.current is []. Without a guard, check() advances the watermark past reminders that came due while the app was closed — the "missed-while-asleep" window — permanently preventing their toast from firing. Track query resolution via a ref and defer the watermark advance when the array is empty AND the query has not resolved yet. First-launch-with-no-reminders still advances correctly: the query resolves to [] with queryResolvedRef === true, so the guard passes. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The screenshot spec still navigated via the deleted view-mode slider, so it landed in the chat view where the filter dropdown is not mounted. Drive it through the inbox filter dropdown instead, seed reminders past the badge query's cached-empty result by invalidating the ["reminders"] query, and finalize the dropdown by dropping the "?view=reminders" URL persistence to local state. Harden waitForAnimations with a bounded race so looping animations (presence dots) and animations that restart mid-sample no longer hang the evaluate until Playwright aborts. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
14ce20c to
6f406c6
Compare
The inbox filter dropdown sits in a justify-end header row inside the list pane. In messages mode the pane is the fixed-width left grid column, so the dropdown lands mid-window. Selecting Reminders collapses the grid to grid-cols-1 and the pane goes full-width, flinging the same right-aligned dropdown to the far window edge. Cap the header row to the list-column width so the dropdown's horizontal position is identical in both modes. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…te-response * origin/main: (194 commits) Fold agent core memory into the session system prompt (#1112) feat(cli): add patches and issues commands for NIP-34 git collaboration (#1073) fix(desktop): stop random timeline message loss + page reconnect replay (#1105) Update README.md fix(desktop): keep thread replies from scrolling channel (#1109) fix(buzz-acp): accept siblings under allowlist author gate (#1108) feat(deploy): add production Helm chart for Buzz (#990) fix(desktop): keep MembersSidebar input usable while an add is in flight (#1106) chore(release): release version 0.3.25 (#1102) fix(desktop): stop dimming deferred message lists (#1104) Smooth channel loading: single-surface timeline state machine (#1099) feat: surface base + persona system prompts in observer feed (#1103) ci: move reminder e2e to a dedicated backend-integration job (#1098) fix: give agent-observer sub a replay-capable limit (#1100) fix: make managed-agent spawn and teardown portable to Windows (#1097) fix(desktop): constrain message timeline width with min-w-0 (#1092) feat(desktop): reminders notifications, snooze, overlay, and inbox view mode (#1093) feat(prompt): add memory hygiene and hoist universal engineering discipline to base prompt (#1085) fix(desktop): correct thread-unread badge flicker, stale clear, phantom count, mention gate, and nested count (#1080) Fix mention chip alignment (#1094) ... # Conflicts: # crates/buzz-cli/src/commands/workflows.rs
…te-response * origin/main: (194 commits) Fold agent core memory into the session system prompt (#1112) feat(cli): add patches and issues commands for NIP-34 git collaboration (#1073) fix(desktop): stop random timeline message loss + page reconnect replay (#1105) Update README.md fix(desktop): keep thread replies from scrolling channel (#1109) fix(buzz-acp): accept siblings under allowlist author gate (#1108) feat(deploy): add production Helm chart for Buzz (#990) fix(desktop): keep MembersSidebar input usable while an add is in flight (#1106) chore(release): release version 0.3.25 (#1102) fix(desktop): stop dimming deferred message lists (#1104) Smooth channel loading: single-surface timeline state machine (#1099) feat: surface base + persona system prompts in observer feed (#1103) ci: move reminder e2e to a dedicated backend-integration job (#1098) fix: give agent-observer sub a replay-capable limit (#1100) fix: make managed-agent spawn and teardown portable to Windows (#1097) fix(desktop): constrain message timeline width with min-w-0 (#1092) feat(desktop): reminders notifications, snooze, overlay, and inbox view mode (#1093) feat(prompt): add memory hygiene and hoist universal engineering discipline to base prompt (#1085) fix(desktop): correct thread-unread badge flicker, stale clear, phantom count, mention gate, and nested count (#1080) Fix mention chip alignment (#1094) ... Co-authored-by: Tyler Longwell <tlongwell@squareup.com> Signed-off-by: Tyler Longwell <tlongwell@squareup.com> # Conflicts: # crates/buzz-cli/src/commands/workflows.rs










Builds four reminder features on the desktop app, all riding a single shared react-query spine. Reminders also move out of a standalone route and into the inbox as a view mode.
Phase 0 —
useRemindersQueryspineA single query keyed by
["reminders", pubkey]wrapsfetchReminders.useReminderMutationswraps create/snooze/complete/cancel and invalidates the shared key on every success, so the badge, channel overlay, panel, and fire-on-due detection all read one source and can never disagree. This also auto-resolves the prior Phase 3 staleness whereonUpdate()did not propagate outside the panel.Phase 1 — Watermark fire-on-due + notification + badge
useReminderNotifications(mounted once inAppShell) detects due reminders with a persisted single-timestamp watermark inlocalStorage(buzz:lastReminderCheck:<pubkey>). On launch and every 30s it fires reminders wherenotBefore > watermark AND notBefore <= now AND status === "pending", coalesces multiple into one toast, then advances the watermark. The watermark seeds tonowon first launch so history is not replayed; a reminder already past at the seed fails the strict>and surfaces only in the panel + badge, never as a toast. The OS toast respectsdesktopEnabled+ theneeds_actionalert slot, with sound + dock bounce. The pure detection predicates (isDue,countDue,dueSince,groupReminders) live inlib/reminderFilters.tsso the highest-risk logic is unit-testable without react-query.Phase 2 — Slack-style snooze dropdown
The hardcoded "snooze 1 hour" button becomes a
Clock-icon dropdown (SnoozeMenu) over a sharedlib/timePresets.tsmodule (TIME_PRESETS), the single source of truth for both the create dialog and snooze. "Custom…" reuses the native date/time input; a sharedparseCustomDateTimefuture-time validator rejectsnotBefore <= nowon both surfaces.Phase 3 — Channel blue-tint overlay
RemindMeLaterProviderderives aSetof active (pending) reminder event IDs from the Phase 0 query and exposes it via context.MessageRowappliesbg-blue-500/10when a message's id is in the set; invalidation re-renders with a fresh set, so no stale tint.Phase 4 — Reminders relocated into the inbox
Reminders is now a
?view=remindersmode inside the inbox, not a standalone nav item or route.Messages | Remindersview-mode toggle. Reminders mode rendersRemindersPanelrows directly (pending + completed viagroupReminders(reminders, includeDone), snooze/complete/cancel inline) and never enters the FeedItem selection model — it does not touchselectedItemIdorInboxDetailPane. This dissolves the detail-pane/selection collision by construction.?item=consume/mirror are gated to Messages mode. View-mode persists in the URL via?view=reminders—validateHomeSearchis extended to allowlistviewso the param survives navigation, reload, and the redirect target.deriveShellRoutestays pathname-only;/?view=reminderskeepsselectedView === "home", accepted intentionally for the notification gate. No"reminders"arm is added to the union./remindersis replaced by a redirect to/?view=reminders(TanStackthrow redirect), preserving back/forward history and bookmarks.routeTree.gen.tsis unchanged (the redirect keeps the route registered). TheBellsidebar item,goReminders, andRemindersScreenare removed.homeBadgeCount.