Skip to content

Defer terminal-notification alerts until animation stops by default - #706

Draft
nedtwigg wants to merge 1 commit into
mainfrom
alert-defer-default-on
Draft

nedtwigg wants to merge 1 commit into
mainfrom
alert-defer-default-on

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Sep 20, 2026

Copy link
Copy Markdown
Member

First of a stacked series simplifying the alert system.

Coding agents (claude, codex) emit their "done" notification while their TUI is still redrawing, so an undeferred ring summons you to a pane that is still animating. deferAlertsUntilQuiet now defaults on; the switch stays as an escape hatch for the protocols' literal timing. The AlertManager field follows DEFAULT_ALERT_SETTINGS so a manager that never receives a settings blob behaves like one that does.

  • docs/specs/alert.md Alarm settings row and the rationale paragraph updated.
  • Two tests that pinned latch-through-output behavior now set the flag off explicitly; one new test pins the shipped default at the manager and at normalizeAlertSettings.
  • Story DeferralEnabledDeferralDisabled (the non-default state is the one worth a snapshot).

Installs that have never saved an alert-settings blob pick the new default up. Any install that has changed any alert setting has the whole blob persisted, so it keeps deferAlertsUntilQuiet: false until the switch is flipped in Settings; the blob has no version field to key a one-shot migration on, and dropping a persisted false on read would make the preference unpersistable (decision recorded in the rationale).

🤖 Generated with Claude Code

Coding agents emit their "done" notification while their TUI is still
redrawing, so the undeferred ring summoned the user to a pane that was
still animating. The switch stays as an escape hatch for the protocols'
literal timing, and the AlertManager field now follows
DEFAULT_ALERT_SETTINGS so a manager that never receives a settings blob
behaves like one that does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 20, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: f1287f6
Status: ✅  Deploy successful!
Preview URL: https://88d96995.mouseterm.pages.dev
Branch Preview URL: https://alert-defer-default-on.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on work in progress, not a merge verdict — mark the PR ready when you want the full review.

The flag gates a second behavior, and nothing a user reads mentions it. deferAlertsUntilQuiet also gates withdrawResumedWatchingRing, which docs/specs/alert.md states as "With deferAlertsUntilQuiet, withdraw a WATCHING ring when watched work resumes confirmed BUSY." That is why the two tests here had to switch the flag off — ALERT_RINGING latches when user has no attention (view hidden) and leaves a stale WATCHING ring alone once output has resumed. So the default flip ships ring withdrawal out of the box as well, while the switch's helper text in lib/src/components/SettingsDialog.tsx ("When the animation watcher is fully armed, terminal notifications wait for the pane to become quiet") and the Alarm settings row both describe only the deferral half. A latched claude-finished ring that disappears when the agent starts a new turn is the case a user will actually hit, with nothing to read about it. Either widen that copy or put withdrawal on its own condition.

"Users who saved the setting keep their value" reaches further than the body says. updateAlertSettings normalizes and persists the whole blob, so a user who once changed only speakDelayMs has deferAlertsUntilQuiet: false sitting in dormouse:alert-settings and never picks up the new default — despite never having expressed a preference about deferral. For a change motivated by an experience every agent user has, the set that keeps the old behavior is "anyone who has opened Settings and touched anything", not "anyone who set this switch". Worth deciding whether that is intended, or whether the persisted key should be dropped when it still matches the old default.

The rationale's closing clause reads as attributing indefinite deferral to the switch being off, when the on state is what produces it — scheduleDeferredNotification re-arms at the detector's quiet deadline with no cap. Since that unbounded case is now the shipped default, it is the sentence most worth being unambiguous. Inline suggestion below.

One small thing outside the diff: standalone/src/browser-sidecar-adapter.test.ts builds its canonical fixture as { ...DEFAULT_ALERT_SETTINGS, deferAlertsUntilQuiet: true }, now identical to DEFAULT_ALERT_SETTINGS — so the assertion no longer distinguishes "forwarded the blob it was handed" from "emitted the default". Flipping that override to false restores it.

## Alarm settings

**Why animation deferral defaults off.** BEL and notification OSCs explicitly ask to alert now, while continuously changing output may never become quiet. Opt-in preserves their established timing and makes indefinite deferral a deliberate choice.
**Why animation deferral defaults on.** Coding agents (`claude`, `codex`) send their notification OSC while their TUI is still redrawing its spinner, so an undeferred ring summons the user to a pane that is still animating (2026-09). The gate engages only while the private detector is fully armed, so a BEL from an otherwise quiet shell still rings at once. Turning the switch off restores the protocols' literal timing, including a ring that continuous output can never quiet.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clause as written reads as though the off state is the one where continuous output can never quiet the ring, but off is exactly the position that rings immediately — the unbounded case belongs to on. Stating it in that order also puts the trade-off the default now carries in front of the reader.

Suggested change
**Why animation deferral defaults on.** Coding agents (`claude`, `codex`) send their notification OSC while their TUI is still redrawing its spinner, so an undeferred ring summons the user to a pane that is still animating (2026-09). The gate engages only while the private detector is fully armed, so a BEL from an otherwise quiet shell still rings at once. Turning the switch off restores the protocols' literal timing, including a ring that continuous output can never quiet.
**Why animation deferral defaults on.** Coding agents (`claude`, `codex`) send their notification OSC while their TUI is still redrawing its spinner, so an undeferred ring summons the user to a pane that is still animating (2026-09). The gate engages only while the private detector is fully armed, so a BEL from an otherwise quiet shell still rings at once. Deferral is unbounded, so continuous output can hold a ring indefinitely; turning the switch off is the escape hatch that restores the protocols' literal timing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants