Skip to content

feat(status): OSC 7501 program status — working state, exact waiting dots, heuristic fallback - #190

Merged
mortenaslo merged 4 commits into
mainfrom
feat/osc7501-status-main
Nov 6, 2026
Merged

mortenaslo merged 4 commits into
mainfrom
feat/osc7501-status-main

Conversation

@mortenaslo

Copy link
Copy Markdown
Contributor

Part of #154 (Part 1). Part 2, the timeline and dashboard, is still open. Re-land of #189, which was merged into feat/bundled-conpty seconds after #188 had already been squash-merged into main, so it never reached main. These are the same four commits, cherry-picked onto current main without conflicts. The tree is identical to #189 plus #184.

Adds the Program Status Protocol (OSC 7501), so programs can report their own state: working (with progress), blocked on permission or input, done, or error. The existing regex detection stays as the fallback.

Claude Code reports out of the box (with #188)

  • CSM answers Claude's OSC 7501 ; ? query with the raw reply. In the real app the trace shows that reply written just before xterm's DA1 answer, and Claude logs programStatus=yes (probe: OSC 7501 reply).
  • In a harness, a prompt produced idle → working → done reports.
  • Setting ProgramStatusProbe (Auto / On / Off, default Auto). Auto answers only on the bundled ConPTY. On the built-in ConPTY, conhost answers DA1 first, so a reply would arrive late as unsolicited input.
  • Every reply is gated:
    • only an exact ? query is answered;
    • at most one reply per 5 s and 3 per session;
    • the reply is written synchronously, so it reaches the program before xterm's DA1 answer.

What changes

  • Parser: a strict, WPF-free parser drops a malformed report whole. It checks the alphabet, id/app syntax, limits, base64, UTF-8, control characters and U+2028/2029, and strips format characters per code point.
  • Record tree: each report replaces its record, clear also removes children, there is a 256-record cap with eviction, records expire on exit and prompt start, and attention transitions are tracked per record.
  • Combined state, by precedence blocked > working > error > done > idle:
    • working: a pulsing ⟳ working · 40% in the sidebar and ⟳ 40% on the toolbar, animated only while visible.
    • blocked + permission: orange dot.
    • other blocked: green dot.
    • any record entering blocked, done or error: badge plus a rate-limited toast. A root done still badges while a background child is working.
  • Fallback: regex detection stands down while any 7501 record exists. It re-arms when the records are cleared, when a prompt start or exit leaves none, or via the new sidebar item Clear status.
  • Acknowledging: a real keystroke or focusing the session acknowledges done/error. xterm's automatic replies do not.
  • After exit: live-state reports that arrive after the process has exited are ignored.
  • Runtime only: nothing is written to state.json.

Reviews

Two read-only review rounds. Fixed as a result:

  • the query reply could be triggered by any output;
  • a background child hid the root's done;
  • a status could get stuck with the fallback permanently off;
  • the parser deviated from the spec in places;
  • the exit race;
  • a stale dev-build setting key could force replies off (fixed by renaming the setting).

Tests

942 unit tests pass (this branch includes #188).

Manual verification

  • Start a claude session and give it a prompt. Expect ⟳ working in the sidebar and toolbar, then the attention badge when it finishes.
  • Ask Claude for something that needs permission. Expect the orange dot.
  • Right-click → Clear status. Everything clears and regex detection is back.
  • Without Claude, in pwsh with $e=[char]27: "$e]7501;state=working:progress=40$e\". Expect ⟳ working · 40%.
  • Minimize the window. The pulse stops, and resumes on restore.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

mortenaslo and others added 4 commits November 6, 2026 18:54
…dots, heuristic fallback

Support the Program Status Protocol (OSC 7501) so programs report their own
state. Part 1 of #154.

- terminal-init.js: OSC 7501 handler (query -> programStatusProbe, report ->
  programStatus) and an observe-only OSC 133 A handler (promptStart).
- TerminalBridge answers the `OSC 7501 ; ?` probe with fixed, app-authored
  bytes, encoded as win32-input-mode key events: the inbox conhost drops OSC
  sequences written to the ConPTY input as plain VT (measured).
- Services/ProgramStatusPayload: strict WPF-free parser/validator (alphabet,
  base64 + UTF-8, control chars, spec limits; bad reports dropped whole).
- Services/ProgramStatusRecords: record tree with replace/clear/eviction,
  exit lifetime, acknowledge, and blocked > working > error > done > idle.
- SessionViewModel maps the aggregate: working -> new pulsing
  "working · N%" indicator (sidebar + toolbar), blocked+permission ->
  approval, other blocked -> input, done/error -> attention badge with a
  rate-limited toast. AlertDetector stands down once 7501 is seen; user
  interaction acknowledges done/error but keeps live states.
- Docs: CLAUDE.md section incl. the measured ConPTY limitation (conhost
  answers DA1 itself, so Claude 2.1.319's probe still settles to "no").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
… attention, re-arm

Review fixes for the OSC 7501 program-status work (#154 Part 1).

- Probe reply OFF by default (AppSettings.AnswerProgramStatusProbe). Under the
  inbox ConPTY it always loses to conhost's own DA1 answer and only reaches the
  program as late stdin starting with ESC. When enabled, ProgramStatusProbeGate
  requires an exact "?" query, win32-input-mode requested by this session's
  ConPTY (ESC[?9001h/l tracked on output), 1 reply per 5s, 3 per session; the
  write goes to the thread pool. Reports are always parsed.
- Attention judged per record: a root done/error/blocked badges and toasts even
  while a working child holds the aggregate; the badge clears only when no
  record is in an attention state.
- Re-arm the heuristic when the store empties via clear / prompt start / exit,
  plus a sidebar "Clear status" item. Not on acknowledge.
- Exit race: working/blocked/idle reports after PTY exit are ignored.
- Parser: id segments and app use [A-Za-z0-9_.+-]{1,32} (bad app = absent),
  a key over 16 bytes drops the report, format chars stripped by rune (tag
  block), U+2028/U+2029 rejected.
- Working pulse runs only while visible (IsVisibleChanged, Loaded/Unloaded,
  minimize).
- Acknowledge on real key events (onKey) or focus, not on every onData;
  allocation-free per-keystroke guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
Builds on the bundled ConPTY: OpenConsole passes OSC input through and
forwards DA1 to xterm.js, so the raw reply reaches the program in order.
Measured with real claude 2.1.319 behind it: programStatus=yes, and a
trivial prompt produced idle -> working -> done reports.

- AnswerProgramStatusProbe becomes bool?: null (default) = answer when the
  session's PTY uses the bundled ConPTY, not on inbox; true/false force it.
- Bundled: raw ESC]7501;?ESC\ reply, no win32-input-mode requirement.
  Inbox (forced): win32-input-mode encoded, only after ?9001h, as before.
  Exact "?" query, 5s rate and 3-per-session cap kept for both.
- The reply is written synchronously on the UI thread, deliberately: it
  must reach the PTY before xterm's DA1 answer, which the next 'input'
  message writes on the same thread; a pool write races it.
- Tests for the enable rule, raw reply, and gating on the bundled path;
  CLAUDE.md, docs/shell-integration.md and README updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
…Off)

Development builds saved "AnswerProgramStatusProbe": false as a plain bool
meaning "experimental, off"; under the later nullable meaning that read as
"force off" and would have silently disabled the probe answer on the bundled
ConPTY. The setting is now AppSettings.ProgramStatusProbe, an enum
ProgramStatusProbeMode { Auto, On, Off } defaulting to Auto, so the stale key
is ignored. Tests cover the stale-key case and the round trip; CLAUDE.md
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
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.

1 participant