Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/vs/platform/agentHost/common/state/protocol/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ export interface ISessionToolCallApprovedAction extends IToolCallActionBase {
approved: true;
/** How the tool was confirmed */
confirmed: ToolCallConfirmationReason;
/** If the user edited the tool input before approving, this is the new input */
userEditedInput?: string;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export function sessionReducer(state: ISessionState, action: ISessionAction, log
status: ToolCallStatus.Running,
...base,
invocationMessage: tc.invocationMessage,
toolInput: tc.toolInput,
toolInput: action.userEditedInput ?? tc.toolInput,
confirmed: action.confirmed,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1029,13 +1029,18 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC
const approved = reason.type !== ToolConfirmKind.Denied && reason.type !== ToolConfirmKind.Skipped;
this._logService.info(`[AgentHost] Tool confirmation: toolCallId=${toolCallId}, approved=${approved}`);
if (approved) {
// Check if the user edited the command in the confirmation UI
const toolSpecificData = invocation.toolSpecificData;
const userEditedInput = toolSpecificData?.kind === 'terminal' ? toolSpecificData.commandLine.userEdited : undefined;

const confirmAction = {
type: ActionType.SessionToolCallConfirmed as const,
session: session.toString(),
turnId,
toolCallId,
approved: true as const,
confirmed: ToolCallConfirmationReason.UserAction,
...(userEditedInput ? { userEditedInput } : {}),
};
const seq = this._clientState.applyOptimistic(confirmAction);
this._config.connection.dispatchAction(confirmAction, this._clientState.clientId, seq);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js
import { Codicon } from '../../../../../../base/common/codicons.js';
import { localize } from '../../../../../../nls.js';
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../../../chat/common/tools/languageModelToolsService.js';
import { IChatTerminalToolInvocationData } from '../../../../chat/common/chatService/chatService.js';
import { RunInTerminalTool } from './runInTerminalTool.js';
import { TerminalToolId } from './toolIds.js';

Expand Down Expand Up @@ -76,7 +77,17 @@ export class ConfirmTerminalCommandTool extends RunInTerminalTool {
return preparedInvocation;
}
override async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {
// This is a confirmation-only tool - just return success
// This is a confirmation-only tool - return the user-edited command if they modified it
const toolSpecificData = invocation.toolSpecificData as IChatTerminalToolInvocationData | undefined;
const userEdited = toolSpecificData?.commandLine.userEdited;
if (userEdited && userEdited !== toolSpecificData?.commandLine.original) {
return {
content: [{
kind: 'text',
value: `The user approved the command but edited it to: \`${userEdited}\`. Use this exact command when executing.`
}]
};
}
return {
content: [{
kind: 'text',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2030,6 +2030,55 @@ suite('RunInTerminalTool', () => {
const result = await confirmTool.prepareToolInvocation(context, CancellationToken.None);
assertConfirmationRequired(result);
});

test('should return user-edited command when user edits the command', async () => {
sandboxEnabled = false;
setAutoApprove({});

const { ConfirmTerminalCommandTool } = await import('../../browser/tools/runInTerminalConfirmationTool.js');
const confirmTool = store.add(instantiationService.createInstance(ConfirmTerminalCommandTool));

const toolSpecificData: IChatTerminalToolInvocationData = {
kind: 'terminal',
commandLine: {
original: 'git commit -m "original message"',
userEdited: 'git commit -m "edited message"',
},
language: 'sh',
};

const invocation = {
toolSpecificData,
} as any;

const result = await confirmTool.invoke(invocation, async () => 0, { report: () => { } }, CancellationToken.None);
const value = (result.content[0] as { kind: 'text'; value: string }).value;
ok(value.includes('edited message'));
ok(value.includes('git commit -m "edited message"'));
});

test('should return yes when user does not edit the command', async () => {
sandboxEnabled = false;
setAutoApprove({});

const { ConfirmTerminalCommandTool } = await import('../../browser/tools/runInTerminalConfirmationTool.js');
const confirmTool = store.add(instantiationService.createInstance(ConfirmTerminalCommandTool));

const toolSpecificData: IChatTerminalToolInvocationData = {
kind: 'terminal',
commandLine: {
original: 'echo hello',
},
language: 'sh',
};

const invocation = {
toolSpecificData,
} as any;

const result = await confirmTool.invoke(invocation, async () => 0, { report: () => { } }, CancellationToken.None);
strictEqual((result.content[0] as { kind: 'text'; value: string }).value, 'yes');
});
});
});

Expand Down