Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts
5267 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';
7
import { Emitter, Event } from '../../../../../base/common/event.js';
8
import { IMarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js';
9
import { stripIcons } from '../../../../../base/common/iconLabels.js';
10
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
11
import { URI } from '../../../../../base/common/uri.js';
12
import { localize } from '../../../../../nls.js';
13
import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../../platform/accessibility/browser/accessibleView.js';
14
import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js';
15
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
16
import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';
17
import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js';
18
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
19
import { IChatExtensionsContent, IChatPullRequestContent, IChatSimpleToolInvocationData, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolResourcesInvocationData, ILegacyChatTerminalToolInvocationData, IToolResultOutputDetailsSerialized, isLegacyChatTerminalToolInvocationData } from '../../common/chatService/chatService.js';
20
import { isResponseVM } from '../../common/model/chatViewModel.js';
21
import { IToolResultInputOutputDetails, IToolResultOutputDetails, isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../../common/tools/languageModelToolsService.js';
22
import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';
23
import { Location } from '../../../../../editor/common/languages.js';
24
25
export class ChatResponseAccessibleView implements IAccessibleViewImplementation {
26
readonly priority = 100;
27
readonly name = 'panelChat';
28
readonly type = AccessibleViewType.View;
29
readonly when = ChatContextKeys.inChatSession;
30
getProvider(accessor: ServicesAccessor) {
31
const widgetService = accessor.get(IChatWidgetService);
32
const widget = widgetService.lastFocusedWidget;
33
if (!widget) {
34
return;
35
}
36
const chatInputFocused = widget.hasInputFocus();
37
if (chatInputFocused) {
38
widget.focusResponseItem();
39
}
40
41
const verifiedWidget: IChatWidget = widget;
42
const focusedItem = verifiedWidget.getFocus();
43
if (!focusedItem) {
44
return;
45
}
46
47
return new ChatResponseAccessibleProvider(verifiedWidget, focusedItem, chatInputFocused);
48
}
49
}
50
51
type ToolSpecificData = IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatToolResourcesInvocationData;
52
type ResultDetails = Array<URI | Location> | IToolResultInputOutputDetails | IToolResultOutputDetails | IToolResultOutputDetailsSerialized;
53
54
function isOutputDetailsSerialized(obj: unknown): obj is IToolResultOutputDetailsSerialized {
55
return typeof obj === 'object' && obj !== null && 'output' in obj &&
56
typeof (obj as IToolResultOutputDetailsSerialized).output === 'object' &&
57
(obj as IToolResultOutputDetailsSerialized).output?.type === 'data' &&
58
typeof (obj as IToolResultOutputDetailsSerialized).output?.base64Data === 'string';
59
}
60
61
export function getToolSpecificDataDescription(toolSpecificData: ToolSpecificData | undefined): string {
62
if (!toolSpecificData) {
63
return '';
64
}
65
66
if (isLegacyChatTerminalToolInvocationData(toolSpecificData) || toolSpecificData.kind === 'terminal') {
67
const terminalData = migrateLegacyTerminalToolSpecificData(toolSpecificData);
68
return terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original;
69
}
70
71
switch (toolSpecificData.kind) {
72
case 'subagent': {
73
const parts: string[] = [];
74
if (toolSpecificData.agentName) {
75
parts.push(localize('subagentName', "Agent: {0}", toolSpecificData.agentName));
76
}
77
if (toolSpecificData.description) {
78
parts.push(toolSpecificData.description);
79
}
80
if (toolSpecificData.prompt) {
81
parts.push(localize('subagentPrompt', "Task: {0}", toolSpecificData.prompt));
82
}
83
return parts.join('. ') || '';
84
}
85
case 'extensions':
86
return toolSpecificData.extensions.length > 0
87
? localize('extensionsList', "Extensions: {0}", toolSpecificData.extensions.join(', '))
88
: '';
89
case 'todoList': {
90
const todos = toolSpecificData.todoList;
91
if (todos.length === 0) {
92
return '';
93
}
94
const todoDescriptions = todos.map(t =>
95
localize('todoItem', "{0} ({1})", t.title, t.status)
96
);
97
return localize('todoListCount', "{0} items: {1}", todos.length, todoDescriptions.join('; '));
98
}
99
case 'pullRequest':
100
return localize('pullRequestInfo', "PR: {0} by {1}", toolSpecificData.title, toolSpecificData.author);
101
case 'input':
102
return typeof toolSpecificData.rawInput === 'string'
103
? toolSpecificData.rawInput
104
: JSON.stringify(toolSpecificData.rawInput);
105
case 'resources': {
106
const values = toolSpecificData.values;
107
if (values.length === 0) {
108
return '';
109
}
110
const paths = values.map(v => {
111
if ('uri' in v && 'range' in v) {
112
// Location
113
return `${v.uri.fsPath || v.uri.path}:${v.range.startLineNumber}`;
114
} else {
115
// URI
116
return v.fsPath || v.path;
117
}
118
}).join(', ');
119
return localize('resourcesList', "Resources: {0}", paths);
120
}
121
case 'simpleToolInvocation': {
122
const inputText = toolSpecificData.input;
123
const outputText = toolSpecificData.output;
124
return localize('simpleToolInvocation', "Input: {0}, Output: {1}", inputText, outputText);
125
}
126
default:
127
return '';
128
}
129
}
130
131
export function getResultDetailsDescription(resultDetails: ResultDetails | undefined): { input?: string; files?: string[]; isError?: boolean } {
132
if (!resultDetails) {
133
return {};
134
}
135
136
if (Array.isArray(resultDetails)) {
137
const files = resultDetails.map(ref => {
138
if (URI.isUri(ref)) {
139
return ref.fsPath || ref.path;
140
}
141
return ref.uri.fsPath || ref.uri.path;
142
});
143
return { files };
144
}
145
146
if (isToolResultInputOutputDetails(resultDetails)) {
147
return {
148
input: resultDetails.input,
149
isError: resultDetails.isError
150
};
151
}
152
153
if (isOutputDetailsSerialized(resultDetails)) {
154
return {
155
input: localize('binaryOutput', "{0} data", resultDetails.output.mimeType)
156
};
157
}
158
159
if (isToolResultOutputDetails(resultDetails)) {
160
return {
161
input: localize('binaryOutput', "{0} data", resultDetails.output.mimeType)
162
};
163
}
164
165
return {};
166
}
167
168
export function getToolInvocationA11yDescription(
169
invocationMessage: string | undefined,
170
pastTenseMessage: string | undefined,
171
toolSpecificData: ToolSpecificData | undefined,
172
resultDetails: ResultDetails | undefined,
173
isComplete: boolean
174
): string {
175
const parts: string[] = [];
176
177
const message = isComplete && pastTenseMessage ? pastTenseMessage : invocationMessage;
178
if (message) {
179
parts.push(message);
180
}
181
182
const toolDataDesc = getToolSpecificDataDescription(toolSpecificData);
183
if (toolDataDesc) {
184
parts.push(toolDataDesc);
185
}
186
187
if (isComplete && resultDetails) {
188
const details = getResultDetailsDescription(resultDetails);
189
if (details.isError) {
190
parts.unshift(localize('errored', "Errored"));
191
}
192
if (details.input && !toolDataDesc) {
193
parts.push(localize('input', "Input: {0}", details.input));
194
}
195
if (details.files && details.files.length > 0) {
196
parts.push(localize('files', "Files: {0}", details.files.join(', ')));
197
}
198
}
199
200
return parts.join('. ');
201
}
202
203
class ChatResponseAccessibleProvider extends Disposable implements IAccessibleViewContentProvider {
204
private _focusedItem!: ChatTreeItem;
205
private readonly _focusedItemDisposables = this._register(new DisposableStore());
206
private readonly _onDidChangeContent = this._register(new Emitter<void>());
207
readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
208
constructor(
209
private readonly _widget: IChatWidget,
210
item: ChatTreeItem,
211
private readonly _wasOpenedFromInput: boolean
212
) {
213
super();
214
this._setFocusedItem(item);
215
}
216
217
readonly id = AccessibleViewProviderId.PanelChat;
218
readonly verbositySettingKey = AccessibilityVerbositySettingId.Chat;
219
readonly options = { type: AccessibleViewType.View };
220
221
provideContent(): string {
222
return this._getContent(this._focusedItem);
223
}
224
225
private _setFocusedItem(item: ChatTreeItem): void {
226
this._focusedItem = item;
227
this._focusedItemDisposables.clear();
228
if (isResponseVM(item)) {
229
this._focusedItemDisposables.add(item.model.onDidChange(() => this._onDidChangeContent.fire()));
230
}
231
}
232
233
private _renderMessageAsPlaintext(message: string | IMarkdownString): string {
234
return typeof message === 'string' ? message : stripIcons(renderAsPlaintext(message, { useLinkFormatter: true }));
235
}
236
237
private _getContent(item: ChatTreeItem): string {
238
const contentParts: string[] = [];
239
240
if (!isResponseVM(item)) {
241
return '';
242
}
243
244
if ('errorDetails' in item && item.errorDetails) {
245
contentParts.push(item.errorDetails.message);
246
}
247
248
// Process all parts in order to maintain the natural flow
249
for (const part of item.response.value) {
250
switch (part.kind) {
251
case 'thinking': {
252
const thinkingValue = Array.isArray(part.value) ? part.value.join('') : (part.value || '');
253
const trimmed = thinkingValue.trim();
254
if (trimmed) {
255
contentParts.push(localize('thinkingContent', "Thinking: {0}", trimmed));
256
}
257
break;
258
}
259
case 'markdownContent': {
260
const text = renderAsPlaintext(part.content, { includeCodeBlocksFences: true, useLinkFormatter: true });
261
if (text.trim()) {
262
contentParts.push(text);
263
}
264
break;
265
}
266
case 'elicitation2':
267
case 'elicitationSerialized': {
268
const title = part.title;
269
let elicitationContent = '';
270
if (typeof title === 'string') {
271
elicitationContent += `${title}\n`;
272
} else if (isMarkdownString(title)) {
273
elicitationContent += renderAsPlaintext(title, { includeCodeBlocksFences: true }) + '\n';
274
}
275
const message = part.message;
276
if (isMarkdownString(message)) {
277
elicitationContent += renderAsPlaintext(message, { includeCodeBlocksFences: true });
278
} else {
279
elicitationContent += message;
280
}
281
if (elicitationContent.trim()) {
282
contentParts.push(elicitationContent);
283
}
284
break;
285
}
286
case 'toolInvocation': {
287
const state = part.state.get();
288
if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.title) {
289
const title = this._renderMessageAsPlaintext(state.confirmationMessages.title);
290
const message = state.confirmationMessages.message ? this._renderMessageAsPlaintext(state.confirmationMessages.message) : '';
291
const toolDataDesc = getToolSpecificDataDescription(part.toolSpecificData);
292
let toolContent = title;
293
if (toolDataDesc) {
294
toolContent += `: ${toolDataDesc}`;
295
}
296
if (message) {
297
toolContent += `\n${message}`;
298
}
299
contentParts.push(toolContent);
300
} else if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {
301
const postApprovalDetails = isToolResultInputOutputDetails(state.resultDetails)
302
? state.resultDetails.input
303
: isToolResultOutputDetails(state.resultDetails)
304
? undefined
305
: toolContentToA11yString(state.contentForModel);
306
contentParts.push(localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", part.toolId) + (postApprovalDetails ?? ''));
307
} else {
308
const resultDetails = IChatToolInvocation.resultDetails(part);
309
const isComplete = IChatToolInvocation.isComplete(part);
310
const description = getToolInvocationA11yDescription(
311
this._renderMessageAsPlaintext(part.invocationMessage),
312
part.pastTenseMessage ? this._renderMessageAsPlaintext(part.pastTenseMessage) : undefined,
313
part.toolSpecificData,
314
resultDetails,
315
isComplete
316
);
317
if (description) {
318
contentParts.push(description);
319
}
320
}
321
break;
322
}
323
case 'toolInvocationSerialized': {
324
const description = getToolInvocationA11yDescription(
325
this._renderMessageAsPlaintext(part.invocationMessage),
326
part.pastTenseMessage ? this._renderMessageAsPlaintext(part.pastTenseMessage) : undefined,
327
part.toolSpecificData,
328
part.resultDetails,
329
part.isComplete
330
);
331
if (description) {
332
contentParts.push(description);
333
}
334
break;
335
}
336
}
337
}
338
339
return this._normalizeWhitespace(contentParts.join('\n'));
340
}
341
342
private _normalizeWhitespace(content: string): string {
343
const lines = content.split(/\r?\n/);
344
const normalized: string[] = [];
345
for (const line of lines) {
346
if (line.trim().length === 0) {
347
continue;
348
}
349
normalized.push(line);
350
}
351
return normalized.join('\n');
352
}
353
354
onClose(): void {
355
this._widget.reveal(this._focusedItem);
356
if (this._wasOpenedFromInput) {
357
this._widget.focusInput();
358
} else {
359
this._widget.focus(this._focusedItem);
360
}
361
}
362
363
provideNextContent(): string | undefined {
364
const next = this._widget.getSibling(this._focusedItem, 'next');
365
if (next) {
366
this._setFocusedItem(next);
367
return this._getContent(next);
368
}
369
return;
370
}
371
372
providePreviousContent(): string | undefined {
373
const previous = this._widget.getSibling(this._focusedItem, 'previous');
374
if (previous) {
375
this._setFocusedItem(previous);
376
return this._getContent(previous);
377
}
378
return;
379
}
380
}
381
382