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