Skip to content

Commit 9b98fab

Browse files
authored
tools: add postapproval step for tool calls (microsoft#272628)
* wip * finish implementation
1 parent 75e6b12 commit 9b98fab

21 files changed

+1055
-456
lines changed

src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type SelectedToolClassification = {
4242

4343
export const AcceptToolConfirmationActionId = 'workbench.action.chat.acceptTool';
4444
export const SkipToolConfirmationActionId = 'workbench.action.chat.skipTool';
45+
export const AcceptToolPostConfirmationActionId = 'workbench.action.chat.acceptToolPostExecution';
46+
export const SkipToolPostConfirmationActionId = 'workbench.action.chat.skipToolPostExecution';
4547

4648
abstract class ToolConfirmationAction extends Action2 {
4749
protected abstract getReason(): ConfirmedReason;
@@ -56,7 +58,7 @@ abstract class ToolConfirmationAction extends Action2 {
5658

5759
for (const item of lastItem.model.response.value) {
5860
const state = item.kind === 'toolInvocation' ? item.state.get() : undefined;
59-
if (state?.type === IChatToolInvocation.StateKind.WaitingForConfirmation) {
61+
if (state?.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state?.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {
6062
state.confirm(this.getReason());
6163
break;
6264
}

src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts

Lines changed: 32 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,21 @@ import { Emitter } from '../../../../../base/common/event.js';
1010
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
1111
import { Disposable } from '../../../../../base/common/lifecycle.js';
1212
import { autorun, ISettableObservable, observableValue } from '../../../../../base/common/observable.js';
13-
import { basename, joinPath } from '../../../../../base/common/resources.js';
1413
import { ThemeIcon } from '../../../../../base/common/themables.js';
1514
import { URI } from '../../../../../base/common/uri.js';
16-
import { generateUuid } from '../../../../../base/common/uuid.js';
1715
import { ITextModel } from '../../../../../editor/common/model.js';
18-
import { localize, localize2 } from '../../../../../nls.js';
19-
import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';
20-
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
21-
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
16+
import { localize } from '../../../../../nls.js';
2217
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
23-
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
24-
import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
25-
import { IFileService } from '../../../../../platform/files/common/files.js';
26-
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
27-
import { ILabelService } from '../../../../../platform/label/common/label.js';
28-
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
29-
import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js';
30-
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
31-
import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../../files/browser/fileConstants.js';
32-
import { getAttachableImageExtension } from '../../common/chatModel.js';
33-
import { IChatRequestVariableEntry } from '../../common/chatVariableEntries.js';
18+
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
3419
import { IChatRendererContent } from '../../common/chatViewModel.js';
3520
import { LanguageModelPartAudience } from '../../common/languageModels.js';
3621
import { ChatTreeItem, IChatCodeBlockInfo } from '../chat.js';
3722
import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions } from '../codeBlockPart.js';
38-
import { ChatAttachmentsContentPart } from './chatAttachmentsContentPart.js';
3923
import { IDisposableReference } from './chatCollections.js';
4024
import { ChatQueryTitlePart } from './chatConfirmationWidget.js';
4125
import { IChatContentPartRenderContext } from './chatContentParts.js';
4226
import { EditorPool } from './chatMarkdownContentPart.js';
27+
import { ChatToolOutputContentSubPart } from './chatToolOutputContentSubPart.js';
4328

4429
export interface IChatCollapsibleIOCodePart {
4530
kind: 'code';
@@ -71,9 +56,17 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
7156
private _currentWidth: number = 0;
7257
private readonly _editorReferences: IDisposableReference<CodeBlockPart>[] = [];
7358
private readonly _titlePart: ChatQueryTitlePart;
59+
private _outputSubPart: ChatToolOutputContentSubPart | undefined;
7460
public readonly domNode: HTMLElement;
7561

76-
readonly codeblocks: IChatCodeBlockInfo[] = [];
62+
get codeblocks(): IChatCodeBlockInfo[] {
63+
const inputCodeblocks = this._editorReferences.map(ref => {
64+
const cbi = this.input.codeBlockInfo;
65+
return cbi;
66+
});
67+
const outputCodeblocks = this._outputSubPart?.codeblocks ?? [];
68+
return [...inputCodeblocks, ...outputCodeblocks];
69+
}
7770

7871
public set title(s: string | IMarkdownString) {
7972
this._titlePart.title = s;
@@ -101,8 +94,6 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
10194
width: number,
10295
@IContextKeyService private readonly contextKeyService: IContextKeyService,
10396
@IInstantiationService private readonly _instantiationService: IInstantiationService,
104-
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
105-
@IFileService private readonly _fileService: IFileService,
10697
) {
10798
super();
10899
this._currentWidth = width;
@@ -163,8 +154,16 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
163154
.filter(p => p.kind === 'data')
164155
.filter(p => !p.audience || p.audience.includes(LanguageModelPartAudience.User));
165156
if (topLevelResources?.length) {
166-
const group = this.addResourceGroup(topLevelResources, container.root);
157+
const resourceSubPart = this._register(this._instantiationService.createInstance(
158+
ChatToolOutputContentSubPart,
159+
this.context,
160+
this.editorPool,
161+
topLevelResources,
162+
this._currentWidth
163+
));
164+
const group = resourceSubPart.domNode;
167165
group.classList.add('chat-collapsible-top-level-resource-group');
166+
container.root.appendChild(group);
168167
this._register(autorun(r => {
169168
group.style.display = expanded.read(r) ? 'none' : '';
170169
}));
@@ -189,89 +188,21 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
189188
contents.outputTitle.remove();
190189
} else {
191190
contents.outputTitle.textContent = localize('chat.output', "Output");
192-
for (let i = 0; i < output.parts.length; i++) {
193-
const part = output.parts[i];
194-
if (part.kind === 'code') {
195-
this.addCodeBlock(part, contents.output);
196-
continue;
197-
}
198-
199-
const group: IChatCollapsibleIODataPart[] = [];
200-
for (let k = i; k < output.parts.length; k++) {
201-
const part = output.parts[k];
202-
if (part.kind !== 'data') {
203-
break;
204-
}
205-
group.push(part);
206-
}
207-
208-
this.addResourceGroup(group, contents.output);
209-
i += group.length - 1; // Skip the parts we just added
210-
}
191+
const outputSubPart = this._register(this._instantiationService.createInstance(
192+
ChatToolOutputContentSubPart,
193+
this.context,
194+
this.editorPool,
195+
output.parts,
196+
this._currentWidth
197+
));
198+
this._outputSubPart = outputSubPart;
199+
this._register(outputSubPart.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
200+
contents.output.appendChild(outputSubPart.domNode);
211201
}
212202

213203
return contents.root;
214204
}
215205

216-
private addResourceGroup(parts: IChatCollapsibleIODataPart[], container: HTMLElement) {
217-
const el = dom.h('.chat-collapsible-io-resource-group', [
218-
dom.h('.chat-collapsible-io-resource-items@items'),
219-
dom.h('.chat-collapsible-io-resource-actions@actions'),
220-
]);
221-
222-
this.fillInResourceGroup(parts, el.items, el.actions).then(() => this._onDidChangeHeight.fire());
223-
224-
container.appendChild(el.root);
225-
return el.root;
226-
}
227-
228-
private async fillInResourceGroup(parts: IChatCollapsibleIODataPart[], itemsContainer: HTMLElement, actionsContainer: HTMLElement) {
229-
const entries = await Promise.all(parts.map(async (part): Promise<IChatRequestVariableEntry> => {
230-
if (part.mimeType && getAttachableImageExtension(part.mimeType)) {
231-
const value = part.value ?? await this._fileService.readFile(part.uri).then(f => f.value.buffer, () => undefined);
232-
return { kind: 'image', id: generateUuid(), name: basename(part.uri), value, mimeType: part.mimeType, isURL: false, references: [{ kind: 'reference', reference: part.uri }] };
233-
} else {
234-
return { kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri };
235-
}
236-
}));
237-
238-
const attachments = this._register(this._instantiationService.createInstance(
239-
ChatAttachmentsContentPart,
240-
{
241-
variables: entries,
242-
limit: 5,
243-
contentReferences: undefined,
244-
domNode: undefined
245-
}
246-
));
247-
248-
attachments.contextMenuHandler = (attachment, event) => {
249-
const index = entries.indexOf(attachment);
250-
const part = parts[index];
251-
if (part) {
252-
event.preventDefault();
253-
event.stopPropagation();
254-
255-
this._contextMenuService.showContextMenu({
256-
menuId: MenuId.ChatToolOutputResourceContext,
257-
menuActionOptions: { shouldForwardArgs: true },
258-
getAnchor: () => ({ x: event.pageX, y: event.pageY }),
259-
getActionsContext: () => ({ parts: [part] } satisfies IChatToolOutputResourceToolbarContext),
260-
});
261-
}
262-
};
263-
264-
itemsContainer.appendChild(attachments.domNode!);
265-
266-
const toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, actionsContainer, MenuId.ChatToolOutputResourceToolbar, {
267-
menuOptions: {
268-
shouldForwardArgs: true,
269-
},
270-
}));
271-
toolbar.context = { parts } satisfies IChatToolOutputResourceToolbarContext;
272-
}
273-
274-
275206
private addCodeBlock(part: IChatCollapsibleIOCodePart, container: HTMLElement) {
276207
const data: ICodeBlockData = {
277208
languageId: part.languageId,
@@ -298,97 +229,6 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
298229
layout(width: number): void {
299230
this._currentWidth = width;
300231
this._editorReferences.forEach(r => r.object.layout(width));
232+
this._outputSubPart?.layout(width);
301233
}
302234
}
303-
304-
interface IChatToolOutputResourceToolbarContext {
305-
parts: IChatCollapsibleIODataPart[];
306-
}
307-
308-
class SaveResourcesAction extends Action2 {
309-
public static readonly ID = 'chat.toolOutput.save';
310-
constructor() {
311-
super({
312-
id: SaveResourcesAction.ID,
313-
title: localize2('chat.saveResources', "Save As..."),
314-
icon: Codicon.cloudDownload,
315-
menu: [{
316-
id: MenuId.ChatToolOutputResourceToolbar,
317-
group: 'navigation',
318-
order: 1
319-
}, {
320-
id: MenuId.ChatToolOutputResourceContext,
321-
}]
322-
});
323-
}
324-
325-
async run(accessor: ServicesAccessor, context: IChatToolOutputResourceToolbarContext) {
326-
const fileDialog = accessor.get(IFileDialogService);
327-
const fileService = accessor.get(IFileService);
328-
const notificationService = accessor.get(INotificationService);
329-
const progressService = accessor.get(IProgressService);
330-
const workspaceContextService = accessor.get(IWorkspaceContextService);
331-
const commandService = accessor.get(ICommandService);
332-
const labelService = accessor.get(ILabelService);
333-
const defaultFilepath = await fileDialog.defaultFilePath();
334-
335-
const savePart = async (part: IChatCollapsibleIODataPart, isFolder: boolean, uri: URI) => {
336-
const target = isFolder ? joinPath(uri, basename(part.uri)) : uri;
337-
try {
338-
if (part.kind === 'data') {
339-
await fileService.copy(part.uri, target, true);
340-
} else {
341-
// MCP doesn't support streaming data, so no sense trying
342-
const contents = await fileService.readFile(part.uri);
343-
await fileService.writeFile(target, contents.value);
344-
}
345-
} catch (e) {
346-
notificationService.error(localize('chat.saveResources.error', "Failed to save {0}: {1}", basename(part.uri), e));
347-
}
348-
};
349-
350-
const withProgress = async (thenReveal: URI, todo: (() => Promise<void>)[]) => {
351-
await progressService.withProgress({
352-
location: ProgressLocation.Notification,
353-
delay: 5_000,
354-
title: localize('chat.saveResources.progress', "Saving resources..."),
355-
}, async report => {
356-
for (const task of todo) {
357-
await task();
358-
report.report({ increment: 1, total: todo.length });
359-
}
360-
});
361-
362-
if (workspaceContextService.isInsideWorkspace(thenReveal)) {
363-
commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, thenReveal);
364-
} else {
365-
notificationService.info(localize('chat.saveResources.reveal', "Saved resources to {0}", labelService.getUriLabel(thenReveal)));
366-
}
367-
};
368-
369-
if (context.parts.length === 1) {
370-
const part = context.parts[0];
371-
const uri = await fileDialog.pickFileToSave(joinPath(defaultFilepath, basename(part.uri)));
372-
if (!uri) {
373-
return;
374-
}
375-
await withProgress(uri, [() => savePart(part, false, uri)]);
376-
} else {
377-
const uris = await fileDialog.showOpenDialog({
378-
title: localize('chat.saveResources.title', "Pick folder to save resources"),
379-
canSelectFiles: false,
380-
canSelectFolders: true,
381-
canSelectMany: false,
382-
defaultUri: workspaceContextService.getWorkspace().folders[0]?.uri,
383-
});
384-
385-
if (!uris?.length) {
386-
return;
387-
}
388-
389-
await withProgress(uris[0], context.parts.map(part => () => savePart(part, true, uris[0])));
390-
}
391-
}
392-
}
393-
394-
registerAction2(SaveResourcesAction);

0 commit comments

Comments
 (0)