Skip to content

fix: show Claude progress updates written between tool calls - #55299

Open
moritzWa wants to merge 3 commits into
anomalyco:devfrom
moritzWa:show-progress-updates
Open

moritzWa wants to merge 3 commits into
anomalyco:devfrom
moritzWa:show-progress-updates

Conversation

@moritzWa

Copy link
Copy Markdown

Issue for this PR

Closes #55298

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Claude models that write progress updates (Claude 5.5+, Fable 5+, Mythos 5.1+) send the notes they write for the user between tool calls as thinking blocks marked narration. opencode loses them in two places, and this PR fixes both.

Request (packages/ai). Anthropic's default adaptive-thinking display is omitted, which also empties progress updates. For models that write progress updates, anthropic-messages.ts now defaults adaptive thinking to display: "summarized". This follows https://platform.claude.com/docs/en/build-with-claude/thinking#progress-updates. It runs before the existing block-binding default and doesn't change:

  • an explicit display
  • disabled or between_tools thinking
  • models that don't write progress updates (no adaptive thinking is turned on for them).

Model detection reuses claudeVersion as writesProgressUpdates in claude-model.ts.

TUI. A progress update arrives as a reasoning part. The only thing marking it is the block type inside the signature. progressUpdate() checks for that marker, and SessionPartView renders those parts with TextPart instead of ReasoningPart. In rows.ts, history treats them as text for grouping. The signature only arrives with session.reasoning.ended, after the deltas have already grouped the part as thinking, so the live path rebuilds rows at that point.

Summarized display isn't guaranteed to be verbatim. It's still the only way to get the note's text to the user, and an empty block shows nothing.

How did you verify your code works?

  • packages/ai: bun test test/provider/anthropic-thinking-binding.test.ts passes (17 tests). New cases cover which models get the display default, explicit adaptive thinking without a display, and that explicit settings are kept.
  • bun typecheck passes in packages/ai and packages/tui.
  • packages/tui: bun test passes 1494 tests and fails 9. The same 9 fail on unmodified dev: dialog-shell-output timeouts, plugin-keymap and plugin-source.
  • I diagnosed this from real Sonnet 5.5 sessions where a draft written before a tool call never appeared. The stored reasoning part had empty text and a narration signature.

Screenshots / recordings

None.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot 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.

The approach makes sense, but I could only partly verify it: a real narration block needs a Claude 5.5 response, and there are no provider credentials here. The ai tests (17/17), the type checks in packages/ai and packages/tui, and the existing session-rows/verbosity tests (41/41) pass. A throwaway test against reduceSessionRows showed a reasoning part with a narration-marked signature becomes a text row, and an ordinary one stays a collapsed reasoning group. Screenshots of a real session before and after would help, since I couldn't produce them.

Public API: unchanged. The only request change is a default display: "summarized" on Anthropic adaptive thinking that doesn't set its own display, for Claude 5.5+, Fable 5+ and Mythos 5.1+.

Concerns:

  • progressUpdate() throws on any reasoning signature that isn't valid base64. That error escapes from reduceSessionRows, the live session.reasoning.ended handler and SessionPartView, so it can break the whole transcript (see the inline comment).
  • Progress updates are detected by decoding the signature's undocumented byte layout, and only in the TUI. The desktop app, web app and run output would still hide these notes. It would hold up better if the Anthropic protocol in packages/ai marked these parts (for example in the reasoning part's provider state), and the TUI only read that flag.

export function progressUpdate(state: SessionMessageAssistantReasoning["state"]) {
const signature = state?.signature
if (typeof signature !== "string") return false
return atob(signature.slice(0, 64)).includes("B\tnarration")

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.

atob throws InvalidCharacterError when the input isn't valid base64. That includes base64url, other characters, or whatever an Anthropic-compatible gateway or third-party provider puts in signature. This runs on every reasoning part with a string signature, inside reduceSessionRows, the live session.reasoning.ended handler and the SessionPartView render. One odd signature can therefore break the whole session view. I checked: reduceSessionRows with state: { signature: "not base64 -_ signature!" } throws.

This needs at least a try/catch that returns false. The "B\tnarration" byte match also depends on an undocumented signature layout. Anthropic's docs describe a beta display: "updates" mode for this case, where any thinking block that arrives with text is a progress update. Doing the detection in the Anthropic protocol (packages/ai), where the provider is known, and storing a flag on the part would avoid decoding provider signatures in the UI. It would also let the desktop app, web app and run output show these notes.

@moritzWa

Copy link
Copy Markdown
Author

Fixed the crash in c797ff2: progressUpdate() now decodes with Buffer.from(..., "base64"), which ignores invalid characters instead of throwing. New test in session-rows.test.ts builds rows with a not base64 -_ signature! reasoning part next to a real narration one. It fails on the previous commit with InvalidCharacterError and passes now. The branch also merges cleanly with current dev.

On moving detection into packages/ai: agreed it is the better home, and it would let the app and run output show these notes too. I kept it in the TUI because the only detector I can verify is the signature's narration marker, and display: "updates" is a beta I can't exercise here. Which do you prefer: (a) merge this TUI fix as is and do the packages/ai flag as a follow-up, or (b) I move it into the Anthropic protocol in this PR, marking the part in its provider state?

Still no before/after screenshot of a live Claude 5.5 session; only the unit-level checks above.

@opencode-agent opencode-agent Bot 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.

The new commit fixes the crash I raised: progressUpdate() now uses Buffer.from(…, "base64"), which ignores characters it doesn't recognise instead of throwing. The added session-rows test covers that case and the narration case. It passes, and the TUI type check passes. I still couldn't check this end to end, because a real progress-update block needs Anthropic traffic.

Public API: unchanged.

Still open from before: narration blocks are detected by decoding the signature in the TUI, not in packages/ai, so other clients won't show these notes.

@moritzWa

Copy link
Copy Markdown
Author

@kitlangton could you take a look when you get a chance? It's still mergeable and checks are green. It covers the direct Anthropic route (narration blocks written between tool calls). @drsmile1001's claude-agent route is separate and would need the signature kept in the stream converter.

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.

Claude progress updates written before a tool call never show

1 participant