# 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.