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

148 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: "<name> is quieting
notifications · because <reason> · <allow summary>", 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 <trigger summary> ·
silences everything except N apps" / "On now — <reason>"), 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.