Skip to content

Chat: keep the conversation to what was actually said (#46)#53

Merged
dnviti merged 1 commit into
chore/version-bump-5.2.0from
feat/issue-46-silent-steps
Jul 26, 2026
Merged

Chat: keep the conversation to what was actually said (#46)#53
dnviti merged 1 commit into
chore/version-bump-5.2.0from
feat/issue-46-silent-steps

Conversation

@dnviti

@dnviti dnviti commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Closes #46. Targets chore/version-bump-5.2.0 (PR #50, the 5.2.0 container), not main.

The problem

A step the agent spent entirely on tools still drew a row in the conversation: a glyph, a clock, three action buttons and a work pill with no sentence beside them. A long task put half a dozen of those between one reply and the next, so skimming the chat meant stepping over rows that said nothing.

What changed

  • A step with no written reply gets no row. MessageBubble renders nothing for a non-user message that would paint no block — independent of the display settings, and while it is still streaming too (the live ribbon is what says the agent is working).
  • The next reply speaks for them. MessageList holds the suppressed ids until a message that does speak comes along and hands them to it. That reply's pill counts the whole stretch — 3 commands · 1 reasoning · 8.1s — and opening it lands on the trace at the start of the stretch rather than at the reply's own last command.
  • Nothing is hidden. The rail still holds every call, the turn strip still counts them, and a trace row or a search hit aimed at a suppressed step now resolves to the reply that speaks for it (rowFor) instead of scrolling to an element that is no longer in the document — TranscriptSearch matches tool output, so that was reachable.
  • Kept: a reply that has opened and produced nothing yet still shows its caret. That is an answer about to arrive, not machinery.

Performance shape is unchanged: the carried ids travel as a joined string rather than an array so React.memo still bites, and the bubble subscribes to the steps it carries (a tool call can report its duration after its own message has closed) — still the per-message tier, never the whole list.

Acceptance criteria

  • A step that produces only tool/action activity and no text does not appear as its own row.
  • All suppressed activity remains fully visible in the trace view.
  • A text reply after silent steps carries a control that surfaces their activity as well as its own.
  • Correct while streaming and when reviewing a past conversation.

Verification

  • npm test — 1381 passing (new cases in chat-messages.test.js, chat-view.test.js)
  • npm run typecheck — clean
  • npm run test:browser — 298 PASS / 0 FAIL, including a new end-to-end check that mounts the real ChatView, counts the rows, clicks the pill and asserts the trace expanded the first silent step, then repeats the case live over streamed events. The suite's virtual-time budget was raised: a run that outgrows it reports no results at all rather than a failure.

🤖 Generated with Claude Code

A step the agent spent entirely on tools still drew a row: a glyph, a
clock, three action buttons and a work pill with no sentence beside them.
A long task put half a dozen of those between one reply and the next.

Those steps no longer get a row. The list holds their ids until a message
that does speak comes along and hands them to it, so the reply's pill
counts the whole stretch and opens the trace at the *start* of it rather
than at the reply's own last command. Nothing is hidden — the rail still
holds every call, the turn strip still counts them, and a trace row or a
search hit aimed at a suppressed step now lands on the reply that speaks
for it instead of scrolling to an element that is not there.

The carried ids travel as a joined string, and the bubble subscribes to
the steps it carries: a tool call can report its duration after its own
message has closed, and the pill that inherited it has to hear about it.
Both stay inside the per-message subscription tier, so a streaming turn
still re-renders one bubble rather than the list.

Kept: a reply that has opened and produced nothing yet still shows its
caret. That is an answer about to arrive, not machinery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 26, 2026 21:59
@dnviti
dnviti merged commit 8691305 into chore/version-bump-5.2.0 Jul 26, 2026
5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the chat transcript rendering so that assistant “steps” which contain only tool calls / reasoning and no written content no longer render as standalone rows, while preserving full trace visibility and making the next spoken reply “carry” the suppressed work (including trace navigation to the start of that stretch).

Changes:

  • Suppress transcript rows for assistant messages with no visible (non-tool/non-thinking) content, and carry their ids forward to the next visible reply for pill counts and trace targeting.
  • Update trace/search-to-transcript navigation so suppressed steps resolve to the visible row that stands for them (rowFor).
  • Add/extend unit and browser E2E coverage for silent-step suppression, carried work pill counts, and trace focusing behavior; increase browser virtual-time budget accordingly.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/client/shell/chat/MessageBubble.tsx Defines the “visible row” rule, carries silent-step ids into work summarization, and targets the work pill at the earliest counted event.
src/client/shell/chat/MessageList.tsx Computes displayed rows per turn (rowsOf) and attaches carried silent-step ids to the next visible message bubble.
src/client/shell/chat/ChatView.tsx Introduces rowFor and updates reveal/scroll behavior so suppressed steps scroll to the correct visible row.
test/chat-messages.test.js Adds unit tests for suppressing tool-only rows, preserving caret behavior, and counting carried work.
test/chat-view.test.js Adds direct tests for rowFor resolution behavior across silent steps and trailing machinery.
test/browser/checks.ts Adds an end-to-end browser check validating row suppression + trace visibility + pill focus (snapshot and live streaming).
test/browser/run.js Increases --virtual-time-budget to accommodate the expanded browser suite.
CHANGELOG.md Documents the user-facing transcript behavior change and the added browser coverage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +337 to +342
export function hasVisibleContent(message: ChatMessage): boolean {
if (message.role === 'user') return true;
if (visibleBlocks(message) > 0) return true;
// Opened and still empty: the caret. See the early return in the bubble.
return message.blocks.length === 0 && Boolean(message.streaming);
}
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