From 1aa235610cf3a1cea299d2c8a5f1bc80f33027c1 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sat, 7 Feb 2026 23:07:45 +0000 Subject: [PATCH] Support rendering subagent details from external agents (#293705) * Support rendering subagent details from external agents * fix tests --- .../common/extensionsApiProposals.ts | 2 +- .../workbench/api/common/extHost.api.impl.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 16 ------- .../api/common/extHostTypeConverters.ts | 14 +++++- src/vs/workbench/api/common/extHostTypes.ts | 13 ++++++ .../chatSubagentContentPart.ts | 42 +++++++++++------- .../chat/browser/widget/chatListRenderer.ts | 43 +++++++++++++++---- .../chat/common/chatService/chatService.ts | 1 + .../contrib/chat/common/model/chatModel.ts | 4 +- .../chatSubagentContentPart.test.ts | 10 ++--- ...ode.proposed.chatParticipantAdditions.d.ts | 28 +++++++++++- 11 files changed, 120 insertions(+), 54 deletions(-) diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 1313a993a86..8769eef63c2 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -52,7 +52,7 @@ const _allApiProposals = { }, chatParticipantAdditions: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts', - version: 2 + version: 3 }, chatParticipantPrivate: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index c58a6d55165..ef3f6a731dd 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -2048,6 +2048,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatRequestTurn2: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatResponseTurn2: extHostTypes.ChatResponseTurn2, + ChatSubagentToolInvocationData: extHostTypes.ChatSubagentToolInvocationData, ChatToolInvocationPart: extHostTypes.ChatToolInvocationPart, ChatLocation: extHostTypes.ChatLocation, ChatSessionStatus: extHostTypes.ChatSessionStatus, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6f2a87d3ecb..0d23cba7e36 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1563,7 +1563,6 @@ export type IChatProgressDto = | IChatResponseClearToPreviousToolInvocationDto | IChatBeginToolInvocationDto | IChatUpdateToolInvocationDto - | IChatExternalToolInvocationUpdateDto | IChatUsageDto; export interface ExtHostUrlsShape { @@ -2375,21 +2374,6 @@ export interface IChatUpdateToolInvocationDto { }; } -/** - * DTO for external tool invocation updates from extensions. - * When isComplete is false, creates a new live tool invocation. - * When isComplete is true, completes an existing tool invocation. - */ -export interface IChatExternalToolInvocationUpdateDto { - kind: 'externalToolInvocationUpdate'; - toolCallId: string; - toolName: string; - isComplete: boolean; - invocationMessage?: string | IMarkdownString; - pastTenseMessage?: string | IMarkdownString; - toolSpecificData?: unknown; -} - export interface IChatUsageDto { kind: 'usage'; promptTokens: number; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 429dfdb5b13..885302dcb9b 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -41,7 +41,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/participants/chatAgents.js'; import { IChatRequestModeInstructions } from '../../contrib/chat/common/model/chatModel.js'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffDataSerialized, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTerminalToolInvocationData, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatWarningMessage, IChatWorkspaceEdit } from '../../contrib/chat/common/chatService/chatService.js'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatExternalToolInvocationUpdate, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffDataSerialized, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTerminalToolInvocationData, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatWarningMessage, IChatWorkspaceEdit } from '../../contrib/chat/common/chatService/chatService.js'; import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js'; import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImageVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; @@ -2899,7 +2899,7 @@ export namespace ChatResponseMovePart { } export namespace ChatToolInvocationPart { - export function from(part: vscode.ChatToolInvocationPart): IChatToolInvocationSerialized | extHostProtocol.IChatExternalToolInvocationUpdateDto { + export function from(part: vscode.ChatToolInvocationPart): IChatToolInvocationSerialized | IChatExternalToolInvocationUpdate { // Check if toolSpecificData is ChatMcpToolInvocationData (has input and output) // If so, convert to resultDetails for rendering via ChatInputOutputMarkdownProgressPart let resultDetails: IToolResultInputOutputDetails | undefined; @@ -2931,6 +2931,7 @@ export namespace ChatToolInvocationPart { invocationMessage: part.invocationMessage ? MarkdownString.from(part.invocationMessage) : undefined, pastTenseMessage: part.pastTenseMessage ? MarkdownString.from(part.pastTenseMessage) : undefined, toolSpecificData, + subagentInvocationId: part.subAgentInvocationId, }; } @@ -3027,6 +3028,15 @@ export namespace ChatToolInvocationPart { } }) }; + } else if (data instanceof types.ChatSubagentToolInvocationData) { + // Convert extension API subagent tool data to internal format + return { + kind: 'subagent', + description: data.description, + agentName: data.agentName, + prompt: data.prompt, + result: data.result, + }; } return data; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1ee94016a03..9e47fb165dc 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3176,6 +3176,19 @@ export class McpToolInvocationContentData { } } +export class ChatSubagentToolInvocationData { + description?: string; + agentName?: string; + prompt?: string; + result?: string; + constructor(description?: string, agentName?: string, prompt?: string, result?: string) { + this.description = description; + this.agentName = agentName; + this.prompt = prompt; + this.result = result; + } +} + export class ChatResponseExternalEditPart { applied: Thenable; didGetApplied!: (value: string) => void; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts index d2f01472493..86a60cfacfd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentContentPart.ts @@ -6,30 +6,30 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { $, AnimationFrameScheduler, DisposableResizeObserver } from '../../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { Lazy } from '../../../../../../base/common/lazy.js'; import { IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { autorun } from '../../../../../../base/common/observable.js'; import { rcut } from '../../../../../../base/common/strings.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { localize } from '../../../../../../nls.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IChatMarkdownContent, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; import { IChatRendererContent } from '../../../common/model/chatViewModel.js'; +import { IRunSubagentToolInputParams } from '../../../common/tools/builtinTools/runSubagentTool.js'; +import { CodeBlockModelCollection } from '../../../common/widget/codeBlockModelCollection.js'; import { ChatTreeItem } from '../../chat.js'; -import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js'; import { ChatCollapsibleMarkdownContentPart } from './chatCollapsibleMarkdownContentPart.js'; -import { IChatMarkdownContent, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; -import { autorun } from '../../../../../../base/common/observable.js'; -import { Lazy } from '../../../../../../base/common/lazy.js'; -import { createThinkingIcon, getToolInvocationIcon } from './chatThinkingContentPart.js'; -import { CollapsibleListPool } from './chatReferencesContentPart.js'; import { EditorPool } from './chatContentCodePools.js'; -import { CodeBlockModelCollection } from '../../../common/widget/codeBlockModelCollection.js'; -import { ChatToolInvocationPart } from './toolInvocationParts/chatToolInvocationPart.js'; +import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { IChatMarkdownAnchorService } from './chatMarkdownAnchorService.js'; -import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { CollapsibleListPool } from './chatReferencesContentPart.js'; +import { createThinkingIcon, getToolInvocationIcon } from './chatThinkingContentPart.js'; import './media/chatSubagentContent.css'; -import { IRunSubagentToolInputParams, RunSubagentTool } from '../../../common/tools/builtinTools/runSubagentTool.js'; +import { ChatToolInvocationPart } from './toolInvocationParts/chatToolInvocationPart.js'; const MAX_TITLE_LENGTH = 100; @@ -88,13 +88,22 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen private userManuallyExpanded: boolean = false; private autoExpandedForConfirmation: boolean = false; + /** + * Check if a tool invocation is the parent subagent tool (the tool that spawns a subagent). + * A parent subagent tool has subagent toolSpecificData but no subAgentInvocationId. + */ + private static isParentSubagentTool(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): boolean { + return toolInvocation.toolSpecificData?.kind === 'subagent' && !toolInvocation.subAgentInvocationId; + } + /** * Extracts subagent info (description, agentName, prompt) from a tool invocation. */ private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } { const defaultDescription = localize('chat.subagent.defaultDescription', 'Running subagent...'); - if (toolInvocation.toolId !== RunSubagentTool.Id) { + // Only parent subagent tools contain the full subagent info + if (!ChatSubagentContentPart.isParentSubagentTool(toolInvocation)) { return { description: defaultDescription, agentName: undefined, prompt: undefined, modelName: undefined }; } @@ -400,7 +409,8 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen * Handles both live and serialized invocations. */ private watchToolCompletion(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): void { - if (toolInvocation.toolId !== RunSubagentTool.Id) { + // Only watch parent subagent tools for completion + if (!ChatSubagentContentPart.isParentSubagentTool(toolInvocation)) { return; } @@ -750,9 +760,9 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen } // Match subagent tool invocations with the same subAgentInvocationId to keep them grouped - if ((other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') && (other.subAgentInvocationId || other.toolId === RunSubagentTool.Id)) { - // For runSubagent tool, use toolCallId as the effective ID - const otherEffectiveId = other.toolId === RunSubagentTool.Id ? other.toolCallId : other.subAgentInvocationId; + if ((other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') && (other.subAgentInvocationId || ChatSubagentContentPart.isParentSubagentTool(other))) { + // For parent subagent tool, use toolCallId as the effective ID + const otherEffectiveId = other.subAgentInvocationId ?? other.toolCallId; // If both have IDs, they must match if (this.subAgentInvocationId && otherEffectiveId) { return this.subAgentInvocationId === otherEffectiveId; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 38beb93405c..8e4414f6a24 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -103,7 +103,6 @@ import { ChatMarkdownDecorationsRenderer } from './chatContentParts/chatMarkdown import { ChatEditorOptions } from './chatOptions.js'; import { ChatCodeBlockContentProvider, CodeBlockPart } from './chatContentParts/codeBlockPart.js'; import { autorun, observableValue } from '../../../../../base/common/observable.js'; -import { RunSubagentTool } from '../../common/tools/builtinTools/runSubagentTool.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { IChatTipService } from '../chatTipService.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; @@ -1450,8 +1449,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { source: ToolDataSource.Internal, toolId: options.toolId ?? RunSubagentTool.Id, toolCallId: toolCallId, - subAgentInvocationId: options.subAgentInvocationId ?? 'test-subagent-id', + subAgentInvocationId: options.subAgentInvocationId, state: observableValue('state', stateValue), kind: 'toolInvocation', toJSON: () => createMockSerializedToolInvocation({ toolId: options.toolId ?? RunSubagentTool.Id, - subAgentInvocationId: options.subAgentInvocationId ?? 'test-subagent-id', + subAgentInvocationId: options.subAgentInvocationId, toolSpecificData: options.toolSpecificData, isComplete: stateType === IChatToolInvocation.StateKind.Completed }) @@ -183,7 +183,7 @@ suite('ChatSubagentContentPart', () => { toolCallId: options.subAgentInvocationId ?? 'test-tool-call-id', toolId: options.toolId ?? RunSubagentTool.Id, source: ToolDataSource.Internal, - subAgentInvocationId: options.subAgentInvocationId ?? 'test-subagent-id', + subAgentInvocationId: options.subAgentInvocationId, kind: 'toolInvocationSerialized' }; } @@ -245,7 +245,7 @@ suite('ChatSubagentContentPart', () => { ): ChatSubagentContentPart { const part = store.add(instantiationService.createInstance( ChatSubagentContentPart, - idOverride ?? toolInvocation.subAgentInvocationId!, + idOverride ?? toolInvocation.subAgentInvocationId ?? toolInvocation.toolCallId, toolInvocation, context, mockMarkdownRenderer, @@ -483,7 +483,6 @@ suite('ChatSubagentContentPart', () => { const toolInvocation = createMockToolInvocation({ toolId: RunSubagentTool.Id, toolCallId: sharedToolCallId, - subAgentInvocationId: 'call-abc' }); const context = createMockRenderContext(false); @@ -492,7 +491,6 @@ suite('ChatSubagentContentPart', () => { const otherInvocation = createMockToolInvocation({ toolId: RunSubagentTool.Id, toolCallId: sharedToolCallId, - subAgentInvocationId: 'call-abc' }); const result = part.hasSameContent(otherInvocation, [], context.element); diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 10051cc255e..77720dfe450 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 2 +// version: 3 declare module 'vscode' { @@ -302,6 +302,30 @@ declare module 'vscode' { values: Array; } + export class ChatSubagentToolInvocationData { + /** + * A description of the subagent's purpose or task. + */ + description?: string; + + /** + * The name of the subagent being invoked. + */ + agentName?: string; + + /** + * The prompt given to the subagent. + */ + prompt?: string; + + /** + * The result text from the subagent after completion. + */ + result?: string; + + constructor(description?: string, agentName?: string, prompt?: string, result?: string); + } + export class ChatToolInvocationPart { toolName: string; toolCallId: string; @@ -311,7 +335,7 @@ declare module 'vscode' { pastTenseMessage?: string | MarkdownString; isConfirmed?: boolean; isComplete?: boolean; - toolSpecificData?: ChatTerminalToolInvocationData | ChatMcpToolInvocationData | ChatTodoToolInvocationData | ChatSimpleToolResultData | ChatToolResourcesInvocationData; + toolSpecificData?: ChatTerminalToolInvocationData | ChatMcpToolInvocationData | ChatTodoToolInvocationData | ChatSimpleToolResultData | ChatToolResourcesInvocationData | ChatSubagentToolInvocationData; subAgentInvocationId?: string; presentation?: 'hidden' | 'hiddenAfterComplete' | undefined;