diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5732aff7..d8a49897 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed assistant messages stopped by output length to show a visible incomplete-response error ([#4290](https://github.com/earendil-works/pi/issues/4290)). - Fixed `--no-session --session-id` so ephemeral CLI runs can use deterministic session IDs for provider cache affinity ([#6070](https://github.com/earendil-works/pi/issues/6070)). - Fixed disk BMP image files to be detected, converted to PNG, and attached through `read` and CLI `@file` inputs ([#6047](https://github.com/earendil-works/pi/issues/6047)). - Fixed auto-retry for provider stream errors that explicitly tell callers to retry the request ([#6019](https://github.com/earendil-works/pi/issues/6019)). 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 32276467..e8781c75 100644 --- a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts @@ -121,21 +121,30 @@ export class AssistantMessageComponent extends Container { } } - // Check if aborted - show after partial content - // But only if there are no tool calls (tool execution components will show the error) + // Check if incomplete/failed - show after partial content. + // For aborted/error tool calls, tool execution components show the error. + // Length stops can happen before a tool call is complete, so surface them here too. const hasToolCalls = message.content.some((c) => c.type === "toolCall"); this.hasToolCalls = hasToolCalls; - if (!hasToolCalls) { + if (message.stopReason === "length") { + this.contentContainer.addChild(new Spacer(1)); + this.contentContainer.addChild( + new Text( + theme.fg( + "error", + "Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.", + ), + 1, + 0, + ), + ); + } else if (!hasToolCalls) { if (message.stopReason === "aborted") { const abortMessage = message.errorMessage && message.errorMessage !== "Request was aborted" ? message.errorMessage : "Operation aborted"; - if (hasVisibleContent) { - this.contentContainer.addChild(new Spacer(1)); - } else { - this.contentContainer.addChild(new Spacer(1)); - } + this.contentContainer.addChild(new Spacer(1)); this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0)); } else if (message.stopReason === "error") { const errorMsg = message.errorMessage || "Unknown error"; diff --git a/packages/coding-agent/test/assistant-message.test.ts b/packages/coding-agent/test/assistant-message.test.ts index 137376a5..c2e74daa 100644 --- a/packages/coding-agent/test/assistant-message.test.ts +++ b/packages/coding-agent/test/assistant-message.test.ts @@ -7,7 +7,10 @@ const OSC133_ZONE_START = "\x1b]133;A\x07"; const OSC133_ZONE_END = "\x1b]133;B\x07"; const OSC133_ZONE_FINAL = "\x1b]133;C\x07"; -function createAssistantMessage(content: AssistantMessage["content"]): AssistantMessage { +function createAssistantMessage( + content: AssistantMessage["content"], + overrides: Partial> = {}, +): AssistantMessage { return { role: "assistant", content, @@ -22,7 +25,7 @@ function createAssistantMessage(content: AssistantMessage["content"]): Assistant totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, - stopReason: "stop", + stopReason: overrides.stopReason ?? "stop", timestamp: Date.now(), }; } @@ -54,4 +57,18 @@ describe("AssistantMessageComponent", () => { expect(rendered.includes(OSC133_ZONE_END)).toBe(false); expect(rendered.includes(OSC133_ZONE_FINAL)).toBe(false); }); + + test("renders length stops as visible errors", () => { + initTheme("dark"); + + const component = new AssistantMessageComponent( + createAssistantMessage([{ type: "thinking", thinking: "private reasoning" }], { stopReason: "length" }), + true, + ); + const rendered = component.render(80).join("\n"); + + expect(rendered).toContain("Thinking..."); + expect(rendered).toContain("maximum output token limit"); + expect(rendered).toContain("response may be incomplete"); + }); });