Conversation
A latched ring used to show only a swinging bell and a tinted header; the whole-Pane wash, perimeter ring, and label were reserved for spoken-alarm delivery. Now the treatment is the ring indicator itself: unlabelled at 10% wash / 3px ring while nothing has spoken, and SPEAKING / SPOKEN stacking on top when the speech sink acts. The unlabelled state flashes a bounded four-cycle burst on arrival and then holds, keyed on the Activity episode and clocked from its start time, so a second track latching inside one episode does not re-flash and a minimize/reattach does not replay a burst that already expired. prefers-reduced-motion and cfg.alert.ringingPaused suppress the burst and keep the static treatment, exactly as they do for the speaking pulse. A ringing Door gets the same static inset ring spoken already uses, with the same arrival burst and "needs attention" in its accessible name. AlertSpeechIndicator is now AlertRingIndicator and reads the Activity store as its gate; the shared alarm animation helpers move from bell-icon-class.ts to design.tsx. The bell stays for now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Pane overlay and the Door each paired `status` with `speechState` on their own and each reached for its own freeze-aware animation class. Both now read `alertRingRow(status, speechState)` and `alertRingBurstProps(row, episode)` from lib/src/components/alert-ring.ts, which also owns the accessible-name table, the single `alarmPulseClass(bounded)` body, and the `animationClockStyle(startedAt)` the bell already computed inline. The bundle exists so the class, the remount key, and the CSS clock can never be applied apart: a Door that keyed on an absent episode never replayed its burst, and the frozen Chromatic build still wrote a dead `animationDelay` into every snapshot. `useAlertRingBurst` anchors that clock once per episode, so a re-render no longer shoves a live burst back to its start. The overlay now animates its perimeter ring alone rather than the wash too, so an alarming Pane composites one layer instead of two, and both components subscribe per Session, so a ring on one Pane no longer re-renders the overlay on every other. The Door paints the unlabelled and SPOKEN edges from one inset overlay (`DOOR_ALARM_INSET_CLASS`) rather than a root shadow plus a child, and drops `data-alert-speech-state` for `data-alert-ring-state`. CSS: `speech-alarm-pulse` is now `alarm-pulse`, behind `--animate-alarm-pulse` and `--animate-alarm-pulse-burst`. Both spec edits fit without a budget change, so the alert.md ratchet from the previous commit is refunded. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
7bad90e
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://31fb148c.mouseterm.pages.dev |
| Branch Preview URL: | https://alert-ring-flash.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
Feedback on work in progress — not a merge verdict. Mark the PR ready when you want the full review.
The rewrite reads cleanly: alertRingRow as the single gate is a real improvement over each surface pairing status with speechState, and gating on the latched ring closes the window where a spoken state outlived its episode (afterScan in lib/src/lib/alert-speech.ts only clears it on the next scan, so the old AlertSpeechIndicator kept painting until then). Anchoring the burst on episode.startedAt rather than mount is right, and the Door comment about never keying the cached element earns its place.
One thing the docs edit drops. layout.md deleted Under SPEAKING both pulse when motion is allowed and cfg.alert.ringingPaused is not set and now hands motion off — Only the perimeter ring animates ... docs/specs/alert.md → Pane Header owns the motion — but the Pane Header paragraph it points at states only the unlabelled burst bound. So after this PR no spec says that SPEAKING pulses for its utterance and SPOKEN never animates. SPOKEN is the unbounded row, and an unbounded full-Pane animation is exactly the cost alert.rationale.md argues against, so by AGENTS.md's deletion test (if deleting a sentence would let a careful editor reintroduce a bug ... its one-line form stays in the spec) that rule belongs in the body. Your own PR description carries the Motion column the specs no longer have.
docs/specs/alert.md is at 7193 words against a 7200 budget, so there are 7 words of headroom — the inline suggestion is exactly +7 and holds your "no budget change". If you would rather also name the pinning test, which that section does for every neighbouring rule (Name the test that pins a rule), that needs a --ratchet in the same PR.
The other note is test hygiene, inline.
Checked and fine
- Rename is complete: no surviving reference to
speech-alarm-pulse,alertSpeakingAnimationClass,data-alert-speech-state,data-alert-speech-wash,AlertSpeechIndicator, orSpoken-alarmanywhere outside.git. bell-icon-class.tsstill uses itscfgimport afteralertSpeakingAnimationClasswas removed, so no unused-import lint.- The inline
animationDelaybeats theanimationshorthand the Tailwindanimate-*class emits, so the burst clock survives — same mechanismAlertBellalready relies on. if (!row || !burst) return nullis reachable only with a ringing status and no episode;setTerminalActivityis the sole writer ofterminalActivityand hydrates one on every ringing transition, so the pair does arrive together.useSyncExternalStoreper-Session selectors do bail out:getActivityreturns the stored object identity andgetAlertSpeechStatea primitive, so a ring on one Pane re-renders one overlay.useAlertRingBurstnot depending oncfg.alert.ringingPausedis safe — it is only ever set inlib/.storybook/preview.ts, never toggled at runtime.- Baseboard's hidden measurement pass draws the ring identically, and
absolute inset-0adds no width, so the fitting pass still measures what the visible Door draws.
Note that Visual Regression Tests skips while the PR is a draft, so the Chromatic baselines for a change that is entirely visual land only when you mark it ready.
| The TODO pill always displays `TODO`; remote notification text belongs in preview/detail surfaces, not inside the pill. Clicking the pill clears TODO, and on clear the pill briefly shows the success flourish before unmounting. | ||
|
|
||
| Spoken-alarm delivery is much louder than the bell: a pointer-transparent treatment spans the whole terminal Pane, labelled `SPEAKING` while the engine actually speaks and `SPOKEN` — quieter, and unbounded — until the ring resolves. **`prefers-reduced-motion` keeps the strong static treatment and suppresses only the pulse**, as does `cfg.alert.ringingPaused` (rationale). The layers, their strengths, placement, and sizing belong to `docs/specs/layout.md` → Spoken-alarm overlay. | ||
| **Must wear the alarm treatment on every ringing terminal Pane**, labelled only once the speech sink acts. **Must bound the unlabelled pulse to one finite burst per episode, never replayed by a remount** (rationale). **`prefers-reduced-motion` keeps the strong static treatment and suppresses only the pulse**, as does `cfg.alert.ringingPaused` (rationale). The three rows, their layers, strengths, and sizing are inventoried by `docs/specs/layout.md` → Alarm overlay. |
There was a problem hiding this comment.
The Pane's SPEAKING/SPOKEN motion rule has no home after this edit: layout.md dropped "Under SPEAKING both pulse when motion is allowed" and now defers motion here, but this paragraph covers only the unlabelled burst. Nothing in either spec stops an editor animating SPOKEN — the unbounded row, and the cost the rationale exists to argue against. Exactly +7 words, which is the headroom alert.md has left against its 7200-word budget.
| **Must wear the alarm treatment on every ringing terminal Pane**, labelled only once the speech sink acts. **Must bound the unlabelled pulse to one finite burst per episode, never replayed by a remount** (rationale). **`prefers-reduced-motion` keeps the strong static treatment and suppresses only the pulse**, as does `cfg.alert.ringingPaused` (rationale). The three rows, their layers, strengths, and sizing are inventoried by `docs/specs/layout.md` → Alarm overlay. | |
| **Must wear the alarm treatment on every ringing terminal Pane**, labelled only once the speech sink acts. **Must bound the unlabelled pulse to one finite burst per episode, never replayed by a remount; `SPEAKING` pulses for its utterance, `SPOKEN` never** (rationale). **`prefers-reduced-motion` keeps the strong static treatment and suppresses only the pulse**, as does `cfg.alert.ringingPaused` (rationale). The three rows, their layers, strengths, and sizing are inventoried by `docs/specs/layout.md` → Alarm overlay. |
| expect(leaf?.querySelector('[data-session-id]')?.getAttribute('data-session-id')).toBe('pane-a'); | ||
| }); |
There was a problem hiding this comment.
pane-a stays ALERT_RINGING for the rest of the file: this suite's afterEach resets the archive service and notepads but not the activity store, and harness.dispose() only unmounts the root. Every later test that renders a pane-a inherits the alarm overlay and a ringing bell in its header — nothing asserts on either today, so this is latent rather than broken, but it is the kind of leak the next test added here pays for. The saveState test further down clears it inline, same shape.
| expect(leaf?.querySelector('[data-session-id]')?.getAttribute('data-session-id')).toBe('pane-a'); | |
| }); | |
| expect(leaf?.querySelector('[data-session-id]')?.getAttribute('data-session-id')).toBe('pane-a'); | |
| // The activity store is Window-global and this file's `afterEach` does not reset it. | |
| await act(async () => { clearTerminalActivity(); }); | |
| }); |
Stacked on #707.
Until now a plain
ALERT_RINGINGSession showed only the bell glyph; the whole-Pane wash + perimeter ring existed only for spoken-alarm delivery. Now every latched ring wears it, and the speech sink only picks the label:ringingspeakingSPEAKINGspokenSPOKENAlertSpeechIndicator→AlertRingIndicator, gated on the Activity store with per-Session selectors; the burst keys on the alertepisodeand is clocked from itsstartedAt, so a remount mid-episode never replays it.alertRingRow) and one motion home (lib/src/components/alert-ring.ts) shared by the Pane overlay and the Door; the Door wears a single inset overlay forringingandspoken.alarm-pulse.layout.md→ "Spoken-alarm overlay" is now "Alarm overlay" and the single inventory of the rows;alert.md,DESIGN.md,theme.mdpoint at it. No budget change.The bell stays in this PR so the diff reviews cleanly; the next PR retires it.
🤖 Generated with Claude Code