Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.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 { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
7
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
8
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
9
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
10
import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js';
11
import { $, append, EventHelper } from '../../../../../base/browser/dom.js';
12
import { AgentSessionSection, IAgentSession, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js';
13
import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js';
14
import { FuzzyScore } from '../../../../../base/common/filters.js';
15
import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
16
import { IChatSessionsService } from '../../common/chatSessionsService.js';
17
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
18
import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js';
19
import { Event } from '../../../../../base/common/event.js';
20
import { Disposable } from '../../../../../base/common/lifecycle.js';
21
import { ITreeContextMenuEvent } from '../../../../../base/browser/ui/tree/tree.js';
22
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
23
import { Separator } from '../../../../../base/common/actions.js';
24
import { RenderIndentGuides, TreeFindMode } from '../../../../../base/browser/ui/tree/abstractTree.js';
25
import { IAgentSessionsService } from './agentSessionsService.js';
26
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
27
import { IListStyles } from '../../../../../base/browser/ui/list/listWidget.js';
28
import { IStyleOverride } from '../../../../../platform/theme/browser/defaultStyles.js';
29
import { IAgentSessionsControl } from './agentSessions.js';
30
import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';
31
import { URI } from '../../../../../base/common/uri.js';
32
import { openSession } from './agentSessionsOpener.js';
33
import { IEditorService } from '../../../../services/editor/common/editorService.js';
34
import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';
35
36
export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOptions {
37
readonly overrideStyles: IStyleOverride<IListStyles>;
38
readonly filter: IAgentSessionsFilter;
39
40
getHoverPosition(): HoverPosition;
41
trackActiveEditorSession(): boolean;
42
}
43
44
type AgentSessionOpenedClassification = {
45
owner: 'bpasero';
46
providerType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider type of the opened agent session.' };
47
comment: 'Event fired when a agent session is opened from the agent sessions control.';
48
};
49
50
type AgentSessionOpenedEvent = {
51
providerType: string;
52
};
53
54
export class AgentSessionsControl extends Disposable implements IAgentSessionsControl {
55
56
private sessionsContainer: HTMLElement | undefined;
57
private sessionsList: WorkbenchCompressibleAsyncDataTree<IAgentSessionsModel, AgentSessionListItem, FuzzyScore> | undefined;
58
59
private visible: boolean = true;
60
61
private focusedAgentSessionArchivedContextKey: IContextKey<boolean>;
62
private focusedAgentSessionReadContextKey: IContextKey<boolean>;
63
private focusedAgentSessionTypeContextKey: IContextKey<string>;
64
65
constructor(
66
private readonly container: HTMLElement,
67
private readonly options: IAgentSessionsControlOptions,
68
@IContextMenuService private readonly contextMenuService: IContextMenuService,
69
@IContextKeyService private readonly contextKeyService: IContextKeyService,
70
@IInstantiationService private readonly instantiationService: IInstantiationService,
71
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
72
@ICommandService private readonly commandService: ICommandService,
73
@IMenuService private readonly menuService: IMenuService,
74
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
75
@ITelemetryService private readonly telemetryService: ITelemetryService,
76
@IEditorService private readonly editorService: IEditorService,
77
) {
78
super();
79
80
this.focusedAgentSessionArchivedContextKey = ChatContextKeys.isArchivedAgentSession.bindTo(this.contextKeyService);
81
this.focusedAgentSessionReadContextKey = ChatContextKeys.isReadAgentSession.bindTo(this.contextKeyService);
82
this.focusedAgentSessionTypeContextKey = ChatContextKeys.agentSessionType.bindTo(this.contextKeyService);
83
84
this.createList(this.container);
85
86
this.registerListeners();
87
}
88
89
private registerListeners(): void {
90
this._register(this.editorService.onDidActiveEditorChange(() => this.revealAndFocusActiveEditorSession()));
91
}
92
93
private revealAndFocusActiveEditorSession(): void {
94
if (
95
!this.options.trackActiveEditorSession() ||
96
!this.visible
97
) {
98
return;
99
}
100
101
const input = this.editorService.activeEditor;
102
const resource = (input instanceof ChatEditorInput) ? input.sessionResource : input?.resource;
103
if (!resource) {
104
return;
105
}
106
107
const matchingSession = this.agentSessionsService.model.getSession(resource);
108
if (matchingSession && this.sessionsList?.hasNode(matchingSession)) {
109
if (this.sessionsList.getRelativeTop(matchingSession) === null) {
110
this.sessionsList.reveal(matchingSession, 0.5); // only reveal when not already visible
111
}
112
113
this.sessionsList.setFocus([matchingSession]);
114
this.sessionsList.setSelection([matchingSession]);
115
}
116
}
117
118
private createList(container: HTMLElement): void {
119
this.sessionsContainer = append(container, $('.agent-sessions-viewer'));
120
121
const sorter = new AgentSessionsSorter(this.options);
122
const list = this.sessionsList = this._register(this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree,
123
'AgentSessionsView',
124
this.sessionsContainer,
125
new AgentSessionsListDelegate(),
126
new AgentSessionsCompressionDelegate(),
127
[
128
this.instantiationService.createInstance(AgentSessionRenderer, this.options),
129
this.instantiationService.createInstance(AgentSessionSectionRenderer),
130
],
131
new AgentSessionsDataSource(this.options.filter, sorter),
132
{
133
accessibilityProvider: new AgentSessionsAccessibilityProvider(),
134
dnd: this.instantiationService.createInstance(AgentSessionsDragAndDrop),
135
identityProvider: new AgentSessionsIdentityProvider(),
136
horizontalScrolling: false,
137
multipleSelectionSupport: false,
138
findWidgetEnabled: true,
139
defaultFindMode: TreeFindMode.Filter,
140
keyboardNavigationLabelProvider: new AgentSessionsKeyboardNavigationLabelProvider(),
141
overrideStyles: this.options.overrideStyles,
142
expandOnlyOnTwistieClick: (element: unknown) => !(isAgentSessionSection(element) && element.section === AgentSessionSection.Archived && this.options.filter.getExcludes().archived),
143
twistieAdditionalCssClass: () => 'force-no-twistie',
144
collapseByDefault: (element: unknown) => isAgentSessionSection(element) && element.section === AgentSessionSection.Archived && this.options.filter.getExcludes().archived,
145
renderIndentGuides: RenderIndentGuides.None,
146
}
147
)) as WorkbenchCompressibleAsyncDataTree<IAgentSessionsModel, AgentSessionListItem, FuzzyScore>;
148
149
ChatContextKeys.agentSessionsViewerFocused.bindTo(list.contextKeyService);
150
151
const model = this.agentSessionsService.model;
152
153
this._register(this.options.filter.onDidChange(async () => {
154
if (this.visible) {
155
this.updateArchivedSectionCollapseState();
156
list.updateChildren();
157
}
158
}));
159
160
this._register(model.onDidChangeSessions(() => {
161
if (this.visible) {
162
list.updateChildren();
163
}
164
}));
165
166
list.setInput(model);
167
168
this._register(list.onDidOpen(e => this.openAgentSession(e)));
169
this._register(list.onContextMenu(e => this.showContextMenu(e)));
170
171
this._register(list.onMouseDblClick(({ element }) => {
172
if (element === null) {
173
this.commandService.executeCommand(ACTION_ID_NEW_CHAT);
174
}
175
}));
176
177
this._register(Event.any(list.onDidChangeFocus, model.onDidChangeSessions)(() => {
178
const focused = list.getFocus().at(0);
179
if (focused && isAgentSession(focused)) {
180
this.focusedAgentSessionArchivedContextKey.set(focused.isArchived());
181
this.focusedAgentSessionReadContextKey.set(focused.isRead());
182
this.focusedAgentSessionTypeContextKey.set(focused.providerType);
183
} else {
184
this.focusedAgentSessionArchivedContextKey.reset();
185
this.focusedAgentSessionReadContextKey.reset();
186
this.focusedAgentSessionTypeContextKey.reset();
187
}
188
}));
189
}
190
191
private async openAgentSession(e: IOpenEvent<AgentSessionListItem | undefined>): Promise<void> {
192
const element = e.element;
193
if (!element || isAgentSessionSection(element)) {
194
return; // Section headers are not openable
195
}
196
197
this.telemetryService.publicLog2<AgentSessionOpenedEvent, AgentSessionOpenedClassification>('agentSessionOpened', {
198
providerType: element.providerType
199
});
200
201
await this.instantiationService.invokeFunction(openSession, element, e);
202
}
203
204
private async showContextMenu({ element, anchor, browserEvent }: ITreeContextMenuEvent<AgentSessionListItem>): Promise<void> {
205
if (!element || isAgentSessionSection(element)) {
206
return; // No context menu for section headers
207
}
208
209
EventHelper.stop(browserEvent, true);
210
211
await this.chatSessionsService.activateChatSessionItemProvider(element.providerType);
212
213
const contextOverlay: Array<[string, boolean | string]> = [];
214
contextOverlay.push([ChatContextKeys.isArchivedAgentSession.key, element.isArchived()]);
215
contextOverlay.push([ChatContextKeys.isReadAgentSession.key, element.isRead()]);
216
contextOverlay.push([ChatContextKeys.agentSessionType.key, element.providerType]);
217
const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay));
218
219
const marshalledSession: IMarshalledAgentSessionContext = { session: element, $mid: MarshalledId.AgentSessionContext };
220
this.contextMenuService.showContextMenu({
221
getActions: () => Separator.join(...menu.getActions({ arg: marshalledSession, shouldForwardArgs: true }).map(([, actions]) => actions)),
222
getAnchor: () => anchor,
223
getActionsContext: () => marshalledSession,
224
});
225
226
menu.dispose();
227
}
228
229
openFind(): void {
230
this.sessionsList?.openFind();
231
}
232
233
private updateArchivedSectionCollapseState(): void {
234
if (!this.sessionsList) {
235
return;
236
}
237
238
const model = this.agentSessionsService.model;
239
for (const child of this.sessionsList.getNode(model).children) {
240
if (!isAgentSessionSection(child.element) || child.element.section !== AgentSessionSection.Archived) {
241
continue;
242
}
243
244
const shouldCollapseArchived = this.options.filter.getExcludes().archived;
245
if (shouldCollapseArchived && !child.collapsed) {
246
this.sessionsList.collapse(child.element);
247
} else if (!shouldCollapseArchived && child.collapsed) {
248
this.sessionsList.expand(child.element);
249
}
250
break;
251
}
252
}
253
254
refresh(): Promise<void> {
255
return this.agentSessionsService.model.resolve(undefined);
256
}
257
258
async update(): Promise<void> {
259
await this.sessionsList?.updateChildren();
260
}
261
262
setVisible(visible: boolean): void {
263
if (this.visible === visible) {
264
return;
265
}
266
267
this.visible = visible;
268
269
if (this.visible) {
270
this.sessionsList?.updateChildren();
271
}
272
}
273
274
layout(height: number, width: number): void {
275
this.sessionsList?.layout(height, width);
276
}
277
278
focus(): void {
279
this.sessionsList?.domFocus();
280
}
281
282
clearFocus(): void {
283
this.sessionsList?.setFocus([]);
284
this.sessionsList?.setSelection([]);
285
}
286
287
scrollToTop(): void {
288
if (this.sessionsList) {
289
this.sessionsList.scrollTop = 0;
290
}
291
}
292
293
getFocus(): IAgentSession[] {
294
const focused = this.sessionsList?.getFocus() ?? [];
295
296
return focused.filter(e => isAgentSession(e));
297
}
298
299
reveal(sessionResource: URI): void {
300
if (!this.sessionsList) {
301
return;
302
}
303
304
const session = this.agentSessionsService.model.getSession(sessionResource);
305
if (!session || !this.sessionsList.hasNode(session)) {
306
return;
307
}
308
309
if (this.sessionsList.getRelativeTop(session) === null) {
310
this.sessionsList.reveal(session, 0.5); // only reveal when not already visible
311
}
312
313
this.sessionsList.setFocus([session]);
314
this.sessionsList.setSelection([session]);
315
}
316
}
317
318