Skip to content

Alerts: key WATCHING on the command name instead of the Session - #325

Merged
nedtwigg merged 8 commits into
mainfrom
per-process-alarms
Jul 28, 2026
Merged

Alerts: key WATCHING on the command name instead of the Session#325
nedtwigg merged 8 commits into
mainfrom
per-process-alarms

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

Why

WATCHING was a per-Session toggle, so the bell had to be re-enabled on every new pane running the same program. But the thing you want to be alerted about is a programclaude, a test runner, a build — not a particular tab.

What changed

WATCHING is now a property of the command. You keep an app-global, persisted set of watched command names; a Session runs the output/silence monitor exactly while its foreground command's first word is in that set. Enable it while claude runs and every claude pane watches, now and later; turn it off anywhere and the rule is gone everywhere. There is no per-Session enable and no per-Session mute, so PersistedAlertState.watchingEnabled is gone too.

With nothing running there is no rule to key on, so the bell changes nothing and opens the alert dialog to say so. The dialog gained the per-command rule switch plus a list of every watched command with a remove control — the only place a rule set on a since-closed pane can be found.

Two behaviors this forced

  • A ring must outlive its monitor. Watching switches off the instant the watched command exits, usually the same instant the ring was raised, so the WATCHING ring moved out of ActivityMonitor into the entry. Removing a rule still silences the ring; a command merely ending does not.
  • WATCHING now outranks COMMAND_EXIT_ARMED in the status projection. A watched command is by definition running, so the old precedence would have masked the monitor's busy/quiet states for the whole run.

Simplification

alert-manager.ts 719 → 596 lines, alert.md 283 → 201, while both gained the new model. Deleted: the duplicated bell transition table (one copy now, in the renderer), AlertManager.dismissOrToggleAlert, toggleAlert/disableAlert, two platform methods net, and the legacy watching-status inference in seed(). startCommandExitWatch and the terminal-state reducer now share one resolveCommandStart.

Playground fake shell

TutorialShell never reported command boundaries, so no playground pane had a command to key on — including the pane hosting the tutorial that teaches the feature. It now emits the same OSC 633 sequences a real integrated shell does (A/B around the prompt, E + C on launch, D on exit). Every pane reports its program, so the bell works everywhere.

Known limitation

WATCHING needs a shell that reports command boundaries, so cmd.exe, fish, and any shell where integration injection did not take no longer get it. Routing the renderer-side keystroke fallback into the manager would buy those shells a worse version of the feature at the cost of a second command-tracking path. Stated plainly in alert.md rather than left implicit.

Tutorial

The desktop alert section was rebuilt around the new model (6 steps → 9, adding the rule spreading across panes plus the two tracks that had no coverage before). It is knowingly in a middling state — it teaches the mechanics but not the attention model, which is the actual idea. TUTORIAL_MOCKUP.md is a working draft for that rework; it lands in a follow-up PR.

Verification

pnpm test (1280 tests), pnpm lint:specs, and build:vscode all pass. Driven end-to-end in the desktop playground: one bell click lights every pane running that command; the dialog shows the rule switch and removable rule list; the ring fires on silence and leaves a TODO; OSC 777 and command-exit both ring without a rule; the bell at a prompt opens the "nothing is running" dialog and creates nothing.

Not exercised by hand: VS Code (the one host where AlertManager is cross-process and the rule set arrives by postMessage) and a real integrated shell in standalone.

🤖 Generated with Claude Code

nedtwigg and others added 3 commits July 28, 2026 09:33
WATCHING was a per-Session toggle, so the bell had to be re-enabled on
every new pane running the same program. But the thing you want to be
alerted about is a *program* -- `claude`, a test runner, a build -- not a
particular tab.

WATCHING is now a property of the command. The user keeps an app-global,
persisted set of watched command names (`dormouse:watched-commands`); a
Session runs the output/silence monitor exactly while its foreground
command's `commandArgv0` is in that set. Enable it while `claude` runs and
every `claude` pane watches, now and later; turn it off anywhere and the
rule is gone everywhere. There is no per-Session enable and no per-Session
mute, so `PersistedAlertState.watchingEnabled` is gone too.

With nothing running there is no rule to key on, so the bell changes
nothing and opens the alert dialog to say so. The dialog gained the
per-command rule switch plus a list of every watched command with a remove
control -- the only place a rule set on a since-closed pane can be found.

Two behaviors this forced:

- A ring must outlive its monitor. Watching switches off the instant the
  watched command exits, usually the same instant the ring was raised, so
  the WATCHING ring moved out of ActivityMonitor into the entry. Removing
  a rule still silences the ring; a command merely ending does not.
- WATCHING now outranks COMMAND_EXIT_ARMED in the status projection. A
  watched command is by definition running, so the old precedence would
  have masked the monitor's busy/quiet states for the whole run.

Simplification along the way: alert-manager.ts 719 -> 596 lines and
alert.md 283 -> 201 while gaining the new model. Deleted the duplicated
bell transition table (one copy now, in the renderer), toggleAlert /
disableAlert / dismissOrToggleAlert, two platform methods net, and the
legacy watching-status inference in seed(). startCommandExitWatch and the
terminal-state reducer now share one resolveCommandStart.

The playground tutorial's alert section is rebuilt around the new model:
6 steps -> 9, adding the rule spreading across panes plus the two tracks
with no coverage before (program-sent notifications, command exit). The
demos fake shell integration with OSC 633 through the real parser.

Known limitation, stated in alert.md rather than worked around: WATCHING
needs a shell that reports command boundaries, so cmd.exe, fish, and any
shell where injection did not take no longer get it. Routing the
renderer-side keystroke fallback into the manager would buy those shells a
worse version of the feature at the cost of a second command-tracking path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking the bell on any playground pane gave "Nothing is running in this
tab". WATCHING is keyed on the running command's name, and `TutorialShell`
never reported command boundaries -- so no playground pane had a command to
key on, including the one hosting the tutorial that teaches the feature.

The fake shell now emits the same OSC 633 sequences a real integrated shell
does: A/B around the prompt, E + C on launch, D on exit (127 for an unknown
command). Panes report `tut`, `changelog`, `ascii-splash`, or whatever the
user types at the prompt, so the bell reads `Alert on all "tut"` and works
everywhere. This also retires the keystroke fallback for playground panes,
which is the correct outcome -- real integration wins.

The `s` demo still overrides both alert panes with a fake `longtask` so the
rule visibly spreads across panes, but it now restores each pane's real
command line afterwards via `reportRunningCommand()` instead of reporting a
bare exit, so a pane whose TUI is still drawing never looks idle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The alert section of the playground tutorial teaches the mechanics but not
the idea. The idea is attention: Dormouse never interrupts you about the
pane you are looking at, and "looking at it" ends either by switching panes
or by simply sitting still for 15 seconds. Today that model appears once,
as an aside in a hint, framed as a way to fail a step rather than as the
point of the feature.

This is a working draft to iterate on, not a spec -- it proposes a four-beat
arc that gives attention its own beat (one task run three ways, with only
the user's behavior changing between runs), and flags the two real design
problems: how to reward a non-event, and that the demo needs two task
lengths to show both the attended and the attention-expired cases.

No code or spec changes. The rework is a later PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 286ed80
Status: ✅  Deploy successful!
Preview URL: https://eefeff60.mouseterm.pages.dev
Branch Preview URL: https://per-process-alarms.mouseterm.pages.dev

View logs

The dialog gained real UI in the WATCHING rework -- a per-command rule
switch, a "nothing is running" explanation, and a removable rule list --
and none of it had stories. Worse, because no story primed a running
command, every existing dialog story was silently rendering the new
no-command variant rather than the case it was named for.

Adds a `primedWatchedCommands` story parameter (cleared before and after
every story, since the rule set is app-global and persists to localStorage)
and a `primedRunningCommand` helper that reports a command through shell
integration so the bell has an argv0 to name.

New `TodoAlertDialog.stories.tsx` renders the dialog directly against a
fixed rect, covering rule on/off, no-command with and without existing
rules, a six-rule list, notification detail alongside a rule, and a
pathological command name. New header stories cover the command-scoped bell
tooltip in all three states plus the no-command dialog via the real
right-click path.

Two fixes fell out of writing them:

- The rule-switch label was unbounded, so a long argv0 stretched the dialog
  to ~640px and pushed the close button into the TODO switch. The label now
  truncates (full name still in the list and the title attribute) and the
  switch grid reserves room for the close button.
- The tooltip play helper hovers instead of focusing: a programmatic
  `.focus()` does not reliably drive React's `onFocus` here, while
  `mouseover` is what React synthesizes `onMouseEnter` from. It retries and
  throws, so a regression shows up in the Interactions panel rather than as
  an empty snapshot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nedtwigg
nedtwigg merged commit bdda2c2 into main Jul 28, 2026
9 checks passed
@nedtwigg
nedtwigg deleted the per-process-alarms branch July 28, 2026 19:01
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