diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index 6e388ab02864a..7a212fe3b53d2 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -600,7 +600,7 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { } // Setup tooltip hover for string context attachments - if (isStringVariableEntry(attachment) && attachment.tooltip) { + if ((isStringVariableEntry(attachment) || attachment.kind === 'generic') && attachment.tooltip) { this._setupTooltipHover(attachment.tooltip); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts index 9360828c6ed38..e2b5918104319 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppModel.ts @@ -9,6 +9,8 @@ import { disposableTimeout } from '../../../../../../../base/common/async.js'; import { decodeBase64 } from '../../../../../../../base/common/buffer.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../../../base/common/event.js'; +import { hash } from '../../../../../../../base/common/hash.js'; +import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { autorun, autorunSelfDisposable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js'; import { basename } from '../../../../../../../base/common/resources.js'; @@ -396,8 +398,12 @@ export class ChatMcpAppModel extends Disposable { result = await this._handleUiMessage(request.params); break; + case 'ui/update-model-context': + result = await this._handleUpdateModelContext(request.params); + break; + case 'notifications/message': - await this._mcpToolCallUI.log(request.params); + await this._mcpToolCallUI.log(request.params as MCP.LoggingMessageNotification['params']); break; default: { @@ -474,8 +480,15 @@ export class ChatMcpAppModel extends Disposable { logging: {}, sandbox: { csp: this._latestCsp, - permissions: { clipboardWrite: true }, + permissions: { clipboardWrite: {} }, }, + updateModelContext: { + audio: {}, + image: {}, + resourceLink: {}, + resource: {}, + structuredContent: {}, + } }, hostContext: this.hostContext.get(), } satisfies Required; @@ -520,6 +533,68 @@ export class ChatMcpAppModel extends Disposable { return { isError: false }; } + private async _handleUpdateModelContext(params: McpApps.McpUiUpdateModelContextRequest['params']): Promise { + const widget = this._chatWidgetService.getWidgetBySessionResource(this.renderData.sessionResource); + if (!widget) { + return {}; + } + + const idPrefix = `mcpui-context-${hash(this.renderData.serverDefinitionId)}-`; + const toDelete = widget.attachmentModel.getAttachmentIDs(); + const idsToDelete = Array.from(toDelete).filter(id => id.startsWith(idPrefix)); + const entries: IChatRequestVariableEntry[] = []; + let entryIndex = 0; + + if (params.content) { + for (const block of params.content) { + const id = `${idPrefix}${entryIndex++}`; + if (block.type === 'image') { + entries.push({ + kind: 'image', + value: decodeBase64(block.data).buffer, + id, + name: 'Image', + mimeType: block.mimeType, + }); + } else if (block.type === 'resource_link') { + const uri = McpResourceURI.fromServer({ id: this.renderData.serverDefinitionId, label: '' }, block.uri); + entries.push({ + kind: 'file', + value: uri, + id, + name: basename(uri), + }); + } else if (block.type === 'text') { + const preview = block.text.replaceAll(/\s+/g, ' ').trim(); + const truncateTo = 20; + entries.push({ + kind: 'generic', + value: block.text, + id, + tooltip: new MarkdownString().appendCodeblock('plaintext', block.text), + name: preview.length > truncateTo ? preview.slice(0, truncateTo) + '…' : preview, + }); + } + } + } + + if (params.structuredContent && Object.keys(params.structuredContent).length > 0) { + const id = `${idPrefix}structured`; + const value = JSON.stringify(params.structuredContent, null, 2); + entries.push({ + kind: 'generic', + value, + tooltip: new MarkdownString().appendCodeblock('json', value), + id, + name: 'UI Data', + }); + } + + widget.attachmentModel.updateContext(idsToDelete, entries); + + return {}; + } + private _handleSizeChanged(params: McpApps.McpUiSizeChangedNotification['params']): void { if (params.height !== undefined) { this._height = params.height; diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index dec115cb277de..33910c91c5982 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -41,6 +41,7 @@ interface IBaseChatRequestVariableEntry { export interface IGenericChatRequestVariableEntry extends IBaseChatRequestVariableEntry { kind: 'generic'; + tooltip?: IMarkdownString; } export interface IChatRequestDirectoryEntry extends IBaseChatRequestVariableEntry { diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts index dd2f5f7003530..d69b1f8b6538c 100644 --- a/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts @@ -19,6 +19,7 @@ export namespace McpApps { | MCP.ReadResourceRequest | MCP.PingRequest | (McpUiOpenLinkRequest & MCP.JSONRPCRequest) + | (McpUiUpdateModelContextRequest & MCP.JSONRPCRequest) | (McpUiMessageRequest & MCP.JSONRPCRequest) | (McpUiRequestDisplayModeRequest & MCP.JSONRPCRequest) | (McpApps.McpUiInitializeRequest & MCP.JSONRPCRequest); @@ -427,6 +428,28 @@ export namespace McpApps { params: McpUiHostContext; } + /** + * @description Request to update the agent's context without requiring a follow-up action (Guest UI -> Host). + * + * Unlike `notifications/message` which is for debugging/logging, this request is intended + * to update the Host's model context. Each request overwrites the previous context sent by the Guest UI. + * Unlike messages, context updates do not trigger follow-ups. + * + * The host will typically defer sending the context to the model until the next user message + * (including `ui/message`), and will only send the last update received. + * + * @see {@link app.App.updateModelContext} for the method that sends this request + */ + export interface McpUiUpdateModelContextRequest { + method: "ui/update-model-context"; + params: { + /** @description Context content blocks (text, image, etc.). */ + content?: ContentBlock[]; + /** @description Structured content for machine-readable context data. */ + structuredContent?: Record; + }; + } + /** * @description Request for graceful shutdown of the Guest UI (Host -> Guest UI). * @see {@link app-bridge.AppBridge.teardownResource} for the host method that sends this @@ -447,6 +470,21 @@ export namespace McpApps { [key: string]: unknown; } + export interface McpUiSupportedContentBlockModalities { + /** @description Host supports text content blocks. */ + text?: {}; + /** @description Host supports image content blocks. */ + image?: {}; + /** @description Host supports audio content blocks. */ + audio?: {}; + /** @description Host supports resource content blocks. */ + resource?: {}; + /** @description Host supports resource link content blocks. */ + resourceLink?: {}; + /** @description Host supports structured content. */ + structuredContent?: {}; + } + /** * @description Capabilities supported by the host application. * @see {@link McpUiInitializeResult} for the initialization result that includes these capabilities @@ -475,6 +513,10 @@ export namespace McpApps { /** @description CSP domains approved by the host. */ csp?: McpUiResourceCsp; }; + /** @description Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns. */ + updateModelContext?: McpUiSupportedContentBlockModalities; + /** @description Host supports receiving content messages (ui/message) from the Guest UI. */ + message?: McpUiSupportedContentBlockModalities; } /** @@ -674,4 +716,6 @@ export namespace McpApps { "ui/notifications/initialized"; export const REQUEST_DISPLAY_MODE_METHOD: McpUiRequestDisplayModeRequest["method"] = "ui/request-display-mode"; + export const UPDATE_MODEL_CONTEXT_METHOD: McpUiUpdateModelContextRequest["method"] = + "ui/update-model-context"; }