Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 3 additions & 1 deletion .github/instructions/api-version.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Read this when changing proposed API in vscode.proposed.*.d.ts file
applyTo: 'src/vscode-dts/**/vscode.proposed.*.d.ts'
---

The following is only required for proposed API related to chat and languageModel proposals. It's optional for other proposed API, but recommended.
The following is only useful for proposed API related to chat and languageModel proposals. It's optional for other proposed API, and not recommended.

When a proposed API is changed in a non-backwards-compatible way, the version number at the top of the file must be incremented. If it doesn't have a version number, we must add one. The format of the number like this:

Expand All @@ -16,3 +16,5 @@ No semver, just a basic incrementing integer. See existing examples in `vscode.p
An example of a non-backwards-compatible change is removing a non-optional property or changing the type to one that is incompatible with the previous type.

An example of a backwards-compatible change is an additive change or deleting a property that was already optional.

Whenever possible, make a backwards-compatible change!
13 changes: 11 additions & 2 deletions extensions/copilot/src/extension/tools/node/vscodeCmdTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,23 @@ class VSCodeCmdTool implements vscode.LanguageModelTool<IVSCodeCmdToolToolInput>
// Populate the Quick Open box with command ID rather than command name to avoid issues where Copilot didn't use the precise name,
// or when the Copilot response language (Spanish, French, etc.) might be different here than the UI one.
const commandStr = commandUri(quickOpenCommand, ['>' + commandId]);
const markdownString = new MarkdownString(l10n.t(`Copilot will execute the [{0}]({1}) (\`{2}\`) command.`, options.input.name, commandStr, options.input.commandId));
let message = l10n.t(`Copilot will execute the [{0}]({1}) (\`{2}\`) command.`, options.input.name, commandStr, options.input.commandId);
if (options.input.args?.length) {
message += `\n\n${l10n.t('Arguments')}:\n\n\`\`\`json\n${JSON.stringify(options.input.args, undefined, 2)}\n\`\`\``;
}
const markdownString = new MarkdownString(message);
markdownString.isTrusted = { enabledCommands: [quickOpenCommand] };
return {
invocationMessage,
confirmationMessages: {
title: l10n.t`Run Command \`${options.input.name}\` (\`${options.input.commandId}\`)?`,
message: markdownString,
approveCombination: options.input.args ? l10n.t`Allow running command \`${options.input.commandId}\` with specific arguments` : l10n.t`Allow running command \`${options.input.commandId}\` without arguments`,
approveCombination: {
message: options.input.args
? l10n.t`Allow running command \`${options.input.commandId}\` with specific arguments`
: l10n.t`Allow running command \`${options.input.commandId}\` without arguments`,
arguments: JSON.stringify(options.input.args),
},
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ declare module 'vscode' {
export interface LanguageModelToolConfirmationMessages {
/**
* When set, a button will be shown allowing the user to approve this particular
* combination of tool and arguments. The value is shown as the label for the
* approval option.
* combination of tool and arguments.
*
* For example, a tool that reads files could set this to `"Allow reading 'foo.txt'"`,
* For example, a tool that reads files could set this to
* `{ message: "Allow reading 'foo.txt'", arguments: JSON.stringify({ file: 'foo.txt' }) }`,
* so that the user can approve that specific file without approving all invocations
* of the tool.
*/
approveCombination?: string | MarkdownString;
approveCombination?: {
/**
* The label shown for the approval option.
*/
message: string | MarkdownString;
/**
* A string representation of the arguments that can be shown to the user.
*/
arguments?: string;
};
}
}
7 changes: 4 additions & 3 deletions src/vs/workbench/api/common/extHostLanguageModelTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,9 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape
checkProposedApiEnabled(item.extension, 'toolInvocationApproveCombination');
}

const approveCombinationLabel = result.confirmationMessages?.approveCombination
? typeConvert.MarkdownString.fromStrict(result.confirmationMessages.approveCombination)
const approveCombination = result.confirmationMessages?.approveCombination;
const approveCombinationLabel = approveCombination
? typeConvert.MarkdownString.fromStrict(approveCombination.message)
: undefined;
const approveCombinationKey = approveCombinationLabel
? await computeCombinationKey(toolId, context.parameters)
Expand All @@ -322,7 +323,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape
confirmationMessages: result.confirmationMessages ? {
title: typeof result.confirmationMessages.title === 'string' ? result.confirmationMessages.title : typeConvert.MarkdownString.from(result.confirmationMessages.title),
message: typeof result.confirmationMessages.message === 'string' ? result.confirmationMessages.message : typeConvert.MarkdownString.from(result.confirmationMessages.message),
approveCombination: approveCombinationLabel && approveCombinationKey ? { label: approveCombinationLabel, key: approveCombinationKey } : undefined,
approveCombination: approveCombinationLabel && approveCombinationKey ? { label: approveCombinationLabel, key: approveCombinationKey, arguments: approveCombination!.arguments } : undefined,
} : undefined,
invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage),
pastTenseMessage: typeConvert.MarkdownString.fromStrict(result.pastTenseMessage),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { Codicon } from '../../../../../base/common/codicons.js';
import { Lazy } from '../../../../../base/common/lazy.js';
import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
import { LRUCache } from '../../../../../base/common/map.js';
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { localize } from '../../../../../nls.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IQuickInputButtonWithToggle, IQuickInputService, IQuickTreeItem, QuickInputButtonLocation } from '../../../../../platform/quickinput/common/quickInput.js';
import { IQuickInputButton, IQuickInputButtonWithToggle, IQuickInputService, IQuickTreeItem, QuickInputButtonLocation } from '../../../../../platform/quickinput/common/quickInput.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
import { ConfirmedReason, ToolConfirmKind } from '../../common/chatService/chatService.js';
import { ILanguageModelToolConfirmationActions, ILanguageModelToolConfirmationContribution, ILanguageModelToolConfirmationContributionQuickTreeItem, ILanguageModelToolConfirmationRef, ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js';
import { IToolData, ToolDataSource } from '../../common/tools/languageModelToolsService.js';
Expand All @@ -28,6 +30,7 @@ const CONTINUE_WITHOUT_REVIEWING_RESULTS = localize('continueWithoutReviewingRes
interface IAutoConfirmEntry {
readonly confirmed: true;
readonly label?: string;
readonly arguments?: string;
}


Expand All @@ -45,13 +48,13 @@ class GenericConfirmStore extends Disposable {
this._profileStore = new Lazy(() => this._register(this._instantiationService.createInstance(ToolConfirmStore, StorageScope.PROFILE, this._storageKey)));
}

public setAutoConfirmation(id: string, scope: 'workspace' | 'profile' | 'session' | 'never', label?: string): void {
public setAutoConfirmation(id: string, scope: 'workspace' | 'profile' | 'session' | 'never', label?: string, args?: string): void {
// Clear from all scopes first
this._workspaceStore.value.setAutoConfirm(id, undefined);
this._profileStore.value.setAutoConfirm(id, undefined);
this._memoryStore.delete(id);

const entry: IAutoConfirmEntry = { confirmed: true, label };
const entry: IAutoConfirmEntry = { confirmed: true, label, arguments: args };
// Set in the appropriate scope
if (scope === 'workspace') {
this._workspaceStore.value.setAutoConfirm(id, entry);
Expand Down Expand Up @@ -91,6 +94,12 @@ class GenericConfirmStore extends Disposable {
?? this._memoryStore.get(id)?.label;
}

public getArguments(id: string): string | undefined {
return this._workspaceStore.value.getAutoConfirm(id)?.arguments
?? this._profileStore.value.getAutoConfirm(id)?.arguments
?? this._memoryStore.get(id)?.arguments;
}

public reset(): void {
this._workspaceStore.value.reset();
this._profileStore.value.reset();
Expand Down Expand Up @@ -136,7 +145,7 @@ class ToolConfirmStore extends Disposable {
) {
super();

// Read stored data — supports both legacy string[] and new Record<string, string | boolean> formats
// Read stored data — supports both legacy string[] and new Record<string, string | boolean | object> formats
const raw = storageService.get(this._storageKey, this._scope);
if (raw) {
try {
Expand All @@ -147,9 +156,15 @@ class ToolConfirmStore extends Disposable {
this._autoConfirmTools.set(key, { confirmed: true });
}
} else if (typeof parsed === 'object' && parsed !== null) {
// New format: Record<string, string | boolean>
for (const [key, value] of Object.entries(parsed)) {
this._autoConfirmTools.set(key, { confirmed: true, label: typeof value === 'string' ? value : undefined });
if (typeof value === 'object' && value !== null) {
// New format: { label?: string; arguments?: string }
const obj = value as { label?: string; arguments?: string };
this._autoConfirmTools.set(key, { confirmed: true, label: obj.label, arguments: obj.arguments });
} else {
// Legacy format: string | boolean
this._autoConfirmTools.set(key, { confirmed: true, label: typeof value === 'string' ? value : undefined });
}
}
}
} catch {
Expand All @@ -159,9 +174,13 @@ class ToolConfirmStore extends Disposable {

this._register(storageService.onWillSaveState(() => {
if (this._didChange) {
const data: Record<string, string | boolean> = {};
const data: Record<string, string | boolean | { label?: string; arguments?: string }> = {};
for (const [key, entry] of this._autoConfirmTools) {
data[key] = entry.label ?? true;
if (entry.arguments) {
data[key] = { label: entry.label, arguments: entry.arguments };
} else {
data[key] = entry.label ?? true;
}
}
this.storageService.store(this._storageKey, JSON.stringify(data), this._scope, StorageTarget.MACHINE);
this._didChange = false;
Expand Down Expand Up @@ -211,6 +230,7 @@ export class LanguageModelToolsConfirmationService extends Disposable implements
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IDialogService private readonly _dialogService: IDialogService,
) {
super();

Expand Down Expand Up @@ -309,15 +329,15 @@ export class LanguageModelToolsConfirmationService extends Disposable implements

// Add combination-level actions when approveCombination is provided
if (ref.combination) {
const { label: combinationLabel, key: combinationKey } = ref.combination;
const { label: combinationLabel, key: combinationKey, arguments: combinationArgs } = ref.combination;
actions.push(
{
label: localize('allowCombinationSession', '{0} in this Session', combinationLabel),
detail: localize('allowCombinationSessionTooltip', 'Allow this particular combination of tool and arguments in this session without confirmation.'),
divider: !!actions.length,
scope: 'session',
select: async () => {
this._combinationConfirmStore.setAutoConfirmation(combinationKey, 'session', combinationLabel);
this._combinationConfirmStore.setAutoConfirmation(combinationKey, 'session', combinationLabel, combinationArgs);
return true;
}
},
Expand All @@ -326,7 +346,7 @@ export class LanguageModelToolsConfirmationService extends Disposable implements
detail: localize('allowCombinationWorkspaceTooltip', 'Allow this particular combination of tool and arguments in this workspace without confirmation.'),
scope: 'workspace',
select: async () => {
this._combinationConfirmStore.setAutoConfirmation(combinationKey, 'workspace', combinationLabel);
this._combinationConfirmStore.setAutoConfirmation(combinationKey, 'workspace', combinationLabel, combinationArgs);
return true;
}
},
Expand All @@ -335,7 +355,7 @@ export class LanguageModelToolsConfirmationService extends Disposable implements
detail: localize('allowCombinationGloballyTooltip', 'Always allow this particular combination of tool and arguments without confirmation.'),
scope: 'profile',
select: async () => {
this._combinationConfirmStore.setAutoConfirmation(combinationKey, 'profile', combinationLabel);
this._combinationConfirmStore.setAutoConfirmation(combinationKey, 'profile', combinationLabel, combinationArgs);
return true;
}
},
Expand Down Expand Up @@ -524,13 +544,14 @@ export class LanguageModelToolsConfirmationService extends Disposable implements
return false;
}

private _getCombinationApprovalsForTool(toolId: string, scope: 'workspace' | 'profile' | 'session'): { key: string; label: string }[] {
private _getCombinationApprovalsForTool(toolId: string, scope: 'workspace' | 'profile' | 'session'): { key: string; label: string; arguments?: string }[] {
const prefix = toolId + ':combination:';
const results: { key: string; label: string }[] = [];
const results: { key: string; label: string; arguments?: string }[] = [];
for (const key of this._combinationConfirmStore.getAllConfirmed()) {
if (key.startsWith(prefix) && this._combinationConfirmStore.getAutoConfirmationIn(key, scope)) {
const label = this._combinationConfirmStore.getLabel(key) ?? key;
results.push({ key, label });
const args = this._combinationConfirmStore.getArguments(key);
results.push({ key, label, arguments: args });
}
}
return results;
Expand All @@ -543,8 +564,14 @@ export class LanguageModelToolsConfirmationService extends Disposable implements
serverId?: string;
scope?: 'workspace' | 'profile';
combinationKey?: string;
combinationArgs?: string;
}

const viewArgsButton: IQuickInputButton = {
iconClass: ThemeIcon.asClassName(Codicon.info),
tooltip: localize('viewCombinationArguments', "View Arguments"),
};

// Helper to track tools under servers
const trackServerTool = (serverId: string, label: string, toolId: string, serversWithTools: Map<string, { label: string; tools: Set<string> }>) => {
if (!serversWithTools.has(serverId)) {
Expand Down Expand Up @@ -661,13 +688,15 @@ export class LanguageModelToolsConfirmationService extends Disposable implements

// Add combination approval children
const combinationApprovals = this._getCombinationApprovalsForTool(tool.id, currentScope);
for (const { key, label } of combinationApprovals) {
for (const { key, label, arguments: args } of combinationApprovals) {
toolChildren.push({
type: 'combination',
toolId: tool.id,
combinationKey: key,
combinationArgs: args,
label,
checked: true,
buttons: args ? [viewArgsButton] : undefined,
});
}

Expand Down Expand Up @@ -802,13 +831,15 @@ export class LanguageModelToolsConfirmationService extends Disposable implements

// Add combination approval children
const combinationApprovals = this._getCombinationApprovalsForTool(tool.id, currentScope);
for (const { key, label } of combinationApprovals) {
for (const { key, label, arguments: args } of combinationApprovals) {
toolChildren.push({
type: 'combination',
toolId: tool.id,
combinationKey: key,
combinationArgs: args,
label,
checked: true,
buttons: args ? [viewArgsButton] : undefined,
});
}

Expand Down Expand Up @@ -930,6 +961,16 @@ export class LanguageModelToolsConfirmationService extends Disposable implements
disposables.add(quickTree.onDidTriggerItemButton(i => {
if (i.item.type === 'manage') {
(i.item as ILanguageModelToolConfirmationContributionQuickTreeItem).onDidTriggerItemButton?.(i.button);
} else if (i.item.type === 'combination' && i.button === viewArgsButton && i.item.combinationArgs) {
this._dialogService.prompt({
message: localize('combinationArguments', "Arguments"),
buttons: [],
custom: {
markdownDetails: [{
markdown: new MarkdownString().appendCodeblock('json', i.item.combinationArgs),
}],
},
});
}
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart {
? {
label: typeof approveCombination.label === 'string' ? approveCombination.label : approveCombination.label.value,
key: approveCombination.key,
arguments: approveCombination.arguments,
}
: undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export interface ILanguageModelToolConfirmationRef {
label: string;
/** Precomputed SHA-256 key for the combination */
key: string;
/** String representation of the arguments for this combination */
arguments?: string;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ export interface IToolConfirmationMessages {
label: string | IMarkdownString;
/** Precomputed SHA-256 key for the combination (set during tool preparation) */
key: string;
/** String representation of the arguments for this combination */
arguments?: string;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ declare module 'vscode' {
export interface LanguageModelToolConfirmationMessages {
/**
* When set, a button will be shown allowing the user to approve this particular
* combination of tool and arguments. The value is shown as the label for the
* approval option.
* combination of tool and arguments.
*
* For example, a tool that reads files could set this to `"Allow reading 'foo.txt'"`,
* For example, a tool that reads files could set this to
* `{ message: "Allow reading 'foo.txt'", arguments: JSON.stringify({ file: 'foo.txt' }) }`,
* so that the user can approve that specific file without approving all invocations
* of the tool.
*/
approveCombination?: string | MarkdownString;
approveCombination?: {
/**
* The label shown for the approval option.
*/
message: string | MarkdownString;
/**
* A string representation of the arguments that can be shown to the user.
*/
arguments?: string;
};
}
}
Loading