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