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