Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsConfirmationService.ts
5240 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Codicon } from '../../../../../base/common/codicons.js';
7
import { Lazy } from '../../../../../base/common/lazy.js';
8
import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
9
import { LRUCache } from '../../../../../base/common/map.js';
10
import { ThemeIcon } from '../../../../../base/common/themables.js';
11
import { localize } from '../../../../../nls.js';
12
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
13
import { IQuickInputButtonWithToggle, IQuickInputService, IQuickTreeItem, QuickInputButtonLocation } from '../../../../../platform/quickinput/common/quickInput.js';
14
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
15
import { ConfirmedReason, ToolConfirmKind } from '../../common/chatService/chatService.js';
16
import { ILanguageModelToolConfirmationActions, ILanguageModelToolConfirmationContribution, ILanguageModelToolConfirmationContributionQuickTreeItem, ILanguageModelToolConfirmationRef, ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js';
17
import { IToolData, ToolDataSource } from '../../common/tools/languageModelToolsService.js';
18
19
const RUN_WITHOUT_APPROVAL = localize('runWithoutApproval', "without approval");
20
const CONTINUE_WITHOUT_REVIEWING_RESULTS = localize('continueWithoutReviewingResults', "without reviewing result");
21
22
23
class GenericConfirmStore extends Disposable {
24
private _workspaceStore: Lazy<ToolConfirmStore>;
25
private _profileStore: Lazy<ToolConfirmStore>;
26
private _memoryStore = new Set<string>();
27
28
constructor(
29
private readonly _storageKey: string,
30
private readonly _instantiationService: IInstantiationService,
31
) {
32
super();
33
this._workspaceStore = new Lazy(() => this._register(this._instantiationService.createInstance(ToolConfirmStore, StorageScope.WORKSPACE, this._storageKey)));
34
this._profileStore = new Lazy(() => this._register(this._instantiationService.createInstance(ToolConfirmStore, StorageScope.PROFILE, this._storageKey)));
35
}
36
37
public setAutoConfirmation(id: string, scope: 'workspace' | 'profile' | 'session' | 'never'): void {
38
// Clear from all scopes first
39
this._workspaceStore.value.setAutoConfirm(id, false);
40
this._profileStore.value.setAutoConfirm(id, false);
41
this._memoryStore.delete(id);
42
43
// Set in the appropriate scope
44
if (scope === 'workspace') {
45
this._workspaceStore.value.setAutoConfirm(id, true);
46
} else if (scope === 'profile') {
47
this._profileStore.value.setAutoConfirm(id, true);
48
} else if (scope === 'session') {
49
this._memoryStore.add(id);
50
}
51
}
52
53
public getAutoConfirmation(id: string): 'workspace' | 'profile' | 'session' | 'never' {
54
if (this._workspaceStore.value.getAutoConfirm(id)) {
55
return 'workspace';
56
}
57
if (this._profileStore.value.getAutoConfirm(id)) {
58
return 'profile';
59
}
60
if (this._memoryStore.has(id)) {
61
return 'session';
62
}
63
return 'never';
64
}
65
66
public getAutoConfirmationIn(id: string, scope: 'workspace' | 'profile' | 'session'): boolean {
67
if (scope === 'workspace') {
68
return this._workspaceStore.value.getAutoConfirm(id);
69
} else if (scope === 'profile') {
70
return this._profileStore.value.getAutoConfirm(id);
71
} else {
72
return this._memoryStore.has(id);
73
}
74
}
75
76
public reset(): void {
77
this._workspaceStore.value.reset();
78
this._profileStore.value.reset();
79
this._memoryStore.clear();
80
}
81
82
public checkAutoConfirmation(id: string): ConfirmedReason | undefined {
83
if (this._workspaceStore.value.getAutoConfirm(id)) {
84
return { type: ToolConfirmKind.LmServicePerTool, scope: 'workspace' };
85
}
86
if (this._profileStore.value.getAutoConfirm(id)) {
87
return { type: ToolConfirmKind.LmServicePerTool, scope: 'profile' };
88
}
89
if (this._memoryStore.has(id)) {
90
return { type: ToolConfirmKind.LmServicePerTool, scope: 'session' };
91
}
92
return undefined;
93
}
94
95
public getAllConfirmed(): Set<string> {
96
const all = new Set<string>();
97
for (const key of this._workspaceStore.value.getAll()) {
98
all.add(key);
99
}
100
for (const key of this._profileStore.value.getAll()) {
101
all.add(key);
102
}
103
for (const key of this._memoryStore) {
104
all.add(key);
105
}
106
return all;
107
}
108
}
109
110
class ToolConfirmStore extends Disposable {
111
private _autoConfirmTools: LRUCache<string, boolean> = new LRUCache<string, boolean>(100);
112
private _didChange = false;
113
114
constructor(
115
private readonly _scope: StorageScope,
116
private readonly _storageKey: string,
117
@IStorageService private readonly storageService: IStorageService,
118
) {
119
super();
120
121
const stored = storageService.getObject<string[]>(this._storageKey, this._scope);
122
if (stored) {
123
for (const key of stored) {
124
this._autoConfirmTools.set(key, true);
125
}
126
}
127
128
this._register(storageService.onWillSaveState(() => {
129
if (this._didChange) {
130
this.storageService.store(this._storageKey, [...this._autoConfirmTools.keys()], this._scope, StorageTarget.MACHINE);
131
this._didChange = false;
132
}
133
}));
134
}
135
136
public reset() {
137
this._autoConfirmTools.clear();
138
this._didChange = true;
139
}
140
141
public getAutoConfirm(id: string): boolean {
142
if (this._autoConfirmTools.get(id)) {
143
this._didChange = true;
144
return true;
145
}
146
147
return false;
148
}
149
150
public setAutoConfirm(id: string, autoConfirm: boolean): void {
151
if (autoConfirm) {
152
this._autoConfirmTools.set(id, true);
153
} else {
154
this._autoConfirmTools.delete(id);
155
}
156
this._didChange = true;
157
}
158
159
public getAll(): string[] {
160
return [...this._autoConfirmTools.keys()];
161
}
162
}
163
164
export class LanguageModelToolsConfirmationService extends Disposable implements ILanguageModelToolsConfirmationService {
165
declare readonly _serviceBrand: undefined;
166
167
private _preExecutionToolConfirmStore: GenericConfirmStore;
168
private _postExecutionToolConfirmStore: GenericConfirmStore;
169
private _preExecutionServerConfirmStore: GenericConfirmStore;
170
private _postExecutionServerConfirmStore: GenericConfirmStore;
171
172
private _contributions = new Map<string, ILanguageModelToolConfirmationContribution>();
173
174
constructor(
175
@IInstantiationService private readonly _instantiationService: IInstantiationService,
176
@IQuickInputService private readonly _quickInputService: IQuickInputService,
177
) {
178
super();
179
180
this._preExecutionToolConfirmStore = this._register(new GenericConfirmStore('chat/autoconfirm', this._instantiationService));
181
this._postExecutionToolConfirmStore = this._register(new GenericConfirmStore('chat/autoconfirm-post', this._instantiationService));
182
this._preExecutionServerConfirmStore = this._register(new GenericConfirmStore('chat/servers/autoconfirm', this._instantiationService));
183
this._postExecutionServerConfirmStore = this._register(new GenericConfirmStore('chat/servers/autoconfirm-post', this._instantiationService));
184
}
185
186
getPreConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined {
187
// Check contribution first
188
const contribution = this._contributions.get(ref.toolId);
189
if (contribution?.getPreConfirmAction) {
190
const result = contribution.getPreConfirmAction(ref);
191
if (result) {
192
return result;
193
}
194
}
195
196
// If contribution disables default approvals, don't check default stores
197
if (contribution && contribution.canUseDefaultApprovals === false) {
198
return undefined;
199
}
200
201
// Check tool-level confirmation
202
const toolResult = this._preExecutionToolConfirmStore.checkAutoConfirmation(ref.toolId);
203
if (toolResult) {
204
return toolResult;
205
}
206
207
// Check server-level confirmation for MCP tools
208
if (ref.source.type === 'mcp') {
209
const serverResult = this._preExecutionServerConfirmStore.checkAutoConfirmation(ref.source.definitionId);
210
if (serverResult) {
211
return serverResult;
212
}
213
}
214
215
return undefined;
216
}
217
218
getPostConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined {
219
// Check contribution first
220
const contribution = this._contributions.get(ref.toolId);
221
if (contribution?.getPostConfirmAction) {
222
const result = contribution.getPostConfirmAction(ref);
223
if (result) {
224
return result;
225
}
226
}
227
228
// If contribution disables default approvals, don't check default stores
229
if (contribution && contribution.canUseDefaultApprovals === false) {
230
return undefined;
231
}
232
233
// Check tool-level confirmation
234
const toolResult = this._postExecutionToolConfirmStore.checkAutoConfirmation(ref.toolId);
235
if (toolResult) {
236
return toolResult;
237
}
238
239
// Check server-level confirmation for MCP tools
240
if (ref.source.type === 'mcp') {
241
const serverResult = this._postExecutionServerConfirmStore.checkAutoConfirmation(ref.source.definitionId);
242
if (serverResult) {
243
return serverResult;
244
}
245
}
246
247
return undefined;
248
}
249
250
getPreConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[] {
251
const actions: ILanguageModelToolConfirmationActions[] = [];
252
253
// Add contribution actions first
254
const contribution = this._contributions.get(ref.toolId);
255
if (contribution?.getPreConfirmActions) {
256
actions.push(...contribution.getPreConfirmActions(ref));
257
}
258
259
// If contribution disables default approvals, only return contribution actions
260
if (contribution && contribution.canUseDefaultApprovals === false) {
261
return actions;
262
}
263
264
// Add default tool-level actions
265
actions.push(
266
{
267
label: localize('allowSession', 'Allow in this Session'),
268
detail: localize('allowSessionTooltip', 'Allow this tool to run in this session without confirmation.'),
269
divider: !!actions.length,
270
select: async () => {
271
this._preExecutionToolConfirmStore.setAutoConfirmation(ref.toolId, 'session');
272
return true;
273
}
274
},
275
{
276
label: localize('allowWorkspace', 'Allow in this Workspace'),
277
detail: localize('allowWorkspaceTooltip', 'Allow this tool to run in this workspace without confirmation.'),
278
select: async () => {
279
this._preExecutionToolConfirmStore.setAutoConfirmation(ref.toolId, 'workspace');
280
return true;
281
}
282
},
283
{
284
label: localize('allowGlobally', 'Always Allow'),
285
detail: localize('allowGloballyTooltip', 'Always allow this tool to run without confirmation.'),
286
select: async () => {
287
this._preExecutionToolConfirmStore.setAutoConfirmation(ref.toolId, 'profile');
288
return true;
289
}
290
}
291
);
292
293
// Add server-level actions for MCP tools
294
if (ref.source.type === 'mcp') {
295
const { serverLabel, definitionId } = ref.source;
296
actions.push(
297
{
298
label: localize('allowServerSession', 'Allow Tools from {0} in this Session', serverLabel),
299
detail: localize('allowServerSessionTooltip', 'Allow all tools from this server to run in this session without confirmation.'),
300
divider: true,
301
select: async () => {
302
this._preExecutionServerConfirmStore.setAutoConfirmation(definitionId, 'session');
303
return true;
304
}
305
},
306
{
307
label: localize('allowServerWorkspace', 'Allow Tools from {0} in this Workspace', serverLabel),
308
detail: localize('allowServerWorkspaceTooltip', 'Allow all tools from this server to run in this workspace without confirmation.'),
309
select: async () => {
310
this._preExecutionServerConfirmStore.setAutoConfirmation(definitionId, 'workspace');
311
return true;
312
}
313
},
314
{
315
label: localize('allowServerGlobally', 'Always Allow Tools from {0}', serverLabel),
316
detail: localize('allowServerGloballyTooltip', 'Always allow all tools from this server to run without confirmation.'),
317
select: async () => {
318
this._preExecutionServerConfirmStore.setAutoConfirmation(definitionId, 'profile');
319
return true;
320
}
321
}
322
);
323
}
324
325
return actions;
326
}
327
328
getPostConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[] {
329
const actions: ILanguageModelToolConfirmationActions[] = [];
330
331
// Add contribution actions first
332
const contribution = this._contributions.get(ref.toolId);
333
if (contribution?.getPostConfirmActions) {
334
actions.push(...contribution.getPostConfirmActions(ref));
335
}
336
337
// If contribution disables default approvals, only return contribution actions
338
if (contribution && contribution.canUseDefaultApprovals === false) {
339
return actions;
340
}
341
342
// Add default tool-level actions
343
actions.push(
344
{
345
label: localize('allowSessionPost', 'Allow Without Review in this Session'),
346
detail: localize('allowSessionPostTooltip', 'Allow results from this tool to be sent without confirmation in this session.'),
347
divider: !!actions.length,
348
select: async () => {
349
this._postExecutionToolConfirmStore.setAutoConfirmation(ref.toolId, 'session');
350
return true;
351
}
352
},
353
{
354
label: localize('allowWorkspacePost', 'Allow Without Review in this Workspace'),
355
detail: localize('allowWorkspacePostTooltip', 'Allow results from this tool to be sent without confirmation in this workspace.'),
356
select: async () => {
357
this._postExecutionToolConfirmStore.setAutoConfirmation(ref.toolId, 'workspace');
358
return true;
359
}
360
},
361
{
362
label: localize('allowGloballyPost', 'Always Allow Without Review'),
363
detail: localize('allowGloballyPostTooltip', 'Always allow results from this tool to be sent without confirmation.'),
364
select: async () => {
365
this._postExecutionToolConfirmStore.setAutoConfirmation(ref.toolId, 'profile');
366
return true;
367
}
368
}
369
);
370
371
// Add server-level actions for MCP tools
372
if (ref.source.type === 'mcp') {
373
const { serverLabel, definitionId } = ref.source;
374
actions.push(
375
{
376
label: localize('allowServerSessionPost', 'Allow Tools from {0} Without Review in this Session', serverLabel),
377
detail: localize('allowServerSessionPostTooltip', 'Allow results from all tools from this server to be sent without confirmation in this session.'),
378
divider: true,
379
select: async () => {
380
this._postExecutionServerConfirmStore.setAutoConfirmation(definitionId, 'session');
381
return true;
382
}
383
},
384
{
385
label: localize('allowServerWorkspacePost', 'Allow Tools from {0} Without Review in this Workspace', serverLabel),
386
detail: localize('allowServerWorkspacePostTooltip', 'Allow results from all tools from this server to be sent without confirmation in this workspace.'),
387
select: async () => {
388
this._postExecutionServerConfirmStore.setAutoConfirmation(definitionId, 'workspace');
389
return true;
390
}
391
},
392
{
393
label: localize('allowServerGloballyPost', 'Always Allow Tools from {0} Without Review', serverLabel),
394
detail: localize('allowServerGloballyPostTooltip', 'Always allow results from all tools from this server to be sent without confirmation.'),
395
select: async () => {
396
this._postExecutionServerConfirmStore.setAutoConfirmation(definitionId, 'profile');
397
return true;
398
}
399
}
400
);
401
}
402
403
return actions;
404
}
405
406
registerConfirmationContribution(toolName: string, contribution: ILanguageModelToolConfirmationContribution): IDisposable {
407
this._contributions.set(toolName, contribution);
408
return {
409
dispose: () => {
410
this._contributions.delete(toolName);
411
}
412
};
413
}
414
415
manageConfirmationPreferences(tools: readonly IToolData[], options?: { defaultScope?: 'workspace' | 'profile' | 'session' }): void {
416
interface IToolTreeItem extends IQuickTreeItem {
417
type: 'tool' | 'server' | 'tool-pre' | 'tool-post' | 'server-pre' | 'server-post' | 'manage';
418
toolId?: string;
419
serverId?: string;
420
scope?: 'workspace' | 'profile';
421
}
422
423
// Helper to track tools under servers
424
const trackServerTool = (serverId: string, label: string, toolId: string, serversWithTools: Map<string, { label: string; tools: Set<string> }>) => {
425
if (!serversWithTools.has(serverId)) {
426
serversWithTools.set(serverId, { label, tools: new Set() });
427
}
428
serversWithTools.get(serverId)!.tools.add(toolId);
429
};
430
431
// Helper to add server tool from source
432
const addServerToolFromSource = (source: ToolDataSource, toolId: string, serversWithTools: Map<string, { label: string; tools: Set<string> }>) => {
433
if (source.type === 'mcp') {
434
trackServerTool(source.definitionId, source.serverLabel || source.label, toolId, serversWithTools);
435
} else if (source.type === 'extension') {
436
trackServerTool(source.extensionId.value, source.label, toolId, serversWithTools);
437
}
438
};
439
440
// Determine which tools should be shown
441
const relevantTools = new Set<string>();
442
const serversWithTools = new Map<string, { label: string; tools: Set<string> }>();
443
444
// Add tools that request approval
445
for (const tool of tools) {
446
if (tool.canRequestPreApproval || tool.canRequestPostApproval || this._contributions.has(tool.id)) {
447
relevantTools.add(tool.id);
448
addServerToolFromSource(tool.source, tool.id, serversWithTools);
449
}
450
}
451
452
// Add tools that have stored approvals (but we can't display them without metadata)
453
for (const id of this._preExecutionToolConfirmStore.getAllConfirmed()) {
454
if (!relevantTools.has(id)) {
455
// Only add if we have the tool data
456
const tool = tools.find(t => t.id === id);
457
if (tool) {
458
relevantTools.add(id);
459
addServerToolFromSource(tool.source, id, serversWithTools);
460
}
461
}
462
}
463
for (const id of this._postExecutionToolConfirmStore.getAllConfirmed()) {
464
if (!relevantTools.has(id)) {
465
// Only add if we have the tool data
466
const tool = tools.find(t => t.id === id);
467
if (tool) {
468
relevantTools.add(id);
469
addServerToolFromSource(tool.source, id, serversWithTools);
470
}
471
}
472
}
473
474
if (relevantTools.size === 0) {
475
return; // Nothing to show
476
}
477
478
// Determine initial scope from options
479
let currentScope = options?.defaultScope ?? 'workspace';
480
481
// Helper function to build tree items based on current scope
482
const buildTreeItems = (): IToolTreeItem[] => {
483
const treeItems: IToolTreeItem[] = [];
484
485
// Add server nodes
486
for (const [serverId, serverInfo] of serversWithTools) {
487
const serverChildren: IToolTreeItem[] = [];
488
489
// Add server-level controls as first children
490
const hasAnyPre = Array.from(serverInfo.tools).some(toolId => {
491
const tool = tools.find(t => t.id === toolId);
492
return tool?.canRequestPreApproval;
493
});
494
const hasAnyPost = Array.from(serverInfo.tools).some(toolId => {
495
const tool = tools.find(t => t.id === toolId);
496
return tool?.canRequestPostApproval;
497
});
498
499
const serverPreConfirmed = this._preExecutionServerConfirmStore.getAutoConfirmationIn(serverId, currentScope);
500
const serverPostConfirmed = this._postExecutionServerConfirmStore.getAutoConfirmationIn(serverId, currentScope);
501
502
// Add individual tools from this server as children
503
for (const toolId of serverInfo.tools) {
504
const tool = tools.find(t => t.id === toolId);
505
if (!tool) {
506
continue;
507
}
508
509
const toolChildren: IToolTreeItem[] = [];
510
const hasPre = !serverPreConfirmed && (tool.canRequestPreApproval || this._preExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope));
511
const hasPost = !serverPostConfirmed && (tool.canRequestPostApproval || this._postExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope));
512
513
// Add child items for granular control when both approval types exist
514
if (hasPre && hasPost) {
515
toolChildren.push({
516
type: 'tool-pre',
517
toolId: tool.id,
518
label: RUN_WITHOUT_APPROVAL,
519
checked: this._preExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope)
520
});
521
toolChildren.push({
522
type: 'tool-post',
523
toolId: tool.id,
524
label: CONTINUE_WITHOUT_REVIEWING_RESULTS,
525
checked: this._postExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope)
526
});
527
}
528
529
// Tool item always has a checkbox
530
const preApproval = this._preExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope);
531
const postApproval = this._postExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope);
532
let checked: boolean | 'mixed';
533
let description: string | undefined;
534
535
if (hasPre && hasPost) {
536
// Both: checkbox is mixed if only one is enabled
537
checked = preApproval && postApproval ? true : (!preApproval && !postApproval ? false : 'mixed');
538
} else if (hasPre) {
539
checked = preApproval;
540
description = RUN_WITHOUT_APPROVAL;
541
} else if (hasPost) {
542
checked = postApproval;
543
description = CONTINUE_WITHOUT_REVIEWING_RESULTS;
544
} else {
545
continue;
546
}
547
548
serverChildren.push({
549
type: 'tool',
550
toolId: tool.id,
551
label: tool.displayName || tool.id,
552
description,
553
checked,
554
collapsed: true,
555
children: toolChildren.length > 0 ? toolChildren : undefined
556
});
557
}
558
559
serverChildren.sort((a, b) => a.label.localeCompare(b.label));
560
561
if (hasAnyPost) {
562
serverChildren.unshift({
563
type: 'server-post',
564
serverId,
565
iconClass: ThemeIcon.asClassName(Codicon.play),
566
label: localize('continueWithoutReviewing', "Continue without reviewing any tool results"),
567
checked: serverPostConfirmed
568
});
569
}
570
if (hasAnyPre) {
571
serverChildren.unshift({
572
type: 'server-pre',
573
serverId,
574
iconClass: ThemeIcon.asClassName(Codicon.play),
575
label: localize('runToolsWithoutApproval', "Run any tool without approval"),
576
checked: serverPreConfirmed
577
});
578
}
579
580
// Server node has checkbox to control both pre and post
581
const serverHasPre = this._preExecutionServerConfirmStore.getAutoConfirmationIn(serverId, currentScope);
582
const serverHasPost = this._postExecutionServerConfirmStore.getAutoConfirmationIn(serverId, currentScope);
583
let serverChecked: boolean | 'mixed';
584
if (hasAnyPre && hasAnyPost) {
585
serverChecked = serverHasPre && serverHasPost ? true : (!serverHasPre && !serverHasPost ? false : 'mixed');
586
} else if (hasAnyPre) {
587
serverChecked = serverHasPre;
588
} else if (hasAnyPost) {
589
serverChecked = serverHasPost;
590
} else {
591
serverChecked = false;
592
}
593
594
const existingItem = quickTree.itemTree.find(i => i.serverId === serverId);
595
treeItems.push({
596
type: 'server',
597
serverId,
598
label: serverInfo.label,
599
checked: serverChecked,
600
children: serverChildren,
601
collapsed: existingItem ? quickTree.isCollapsed(existingItem) : true,
602
pickable: false
603
});
604
}
605
606
// Add individual tool nodes (only for non-MCP/extension tools)
607
const sortedTools = tools.slice().sort((a, b) => a.displayName.localeCompare(b.displayName));
608
for (const tool of sortedTools) {
609
if (!relevantTools.has(tool.id)) {
610
continue;
611
}
612
613
// Skip tools that belong to MCP/extension servers (they're shown under server nodes)
614
if (tool.source.type === 'mcp' || tool.source.type === 'extension') {
615
continue;
616
}
617
618
const contributed = this._contributions.get(tool.id);
619
const toolChildren: IToolTreeItem[] = [];
620
621
const manageActions = contributed?.getManageActions?.();
622
if (manageActions) {
623
toolChildren.push(...manageActions.map(action => ({
624
type: 'manage' as const,
625
...action,
626
})));
627
}
628
629
630
let checked: boolean | 'mixed' = false;
631
let description: string | undefined;
632
let pickable = false;
633
634
if (contributed?.canUseDefaultApprovals !== false) {
635
pickable = true;
636
const hasPre = tool.canRequestPreApproval || this._preExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope);
637
const hasPost = tool.canRequestPostApproval || this._postExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope);
638
639
// Add child items for granular control when both approval types exist
640
if (hasPre && hasPost) {
641
toolChildren.push({
642
type: 'tool-pre',
643
toolId: tool.id,
644
label: RUN_WITHOUT_APPROVAL,
645
checked: this._preExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope)
646
});
647
toolChildren.push({
648
type: 'tool-post',
649
toolId: tool.id,
650
label: CONTINUE_WITHOUT_REVIEWING_RESULTS,
651
checked: this._postExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope)
652
});
653
}
654
655
// Tool item always has a checkbox
656
const preApproval = this._preExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope);
657
const postApproval = this._postExecutionToolConfirmStore.getAutoConfirmationIn(tool.id, currentScope);
658
659
if (hasPre && hasPost) {
660
// Both: checkbox is mixed if only one is enabled
661
checked = preApproval && postApproval ? true : (!preApproval && !postApproval ? false : 'mixed');
662
} else if (hasPre) {
663
checked = preApproval;
664
description = RUN_WITHOUT_APPROVAL;
665
} else if (hasPost) {
666
checked = postApproval;
667
description = CONTINUE_WITHOUT_REVIEWING_RESULTS;
668
} else {
669
// No approval capabilities - shouldn't happen but handle it
670
checked = false;
671
}
672
}
673
674
treeItems.push({
675
type: 'tool',
676
toolId: tool.id,
677
label: tool.displayName || tool.id,
678
description,
679
checked,
680
pickable,
681
collapsed: true,
682
children: toolChildren.length > 0 ? toolChildren : undefined
683
});
684
}
685
686
return treeItems;
687
};
688
689
const disposables = new DisposableStore();
690
const quickTree = disposables.add(this._quickInputService.createQuickTree<IToolTreeItem>());
691
quickTree.ignoreFocusOut = true;
692
quickTree.sortByLabel = false;
693
694
// Only show toggle if not in session scope
695
if (currentScope !== 'session') {
696
const scopeButton: IQuickInputButtonWithToggle = {
697
iconClass: ThemeIcon.asClassName(Codicon.folder),
698
tooltip: localize('workspaceScope', "Configure for this workspace only"),
699
toggle: { checked: currentScope === 'workspace' },
700
location: QuickInputButtonLocation.Input
701
};
702
quickTree.buttons = [scopeButton];
703
disposables.add(quickTree.onDidTriggerButton(button => {
704
if (button === scopeButton) {
705
currentScope = currentScope === 'workspace' ? 'profile' : 'workspace';
706
updatePlaceholder();
707
quickTree.setItemTree(buildTreeItems());
708
}
709
}));
710
}
711
712
const updatePlaceholder = () => {
713
if (currentScope === 'session') {
714
quickTree.placeholder = localize('configureSessionToolApprovals', "Configure session tool approvals");
715
} else {
716
quickTree.placeholder = currentScope === 'workspace'
717
? localize('configureWorkspaceToolApprovals', "Configure workspace tool approvals")
718
: localize('configureGlobalToolApprovals', "Configure global tool approvals");
719
}
720
};
721
updatePlaceholder();
722
723
quickTree.setItemTree(buildTreeItems());
724
725
disposables.add(quickTree.onDidChangeCheckboxState(item => {
726
const newState = item.checked ? currentScope : 'never';
727
728
if (item.type === 'server' && item.serverId) {
729
// Server-level checkbox: update both pre and post based on server capabilities
730
const serverInfo = serversWithTools.get(item.serverId);
731
if (serverInfo) {
732
this._preExecutionServerConfirmStore.setAutoConfirmation(item.serverId, newState);
733
this._postExecutionServerConfirmStore.setAutoConfirmation(item.serverId, newState);
734
}
735
} else if (item.type === 'tool' && item.toolId) {
736
const tool = tools.find(t => t.id === item.toolId);
737
if (tool?.canRequestPostApproval || newState === 'never') {
738
this._postExecutionToolConfirmStore.setAutoConfirmation(item.toolId, newState);
739
}
740
if (tool?.canRequestPreApproval || newState === 'never') {
741
this._preExecutionToolConfirmStore.setAutoConfirmation(item.toolId, newState);
742
}
743
} else if (item.type === 'tool-pre' && item.toolId) {
744
this._preExecutionToolConfirmStore.setAutoConfirmation(item.toolId, newState);
745
} else if (item.type === 'tool-post' && item.toolId) {
746
this._postExecutionToolConfirmStore.setAutoConfirmation(item.toolId, newState);
747
} else if (item.type === 'server-pre' && item.serverId) {
748
this._preExecutionServerConfirmStore.setAutoConfirmation(item.serverId, newState);
749
quickTree.setItemTree(buildTreeItems());
750
} else if (item.type === 'server-post' && item.serverId) {
751
this._postExecutionServerConfirmStore.setAutoConfirmation(item.serverId, newState);
752
quickTree.setItemTree(buildTreeItems());
753
} else if (item.type === 'manage') {
754
(item as ILanguageModelToolConfirmationContributionQuickTreeItem).onDidChangeChecked?.(!!item.checked);
755
}
756
}));
757
758
disposables.add(quickTree.onDidTriggerItemButton(i => {
759
if (i.item.type === 'manage') {
760
(i.item as ILanguageModelToolConfirmationContributionQuickTreeItem).onDidTriggerItemButton?.(i.button);
761
}
762
}));
763
764
disposables.add(quickTree.onDidAccept(() => {
765
for (const item of quickTree.activeItems) {
766
if (item.type === 'manage') {
767
(item as ILanguageModelToolConfirmationContributionQuickTreeItem).onDidOpen?.();
768
quickTree.hide();
769
}
770
}
771
}));
772
773
disposables.add(quickTree.onDidHide(() => {
774
disposables.dispose();
775
}));
776
777
quickTree.show();
778
}
779
780
public resetToolAutoConfirmation(): void {
781
this._preExecutionToolConfirmStore.reset();
782
this._postExecutionToolConfirmStore.reset();
783
this._preExecutionServerConfirmStore.reset();
784
this._postExecutionServerConfirmStore.reset();
785
786
// Reset all contributions
787
for (const contribution of this._contributions.values()) {
788
contribution.reset?.();
789
}
790
}
791
}
792
793