Bound the notification app list, and give Focus a real editor
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -358,3 +358,104 @@ trap, sound-cards' jq context rebind). displays-contract and switcher-contract
|
||||
— the locked-session holdovers — both pass unlocked. Later phases append new
|
||||
deferred contracts below as before; this line is the baseline they diverge
|
||||
from.
|
||||
|
||||
## Phase 7 (Notifications & Focus) — append below
|
||||
|
||||
Spec: `2026-08-24-notifications-focus-redesign.md`. Notifications & Focus became
|
||||
two tabs. Per-application rules grew from one bool to seven fields with a real
|
||||
editor behind them, Do Not Disturb gained a second exception, and focus modes
|
||||
got create/rename/delete/reorder plus trigger-kind editing on a page of their
|
||||
own.
|
||||
|
||||
**Nothing in this wave was run.** Three agents were editing the tree
|
||||
concurrently, and the one runtime harness here (`notification-app-rules-contract`)
|
||||
boots a real Quickshell against a private D-Bus session. What *was* verified is
|
||||
listed as static below: `bash -n` on every changed contract, and — for the two
|
||||
contracts whose assertions are computed — the extracted blocks replayed in
|
||||
`bun` and `python3` against the landed services, so the expected values in the
|
||||
assertions are the values the shipped code actually produces.
|
||||
|
||||
### New contracts (0)
|
||||
|
||||
None. The redesign added surface to services and pages that already had
|
||||
contracts, so the README count line stays at **169** and `setup/readme-contract`
|
||||
needs nothing.
|
||||
|
||||
### Updated contracts (4)
|
||||
|
||||
| Contract | What it now pins | Verified |
|
||||
|---|---|---|
|
||||
| `quickshell/notification-app-rules-contract` | The rule shape, as two properties rather than a literal: every field past `enabled` is **optional** (an old `{enabled}`-only blob reads back with `sound`/`display`/`urgency`/`lastSeenMs`/`name`/`icon` at their defaults) and the field set is **closed** (unknown keys, the stale lock-screen pair, and out-of-vocabulary `display`/`urgency` values are all dropped; a non-numeric or negative `lastSeenMs` reads as 0). Plus: `forgetApp` deleting the key rather than writing an all-defaults rule; `rememberApplication` stamping `lastSeenMs`/`name`/`icon` from the clock; `effectiveUrgency` and all four of its consumers (`playBell`'s low check, `notificationTimeoutMs`'s critical duration, the breakthrough gate, `NotificationCard`'s critical edge); the per-app `sound` switch in `playBell`; `display: "history"` reaching history and unread but not the popup list or the bell; and the popup gate literal `!root.doNotDisturb \|\| FocusModes.allows(appId) \|\| breaksThrough` with `breaksThrough` being `Settings.criticalBreaksThrough` ANDed with effective-critical. The schema key and its `Settings` reader are pinned too. Page assertions now read the page **and** the components it delegates rows to (`NotificationAppRow.qml`), so pulling the expanded body into a component does not read as the feature being deleted. Runtime: four new fixtures — `historyOnly`, `forgetting`, `urgency`, `breakthrough` — and the two existing jq literals became field-by-field predicates, because `lastSeenMs` is a wall clock that cannot be written down. | **The whole static half was run** against the landed tree and prints PASS. The runtime half is **deferred**. |
|
||||
| `quickshell/focus-modes-contract` | Every existing pin is untouched and still passes: conditions-not-alarms, the midnight-crossing / malformed / zero-length schedule arithmetic, single DND ownership, the gaming hook reporting rather than silencing, the retired `gamingSilenceNotifications` key staying out of the schema, and the exception list being both consulted and editable. New: `createMode`/`removeMode`/`renameMode`/`moveMode`/`setTriggerKind`/`seedTrigger`/`uniqueId`/`hasMode` by name; a `bun` replay of `seedTrigger` (all five kinds and their seeds, handed-in fields kept, unknown kinds refused with `null` rather than stored) and of `uniqueId` (slugging, suffixing past a taken id, and the `"mode"` fallback); the "order is priority" claim having to appear on the page; and the manual-mode semantics being *preserved* rather than invented — `automatic` still filters manual-only modes out and the fall-through still carries its note. The editable-list needle moved from `NotificationsPage.qml` to the Focus surface (`FocusPage.qml` + `Focus*.qml`), because the chips live in `FocusModeRow.qml`. | **Statically verified end to end**: every grep re-run against the landed files, the schedule arithmetic replayed in `python3`, and the new API block replayed in `bun` — all green. |
|
||||
| `quickshell/settings-pages-contract` | `Focus` added to the component list (root type `SettingsPage`, no copied Flickable scaffold) and `focus` to the runtime page-routing sweep. The four Notifications sliders and the `zeroLabel: "Never"` rule are unchanged and still hit. | **Static half verified**: `FocusPage.qml`'s root type and the four slider regexes replayed in `python3`. The routing sweep is **deferred** — it starts an isolated shell. |
|
||||
| `quickshell/settings-jump-contract` | GamingPage's "Open Focus" now has to go through `ShellState.openSettings("focus")`, and assigning `ShellState.settingsPage` by hand is banned there — the old code did exactly that, which skipped `SettingsRoutes.resolve()` and so skipped this contract's own guard entirely. `NotificationCard`'s jump stays `"notifications"` and is unchanged. | **Statically verified** against the landed `GamingPage.qml` and `NotificationCard.qml`. |
|
||||
|
||||
### Contracts deliberately left alone
|
||||
|
||||
- `quickshell/search-routing-contract` — needed no edit. It derives page files
|
||||
from `SettingsShell`'s switch, so `focus` resolved to `FocusPage.qml` the
|
||||
moment B landed the case and the Component. Replayed by hand against the
|
||||
landed tree: **144 routed settings, 0 violations** with the focus group
|
||||
moved.
|
||||
- `quickshell/settings-window-contract` — enumerates no tabs. It routes
|
||||
`displays` and the retired `desktop` id only.
|
||||
- `quickshell/settings-ownership-contract` — its duplicate-row scan and its
|
||||
`groupPages` parse were replayed against the edited `SettingsSearch.qml` and
|
||||
still pass.
|
||||
- `setup/readme-contract` — no contract file added or removed; `find` still
|
||||
counts 169 and the README still claims 169.
|
||||
|
||||
### Docs updated in the same wave
|
||||
|
||||
- `services/SettingsSearch.qml` — `groupPages` `"focus"` moved from
|
||||
`"notifications"` to `"focus"`, plus eight hand-written entries: Do Not
|
||||
Disturb, Quiet hours, Critical alerts break through, Application
|
||||
notification rules, Forget an app's notifications, Per-app notification
|
||||
sound, and Banners or history (all → `notifications`), and Focus session
|
||||
duration (→ `focus`). Checked against `settings-search-contract`'s fixed
|
||||
query list: none of the new labels or details contains any of its queried
|
||||
substrings, so no existing top result moves.
|
||||
|
||||
### Still open before the run
|
||||
|
||||
- **A live bug this wave removed, worth knowing about.**
|
||||
`notification-app-rules-contract` used to `perl`-graft a temporary
|
||||
`notificationAppRules` schema key into its copy of `PreferenceSchema.qml`,
|
||||
from the era before that key shipped. The key ships now, and the graft's
|
||||
anchor comment still exists — so the contract was defining it **twice** in
|
||||
the copied schema on every run. The graft is gone, replaced by an assertion
|
||||
that both `notificationAppRules` and `criticalBreaksThrough` are present.
|
||||
- `NotificationAppRulesHarness.qml`'s fake notification now spells out
|
||||
`urgency`, `expireTimeout` and `hints`. It previously left all three
|
||||
undefined, which meant the timeout path and the urgency path were exercised
|
||||
in their undefined branch rather than their ordinary one. If a fixture
|
||||
behaves differently than expected on the first run, that change is the first
|
||||
place to look.
|
||||
- The `breakthrough` fixture drives `Settings.criticalBreaksThrough` by writing
|
||||
the preference and reading the binding back in the same JS call. That is
|
||||
synchronous through `DesktopPreferences.set` (it reassigns `values` and bumps
|
||||
`revision` before returning), but it has not been observed. If the fixture
|
||||
reports `through: 0`, suspect binding timing before suspecting the gate.
|
||||
- **`focusDurationMinutes` is now editable on two pages**: a `SliderRow` on
|
||||
`WorkspacesPage.qml` and segmented chips on `FocusPage.qml`. No contract
|
||||
catches it — `settings-ownership-contract` only scans the five schema-bound
|
||||
row types and the chips are a `SegmentedRow` reading `DesktopPreferences`
|
||||
directly, and `search-routing-contract` only sees `setting:` rows — but it is
|
||||
a real ownership violation by `modules/settings/README.md`'s own rule. Decide
|
||||
before the run: either the Workspaces row goes, or the key's `group` moves to
|
||||
`focus` and the mirror gets named in `settings-ownership-contract`.
|
||||
- **A stale comment in `config/PreferenceSchema.qml`** above
|
||||
`focusDurationMinutes` still says "The focus group routes to Notifications,
|
||||
which is where focusModes renders." It routes to `focus` now. Left for the
|
||||
schema's owner rather than edited across agent lines.
|
||||
- Run order for the sweep: `focus-modes-contract` first (pure static, no
|
||||
compositor and no shell), then `settings-jump-contract` and
|
||||
`search-routing-contract` (also static), then
|
||||
`notification-app-rules-contract` (private D-Bus, isolated shell), and
|
||||
`settings-pages-contract` last — it starts an isolated Quickshell beside the
|
||||
live one and its own cleanup is what protects the running session.
|
||||
- Nothing here plays a sound on purpose, but every delivery fixture reaches
|
||||
`playBell`, which shells out through `SoundFeedback.playCommand`. That was
|
||||
already true of the existing `exercise` fixture; the new fixtures add four
|
||||
more chances for it. Keep the harness free of anything that turns the volume
|
||||
up.
|
||||
|
||||
Reference in New Issue
Block a user