Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
);
}
};
1 change: 1 addition & 0 deletions packages/coding-agent/test/first-run-onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions packages/mcp/tool-result-renderer.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> & { error?: unknown };
export type McpToolResultDetails = Record<string, unknown> & { error?: unknown };
type McpToolContentBlock = AgentToolResult<McpToolResultDetails>["content"][number];

interface RenderTheme {
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions test/unit/chat-session-host-02.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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);
Expand All @@ -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 () => {
Expand Down
58 changes: 58 additions & 0 deletions test/unit/mcp-tool-result-renderer.test.ts
Original file line number Diff line number Diff line change
@@ -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<McpToolResultDetails> {
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 },
);
});
});
Loading