diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b7cb36570..402af444b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -72,6 +72,9 @@ ### Fixed - Fixed the `auto` thinking classifier failing every turn on Anthropic models served through LiteLLM/Vertex with `max_tokens must be greater than thinking.budget_tokens`. The classifier's disabled-reasoning request is downgraded to the lowest reasoning effort on the `openai-completions` transport, which the proxy translates to an Anthropic thinking budget of at least 1024 tokens; the classifier now reserves enough output room (4096) to clear that budget instead of capping at exactly 1024 ([#8610](https://github.com/can1357/oh-my-pi/issues/8610)). +### Fixed + +- Fixed `always-ask` approval prompts bypassing edit preview readiness when a built-in tool executes under its wire-level alias, such as `edit` running as `apply_patch` ([#8607](https://github.com/can1357/oh-my-pi/issues/8607)). ## [17.3.4] - 2026-08-14 diff --git a/packages/coding-agent/src/extensibility/extensions/wrapper.ts b/packages/coding-agent/src/extensibility/extensions/wrapper.ts index 335a7abcd..f6ca64cdf 100644 --- a/packages/coding-agent/src/extensibility/extensions/wrapper.ts +++ b/packages/coding-agent/src/extensibility/extensions/wrapper.ts @@ -265,7 +265,10 @@ export class ExtensionToolWrapper this.runner.waitForToolApprovalPreview(toolCallId)); } diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index aa868f374..fc39a3674 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -1988,54 +1988,64 @@ describe("ExtensionRunner", () => { delete globalState.__approvalEvents; }); - it("does not present approval before the tool preview is ready", async () => { - const result = await loadTestExtensions(); - const runner = new ExtensionRunner( - result.extensions, - result.runtime, - tempDir.path(), - sessionManager, - modelRegistry, - ); - const preview = Promise.withResolvers(); - const order: string[] = []; - runner.setToolApprovalPreviewWaiter(async toolCallId => { - order.push(`preview_wait:${toolCallId}`); - await preview.promise; - order.push("preview_ready"); - }); - initializeRunner( - runner, - vi.fn(async () => { - order.push("ui_select"); - return "Approve"; - }), - ); - - const wrapper = new ExtensionToolWrapper(approvalTool, runner); - const execution = (wrapper as ExtensionToolWrapper).execute("call-preview", {}, undefined, undefined, { - sessionManager, - modelRegistry, - model: undefined, - isIdle: () => true, - hasQueuedMessages: () => false, - abort: () => {}, - settings: { - get: (key: string) => (key === "tools.approvalMode" ? "always-ask" : {}), - } as never, - toolCall: { - batchId: "batch-preview", - index: 0, - total: 1, - toolCalls: [{ id: "call-preview", name: "dangerous_tool" }], + it("does not present approval before canonical or wire-aliased tool previews are ready", async () => { + const cases = [ + { tool: approvalTool, wireName: "dangerous_tool", toolCallId: "call-preview" }, + { + tool: { ...approvalTool, name: "edit", customWireName: "apply_patch" }, + wireName: "apply_patch", + toolCallId: "call-aliased-preview", }, - }); - await Promise.resolve(); - expect(order).toEqual(["preview_wait:call-preview"]); + ]; + for (const { tool, wireName, toolCallId } of cases) { + const result = await loadTestExtensions(); + const runner = new ExtensionRunner( + result.extensions, + result.runtime, + tempDir.path(), + sessionManager, + modelRegistry, + ); + const preview = Promise.withResolvers(); + const order: string[] = []; + runner.setToolApprovalPreviewWaiter(async waitedToolCallId => { + order.push(`preview_wait:${waitedToolCallId}`); + await preview.promise; + order.push("preview_ready"); + }); + initializeRunner( + runner, + vi.fn(async () => { + order.push("ui_select"); + return "Approve"; + }), + ); - preview.resolve(); - await execution; - expect(order).toEqual(["preview_wait:call-preview", "preview_ready", "ui_select"]); + const wrapper = new ExtensionToolWrapper(tool, runner); + const execution = (wrapper as ExtensionToolWrapper).execute(toolCallId, {}, undefined, undefined, { + sessionManager, + modelRegistry, + model: undefined, + isIdle: () => true, + hasQueuedMessages: () => false, + abort: () => {}, + settings: { + get: (key: string) => (key === "tools.approvalMode" ? "always-ask" : {}), + } as never, + toolCall: { + batchId: `batch-${toolCallId}`, + index: 0, + total: 1, + toolCalls: [{ id: toolCallId, name: wireName }], + }, + }); + await Promise.resolve(); + expect(order).toEqual([`preview_wait:${toolCallId}`]); + + preview.resolve(); + await execution; + expect(order).toEqual([`preview_wait:${toolCallId}`, "preview_ready", "ui_select"]); + } }); it("emits resolved false when approval is denied", async () => {