Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.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 { raceCancellablePromises, timeout } from '../../../../../base/common/async.js';
8
import { Emitter, Event } from '../../../../../base/common/event.js';
9
import { combinedDisposable, Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
10
import { isEqual } from '../../../../../base/common/resources.js';
11
import { URI } from '../../../../../base/common/uri.js';
12
import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js';
13
import { ACTIVE_GROUP, IEditorService, type PreferredGroup } from '../../../../services/editor/common/editorService.js';
14
import { IEditorGroup, IEditorGroupsService, isEditorGroup } from '../../../../services/editor/common/editorGroupsService.js';
15
import { IViewsService } from '../../../../services/views/common/viewsService.js';
16
import { IChatService } from '../../common/chatService/chatService.js';
17
import { ChatAgentLocation } from '../../common/constants.js';
18
import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService, IQuickChatService, isIChatViewViewContext } from '../chat.js';
19
import { ChatEditor, IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js';
20
import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';
21
import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js';
22
23
export class ChatWidgetService extends Disposable implements IChatWidgetService {
24
25
declare readonly _serviceBrand: undefined;
26
27
private _widgets: IChatWidget[] = [];
28
private _lastFocusedWidget: IChatWidget | undefined = undefined;
29
30
private readonly _onDidAddWidget = this._register(new Emitter<IChatWidget>());
31
readonly onDidAddWidget = this._onDidAddWidget.event;
32
33
private readonly _onDidBackgroundSession = this._register(new Emitter<URI>());
34
readonly onDidBackgroundSession = this._onDidBackgroundSession.event;
35
36
constructor(
37
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
38
@IViewsService private readonly viewsService: IViewsService,
39
@IQuickChatService private readonly quickChatService: IQuickChatService,
40
@ILayoutService private readonly layoutService: ILayoutService,
41
@IEditorService private readonly editorService: IEditorService,
42
@IChatService private readonly chatService: IChatService,
43
) {
44
super();
45
}
46
47
get lastFocusedWidget(): IChatWidget | undefined {
48
return this._lastFocusedWidget;
49
}
50
51
getAllWidgets(): ReadonlyArray<IChatWidget> {
52
return this._widgets;
53
}
54
55
getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray<IChatWidget> {
56
return this._widgets.filter(w => w.location === location);
57
}
58
59
getWidgetByInputUri(uri: URI): IChatWidget | undefined {
60
return this._widgets.find(w => isEqual(w.input.inputUri, uri));
61
}
62
63
getWidgetBySessionResource(sessionResource: URI): IChatWidget | undefined {
64
return this._widgets.find(w => isEqual(w.viewModel?.sessionResource, sessionResource));
65
}
66
67
async revealWidget(preserveFocus?: boolean): Promise<IChatWidget | undefined> {
68
const last = this.lastFocusedWidget;
69
if (last && await this.reveal(last, preserveFocus)) {
70
return last;
71
}
72
73
return (await this.viewsService.openView<ChatViewPane>(ChatViewId, !preserveFocus))?.widget;
74
}
75
76
async reveal(widget: IChatWidget, preserveFocus?: boolean): Promise<boolean> {
77
if (widget.viewModel?.sessionResource) {
78
const alreadyOpenWidget = await this.revealSessionIfAlreadyOpen(widget.viewModel.sessionResource, { preserveFocus });
79
if (alreadyOpenWidget) {
80
return true;
81
}
82
}
83
84
if (isIChatViewViewContext(widget.viewContext)) {
85
const view = await this.viewsService.openView(widget.viewContext.viewId, !preserveFocus);
86
if (!preserveFocus) {
87
view?.focus();
88
}
89
return !!view;
90
}
91
92
return false;
93
}
94
95
/**
96
* Reveal the session if already open, otherwise open it.
97
*/
98
openSession(sessionResource: URI, target?: typeof ChatViewPaneTarget): Promise<IChatWidget | undefined>;
99
openSession(sessionResource: URI, target?: PreferredGroup, options?: IChatEditorOptions): Promise<IChatWidget | undefined>;
100
async openSession(sessionResource: URI, target?: typeof ChatViewPaneTarget | PreferredGroup, options?: IChatEditorOptions): Promise<IChatWidget | undefined> {
101
// Reveal if already open unless instructed otherwise
102
if (typeof target === 'undefined' || options?.revealIfOpened) {
103
const alreadyOpenWidget = await this.revealSessionIfAlreadyOpen(sessionResource, options);
104
if (alreadyOpenWidget) {
105
return alreadyOpenWidget;
106
}
107
} else {
108
await this.prepareSessionForMove(sessionResource, target);
109
}
110
111
// Load this session in chat view
112
if (target === ChatViewPaneTarget) {
113
const chatView = await this.viewsService.openView<ChatViewPane>(ChatViewId, !options?.preserveFocus);
114
if (chatView) {
115
await chatView.loadSession(sessionResource);
116
if (!options?.preserveFocus) {
117
chatView.focusInput();
118
}
119
}
120
return chatView?.widget;
121
}
122
123
// Open in chat editor
124
const pane = await this.editorService.openEditor({
125
resource: sessionResource,
126
options: {
127
...options,
128
revealIfOpened: options?.revealIfOpened ?? true // always try to reveal if already opened unless explicitly told not to
129
}
130
}, target);
131
return pane instanceof ChatEditor ? pane.widget : undefined;
132
}
133
134
private async revealSessionIfAlreadyOpen(sessionResource: URI, options?: IChatEditorOptions): Promise<IChatWidget | undefined> {
135
// Already open in chat view?
136
const chatView = this.viewsService.getViewWithId<ChatViewPane>(ChatViewId);
137
if (chatView?.widget.viewModel?.sessionResource && isEqual(chatView.widget.viewModel.sessionResource, sessionResource)) {
138
const view = await this.viewsService.openView(ChatViewId, !options?.preserveFocus);
139
if (!options?.preserveFocus) {
140
view?.focus();
141
}
142
return chatView.widget;
143
}
144
145
// Already open in an editor?
146
const existingEditor = this.findExistingChatEditorByUri(sessionResource);
147
if (existingEditor) {
148
const existingEditorWindowId = existingEditor.group.windowId;
149
150
// focus transfer to other documents is async. If we depend on the focus
151
// being synchronously transferred in consuming code, this can fail, so
152
// wait for it to propagate
153
const isGroupActive = () => dom.getWindow(this.layoutService.activeContainer).vscodeWindowId === existingEditorWindowId;
154
155
let ensureFocusTransfer: Promise<void> | undefined;
156
if (!isGroupActive()) {
157
ensureFocusTransfer = raceCancellablePromises([
158
timeout(500),
159
Event.toPromise(Event.once(Event.filter(this.layoutService.onDidChangeActiveContainer, isGroupActive))),
160
]);
161
}
162
163
const pane = await existingEditor.group.openEditor(existingEditor.editor, options);
164
await ensureFocusTransfer;
165
return pane instanceof ChatEditor ? pane.widget : undefined;
166
}
167
168
// Already open in quick chat?
169
if (isEqual(sessionResource, this.quickChatService.sessionResource)) {
170
this.quickChatService.focus();
171
return undefined;
172
}
173
174
return undefined;
175
}
176
177
private async prepareSessionForMove(sessionResource: URI, target: typeof ChatViewPaneTarget | PreferredGroup | undefined): Promise<void> {
178
const existingWidget = this.getWidgetBySessionResource(sessionResource);
179
if (existingWidget) {
180
const existingEditor = isIChatViewViewContext(existingWidget.viewContext) ?
181
undefined :
182
this.findExistingChatEditorByUri(sessionResource);
183
184
if (isIChatViewViewContext(existingWidget.viewContext) && target === ChatViewPaneTarget) {
185
return;
186
}
187
188
if (!isIChatViewViewContext(existingWidget.viewContext) && target !== ChatViewPaneTarget && existingEditor && this.isSameEditorTarget(existingEditor.group.id, target)) {
189
return;
190
}
191
192
if (existingEditor) {
193
// widget.clear() on an editor leaves behind an empty chat editor
194
await this.editorService.closeEditor({ editor: existingEditor.editor, groupId: existingEditor.group.id }, { preserveFocus: true });
195
} else {
196
await existingWidget.clear();
197
}
198
}
199
}
200
201
private findExistingChatEditorByUri(sessionUri: URI): { editor: ChatEditorInput; group: IEditorGroup } | undefined {
202
for (const group of this.editorGroupsService.groups) {
203
for (const editor of group.editors) {
204
if (editor instanceof ChatEditorInput && isEqual(editor.sessionResource, sessionUri)) {
205
return { editor, group };
206
}
207
}
208
}
209
return undefined;
210
}
211
212
private isSameEditorTarget(currentGroupId: number, target?: PreferredGroup): boolean {
213
return typeof target === 'number' && target === currentGroupId ||
214
target === ACTIVE_GROUP && this.editorGroupsService.activeGroup?.id === currentGroupId ||
215
isEditorGroup(target) && target.id === currentGroupId;
216
}
217
218
private setLastFocusedWidget(widget: IChatWidget | undefined): void {
219
if (widget === this._lastFocusedWidget) {
220
return;
221
}
222
223
this._lastFocusedWidget = widget;
224
}
225
226
register(newWidget: IChatWidget): IDisposable {
227
if (this._widgets.some(widget => widget === newWidget)) {
228
throw new Error('Cannot register the same widget multiple times');
229
}
230
231
this._widgets.push(newWidget);
232
this._onDidAddWidget.fire(newWidget);
233
234
if (!this._lastFocusedWidget) {
235
this.setLastFocusedWidget(newWidget);
236
}
237
238
return combinedDisposable(
239
newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)),
240
newWidget.onDidChangeViewModel(({ previousSessionResource, currentSessionResource }) => {
241
if (!previousSessionResource || (currentSessionResource && isEqual(previousSessionResource, currentSessionResource))) {
242
return;
243
}
244
245
// Timeout to ensure it wasn't just moving somewhere else
246
void timeout(200).then(() => {
247
if (!this.getWidgetBySessionResource(previousSessionResource) && this.chatService.getSession(previousSessionResource)) {
248
this._onDidBackgroundSession.fire(previousSessionResource);
249
}
250
});
251
}),
252
toDisposable(() => {
253
this._widgets.splice(this._widgets.indexOf(newWidget), 1);
254
if (this._lastFocusedWidget === newWidget) {
255
this.setLastFocusedWidget(undefined);
256
}
257
})
258
);
259
}
260
}
261
262