Skip to content

Start a busy run only from input or an OSC 9;4 report, so a repaint after a click never rings - #1222

Merged
nedtwigg merged 8 commits into
mainfrom
alert-input-gate
Oct 22, 2026
Merged

nedtwigg merged 8 commits into
mainfrom
alert-input-gate

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Clicking into an idle Claude Code pane rang a false "claude went quiet" once you left. The click sends a focus report (ESC[I), and Claude answers by repainting real text. In one report the repaint began 1.4 s after the click and lasted 1.8 s. The output/silence detector read that as work, so when the user moved away the settle rang. Two diagnostic reports (2026-10-19, surface:12 and surface:42) show the same sequence: click, repaint, leave, ring.

The rule: a busy run must begin within cfg.alert.workStartWindow (5 s) of either:

  • input: any PTY write except a terminal reply or a mouse report, which includes keys, a paste, a Client, dor send and launches;
  • an active OSC 9;4 report, so a turn Claude starts by itself (a background task waking it, /loop) still reads as busy and still defers a ring it owes.

Output that would start a run later is ignored entirely, the way echo is: a repaint on focus or resize, or a status-line tick. Once a run has begun, all of its output counts until it settles.

  • One write hook. AlertManager.onWrite(id, data, { userInput }) hears every PTY write before it reaches the PTY: it acknowledges human input and opens the work-start window. alertedPty.write (both hosts and Pocket's Burrow path) and FakePtyAdapter.writePty call it instead of their copied prologues.
  • Classification. inputCanStartWork in terminal-report-filter.ts reads a chunk the way the renderer's input branch does. It returns early on a write that doesn't start with ESC, so a large paste never goes through the regexes.
  • Resize grace deleted. The gate covers what the 500 ms grace after each resize covered. QuiesceDetector.onResize, AlertManager.onResize, alertedPty.resize and cfg.alert.resizeDebounce are gone, and the hosts resize the PTY directly.
  • A wheel tick is input. A third report (2026-10-21, surface:164) was a wheel scroll through Claude Code's transcript, which repaints for as long as the scroll lasts. A wheel report sent to the program now acknowledges as user input, like a keystroke, locally and from a Client. It never opens the work-start window, because scrolling asks for no work: a repaint trailing the scroll, or a status-line tick after it, is still ignored. It also leaves a finalized selection alone, as a scroll did before. Hover, drag-motion and button reports stay passive; a click acknowledges through the Pane. stripPassiveMouseReportsFromInput in terminal-report-filter.ts draws the line, and inputCanStartWork takes userInput so that a human's reply-shaped key (Shift+F3) still starts work while a wheel tick doesn't.
  • Spec. docs/specs/alert.md → WATCHING Track states the rule. The rationale records the reports and the cost.

Behavior changes:

  • Scrolling a program that asked for wheel reports acknowledges it with input. It clears the ring and the TODO, as typing does. That includes a scroll from Pocket.
  • Output nothing asked for no longer forgets an acknowledgement or cancels a --until quiet grace window. So a repaint after a click doesn't turn a stale idle ping into a fresh ring, and an await on an idle Session resolves idle instead of being kept parked by a repaint.
  • A watched command whose first output comes more than 5 s after Enter never reads as busy. The default watch list (claude, codex, pi) starts output at once.
  • Output that resumes after a mid-run settle, with no input or OSC 9;4 report inside the window, is ignored too. An owed ring stays up through it, and a later await resolves on that ring. Letting an owed ring reopen the gate would bring back a resize or status-line repaint hiding a ring that is already showing. The default agents keep a spinner going through a turn, so they never settle mid-run.

Tests:

  • New tests cover the gate in the detector and the field scenario through alertedPty:
    • a click, a focus report and a 2 s repaint, then leaving, never rings;
    • dor send followed by work rings;
    • a progress report with no input defers an owed ring;
    • a userInput write that looks like a reply (Shift+F3, ESC[1;2R) still starts work;
    • inputCanStartWork unit cases.
    • wheel reports in SGR, URXVT and X10 encodings, with modifiers, acknowledge as keystrokes; hover and button reports don't;
    • scrolling an idle program for 3 s while it repaints never rings, nor does a repaint that starts 1.5 s after one tick and lasts 2 s;
    • a wheel tick leaves a finalized selection alone;
    • a Client's wheel report acknowledges, while its tap or hover report doesn't.
  • Each new test goes red with its line deleted.
  • Existing tests now give the program input before its work, through shared press / work helpers in alert-manager-test-utils.ts. The website's alert-program tests type through FakePtyAdapter.writePty, the way the playground does.

Verified locally:

  • lib: 6028 passed, plus tsc -b.
  • website: 448 passed.
  • vscode-ext: 229 passed, plus tsc.
  • standalone: 296 passed, plus tsc.
  • The spec lint passes.

🤖 Generated with Claude Code

nedtwigg and others added 2 commits October 19, 2026 22:31
…never work

A click into an idle Claude Code pane sends a focus report; Claude repaints
text, the detector called it work, and leaving the pane rang a false
"claude went quiet". A busy run now has to begin within
cfg.alert.workStartWindow of a write that is not a terminal reply or mouse
report, which also replaces the resize grace window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ress start work

/simplify pass: the host and the fake adapter shared a two-step input
prologue; it is now one call. An active OSC 9;4 report opens the work-start
window like input, so a turn Claude starts unasked still reads as busy and
still defers an owed ring. Classification skips writes already marked as
human input and returns early when a write doesn't start with ESC. Tests use
shared press/work helpers, and the dead resize setup is gone.

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

cloudflare-workers-and-pages Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 51c38e3
Status: ✅  Deploy successful!
Preview URL: https://c02dcf93.mouseterm.pages.dev
Branch Preview URL: https://alert-input-gate.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.

One thing to decide before this merges, inline on the gate: output that resumes after a mid-run settle is now ignored once it falls outside the work-start window.

Separately, a one-line leftover outside the diff: the alertedPtys doc comment in vscode-ext/src/message-router.ts still reads "Every PTY write and resize, as the alerts must see it." alertedPty no longer handles resizes.

Comment thread lib/src/lib/quiesce-detector.ts
…the mid-run settle trade-off

The alert-program tests fed the tutorial shell directly, so no input reached
the alert host and the agent's turn never read as busy; they now write through
FakePtyAdapter.writePty as the playground does. Output resuming after a
mid-run settle with no input or progress report inside the window is ignored
like any other; the rationale says so and why an owed ring does not reopen
the gate. The vscode alertedPtys comment no longer mentions resizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dormouse-bot
dormouse-bot previously approved these changes Oct 20, 2026
nedtwigg and others added 3 commits October 20, 2026 17:49
Scrolling Claude Code's transcript sends wheel reports, and the program
repaints for as long as the scroll lasts. As a mouse-only chunk the scroll
acknowledged nothing, so reading a finished turn never answered its ring.
A wheel tick is now user input like a keystroke: it acknowledges, opens the
echo window so the repaint is ignored while the scroll lasts, and lets work
start. Hover, drag-motion, and button reports stay passive, a click
acknowledging through the Pane's DOM handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

The wheel-as-input commit gives a scroll the same false-ring path a click had, and it also changes what a scroll does to a selection. Details inline.

Comment thread lib/src/lib/alert-manager.ts Outdated
Comment thread lib/src/lib/terminal-lifecycle.ts
@dormouse-bot
dormouse-bot dismissed their stale review October 21, 2026 01:02

Superseded by the review on a later commit.

nedtwigg and others added 2 commits October 20, 2026 18:04
… ending a selection

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg
nedtwigg merged commit 0ac956a into main Oct 22, 2026
18 checks passed
@nedtwigg
nedtwigg deleted the alert-input-gate branch October 22, 2026 17:03
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