Path: blob/main/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityService.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 * 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 { Event } from '../../../../../base/common/event.js';9import { MarkdownString } from '../../../../../base/common/htmlContent.js';10import { Disposable, DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js';11import { URI } from '../../../../../base/common/uri.js';12import { localize } from '../../../../../nls.js';13import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';14import { AccessibilityProgressSignalScheduler } from '../../../../../platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler.js';15import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';16import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';17import { FocusMode } from '../../../../../platform/native/common/native.js';18import { IHostService } from '../../../../services/host/browser/host.js';19import { AccessibilityVoiceSettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';20import { ElicitationState, IChatElicitationRequest, IChatService } from '../../common/chatService/chatService.js';21import { IChatResponseViewModel } from '../../common/model/chatViewModel.js';22import { ChatConfiguration } from '../../common/constants.js';23import { IChatAccessibilityService, IChatWidgetService } from '../chat.js';24import { ChatWidget } from '../widget/chatWidget.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 notifications: Set<DisposableStore> = new Set();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}5556override dispose(): void {57for (const ds of Array.from(this.notifications)) {58ds.dispose();59}60this.notifications.clear();61super.dispose();62}6364acceptRequest(uri: URI, skipRequestSignal?: boolean): void {65if (!skipRequestSignal) {66this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true });67}68this._pendingSignalMap.set(uri, this._instantiationService.createInstance(AccessibilityProgressSignalScheduler, CHAT_RESPONSE_PENDING_ALLOWANCE_MS, undefined));69}7071disposeRequest(requestId: URI): void {72this._pendingSignalMap.deleteAndDispose(requestId);73}7475acceptResponse(widget: ChatWidget, container: HTMLElement, response: IChatResponseViewModel | string | undefined, requestId: URI, isVoiceInput?: boolean): void {76this._pendingSignalMap.deleteAndDispose(requestId);77const isPanelChat = typeof response !== 'string';78const responseContent = typeof response === 'string' ? response : response?.response.toString();79this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true });80if (!response || !responseContent) {81return;82}83const plainTextResponse = renderAsPlaintext(new MarkdownString(responseContent));84const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : '';85this._showOSNotification(widget, container, plainTextResponse + errorDetails);86if (!isVoiceInput || this._configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) !== 'on') {87status(plainTextResponse + errorDetails);88}89}90acceptElicitation(elicitation: IChatElicitationRequest): void {91if (elicitation.state.get() !== ElicitationState.Pending) {92return;93}94const title = typeof elicitation.title === 'string' ? elicitation.title : elicitation.title.value;95const message = typeof elicitation.message === 'string' ? elicitation.message : elicitation.message.value;96alert(title + ' ' + message);97this._accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired, { allowManyInParallel: true });98}99100private async _showOSNotification(widget: ChatWidget, container: HTMLElement, responseContent: string): Promise<void> {101if (!this._configurationService.getValue(ChatConfiguration.NotifyWindowOnResponseReceived)) {102return;103}104105const targetWindow = dom.getWindow(container);106if (!targetWindow) {107return;108}109110if (targetWindow.document.hasFocus()) {111return;112}113114// Don't show notification if there's no meaningful content115if (!responseContent || !responseContent.trim()) {116return;117}118119await this._hostService.focus(targetWindow, { mode: FocusMode.Notify });120121// Dispose any previous unhandled notifications to avoid replacement/coalescing.122for (const ds of Array.from(this.notifications)) {123ds.dispose();124this.notifications.delete(ds);125}126127const title = widget?.viewModel?.model.title ? localize('chatTitle', "Chat: {0}", widget.viewModel.model.title) : localize('chat.untitledChat', "Untitled Chat");128const notification = await dom.triggerNotification(title,129{130detail: localize('notificationDetail', "New chat response.")131}132);133if (!notification) {134return;135}136137const disposables = new DisposableStore();138disposables.add(notification);139this.notifications.add(disposables);140141disposables.add(Event.once(notification.onClick)(async () => {142await this._hostService.focus(targetWindow, { mode: FocusMode.Force });143await this._widgetService.reveal(widget);144widget.focusInput();145disposables.dispose();146this.notifications.delete(disposables);147}));148149disposables.add(this._hostService.onDidChangeFocus(focus => {150if (focus) {151disposables.dispose();152this.notifications.delete(disposables);153}154}));155}156157}158159160