diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 316a4e5606f..ba3b0487d38 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2961,6 +2961,18 @@ export namespace ChatToolInvocationPart { status: todoStatusEnumToString(todo.status) })) }; + } else if (data && 'values' in data && Array.isArray(data.values)) { + // Convert extension API resources tool data to internal format + return { + kind: 'resources', + values: data.values.map((v: any) => { + if (v instanceof types.Location) { + return Location.from(v); + } else { + return URI.revive(v); + } + }) + }; } return data; } diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index c4866fed119..5693d529263 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -16,7 +16,7 @@ import { ServicesAccessor } from '../../../../../platform/instantiation/common/i import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, ILegacyChatTerminalToolInvocationData, IToolResultOutputDetailsSerialized, isLegacyChatTerminalToolInvocationData } from '../../common/chatService/chatService.js'; +import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolResourcesInvocationData, ILegacyChatTerminalToolInvocationData, IToolResultOutputDetailsSerialized, isLegacyChatTerminalToolInvocationData } from '../../common/chatService/chatService.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; import { IToolResultInputOutputDetails, IToolResultOutputDetails, isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../../common/tools/languageModelToolsService.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js'; @@ -48,7 +48,7 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation } } -type ToolSpecificData = IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData; +type ToolSpecificData = IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatToolResourcesInvocationData; type ResultDetails = Array | IToolResultInputOutputDetails | IToolResultOutputDetails | IToolResultOutputDetailsSerialized; function isOutputDetailsSerialized(obj: unknown): obj is IToolResultOutputDetailsSerialized { @@ -102,6 +102,22 @@ export function getToolSpecificDataDescription(toolSpecificData: ToolSpecificDat return typeof toolSpecificData.rawInput === 'string' ? toolSpecificData.rawInput : JSON.stringify(toolSpecificData.rawInput); + case 'resources': { + const values = toolSpecificData.values; + if (values.length === 0) { + return ''; + } + const paths = values.map(v => { + if ('uri' in v && 'range' in v) { + // Location + return `${v.uri.fsPath || v.uri.path}:${v.range.startLineNumber}`; + } else { + // URI + return v.fsPath || v.path; + } + }).join(', '); + return localize('resourcesList', "Resources: {0}", paths); + } default: return ''; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts index 7e8eb8b1477..c60d8cb52cf 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts @@ -183,6 +183,10 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa return this.instantiationService.createInstance(ChatTerminalToolProgressPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockStartIndex, this.codeBlockModelCollection); } + if (this.toolInvocation.toolSpecificData?.kind === 'resources' && this.toolInvocation.toolSpecificData.values.length > 0) { + return this.instantiationService.createInstance(ChatResultListSubPart, this.toolInvocation, this.context, this.toolInvocation.pastTenseMessage ?? this.toolInvocation.invocationMessage, this.toolInvocation.toolSpecificData.values, this.listPool); + } + const resultDetails = IChatToolInvocation.resultDetails(this.toolInvocation); if (Array.isArray(resultDetails) && resultDetails.length) { return this.instantiationService.createInstance(ChatResultListSubPart, this.toolInvocation, this.context, this.toolInvocation.pastTenseMessage ?? this.toolInvocation.invocationMessage, resultDetails, this.listPool); diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index ff9fdf7e038..ee8f16c2cac 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -535,7 +535,7 @@ export type ConfirmedReason = export interface IChatToolInvocation { readonly presentation: IPreparedToolInvocation['presentation']; - readonly toolSpecificData?: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData; + readonly toolSpecificData?: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatToolResourcesInvocationData; readonly originMessage: string | IMarkdownString | undefined; readonly invocationMessage: string | IMarkdownString; readonly pastTenseMessage: string | IMarkdownString | undefined; @@ -793,7 +793,7 @@ export interface IToolResultOutputDetailsSerialized { */ export interface IChatToolInvocationSerialized { presentation: IPreparedToolInvocation['presentation']; - toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData; + toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatToolResourcesInvocationData; invocationMessage: string | IMarkdownString; originMessage: string | IMarkdownString | undefined; pastTenseMessage: string | IMarkdownString | undefined; @@ -840,6 +840,11 @@ export interface IChatTodoListContent { }>; } +export interface IChatToolResourcesInvocationData { + readonly kind: 'resources'; + readonly values: Array; +} + export interface IChatMcpServersStarting { readonly kind: 'mcpServersStarting'; readonly state?: IObservable; // not hydrated when serialized diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts index 728883ddfe1..7b2b9af2278 100644 --- a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -9,7 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/ import { Range } from '../../../../../../editor/common/core/range.js'; import { Location } from '../../../../../../editor/common/languages.js'; import { getToolSpecificDataDescription, getResultDetailsDescription, getToolInvocationA11yDescription } from '../../../browser/accessibility/chatResponseAccessibleView.js'; -import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData } from '../../../common/chatService/chatService.js'; +import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolResourcesInvocationData } from '../../../common/chatService/chatService.js'; suite('ChatResponseAccessibleView', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -152,6 +152,56 @@ suite('ChatResponseAccessibleView', () => { assert.ok(result.includes('key')); assert.ok(result.includes('value')); }); + + test('returns resources list for resources data with URIs', () => { + const resourcesData: IChatToolResourcesInvocationData = { + kind: 'resources', + values: [ + URI.file('/path/to/file1.ts'), + URI.file('/path/to/file2.ts') + ] + }; + const result = getToolSpecificDataDescription(resourcesData); + assert.ok(result.includes('file1.ts')); + assert.ok(result.includes('file2.ts')); + }); + + test('returns resources list for resources data with Locations', () => { + const resourcesData: IChatToolResourcesInvocationData = { + kind: 'resources', + values: [ + { uri: URI.file('/path/to/file1.ts'), range: new Range(1, 1, 10, 1) }, + { uri: URI.file('/path/to/file2.ts'), range: new Range(5, 1, 15, 1) } + ] + }; + const result = getToolSpecificDataDescription(resourcesData); + assert.ok(result.includes('file1.ts')); + assert.ok(result.includes(':1')); // Line number included for Locations + assert.ok(result.includes('file2.ts')); + assert.ok(result.includes(':5')); // Line number included for Locations + }); + + test('returns resources list for mixed URIs and Locations', () => { + const resourcesData: IChatToolResourcesInvocationData = { + kind: 'resources', + values: [ + URI.file('/path/to/file1.ts'), + { uri: URI.file('/path/to/file2.ts'), range: new Range(10, 1, 20, 1) } + ] + }; + const result = getToolSpecificDataDescription(resourcesData); + assert.ok(result.includes('file1.ts')); + assert.ok(result.includes('file2.ts')); + assert.ok(result.includes(':10')); // Line number for Location only + }); + + test('returns empty for empty resources array', () => { + const resourcesData: IChatToolResourcesInvocationData = { + kind: 'resources', + values: [] + }; + assert.strictEqual(getToolSpecificDataDescription(resourcesData), ''); + }); }); suite('getResultDetailsDescription', () => { diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index a5b9e66d572..6086bc40b55 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -280,6 +280,13 @@ declare module 'vscode' { }>; } + export interface ChatToolResourcesInvocationData { + /** + * Array of file URIs or locations to display as a collapsible list + */ + values: Array; + } + export class ChatToolInvocationPart { toolName: string; toolCallId: string; @@ -289,7 +296,7 @@ declare module 'vscode' { pastTenseMessage?: string | MarkdownString; isConfirmed?: boolean; isComplete?: boolean; - toolSpecificData?: ChatTerminalToolInvocationData | ChatMcpToolInvocationData | ChatTodoToolInvocationData; + toolSpecificData?: ChatTerminalToolInvocationData | ChatMcpToolInvocationData | ChatTodoToolInvocationData | ChatToolResourcesInvocationData; subAgentInvocationId?: string; presentation?: 'hidden' | 'hiddenAfterComplete' | undefined;