feat(plan): hide plan write and edit operations on plans in Plan Mode (#19012)

This commit is contained in:
Jerop Kipruto
2026-02-13 18:15:21 -05:00
committed by GitHub
parent 4e1b3b5f57
commit 9df604b01b
20 changed files with 373 additions and 108 deletions
@@ -2274,4 +2274,87 @@ describe('CoreToolScheduler Sequential Execution', () => {
);
});
});
describe('ApprovalMode Preservation', () => {
it('should preserve approvalMode throughout tool lifecycle', async () => {
// Arrange
const executeFn = vi.fn().mockResolvedValue({
llmContent: 'Tool executed',
returnDisplay: 'Tool executed',
});
const mockTool = new MockTool({
name: 'mockTool',
execute: executeFn,
shouldConfirmExecute: MOCK_TOOL_SHOULD_CONFIRM_EXECUTE,
});
const mockToolRegistry = {
getTool: () => mockTool,
getAllToolNames: () => ['mockTool'],
} as unknown as ToolRegistry;
const onAllToolCallsComplete = vi.fn();
const onToolCallsUpdate = vi.fn();
// Set approval mode to PLAN
const mockConfig = createMockConfig({
getToolRegistry: () => mockToolRegistry,
getApprovalMode: () => ApprovalMode.PLAN,
// Ensure policy engine returns ASK_USER to trigger AwaitingApproval state
getPolicyEngine: () =>
({
check: async () => ({ decision: PolicyDecision.ASK_USER }),
}) as unknown as PolicyEngine,
});
mockConfig.getHookSystem = vi.fn().mockReturnValue(undefined);
const scheduler = new CoreToolScheduler({
config: mockConfig,
onAllToolCallsComplete,
onToolCallsUpdate,
getPreferredEditor: () => 'vscode',
});
const abortController = new AbortController();
const request = {
callId: '1',
name: 'mockTool',
args: { param: 'value' },
isClientInitiated: false,
prompt_id: 'test-prompt',
};
// Act - Schedule
const schedulePromise = scheduler.schedule(
request,
abortController.signal,
);
// Assert - Check AwaitingApproval state
const awaitingCall = (await waitForStatus(
onToolCallsUpdate,
CoreToolCallStatus.AwaitingApproval,
)) as WaitingToolCall;
expect(awaitingCall).toBeDefined();
expect(awaitingCall.approvalMode).toBe(ApprovalMode.PLAN);
// Act - Confirm
await (
awaitingCall.confirmationDetails as ToolCallConfirmationDetails
).onConfirm(ToolConfirmationOutcome.ProceedOnce);
// Wait for completion
await schedulePromise;
// Assert - Check Success state
expect(onAllToolCallsComplete).toHaveBeenCalled();
const completedCalls = onAllToolCallsComplete.mock
.calls[0][0] as ToolCall[];
expect(completedCalls).toHaveLength(1);
expect(completedCalls[0].status).toBe(CoreToolCallStatus.Success);
expect(completedCalls[0].approvalMode).toBe(ApprovalMode.PLAN);
});
});
});
+15 -1
View File
@@ -217,6 +217,7 @@ export class CoreToolScheduler {
const invocation = currentCall.invocation;
const outcome = currentCall.outcome;
const approvalMode = currentCall.approvalMode;
switch (newStatus) {
case CoreToolCallStatus.Success: {
@@ -232,6 +233,7 @@ export class CoreToolScheduler {
response: auxiliaryData as ToolCallResponseInfo,
durationMs,
outcome,
approvalMode,
} as SuccessfulToolCall;
}
case CoreToolCallStatus.Error: {
@@ -246,6 +248,7 @@ export class CoreToolScheduler {
response: auxiliaryData as ToolCallResponseInfo,
durationMs,
outcome,
approvalMode,
} as ErroredToolCall;
}
case CoreToolCallStatus.AwaitingApproval:
@@ -259,6 +262,7 @@ export class CoreToolScheduler {
startTime: existingStartTime,
outcome,
invocation,
approvalMode,
} as WaitingToolCall;
case CoreToolCallStatus.Scheduled:
return {
@@ -268,6 +272,7 @@ export class CoreToolScheduler {
startTime: existingStartTime,
outcome,
invocation,
approvalMode,
} as ScheduledToolCall;
case CoreToolCallStatus.Cancelled: {
const durationMs = existingStartTime
@@ -316,6 +321,7 @@ export class CoreToolScheduler {
},
durationMs,
outcome,
approvalMode,
} as CancelledToolCall;
}
case CoreToolCallStatus.Validating:
@@ -326,6 +332,7 @@ export class CoreToolScheduler {
startTime: existingStartTime,
outcome,
invocation,
approvalMode,
} as ValidatingToolCall;
case CoreToolCallStatus.Executing:
return {
@@ -335,6 +342,7 @@ export class CoreToolScheduler {
startTime: existingStartTime,
outcome,
invocation,
approvalMode,
} as ExecutingToolCall;
default: {
const exhaustiveCheck: never = newStatus;
@@ -373,6 +381,7 @@ export class CoreToolScheduler {
status: CoreToolCallStatus.Error,
tool: call.tool,
response,
approvalMode: call.approvalMode,
} as ErroredToolCall;
}
@@ -496,6 +505,7 @@ export class CoreToolScheduler {
);
}
const requestsToProcess = Array.isArray(request) ? request : [request];
const currentApprovalMode = this.config.getApprovalMode();
this.completedToolCallsForBatch = [];
const newToolCalls: ToolCall[] = requestsToProcess.map(
@@ -518,6 +528,7 @@ export class CoreToolScheduler {
ToolErrorType.TOOL_NOT_REGISTERED,
),
durationMs: 0,
approvalMode: currentApprovalMode,
};
}
@@ -536,6 +547,7 @@ export class CoreToolScheduler {
ToolErrorType.INVALID_TOOL_PARAMS,
),
durationMs: 0,
approvalMode: currentApprovalMode,
};
}
@@ -545,6 +557,7 @@ export class CoreToolScheduler {
tool: toolInstance,
invocation: invocationOrError,
startTime: Date.now(),
approvalMode: currentApprovalMode,
};
},
);
@@ -920,7 +933,7 @@ export class CoreToolScheduler {
this.toolCalls = this.toolCalls.map((tc) =>
tc.request.callId === completedCall.request.callId
? completedCall
? { ...completedCall, approvalMode: tc.approvalMode }
: tc,
);
this.notifyToolCallsUpdate();
@@ -1049,6 +1062,7 @@ export class CoreToolScheduler {
},
durationMs,
outcome: ToolConfirmationOutcome.Cancel,
approvalMode: queuedCall.approvalMode,
});
}
}