Skip to content

Rebuild alerts: presence-gated rings, one ring model, shared host, host-side delivery - #776

Open
nedtwigg wants to merge 16 commits into
mainfrom
alert-improve
Open

nedtwigg wants to merge 16 commits into
mainfrom
alert-improve

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

Alerts had false negatives and false positives. Most of both came from one abstraction: attend() meant "the user typed or clicked in this pane in the last 15 s". It was checked once, when a completion happened, and a "yes" threw the completion away.

Replaying a recorded Claude Code session showed the worst case. A user presses Enter and walks away without leaving the window. Claude's "needs your permission" arrives inside that 15 s window and is dropped for good: no ring, no TODO, no push. Conversely, watching a pane for 15 s without typing made it ring.

This PR rebuilds the alert system in six phases. Each phase is its own commit followed by a /simplify commit, then a whole-branch code review and a merge of origin/main.

What changes for users

  • No more lost summons. A completion that arrives while you're engaged with a pane is held.
    • It is dropped when you acknowledge the pane or explicitly move away.
    • It rings when you go idle (no typing, mouse, or scroll for the Inactivity timeout).
    • Dropping on an explicit move-away is flagged in the spec as possibly the wrong call.
  • No more rings on the pane you're looking at. Presence (window focused, plus recent key, mouse, or wheel input anywhere in the window) and focus (the passthrough pane) replace the keystroke lease. Command-mode selection is not focus.
  • Only real gestures acknowledge. A keystroke, paste, drop, click, Door, tap, or zoom acknowledges. Spawns, splits, promotions, dor reveals, and scroll swipes never do.
    • Any keystroke clears TODO. This fixes Windows win32-input-mode Enter, kitty CSI 13 u, and one-key answers to prompts.
  • Your own typing isn't "work". Echo within 250 ms of input no longer drives the busy/quiet detector.
  • One ring per summons, with the best text.
    • WATCHING, terminal reports, and command exits are sources on one ring, with uniform TODO rules.
    • Detail follows one richness order, so a bell never replaces an OSC message or exit code.
    • A report about a state you already acknowledged updates the TODO instead of ringing again.
    • Progress is its own sub-state: abandoned cycles end at command boundaries, and titles name the command.
  • Deferral is bounded at 30 s, and the command-exit minimum runtime is its own constant.
  • WATCHING keys on the command the line waits on.
    • Skips wrappers: sudo, npx, caffeinate, …
    • A list keys on its last command, and runners key by script (pnpm devpnpm test, while a bare pnpm rule still covers both).
    • Understands heredocs and loops.
    • fish 4 133;C command lines are read.
    • bash now reports the full submitted line, and fzf bind -x keys no longer count as commands.
  • Speech and push are scheduled host-side and gated on presence.
    • Speech needs you not engaged with that pane.
    • Push needs every viewer away, and in VS Code an active window counts as present.
    • The host sends push itself, so a suspended window no longer delays the walked-away channel.
  • Input edge cases fixed:
    • ConEmu OSC 9;<n> no longer rings.
    • Panes no longer inherit another terminal's identity env vars (e.g. VTE_VERSION, WT_SESSION, TMUX).
    • Overflowed Doors show their rings and TODOs.
    • A helper promoted mid-command keeps its command.
    • Browser-pane clicks no longer hide TODOs.
    • Mobile taps and input acknowledge.
  • iTerm2 identity 3.6.6. This is the lowest version that unlocks Claude Code's per-turn OSC 9;4 progress. 3.7.0 adds sequences we'd mishandle. A turn ending on a pane you're not engaged with rings "claude finished".

Architecture

  • Standalone's AlertManager moved from each window's webview into the sidecar, the same shape as VS Code's extension host.
    • One shared alert host and client serve both hosts over one alert:command wire.
    • A reload keeps rings and TODOs, and alert timers no longer run in a throttled WKWebView.
    • The per-window Workspace-transfer snapshot machinery is deleted.
  • User input rides the PTY write, so the host acknowledges before the echo. The resize grace rides the PTY resize, and the persisted alert rides the spawn.
  • Rust routing:
    • one "showing" route for every id-keyed event, which fixes a pty:exit before a transfer's mark;
    • window stamping for alert commands;
    • await results go only to their window;
    • the tool-event hold is gone.

Testing

  • Verified: full root pnpm test on the merged tree, standalone tsc --noEmit, and Rust cargo test --lib (115). lib vitest has 4009 tests.
    • Every new test was checked by reverting its fix and watching it go red.
    • Regression suites replay recorded Claude Code PTY timelines, captured under Dormouse's env at iTerm 3.5.0 and 3.6.6, through the real host parse site.
  • Live checks in the browser harness:
    • a watched settle rings;
    • a reload keeps it;
    • typing acknowledges before the echo;
    • dor await --until quiet resolves;
    • speech is spoken once for an unwatched pane and never for the watched one.
  • Not verified live:
    • VS Code multi-webview presence and window.state wiring (needs an Extension Development Host);
    • mobile taps on a real phone;
    • a real permission prompt end-to-end in the desktop app.

Deferred (spec ## Future)

  • Cross-window VS Code presence.
  • OS-level idle time as a push gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF

nedtwigg and others added 14 commits September 23, 2026 16:25
- WATCHING keys on the last command of a list, past transparent wrappers,
  with script runners keyed as `<runner> <script>`; a bare runner rule still
  covers every script.
- bash reports the whole submitted line when history proves it, and a
  `bind -x` key (fzf) is no longer a command on bash 4+.
- fish 4 / kitty command lines on OSC 133;C are read.
- ConEmu OSC 9;<n> subcommands no longer ring.
- Panes no longer inherit another terminal's identity variables.
- Overflowed Doors surface their rings and TODOs on the "N more" arrows.
- A helper promoted mid-command keeps its running command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Command lines from OSC 633;E and 133;C share one bound/decode/sanitize
  path that keeps line breaks, so a multi-line command keys on its last
  command outside unit tests too.
- The tokenizer emits grouping parentheses and keeps command substitutions
  and array assignments whole, replacing a quote-blind regex pass.
- Rule lookup lives in one getter used by the terminal context and the
  tutorial detector; the overflow arrow reserves one measured width.
- Smaller cleanups: one OSC 9 subcommand switch, shared decoders and flag
  specs, one env-strip regex, test helpers, trimmed prose.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- WATCHING, terminal reports and command exits are sources on one ring:
  every ring sets TODO when it opens, and an await, resumed work or rule
  removal takes back only the TODO and detail that ring set.
- Notification detail follows one richness order, so a bell never
  replaces an OSC 9 message or an exit code.
- A report about a state the user already acknowledged, with no output
  since, updates the TODO instead of summoning again.
- Progress is its own sub-state: updates never take a ring away, command
  boundaries end abandoned cycles silently, titles name the command.
- Deferral behind animation is capped at 30 s; the command-exit minimum
  runtime is its own constant; hosts apply one chunk's events in order.
- WATCHING detail is never persisted, so older builds keep restoring.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Persisted sessions tolerate an unknown notification source (dropping
  only that detail); the writer strips to the sources strict older builds
  know, as a frozen allowlist.
- The ring keeps one richness helper (also used by deferral, which now
  keeps the richer report), a watching record, and the receipt it replaced;
  derived and duplicated flags are gone.
- The acknowledged-report rule lives in the completion dispatcher; the
  withdrawal reason decides whether the detector resets.
- One ordered, timestamping event dispatcher serves the VS Code host, the
  fake adapter, and live replay.
- Tests share arm/finish/heartbeat helpers; stale "track" wording fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The renderer reports presence (window focused and visible, with typing,
  mouse, or wheel input within the inactivity timeout) and focus (the
  passthrough pane, or a context's source); a Session is engaged when both
  point at it. VS Code webviews and standalone windows are separate viewers.
- A completion while engaged is held, not dropped: acknowledging or
  leaving drops it, going idle rings it. The walk-away permission prompt
  now reaches the user.
- Only acknowledgement clears a ring. Any keystroke, paste, or drop into a
  pane clears its TODO (Windows, kitty, and one-key answers included); a
  click, Door, or tap keeps it. `dor` reveals move focus without
  acknowledging; browser panes never acknowledge.
- The user's own keystroke echo no longer counts as work.
- Mobile taps and input acknowledge through the mobile composition.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
- Entering passthrough no longer acknowledges on its own: pane clicks,
  zoom, the dev-server chip, Doors, and keyboard Enter acknowledge at their
  handlers, so spawns, splits, promotions, `dor` reveals and a raw frame
  taking focus can never clear a ring.
- User input is one `writePty(…, { userInput })`: the host clears the
  ring and TODO and opens the echo window before writing (one message per
  keystroke in VS Code). Mobile input uses the same helper.
- Rings and held completions share one source-set shape and one
  hold-or-deliver path; viewers store only present focus.
- Shared window-focus source, ref-counted engagement reporter, one VS Code
  realm teardown, `inactivityTimeout` naming, shared test utilities.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
- One AlertManager per app, beside the PTYs and the parse site, fed in
  stream order; windows are viewers (stamped by label) that send commands
  and render `alert:state`, exactly like VS Code's extension host.
- A window reload keeps its rings and TODOs; alert timers no longer run
  in a throttled webview; awaits survive a Workspace transfer and the
  per-window transfer snapshot machinery is deleted.
- User input rides the PTY write so the sidecar acknowledges before the
  echo; remote Client writes acknowledge and remote resizes get the resize
  grace in both hosts; a killed VS Code panel removes its entries.
- Exited PTYs stay owned until killed, so clearing their TODO still
  reaches the window.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
- One host module (commands, per-realm awaits with synchronous answers on
  realm end, validation, store binding) and one client serve both hosts
  over one `alert:command` wire; the fake adapter runs the real host.
- One owner PTY stream and one Client-input wrapper feed the manager in
  both hosts; the resize grace rides the PTY resize and the persisted
  alert rides the spawn, so no separate resize, seed or remove calls.
- Alert entries go when ownership drops (kill, orphan reap), fixing the
  leak on window close; a `sync` command re-syncs a reconnected viewer.
- Rust: the tool-event hold is gone, one "showing" route covers every
  id-keyed event (fixing an exit before a transfer's mark), await results
  go to their window, and stamping/serialization are shared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
- One delivery scheduler inside the shared alert host (VS Code extension
  host, standalone sidecar, fake adapter): once per sink per episode, the
  deadline fixed at the episode's start, rechecked at the deadline.
- Speech needs the user not engaged with that pane; push needs every
  viewer away. A failed gate consumes the delivery.
- Realms publish their Sessions' Workspace overrides; the host resolves
  them against the settings it holds and sends `alert:deliver` to the
  owning viewer, which speaks or pushes with the pane's label.
- Deletes the renderer ring watcher, its receipts, and the Workspace
  transfer's delivery pause/resume/snapshot machinery.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
…per command

- The shared alert host sends push itself (standalone: the sidecar's
  Burrow service; VS Code: this window's service or the broker over the
  peer link), titled by labels each window publishes with its Workspace
  overrides on a throttle; windows only speak (`alert:speak`), so a
  suspended window no longer delays the walked-away channel.
- A VS Code window in active use counts as presence for push.
- A command start ends the "already acknowledged" state, so a command
  that stays silent until its notification rings again.
- One scheduler map and per-sink table; the push-device fence lives with
  the device list; shared speech test stub; routing and gates stated once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
- 3.6.6 is the newest iTerm2 whose escape-code additions Dormouse fully
  supports (OSC 9;4 progress, color-scheme reporting); Claude Code now
  reports each turn as a progress cycle, so a turn ending on a pane the
  user isn't engaged with rings "claude finished".
- Color-scheme queries are answered explicitly and classified as
  terminal reports, never input.
- A regression suite replays a recorded Claude Code turn through the real
  host parse site for all four engagement outcomes.
- The delivery scheduler keeps a Session's published policy and label
  through a respawn and drops it only on removal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
- The identity rule advertises the lowest iTerm2 version that unlocks the
  version-gated behavior Dormouse supports, never one whose additions it
  would mishandle; the recorded Claude turn is described once, in its test.
- One step runner and episode collector serve the engagement and manager
  tests; the VT extension options live in their own module.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
- WATCHING keys skip here-document bodies and treat shell reserved words
  as grammar, so heredocs and loops key on the command they run.
- Presence does not lapse while an iframe browser pane holds focus (its
  input is invisible to the window), so push waits for a real absence.
- A partial publication no longer drops a Session's delivery overrides
  and silently cancels its pending speech or push.
- Mobile acknowledges a tap, not a scroll swipe or radial drag; zoom and
  unzoom always acknowledge.
- A viewer's `sync` re-sends only its own Sessions and stores to it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: b0d6dd7
Status: ✅  Deploy successful!
Preview URL: https://2cf97dc1.mouseterm.pages.dev
Branch Preview URL: https://alert-improve.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.

This is feedback on work in progress, not a merge verdict. Mark the PR ready when you want the full review. The inline comments cover four behavior bugs and one stale spec sentence: two in the WATCHING key, one in remote input acknowledgement, and one in held-report deferral.

Comment thread lib/src/lib/terminal-state.ts
Comment thread lib/src/lib/terminal-state.ts Outdated
Comment thread lib/src/host/remote/sidecar-entry.ts Outdated
Comment thread lib/src/lib/alert-manager.ts Outdated
Comment thread docs/specs/alert.md Outdated
@nedtwigg
nedtwigg marked this pull request as ready for review September 24, 2026 07:25
@argos-ci

argos-ci Bot commented Sep 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
storybook-chromium (Inspect) ⚠️ Changes detected (Review) 23 changed Sep 24, 2026, 8:10 AM
storybook-webkit (Inspect) ⚠️ Changes detected (Review) 25 changed Sep 24, 2026, 8:10 AM
Deployment Status Branch Updated (UTC)
preview (Open) Ready alert-improve Sep 24, 2026, 8:10 AM

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

Three findings in addition to the draft review's open threads.

Comment thread lib/src/lib/terminal-state.ts
Comment thread lib/src/lib/terminal-state.ts Outdated
Comment thread standalone/src-tauri/src/lib.rs
nedtwigg and others added 2 commits September 24, 2026 00:51
… bound, hand-back ownership

- The command tokenizer keeps a redirection's `&` or `|` in its word
  (`2>&1`, `<&3`, `>|`, `&>`, `&>>`), reads `|&` and fish's `&|` as pipes,
  and drops a comment (`#` starting a word) through the end of its line; the
  watch key skips redirections, so `make 2>&1 | tee log`, `pnpm build &> log`
  and `claude # x; then deploy` key on the program they run.
- fish's grammar words are grammar: `end` closes, `and` / `or` / `not` /
  `begin` open, and a fish `case` line is patterns alone; POSIX's `case WORD`
  newline `in` still works.
- `isWatchKey` checks the drive prefix on the program only, so
  `npm run b:dev` keys `npm b:dev` instead of falling back to `npm`.
- A remote Client's write acknowledges only when it holds more than mouse
  reports, in both hosts, through `alertedPty.writeClientInput`.
- A held report whose deferral already came due rings on idle escalation
  instead of starting a fresh deferral ceiling.
- `hand_back` returns a transferring id to its source even after the target
  window's close dropped its owner; a killed id stays unowned.
- Drop the stale renderer-local episode claims; `AlertState.episode` is
  required. Restore the original inactivity-timeout helper copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
- Opening a ring no longer sets `todo`; the ring shows its own detail, and
  public `notification` is the ring's detail while ringing, else the TODO's.
- Acknowledging without input (click, Door, zoom, tap), dismissing (`a`), or
  `t` on a ringing Session turns the ring into a TODO carrying its detail;
  typing, a paste or drop, and the pill clear both. `t` on a standing TODO
  still turns it off.
- A withdrawal (await, resumed work, rule removal) takes only the ring and
  its detail; the prior-TODO restore machinery is gone. A dropped hold or
  deferral leaves no TODO.
- An unacknowledged ring persists as the TODO a look would leave, with its
  detail, within the strict-reader source allowlist.
- Specs, the VS Code README, the tutorial hint and detector comment, and the
  stories and fixtures that primed a fresh ring with TODO follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwN55bUVmCNmPiX7vHGCVF
@nedtwigg

Copy link
Copy Markdown
Member Author

Two product changes from the owner ride along with the round-1 fixes:

  • TODO is now a two-step (b0d6dd7). A ring never sets TODO by itself: the ring and its detail are the summons. Looking at it without typing (click, Door, zoom, a, t, a mobile tap) turns the ring into a TODO carrying its detail; dealing with it (typing, paste or drop, a Client's keystrokes, the TODO pill) clears both. Awaits, resumed work and rule removal take only the ring and its detail, so the old "restore what the ring found" machinery is gone. A dropped hold or deferral still leaves no TODO. A ring nobody acknowledged is saved as the TODO a look would have left (within the strict-reader source allowlist), so an unseen alarm survives a restart.
  • The Settings inactivity-timeout helper text is back to "User has walked away after this much inactivity." (7fce3be).

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
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