From 45203abfa0ed6057bdb91e476fdc2730ff24370e Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 15 Jul 2026 12:52:53 +0200 Subject: [PATCH] fix(coding-agent): Coalesce adjacent assistant thinking blocks into one section --- packages/coding-agent/CHANGELOG.md | 3 +- .../components/assistant-message.ts | 34 +++++++++++++------ .../test/assistant-message.test.ts | 18 ++++++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ee2edfbe..b4f5dcf4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -34,11 +34,12 @@ ### Fixed -- Fixed configured-provider catalog refresh to parse pi.dev's model-ID keyed responses, treat unimplemented routes as unavailable overlays, and show concise refresh status in `/model`. +- Fixed configured-provider catalog refresh to parse pi.dev's model-ID keyed responses, throttle checks to once per four hours, send the versioned pi user agent, treat unimplemented routes as unavailable overlays, and show concise refresh status in `/model`. - Fixed inherited OpenRouter model context windows to use the top provider's actual context length ([#6481](https://github.com/earendil-works/pi-mono/pull/6481) by [@davidbrai](https://github.com/davidbrai)). - Fixed inherited OpenRouter OpenAI-compatible session IDs to use the `x-session-id` header instead of OpenAI-specific session-affinity fields ([#6366](https://github.com/earendil-works/pi/issues/6366)). - Fixed `Ctrl+V` to paste clipboard text when the pasteboard does not contain an image. - Fixed `/login amazon-bedrock` to prompt for and save a Bedrock API key instead of only displaying ambient AWS credential setup instructions. +- Fixed adjacent assistant thinking blocks to render as one thinking section. ## [0.80.6] - 2026-07-09 diff --git a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts index d6b17d37..6c9a2a40 100644 --- a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts @@ -101,7 +101,24 @@ export class AssistantMessageComponent extends Container { // Assistant text messages with no background - trim the text // Set paddingY=0 to avoid extra spacing before tool executions this.contentContainer.addChild(new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme)); - } else if (content.type === "thinking" && content.thinking.trim()) { + } else if (content.type === "thinking") { + const thinkingBlocks: string[] = []; + for (; i < message.content.length; i++) { + const thinkingContent = message.content[i]; + if (thinkingContent.type !== "thinking") { + break; + } + const thinking = thinkingContent.thinking.trim(); + if (thinking) { + thinkingBlocks.push(thinking); + } + } + i--; + + if (thinkingBlocks.length === 0) { + continue; + } + // Add spacing only when another visible assistant content block follows. // This avoids a superfluous blank line before separately-rendered tool execution blocks. const hasVisibleContentAfter = message.content @@ -109,24 +126,21 @@ export class AssistantMessageComponent extends Container { .some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim())); if (this.hideThinkingBlock) { - // Show static thinking label when hidden + // Show one static label for each run of thinking blocks when hidden. this.contentContainer.addChild( new Text(theme.italic(theme.fg("thinkingText", this.hiddenThinkingLabel)), this.outputPad, 0), ); - if (hasVisibleContentAfter) { - this.contentContainer.addChild(new Spacer(1)); - } } else { - // Thinking traces in thinkingText color, italic + // Render each run of thinking blocks as one Markdown section. this.contentContainer.addChild( - new Markdown(content.thinking.trim(), this.outputPad, 0, this.markdownTheme, { + new Markdown(thinkingBlocks.join("\n\n"), this.outputPad, 0, this.markdownTheme, { color: (text: string) => theme.fg("thinkingText", text), italic: true, }), ); - if (hasVisibleContentAfter) { - this.contentContainer.addChild(new Spacer(1)); - } + } + if (hasVisibleContentAfter) { + this.contentContainer.addChild(new Spacer(1)); } } } diff --git a/packages/coding-agent/test/assistant-message.test.ts b/packages/coding-agent/test/assistant-message.test.ts index 2244df99..ec406793 100644 --- a/packages/coding-agent/test/assistant-message.test.ts +++ b/packages/coding-agent/test/assistant-message.test.ts @@ -74,6 +74,24 @@ describe("AssistantMessageComponent", () => { expect(rendered).toContain("response may be incomplete"); }); + test("coalesces adjacent thinking blocks into one hidden thinking label", () => { + initTheme("dark"); + + const component = new AssistantMessageComponent( + createAssistantMessage([ + { type: "thinking", thinking: "first thought" }, + { type: "thinking", thinking: "" }, + { type: "thinking", thinking: "second thought" }, + { type: "text", text: "answer" }, + ]), + true, + ); + const rendered = stripAnsi(component.render(80).join("\n")); + + expect(rendered.match(/Thinking\.\.\./g)).toHaveLength(1); + expect(rendered).toContain("answer"); + }); + test("uses configured output padding for text and thinking", () => { initTheme("dark");