diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b6e3ee43e..a7025d5a0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -13,6 +13,7 @@ - Fixed responses truncated at the output-token cap (`stopReason: "length"`) dead-ending the model's work with a "maximum output token limit" error when the context was still below the auto-compaction threshold. Previously auto-continuation only happened as a side effect of threshold compaction, so a length truncation with input room to spare left the task half-finished. Length-truncated turns now continue directly without compacting: the incomplete assistant is removed from retry context and the generation resumes automatically so the model finishes where it left off, bounded by a small consecutive-continuation cap so a turn that keeps exceeding the per-turn output cap still terminates. Compaction-driven `willRetry: true` continuations are now consistently tracked through the normal prompt lifecycle, so `AgentSession.prompt()` waits for threshold length-stop continuation before resolving instead of treating it as fire-and-forget. - Fixed auto-compaction after OpenAI Responses output-budget underflow errors such as `Invalid 'max_output_tokens': integer below minimum value. Expected a value >= 16, but got 1 instead.` Atomic now classifies that specific context-pressure failure as retry-worthy when the live context crosses the compaction threshold, removes the empty error assistant from retry context after compaction, and automatically continues from the preceding work anchor instead of waiting for the user to type `Continue`. Generic `invalid_request_body` errors such as malformed tool schemas are still not auto-retried, OpenAI Responses payload sanitization now prevents `max_output_tokens` values below the provider minimum of 16 from being sent, and repeated output-budget underflow continuation is capped at one compact-and-retry attempt so an unrecoverable context terminates visibly instead of looping or stalling. - Fixed bundled workflow durable resume for reusable `git_worktree_dir` worktrees so resumed runs reuse the original invocation repository/cwd and report slow Git subprocess timeouts as Git timeouts instead of repository-detection failures. +- Removed the queued-message restore success status from the interactive UI so restoring queued messages silently returns them to the editor without showing transient restore copy. - Fixed bundled MCP proxy metadata paths so explicit cold-cache `search`, `describe`, and server-list requests hydrate lazy server metadata on demand without reconnecting all lazy servers during startup. ## [0.9.5-alpha.8] - 2026-07-08 diff --git a/packages/coding-agent/src/modes/interactive/components/chat-session-host-actions.ts b/packages/coding-agent/src/modes/interactive/components/chat-session-host-actions.ts index 5150046a2..ec23e4437 100644 --- a/packages/coding-agent/src/modes/interactive/components/chat-session-host-actions.ts +++ b/packages/coding-agent/src/modes/interactive/components/chat-session-host-actions.ts @@ -192,10 +192,6 @@ export function restoreQueuedMessagesToEditor< state.compactionQueuedMessages = []; setChatSessionEditorText(state, restoredText); state.getAgentSession?.()?.clearQueue(); - notifyChatSessionStatus( - state, - `Restored ${queuedMessages.length} queued message${queuedMessages.length === 1 ? "" : "s"} to editor`, - ); state.requestRender?.(); return true; } diff --git a/packages/coding-agent/src/modes/interactive/interactive-process-lifecycle.ts b/packages/coding-agent/src/modes/interactive/interactive-process-lifecycle.ts index 490340b9e..36ef991cf 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-process-lifecycle.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-process-lifecycle.ts @@ -246,9 +246,5 @@ InteractiveModeBase.prototype.handleDequeue = function(this: InteractiveModeBase const restored = this.restoreQueuedMessagesToEditor(); if (restored === 0) { this.showStatus("No queued messages to restore"); - } else { - this.showStatus( - `Restored ${restored} queued message${restored > 1 ? "s" : ""} to editor`, - ); } }; diff --git a/packages/coding-agent/test/first-run-onboarding.test.ts b/packages/coding-agent/test/first-run-onboarding.test.ts index 2855c06ed..7d2bd5e50 100644 --- a/packages/coding-agent/test/first-run-onboarding.test.ts +++ b/packages/coding-agent/test/first-run-onboarding.test.ts @@ -342,6 +342,7 @@ describe("first-run onboarding", () => { hostWithClear.clearFirstRunOnboardingUi = () => clear.call(hostWithClear); await handleClearCommand.call(hostWithClear); + expect(host.ensureDeferredStartupComplete).toHaveBeenCalledTimes(1); expect(host.ensureDeferredStartupComplete).toHaveBeenCalledTimes(1); expect(host.firstRunNoticeVisible).toBe(false); diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index b0b535631..9d1516529 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Aligned MCP tool result expansion hints with the CLI-wide `Ctrl+o` keybinding copy. - Fixed MCP background direct-tool warmup cancellation so stale in-flight connects are discarded before metadata/cache mutation, direct-tool refresh callbacks are guarded to the active session, completed warmups clear their active handle safely, and env-selected direct tool servers hydrate without forcing every lazy server eager. ## [0.9.4] - 2026-07-03 diff --git a/packages/mcp/README.md b/packages/mcp/README.md index a5c7aaca0..fffa04676 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -332,7 +332,7 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/mcp.json` only | Connect | `mcp({ connect: "server-name" })` | | UI messages | `mcp({ action: "ui-messages" })` | -MCP proxy and direct-tool results render compactly by default: long text shows the first three lines plus a `CTRL+O Expand` hint, while the full result remains available when expanded and is still returned unchanged to the model. +MCP proxy and direct-tool results render compactly by default: long text shows the first three lines plus a `Ctrl+o Expand` hint, while the full result remains available when expanded and is still returned unchanged to the model. Search includes both MCP tools and Pi tools (from extensions). Pi tools appear first with `[pi tool]` prefix. Space-separated words are OR'd. diff --git a/packages/mcp/tool-result-renderer.ts b/packages/mcp/tool-result-renderer.ts index d08151fa3..34154cedf 100644 --- a/packages/mcp/tool-result-renderer.ts +++ b/packages/mcp/tool-result-renderer.ts @@ -1,7 +1,7 @@ -import type { AgentToolResult, ToolRenderResultOptions } from "@bastani/atomic"; +import { keyText, type AgentToolResult, type ToolRenderResultOptions } from "@bastani/atomic"; import { Text } from "@earendil-works/pi-tui"; -type McpToolResultDetails = Record & { error?: unknown }; +export type McpToolResultDetails = Record & { error?: unknown }; type McpToolContentBlock = AgentToolResult["content"][number]; interface RenderTheme { @@ -58,7 +58,7 @@ export function renderMcpToolResult( .map((line) => line === "…" ? theme.fg("muted", line) : theme.fg("toolOutput", line)) .join("\n"); const hint = display.truncated && !options.expanded - ? `\n${theme.fg("muted", "(CTRL+O Expand)")}` + ? `\n${theme.fg("muted", `(${keyText("app.tools.expand")} Expand)`)}` : ""; return new Text(`${output}${hint}`, 0, 0); diff --git a/test/unit/chat-session-host-02.test.ts b/test/unit/chat-session-host-02.test.ts index 7ef4b6f99..0870d310d 100644 --- a/test/unit/chat-session-host-02.test.ts +++ b/test/unit/chat-session-host-02.test.ts @@ -66,6 +66,7 @@ test("ChatSessionHost clears busy state when model fallback fails", () => { host.dispose(); }); test("ChatSessionHost preserves compaction queued messages when flush fails", async () => { + const statusMessages: string[] = []; const host = makeHost({ getActionKeyDisplay: (action) => (action === "app.message.dequeue" ? "⌥↑" : action), commands: { @@ -74,6 +75,7 @@ test("ChatSessionHost preserves compaction queued messages when flush fails", as }, followUp: async () => {}, }, + showStatus: (message) => statusMessages.push(message), }); host.applyAgentEvent({ type: "compaction_start", reason: "manual" } as never); @@ -99,6 +101,11 @@ test("ChatSessionHost preserves compaction queued messages when flush fails", as assert.match(pending, /second/); assert.equal(host.restoreQueuedMessagesToEditor(), true); assert.equal(host.inputText(), "first\n\nsecond"); + assert.doesNotMatch(host.statusText(), /Restored .*queued message/); + assert.deepEqual( + statusMessages.filter((message) => /Restored .*queued message/.test(message)), + [], + ); host.dispose(); }); test("ChatSessionHost delegates handled slash commands before prompt routing", async () => { diff --git a/test/unit/mcp-tool-result-renderer.test.ts b/test/unit/mcp-tool-result-renderer.test.ts new file mode 100644 index 000000000..4bdda68ee --- /dev/null +++ b/test/unit/mcp-tool-result-renderer.test.ts @@ -0,0 +1,58 @@ +import { describe, test } from "bun:test"; +import assert from "node:assert/strict"; +import { keyText, type AgentToolResult, type ToolRenderResultOptions } from "../../packages/coding-agent/src/index.ts"; +import { + formatMcpToolResultLines, + renderMcpToolResult, + type McpToolResultDetails, +} from "../../packages/mcp/tool-result-renderer.ts"; + +const theme = { + fg: (_name: string, text: string) => text, +}; + +function textResult(text: string): AgentToolResult { + return { + content: [{ type: "text", text }], + details: {}, + terminate: false, + }; +} + +function renderResult(text: string, options: ToolRenderResultOptions): string { + return renderMcpToolResult(textResult(text), options, theme).render(80).join("\n"); +} + +describe("MCP tool result rendering", () => { + test("formats collapsed result hint with the configured expand keybinding", () => { + const rendered = renderResult("one\ntwo\nthree\nfour", { + expanded: false, + isPartial: false, + }); + + assert.match(rendered, new RegExp(`\\(${keyText("app.tools.expand")} Expand\\)`)); + assert.doesNotMatch(rendered, /CTRL\+O/); + assert.doesNotMatch(rendered, /Ctrl\+o/); + }); + + test("omits expand hint when result is already expanded", () => { + const rendered = renderResult("one\ntwo\nthree\nfour", { + expanded: true, + isPartial: false, + }); + + assert.doesNotMatch(rendered, new RegExp(`${keyText("app.tools.expand")} Expand`)); + assert.match(rendered, /four/); + }); + + test("reports truncation only when collapsed content exceeds the line budget", () => { + assert.deepEqual( + formatMcpToolResultLines(textResult("one\ntwo\nthree\nfour"), false), + { lines: ["one", "two", "three", "…"], truncated: true }, + ); + assert.deepEqual( + formatMcpToolResultLines(textResult("one\ntwo\nthree\nfour"), true), + { lines: ["one", "two", "three", "four"], truncated: false }, + ); + }); +});