Path: blob/main/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { coalesce } from '../../../../../base/common/arrays.js';6import { CancellationToken } from '../../../../../base/common/cancellation.js';7import { Codicon } from '../../../../../base/common/codicons.js';8import { Emitter } from '../../../../../base/common/event.js';9import { Disposable, DisposableResourceMap } from '../../../../../base/common/lifecycle.js';10import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js';11import { equals } from '../../../../../base/common/objects.js';12import { autorun, observableSignalFromEvent } from '../../../../../base/common/observable.js';13import { isEqual } from '../../../../../base/common/resources.js';14import { URI } from '../../../../../base/common/uri.js';15import { IWorkbenchContribution } from '../../../../common/contributions.js';16import { convertLegacyChatSessionTiming, IChatDetail, IChatService, IChatSessionTiming } from '../../common/chatService/chatService.js';17import { chatModelToChatDetail } from '../../common/chatService/chatServiceImpl.js';18import { ChatSessionStatus, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';19import { IChatModel } from '../../common/model/chatModel.js';20import { getChatSessionType } from '../../common/model/chatUri.js';21import { getInProgressSessionDescription } from '../chatSessions/chatSessionDescription.js';22import { chatResponseStateToSessionStatus, getSessionStatusForModel } from '../chatSessions/chatSessions.contribution.js';2324export class LocalAgentsSessionsController extends Disposable implements IChatSessionItemController, IWorkbenchContribution {2526static readonly ID = 'workbench.contrib.localAgentsSessionsController';2728readonly chatSessionType = localChatSessionType;2930readonly _onDidChangeChatSessionItems = this._register(new Emitter<IChatSessionItemsDelta>());31readonly onDidChangeChatSessionItems = this._onDidChangeChatSessionItems.event;3233private readonly _modelListeners = this._register(new DisposableResourceMap());3435private _isDisposed = false;3637constructor(38@IChatService private readonly chatService: IChatService,39@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,40) {41super();4243this._register(this.chatSessionsService.registerChatSessionItemController(this.chatSessionType, this));4445this.registerListeners();46}4748override dispose(): void {49this._isDisposed = true;50super.dispose();51}5253private _items = new ResourceMap<LocalChatSessionItem>();54get items(): readonly IChatSessionItem[] {55return Array.from(this._items.values());56}5758async refresh(token: CancellationToken): Promise<void> {59const newItems = await this.provideChatSessionItems(token);6061this._items.clear();62for (const item of newItems) {63this._items.set(item.resource, item);64}65}6667private registerListeners(): void {68const addModelListeners = async (model: IChatModel) => {69if (getChatSessionType(model.sessionResource) !== this.chatSessionType) {70return;71}7273await this.refresh(CancellationToken.None);74if (this._isDisposed) {75return;76}7778this.tryUpdateLiveSessionItem(model);7980const requestChangeListener = model.lastRequestObs.map(last => last?.response && observableSignalFromEvent('chatSessions.modelRequestChangeListener', last.response.onDidChange));81const modelChangeListener = observableSignalFromEvent('chatSessions.modelChangeListener', model.onDidChange);82this._modelListeners.set(model.sessionResource, autorun(reader => {83requestChangeListener.read(reader)?.read(reader);84modelChangeListener.read(reader);8586this.tryUpdateLiveSessionItem(model);87}));88};8990this._register(this.chatService.onDidCreateModel(model => addModelListeners(model)));91for (const model of this.chatService.chatModels.get()) {92addModelListeners(model);93}9495this._register(this.chatService.onDidDisposeSession(e => {96for (const sessionResource of e.sessionResources) {97this._modelListeners.deleteAndDispose(sessionResource);98}99100const removedSessionResources = e.sessionResources.filter(resource => getChatSessionType(resource) === this.chatSessionType);101if (removedSessionResources.length) {102this._onDidChangeChatSessionItems.fire({ removed: removedSessionResources });103}104}));105}106107private async tryUpdateLiveSessionItem(model: IChatModel): Promise<void> {108const existing = this._items.get(model.sessionResource);109if (!existing) {110return;111}112113const updated = new LocalChatSessionItem(await chatModelToChatDetail(model), model);114if (existing.isEqual(updated)) {115return;116}117118this._items.set(existing.resource, updated);119this._onDidChangeChatSessionItems.fire({ addedOrUpdated: [updated] });120}121122private async provideChatSessionItems(token: CancellationToken): Promise<LocalChatSessionItem[]> {123const sessions: LocalChatSessionItem[] = [];124const sessionsByResource = new ResourceSet();125126for (const sessionDetail of await this.chatService.getLiveSessionItems()) {127const editorSession = this.toChatSessionItem(sessionDetail);128if (!editorSession) {129continue;130}131132sessionsByResource.add(sessionDetail.sessionResource);133sessions.push(editorSession);134}135136if (!token.isCancellationRequested) {137const history = await this.getHistoryItems();138sessions.push(...history.filter(historyItem => !sessionsByResource.has(historyItem.resource)));139}140141return sessions;142}143144private async getHistoryItems(): Promise<LocalChatSessionItem[]> {145try {146const historyItems = await this.chatService.getHistorySessionItems();147148return coalesce(historyItems.map(history => this.toChatSessionItem(history)));149} catch (error) {150return [];151}152}153154private toChatSessionItem(chat: IChatDetail): LocalChatSessionItem | undefined {155const model = this.chatService.getSession(chat.sessionResource);156157if (model) {158if (!model.hasRequests) {159return undefined; // ignore sessions without requests160}161} else if (chat.isActive) {162// Sessions that are active but don't have a chat model are ultimately untitled with no requests163return undefined;164}165166return new LocalChatSessionItem(chat, model);167}168}169170class LocalChatSessionItem implements IChatSessionItem {171readonly resource: URI;172readonly iconPath = Codicon.chatSparkle;173174readonly label: string;175readonly description: string | undefined;176readonly status: ChatSessionStatus | undefined;177readonly timing: IChatSessionTiming;178readonly changes: IChatSessionItem['changes'];179180constructor(chatDetail: IChatDetail, model: IChatModel | undefined) {181this.resource = chatDetail.sessionResource;182this.label = chatDetail.title;183this.description = model ? getInProgressSessionDescription(model) : undefined;184this.status = (model && getSessionStatusForModel(model)) ?? chatResponseStateToSessionStatus(chatDetail.lastResponseState);185this.timing = convertLegacyChatSessionTiming(chatDetail.timing);186this.changes = chatDetail.stats ? {187insertions: chatDetail.stats.added,188deletions: chatDetail.stats.removed,189files: chatDetail.stats.fileCount,190} : undefined;191}192193isEqual(other: LocalChatSessionItem): boolean {194return isEqual(this.resource, other.resource)195&& this.label === other.label196&& this.description === other.description197&& this.status === other.status198&& this.timing.created === other.timing.created199&& this.timing.lastRequestStarted === other.timing.lastRequestStarted200&& this.timing.lastRequestEnded === other.timing.lastRequestEnded201&& equals(this.changes, other.changes);202}203}204205206