Skip to content

THI-416: Done banners say Claude's last reply, not the status bar - #50

Merged
tdody merged 6 commits into
mainfrom
thibaultdody/thi-416-done-banners-show-claudes-status-bar-instead-of-the-reply
Oct 30, 2026
Merged

tdody merged 6 commits into
mainfrom
thibaultdody/thi-416-done-banners-show-claudes-status-bar-instead-of-the-reply

Conversation

@tdody

@tdody tdody commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes THI-416. A "{pane} is done" banner's body was a line read off the pane's screen (w.last_line), and on live panes that line was rarely the reply. In prod's /api/state it was usually one of these:

  • the turn-timing line: ✻ Brewed for 42s · done 1:27 PM;
  • a ⎿ line: ⎿ Allowed by auto mode classifier, ⎿ Tip: …;
  • the right-aligned new task? /clear to save 433.4k tokens hint above the composer;
  • for a named session, the footer: the custom statusline and the ⧉ row. Claude prints the session name in the composer's top rule (──── name ─). DIALOG_RULE_RE doesn't match a labeled rule, so the composer went undetected and THI-434's footer cut never applied.

The reply, from the transcript (turns.py, window_view.py)

  • For a done Claude pane, /api/state now carries last_reply. turns.last_reply_for_session reads the session's JSONL backward from the end to the newest assistant text.
    • Bookkeeping rows are skipped: turn_duration, artifact-autoreact-ledger, mode, and meta and sidechain events.
    • It returns None once a prompt follows that text, so a new turn never shows the previous reply.
    • Markdown is flattened (links to their text, no ** or backticks, no headings or code blocks) and the result capped at 400 characters.
  • Cost: under 1 ms uncached on live transcripts (2–54 MB). Results are cached per session on the file's size and mtime, so a done pane costs one stat per poll. Working panes aren't read.
  • There's no newest-mtime-in-cwd fallback: a wrong transcript would put another pane's words in the banner.

Banner (notify/copy.js): last_reply, then recap, then last_line, then "Finished.".

Screen fallback (panes.py), for when no transcript is found:

  • _composer_rows accepts a named top rule (_COMPOSER_TOP_RE). The lower rule and a dialog's rule must still be plain.
  • _last_meaningful_line returns the start of the newest reply on screen: its ⏺ row plus the wrapped rows under it, up to the first blank row. Its last row had read as half a sentence; a banner said "that use it would fail.". It returns "" when the newest ⏺ is a tool call, including collapsed groups and MCP calls, so the banner says "Finished." instead of a tool header. With no ⏺ on screen, it falls back to the closest line that isn't chrome or the turn's own (✻, ⎿, rows indented 20+ columns). All of this is local to it, so _detect_asked_question reads the same lines as before.
  • This matters beyond panes without a transcript. After a server update, an open page runs the old notification code (recap || last_line) until it's reloaded, and that's the code that produced the half-sentence banner on this machine.
  • _split_buffers strips every escape (_strip_escapes), OSC 8 hyperlinks included, not just SGR.

Docs: docs/channels.md (how last_reply is read), invariant 2 (the named rule), and the done row of the native-notifications spec.

Worth a look in review

The named-rule fix also changes Reply on named sessions. composer_ready used to find no composer there, so a banner's Reply always got a 409. Now it can type the reply. Every other check is unchanged:

  • one ❯ row, empty or a ghost suggestion;
  • no dialog above;
  • composer_holds before Enter.

A typed draft under a named rule is still refused (tested). If you'd rather keep Reply off for named sessions, the change is limited to _composer_rows.

Test plan

  • uv run pytest -q: 2027 passed. New:
    • test_turns.py: the reply under bookkeeping, a notification turn, None after a prompt, meta and sidechain skipped, the markdown flattening, the cap, a read past one chunk, giving up past the reach, a torn row, and the per-session cache.
    • test_window_view.py: last_reply published for a done pane only.
    • test_panes.py:
      • last_line cases for a named session's footer with an OSC 8 link, a ⎿ tip under the reply, the /clear hint, the start of a multi-paragraph reply rather than its last row, and turns that ended in a tool or MCP call;
      • a named session's composer is detected and composer_ready;
      • the three cases that expected ✻ Brewed for … now expect the reply line above it.
  • npx vitest run: 1299 passed, including the done body's priority (last_reply > recap > last_line > "Finished.").
  • bin/check: passes (3 pre-existing biome warnings in files this PR doesn't touch).
  • static/dist rebuilt.
  • Dev server (:17375) on live panes:
    • both done panes' banner bodies were their real first sentence, e.g. "I pushed 6caa5f4b35 to #7574, covering 12 of Brendan's 18 comments.", built by running payloadFor over the live /api/state;
    • every idle pane's last_line was the start of its newest reply, including the named session and the one under the /clear hint.
  • A real macOS banner after deploy.

🤖 Generated with Claude Code

tdody and others added 6 commits October 29, 2026 18:43
…re recap and last_line (THI-416)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eply, read backward from the end and cached per session (THI-416)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…session's composer, the turn's ✻/⎿ lines, right-aligned hints and OSC 8 links no longer pass for it (THI-416)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the named composer rule (invariant 2) (THI-416)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t paragraph joined, not its last row; empty after a tool call (THI-416)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@tdody
tdody merged commit a640643 into main Oct 30, 2026
2 checks passed
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