Merge pull request #286642 from microsoft/dev/mjbvz/chat-session-item-controller

Explore a controller based chat session item API
This commit is contained in:
Matt Bierner
2026-01-13 15:35:48 -08:00
committed by GitHub
7 changed files with 365 additions and 28 deletions

View File

@@ -899,6 +899,7 @@ export default tseslint.config(
],
'verbs': [
'accept',
'archive',
'change',
'close',
'collapse',

View File

@@ -69,7 +69,7 @@ const _allApiProposals = {
},
chatSessionsProvider: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts',
version: 3
version: 4
},
chatStatusItem: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts',

View File

@@ -382,7 +382,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
));
}
$onDidChangeChatSessionItems(handle: number): void {
this._itemProvidersRegistrations.get(handle)?.onDidChangeItems.fire();
}
@@ -491,6 +490,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
resource: uri,
iconPath: session.iconPath,
tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined,
archived: session.archived,
} satisfies IChatSessionItem;
}));
} catch (error) {

View File

@@ -1530,6 +1530,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'chatSessionsProvider');
return extHostChatSessions.registerChatSessionItemProvider(extension, chatSessionType, provider);
},
createChatSessionItemController: (chatSessionType: string, refreshHandler: () => Thenable<void>) => {
checkProposedApiEnabled(extension, 'chatSessionsProvider');
return extHostChatSessions.createChatSessionItemController(extension, chatSessionType, refreshHandler);
},
registerChatSessionContentProvider(scheme: string, provider: vscode.ChatSessionContentProvider, chatParticipant: vscode.ChatParticipant, capabilities?: vscode.ChatSessionCapabilities) {
checkProposedApiEnabled(extension, 'chatSessionsProvider');
return extHostChatSessions.registerChatSessionContentProvider(extension, scheme, chatParticipant, provider, capabilities);

View File

@@ -2,12 +2,14 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable local/code-no-native-private */
import type * as vscode from 'vscode';
import { coalesce } from '../../../base/common/arrays.js';
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
import { CancellationError } from '../../../base/common/errors.js';
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
import { Emitter } from '../../../base/common/event.js';
import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../base/common/map.js';
import { MarshalledId } from '../../../base/common/marshallingIds.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
@@ -29,6 +31,175 @@ import { basename } from '../../../base/common/resources.js';
import { Diagnostic } from './extHostTypeConverters.js';
import { SymbolKind, SymbolKinds } from '../../../editor/common/languages.js';
// #region Chat Session Item Controller
class ChatSessionItemImpl implements vscode.ChatSessionItem {
#label: string;
#iconPath?: vscode.IconPath;
#description?: string | vscode.MarkdownString;
#badge?: string | vscode.MarkdownString;
#status?: vscode.ChatSessionStatus;
#archived?: boolean;
#tooltip?: string | vscode.MarkdownString;
#timing?: { startTime: number; endTime?: number };
#changes?: readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number };
#onChanged: () => void;
readonly resource: vscode.Uri;
constructor(resource: vscode.Uri, label: string, onChanged: () => void) {
this.resource = resource;
this.#label = label;
this.#onChanged = onChanged;
}
get label(): string {
return this.#label;
}
set label(value: string) {
if (this.#label !== value) {
this.#label = value;
this.#onChanged();
}
}
get iconPath(): vscode.IconPath | undefined {
return this.#iconPath;
}
set iconPath(value: vscode.IconPath | undefined) {
if (this.#iconPath !== value) {
this.#iconPath = value;
this.#onChanged();
}
}
get description(): string | vscode.MarkdownString | undefined {
return this.#description;
}
set description(value: string | vscode.MarkdownString | undefined) {
if (this.#description !== value) {
this.#description = value;
this.#onChanged();
}
}
get badge(): string | vscode.MarkdownString | undefined {
return this.#badge;
}
set badge(value: string | vscode.MarkdownString | undefined) {
if (this.#badge !== value) {
this.#badge = value;
this.#onChanged();
}
}
get status(): vscode.ChatSessionStatus | undefined {
return this.#status;
}
set status(value: vscode.ChatSessionStatus | undefined) {
if (this.#status !== value) {
this.#status = value;
this.#onChanged();
}
}
get archived(): boolean | undefined {
return this.#archived;
}
set archived(value: boolean | undefined) {
if (this.#archived !== value) {
this.#archived = value;
this.#onChanged();
}
}
get tooltip(): string | vscode.MarkdownString | undefined {
return this.#tooltip;
}
set tooltip(value: string | vscode.MarkdownString | undefined) {
if (this.#tooltip !== value) {
this.#tooltip = value;
this.#onChanged();
}
}
get timing(): { startTime: number; endTime?: number } | undefined {
return this.#timing;
}
set timing(value: { startTime: number; endTime?: number } | undefined) {
if (this.#timing !== value) {
this.#timing = value;
this.#onChanged();
}
}
get changes(): readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number } | undefined {
return this.#changes;
}
set changes(value: readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number } | undefined) {
if (this.#changes !== value) {
this.#changes = value;
this.#onChanged();
}
}
}
class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection {
readonly #items = new ResourceMap<vscode.ChatSessionItem>();
#onItemsChanged: () => void;
constructor(onItemsChanged: () => void) {
this.#onItemsChanged = onItemsChanged;
}
get size(): number {
return this.#items.size;
}
replace(items: readonly vscode.ChatSessionItem[]): void {
this.#items.clear();
for (const item of items) {
this.#items.set(item.resource, item);
}
this.#onItemsChanged();
}
forEach(callback: (item: vscode.ChatSessionItem, collection: vscode.ChatSessionItemCollection) => unknown, thisArg?: any): void {
for (const [_, item] of this.#items) {
callback.call(thisArg, item, this);
}
}
add(item: vscode.ChatSessionItem): void {
this.#items.set(item.resource, item);
this.#onItemsChanged();
}
delete(resource: vscode.Uri): void {
this.#items.delete(resource);
this.#onItemsChanged();
}
get(resource: vscode.Uri): vscode.ChatSessionItem | undefined {
return this.#items.get(resource);
}
[Symbol.iterator](): Iterator<readonly [id: URI, chatSessionItem: vscode.ChatSessionItem]> {
return this.#items.entries();
}
}
// #endregion
class ExtHostChatSession {
private _stream: ChatAgentResponseStream;
@@ -62,13 +233,20 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
readonly extension: IExtensionDescription;
readonly disposable: DisposableStore;
}>();
private readonly _chatSessionItemControllers = new Map<number, {
readonly sessionType: string;
readonly controller: vscode.ChatSessionItemController;
readonly extension: IExtensionDescription;
readonly disposable: DisposableStore;
}>();
private _nextChatSessionItemProviderHandle = 0;
private readonly _chatSessionContentProviders = new Map<number, {
readonly provider: vscode.ChatSessionContentProvider;
readonly extension: IExtensionDescription;
readonly capabilities?: vscode.ChatSessionCapabilities;
readonly disposable: DisposableStore;
}>();
private _nextChatSessionItemProviderHandle = 0;
private _nextChatSessionItemControllerHandle = 0;
private _nextChatSessionContentProviderHandle = 0;
/**
@@ -140,6 +318,52 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
};
}
createChatSessionItemController(extension: IExtensionDescription, id: string, refreshHandler: () => Thenable<void>): vscode.ChatSessionItemController {
const controllerHandle = this._nextChatSessionItemControllerHandle++;
const disposables = new DisposableStore();
// TODO: Currently not hooked up
const onDidArchiveChatSessionItem = disposables.add(new Emitter<vscode.ChatSessionItem>());
const collection = new ChatSessionItemCollectionImpl(() => {
this._proxy.$onDidChangeChatSessionItems(controllerHandle);
});
let isDisposed = false;
const controller: vscode.ChatSessionItemController = {
id,
refreshHandler,
items: collection,
onDidArchiveChatSessionItem: onDidArchiveChatSessionItem.event,
createChatSessionItem: (resource: vscode.Uri, label: string) => {
if (isDisposed) {
throw new Error('ChatSessionItemController has been disposed');
}
return new ChatSessionItemImpl(resource, label, () => {
// TODO: Optimize to only update the specific item
this._proxy.$onDidChangeChatSessionItems(controllerHandle);
});
},
dispose: () => {
isDisposed = true;
disposables.dispose();
},
};
this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, sessionType: id });
this._proxy.$registerChatSessionItemProvider(controllerHandle, id);
disposables.add(toDisposable(() => {
this._chatSessionItemControllers.delete(controllerHandle);
this._proxy.$unregisterChatSessionItemProvider(controllerHandle);
}));
return controller;
}
registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionScheme: string, chatParticipant: vscode.ChatParticipant, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable {
const handle = this._nextChatSessionContentProviderHandle++;
const disposables = new DisposableStore();
@@ -184,13 +408,14 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
}
}
private convertChatSessionItem(sessionType: string, sessionContent: vscode.ChatSessionItem): IChatSessionItem {
private convertChatSessionItem(sessionContent: vscode.ChatSessionItem): IChatSessionItem {
return {
resource: sessionContent.resource,
label: sessionContent.label,
description: sessionContent.description ? typeConvert.MarkdownString.from(sessionContent.description) : undefined,
badge: sessionContent.badge ? typeConvert.MarkdownString.from(sessionContent.badge) : undefined,
status: this.convertChatSessionStatus(sessionContent.status),
archived: sessionContent.archived,
tooltip: typeConvert.MarkdownString.fromStrict(sessionContent.tooltip),
timing: {
startTime: sessionContent.timing?.startTime ?? 0,
@@ -207,21 +432,35 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
}
async $provideChatSessionItems(handle: number, token: vscode.CancellationToken): Promise<IChatSessionItem[]> {
const entry = this._chatSessionItemProviders.get(handle);
if (!entry) {
this._logService.error(`No provider registered for handle ${handle}`);
return [];
}
let items: vscode.ChatSessionItem[];
const sessions = await entry.provider.provideChatSessionItems(token);
if (!sessions) {
return [];
const controller = this._chatSessionItemControllers.get(handle);
if (controller) {
// Call the refresh handler to populate items
await controller.controller.refreshHandler();
if (token.isCancellationRequested) {
return [];
}
items = Array.from(controller.controller.items, x => x[1]);
} else {
const itemProvider = this._chatSessionItemProviders.get(handle);
if (!itemProvider) {
this._logService.error(`No provider registered for handle ${handle}`);
return [];
}
items = await itemProvider.provider.provideChatSessionItems(token) ?? [];
if (token.isCancellationRequested) {
return [];
}
}
const response: IChatSessionItem[] = [];
for (const sessionContent of sessions) {
for (const sessionContent of items) {
this._sessionItems.set(sessionContent.resource, sessionContent);
response.push(this.convertChatSessionItem(entry.sessionType, sessionContent));
response.push(this.convertChatSessionItem(sessionContent));
}
return response;
}

View File

@@ -73,6 +73,7 @@ export interface IChatSessionsExtensionPoint {
readonly commands?: IChatSessionCommandContribution[];
readonly canDelegate?: boolean;
}
export interface IChatSessionItem {
resource: URI;
label: string;

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// version: 3
// version: 4
declare module 'vscode' {
/**
@@ -26,6 +26,25 @@ declare module 'vscode' {
InProgress = 2
}
export namespace chat {
/**
* Registers a new {@link ChatSessionItemProvider chat session item provider}.
*
* To use this, also make sure to also add `chatSessions` contribution in the `package.json`.
*
* @param chatSessionType The type of chat session the provider is for.
* @param provider The provider to register.
*
* @returns A disposable that unregisters the provider when disposed.
*/
export function registerChatSessionItemProvider(chatSessionType: string, provider: ChatSessionItemProvider): Disposable;
/**
* Creates a new {@link ChatSessionItemController chat session item controller} with the given unique identifier.
*/
export function createChatSessionItemController(id: string, refreshHandler: () => Thenable<void>): ChatSessionItemController;
}
/**
* Provides a list of information about chat sessions.
*/
@@ -52,6 +71,86 @@ declare module 'vscode' {
// #endregion
}
/**
* Provides a list of information about chat sessions.
*/
export interface ChatSessionItemController {
readonly id: string;
/**
* Unregisters the controller, disposing of its associated chat session items.
*/
dispose(): void;
/**
* Managed collection of chat session items
*/
readonly items: ChatSessionItemCollection;
/**
* Creates a new managed chat session item that be added to the collection.
*/
createChatSessionItem(resource: Uri, label: string): ChatSessionItem;
/**
* Handler called to refresh the collection of chat session items.
*
* This is also called on first load to get the initial set of items.
*/
refreshHandler: () => Thenable<void>;
/**
* Fired when an item is archived by the editor
*
* TODO: expose archive state on the item too?
*/
readonly onDidArchiveChatSessionItem: Event<ChatSessionItem>;
}
/**
* A collection of chat session items. It provides operations for managing and iterating over the items.
*/
export interface ChatSessionItemCollection extends Iterable<readonly [id: Uri, chatSessionItem: ChatSessionItem]> {
/**
* Gets the number of items in the collection.
*/
readonly size: number;
/**
* Replaces the items stored by the collection.
* @param items Items to store.
*/
replace(items: readonly ChatSessionItem[]): void;
/**
* Iterate over each entry in this collection.
*
* @param callback Function to execute for each entry.
* @param thisArg The `this` context used when invoking the handler function.
*/
forEach(callback: (item: ChatSessionItem, collection: ChatSessionItemCollection) => unknown, thisArg?: any): void;
/**
* Adds the chat session item to the collection. If an item with the same resource URI already
* exists, it'll be replaced.
* @param item Item to add.
*/
add(item: ChatSessionItem): void;
/**
* Removes a single chat session item from the collection.
* @param resource Item resource to delete.
*/
delete(resource: Uri): void;
/**
* Efficiently gets a chat session item by resource, if it exists, in the collection.
* @param resource Item resource to get.
* @returns The found item or undefined if it does not exist.
*/
get(resource: Uri): ChatSessionItem | undefined;
}
export interface ChatSessionItem {
/**
* The resource associated with the chat session.
@@ -90,6 +189,11 @@ declare module 'vscode' {
*/
tooltip?: string | MarkdownString;
/**
* Whether the chat session has been archived.
*/
archived?: boolean;
/**
* The times at which session started and ended
*/
@@ -268,18 +372,6 @@ declare module 'vscode' {
}
export namespace chat {
/**
* Registers a new {@link ChatSessionItemProvider chat session item provider}.
*
* To use this, also make sure to also add `chatSessions` contribution in the `package.json`.
*
* @param chatSessionType The type of chat session the provider is for.
* @param provider The provider to register.
*
* @returns A disposable that unregisters the provider when disposed.
*/
export function registerChatSessionItemProvider(chatSessionType: string, provider: ChatSessionItemProvider): Disposable;
/**
* Registers a new {@link ChatSessionContentProvider chat session content provider}.
*