Files
Panama/docs/superpowers/specs/2026-08-24-notifications-focus-redesign.md

9.2 KiB
Raw Permalink Blame History

Notifications & Focus redesign — two tabs, nothing unbounded

Approved mock: home-mocks/notifications.html (scratchpad, :8642). This spec is the implementation contract; where mock and spec disagree, the spec wins.

Goals

  1. Bound the app list. Recent senders + customized apps render up top; everything else sits behind a collapsed, searchable "All apps" expander. Rows expand in place to their controls.
  2. Real per-app rules: sound on/off, banners-vs-history, urgency override, forget.
  3. A real focus-mode editor: create/rename/delete/reorder (order IS priority), trigger-kind editing for all five kinds, schedules with day pills, chip-based interrupt lists.
  4. Two tabs: Notifications | Focus, as category tabs like Shell.

Non-goals: lock-screen privacy (hyprlock cannot render notifications; contract-banned), per-app counters (no data), time-based history retention, merging duplicate app identities (show the raw id honestly instead).

Rule shape (pinned — service and UI program against this)

normalizedAppRule in Notifs.qml grows from { enabled } to:

Field Type Default Meaning
enabled bool true Off = rejected before tracking/history/unread/toast (unchanged)
sound bool true false = playBell skips this app
display string "banners" "history" = file in history + unread, no popup, no bell
urgency string "auto" "low" / "critical" override what the app claims
lastSeenMs int 0 stamped in rememberApplication on every notification
name string "" display name cached at remember time (resolution stays live-first)
icon string "" icon cached at remember time (DesktopEntries lookup, else appIcon)

Unknown/stale fields (incl. the old lock-screen pair) keep being dropped on read. Old {enabled}-only blobs stay valid — every new field is optional with the defaults above. New API: forgetApp(appId) deletes the rule key outright. effectiveUrgency(notification) — the app's urgency override applied over notification.urgency; consumed by playBell (low = silent), the popup timeout choice (critical duration), the DND breakthrough gate, and NotificationCard's critical edge.

Critical breakthrough: new schema key criticalBreaksThrough (bool, def false, group notifications, label "Critical alerts break through"). Popup gate becomes: show when !doNotDisturb || FocusModes.allows(appId) || (Settings.criticalBreaksThrough && effectiveUrgency(n) === critical).

FocusModes API additions (pinned)

  • createMode(name) → new mode { id: unique slug, name, enabled: true, triggers: [{kind: "manual"}], silence: true, keepAwake: false, allow: [] }, appended (lowest priority).
  • removeMode(id), renameMode(id, name) (non-empty, trimmed).
  • moveMode(id, delta) — reorder; order is priority and the UI says so.
  • setTriggerKind(id, kind, fields) — replaces the mode's triggers with one trigger of the new kind. Kind-specific seeds: schedule → { start: "22:00", end: "07:00", days: [0..6] }; workspace → { id: 1 }; fullscreen/game/manual → no fields. (Shipped modes each carry one trigger; a hand-edited multi-trigger mode collapses to one on first kind change — the editor edits triggers[0] and that is documented in a comment.)
  • Manual-mode semantics: investigate how a manual-trigger mode activates today and PRESERVE it exactly; if manual modes currently have no activation path besides enabled, the header toggle keeps meaning enabled and the editor's "Turns on: Manually" detail explains that a manual mode quiets things whenever it is switched on. Do not invent new activation machinery.
  • Existing setEnabled/update/reschedule-style schedule + day editing semantics stay; withinWindow, single-DND-ownership, and the gaming report-don't-silence rule are contract-pinned and untouched.

Routing (pinned)

SettingsRoutes category notifications gains tabs: [{ page: "notifications", label: "Notifications" }, { page: "focus", label: "Focus" }]. New leaf focus → new FocusPage.qml; SettingsShell case + Component. groupPages "focus" moves "notifications""focus" (the focusModes entry renders there now). GamingPage's "Open Focus" action retargets openSettings("focus"). NotificationCard's "Notification settings" jump stays "notifications".

Notifications tab (NotificationsPage.qml rebuilt)

Lede unchanged. Cards:

  1. Quiet — Do Not Disturb toggle; "Critical alerts break through" ToggleRow (criticalBreaksThrough); "Quiet hours" ActionRow whose detail states the Sleep mode's live schedule (or "not scheduled" when Sleep lacks/disabled) and whose button opens the Focus tab (openSettings("focus")).
  2. Banners & history — the four existing schema sliders (names contract-pinned; critical zero renders "Never"); history count + Clear folded into the history row.
  3. Applications — subtitle per mock. Sections:
    • Recent: apps with lastSeenMs within 7 days, newest first.
    • Customized: any app with a non-default field (and not already in Recent).
    • All apps (N): collapsed expander with an inline search field; alphabetical. Rows: cached icon (fallback letter tile), name, subtitle (relative last-seen when known · state summary like "sound off"/"History only", else the raw appId), enabled toggle, chevron. Expanded body: Play sound toggle · "Show as" dropdown (Banners & history / History only) · "Urgency" dropdown (App decides / Treat as low / Treat as critical) · "Forget this app" ActionRow (detail: "Remove its rule; it returns on its next notification").
  4. Active-mode banner at top (ok-tinted) when a focus mode is active: " is quieting notifications · because · ", button "Open Focus".

Focus tab (FocusPage.qml, new)

Lede: "Modes quiet this machine on their own terms — first matching mode wins, and the order below is the priority."

  1. Focus modes card — accordion (one open at a time): drag grip (reorder = priority; also keyboard up/down on the grip), mode glyph, name, summary line ("Turns on · silences everything except N apps" / "On now — "), enabled toggle, chevron. Expanded: "Turns on" dropdown (five kinds) + kind fields (schedule start/end TimeOfDayRow- style or validated HH:MM inputs + seven day pills; workspace id picker); "Silence notifications" toggle; "May interrupt" chip row (chips with ×, "+ Add app" opens a searchable picker over known apps — reuse the rules list's app universe); "Keep the screen awake" toggle; Rename + Delete mode buttons. "+ New focus mode" dashed row at the bottom.
  2. Focus sessions card — default duration segmented chips (25/45/60/90 → focusDurationMinutes), Caffeine toggle, session status row + Start focus/Show controls (existing behaviors move over unchanged).

Reuse existing row widgets and the Displays/Sound phase components (OptionPickerRow, etc.) before inventing new ones. No continuously repainting animations. All new components get qmldir lines in the same wave as first reference.

Search & docs

Hand-written entries (page per target): Do Not Disturb, Quiet hours, Critical alerts break through, Application notification rules, Forget an app's notifications, Per-app notification sound → notifications; Focus modes detail already schema-indexed (now routes to focus), plus Focus session duration → focus if not covered by the workspaces group move. Docs and launcher commands regenerate after the schema lands (orchestrator's audit pass).

Contracts (write, do NOT run — cite in the backlog for the next sweep)

  • notification-app-rules-contract: extend the pinned rule shape to the table above (defaults, optional back-compat, stale-field dropping incl. lock-screen pair), pin forgetApp, the display="history" no-popup-no-bell path, sound=false no-bell, effectiveUrgency consumers, and the breakthrough gate literal.
  • focus-modes-contract: pin the new CRUD/reorder/trigger APIs, keep every existing pin (conditions-not-alarms, DND ownership, gaming reports, exception list consulted + editable — the editable needle moves to FocusPage).
  • settings-pages-contract (page id list + component list), settings-jump-contract (GamingPage → focus), search-routing-contract expectations, settings-window-contract if it enumerates tabs.
  • Backlog spec gains a Phase 7 section listing all of it.

Agent ownership (parallel)

  • A — services: services/Notifs.qml, services/FocusModes.qml, config/PreferenceSchema.qml (one new key; comments above braces).
  • B — UI: modules/settings/NotificationsPage.qml, new modules/settings/FocusPage.qml + new components + modules/settings/qmldir, services/SettingsRoutes.qml, modules/settings/SettingsShell.qml, modules/notifications/NotificationCard.qml (effectiveUrgency), modules/settings/GamingPage.qml (Open Focus target).
  • C — periphery: services/SettingsSearch.qml, the contracts above + harness fixtures, test-backlog spec, README count line only if the contract count changes.

B programs against the pinned shapes; A must not change them without updating this spec.