What happened?
When an extension intercepts an edit tool call, waits for user input via ctx.ui.select() / ctx.ui.input(), and then blocks the tool with { block: true, reason }, the TUI renders the blocked edit result twice.
The session JSONL contains only one toolResult for that toolCallId, so this appears to be a visual/rendering duplication, not two actual tool results.
Steps to reproduce
- Use an extension that intercepts
edit tool calls in pi.on("tool_call"), asks the user for confirmation with ctx.ui.select() or ctx.ui.input(), and then returns { block: true, reason: "..." } when the user rejects the edit.
- Ask the model to edit a file that already exists, for example:
edit test.txt and change one word.
- When the extension confirmation UI appears, reject/block the edit and enter any note.
- Observe the TUI output for the blocked edit result twice:
pi-session-2026-04-27T17-04-03-181Z_019dcfe5-e46c-715f-a838-3284e59b6acc.html
For the repro, i used this simple extension:
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
export default function mutationGuardRepro(pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => {
if (!isToolCallEventType("edit", event)) return undefined;
const path = event.input.path;
if (!ctx.hasUI) {
return {
block: true,
reason: `Edit ${path} blocked: confirmation requires UI.`,
};
}
const choice = await ctx.ui.select(`Confirm edit ${path}`, [
"Allow",
"Block with note",
]);
if (choice === "Allow") {
return undefined;
}
const note = (await ctx.ui.input("Reject note", "Type note")) ?? "";
return {
block: true,
reason: `Mutation rejected by user for edit ${path}. User note: ${note.trim() || "none"}`,
};
});
}
Expected behavior
The blocked edit result should be rendered once.
Version
0.70.2
What happened?
When an extension intercepts an
edittool call, waits for user input viactx.ui.select()/ctx.ui.input(), and then blocks the tool with{ block: true, reason }, the TUI renders the blocked edit result twice.The session JSONL contains only one
toolResultfor thattoolCallId, so this appears to be a visual/rendering duplication, not two actual tool results.Steps to reproduce
edittool calls inpi.on("tool_call"), asks the user for confirmation withctx.ui.select()orctx.ui.input(), and then returns{ block: true, reason: "..." }when the user rejects the edit.edit test.txt and change one word.pi-session-2026-04-27T17-04-03-181Z_019dcfe5-e46c-715f-a838-3284e59b6acc.html
For the repro, i used this simple extension:
Expected behavior
The blocked edit result should be rendered once.
Version
0.70.2