Skip to content

Commit d4baa54

Browse files
authored
mcp: allow dnd on tool-generated resources (microsoft#250128)
1 parent 6e189da commit d4baa54

File tree

6 files changed

+51
-22
lines changed

6 files changed

+51
-22
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class ChatAttachmentsContentPart extends Disposable {
2828
private readonly _contextResourceLabels: ResourceLabels;
2929

3030
public contextMenuHandler?: (attachment: IChatRequestVariableEntry, event: MouseEvent) => void;
31+
public dragStartHandler?: (attachment: IChatRequestVariableEntry, event: DragEvent, element: HTMLElement) => void;
3132

3233
constructor(
3334
private readonly variables: IChatRequestVariableEntry[],
@@ -93,6 +94,7 @@ export class ChatAttachmentsContentPart extends Disposable {
9394
}
9495

9596
this._register(dom.addDisposableListener(widget.element, 'contextmenu', e => this.contextMenuHandler?.(attachment, e)));
97+
this._register(dom.addDisposableListener(widget.element, 'dragstart', e => this.dragStartHandler?.(attachment, e, widget.element)));
9698

9799
if (this.attachedContextDisposables.isDisposed) {
98100
widget.dispose();

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { DataTransfers } from '../../../../../base/browser/dnd.js';
67
import * as dom from '../../../../../base/browser/dom.js';
78
import { ButtonWithIcon } from '../../../../../base/browser/ui/button/button.js';
9+
import { applyDragImage } from '../../../../../base/browser/ui/dnd/dnd.js';
810
import { assertNever } from '../../../../../base/common/assert.js';
911
import { VSBuffer } from '../../../../../base/common/buffer.js';
1012
import { Codicon } from '../../../../../base/common/codicons.js';
@@ -32,6 +34,7 @@ import { ILabelService } from '../../../../../platform/label/common/label.js';
3234
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
3335
import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js';
3436
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
37+
import { fillEditorsDragData } from '../../../../browser/dnd.js';
3538
import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../../files/browser/fileConstants.js';
3639
import { getAttachableImageExtension, IChatRequestVariableEntry, OmittedState } from '../../common/chatModel.js';
3740
import { IChatRendererContent } from '../../common/chatViewModel.js';
@@ -55,6 +58,7 @@ export interface IChatCollapsibleIODataPart {
5558
kind: 'data';
5659
value: Uint8Array;
5760
mimeType: string;
61+
uri?: URI;
5862
}
5963

6064
export interface IChatCollapsibleIOResourcePart {
@@ -211,7 +215,7 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
211215

212216
const entries = parts.map((part): IChatRequestVariableEntry => {
213217
if (part.kind === 'data' && getAttachableImageExtension(part.mimeType)) {
214-
return { kind: 'image', id: generateUuid(), name: `image.${getAttachableImageExtension(part.mimeType)}`, value: part.value, mimeType: part.mimeType, isURL: false };
218+
return { kind: 'image', id: generateUuid(), name: part.uri ? basename(part.uri) : `image.${getAttachableImageExtension(part.mimeType)}`, value: part.value, mimeType: part.mimeType, isURL: false };
215219
} else if (part.kind === 'resource') {
216220
return { kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri };
217221
} else if (part.kind === 'data') {
@@ -244,6 +248,23 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable {
244248
}
245249
};
246250

251+
attachments.dragStartHandler = (attachment, event, element) => {
252+
if (!event.dataTransfer) {
253+
return;
254+
}
255+
256+
const index = entries.indexOf(attachment);
257+
const part = parts[index];
258+
if (!part.uri) {
259+
return;
260+
}
261+
262+
applyDragImage(event, element, attachment.name);
263+
event.dataTransfer.effectAllowed = 'copy';
264+
event.dataTransfer.setData(DataTransfers.TEXT, part.uri.toString());
265+
this._instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [part.uri!], event));
266+
};
267+
247268
el.items.appendChild(attachments.domNode!);
248269

249270
const toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, el.actions, MenuId.ChatToolOutputResourceToolbar, {
@@ -317,7 +338,7 @@ class SaveResourcesAction extends Action2 {
317338
const defaultFilepath = await fileDialog.defaultFilePath();
318339

319340
const partBasename = (part: IChatCollapsibleIODataPart | IChatCollapsibleIOResourcePart) =>
320-
part.kind === 'resource' ? basename(part.uri) : ('file' + (getExtensionForMimeType(part.mimeType) || ''));
341+
(part.kind === 'resource' || part.uri) ? basename(part.uri!) : ('file' + (getExtensionForMimeType(part.mimeType) || ''));
321342

322343
const savePart = async (part: IChatCollapsibleIODataPart | IChatCollapsibleIOResourcePart, isFolder: boolean, uri: URI) => {
323344
const target = isFolder ? joinPath(uri, partBasename(part)) : uri;

src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationS
101101
if (o.type === 'data') {
102102
const decoded = decodeBase64(o.value64).buffer;
103103
if (getAttachableImageExtension(o.mimeType)) {
104-
return { kind: 'data', value: decoded, mimeType: o.mimeType };
104+
return { kind: 'data', value: decoded, mimeType: o.mimeType, uri: o.uri };
105105
} else {
106106
return toCodePart(localize('toolResultData', "Data of type {0} ({1} bytes)", o.mimeType, decoded.byteLength));
107107
}

src/vs/workbench/contrib/chat/common/languageModelToolsService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export function isToolInvocationContext(obj: any): obj is IToolInvocationContext
125125

126126
export interface IToolResultInputOutputDetails {
127127
readonly input: string;
128-
readonly output: ({ type: 'text'; value: string } | { type: 'data'; mimeType: string; value64: string } | { type: 'resource'; uri: URI })[];
128+
readonly output: ({ type: 'text'; value: string } | { type: 'data'; mimeType: string; value64: string; uri?: URI } | { type: 'resource'; uri: URI })[];
129129
readonly isError?: boolean;
130130
}
131131

src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr
4141
| FileSystemProviderCapabilities.Readonly
4242
| FileSystemProviderCapabilities.PathCaseSensitive
4343
| FileSystemProviderCapabilities.FileReadStream
44-
| FileSystemProviderCapabilities.FileAtomicRead;
44+
| FileSystemProviderCapabilities.FileAtomicRead
45+
| FileSystemProviderCapabilities.FileReadWrite;
4546

4647
constructor(
4748
@IInstantiationService private readonly _instantiationService: IInstantiationService,

src/vs/workbench/contrib/mcp/common/mcpService.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { markdownCommandLink, MarkdownString } from '../../../../base/common/htm
1111
import { Disposable, DisposableStore, IReference, toDisposable } from '../../../../base/common/lifecycle.js';
1212
import { equals } from '../../../../base/common/objects.js';
1313
import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js';
14+
import { URI } from '../../../../base/common/uri.js';
1415
import { localize } from '../../../../nls.js';
1516
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
1617
import { ILogService } from '../../../../platform/log/common/log.js';
@@ -280,7 +281,7 @@ class McpToolImplementation implements IToolImpl {
280281
isError: callResult.isError === true,
281282
};
282283

283-
for (let item of callResult.content) {
284+
for (const item of callResult.content) {
284285
const audience = item.annotations?.audience || ['assistant'];
285286
if (audience.includes('user')) {
286287
if (item.type === 'text') {
@@ -289,9 +290,15 @@ class McpToolImplementation implements IToolImpl {
289290
}
290291

291292
// Rewrite image rsources to images so they are inlined nicely
292-
if (item.type === 'resource' && item.resource.mimeType && getAttachableImageExtension(item.resource.mimeType) && 'blob' in item.resource) {
293-
item = { type: 'image', mimeType: item.resource.mimeType, data: item.resource.blob };
294-
}
293+
const addAsInlineData = (mimeType: string, value64: string, uri?: URI) => {
294+
details.output.push({ type: 'data', mimeType, value64, uri });
295+
if (isForModel) {
296+
result.content.push({
297+
kind: 'data',
298+
value: { mimeType, data: decodeBase64(value64) }
299+
});
300+
}
301+
};
295302

296303
const isForModel = audience.includes('assistant');
297304
if (item.type === 'text') {
@@ -303,21 +310,19 @@ class McpToolImplementation implements IToolImpl {
303310
});
304311
}
305312
} else if (item.type === 'image' || item.type === 'audio') {
306-
details.output.push({ type: 'data', mimeType: item.mimeType, value64: item.data });
307-
if (isForModel) {
308-
result.content.push({
309-
kind: 'data',
310-
value: { mimeType: item.mimeType, data: decodeBase64(item.data) }
311-
});
312-
}
313+
addAsInlineData(item.mimeType, item.data);
313314
} else if (item.type === 'resource') {
314315
const uri = McpResourceURI.fromServer(this._server.definition, item.resource.uri);
315-
details.output.push({ type: 'resource', uri });
316-
if (isForModel) {
317-
result.content.push({
318-
kind: 'text',
319-
value: 'text' in item.resource ? item.resource.text : `The tool returns a resource which can be read from the URI ${uri}`,
320-
});
316+
if (item.resource.mimeType && getAttachableImageExtension(item.resource.mimeType) && 'blob' in item.resource) {
317+
addAsInlineData(item.resource.mimeType, item.resource.blob, uri);
318+
} else {
319+
details.output.push({ type: 'resource', uri });
320+
if (isForModel) {
321+
result.content.push({
322+
kind: 'text',
323+
value: 'text' in item.resource ? item.resource.text : `The tool returns a resource which can be read from the URI ${uri}`,
324+
});
325+
}
321326
}
322327
}
323328
}

0 commit comments

Comments
 (0)