Path: blob/main/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityService.ts
5251 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 * as dom from '../../../../../base/browser/dom.js';6import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';7import { alert, status } from '../../../../../base/browser/ui/aria/aria.js';8import { MarkdownString } from '../../../../../base/common/htmlContent.js';9import { Disposable, DisposableMap, DisposableSet, toDisposable } from '../../../../../base/common/lifecycle.js';10import { URI } from '../../../../../base/common/uri.js';11import { localize } from '../../../../../nls.js';12import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';13import { AccessibilityProgressSignalScheduler } from '../../../../../platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler.js';14import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';15import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';16import { FocusMode } from '../../../../../platform/native/common/native.js';17import { IHostService } from '../../../../services/host/browser/host.js';18import { AccessibilityVoiceSettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';19import { ElicitationState, IChatElicitationRequest, IChatService } from '../../common/chatService/chatService.js';20import { IChatResponseViewModel } from '../../common/model/chatViewModel.js';21import { ChatConfiguration } from '../../common/constants.js';22import { IChatAccessibilityService, IChatWidgetService } from '../chat.js';23import { ChatWidget } from '../widget/chatWidget.js';24import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';2526const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000;27export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService {28declare readonly _serviceBrand: undefined;2930private _pendingSignalMap: DisposableMap<URI, AccessibilityProgressSignalScheduler> = this._register(new DisposableMap());3132private readonly toasts = this._register(new DisposableSet());3334constructor(35@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,36@IInstantiationService private readonly _instantiationService: IInstantiationService,37@IConfigurationService private readonly _configurationService: IConfigurationService,38@IHostService private readonly _hostService: IHostService,39@IChatWidgetService private readonly _widgetService: IChatWidgetService,40@IChatService private readonly _chatService: IChatService,41) {42super();43this._register(this._widgetService.onDidBackgroundSession(e => {44const session = this._chatService.getSession(e);45if (!session) {46return;47}48const requestInProgress = session.requestInProgress.get();49if (!requestInProgress) {50return;51}52this.disposeRequest(e);53}));54}5556acceptRequest(uri: URI, skipRequestSignal?: boolean): void {57if (!skipRequestSignal) {58this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true });59}60this._pendingSignalMap.set(uri, this._instantiationService.createInstance(AccessibilityProgressSignalScheduler, CHAT_RESPONSE_PENDING_ALLOWANCE_MS, undefined));61}6263disposeRequest(requestId: URI): void {64this._pendingSignalMap.deleteAndDispose(requestId);65}6667acceptResponse(widget: ChatWidget, container: HTMLElement, response: IChatResponseViewModel | string | undefined, requestId: URI, isVoiceInput?: boolean): void {68this._pendingSignalMap.deleteAndDispose(requestId);69const isPanelChat = typeof response !== 'string';70const responseContent = typeof response === 'string' ? response : response?.response.toString();71this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true });72if (!response || !responseContent) {73return;74}75const plainTextResponse = renderAsPlaintext(new MarkdownString(responseContent));76const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : '';77this._showOSNotification(widget, container, plainTextResponse + errorDetails);78if (!isVoiceInput || this._configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) !== 'on') {79status(plainTextResponse + errorDetails);80}81}82acceptElicitation(elicitation: IChatElicitationRequest): void {83if (elicitation.state.get() !== ElicitationState.Pending) {84return;85}86const title = typeof elicitation.title === 'string' ? elicitation.title : elicitation.title.value;87const message = typeof elicitation.message === 'string' ? elicitation.message : elicitation.message.value;88alert(title + ' ' + message);89this._accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired, { allowManyInParallel: true });90}9192private async _showOSNotification(widget: ChatWidget, container: HTMLElement, responseContent: string): Promise<void> {93if (!this._configurationService.getValue(ChatConfiguration.NotifyWindowOnResponseReceived)) {94return;95}9697const targetWindow = dom.getWindow(container);98if (!targetWindow) {99return;100}101102if (targetWindow.document.hasFocus()) {103return;104}105106// Don't show notification if there's no meaningful content107if (!responseContent || !responseContent.trim()) {108return;109}110111await this._hostService.focus(targetWindow, { mode: FocusMode.Notify });112113// Dispose any previous unhandled notifications to avoid replacement/coalescing.114this.toasts.clearAndDisposeAll();115116const title = widget?.viewModel?.model.title ? localize('chatTitle', "Chat: {0}", widget.viewModel.model.title) : localize('chat.untitledChat', "Untitled Chat");117118const cts = new CancellationTokenSource();119const disposable = toDisposable(() => cts.dispose(true));120this.toasts.add(disposable);121122const { clicked } = await this._hostService.showToast({ title, body: localize('notificationDetail', "New chat response.") }, cts.token);123this.toasts.deleteAndDispose(disposable);124if (clicked) {125await this._hostService.focus(targetWindow, { mode: FocusMode.Force });126await this._widgetService.reveal(widget);127widget.focusInput();128}129}130131}132133134