From 3f197a65831ce81ce2526f90e845caef0bb57f9e Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:18:02 -0800 Subject: [PATCH 1/7] Initial sketch of a controller based chat session item API For #276243 Explores moving the chat session item api to use a controller instead of a provider --- eslint.config.js | 1 + .../vscode.proposed.chatSessionsProvider.d.ts | 106 +++++++++++++----- 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 52eb95c5ff0..0cf09d0b24f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -899,6 +899,7 @@ export default tseslint.config( ], 'verbs': [ 'accept', + 'archive', 'change', 'close', 'collapse', diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 2ec68c1731e..12c664326d6 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -26,30 +26,96 @@ declare module 'vscode' { InProgress = 2 } + export namespace chat { + /** + * Creates a new {@link ChatSessionItemController chat session item controller} with the given unique identifier. + */ + export function createChatSessionItemController(id: string): ChatSessionItemController; + } + /** * Provides a list of information about chat sessions. */ - export interface ChatSessionItemProvider { - /** - * Event that the provider can fire to signal that chat sessions have changed. - */ - readonly onDidChangeChatSessionItems: Event; + export class ChatSessionItemController { + readonly id: string; /** - * Provides a list of chat sessions. + * Unregisters the controller, disposing of its associated chat session items. */ - // TODO: Do we need a flag to try auth if needed? - provideChatSessionItems(token: CancellationToken): ProviderResult; - - // #region Unstable parts of API + dispose(): void; /** - * Event that the provider can fire to signal that the current (original) chat session should be replaced with a new (modified) chat session. - * The UI can use this information to gracefully migrate the user to the new session. + * Managed collection of chat session items */ - readonly onDidCommitChatSessionItem: Event<{ original: ChatSessionItem /** untitled */; modified: ChatSessionItem /** newly created */ }>; + readonly items: ChatSessionItemCollection; - // #endregion + /** + * 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; + + /** + * Fired when an item is archived by the editor + * + * TODO: expose archive state on the item too? Or should this + */ + readonly onDidArchiveChatSessionItem: Event; + + /** + * Fired when an item is disposed by the editor + */ + readonly onDidDisposeChatSessionItem: Event; + } + + /** + * A collection of chat session items. It provides operations for managing and iterating over the items. + */ + export interface ChatSessionItemCollection extends Iterable { + /** + * 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 { @@ -268,18 +334,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}. * From e45ac30b90b0de04d8cafda7ab6ad96922eaa5bd Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:46:29 -0800 Subject: [PATCH 2/7] Hookup basic proxy impl --- .../workbench/api/common/extHost.api.impl.ts | 5 +- .../api/common/extHostChatSessions.ts | 224 +++++++++++++++--- src/vs/workbench/api/common/extHostTypes.ts | 11 + .../vscode.proposed.chatSessionsProvider.d.ts | 6 +- 4 files changed, 211 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ccfd8731f6e..99e61873974 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1525,9 +1525,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return _asExtensionEvent(extHostChatAgents2.onDidDisposeChatSession)(listeners, thisArgs, disposables); }, - registerChatSessionItemProvider: (chatSessionType: string, provider: vscode.ChatSessionItemProvider) => { + createChatSessionItemController: (id: string, refreshHandler: () => Thenable) => { checkProposedApiEnabled(extension, 'chatSessionsProvider'); - return extHostChatSessions.registerChatSessionItemProvider(extension, chatSessionType, provider); + return extHostChatSessions.createChatSessionItemController(extension, id, refreshHandler); }, registerChatSessionContentProvider(scheme: string, provider: vscode.ChatSessionContentProvider, chatParticipant: vscode.ChatParticipant, capabilities?: vscode.ChatSessionCapabilities) { checkProposedApiEnabled(extension, 'chatSessionsProvider'); @@ -1865,6 +1865,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InteractiveSessionVoteDirection: extHostTypes.InteractiveSessionVoteDirection, ChatCopyKind: extHostTypes.ChatCopyKind, ChatSessionChangedFile: extHostTypes.ChatSessionChangedFile, + ChatSessionItemController: extHostTypes.ChatSessionItemController, ChatEditingSessionActionOutcome: extHostTypes.ChatEditingSessionActionOutcome, InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, DebugStackFrame: extHostTypes.DebugStackFrame, diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 260627104c1..2c926f086e9 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -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,148 @@ 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 { + readonly resource: vscode.Uri; + #label: string; + #iconPath?: vscode.IconPath; + #description?: string | vscode.MarkdownString; + #badge?: string | vscode.MarkdownString; + #status?: vscode.ChatSessionStatus; + #tooltip?: string | vscode.MarkdownString; + #timing?: { startTime: number; endTime?: number }; + #changes?: readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number }; + #onChanged: () => void; + + 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) { + this.#label = value; + this.#onChanged(); + } + + get iconPath(): vscode.IconPath | undefined { + return this.#iconPath; + } + + set iconPath(value: vscode.IconPath | undefined) { + this.#iconPath = value; + this.#onChanged(); + } + + get description(): string | vscode.MarkdownString | undefined { + return this.#description; + } + + set description(value: string | vscode.MarkdownString | undefined) { + this.#description = value; + this.#onChanged(); + } + + get badge(): string | vscode.MarkdownString | undefined { + return this.#badge; + } + + set badge(value: string | vscode.MarkdownString | undefined) { + this.#badge = value; + this.#onChanged(); + } + + get status(): vscode.ChatSessionStatus | undefined { + return this.#status; + } + + set status(value: vscode.ChatSessionStatus | undefined) { + this.#status = value; + this.#onChanged(); + } + + get tooltip(): string | vscode.MarkdownString | undefined { + return this.#tooltip; + } + + set tooltip(value: string | vscode.MarkdownString | undefined) { + this.#tooltip = value; + this.#onChanged(); + } + + get timing(): { startTime: number; endTime?: number } | undefined { + return this.#timing; + } + + set timing(value: { startTime: number; endTime?: number } | undefined) { + 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) { + this.#changes = value; + this.#onChanged(); + } +} + +class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection { + readonly #items = new ResourceMap(); + #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](): Generator { + for (const [uri, item] of this.#items) { + yield [uri, item] as const; + } + } +} + +// #endregion + class ExtHostChatSession { private _stream: ChatAgentResponseStream; @@ -56,9 +200,9 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio private static _sessionHandlePool = 0; private readonly _proxy: Proxied; - private readonly _chatSessionItemProviders = new Map(); @@ -68,7 +212,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio readonly capabilities?: vscode.ChatSessionCapabilities; readonly disposable: DisposableStore; }>(); - private _nextChatSessionItemProviderHandle = 0; + private _nextChatSessionItemControllerHandle = 0; private _nextChatSessionContentProviderHandle = 0; /** @@ -111,30 +255,50 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }); } - registerChatSessionItemProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionItemProvider): vscode.Disposable { - const handle = this._nextChatSessionItemProviderHandle++; + createChatSessionItemController(extension: IExtensionDescription, id: string, refreshHandler: () => Thenable): vscode.ChatSessionItemController { + const handle = this._nextChatSessionItemControllerHandle++; const disposables = new DisposableStore(); - this._chatSessionItemProviders.set(handle, { provider, extension, disposable: disposables, sessionType: chatSessionType }); - this._proxy.$registerChatSessionItemProvider(handle, chatSessionType); - if (provider.onDidChangeChatSessionItems) { - disposables.add(provider.onDidChangeChatSessionItems(() => { - this._proxy.$onDidChangeChatSessionItems(handle); - })); - } - if (provider.onDidCommitChatSessionItem) { - disposables.add(provider.onDidCommitChatSessionItem((e) => { - const { original, modified } = e; - this._proxy.$onDidCommitChatSessionItem(handle, original.resource, modified.resource); - })); - } - return { + // TODO: Currently not hooked up + const onDidArchiveChatSessionItem = disposables.add(new Emitter()); + const onDidDisposeChatSessionItem = disposables.add(new Emitter()); + + const collection = new ChatSessionItemCollectionImpl(() => { + this._proxy.$onDidChangeChatSessionItems(handle); + }); + + let isDisposed = false; + + const controller: vscode.ChatSessionItemController = { + id, + refreshHandler, + items: collection, + onDidArchiveChatSessionItem: onDidArchiveChatSessionItem.event, + onDidDisposeChatSessionItem: onDidDisposeChatSessionItem.event, + createChatSessionItem: (resource: vscode.Uri, label: string) => { + if (isDisposed) { + throw new Error('ChatSessionItemController has been disposed'); + } + + return new ChatSessionItemImpl(resource, label, () => { + this._proxy.$onDidChangeChatSessionItems(handle); + }); + }, dispose: () => { - this._chatSessionItemProviders.delete(handle); + isDisposed = true; disposables.dispose(); - this._proxy.$unregisterChatSessionItemProvider(handle); - } + }, }; + + this._chatSessionItemControllers.set(handle, { controller, extension, disposable: disposables, sessionType: id }); + this._proxy.$registerChatSessionItemProvider(handle, id); + + disposables.add(toDisposable(() => { + this._chatSessionItemControllers.delete(handle); + this._proxy.$unregisterChatSessionItemProvider(handle); + })); + + return controller; } registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionScheme: string, chatParticipant: vscode.ChatParticipant, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable { @@ -204,19 +368,19 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } async $provideChatSessionItems(handle: number, token: vscode.CancellationToken): Promise { - const entry = this._chatSessionItemProviders.get(handle); + const entry = this._chatSessionItemControllers.get(handle); if (!entry) { - this._logService.error(`No provider registered for handle ${handle}`); + this._logService.error(`No controller registered for handle ${handle}`); return []; } - const sessions = await entry.provider.provideChatSessionItems(token); - if (!sessions) { - return []; - } + // Call the refresh handler to populate items + await entry.controller.refreshHandler(); + + const items = [...entry.controller.items]; 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)); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 41cbfdd1738..0a718a8c547 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3431,6 +3431,17 @@ export class ChatSessionChangedFile { constructor(public readonly modifiedUri: vscode.Uri, public readonly insertions: number, public readonly deletions: number, public readonly originalUri?: vscode.Uri) { } } +// Stub class - actual implementation is in extHostChatSessions.ts +export class ChatSessionItemController { + readonly id!: string; + readonly items!: vscode.ChatSessionItemCollection; + refreshHandler!: () => Thenable; + readonly onDidArchiveChatSessionItem!: vscode.Event; + readonly onDidDisposeChatSessionItem!: vscode.Event; + createChatSessionItem(_resource: vscode.Uri, _label: string): vscode.ChatSessionItem { throw new Error('Stub'); } + dispose(): void { throw new Error('Stub'); } +} + export enum ChatResponseReferencePartStatusKind { Complete = 1, Partial = 2, diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 12c664326d6..c44f0991d36 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -30,7 +30,7 @@ declare module 'vscode' { /** * Creates a new {@link ChatSessionItemController chat session item controller} with the given unique identifier. */ - export function createChatSessionItemController(id: string): ChatSessionItemController; + export function createChatSessionItemController(id: string, refreshHandler: () => Thenable): ChatSessionItemController; } /** @@ -64,7 +64,7 @@ declare module 'vscode' { /** * Fired when an item is archived by the editor * - * TODO: expose archive state on the item too? Or should this + * TODO: expose archive state on the item too? */ readonly onDidArchiveChatSessionItem: Event; @@ -77,7 +77,7 @@ declare module 'vscode' { /** * A collection of chat session items. It provides operations for managing and iterating over the items. */ - export interface ChatSessionItemCollection extends Iterable { + export interface ChatSessionItemCollection extends Iterable { /** * Gets the number of items in the collection. */ From 25bdc74f90a7494e5438d7d332da263331c57020 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:44:43 -0800 Subject: [PATCH 3/7] Cleanup --- .../workbench/api/common/extHost.api.impl.ts | 5 +- .../api/common/extHostChatSessions.ts | 122 +++++++++++++----- src/vs/workbench/api/common/extHostTypes.ts | 11 -- .../vscode.proposed.chatSessionsProvider.d.ts | 45 ++++++- 4 files changed, 135 insertions(+), 48 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 99e61873974..ad4c3bf659f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1525,6 +1525,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return _asExtensionEvent(extHostChatAgents2.onDidDisposeChatSession)(listeners, thisArgs, disposables); }, + registerChatSessionItemProvider: (id: string, provider: vscode.ChatSessionItemProvider) => { + checkProposedApiEnabled(extension, 'chatSessionsProvider'); + return extHostChatSessions.registerChatSessionItemProvider(extension, id, provider); + }, createChatSessionItemController: (id: string, refreshHandler: () => Thenable) => { checkProposedApiEnabled(extension, 'chatSessionsProvider'); return extHostChatSessions.createChatSessionItemController(extension, id, refreshHandler); @@ -1865,7 +1869,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InteractiveSessionVoteDirection: extHostTypes.InteractiveSessionVoteDirection, ChatCopyKind: extHostTypes.ChatCopyKind, ChatSessionChangedFile: extHostTypes.ChatSessionChangedFile, - ChatSessionItemController: extHostTypes.ChatSessionItemController, ChatEditingSessionActionOutcome: extHostTypes.ChatEditingSessionActionOutcome, InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, DebugStackFrame: extHostTypes.DebugStackFrame, diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 2c926f086e9..6ecdfc4d375 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -56,8 +56,10 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } set label(value: string) { - this.#label = value; - this.#onChanged(); + if (this.#label !== value) { + this.#label = value; + this.#onChanged(); + } } get iconPath(): vscode.IconPath | undefined { @@ -65,8 +67,10 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } set iconPath(value: vscode.IconPath | undefined) { - this.#iconPath = value; - this.#onChanged(); + if (this.#iconPath !== value) { + this.#iconPath = value; + this.#onChanged(); + } } get description(): string | vscode.MarkdownString | undefined { @@ -74,8 +78,10 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } set description(value: string | vscode.MarkdownString | undefined) { - this.#description = value; - this.#onChanged(); + if (this.#description !== value) { + this.#description = value; + this.#onChanged(); + } } get badge(): string | vscode.MarkdownString | undefined { @@ -83,8 +89,10 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } set badge(value: string | vscode.MarkdownString | undefined) { - this.#badge = value; - this.#onChanged(); + if (this.#badge !== value) { + this.#badge = value; + this.#onChanged(); + } } get status(): vscode.ChatSessionStatus | undefined { @@ -92,8 +100,10 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } set status(value: vscode.ChatSessionStatus | undefined) { - this.#status = value; - this.#onChanged(); + if (this.#status !== value) { + this.#status = value; + this.#onChanged(); + } } get tooltip(): string | vscode.MarkdownString | undefined { @@ -101,8 +111,10 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } set tooltip(value: string | vscode.MarkdownString | undefined) { - this.#tooltip = value; - this.#onChanged(); + if (this.#tooltip !== value) { + this.#tooltip = value; + this.#onChanged(); + } } get timing(): { startTime: number; endTime?: number } | undefined { @@ -110,8 +122,10 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } set timing(value: { startTime: number; endTime?: number } | undefined) { - this.#timing = value; - this.#onChanged(); + if (this.#timing !== value) { + this.#timing = value; + this.#onChanged(); + } } get changes(): readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number } | undefined { @@ -119,8 +133,10 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } set changes(value: readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number } | undefined) { - this.#changes = value; - this.#onChanged(); + if (this.#changes !== value) { + this.#changes = value; + this.#onChanged(); + } } } @@ -200,12 +216,19 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio private static _sessionHandlePool = 0; private readonly _proxy: Proxied; + private readonly _chatSessionItemProviders = new Map(); private readonly _chatSessionItemControllers = new Map(); + private _nextChatSessionItemProviderHandle = 0; private readonly _chatSessionContentProviders = new Map { + this._proxy.$onDidChangeChatSessionItems(handle); + })); + } + if (provider.onDidCommitChatSessionItem) { + disposables.add(provider.onDidCommitChatSessionItem((e) => { + const { original, modified } = e; + this._proxy.$onDidCommitChatSessionItem(handle, original.resource, modified.resource); + })); + } + return { + dispose: () => { + this._chatSessionItemProviders.delete(handle); + disposables.dispose(); + this._proxy.$unregisterChatSessionItemProvider(handle); + } + }; + } + + createChatSessionItemController(extension: IExtensionDescription, id: string, refreshHandler: () => Thenable): vscode.ChatSessionItemController { const handle = this._nextChatSessionItemControllerHandle++; const disposables = new DisposableStore(); // TODO: Currently not hooked up const onDidArchiveChatSessionItem = disposables.add(new Emitter()); - const onDidDisposeChatSessionItem = disposables.add(new Emitter()); const collection = new ChatSessionItemCollectionImpl(() => { this._proxy.$onDidChangeChatSessionItems(handle); @@ -274,7 +323,6 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio refreshHandler, items: collection, onDidArchiveChatSessionItem: onDidArchiveChatSessionItem.event, - onDidDisposeChatSessionItem: onDidDisposeChatSessionItem.event, createChatSessionItem: (resource: vscode.Uri, label: string) => { if (isDisposed) { throw new Error('ChatSessionItemController has been disposed'); @@ -345,7 +393,7 @@ 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, @@ -368,21 +416,35 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } async $provideChatSessionItems(handle: number, token: vscode.CancellationToken): Promise { - const entry = this._chatSessionItemControllers.get(handle); - if (!entry) { - this._logService.error(`No controller registered for handle ${handle}`); - return []; + let items: vscode.ChatSessionItem[]; + + 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 []; + } } - // Call the refresh handler to populate items - await entry.controller.refreshHandler(); - - const items = [...entry.controller.items]; - const response: IChatSessionItem[] = []; - for (const [_, sessionContent] of items) { + 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; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 0a718a8c547..41cbfdd1738 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3431,17 +3431,6 @@ export class ChatSessionChangedFile { constructor(public readonly modifiedUri: vscode.Uri, public readonly insertions: number, public readonly deletions: number, public readonly originalUri?: vscode.Uri) { } } -// Stub class - actual implementation is in extHostChatSessions.ts -export class ChatSessionItemController { - readonly id!: string; - readonly items!: vscode.ChatSessionItemCollection; - refreshHandler!: () => Thenable; - readonly onDidArchiveChatSessionItem!: vscode.Event; - readonly onDidDisposeChatSessionItem!: vscode.Event; - createChatSessionItem(_resource: vscode.Uri, _label: string): vscode.ChatSessionItem { throw new Error('Stub'); } - dispose(): void { throw new Error('Stub'); } -} - export enum ChatResponseReferencePartStatusKind { Complete = 1, Partial = 2, diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index c44f0991d36..b7e53e5d49b 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -27,6 +27,18 @@ 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; + /** * Creates a new {@link ChatSessionItemController chat session item controller} with the given unique identifier. */ @@ -36,7 +48,33 @@ declare module 'vscode' { /** * Provides a list of information about chat sessions. */ - export class ChatSessionItemController { + export interface ChatSessionItemProvider { + /** + * Event that the provider can fire to signal that chat sessions have changed. + */ + readonly onDidChangeChatSessionItems: Event; + + /** + * Provides a list of chat sessions. + */ + // TODO: Do we need a flag to try auth if needed? + provideChatSessionItems(token: CancellationToken): ProviderResult; + + // #region Unstable parts of API + + /** + * Event that the provider can fire to signal that the current (original) chat session should be replaced with a new (modified) chat session. + * The UI can use this information to gracefully migrate the user to the new session. + */ + readonly onDidCommitChatSessionItem: Event<{ original: ChatSessionItem /** untitled */; modified: ChatSessionItem /** newly created */ }>; + + // #endregion + } + + /** + * Provides a list of information about chat sessions. + */ + export interface ChatSessionItemController { readonly id: string; /** @@ -67,11 +105,6 @@ declare module 'vscode' { * TODO: expose archive state on the item too? */ readonly onDidArchiveChatSessionItem: Event; - - /** - * Fired when an item is disposed by the editor - */ - readonly onDidDisposeChatSessionItem: Event; } /** From 242baf74bc4c1b77a24f1d7149539965227c1a87 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:34:17 -0800 Subject: [PATCH 4/7] Add archived property --- .../api/browser/mainThreadChatSessions.ts | 3 +- .../api/common/extHostChatSessions.ts | 28 ++++++++++++++----- .../chat/common/chatSessionsService.ts | 1 + .../vscode.proposed.chatSessionsProvider.d.ts | 5 ++++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 791a22f677f..fa5e7997e62 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -382,7 +382,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat )); } - $onDidChangeChatSessionItems(handle: number): void { this._itemProvidersRegistrations.get(handle)?.onDidChangeItems.fire(); } @@ -490,7 +489,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat changes: revive(session.changes), resource: uri, iconPath: session.iconPath, - tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined, + tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined, archived: session.archived, } satisfies IChatSessionItem; })); } catch (error) { diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 6ecdfc4d375..9c751cd07a6 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -40,6 +40,7 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { #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 }; @@ -106,6 +107,17 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } } + 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; } @@ -306,14 +318,14 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio createChatSessionItemController(extension: IExtensionDescription, id: string, refreshHandler: () => Thenable): vscode.ChatSessionItemController { - const handle = this._nextChatSessionItemControllerHandle++; + const controllerHandle = this._nextChatSessionItemControllerHandle++; const disposables = new DisposableStore(); // TODO: Currently not hooked up const onDidArchiveChatSessionItem = disposables.add(new Emitter()); const collection = new ChatSessionItemCollectionImpl(() => { - this._proxy.$onDidChangeChatSessionItems(handle); + this._proxy.$onDidChangeChatSessionItems(controllerHandle); }); let isDisposed = false; @@ -329,7 +341,8 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } return new ChatSessionItemImpl(resource, label, () => { - this._proxy.$onDidChangeChatSessionItems(handle); + // TODO: Optimize to only update the specific item + this._proxy.$onDidChangeChatSessionItems(controllerHandle); }); }, dispose: () => { @@ -338,12 +351,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }, }; - this._chatSessionItemControllers.set(handle, { controller, extension, disposable: disposables, sessionType: id }); - this._proxy.$registerChatSessionItemProvider(handle, id); + this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, sessionType: id }); + this._proxy.$registerChatSessionItemProvider(controllerHandle, id); disposables.add(toDisposable(() => { - this._chatSessionItemControllers.delete(handle); - this._proxy.$unregisterChatSessionItemProvider(handle); + this._chatSessionItemControllers.delete(controllerHandle); + this._proxy.$unregisterChatSessionItemProvider(controllerHandle); })); return controller; @@ -400,6 +413,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio 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, diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 6e50c8e5a7b..95352a55f2c 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -62,6 +62,7 @@ export interface IChatSessionsExtensionPoint { readonly commands?: IChatSessionCommandContribution[]; readonly canDelegate?: boolean; } + export interface IChatSessionItem { resource: URI; label: string; diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index b7e53e5d49b..bdef678b5aa 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -189,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 */ From 93ff336d5185459a117d520b3f735c75bb4a1887 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:39:35 -0800 Subject: [PATCH 5/7] Cleanup --- src/vs/workbench/api/browser/mainThreadChatSessions.ts | 3 ++- src/vs/workbench/api/common/extHost.api.impl.ts | 8 ++++---- src/vs/workbench/api/common/extHostChatSessions.ts | 9 ++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index fa5e7997e62..7f8142003d5 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -489,7 +489,8 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat changes: revive(session.changes), resource: uri, iconPath: session.iconPath, - tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined, archived: session.archived, + tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined, + archived: session.archived, } satisfies IChatSessionItem; })); } catch (error) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ad4c3bf659f..4d893851be1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1525,13 +1525,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return _asExtensionEvent(extHostChatAgents2.onDidDisposeChatSession)(listeners, thisArgs, disposables); }, - registerChatSessionItemProvider: (id: string, provider: vscode.ChatSessionItemProvider) => { + registerChatSessionItemProvider: (chatSessionType: string, provider: vscode.ChatSessionItemProvider) => { checkProposedApiEnabled(extension, 'chatSessionsProvider'); - return extHostChatSessions.registerChatSessionItemProvider(extension, id, provider); + return extHostChatSessions.registerChatSessionItemProvider(extension, chatSessionType, provider); }, - createChatSessionItemController: (id: string, refreshHandler: () => Thenable) => { + createChatSessionItemController: (chatSessionType: string, refreshHandler: () => Thenable) => { checkProposedApiEnabled(extension, 'chatSessionsProvider'); - return extHostChatSessions.createChatSessionItemController(extension, id, refreshHandler); + return extHostChatSessions.createChatSessionItemController(extension, chatSessionType, refreshHandler); }, registerChatSessionContentProvider(scheme: string, provider: vscode.ChatSessionContentProvider, chatParticipant: vscode.ChatParticipant, capabilities?: vscode.ChatSessionCapabilities) { checkProposedApiEnabled(extension, 'chatSessionsProvider'); diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 9c751cd07a6..65253dbe9ce 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -34,7 +34,6 @@ import { SymbolKind, SymbolKinds } from '../../../editor/common/languages.js'; // #region Chat Session Item Controller class ChatSessionItemImpl implements vscode.ChatSessionItem { - readonly resource: vscode.Uri; #label: string; #iconPath?: vscode.IconPath; #description?: string | vscode.MarkdownString; @@ -46,6 +45,8 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { #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; @@ -192,10 +193,8 @@ class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection return this.#items.get(resource); } - *[Symbol.iterator](): Generator { - for (const [uri, item] of this.#items) { - yield [uri, item] as const; - } + [Symbol.iterator](): Iterator { + return this.#items.entries(); } } From bfd6eed65bfa65d77440864372e75679e0df1c17 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:41:16 -0800 Subject: [PATCH 6/7] Bump api version --- src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index bdef678b5aa..213feb92c00 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -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' { /** From 49ee0d00694f3a90c57f79b9f5c51229fdfefaac Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:09:18 -0800 Subject: [PATCH 7/7] Also update proposal file --- src/vs/platform/extensions/common/extensionsApiProposals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 65bcacd26e6..ddefdd8934b 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -65,7 +65,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',