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
4780 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 { isMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
9
import { stripIcons } from '../../../../../base/common/iconLabels.js';
10
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
11
import { localize } from '../../../../../nls.js';
12
import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../../platform/accessibility/browser/accessibleView.js';
13
import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js';
14
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
15
import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';
16
import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js';
17
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
18
import { IChatToolInvocation } from '../../common/chatService/chatService.js';
19
import { isResponseVM } from '../../common/model/chatViewModel.js';
20
import { isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../../common/tools/languageModelToolsService.js';
21
import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';
22
23
export class ChatResponseAccessibleView implements IAccessibleViewImplementation {
24
readonly priority = 100;
25
readonly name = 'panelChat';
26
readonly type = AccessibleViewType.View;
27
readonly when = ChatContextKeys.inChatSession;
28
getProvider(accessor: ServicesAccessor) {
29
const widgetService = accessor.get(IChatWidgetService);
30
const widget = widgetService.lastFocusedWidget;
31
if (!widget) {
32
return;
33
}
34
const chatInputFocused = widget.hasInputFocus();
35
if (chatInputFocused) {
36
widget.focusResponseItem();
37
}
38
39
const verifiedWidget: IChatWidget = widget;
40
const focusedItem = verifiedWidget.getFocus();
41
if (!focusedItem) {
42
return;
43
}
44
45
return new ChatResponseAccessibleProvider(verifiedWidget, focusedItem, chatInputFocused);
46
}
47
}
48
49
class ChatResponseAccessibleProvider extends Disposable implements IAccessibleViewContentProvider {
50
private _focusedItem!: ChatTreeItem;
51
private readonly _focusedItemDisposables = this._register(new DisposableStore());
52
private readonly _onDidChangeContent = this._register(new Emitter<void>());
53
readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
54
constructor(
55
private readonly _widget: IChatWidget,
56
item: ChatTreeItem,
57
private readonly _wasOpenedFromInput: boolean
58
) {
59
super();
60
this._setFocusedItem(item);
61
}
62
63
readonly id = AccessibleViewProviderId.PanelChat;
64
readonly verbositySettingKey = AccessibilityVerbositySettingId.Chat;
65
readonly options = { type: AccessibleViewType.View };
66
67
provideContent(): string {
68
return this._getContent(this._focusedItem);
69
}
70
71
private _setFocusedItem(item: ChatTreeItem): void {
72
this._focusedItem = item;
73
this._focusedItemDisposables.clear();
74
if (isResponseVM(item)) {
75
this._focusedItemDisposables.add(item.model.onDidChange(() => this._onDidChangeContent.fire()));
76
}
77
}
78
79
private _getContent(item: ChatTreeItem): string {
80
let responseContent = isResponseVM(item) ? item.response.toString() : '';
81
if (!responseContent && 'errorDetails' in item && item.errorDetails) {
82
responseContent = item.errorDetails.message;
83
}
84
if (isResponseVM(item)) {
85
item.response.value.filter(item => item.kind === 'elicitation2' || item.kind === 'elicitationSerialized').forEach(elicitation => {
86
const title = elicitation.title;
87
if (typeof title === 'string') {
88
responseContent += `${title}\n`;
89
} else if (isMarkdownString(title)) {
90
responseContent += renderAsPlaintext(title, { includeCodeBlocksFences: true }) + '\n';
91
}
92
const message = elicitation.message;
93
if (isMarkdownString(message)) {
94
responseContent += renderAsPlaintext(message, { includeCodeBlocksFences: true });
95
} else {
96
responseContent += message;
97
}
98
});
99
const toolInvocations = item.response.value.filter(item => item.kind === 'toolInvocation');
100
for (const toolInvocation of toolInvocations) {
101
const state = toolInvocation.state.get();
102
if (toolInvocation.confirmationMessages?.title && state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) {
103
const title = typeof toolInvocation.confirmationMessages.title === 'string' ? toolInvocation.confirmationMessages.title : toolInvocation.confirmationMessages.title.value;
104
const message = typeof toolInvocation.confirmationMessages.message === 'string' ? toolInvocation.confirmationMessages.message : stripIcons(renderAsPlaintext(toolInvocation.confirmationMessages.message!));
105
let input = '';
106
if (toolInvocation.toolSpecificData) {
107
if (toolInvocation.toolSpecificData?.kind === 'terminal') {
108
const terminalData = migrateLegacyTerminalToolSpecificData(toolInvocation.toolSpecificData);
109
input = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original;
110
} else {
111
input = toolInvocation.toolSpecificData?.kind === 'extensions'
112
? JSON.stringify(toolInvocation.toolSpecificData.extensions)
113
: toolInvocation.toolSpecificData?.kind === 'todoList'
114
? JSON.stringify(toolInvocation.toolSpecificData.todoList)
115
: toolInvocation.toolSpecificData?.kind === 'pullRequest'
116
? JSON.stringify(toolInvocation.toolSpecificData)
117
: JSON.stringify(toolInvocation.toolSpecificData.rawInput);
118
}
119
}
120
responseContent += `${title}`;
121
if (input) {
122
responseContent += `: ${input}`;
123
}
124
responseContent += `\n${message}\n`;
125
} else if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {
126
const postApprovalDetails = isToolResultInputOutputDetails(state.resultDetails)
127
? state.resultDetails.input
128
: isToolResultOutputDetails(state.resultDetails)
129
? undefined
130
: toolContentToA11yString(state.contentForModel);
131
responseContent += localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", toolInvocation.toolId) + (postApprovalDetails ?? '') + '\n';
132
} else {
133
const resultDetails = IChatToolInvocation.resultDetails(toolInvocation);
134
if (resultDetails && 'input' in resultDetails) {
135
responseContent += '\n' + (resultDetails.isError ? 'Errored ' : 'Completed ');
136
responseContent += `${`${typeof toolInvocation.invocationMessage === 'string' ? toolInvocation.invocationMessage : stripIcons(renderAsPlaintext(toolInvocation.invocationMessage))} with input: ${resultDetails.input}`}\n`;
137
}
138
}
139
}
140
141
const pastConfirmations = item.response.value.filter(item => item.kind === 'toolInvocationSerialized');
142
for (const pastConfirmation of pastConfirmations) {
143
if (pastConfirmation.isComplete && pastConfirmation.resultDetails && 'input' in pastConfirmation.resultDetails) {
144
if (pastConfirmation.pastTenseMessage) {
145
responseContent += `\n${`${typeof pastConfirmation.pastTenseMessage === 'string' ? pastConfirmation.pastTenseMessage : stripIcons(renderAsPlaintext(pastConfirmation.pastTenseMessage))} with input: ${pastConfirmation.resultDetails.input}`}\n`;
146
}
147
}
148
}
149
}
150
const plainText = renderAsPlaintext(new MarkdownString(responseContent), { includeCodeBlocksFences: true });
151
return this._normalizeWhitespace(plainText);
152
}
153
154
private _normalizeWhitespace(content: string): string {
155
const lines = content.split(/\r?\n/);
156
const normalized: string[] = [];
157
for (const line of lines) {
158
if (line.trim().length === 0) {
159
continue;
160
}
161
normalized.push(line);
162
}
163
return normalized.join('\n');
164
}
165
166
onClose(): void {
167
this._widget.reveal(this._focusedItem);
168
if (this._wasOpenedFromInput) {
169
this._widget.focusInput();
170
} else {
171
this._widget.focus(this._focusedItem);
172
}
173
}
174
175
provideNextContent(): string | undefined {
176
const next = this._widget.getSibling(this._focusedItem, 'next');
177
if (next) {
178
this._setFocusedItem(next);
179
return this._getContent(next);
180
}
181
return;
182
}
183
184
providePreviousContent(): string | undefined {
185
const previous = this._widget.getSibling(this._focusedItem, 'previous');
186
if (previous) {
187
this._setFocusedItem(previous);
188
return this._getContent(previous);
189
}
190
return;
191
}
192
}
193
194