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
5272 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 { 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 (preferred)
112
if (target === ChatViewPaneTarget || typeof target === 'undefined') {
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() && !options?.preserveFocus) {
157
ensureFocusTransfer = Event.toPromise(Event.once(Event.filter(this.layoutService.onDidChangeActiveContainer, isGroupActive)));
158
}
159
160
const pane = await existingEditor.group.openEditor(existingEditor.editor, options);
161
await ensureFocusTransfer;
162
return pane instanceof ChatEditor ? pane.widget : undefined;
163
}
164
165
// Already open in quick chat?
166
if (isEqual(sessionResource, this.quickChatService.sessionResource)) {
167
this.quickChatService.focus();
168
return undefined;
169
}
170
171
return undefined;
172
}
173
174
private async prepareSessionForMove(sessionResource: URI, target: typeof ChatViewPaneTarget | PreferredGroup | undefined): Promise<void> {
175
const existingWidget = this.getWidgetBySessionResource(sessionResource);
176
if (existingWidget) {
177
const existingEditor = isIChatViewViewContext(existingWidget.viewContext) ?
178
undefined :
179
this.findExistingChatEditorByUri(sessionResource);
180
181
if (isIChatViewViewContext(existingWidget.viewContext) && target === ChatViewPaneTarget) {
182
return;
183
}
184
185
if (!isIChatViewViewContext(existingWidget.viewContext) && target !== ChatViewPaneTarget && existingEditor && this.isSameEditorTarget(existingEditor.group.id, target)) {
186
return;
187
}
188
189
if (existingEditor) {
190
// widget.clear() on an editor leaves behind an empty chat editor
191
await this.editorService.closeEditor({ editor: existingEditor.editor, groupId: existingEditor.group.id }, { preserveFocus: true });
192
} else {
193
await existingWidget.clear();
194
}
195
}
196
}
197
198
private findExistingChatEditorByUri(sessionUri: URI): { editor: ChatEditorInput; group: IEditorGroup } | undefined {
199
for (const group of this.editorGroupsService.groups) {
200
for (const editor of group.editors) {
201
if (editor instanceof ChatEditorInput && isEqual(editor.sessionResource, sessionUri)) {
202
return { editor, group };
203
}
204
}
205
}
206
return undefined;
207
}
208
209
private isSameEditorTarget(currentGroupId: number, target?: PreferredGroup): boolean {
210
return typeof target === 'number' && target === currentGroupId ||
211
target === ACTIVE_GROUP && this.editorGroupsService.activeGroup?.id === currentGroupId ||
212
isEditorGroup(target) && target.id === currentGroupId;
213
}
214
215
private setLastFocusedWidget(widget: IChatWidget | undefined): void {
216
if (widget === this._lastFocusedWidget) {
217
return;
218
}
219
220
this._lastFocusedWidget = widget;
221
}
222
223
register(newWidget: IChatWidget): IDisposable {
224
if (this._widgets.some(widget => widget === newWidget)) {
225
throw new Error('Cannot register the same widget multiple times');
226
}
227
228
this._widgets.push(newWidget);
229
this._onDidAddWidget.fire(newWidget);
230
231
if (!this._lastFocusedWidget) {
232
this.setLastFocusedWidget(newWidget);
233
}
234
235
return combinedDisposable(
236
newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)),
237
newWidget.onDidChangeViewModel(({ previousSessionResource, currentSessionResource }) => {
238
if (!previousSessionResource || (currentSessionResource && isEqual(previousSessionResource, currentSessionResource))) {
239
return;
240
}
241
242
// Timeout to ensure it wasn't just moving somewhere else
243
void timeout(200).then(() => {
244
if (!this.getWidgetBySessionResource(previousSessionResource) && this.chatService.getSession(previousSessionResource)) {
245
this._onDidBackgroundSession.fire(previousSessionResource);
246
}
247
});
248
}),
249
toDisposable(() => {
250
this._widgets.splice(this._widgets.indexOf(newWidget), 1);
251
if (this._lastFocusedWidget === newWidget) {
252
this.setLastFocusedWidget(undefined);
253
}
254
})
255
);
256
}
257
}
258
259