Skip to content

Commit b39ecc3

Browse files
authored
Merge pull request #286642 from microsoft/dev/mjbvz/chat-session-item-controller
Explore a controller based chat session item API
2 parents 45aced5 + 49ee0d0 commit b39ecc3

File tree

7 files changed

+365
-28
lines changed

7 files changed

+365
-28
lines changed

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,7 @@ export default tseslint.config(
899899
],
900900
'verbs': [
901901
'accept',
902+
'archive',
902903
'change',
903904
'close',
904905
'collapse',

src/vs/platform/extensions/common/extensionsApiProposals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const _allApiProposals = {
6969
},
7070
chatSessionsProvider: {
7171
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts',
72-
version: 3
72+
version: 4
7373
},
7474
chatStatusItem: {
7575
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts',

src/vs/workbench/api/browser/mainThreadChatSessions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
382382
));
383383
}
384384

385-
386385
$onDidChangeChatSessionItems(handle: number): void {
387386
this._itemProvidersRegistrations.get(handle)?.onDidChangeItems.fire();
388387
}
@@ -491,6 +490,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
491490
resource: uri,
492491
iconPath: session.iconPath,
493492
tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined,
493+
archived: session.archived,
494494
} satisfies IChatSessionItem;
495495
}));
496496
} catch (error) {

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,6 +1530,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
15301530
checkProposedApiEnabled(extension, 'chatSessionsProvider');
15311531
return extHostChatSessions.registerChatSessionItemProvider(extension, chatSessionType, provider);
15321532
},
1533+
createChatSessionItemController: (chatSessionType: string, refreshHandler: () => Thenable<void>) => {
1534+
checkProposedApiEnabled(extension, 'chatSessionsProvider');
1535+
return extHostChatSessions.createChatSessionItemController(extension, chatSessionType, refreshHandler);
1536+
},
15331537
registerChatSessionContentProvider(scheme: string, provider: vscode.ChatSessionContentProvider, chatParticipant: vscode.ChatParticipant, capabilities?: vscode.ChatSessionCapabilities) {
15341538
checkProposedApiEnabled(extension, 'chatSessionsProvider');
15351539
return extHostChatSessions.registerChatSessionContentProvider(extension, scheme, chatParticipant, provider, capabilities);

src/vs/workbench/api/common/extHostChatSessions.ts

Lines changed: 252 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
5+
/* eslint-disable local/code-no-native-private */
56

67
import type * as vscode from 'vscode';
78
import { coalesce } from '../../../base/common/arrays.js';
89
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
910
import { CancellationError } from '../../../base/common/errors.js';
10-
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
11+
import { Emitter } from '../../../base/common/event.js';
12+
import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
1113
import { ResourceMap } from '../../../base/common/map.js';
1214
import { MarshalledId } from '../../../base/common/marshallingIds.js';
1315
import { URI, UriComponents } from '../../../base/common/uri.js';
@@ -29,6 +31,175 @@ import { basename } from '../../../base/common/resources.js';
2931
import { Diagnostic } from './extHostTypeConverters.js';
3032
import { SymbolKind, SymbolKinds } from '../../../editor/common/languages.js';
3133

34+
// #region Chat Session Item Controller
35+
36+
class ChatSessionItemImpl implements vscode.ChatSessionItem {
37+
#label: string;
38+
#iconPath?: vscode.IconPath;
39+
#description?: string | vscode.MarkdownString;
40+
#badge?: string | vscode.MarkdownString;
41+
#status?: vscode.ChatSessionStatus;
42+
#archived?: boolean;
43+
#tooltip?: string | vscode.MarkdownString;
44+
#timing?: { startTime: number; endTime?: number };
45+
#changes?: readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number };
46+
#onChanged: () => void;
47+
48+
readonly resource: vscode.Uri;
49+
50+
constructor(resource: vscode.Uri, label: string, onChanged: () => void) {
51+
this.resource = resource;
52+
this.#label = label;
53+
this.#onChanged = onChanged;
54+
}
55+
56+
get label(): string {
57+
return this.#label;
58+
}
59+
60+
set label(value: string) {
61+
if (this.#label !== value) {
62+
this.#label = value;
63+
this.#onChanged();
64+
}
65+
}
66+
67+
get iconPath(): vscode.IconPath | undefined {
68+
return this.#iconPath;
69+
}
70+
71+
set iconPath(value: vscode.IconPath | undefined) {
72+
if (this.#iconPath !== value) {
73+
this.#iconPath = value;
74+
this.#onChanged();
75+
}
76+
}
77+
78+
get description(): string | vscode.MarkdownString | undefined {
79+
return this.#description;
80+
}
81+
82+
set description(value: string | vscode.MarkdownString | undefined) {
83+
if (this.#description !== value) {
84+
this.#description = value;
85+
this.#onChanged();
86+
}
87+
}
88+
89+
get badge(): string | vscode.MarkdownString | undefined {
90+
return this.#badge;
91+
}
92+
93+
set badge(value: string | vscode.MarkdownString | undefined) {
94+
if (this.#badge !== value) {
95+
this.#badge = value;
96+
this.#onChanged();
97+
}
98+
}
99+
100+
get status(): vscode.ChatSessionStatus | undefined {
101+
return this.#status;
102+
}
103+
104+
set status(value: vscode.ChatSessionStatus | undefined) {
105+
if (this.#status !== value) {
106+
this.#status = value;
107+
this.#onChanged();
108+
}
109+
}
110+
111+
get archived(): boolean | undefined {
112+
return this.#archived;
113+
}
114+
115+
set archived(value: boolean | undefined) {
116+
if (this.#archived !== value) {
117+
this.#archived = value;
118+
this.#onChanged();
119+
}
120+
}
121+
122+
get tooltip(): string | vscode.MarkdownString | undefined {
123+
return this.#tooltip;
124+
}
125+
126+
set tooltip(value: string | vscode.MarkdownString | undefined) {
127+
if (this.#tooltip !== value) {
128+
this.#tooltip = value;
129+
this.#onChanged();
130+
}
131+
}
132+
133+
get timing(): { startTime: number; endTime?: number } | undefined {
134+
return this.#timing;
135+
}
136+
137+
set timing(value: { startTime: number; endTime?: number } | undefined) {
138+
if (this.#timing !== value) {
139+
this.#timing = value;
140+
this.#onChanged();
141+
}
142+
}
143+
144+
get changes(): readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number } | undefined {
145+
return this.#changes;
146+
}
147+
148+
set changes(value: readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number } | undefined) {
149+
if (this.#changes !== value) {
150+
this.#changes = value;
151+
this.#onChanged();
152+
}
153+
}
154+
}
155+
156+
class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection {
157+
readonly #items = new ResourceMap<vscode.ChatSessionItem>();
158+
#onItemsChanged: () => void;
159+
160+
constructor(onItemsChanged: () => void) {
161+
this.#onItemsChanged = onItemsChanged;
162+
}
163+
164+
get size(): number {
165+
return this.#items.size;
166+
}
167+
168+
replace(items: readonly vscode.ChatSessionItem[]): void {
169+
this.#items.clear();
170+
for (const item of items) {
171+
this.#items.set(item.resource, item);
172+
}
173+
this.#onItemsChanged();
174+
}
175+
176+
forEach(callback: (item: vscode.ChatSessionItem, collection: vscode.ChatSessionItemCollection) => unknown, thisArg?: any): void {
177+
for (const [_, item] of this.#items) {
178+
callback.call(thisArg, item, this);
179+
}
180+
}
181+
182+
add(item: vscode.ChatSessionItem): void {
183+
this.#items.set(item.resource, item);
184+
this.#onItemsChanged();
185+
}
186+
187+
delete(resource: vscode.Uri): void {
188+
this.#items.delete(resource);
189+
this.#onItemsChanged();
190+
}
191+
192+
get(resource: vscode.Uri): vscode.ChatSessionItem | undefined {
193+
return this.#items.get(resource);
194+
}
195+
196+
[Symbol.iterator](): Iterator<readonly [id: URI, chatSessionItem: vscode.ChatSessionItem]> {
197+
return this.#items.entries();
198+
}
199+
}
200+
201+
// #endregion
202+
32203
class ExtHostChatSession {
33204
private _stream: ChatAgentResponseStream;
34205

@@ -62,13 +233,20 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
62233
readonly extension: IExtensionDescription;
63234
readonly disposable: DisposableStore;
64235
}>();
236+
private readonly _chatSessionItemControllers = new Map<number, {
237+
readonly sessionType: string;
238+
readonly controller: vscode.ChatSessionItemController;
239+
readonly extension: IExtensionDescription;
240+
readonly disposable: DisposableStore;
241+
}>();
242+
private _nextChatSessionItemProviderHandle = 0;
65243
private readonly _chatSessionContentProviders = new Map<number, {
66244
readonly provider: vscode.ChatSessionContentProvider;
67245
readonly extension: IExtensionDescription;
68246
readonly capabilities?: vscode.ChatSessionCapabilities;
69247
readonly disposable: DisposableStore;
70248
}>();
71-
private _nextChatSessionItemProviderHandle = 0;
249+
private _nextChatSessionItemControllerHandle = 0;
72250
private _nextChatSessionContentProviderHandle = 0;
73251

74252
/**
@@ -140,6 +318,52 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
140318
};
141319
}
142320

321+
322+
createChatSessionItemController(extension: IExtensionDescription, id: string, refreshHandler: () => Thenable<void>): vscode.ChatSessionItemController {
323+
const controllerHandle = this._nextChatSessionItemControllerHandle++;
324+
const disposables = new DisposableStore();
325+
326+
// TODO: Currently not hooked up
327+
const onDidArchiveChatSessionItem = disposables.add(new Emitter<vscode.ChatSessionItem>());
328+
329+
const collection = new ChatSessionItemCollectionImpl(() => {
330+
this._proxy.$onDidChangeChatSessionItems(controllerHandle);
331+
});
332+
333+
let isDisposed = false;
334+
335+
const controller: vscode.ChatSessionItemController = {
336+
id,
337+
refreshHandler,
338+
items: collection,
339+
onDidArchiveChatSessionItem: onDidArchiveChatSessionItem.event,
340+
createChatSessionItem: (resource: vscode.Uri, label: string) => {
341+
if (isDisposed) {
342+
throw new Error('ChatSessionItemController has been disposed');
343+
}
344+
345+
return new ChatSessionItemImpl(resource, label, () => {
346+
// TODO: Optimize to only update the specific item
347+
this._proxy.$onDidChangeChatSessionItems(controllerHandle);
348+
});
349+
},
350+
dispose: () => {
351+
isDisposed = true;
352+
disposables.dispose();
353+
},
354+
};
355+
356+
this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, sessionType: id });
357+
this._proxy.$registerChatSessionItemProvider(controllerHandle, id);
358+
359+
disposables.add(toDisposable(() => {
360+
this._chatSessionItemControllers.delete(controllerHandle);
361+
this._proxy.$unregisterChatSessionItemProvider(controllerHandle);
362+
}));
363+
364+
return controller;
365+
}
366+
143367
registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionScheme: string, chatParticipant: vscode.ChatParticipant, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable {
144368
const handle = this._nextChatSessionContentProviderHandle++;
145369
const disposables = new DisposableStore();
@@ -184,13 +408,14 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
184408
}
185409
}
186410

187-
private convertChatSessionItem(sessionType: string, sessionContent: vscode.ChatSessionItem): IChatSessionItem {
411+
private convertChatSessionItem(sessionContent: vscode.ChatSessionItem): IChatSessionItem {
188412
return {
189413
resource: sessionContent.resource,
190414
label: sessionContent.label,
191415
description: sessionContent.description ? typeConvert.MarkdownString.from(sessionContent.description) : undefined,
192416
badge: sessionContent.badge ? typeConvert.MarkdownString.from(sessionContent.badge) : undefined,
193417
status: this.convertChatSessionStatus(sessionContent.status),
418+
archived: sessionContent.archived,
194419
tooltip: typeConvert.MarkdownString.fromStrict(sessionContent.tooltip),
195420
timing: {
196421
startTime: sessionContent.timing?.startTime ?? 0,
@@ -207,21 +432,35 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
207432
}
208433

209434
async $provideChatSessionItems(handle: number, token: vscode.CancellationToken): Promise<IChatSessionItem[]> {
210-
const entry = this._chatSessionItemProviders.get(handle);
211-
if (!entry) {
212-
this._logService.error(`No provider registered for handle ${handle}`);
213-
return [];
214-
}
435+
let items: vscode.ChatSessionItem[];
436+
437+
const controller = this._chatSessionItemControllers.get(handle);
438+
if (controller) {
439+
// Call the refresh handler to populate items
440+
await controller.controller.refreshHandler();
441+
if (token.isCancellationRequested) {
442+
return [];
443+
}
215444

216-
const sessions = await entry.provider.provideChatSessionItems(token);
217-
if (!sessions) {
218-
return [];
445+
items = Array.from(controller.controller.items, x => x[1]);
446+
} else {
447+
448+
const itemProvider = this._chatSessionItemProviders.get(handle);
449+
if (!itemProvider) {
450+
this._logService.error(`No provider registered for handle ${handle}`);
451+
return [];
452+
}
453+
454+
items = await itemProvider.provider.provideChatSessionItems(token) ?? [];
455+
if (token.isCancellationRequested) {
456+
return [];
457+
}
219458
}
220459

221460
const response: IChatSessionItem[] = [];
222-
for (const sessionContent of sessions) {
461+
for (const sessionContent of items) {
223462
this._sessionItems.set(sessionContent.resource, sessionContent);
224-
response.push(this.convertChatSessionItem(entry.sessionType, sessionContent));
463+
response.push(this.convertChatSessionItem(sessionContent));
225464
}
226465
return response;
227466
}

src/vs/workbench/contrib/chat/common/chatSessionsService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface IChatSessionsExtensionPoint {
7373
readonly commands?: IChatSessionCommandContribution[];
7474
readonly canDelegate?: boolean;
7575
}
76+
7677
export interface IChatSessionItem {
7778
resource: URI;
7879
label: string;

0 commit comments

Comments
 (0)