Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/agentSessions/localAgentSessionsController.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { coalesce } from '../../../../../base/common/arrays.js';
7
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { Codicon } from '../../../../../base/common/codicons.js';
9
import { Emitter } from '../../../../../base/common/event.js';
10
import { Disposable, DisposableResourceMap } from '../../../../../base/common/lifecycle.js';
11
import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js';
12
import { equals } from '../../../../../base/common/objects.js';
13
import { autorun, observableSignalFromEvent } from '../../../../../base/common/observable.js';
14
import { isEqual } from '../../../../../base/common/resources.js';
15
import { URI } from '../../../../../base/common/uri.js';
16
import { IWorkbenchContribution } from '../../../../common/contributions.js';
17
import { convertLegacyChatSessionTiming, IChatDetail, IChatService, IChatSessionTiming } from '../../common/chatService/chatService.js';
18
import { chatModelToChatDetail } from '../../common/chatService/chatServiceImpl.js';
19
import { ChatSessionStatus, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';
20
import { IChatModel } from '../../common/model/chatModel.js';
21
import { getChatSessionType } from '../../common/model/chatUri.js';
22
import { getInProgressSessionDescription } from '../chatSessions/chatSessionDescription.js';
23
import { chatResponseStateToSessionStatus, getSessionStatusForModel } from '../chatSessions/chatSessions.contribution.js';
24
25
export class LocalAgentsSessionsController extends Disposable implements IChatSessionItemController, IWorkbenchContribution {
26
27
static readonly ID = 'workbench.contrib.localAgentsSessionsController';
28
29
readonly chatSessionType = localChatSessionType;
30
31
readonly _onDidChangeChatSessionItems = this._register(new Emitter<IChatSessionItemsDelta>());
32
readonly onDidChangeChatSessionItems = this._onDidChangeChatSessionItems.event;
33
34
private readonly _modelListeners = this._register(new DisposableResourceMap());
35
36
private _isDisposed = false;
37
38
constructor(
39
@IChatService private readonly chatService: IChatService,
40
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
41
) {
42
super();
43
44
this._register(this.chatSessionsService.registerChatSessionItemController(this.chatSessionType, this));
45
46
this.registerListeners();
47
}
48
49
override dispose(): void {
50
this._isDisposed = true;
51
super.dispose();
52
}
53
54
private _items = new ResourceMap<LocalChatSessionItem>();
55
get items(): readonly IChatSessionItem[] {
56
return Array.from(this._items.values());
57
}
58
59
async refresh(token: CancellationToken): Promise<void> {
60
const newItems = await this.provideChatSessionItems(token);
61
62
this._items.clear();
63
for (const item of newItems) {
64
this._items.set(item.resource, item);
65
}
66
}
67
68
private registerListeners(): void {
69
const addModelListeners = async (model: IChatModel) => {
70
if (getChatSessionType(model.sessionResource) !== this.chatSessionType) {
71
return;
72
}
73
74
await this.refresh(CancellationToken.None);
75
if (this._isDisposed) {
76
return;
77
}
78
79
this.tryUpdateLiveSessionItem(model);
80
81
const requestChangeListener = model.lastRequestObs.map(last => last?.response && observableSignalFromEvent('chatSessions.modelRequestChangeListener', last.response.onDidChange));
82
const modelChangeListener = observableSignalFromEvent('chatSessions.modelChangeListener', model.onDidChange);
83
this._modelListeners.set(model.sessionResource, autorun(reader => {
84
requestChangeListener.read(reader)?.read(reader);
85
modelChangeListener.read(reader);
86
87
this.tryUpdateLiveSessionItem(model);
88
}));
89
};
90
91
this._register(this.chatService.onDidCreateModel(model => addModelListeners(model)));
92
for (const model of this.chatService.chatModels.get()) {
93
addModelListeners(model);
94
}
95
96
this._register(this.chatService.onDidDisposeSession(e => {
97
for (const sessionResource of e.sessionResources) {
98
this._modelListeners.deleteAndDispose(sessionResource);
99
}
100
101
const removedSessionResources = e.sessionResources.filter(resource => getChatSessionType(resource) === this.chatSessionType);
102
if (removedSessionResources.length) {
103
this._onDidChangeChatSessionItems.fire({ removed: removedSessionResources });
104
}
105
}));
106
}
107
108
private async tryUpdateLiveSessionItem(model: IChatModel): Promise<void> {
109
const existing = this._items.get(model.sessionResource);
110
if (!existing) {
111
return;
112
}
113
114
const updated = new LocalChatSessionItem(await chatModelToChatDetail(model), model);
115
if (existing.isEqual(updated)) {
116
return;
117
}
118
119
this._items.set(existing.resource, updated);
120
this._onDidChangeChatSessionItems.fire({ addedOrUpdated: [updated] });
121
}
122
123
private async provideChatSessionItems(token: CancellationToken): Promise<LocalChatSessionItem[]> {
124
const sessions: LocalChatSessionItem[] = [];
125
const sessionsByResource = new ResourceSet();
126
127
for (const sessionDetail of await this.chatService.getLiveSessionItems()) {
128
const editorSession = this.toChatSessionItem(sessionDetail);
129
if (!editorSession) {
130
continue;
131
}
132
133
sessionsByResource.add(sessionDetail.sessionResource);
134
sessions.push(editorSession);
135
}
136
137
if (!token.isCancellationRequested) {
138
const history = await this.getHistoryItems();
139
sessions.push(...history.filter(historyItem => !sessionsByResource.has(historyItem.resource)));
140
}
141
142
return sessions;
143
}
144
145
private async getHistoryItems(): Promise<LocalChatSessionItem[]> {
146
try {
147
const historyItems = await this.chatService.getHistorySessionItems();
148
149
return coalesce(historyItems.map(history => this.toChatSessionItem(history)));
150
} catch (error) {
151
return [];
152
}
153
}
154
155
private toChatSessionItem(chat: IChatDetail): LocalChatSessionItem | undefined {
156
const model = this.chatService.getSession(chat.sessionResource);
157
158
if (model) {
159
if (!model.hasRequests) {
160
return undefined; // ignore sessions without requests
161
}
162
} else if (chat.isActive) {
163
// Sessions that are active but don't have a chat model are ultimately untitled with no requests
164
return undefined;
165
}
166
167
return new LocalChatSessionItem(chat, model);
168
}
169
}
170
171
class LocalChatSessionItem implements IChatSessionItem {
172
readonly resource: URI;
173
readonly iconPath = Codicon.chatSparkle;
174
175
readonly label: string;
176
readonly description: string | undefined;
177
readonly status: ChatSessionStatus | undefined;
178
readonly timing: IChatSessionTiming;
179
readonly changes: IChatSessionItem['changes'];
180
181
constructor(chatDetail: IChatDetail, model: IChatModel | undefined) {
182
this.resource = chatDetail.sessionResource;
183
this.label = chatDetail.title;
184
this.description = model ? getInProgressSessionDescription(model) : undefined;
185
this.status = (model && getSessionStatusForModel(model)) ?? chatResponseStateToSessionStatus(chatDetail.lastResponseState);
186
this.timing = convertLegacyChatSessionTiming(chatDetail.timing);
187
this.changes = chatDetail.stats ? {
188
insertions: chatDetail.stats.added,
189
deletions: chatDetail.stats.removed,
190
files: chatDetail.stats.fileCount,
191
} : undefined;
192
}
193
194
isEqual(other: LocalChatSessionItem): boolean {
195
return isEqual(this.resource, other.resource)
196
&& this.label === other.label
197
&& this.description === other.description
198
&& this.status === other.status
199
&& this.timing.created === other.timing.created
200
&& this.timing.lastRequestStarted === other.timing.lastRequestStarted
201
&& this.timing.lastRequestEnded === other.timing.lastRequestEnded
202
&& equals(this.changes, other.changes);
203
}
204
}
205
206