feat(core): display declined confirmation code diff (#4440)

This commit is contained in:
Yuki Okita 2025-07-20 02:47:09 +09:00 committed by GitHub
parent f650be2c3a
commit 73d5d988f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 138 additions and 1 deletions

View File

@ -407,3 +407,123 @@ describe('convertToFunctionResponse', () => {
});
});
});
describe('CoreToolScheduler edit cancellation', () => {
it('should preserve diff when an edit is cancelled', async () => {
class MockEditTool extends BaseTool<Record<string, unknown>, ToolResult> {
constructor() {
super(
'mockEditTool',
'mockEditTool',
'A mock edit tool',
Icon.Pencil,
{},
);
}
async shouldConfirmExecute(
_params: Record<string, unknown>,
_abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> {
return {
type: 'edit',
title: 'Confirm Edit',
fileName: 'test.txt',
fileDiff:
'--- test.txt\n+++ test.txt\n@@ -1,1 +1,1 @@\n-old content\n+new content',
originalContent: 'old content',
newContent: 'new content',
onConfirm: async () => {},
};
}
async execute(
_params: Record<string, unknown>,
_abortSignal: AbortSignal,
): Promise<ToolResult> {
return {
llmContent: 'Edited successfully',
returnDisplay: 'Edited successfully',
};
}
}
const mockEditTool = new MockEditTool();
const toolRegistry = {
getTool: () => mockEditTool,
getFunctionDeclarations: () => [],
tools: new Map(),
discovery: {} as any,
registerTool: () => {},
getToolByName: () => mockEditTool,
getToolByDisplayName: () => mockEditTool,
getTools: () => [],
discoverTools: async () => {},
getAllTools: () => [],
getToolsByServer: () => [],
};
const onAllToolCallsComplete = vi.fn();
const onToolCallsUpdate = vi.fn();
const mockConfig = {
getSessionId: () => 'test-session-id',
getUsageStatisticsEnabled: () => true,
getDebugMode: () => false,
} as unknown as Config;
const scheduler = new CoreToolScheduler({
config: mockConfig,
toolRegistry: Promise.resolve(toolRegistry as any),
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
});
const abortController = new AbortController();
const request = {
callId: '1',
name: 'mockEditTool',
args: {},
isClientInitiated: false,
prompt_id: 'prompt-id-1',
};
await scheduler.schedule([request], abortController.signal);
// Wait for the tool to reach awaiting_approval state
const awaitingCall = onToolCallsUpdate.mock.calls.find(
(call) => call[0][0].status === 'awaiting_approval',
)?.[0][0];
expect(awaitingCall).toBeDefined();
// Cancel the edit
const confirmationDetails = await mockEditTool.shouldConfirmExecute(
{},
abortController.signal,
);
if (confirmationDetails) {
await scheduler.handleConfirmationResponse(
'1',
confirmationDetails.onConfirm,
ToolConfirmationOutcome.Cancel,
abortController.signal,
);
}
expect(onAllToolCallsComplete).toHaveBeenCalled();
const completedCalls = onAllToolCallsComplete.mock
.calls[0][0] as ToolCall[];
expect(completedCalls[0].status).toBe('cancelled');
// Check that the diff is preserved
const cancelledCall = completedCalls[0] as any;
expect(cancelledCall.response.resultDisplay).toBeDefined();
expect(cancelledCall.response.resultDisplay.fileDiff).toBe(
'--- test.txt\n+++ test.txt\n@@ -1,1 +1,1 @@\n-old content\n+new content',
);
expect(cancelledCall.response.resultDisplay.fileName).toBe('test.txt');
});
});

View File

@ -11,6 +11,7 @@ import {
Tool,
ToolCallConfirmationDetails,
ToolResult,
ToolResultDisplay,
ToolRegistry,
ApprovalMode,
EditorType,
@ -335,6 +336,22 @@ export class CoreToolScheduler {
const durationMs = existingStartTime
? Date.now() - existingStartTime
: undefined;
// Preserve diff for cancelled edit operations
let resultDisplay: ToolResultDisplay | undefined = undefined;
if (currentCall.status === 'awaiting_approval') {
const waitingCall = currentCall as WaitingToolCall;
if (waitingCall.confirmationDetails.type === 'edit') {
resultDisplay = {
fileDiff: waitingCall.confirmationDetails.fileDiff,
fileName: waitingCall.confirmationDetails.fileName,
originalContent:
waitingCall.confirmationDetails.originalContent,
newContent: waitingCall.confirmationDetails.newContent,
};
}
}
return {
request: currentCall.request,
tool: toolInstance,
@ -350,7 +367,7 @@ export class CoreToolScheduler {
},
},
},
resultDisplay: undefined,
resultDisplay,
error: undefined,
},
durationMs,