Skip to content

Notifications UX: Jump to Latest Unread + per-tab / per-surface dots + tap deeplinks#266

Merged
sbertix merged 4 commits intomainfrom
sbertix/notifications-ux
Apr 21, 2026
Merged

Notifications UX: Jump to Latest Unread + per-tab / per-surface dots + tap deeplinks#266
sbertix merged 4 commits intomainfrom
sbertix/notifications-ux

Conversation

@sbertix
Copy link
Copy Markdown
Collaborator

@sbertix sbertix commented Apr 21, 2026

Summary

  • Jump to Latest Unread (⌘⇧U) — new menu item under Worktrees that selects the worktree owning the newest unread notification, focuses the originating surface, and marks only that notification read. Disabled when there are no unread. Rebindable via AppShortcuts.jumpToLatestUnread.
  • Per-surface orange dot — small indicator overlayed on any split/surface whose unread notifications have not been dismissed.
  • Per-tab orange dot — shown in the close-button slot when the tab is idle and any surface inside it has unread notifications. Hover swaps it for the close X.
  • System notification taps route through deeplinks — each UNNotification now carries a supacode://worktree/<id>/tab/<tab>/surface/<surface> payload. Tapping it dispatches a deeplinkReceived action, so the user lands on the exact surface that posted.

Notable internals

  • WorktreeTerminalNotification.createdAt (new) is driven by the injected date dependency, so ordering is deterministic in tests.
  • WorktreeTerminalManager.latestUnreadNotificationLocation() iterates each worktree's unread list newest-first and falls through closed surfaces to the next focusable one.
  • SystemNotificationClient.send now takes an optional deeplink URL stored in userInfo. The delegate uses the async didReceive: variant on the main actor.
  • New NotificationLocation value type ties (worktree, tab, surface, notification) together for the reducer path.
  • All new silent fallbacks log via SupaLogger breadcrumbs (degraded deeplink, stale worktree, skipped closed surface, malformed URL payload).

Test plan

  • Post a notification from an agent hook in worktree A; confirm sidebar, tab, and surface all show the orange dot.
  • Post notifications in two different worktrees; ⌘⇧U lands on the newest.
  • Post a notification, close the surface before acting; ⌘⇧U now jumps to the next-newest focusable.
  • With zero unread, the Worktrees → Jump to Latest Unread menu item is disabled.
  • Click a delivered macOS banner; the app focuses the exact tab + surface that posted it.
  • Hover a tab with unread: close X replaces the orange dot; leave hover, dot returns.
  • make test — 951 tests pass.
  • make check — lint + format clean.

Closes #244.

sbertix added 4 commits April 21, 2026 17:01
Adds ⌘⇧U to jump to the newest unread notification across worktrees,
selecting the worktree + focusing the source surface and marking only
that notification as read; the menu item disables when nothing is
unread. Surfaces now render an orange dot overlay while they have
unread notifications, mirroring the sidebar indicator. System
notifications carry a deeplink payload so tapping one routes through
the existing deeplink handler to land on the exact tab + surface that
posted it.

Closes #244.
- Tab notification dot now lives in the close-button slot: visible
  when the tab is idle and has unread notifications, replaced by the
  close X on hover.
- `SystemNotificationClient` delegate methods now run on the main
  actor directly, so accessing `onDeeplinkTap` no longer needs a
  bridging hop. Added `SupaLogger` breadcrumbs for malformed deeplink
  payloads, unresolved surface deeplinks, and stale-worktree jumps.
- `latestUnreadNotificationLocation` now walks each worktree's unread
  list newest-first until it finds a focusable surface, so a closed
  surface on the newest notification no longer hides older focusable
  ones. Logs once when every unread points at a closed surface.
- Collapsed duplicate `tabID(forSurfaceID:)` / `tabId(containing:)`
  into a single `tabID(containing:)` on `WorktreeTerminalState`.
- Dropped the `createdAt = Date()` default on
  `WorktreeTerminalNotification.init` so production code must pass the
  injected clock; tests use `.distantPast`.
- Surface dot now animates via an always-mounted `.opacity`/`.animation`
  pair rather than a transition without a driver.
- Added `WorktreeTerminalManagerTests` coverage for cross-worktree
  ordering, closed-surface fallback, tab-level unread aggregation, and
  `markNotificationRead` targeting a single id.
- Tightened the happy-path jump test to assert exact focus command.
- `urlOrWarn` now carries `worktreeID` / `surfaceID` into its warning
  so diagnostics correlate to the originating surface.
- Log a debug breadcrumb when `latestUnreadNotificationLocation` skips
  a closed surface in favour of an older focusable one, preserving the
  "which notification was chosen" trace.
- `jumpToLatestUnread` logs a debug line when invoked with no unread,
  so the two no-op branches (menu gated vs stale worktree) are
  distinguishable in logs.
- New test covers the cross-worktree tie-break path: worktree A's
  newest unread is orphaned, A's older focusable is older than
  worktree B's only focusable, B wins.
- `hasUnseenNotificationForTabIDWalksSplitTree` now asserts the first
  leaf also lights the tab (previously only the second leaf was
  exercised), and drops the `_ = firstLeaf` warning-silencer.
@sbertix sbertix enabled auto-merge (squash) April 21, 2026 18:48
@sbertix sbertix merged commit 072ad1e into main Apr 21, 2026
2 checks passed
@sbertix sbertix deleted the sbertix/notifications-ux branch April 21, 2026 18:52
@tuist
Copy link
Copy Markdown

tuist Bot commented Apr 21, 2026

🛠️ Tuist Run Report 🛠️

Builds 🔨

Scheme Status Duration Commit
supacode 37.4s e0327428e

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.

Feature Request: Jump to Latest Unread

1 participant