From d0e614fd9ee1856d4917b83e460ddb7909aebb04 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:36:57 +0200 Subject: [PATCH 1/6] Show combination tool approval args Fixes #306264 --- .../instructions/api-version.instructions.md | 4 +- .../extension/tools/node/vscodeCmdTool.tsx | 13 +++- ...osed.toolInvocationApproveCombination.d.ts | 17 ++++- .../api/common/extHostLanguageModelTools.ts | 7 +- .../languageModelToolsConfirmationService.ts | 75 ++++++++++++++----- .../chatToolConfirmationSubPart.ts | 1 + .../languageModelToolsConfirmationService.ts | 2 + .../common/tools/languageModelToolsService.ts | 2 + ...osed.toolInvocationApproveCombination.d.ts | 17 ++++- 9 files changed, 107 insertions(+), 31 deletions(-) diff --git a/.github/instructions/api-version.instructions.md b/.github/instructions/api-version.instructions.md index cd91d08124263..a109edad41523 100644 --- a/.github/instructions/api-version.instructions.md +++ b/.github/instructions/api-version.instructions.md @@ -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: @@ -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! diff --git a/extensions/copilot/src/extension/tools/node/vscodeCmdTool.tsx b/extensions/copilot/src/extension/tools/node/vscodeCmdTool.tsx index bda85343278da..42bc0fab5b2f0 100644 --- a/extensions/copilot/src/extension/tools/node/vscodeCmdTool.tsx +++ b/extensions/copilot/src/extension/tools/node/vscodeCmdTool.tsx @@ -94,14 +94,23 @@ class VSCodeCmdTool implements vscode.LanguageModelTool // 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), + }, }, }; } diff --git a/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts b/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts index 070cf51ef409d..b3f1a41f857a0 100644 --- a/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts +++ b/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts @@ -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; + }; } } diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 0ea35b655f6c1..f10129cfb766f 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -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) @@ -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), diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts index ed9457893d3ef..e955753cfcc2c 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts @@ -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'; @@ -28,6 +30,7 @@ const CONTINUE_WITHOUT_REVIEWING_RESULTS = localize('continueWithoutReviewingRes interface IAutoConfirmEntry { readonly confirmed: true; readonly label?: string; + readonly arguments?: string; } @@ -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); @@ -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(); @@ -136,7 +145,7 @@ class ToolConfirmStore extends Disposable { ) { super(); - // Read stored data — supports both legacy string[] and new Record formats + // Read stored data — supports both legacy string[] and new Record formats const raw = storageService.get(this._storageKey, this._scope); if (raw) { try { @@ -147,9 +156,15 @@ class ToolConfirmStore extends Disposable { this._autoConfirmTools.set(key, { confirmed: true }); } } else if (typeof parsed === 'object' && parsed !== null) { - // New format: Record 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 { @@ -159,9 +174,13 @@ class ToolConfirmStore extends Disposable { this._register(storageService.onWillSaveState(() => { if (this._didChange) { - const data: Record = {}; + const data: Record = {}; 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; @@ -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(); @@ -309,7 +329,7 @@ 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), @@ -317,7 +337,7 @@ export class LanguageModelToolsConfirmationService extends Disposable implements divider: !!actions.length, scope: 'session', select: async () => { - this._combinationConfirmStore.setAutoConfirmation(combinationKey, 'session', combinationLabel); + this._combinationConfirmStore.setAutoConfirmation(combinationKey, 'session', combinationLabel, combinationArgs); return true; } }, @@ -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; } }, @@ -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; } }, @@ -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; @@ -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 }>) => { if (!serversWithTools.has(serverId)) { @@ -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, }); } @@ -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, }); } @@ -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), + }], + }, + }); } })); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts index 0d187e8168697..2ada03f701601 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts @@ -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; diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts index fb2e048ded0a7..c294eafca4d2b 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts @@ -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; }; } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index 318347b6be2e2..a44cb3a8e00cc 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -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; }; } diff --git a/src/vscode-dts/vscode.proposed.toolInvocationApproveCombination.d.ts b/src/vscode-dts/vscode.proposed.toolInvocationApproveCombination.d.ts index 070cf51ef409d..b3f1a41f857a0 100644 --- a/src/vscode-dts/vscode.proposed.toolInvocationApproveCombination.d.ts +++ b/src/vscode-dts/vscode.proposed.toolInvocationApproveCombination.d.ts @@ -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; + }; } } From 4da691dfe22bf1a082a9d39a5a3d0aee2dd9d118 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:13:37 +0200 Subject: [PATCH 2/6] CCR feedback --- .../extension/tools/node/vscodeCmdTool.tsx | 14 ++++--- .../languageModelToolsConfirmationService.ts | 2 +- ...guageModelToolsConfirmationService.test.ts | 39 ++++++++++++++++++- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/extensions/copilot/src/extension/tools/node/vscodeCmdTool.tsx b/extensions/copilot/src/extension/tools/node/vscodeCmdTool.tsx index 42bc0fab5b2f0..e4f95d6a94218 100644 --- a/extensions/copilot/src/extension/tools/node/vscodeCmdTool.tsx +++ b/extensions/copilot/src/extension/tools/node/vscodeCmdTool.tsx @@ -94,11 +94,13 @@ class VSCodeCmdTool implements vscode.LanguageModelTool // 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]); - 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 hasArguments = !!options.input.args?.length; + const markdownString = new MarkdownString(); + markdownString.appendMarkdown(l10n.t(`Copilot will execute the [{0}]({1}) (\`{2}\`) command.`, options.input.name, commandStr, options.input.commandId)); + if (hasArguments) { + markdownString.appendMarkdown(`\n\n${l10n.t('Arguments')}:\n\n`); + markdownString.appendCodeblock(JSON.stringify(options.input.args, undefined, 2), 'json'); } - const markdownString = new MarkdownString(message); markdownString.isTrusted = { enabledCommands: [quickOpenCommand] }; return { invocationMessage, @@ -106,10 +108,10 @@ class VSCodeCmdTool implements vscode.LanguageModelTool title: l10n.t`Run Command \`${options.input.name}\` (\`${options.input.commandId}\`)?`, message: markdownString, approveCombination: { - message: options.input.args + message: hasArguments ? 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), + arguments: hasArguments ? JSON.stringify(options.input.args) : undefined, }, }, }; diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts index e955753cfcc2c..6ad89c9a47f0f 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts @@ -953,7 +953,7 @@ export class LanguageModelToolsConfirmationService extends Disposable implements } else if (item.type === 'manage') { (item as ILanguageModelToolConfirmationContributionQuickTreeItem).onDidChangeChecked?.(!!item.checked); } else if (item.type === 'combination' && item.combinationKey) { - this._combinationConfirmStore.setAutoConfirmation(item.combinationKey, newState); + this._combinationConfirmStore.setAutoConfirmation(item.combinationKey, newState, item.label, item.combinationArgs); quickTree.setItemTree(buildTreeItems()); } })); diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsConfirmationService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsConfirmationService.test.ts index 94e98ecbadc60..5fbdef75ca71b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsConfirmationService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsConfirmationService.test.ts @@ -44,12 +44,13 @@ suite('LanguageModelToolsConfirmationService', () => { }; } - async function createCombinationRef(toolId: string, parameters: unknown, combinationLabel: string): Promise { + async function createCombinationRef(toolId: string, parameters: unknown, combinationLabel: string, combinationArgs?: string): Promise { return { ...createToolRef(toolId, ToolDataSource.Internal, parameters), combination: { label: combinationLabel, key: await computeCombinationKey(toolId, parameters), + arguments: combinationArgs, }, }; } @@ -665,4 +666,40 @@ suite('LanguageModelToolsConfirmationService', () => { const ref2 = createToolRef('tool2'); assert.deepStrictEqual(newService.getPreConfirmAction(ref2), { type: ToolConfirmKind.LmServicePerTool, scope: 'workspace' }); }); + + test('object storage format with arguments round-trips across restart', () => { + // Pre-seed storage with the new object format containing arguments + const storageService = instantiationService.get(IStorageService); + const data: Record = { + 'tool1:combination:12345': { label: 'Allow reading foo.txt', arguments: '["foo.txt"]' }, + 'tool2:combination:67890': { label: 'Allow command with args' }, + }; + storageService.store('chat/autoconfirm-combination', JSON.stringify(data), StorageScope.WORKSPACE, StorageTarget.MACHINE); + + const newService = store.add(instantiationService.createInstance(LanguageModelToolsConfirmationService)); + + // Both combination keys should be auto-confirmed + const ref1: ILanguageModelToolConfirmationRef = { + ...createToolRef('tool1'), + combination: { label: 'Allow reading foo.txt', key: 'tool1:combination:12345', arguments: '["foo.txt"]' }, + }; + const ref2: ILanguageModelToolConfirmationRef = { + ...createToolRef('tool2'), + combination: { label: 'Allow command with args', key: 'tool2:combination:67890' }, + }; + + assert.deepStrictEqual(newService.getPreConfirmAction(ref1), { type: ToolConfirmKind.LmServicePerTool, scope: 'workspace' }); + assert.deepStrictEqual(newService.getPreConfirmAction(ref2), { type: ToolConfirmKind.LmServicePerTool, scope: 'workspace' }); + }); + + test('combination approval with arguments persists via workspace scope', async () => { + const ref = await createCombinationRef('testTool', { file: 'foo.txt' }, 'Allow reading "foo.txt"', '{"file":"foo.txt"}'); + + const actions = service.getPreConfirmActions(ref); + const combinationAction = actions.find(a => a.label.includes('Allow reading "foo.txt"') && a.scope === 'workspace'); + assert.ok(combinationAction); + await combinationAction.select(); + + assert.deepStrictEqual(service.getPreConfirmAction(ref), { type: ToolConfirmKind.LmServicePerTool, scope: 'workspace' }); + }); }); From cb9f093fcbb4fb6e72e8046e705edeebc592fe70 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:04:58 +0200 Subject: [PATCH 3/6] Update api versions --- ...oposed.toolInvocationApproveCombination.d.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts b/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts index b3f1a41f857a0..070cf51ef409d 100644 --- a/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts +++ b/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts @@ -10,22 +10,13 @@ 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. + * combination of tool and arguments. The value is shown as the label for the + * approval option. * - * For example, a tool that reads files could set this to - * `{ message: "Allow reading 'foo.txt'", arguments: JSON.stringify({ file: 'foo.txt' }) }`, + * For example, a tool that reads files could set this to `"Allow reading 'foo.txt'"`, * so that the user can approve that specific file without approving all invocations * of the tool. */ - 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; - }; + approveCombination?: string | MarkdownString; } } From 811ef071c542f7ebf0b6e93fc1fccc60e1c201b0 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:07:59 +0200 Subject: [PATCH 4/6] Update api version --- extensions/copilot/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 5525c801c6717..38701e316f9e2 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -6404,7 +6404,7 @@ "node-gyp": "npm:node-gyp@10.3.1", "zod": "3.25.76" }, - "vscodeCommit": "afba0a4a1fc1e34dae9073d6787b6b541bda23eb", + "vscodeCommit": "4660585937f28ea51ba45eeccff248ce5e3ad57b", "__metadata": { "id": "7ec7d6e6-b89e-4cc5-a59b-d6c4d238246f", "publisherId": { From 94c8e2adc50e26ef70af85a0de3a9efed757acaa Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:16:53 +0200 Subject: [PATCH 5/6] Not sure what happend there... --- ...oposed.toolInvocationApproveCombination.d.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts b/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts index 070cf51ef409d..b3f1a41f857a0 100644 --- a/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts +++ b/extensions/copilot/src/extension/vscode.proposed.toolInvocationApproveCombination.d.ts @@ -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; + }; } } From 46cdd21d91a285f1370312ce3fdcfcca1f1f2941 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:34:40 +0200 Subject: [PATCH 6/6] Fix dts check to use local --- extensions/copilot/package.json | 2 +- .../copilot/script/build/vscodeDtsCheck.js | 45 +++++++------ .../copilot/script/build/vscodeDtsUpdate.js | 66 ++++++++----------- 3 files changed, 49 insertions(+), 64 deletions(-) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 38701e316f9e2..18a49134c577f 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -6404,7 +6404,7 @@ "node-gyp": "npm:node-gyp@10.3.1", "zod": "3.25.76" }, - "vscodeCommit": "4660585937f28ea51ba45eeccff248ce5e3ad57b", + "vscodeCommit": "94c8e2adc50e26ef70af85a0de3a9efed757acaa", "__metadata": { "id": "7ec7d6e6-b89e-4cc5-a59b-d6c4d238246f", "publisherId": { diff --git a/extensions/copilot/script/build/vscodeDtsCheck.js b/extensions/copilot/script/build/vscodeDtsCheck.js index 97472ed508393..0baf318fc97c2 100644 --- a/extensions/copilot/script/build/vscodeDtsCheck.js +++ b/extensions/copilot/script/build/vscodeDtsCheck.js @@ -4,48 +4,47 @@ *--------------------------------------------------------------------------------------------*/ // Usage: node script/build/vscodeDtsCheck.js -// Reads vscodeCommit from package.json, re-downloads proposed d.ts files -// at that commit, checks if any differ from what's committed, then restores -// the originals. Exits with code 1 if files are out of date. +// Compares proposed d.ts files in src/extension/ against the repo's +// src/vscode-dts/ directory. Exits with code 1 if files are out of date. -const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); +const vscodeDtsDir = path.resolve('..', '..', 'src', 'vscode-dts'); const targetDir = path.resolve('src', 'extension'); function main() { const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')); - const sha = pkg.vscodeCommit; - if (!sha) { - console.error('No vscodeCommit found in package.json. Run "npm run vscode-dts:update" first.'); + const proposals = pkg.enabledApiProposals; + if (!proposals || proposals.length === 0) { + console.error('No enabledApiProposals found in package.json.'); process.exit(1); } - console.log(`Checking proposed d.ts files against vscodeCommit: ${sha}`); + console.log('Checking proposed d.ts files against src/vscode-dts/...'); - // Download proposed d.ts files using the commit SHA - execSync(`node node_modules/@vscode/dts/index.js dev ${sha}`, { stdio: 'inherit' }); - - // Compare downloaded files with committed ones - const downloaded = fs.readdirSync('.').filter(f => f.startsWith('vscode.') && f.endsWith('.ts')); const mismatched = []; - for (const f of downloaded) { - const committedPath = path.join(targetDir, f); - const newContent = fs.readFileSync(f, 'utf-8'); + for (const proposal of proposals) { + const fileName = `vscode.proposed.${proposal}.d.ts`; + const sourcePath = path.join(vscodeDtsDir, fileName); + const committedPath = path.join(targetDir, fileName); + + if (!fs.existsSync(sourcePath)) { + console.warn(`Warning: ${fileName} not found in src/vscode-dts/, skipping`); + continue; + } + + const sourceContent = fs.readFileSync(sourcePath, 'utf-8'); if (!fs.existsSync(committedPath)) { - mismatched.push(f + ' (missing)'); + mismatched.push(fileName + ' (missing)'); } else { - const oldContent = fs.readFileSync(committedPath, 'utf-8'); - if (oldContent !== newContent) { - mismatched.push(f); + const committedContent = fs.readFileSync(committedPath, 'utf-8'); + if (sourceContent !== committedContent) { + mismatched.push(fileName); } } - - // Clean up the downloaded file - fs.unlinkSync(f); } if (mismatched.length > 0) { diff --git a/extensions/copilot/script/build/vscodeDtsUpdate.js b/extensions/copilot/script/build/vscodeDtsUpdate.js index 7748412430e74..b69a3ed6c07c6 100644 --- a/extensions/copilot/script/build/vscodeDtsUpdate.js +++ b/extensions/copilot/script/build/vscodeDtsUpdate.js @@ -3,58 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// Usage: node script/build/vscodeDtsUpdate.js [branch] -// Downloads proposed API d.ts files from the given branch (default: main) -// of microsoft/vscode and writes the resolved commit SHA to package.json. +// Usage: node script/build/vscodeDtsUpdate.js +// Copies proposed API d.ts files from the repo's src/vscode-dts/ directory +// into this extension's src/extension/ folder based on enabledApiProposals. const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); -const https = require('https'); -const branch = process.argv[2] || 'main'; +const vscodeDtsDir = path.resolve('..', '..', 'src', 'vscode-dts'); +const targetDir = path.resolve('src', 'extension'); -function resolveCommitSha(branch) { - return new Promise((resolve, reject) => { - const options = { - hostname: 'api.github.com', - path: `/repos/microsoft/vscode/commits/${encodeURIComponent(branch)}`, - headers: { 'User-Agent': 'vscode-copilot-chat', 'Accept': 'application/vnd.github.sha' } - }; - https.get(options, res => { - if (res.statusCode !== 200) { - reject(new Error(`Failed to resolve commit for branch "${branch}": HTTP ${res.statusCode}`)); - return; - } - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', () => resolve(data.trim())); - }).on('error', reject); - }); -} - -async function main() { - const sha = await resolveCommitSha(branch); - console.log(`Resolved branch "${branch}" to commit ${sha}`); - - // Download proposed d.ts files using the commit SHA - execSync(`node node_modules/@vscode/dts/index.js dev ${sha}`, { stdio: 'inherit' }); +function main() { + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')); + const proposals = pkg.enabledApiProposals; + if (!proposals || proposals.length === 0) { + console.error('No enabledApiProposals found in package.json.'); + process.exit(1); + } - // Move downloaded files to src/extension/ - const files = fs.readdirSync('.').filter(f => f.startsWith('vscode.') && f.endsWith('.ts')); - for (const f of files) { - fs.renameSync(f, path.join('src', 'extension', f)); + let copied = 0; + for (const proposal of proposals) { + const fileName = `vscode.proposed.${proposal}.d.ts`; + const sourcePath = path.join(vscodeDtsDir, fileName); + if (!fs.existsSync(sourcePath)) { + console.warn(`Warning: ${fileName} not found in src/vscode-dts/`); + continue; + } + fs.copyFileSync(sourcePath, path.join(targetDir, fileName)); + copied++; } + console.log(`Copied ${copied} proposed API type definitions from src/vscode-dts/.`); - // Write the commit SHA to package.json + // Write the current commit SHA to package.json for reference + const sha = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim(); const pkgPath = path.resolve('package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); pkg.vscodeCommit = sha; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, '\t') + '\n'); console.log(`Wrote vscodeCommit: ${sha} to package.json`); } -main().catch(err => { - console.error(err); - process.exit(1); -}); +main();