You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Claude progress updates written before a tool call never show #55298
opencode never shows these notes, for two reasons:
With adaptive thinking, Anthropic's default display is omitted, so the progress-update block comes back with empty text. Only the signature is left.
Even when the block has text (display summarized), the TUI renders it as collapsed reasoning, not as a message.
So when the model writes "Here is the draft: ..." and then calls a tool, the user never sees the draft. The model then refers to "the draft above", which isn't on screen.
This is the same bug as #52735, which was closed when v1 was frozen. It still happens on dev (v2).
Steps to reproduce
Use anthropic/claude-sonnet-5-5 (or claude-opus-5-5).
Ask for something where the model writes a message for you and then runs a tool, e.g. "write a two-line poem, then run ls".
The poem doesn't show. In the stored session, the assistant message contains a reasoning part with empty text whose signature decodes to a narration block, right before the tool call.
Thanks for the detailed report. I reproduced both halves of this on 2.0.28 (the latest release), and both still happen on the latest dev (1c28c2fe). Neither check used real Anthropic traffic.
Request side. I fed hand-written Anthropic responses through OpenCode's request and response handling:
With no variant selected, the request for claude-opus-5-5 / claude-sonnet-5-5 turns on adaptive thinking but sends no display. Anthropic then falls back to its default, omitted.
A response containing a progress-update block with only a signature (decoding to narration), followed by a tool call, is stored as a reasoning part with empty text and no text part. That is the shape you described.
Variants already ask for display: "summarized", so only the no-variant default is affected.
TUI side. Even when the note has text, it is shown only as a collapsed + Thought line above the tool call. The closing reply's "the draft above" then points at nothing visible; the recording and screenshot below show this. The test harness's simulated model can't send Anthropic responses, so I sent the note as a plain reasoning block. OpenCode stores both the same way, and the TUI has no special handling for narration blocks.
This is the same symptom as #52735, which was a v1 report. Your PR #55299 targets both halves.
recording.mp4
Reproduction script (drive.ts)
// Reproduces anomalyco/opencode#55298 (TUI side): a Claude progress update arrives as a reasoning part// (an Anthropic `thinking` block with a "narration" signature) right before a tool call. The TUI treats it// like any other reasoning, so the note written for the user is not shown as a message.// Drive's simulated model speaks OpenAI chat, so the progress update is sent as a reasoning delta here;// opencode stores both as the same `reasoning` part type and the TUI has no notion of narration.import{Effect}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constNOTE="Here is the draft poem: Roses in the terminal glow / bugs retreat where tests now grow."exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),project: {git: true,files: {"a.txt": "hi\n"}},config: {permission: {"*": "allow"}}asany,tui: {recording: true},},({ ui, llm })=>Effect.gen(function*(){yield*llm.queue(Llm.reasoning(NOTE),Llm.toolCall({index: 0,id: "call_ls",name: "shell",input: {command: "ls",description: "List files"}}),Llm.finish("tool-calls"),)yield*llm.queue(Llm.text("Done. The poem is in the draft above."))yield*ui.submit("write a two-line poem, then run `ls`")yield*ui.waitFor("draft above",{timeout: 30000}asany)yield*Effect.sleep("1500 millis")yield*ui.screenshot("after-turn")}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Same symptom here through a third-party provider. There is also one detail about this path that affects what #55299 can do.
Setup: opencode 2.1.3 with @openchamber/opencode-claude 1.3.8, which wraps the Claude Code Agent SDK behind a local proxy and registers as an @opencode/ai/providers/openai-compatible provider (claude-code). Model claude-opus-5-5, variant medium.
The stored assistant message for the affected step has no text part:
Unlike the case in the report above, the text is not empty here. This plugin sets thinking-display to summarized and showThinkingSummaries to true, so a block that would come back empty under omitted arrives with text. The problem is classification, not an empty block.
The OpenAI-compatible path never reaches anthropic-messages.ts. The proxy maps thinking_delta to a reasoning_content delta, and opencode stores that as a reasoning part. No top-level signature is produced: anthropic-messages.ts and bedrock-converse.ts write one into provider metadata, and this route does not. Since #55299 reads that top-level field, the check will not apply here even after it lands.
Two consecutive blocks arrive as one part. For the same turn, Claude Code's own session transcript recorded two successive thinking blocks (309 and 148 characters), while opencode stored a single reasoning part whose text equals the two concatenated (457 characters). Even a classifier that receives both blocks cannot say afterwards where one ended and the next began.
So the gap is at the part level: AssistantContent has text, reasoning and tool, and none of them can carry "this came from a separate block written for the user". A provider cannot report that distinction even when it knows it.
Two things would help, in either order:
Let a provider pass the classification through without interpreting it. If reasoning_details entries with a signature field are preserved in provider metadata, a wrapper could forward the value verbatim and opencode could decide what to do with it. openai-chat.ts already keeps every field on the reasoning.text variant, so the receiving side may be most of the way there.
Keep separate blocks separate. One part per block would let a later classifier, or a UI, treat the two differently.
One unrelated question, in case you know: Anthropic documents a connector_text block described as carrying "narration text that some tool-using responses include between tool calls". I could not find it in the Messages API response schema, and neither 2.1.3 nor dev references it, so I may be reading a type that never reaches a client. If it does reach one, that would be the natural home for this text.
@rekram1-node, #55299 is the scoped fix for this: it keeps the narration blocks in the request and shows them in the transcript. I know you'd prefer a larger change for the unselected-effort case. Would you rather merge this as an interim fix, or should I fold it into that work?
@drsmile1001, you're right that the OpenAI-compatible route never sets the top-level signature, so #55299 doesn't cover it. That route needs its own follow-up.
Follow-up for the route I described above: @openchamber/opencode-claude has since been archived in favour of the built-in claude-agent provider, so I re-checked on 2.2.6 with claude-agent/opus, variant medium.
Blocks are no longer merged.convert-claude-stream.ts uses the message ID plus the block index as the part ID, so each thinking block becomes its own reasoning part. Stored steps include the shape reasoning, reasoning, tool.
The signature is dropped, so #55299 will not apply here either. Every thinking block is emitted as reasoning-start / reasoning-delta / reasoning-end, signature_delta is not handled, and the reasoning-end provider metadata carries only sessionID and messageID. On this route, the stored reasoning parts have exactly those two keys under native and no signature. query-options.ts sets display: "summarized", so these parts always have text and always render as a Thought block.
Since the provider now lives in packages/core, there is no third-party layer in between: the stream converter sees signature_delta and could keep its value on the reasoning part's provider metadata, the way anthropic-messages.ts does. That would let #55299, or whatever replaces it in the larger cleanup, cover this route without a separate change.
Activity
opencode-agent commentedon Oct 14, 2026
Thanks for the detailed report. I reproduced both halves of this on 2.0.28 (the latest release), and both still happen on the latest
dev(1c28c2fe). Neither check used real Anthropic traffic.Request side. I fed hand-written Anthropic responses through OpenCode's request and response handling:
claude-opus-5-5/claude-sonnet-5-5turns on adaptive thinking but sends nodisplay. Anthropic then falls back to its default,omitted.narration), followed by a tool call, is stored as a reasoning part with empty text and no text part. That is the shape you described.display: "summarized", so only the no-variant default is affected.TUI side. Even when the note has text, it is shown only as a collapsed
+ Thoughtline above the tool call. The closing reply's "the draft above" then points at nothing visible; the recording and screenshot below show this. The test harness's simulated model can't send Anthropic responses, so I sent the note as a plain reasoning block. OpenCode stores both the same way, and the TUI has no special handling fornarrationblocks.This is the same symptom as #52735, which was a v1 report. Your PR #55299 targets both halves.
recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.rekram1-node commentedon Oct 14, 2026
Yeah recommend selecting a reasoning effort for now using
/variantsor/thinkingWe will make the unselected experience better soon prefer doing a larger cleanup across all the providers than just a hack here or there
drsmile1001 commentedon Oct 23, 2026
Same symptom here through a third-party provider. There is also one detail about this path that affects what #55299 can do.
Setup: opencode 2.1.3 with
@openchamber/opencode-claude1.3.8, which wraps the Claude Code Agent SDK behind a local proxy and registers as an@opencode/ai/providers/openai-compatibleprovider (claude-code). Modelclaude-opus-5-5, variantmedium.The stored assistant message for the affected step has no
textpart:[ { "type": "reasoning", "text": "<text>", "native": { "reasoningField": "reasoning_content" } }, { "type": "tool", "name": "question", "state": { "status": "error", "error": { "type": "aborted", "message": "Tool execution interrupted" } } } ]Unlike the case in the report above, the text is not empty here. This plugin sets
thinking-displaytosummarizedandshowThinkingSummariesto true, so a block that would come back empty underomittedarrives with text. The problem is classification, not an empty block.The OpenAI-compatible path never reaches
anthropic-messages.ts. The proxy mapsthinking_deltato areasoning_contentdelta, and opencode stores that as a reasoning part. No top-level signature is produced:anthropic-messages.tsandbedrock-converse.tswrite one into provider metadata, and this route does not. Since #55299 reads that top-level field, the check will not apply here even after it lands.Two consecutive blocks arrive as one part. For the same turn, Claude Code's own session transcript recorded two successive thinking blocks (309 and 148 characters), while opencode stored a single reasoning part whose text equals the two concatenated (457 characters). Even a classifier that receives both blocks cannot say afterwards where one ended and the next began.
So the gap is at the part level:
AssistantContenthastext,reasoningandtool, and none of them can carry "this came from a separate block written for the user". A provider cannot report that distinction even when it knows it.Two things would help, in either order:
reasoning_detailsentries with asignaturefield are preserved in provider metadata, a wrapper could forward the value verbatim and opencode could decide what to do with it.openai-chat.tsalready keeps every field on thereasoning.textvariant, so the receiving side may be most of the way there.One unrelated question, in case you know: Anthropic documents a
connector_textblock described as carrying "narration text that some tool-using responses include between tool calls". I could not find it in the Messages API response schema, and neither 2.1.3 nordevreferences it, so I may be reading a type that never reaches a client. If it does reach one, that would be the natural home for this text.moritzWa commentedon Oct 28, 2026
@rekram1-node, #55299 is the scoped fix for this: it keeps the
narrationblocks in the request and shows them in the transcript. I know you'd prefer a larger change for the unselected-effort case. Would you rather merge this as an interim fix, or should I fold it into that work?@drsmile1001, you're right that the OpenAI-compatible route never sets the top-level signature, so #55299 doesn't cover it. That route needs its own follow-up.
drsmile1001 commentedon Nov 9, 2026
Follow-up for the route I described above:
@openchamber/opencode-claudehas since been archived in favour of the built-inclaude-agentprovider, so I re-checked on 2.2.6 withclaude-agent/opus, variantmedium.Blocks are no longer merged.
convert-claude-stream.tsuses the message ID plus the block index as the part ID, so each thinking block becomes its own reasoning part. Stored steps include the shapereasoning, reasoning, tool.The signature is dropped, so #55299 will not apply here either. Every
thinkingblock is emitted asreasoning-start/reasoning-delta/reasoning-end,signature_deltais not handled, and thereasoning-endprovider metadata carries onlysessionIDandmessageID. On this route, the stored reasoning parts have exactly those two keys undernativeand nosignature.query-options.tssetsdisplay: "summarized", so these parts always have text and always render as a Thought block.Since the provider now lives in
packages/core, there is no third-party layer in between: the stream converter seessignature_deltaand could keep its value on the reasoning part's provider metadata, the wayanthropic-messages.tsdoes. That would let #55299, or whatever replaces it in the larger cleanup, cover this route without a separate change.