Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatWindowNotifier.ts
5248 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 { mainWindow } from '../../../../base/browser/window.js';
8
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
9
import { Disposable, DisposableResourceMap, toDisposable } from '../../../../base/common/lifecycle.js';
10
import { autorunDelta, autorunIterableDelta } from '../../../../base/common/observable.js';
11
import { URI } from '../../../../base/common/uri.js';
12
import { localize } from '../../../../nls.js';
13
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
14
import { ICommandService } from '../../../../platform/commands/common/commands.js';
15
import { FocusMode } from '../../../../platform/native/common/native.js';
16
import { IWorkbenchContribution } from '../../../common/contributions.js';
17
import { IHostService } from '../../../services/host/browser/host.js';
18
import { IChatModel, IChatRequestNeedsInputInfo } from '../common/model/chatModel.js';
19
import { IChatService } from '../common/chatService/chatService.js';
20
import { IChatWidgetService } from './chat.js';
21
import { AcceptToolConfirmationActionId, IToolConfirmationActionContext } from './actions/chatToolActions.js';
22
23
/**
24
* Observes all live chat models and triggers OS notifications when any model
25
* transitions to needing input (confirmation/elicitation).
26
*/
27
export class ChatWindowNotifier extends Disposable implements IWorkbenchContribution {
28
29
static readonly ID = 'workbench.contrib.chatWindowNotifier';
30
31
private readonly _activeNotifications = this._register(new DisposableResourceMap());
32
33
constructor(
34
@IChatService private readonly _chatService: IChatService,
35
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
36
@IHostService private readonly _hostService: IHostService,
37
@IConfigurationService private readonly _configurationService: IConfigurationService,
38
@ICommandService private readonly _commandService: ICommandService,
39
) {
40
super();
41
42
const modelTrackers = this._register(new DisposableResourceMap());
43
44
this._register(autorunIterableDelta(
45
reader => this._chatService.chatModels.read(reader),
46
({ addedValues, removedValues }) => {
47
for (const model of addedValues) {
48
modelTrackers.set(model.sessionResource, this._trackModel(model));
49
}
50
for (const model of removedValues) {
51
modelTrackers.deleteAndDispose(model.sessionResource);
52
}
53
}
54
));
55
}
56
57
private _trackModel(model: IChatModel) {
58
return autorunDelta(model.requestNeedsInput, ({ lastValue, newValue }) => {
59
const currentNeedsInput = !!newValue;
60
const previousNeedsInput = !!lastValue;
61
62
// Only notify on transition from false -> true
63
if (!previousNeedsInput && currentNeedsInput && newValue) {
64
this._notifyIfNeeded(model.sessionResource, newValue);
65
} else if (previousNeedsInput && !currentNeedsInput) {
66
// Clear any active notification for this session when input is no longer needed
67
this._clearNotification(model.sessionResource);
68
}
69
});
70
}
71
72
private async _notifyIfNeeded(sessionResource: URI, info: IChatRequestNeedsInputInfo): Promise<void> {
73
// Check configuration
74
if (!this._configurationService.getValue<boolean>('chat.notifyWindowOnConfirmation')) {
75
return;
76
}
77
78
// Find the widget to determine the target window
79
const widget = this._chatWidgetService.getWidgetBySessionResource(sessionResource);
80
const targetWindow = widget ? dom.getWindow(widget.domNode) : mainWindow;
81
82
// Only notify if window doesn't have focus
83
if (targetWindow.document.hasFocus()) {
84
return;
85
}
86
87
// Clear any existing notification for this session
88
this._clearNotification(sessionResource);
89
90
// Focus window in notify mode (flash taskbar/dock)
91
await this._hostService.focus(targetWindow, { mode: FocusMode.Notify });
92
93
// Create OS notification
94
const notificationTitle = info.title ? localize('chatTitle', "Chat: {0}", info.title) : localize('chat.untitledChat', "Untitled Chat");
95
96
const cts = new CancellationTokenSource();
97
this._activeNotifications.set(sessionResource, toDisposable(() => cts.dispose(true)));
98
99
try {
100
const result = await this._hostService.showToast({
101
title: this._sanitizeOSToastText(notificationTitle),
102
body: info.detail ? this._sanitizeOSToastText(info.detail) : localize('notificationDetail', "Approval needed to continue."),
103
actions: [localize('allowAction', "Allow")],
104
}, cts.token);
105
106
if (result.clicked || typeof result.actionIndex === 'number') {
107
await this._hostService.focus(targetWindow, { mode: FocusMode.Force });
108
109
const widget = await this._chatWidgetService.openSession(sessionResource);
110
widget?.focusInput();
111
112
if (result.actionIndex === 0 /* Allow */) {
113
await this._commandService.executeCommand(AcceptToolConfirmationActionId, { sessionResource } satisfies IToolConfirmationActionContext);
114
}
115
}
116
} finally {
117
this._clearNotification(sessionResource);
118
}
119
}
120
121
private _sanitizeOSToastText(text: string): string {
122
return text.replace(/`/g, '\''); // convert backticks to single quotes
123
}
124
125
private _clearNotification(sessionResource: URI): void {
126
this._activeNotifications.deleteAndDispose(sessionResource);
127
}
128
}
129
130