From ee699b749f7bb3e1f72a4bbdc8e351998cf7940b Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:39:13 -0700 Subject: [PATCH] Make chat sessions give a delta of what has changed For #300770 --- src/vs/sessions/test/web.test.ts | 15 ++-- .../api/browser/mainThreadChatSessions.ts | 46 +++++----- .../workbench/api/common/extHost.protocol.ts | 1 - .../agentSessions/agentSessionsModel.ts | 15 +++- .../localAgentSessionsController.ts | 46 ++++++---- .../chatSessions/chatSessions.contribution.ts | 62 +++++++++---- .../chatSetup/chatSetupGrowthSession.ts | 10 ++- .../chat/common/chatService/chatService.ts | 4 +- .../common/chatService/chatServiceImpl.ts | 4 +- .../chat/common/chatSessionsService.ts | 9 +- .../agentSessionViewModel.test.ts | 87 ++++++++++--------- .../localAgentSessionsController.test.ts | 30 ++++++- .../common/chatService/mockChatService.ts | 19 ++-- .../test/common/mockChatSessionsService.ts | 8 +- 14 files changed, 223 insertions(+), 133 deletions(-) diff --git a/src/vs/sessions/test/web.test.ts b/src/vs/sessions/test/web.test.ts index 77c7a39df77..c515d0e54ff 100644 --- a/src/vs/sessions/test/web.test.ts +++ b/src/vs/sessions/test/web.test.ts @@ -22,7 +22,7 @@ import { URI } from '../../base/common/uri.js'; import { Disposable } from '../../base/common/lifecycle.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../workbench/common/contributions.js'; import { IChatProgress } from '../../workbench/contrib/chat/common/chatService/chatService.js'; -import { IChatSessionsService, IChatSessionItem, IChatSessionFileChange, ChatSessionStatus, IChatSessionHistoryItem } from '../../workbench/contrib/chat/common/chatSessionsService.js'; +import { IChatSessionsService, IChatSessionItem, IChatSessionFileChange, ChatSessionStatus, IChatSessionHistoryItem, IChatSessionItemsDelta } from '../../workbench/contrib/chat/common/chatSessionsService.js'; import { IGitService, IGitExtensionDelegate, IGitRepository } from '../../workbench/contrib/git/common/gitService.js'; import { IFileService } from '../../platform/files/common/files.js'; import { InMemoryFileSystemProvider } from '../../platform/files/common/inMemoryFilesystemProvider.js'; @@ -228,7 +228,7 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu static readonly ID = 'sessions.test.mockChatAgent'; private readonly _sessionItems: IChatSessionItem[] = []; - private readonly _itemsChangedEmitter = new Emitter(); + private readonly _itemsChangedEmitter = new Emitter(); private readonly _sessionHistory = new Map(); constructor( @@ -273,6 +273,7 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu // Add or update session in list const existing = this._sessionItems.find(s => s.resource.toString() === key); + let addedOrUpdated: IChatSessionItem | undefined = existing; if (existing) { existing.timing.lastRequestStarted = now; existing.timing.lastRequestEnded = now; @@ -280,15 +281,19 @@ class MockChatAgentContribution extends Disposable implements IWorkbenchContribu existing.changes = changes; } } else { - this._sessionItems.push({ + addedOrUpdated = { resource, label: message.slice(0, 50) || 'Mock Session', status: ChatSessionStatus.Completed, timing: { created: now, lastRequestStarted: now, lastRequestEnded: now }, ...(changes ? { changes } : {}), - }); + }; + this._sessionItems.push(addedOrUpdated); + } + + if (addedOrUpdated) { + this._itemsChangedEmitter.fire({ addedOrUpdated: [addedOrUpdated] }); } - this._itemsChangedEmitter.fire(); } private registerMockAgents(): void { diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index dd3b219228f..62fe90f1c7c 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -15,6 +15,7 @@ import { isEqual } from '../../../base/common/resources.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { IDialogService } from '../../../platform/dialogs/common/dialogs.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { hasValidDiff, IAgentSession } from '../../contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../contrib/chat/browser/agentSessions/agentSessionsService.js'; @@ -24,7 +25,7 @@ import { ChatEditorInput } from '../../contrib/chat/browser/widgetHosts/editor/c import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { awaitStatsForSession } from '../../contrib/chat/common/chat.js'; import { IChatContentInlineReference, IChatProgress, IChatService, ResponseModelState } from '../../contrib/chat/common/chatService/chatService.js'; -import { ChatSessionStatus, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemController, IChatSessionProviderOptionItem, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; +import { ChatSessionStatus, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionProviderOptionItem, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; import { isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.js'; @@ -329,16 +330,25 @@ class MainThreadChatSessionItemController extends Disposable implements IChatSes private readonly _proxy: ExtHostChatSessionsShape; private readonly _handle: number; - private readonly _onDidChangeChatSessionItems = this._register(new Emitter()); + private readonly _onDidChangeChatSessionItems = this._register(new Emitter()); public readonly onDidChangeChatSessionItems = this._onDidChangeChatSessionItems.event; constructor( proxy: ExtHostChatSessionsShape, - handle: number + chatSessionType: string, + handle: number, + @IChatService chatService: IChatService, ) { super(); this._proxy = proxy; this._handle = handle; + + this._register(chatService.registerChatModelChangeListeners(chatSessionType, (sessionResource) => { + const item = this._items.get(sessionResource); + if (item) { + this._onDidChangeChatSessionItems.fire({ addedOrUpdated: [item] }); + } + })); } private readonly _items = new ResourceMap(); @@ -361,7 +371,9 @@ class MainThreadChatSessionItemController extends Disposable implements IChatSes changes: revive(dto.changes), }; this._items.set(item.resource, item); - this._onDidChangeChatSessionItems.fire(); + this._onDidChangeChatSessionItems.fire({ + addedOrUpdated: [item], + }); return item; } @@ -373,17 +385,18 @@ class MainThreadChatSessionItemController extends Disposable implements IChatSes for (const uri of change.removed) { this._items.delete(uri); } - this._onDidChangeChatSessionItems.fire(); + this._onDidChangeChatSessionItems.fire({ + addedOrUpdated: change.addedOrUpdated, + removed: change.removed, + }); } addOrUpdateItem(item: IChatSessionItem): void { warnOnUntitledSessionResource(item.resource); this._items.set(item.resource, item); - this._onDidChangeChatSessionItems.fire(); - } - - fireOnDidChangeChatSessionItems(): void { - this._onDidChangeChatSessionItems.fire(); + this._onDidChangeChatSessionItems.fire({ + addedOrUpdated: [item], + }); } } @@ -413,6 +426,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat @IEditorService private readonly _editorService: IEditorService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @ILogService private readonly _logService: ILogService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -446,7 +460,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat $registerChatSessionItemController(handle: number, chatSessionType: string): void { const disposables = new DisposableStore(); - const controller = disposables.add(new MainThreadChatSessionItemController(this._proxy, handle)); + const controller = disposables.add(this._instantiationService.createInstance(MainThreadChatSessionItemController, this._proxy, chatSessionType, handle)); disposables.add(this._chatSessionsService.registerChatSessionItemController(chatSessionType, controller)); this._itemControllerRegistrations.set(handle, { @@ -454,11 +468,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat controller, dispose: () => disposables.dispose(), }); - - disposables.add(this._chatService.registerChatModelChangeListeners( - chatSessionType, - () => controller.fireOnDidChangeChatSessionItems() - )); } private getController(handle: number): MainThreadChatSessionItemController { @@ -469,11 +478,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat return registration.controller; } - $onDidChangeChatSessionItems(handle: number): void { - const controller = this.getController(handle); - controller.fireOnDidChangeChatSessionItems(); - } - private async _resolveSessionItem(item: Dto): Promise { const uri = URI.revive(item.resource); const model = this._chatService.getSession(uri); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c276b416244..85c532b6d4a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3587,7 +3587,6 @@ export interface MainThreadChatSessionsShape extends IDisposable { $unregisterChatSessionItemController(controllerHandle: number): void; $updateChatSessionItems(controllerHandle: number, change: IChatSessionItemsChange): Promise; $addOrUpdateChatSessionItem(controllerHandle: number, item: Dto): Promise; - $onDidChangeChatSessionItems(controllerHandle: number): void; $onDidCommitChatSessionItem(controllerHandle: number, original: UriComponents, modified: UriComponents): void; $registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void; $unregisterChatSessionContentProvider(handle: number): void; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index e66ecd14f44..ca12cdfc846 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -27,6 +27,7 @@ import { IChatEntitlementService } from '../../../../services/chat/common/chatEn import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js'; import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionFileChange2, IChatSessionItem, IChatSessionsService, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; +import { getChatSessionType } from '../../common/model/chatUri.js'; import { IChatWidgetService } from '../chat.js'; import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName, isBuiltInAgentSessionProvider } from './agentSessions.js'; @@ -434,7 +435,19 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode // Sessions updates this._register(this.chatSessionsService.onDidChangeItemsProviders(({ chatSessionType }) => this.resolve(chatSessionType))); this._register(this.chatSessionsService.onDidChangeAvailability(() => this.resolve(undefined))); - this._register(this.chatSessionsService.onDidChangeSessionItems(({ chatSessionType }) => this.updateItems([chatSessionType], CancellationToken.None))); + this._register(this.chatSessionsService.onDidChangeSessionItems((delta) => { + const changedChatSessionTypes = new Set(); + + for (const resource of delta.addedOrUpdated ?? []) { + changedChatSessionTypes.add(getChatSessionType(resource.resource)); + } + + for (const resource of delta.removed ?? []) { + changedChatSessionTypes.add(getChatSessionType(resource)); + } + + this.updateItems(Array.from(changedChatSessionTypes), CancellationToken.None); + })); this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.resolve(undefined))); this._register(this.workspaceTrustManagementService.onDidChangeTrust(() => this.resolve(undefined))); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts index 569dffd129c..de19dfcec9b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts @@ -10,12 +10,14 @@ import { Emitter } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { IChatModel } from '../../common/model/chatModel.js'; -import { convertLegacyChatSessionTiming, IChatDetail, IChatService, ResponseModelState } from '../../common/chatService/chatService.js'; -import { ChatSessionStatus, IChatSessionItem, IChatSessionItemController, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; -import { getChatSessionType } from '../../common/model/chatUri.js'; +import { isEqual } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { convertLegacyChatSessionTiming, IChatDetail, IChatService, ResponseModelState } from '../../common/chatService/chatService.js'; +import { ChatSessionStatus, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; +import { IChatModel } from '../../common/model/chatModel.js'; +import { getChatSessionType } from '../../common/model/chatUri.js'; export class LocalAgentsSessionsController extends Disposable implements IChatSessionItemController, IWorkbenchContribution { @@ -26,7 +28,7 @@ export class LocalAgentsSessionsController extends Disposable implements IChatSe private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; - readonly _onDidChangeChatSessionItems = this._register(new Emitter()); + readonly _onDidChangeChatSessionItems = this._register(new Emitter()); readonly onDidChangeChatSessionItems = this._onDidChangeChatSessionItems.event; constructor( @@ -51,20 +53,32 @@ export class LocalAgentsSessionsController extends Disposable implements IChatSe } private registerListeners(): void { - const refreshItems = async () => { - this._onDidChangeChatSessionItems.fire(); - await this.refresh(CancellationToken.None); - this._onDidChangeChatSessionItems.fire(); - }; + this._register(this.chatService.registerChatModelChangeListeners(Schemas.vscodeLocalChatSession, async sessionResource => { + if (getChatSessionType(sessionResource) !== this.chatSessionType) { + return; + } - this._register(this.chatService.registerChatModelChangeListeners(Schemas.vscodeLocalChatSession, refreshItems)); + let item = this.getItem(sessionResource); + if (!item) { + await this.refresh(CancellationToken.None); + item = this.getItem(sessionResource); + } - this._register(this.chatService.onDidDisposeSession(e => { - const session = e.sessionResource.filter(resource => getChatSessionType(resource) === this.chatSessionType); - if (session.length > 0) { - refreshItems(); + if (item) { + this._onDidChangeChatSessionItems.fire({ addedOrUpdated: [item] }); } })); + + this._register(this.chatService.onDidDisposeSession(e => { + const removedSessionResources = e.sessionResource.filter(resource => getChatSessionType(resource) === this.chatSessionType); + if (removedSessionResources.length) { + this._onDidChangeChatSessionItems.fire({ removed: removedSessionResources }); + } + })); + } + + private getItem(sessionResource: URI): IChatSessionItem | undefined { + return this._items.find(item => isEqual(item.resource, sessionResource)); } private async provideChatSessionItems(token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 63e9a52fb99..e5b749710f4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -9,7 +9,7 @@ import { CancellationToken, CancellationTokenSource } from '../../../../../base/ import { Codicon } from '../../../../../base/common/codicons.js'; import { AsyncEmitter, Emitter, Event } from '../../../../../base/common/event.js'; import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; +import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; import * as resources from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -31,7 +31,7 @@ import { ExtensionsRegistry } from '../../../../services/extensions/common/exten import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemController, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; +import { IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; @@ -47,7 +47,10 @@ import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { AgentSessionProviders, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; import { BugIndicatingError, isCancellationError } from '../../../../../base/common/errors.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; -import { isUntitledChatSession, LocalChatSessionUri } from '../../common/model/chatUri.js'; +import { + getChatSessionType, + isUntitledChatSession, LocalChatSessionUri +} from '../../common/model/chatUri.js'; import { assertNever } from '../../../../../base/common/assert.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { Target } from '../../common/promptSyntax/promptTypes.js'; @@ -276,7 +279,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ private readonly _onDidChangeItemsProviders = this._register(new Emitter<{ readonly chatSessionType: string }>()); readonly onDidChangeItemsProviders = this._onDidChangeItemsProviders.event; - private readonly _onDidChangeSessionItems = this._register(new Emitter<{ readonly chatSessionType: string }>()); + private readonly _onDidChangeSessionItems = this._register(new Emitter()); readonly onDidChangeSessionItems = this._onDidChangeSessionItems.event; private readonly _onDidChangeAvailability = this._register(new Emitter()); @@ -350,10 +353,17 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } })); - this._register(this.onDidChangeSessionItems(({ chatSessionType }) => { - this.updateInProgressStatus(chatSessionType).catch(error => { - this._logService.warn(`Failed to update progress status for '${chatSessionType}':`, error); - }); + this._register(this.onDidChangeSessionItems((delta) => { + const changedChatSessionTypes = new Set(); + for (const resource of delta.addedOrUpdated ?? []) { + changedChatSessionTypes.add(getChatSessionType(resource.resource)); + } + + for (const chatSessionType of changedChatSessionTypes) { + this.updateInProgressStatus(chatSessionType).catch(error => { + this._logService.warn(`Failed to update progress status for '${chatSessionType}':`, error); + }); + } })); this._register(this._labelService.registerFormatter({ @@ -597,7 +607,11 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } private _evaluateAvailability(): void { - let hasChanges = false; + const newlyEnabledChatSessionTypes = new Set(); + const newlyDisabledChatSessionTypes = new Set(); + + const disposedChatSessions = new ResourceSet(); + for (const { contribution, extension } of this._contributions.values()) { const isCurrentlyRegistered = this._contributionDisposables.has(contribution.type); const shouldBeRegistered = this._isContributionAvailable(contribution); @@ -607,21 +621,25 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ this._contributionDisposables.deleteAndDispose(contribution.type); // Also dispose any cached sessions for this contribution - this._disposeSessionsForContribution(contribution.type); - hasChanges = true; + for (const sessionResource of this._disposeSessionsForContribution(contribution.type)) { + disposedChatSessions.add(sessionResource); + } + + newlyDisabledChatSessionTypes.add(contribution.type); } else if (!isCurrentlyRegistered && shouldBeRegistered) { // Enable the contribution by registering it this._enableContribution(contribution, extension); - hasChanges = true; + newlyEnabledChatSessionTypes.add(contribution.type); } } - if (hasChanges) { + if (newlyEnabledChatSessionTypes.size > 0 || newlyDisabledChatSessionTypes.size > 0) { this._onDidChangeAvailability.fire(); - for (const chatSessionType of this._itemControllers.keys()) { + for (const chatSessionType of [...newlyEnabledChatSessionTypes, ...newlyDisabledChatSessionTypes]) { this._onDidChangeItemsProviders.fire({ chatSessionType }); } - for (const { contribution } of this._contributions.values()) { - this._onDidChangeSessionItems.fire({ chatSessionType: contribution.type }); + + if (disposedChatSessions.size > 0) { + this._onDidChangeSessionItems.fire({ removed: Array.from(disposedChatSessions) }); } } this._updateHasCanDelegateProvidersContextKey(); @@ -638,7 +656,12 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ disposableStore.add(this._registerMenuItems(contribution, ext)); } - private _disposeSessionsForContribution(contributionId: string): void { + /** + * Disposes of all sessions that belong to a contribution + * + * @returns List of session resources that were disposed. + */ + private _disposeSessionsForContribution(contributionId: string): URI[] { // Find and dispose all sessions that belong to this contribution const sessionsToDispose: URI[] = []; for (const [sessionResource, sessionData] of this._sessions) { @@ -657,6 +680,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ sessionData.dispose(); // This will call _onWillDisposeSession and clean up } } + return sessionsToDispose; } private _registerAgent(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable { @@ -866,8 +890,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ this._itemControllers.set(chatSessionType, { controller, initialRefresh: controller.refresh(initialRefreshCts.token) }); this._onDidChangeItemsProviders.fire({ chatSessionType }); - disposables.add(controller.onDidChangeChatSessionItems(() => { - this._onDidChangeSessionItems.fire({ chatSessionType }); + disposables.add(controller.onDidChangeChatSessionItems(e => { + this._onDidChangeSessionItems.fire(e); })); this.updateInProgressStatus(chatSessionType).catch(error => { diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupGrowthSession.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupGrowthSession.ts index 1563924cdb3..1382c23832e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupGrowthSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupGrowthSession.ts @@ -14,7 +14,7 @@ import { ICommandService } from '../../../../../platform/commands/common/command import { ILogService } from '../../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ILifecycleService, LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; -import { ChatSessionStatus, IChatSessionItem, IChatSessionItemController, IChatSessionsService } from '../../common/chatSessionsService.js'; +import { ChatSessionStatus, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionsService } from '../../common/chatSessionsService.js'; import { AgentSessionProviders } from '../agentSessions/agentSessions.js'; import { IAgentSession } from '../agentSessions/agentSessionsModel.js'; import { ISessionOpenerParticipant, ISessionOpenOptions, sessionOpenerRegistry } from '../agentSessions/agentSessionsOpener.js'; @@ -36,8 +36,8 @@ export class GrowthSessionController extends Disposable implements IChatSessionI private static readonly SESSION_URI = URI.from({ scheme: AgentSessionProviders.Growth, path: '/growth-welcome' }); - private readonly _onDidChangeChatSessionItems = this._register(new Emitter()); - readonly onDidChangeChatSessionItems: Event = this._onDidChangeChatSessionItems.event; + private readonly _onDidChangeChatSessionItems = this._register(new Emitter()); + readonly onDidChangeChatSessionItems = this._onDidChangeChatSessionItems.event; private readonly _onDidDismiss = this._register(new Emitter()); readonly onDidDismiss: Event = this._onDidDismiss.event; @@ -103,7 +103,9 @@ export class GrowthSessionController extends Disposable implements IChatSessionI this.storageService.store(GrowthSessionController.STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.USER); // Fire change event first so that listeners (like the model) see empty items - this._onDidChangeChatSessionItems.fire(); + this._onDidChangeChatSessionItems.fire({ + removed: [GrowthSessionController.SESSION_URI], + }); // Then fire dismiss event which triggers unregistration of the controller. this._onDidDismiss.fire(); } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index a083e6212a5..b7ea2228696 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -1499,7 +1499,7 @@ export interface IChatService { readonly onDidReceiveQuestionCarouselAnswer: Event<{ requestId: string; resolveId: string; answers: IChatQuestionAnswers | undefined }>; notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: IChatQuestionAnswers | undefined): void; - readonly onDidDisposeSession: Event<{ readonly sessionResource: URI[]; readonly reason: 'cleared' }>; + readonly onDidDisposeSession: Event<{ readonly sessionResource: readonly URI[]; readonly reason: 'cleared' }>; transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise; @@ -1510,7 +1510,7 @@ export interface IChatService { /** * @deprecated */ - registerChatModelChangeListeners(chatSessionType: string, onChange: () => void): IDisposable; + registerChatModelChangeListeners(chatSessionType: string, onChange: (chatSessionResource: URI) => void): IDisposable; /** * For tests only! diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 2d554ce0f8e..d00dff8c817 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -1615,7 +1615,7 @@ export class ChatService extends Disposable implements IChatService { return localSessionId; } - public registerChatModelChangeListeners(chatSessionType: string, onChange: () => void): IDisposable { + public registerChatModelChangeListeners(chatSessionType: string, onChange: (chatSessionResource: URI) => void): IDisposable { const disposableStore = new DisposableStore(); const chatModelsICareAbout = this.chatModels.map(models => Array.from(models).filter((model: IChatModel) => model.sessionResource.scheme === chatSessionType) @@ -1638,7 +1638,7 @@ export class ChatService extends Disposable implements IChatService { listeners.set(added.sessionResource, autorun(reader => { requestChangeListener.read(reader)?.read(reader); modelChangeListener.read(reader); - onChange(); + onChange(added.sessionResource); })); }); } diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 65834a4303e..01020f2b624 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -198,9 +198,14 @@ export interface IChatNewSessionRequest { readonly command?: string; } +export interface IChatSessionItemsDelta { + readonly addedOrUpdated?: readonly IChatSessionItem[]; + readonly removed?: readonly URI[]; +} + export interface IChatSessionItemController { - readonly onDidChangeChatSessionItems: Event; + readonly onDidChangeChatSessionItems: Event; get items(): readonly IChatSessionItem[]; @@ -229,7 +234,7 @@ export interface IChatSessionsService { // #region Chat session item provider support readonly onDidChangeItemsProviders: Event<{ readonly chatSessionType: string }>; - readonly onDidChangeSessionItems: Event<{ readonly chatSessionType: string }>; + readonly onDidChangeSessionItems: Event; readonly onDidChangeAvailability: Event; readonly onDidChangeInProgress: Event; diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index 34a53d0cc1f..1ffa63a0de0 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -77,7 +77,7 @@ suite('AgentSessions', () => { test('should resolve sessions from controllers', async () => { return runWithFakedTimers({}, async () => { - const chatSessionType = 'test-type'; + const chatSessionType = chatSessionTestType; const controller = new StaticChatSessionItemController([ makeSimpleSessionItem('session-1', { label: 'Test Session 1' @@ -93,9 +93,9 @@ suite('AgentSessions', () => { await viewModel.resolve(undefined); assert.strictEqual(viewModel.sessions.length, 2); - assert.strictEqual(viewModel.sessions[0].resource.toString(), 'test://session-1'); + assert.strictEqual(viewModel.sessions[0].resource.toString(), `${chatSessionTestType}://session-1`); assert.strictEqual(viewModel.sessions[0].label, 'Test Session 1'); - assert.strictEqual(viewModel.sessions[1].resource.toString(), 'test://session-2'); + assert.strictEqual(viewModel.sessions[1].resource.toString(), `${chatSessionTestType}://session-2`); assert.strictEqual(viewModel.sessions[1].label, 'Test Session 2'); }); }); @@ -114,8 +114,8 @@ suite('AgentSessions', () => { await viewModel.resolve(undefined); assert.strictEqual(viewModel.sessions.length, 2); - assert.strictEqual(viewModel.sessions[0].resource.toString(), 'test://session-1'); - assert.strictEqual(viewModel.sessions[1].resource.toString(), 'test://session-2'); + assert.strictEqual(viewModel.sessions[0].resource.toString(), `${chatSessionTestType}://session-1`); + assert.strictEqual(viewModel.sessions[1].resource.toString(), `${chatSessionTestType}://session-2`); }); }); @@ -123,7 +123,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); let willResolveFired = false; @@ -150,7 +150,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); let sessionsChangedFired = false; @@ -180,7 +180,7 @@ suite('AgentSessions', () => { changes: { files: 1, insertions: 10, deletions: 5 } }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -241,7 +241,7 @@ suite('AgentSessions', () => { test('should respond to onDidChangeItemsProviders event', async () => { return runWithFakedTimers({}, async () => { - const chatSessionType = 'test-type'; + const chatSessionType = chatSessionTestType; const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); mockChatSessionsService.registerChatSessionItemController(chatSessionType, controller); @@ -263,7 +263,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); @@ -280,15 +280,16 @@ suite('AgentSessions', () => { test('should respond to onDidChangeSessionItems event', async () => { return runWithFakedTimers({}, async () => { - const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); + const testSession = makeSimpleSessionItem('session-1'); + const controller = new StaticChatSessionItemController([testSession]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); // Trigger event - this should automatically call resolve - mockChatSessionsService.fireDidChangeSessionItems('test-type'); + mockChatSessionsService.fireDidChangeSessionItems({ addedOrUpdated: [testSession] }); // Wait for the sessions to be resolved await sessionsChangedPromise; @@ -301,13 +302,13 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); await viewModel.resolve(undefined); assert.strictEqual(viewModel.sessions.length, 1); - assert.strictEqual(viewModel.sessions[0].providerType, 'test-type'); + assert.strictEqual(viewModel.sessions[0].providerType, chatSessionTestType); }); }); @@ -315,7 +316,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -347,7 +348,7 @@ suite('AgentSessions', () => { } ]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -375,7 +376,7 @@ suite('AgentSessions', () => { get items() { return _items; } }; - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -415,7 +416,7 @@ suite('AgentSessions', () => { timing: makeNewSessionTiming() }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -437,7 +438,7 @@ suite('AgentSessions', () => { } }; - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); // Registering calls a refresh initially assert.strictEqual(controllerCallCount, 1); @@ -669,7 +670,7 @@ suite('AgentSessions', () => { function createSession(overrides: Partial = {}): IAgentSession { return { - providerType: 'test-type', + providerType: chatSessionTestType, providerLabel: 'Test Provider', icon: Codicon.chatSparkle, resource: URI.parse('test://session'), @@ -1214,7 +1215,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1236,7 +1237,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1256,7 +1257,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1284,7 +1285,7 @@ suite('AgentSessions', () => { timing: makeNewSessionTiming() }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1303,7 +1304,7 @@ suite('AgentSessions', () => { timing: makeNewSessionTiming() }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1358,7 +1359,7 @@ suite('AgentSessions', () => { timing: futureSessionTiming, }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1379,7 +1380,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1401,7 +1402,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1424,7 +1425,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1455,7 +1456,7 @@ suite('AgentSessions', () => { timing: oldSessionTiming, }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1480,7 +1481,7 @@ suite('AgentSessions', () => { timing: newSessionTiming, }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1506,7 +1507,7 @@ suite('AgentSessions', () => { timing: sessionTiming, }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1532,7 +1533,7 @@ suite('AgentSessions', () => { timing: sessionTiming, }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1557,7 +1558,7 @@ suite('AgentSessions', () => { timing: newSessionTiming, }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1590,7 +1591,7 @@ suite('AgentSessions', () => { timing: newSessionTiming, }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1629,7 +1630,7 @@ suite('AgentSessions', () => { timing: newSessionTiming, }]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1659,7 +1660,7 @@ suite('AgentSessions', () => { lastRequestEnded: Date.UTC(2025, 10 /* November */, 2), }; - const chatSessionType = 'test-type'; + const chatSessionType = chatSessionTestType; const controller = new StaticChatSessionItemController([{ resource: URI.parse('test://old-session'), label: 'Old Session', @@ -1727,7 +1728,7 @@ suite('AgentSessions', () => { get items() { return _items; } }; - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1757,7 +1758,7 @@ suite('AgentSessions', () => { get items() { return _items; } }; - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); await viewModel.resolve(undefined); @@ -1967,7 +1968,7 @@ suite('AgentSessions', () => { return runWithFakedTimers({}, async () => { const controller = new StaticChatSessionItemController([makeSimpleSessionItem('session-1')]); - mockChatSessionsService.registerChatSessionItemController('test-type', controller); + mockChatSessionsService.registerChatSessionItemController(chatSessionTestType, controller); viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); // Set willShutdown to true @@ -2018,9 +2019,11 @@ suite('AgentSessions', () => { }); // End of Agent Sessions suite +const chatSessionTestType = 'test-type'; + function makeSimpleSessionItem(id: string, overrides?: Partial): IChatSessionItem { return { - resource: URI.parse(`test://${id}`), + resource: URI.parse(`${chatSessionTestType}://${id}`), label: `Session ${id}`, timing: makeNewSessionTiming(), ...overrides diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts index 5f64164b246..e8fc5382908 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsController.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { Emitter } from '../../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { observableValue } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -585,14 +585,25 @@ suite('LocalAgentsSessionsController', () => { // Add the session first mockChatService.addSession(mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Test Session', + lastMessageDate: Date.now(), + isActive: true, + timing: createTestTiming(), + lastResponseState: ResponseModelState.Complete + }]); let changeEventCount = 0; disposables.add(controller.onDidChangeChatSessionItems(() => { changeEventCount++; })); + const onDidChangeChatSessionItems = Event.toPromise(controller.onDidChangeChatSessionItems); + // Simulate progress change by triggering the progress listener - mockChatService.triggerProgressEvent(); + mockChatService.triggerProgressEvent(sessionResource); + await onDidChangeChatSessionItems; assert.strictEqual(changeEventCount, 1); }); @@ -611,15 +622,26 @@ suite('LocalAgentsSessionsController', () => { // Add the session first mockChatService.addSession(mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Test Session', + lastMessageDate: Date.now(), + isActive: true, + timing: createTestTiming(), + lastResponseState: ResponseModelState.Complete + }]); let changeEventCount = 0; disposables.add(controller.onDidChangeChatSessionItems(() => { changeEventCount++; })); - // Simulate progress change by triggering the progress listener - mockChatService.triggerProgressEvent(); + const onDidChangeChatSessionItems = Event.toPromise(controller.onDidChangeChatSessionItems); + // Simulate progress change by triggering the progress listener + mockChatService.triggerProgressEvent(sessionResource); + + await onDidChangeChatSessionItems; assert.strictEqual(changeEventCount, 1); }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index e268a3a137e..b5912d5e4c1 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../../base/common/map.js'; import { ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ChatRequestQueueKind, ChatSendResult, IChatDetail, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../../common/chatService/chatService.js'; @@ -22,7 +23,7 @@ export class MockChatService implements IChatService { readonly onDidSubmitRequest = Event.None; readonly onDidCreateModel = Event.None; - private sessions = new Map(); + private readonly sessions = new ResourceMap(); private liveSessionItems: IChatDetail[] = []; private historySessionItems: IChatDetail[] = []; @@ -50,13 +51,13 @@ export class MockChatService implements IChatService { } addSession(session: IChatModel): void { - this.sessions.set(session.sessionResource.toString(), session); + this.sessions.set(session.sessionResource, session); // Update the chatModels observable this._chatModels.set([...this.sessions.values()], undefined); } removeSession(sessionResource: URI): void { - this.sessions.delete(sessionResource.toString()); + this.sessions.delete(sessionResource); // Update the chatModels observable this._chatModels.set([...this.sessions.values()], undefined); } @@ -78,7 +79,7 @@ export class MockChatService implements IChatService { } getSession(sessionResource: URI): IChatModel | undefined { - return this.sessions.get(sessionResource.toString()); + return this.sessions.get(sessionResource); } getLatestRequest(): IChatRequestModel | undefined { @@ -190,9 +191,9 @@ export class MockChatService implements IChatService { } - private onChange?: () => void; + private onChange?: (sessionResource: URI) => void; - registerChatModelChangeListeners(chatSessionType: string, onChange: () => void): IDisposable { + registerChatModelChangeListeners(chatSessionType: string, onChange: (sessionResource: URI) => void): IDisposable { // Store the emitter so tests can trigger it this.onChange = onChange; return { @@ -203,9 +204,7 @@ export class MockChatService implements IChatService { } // Helper method for tests to trigger progress events - triggerProgressEvent(): void { - if (this.onChange) { - this.onChange(); - } + triggerProgressEvent(sessionResource: URI): void { + this.onChange?.(sessionResource); } } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index 883337c04ee..754091ae0c1 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -11,7 +11,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { IChatAgentAttachmentCapabilities } from '../../common/participants/chatAgents.js'; import { IChatModel } from '../../common/model/chatModel.js'; -import { IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionItemController, IChatSessionItem, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; +import { IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionItemController, IChatSessionItem, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, ResolvedChatSessionsExtensionPoint, IChatSessionItemsDelta } from '../../common/chatSessionsService.js'; import { Target } from '../../common/promptSyntax/promptTypes.js'; export class MockChatSessionsService implements IChatSessionsService { @@ -22,7 +22,7 @@ export class MockChatSessionsService implements IChatSessionsService { private readonly _onDidChangeItemsProviders = new Emitter<{ readonly chatSessionType: string }>(); readonly onDidChangeItemsProviders = this._onDidChangeItemsProviders.event; - private readonly _onDidChangeSessionItems = new Emitter<{ readonly chatSessionType: string }>(); + private readonly _onDidChangeSessionItems = new Emitter(); readonly onDidChangeSessionItems = this._onDidChangeSessionItems.event; private readonly _onDidChangeAvailability = new Emitter(); @@ -52,8 +52,8 @@ export class MockChatSessionsService implements IChatSessionsService { this._onDidChangeItemsProviders.fire(event); } - fireDidChangeSessionItems(chatSessionType: string): void { - this._onDidChangeSessionItems.fire({ chatSessionType }); + fireDidChangeSessionItems(event: IChatSessionItemsDelta): void { + this._onDidChangeSessionItems.fire(event); } fireDidChangeAvailability(): void {