Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts
5262 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 { ThrottledDelayer } from '../../../../../base/common/async.js';
8
import { CancellationToken } from '../../../../../base/common/cancellation.js';
9
import { Codicon } from '../../../../../base/common/codicons.js';
10
import { Emitter, Event } from '../../../../../base/common/event.js';
11
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
12
import { Disposable } from '../../../../../base/common/lifecycle.js';
13
import { ResourceMap } from '../../../../../base/common/map.js';
14
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
15
import { safeStringify } from '../../../../../base/common/objects.js';
16
import { ThemeIcon } from '../../../../../base/common/themables.js';
17
import { URI, UriComponents } from '../../../../../base/common/uri.js';
18
import { localize } from '../../../../../nls.js';
19
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
20
import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js';
21
import { IProductService } from '../../../../../platform/product/common/productService.js';
22
import { Registry } from '../../../../../platform/registry/common/platform.js';
23
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
24
import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
25
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
26
import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js';
27
import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionFileChange2, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';
28
import { IChatWidgetService } from '../chat.js';
29
import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName, isBuiltInAgentSessionProvider } from './agentSessions.js';
30
31
//#region Interfaces, Types
32
33
export { ChatSessionStatus as AgentSessionStatus, isSessionInProgressStatus } from '../../common/chatSessionsService.js';
34
35
export interface IAgentSessionsModel {
36
37
readonly onWillResolve: Event<void>;
38
readonly onDidResolve: Event<void>;
39
40
readonly onDidChangeSessions: Event<void>;
41
readonly onDidChangeSessionArchivedState: Event<IAgentSession>;
42
43
readonly resolved: boolean;
44
45
readonly sessions: IAgentSession[];
46
getSession(resource: URI): IAgentSession | undefined;
47
48
resolve(provider: string | string[] | undefined): Promise<void>;
49
}
50
51
interface IAgentSessionData extends Omit<IChatSessionItem, 'archived' | 'iconPath'> {
52
53
readonly providerType: string;
54
readonly providerLabel: string;
55
56
readonly resource: URI;
57
58
readonly status: AgentSessionStatus;
59
60
readonly tooltip?: string | IMarkdownString;
61
62
readonly label: string;
63
readonly description?: string | IMarkdownString;
64
readonly badge?: string | IMarkdownString;
65
readonly icon: ThemeIcon;
66
67
readonly timing: IChatSessionItem['timing'];
68
69
readonly changes?: IChatSessionItem['changes'];
70
}
71
72
/**
73
* Checks if the provided changes object represents valid diff information.
74
*/
75
export function hasValidDiff(changes: IAgentSession['changes']): boolean {
76
if (!changes) {
77
return false;
78
}
79
80
if (changes instanceof Array) {
81
return changes.length > 0;
82
}
83
84
return changes.files > 0 || changes.insertions > 0 || changes.deletions > 0;
85
}
86
87
/**
88
* Gets a summary of agent session changes, converting from array format to object format if needed.
89
*/
90
export function getAgentChangesSummary(changes: IAgentSession['changes']) {
91
if (!changes) {
92
return;
93
}
94
95
if (!(changes instanceof Array)) {
96
return changes;
97
}
98
99
let insertions = 0;
100
let deletions = 0;
101
for (const change of changes) {
102
insertions += change.insertions;
103
deletions += change.deletions;
104
}
105
106
return { files: changes.length, insertions, deletions };
107
}
108
109
export interface IAgentSession extends IAgentSessionData {
110
isArchived(): boolean;
111
setArchived(archived: boolean): void;
112
113
isRead(): boolean;
114
setRead(read: boolean): void;
115
}
116
117
interface IInternalAgentSessionData extends IAgentSessionData {
118
119
/**
120
* The `archived` property is provided by the session provider
121
* and will be used as the initial value if the user has not
122
* changed the archived state for the session previously. It
123
* is kept internal to not expose it publicly. Use `isArchived()`
124
* and `setArchived()` methods instead.
125
*/
126
readonly archived: boolean | undefined;
127
}
128
129
interface IInternalAgentSession extends IAgentSession, IInternalAgentSessionData { }
130
131
export function isLocalAgentSessionItem(session: IAgentSession): boolean {
132
return session.providerType === AgentSessionProviders.Local;
133
}
134
135
export function isAgentSession(obj: unknown): obj is IAgentSession {
136
const session = obj as IAgentSession | undefined;
137
138
return URI.isUri(session?.resource) && typeof session.setArchived === 'function' && typeof session.setRead === 'function';
139
}
140
141
export function isAgentSessionsModel(obj: unknown): obj is IAgentSessionsModel {
142
const sessionsModel = obj as IAgentSessionsModel | undefined;
143
144
return Array.isArray(sessionsModel?.sessions) && typeof sessionsModel?.getSession === 'function';
145
}
146
147
interface IAgentSessionState {
148
readonly archived?: boolean;
149
readonly read?: number /* last date turned read */;
150
}
151
152
export const enum AgentSessionSection {
153
154
// Default Grouping (by date)
155
InProgress = 'inProgress',
156
Today = 'today',
157
Yesterday = 'yesterday',
158
Week = 'week',
159
Older = 'older',
160
Archived = 'archived',
161
162
// Capped Grouping
163
More = 'more',
164
}
165
166
export interface IAgentSessionSection {
167
readonly section: AgentSessionSection;
168
readonly label: string;
169
readonly sessions: IAgentSession[];
170
}
171
172
export function isAgentSessionSection(obj: unknown): obj is IAgentSessionSection {
173
const candidate = obj as IAgentSessionSection;
174
175
return typeof candidate.section === 'string' && Array.isArray(candidate.sessions);
176
}
177
178
export interface IMarshalledAgentSessionContext {
179
readonly $mid: MarshalledId.AgentSessionContext;
180
181
readonly session: IAgentSession;
182
readonly sessions: IAgentSession[]; // support for multi-selection
183
}
184
185
export function isMarshalledAgentSessionContext(thing: unknown): thing is IMarshalledAgentSessionContext {
186
if (typeof thing === 'object' && thing !== null) {
187
const candidate = thing as IMarshalledAgentSessionContext;
188
return candidate.$mid === MarshalledId.AgentSessionContext && typeof candidate.session === 'object' && candidate.session !== null;
189
}
190
191
return false;
192
}
193
194
//#endregion
195
196
//#region Sessions Logger
197
198
const agentSessionsOutputChannelId = 'agentSessionsOutput';
199
const agentSessionsOutputChannelLabel = localize('agentSessionsOutput', "Agent Sessions");
200
201
function statusToString(status: AgentSessionStatus): string {
202
switch (status) {
203
case AgentSessionStatus.Failed: return 'Failed';
204
case AgentSessionStatus.Completed: return 'Completed';
205
case AgentSessionStatus.InProgress: return 'InProgress';
206
case AgentSessionStatus.NeedsInput: return 'NeedsInput';
207
default: return `Unknown(${status})`;
208
}
209
}
210
211
class AgentSessionsLogger extends Disposable {
212
213
private isChannelRegistered = false;
214
215
constructor(
216
private readonly getSessionsData: () => {
217
sessions: Iterable<IInternalAgentSession>;
218
sessionStates: ResourceMap<IAgentSessionState>;
219
},
220
@ILogService private readonly logService: ILogService,
221
@IOutputService private readonly outputService: IOutputService,
222
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService
223
) {
224
super();
225
226
this.updateChannelRegistration();
227
this.registerListeners();
228
}
229
230
private updateChannelRegistration(): void {
231
const chatDisabled = this.chatEntitlementService.sentiment.hidden;
232
233
if (chatDisabled && this.isChannelRegistered) {
234
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).removeChannel(agentSessionsOutputChannelId);
235
this.isChannelRegistered = false;
236
} else if (!chatDisabled && !this.isChannelRegistered) {
237
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({
238
id: agentSessionsOutputChannelId,
239
label: agentSessionsOutputChannelLabel,
240
log: false
241
});
242
this.isChannelRegistered = true;
243
}
244
}
245
246
private registerListeners(): void {
247
this._register(this.logService.onDidChangeLogLevel(level => {
248
if (level === LogLevel.Trace) {
249
this.logAllStatsIfTrace('Log level changed to trace');
250
}
251
}));
252
253
this._register(this.chatEntitlementService.onDidChangeSentiment(() => {
254
this.updateChannelRegistration();
255
}));
256
}
257
258
logIfTrace(msg: string): void {
259
if (this.logService.getLevel() !== LogLevel.Trace) {
260
return;
261
}
262
263
this.trace(`[Agent Sessions] ${msg}`);
264
}
265
266
logAllStatsIfTrace(reason: string): void {
267
if (this.logService.getLevel() !== LogLevel.Trace) {
268
return;
269
}
270
271
this.logAllSessions(reason);
272
this.logSessionStates();
273
}
274
275
private logAllSessions(reason: string): void {
276
const { sessions, sessionStates } = this.getSessionsData();
277
278
const lines: string[] = [];
279
lines.push(`=== Agent Sessions (${reason}) ===`);
280
281
let count = 0;
282
for (const session of sessions) {
283
count++;
284
const state = sessionStates.get(session.resource);
285
286
lines.push(`--- Session: ${session.label} ---`);
287
lines.push(` Resource: ${session.resource.toString()}`);
288
lines.push(` Provider Type: ${session.providerType}`);
289
lines.push(` Provider Label: ${session.providerLabel}`);
290
lines.push(` Status: ${statusToString(session.status)}`);
291
lines.push(` Icon: ${session.icon.id}`);
292
293
if (session.description) {
294
lines.push(` Description: ${typeof session.description === 'string' ? session.description : session.description.value}`);
295
}
296
if (session.badge) {
297
lines.push(` Badge: ${typeof session.badge === 'string' ? session.badge : session.badge.value}`);
298
}
299
if (session.tooltip) {
300
lines.push(` Tooltip: ${typeof session.tooltip === 'string' ? session.tooltip : session.tooltip.value}`);
301
}
302
303
// Timing info
304
lines.push(` Timing:`);
305
lines.push(` Created: ${session.timing.created ? new Date(session.timing.created).toISOString() : 'N/A'}`);
306
lines.push(` Last Request Started: ${session.timing.lastRequestStarted ? new Date(session.timing.lastRequestStarted).toISOString() : 'N/A'}`);
307
lines.push(` Last Request Ended: ${session.timing.lastRequestEnded ? new Date(session.timing.lastRequestEnded).toISOString() : 'N/A'}`);
308
309
// Changes info
310
if (session.changes) {
311
const summary = getAgentChangesSummary(session.changes);
312
if (summary) {
313
lines.push(` Changes: ${summary.files} files, +${summary.insertions} -${summary.deletions}`);
314
}
315
}
316
317
// Our state (read/unread, archived)
318
lines.push(` State:`);
319
lines.push(` Archived (provider): ${session.archived ?? 'N/A'}`);
320
lines.push(` Archived (computed): ${session.isArchived()}`);
321
lines.push(` Archived (stored): ${state?.archived ?? 'N/A'}`);
322
lines.push(` Read: ${session.isRead()}`);
323
lines.push(` Read date (stored): ${state?.read ? new Date(state.read).toISOString() : 'N/A'}`);
324
325
lines.push('');
326
}
327
328
lines.unshift(`Total sessions: ${count}`, '');
329
330
lines.push(`=== End Agent Sessions ===`);
331
332
this.trace(lines.join('\n'));
333
}
334
335
private logSessionStates(): void {
336
const { sessionStates } = this.getSessionsData();
337
338
const lines: string[] = [];
339
lines.push(`=== Session States ===`);
340
lines.push(`Total stored states: ${sessionStates.size}`);
341
lines.push('');
342
343
for (const [resource, state] of sessionStates) {
344
lines.push(`URI: ${resource.toString()}`);
345
lines.push(` Archived: ${state.archived}`);
346
lines.push(` Read: ${state.read ? new Date(state.read).toISOString() : '0 (unread)'}`);
347
lines.push('');
348
}
349
350
lines.push(`=== End Session States ===`);
351
352
this.trace(lines.join('\n'));
353
}
354
355
private trace(msg: string): void {
356
const channel = this.outputService.getChannel(agentSessionsOutputChannelId);
357
if (!channel) {
358
return;
359
}
360
361
channel.append(`${msg}\n`);
362
}
363
}
364
365
//#endregion
366
367
export class AgentSessionsModel extends Disposable implements IAgentSessionsModel {
368
369
private readonly _onWillResolve = this._register(new Emitter<void>());
370
readonly onWillResolve = this._onWillResolve.event;
371
372
private readonly _onDidResolve = this._register(new Emitter<void>());
373
readonly onDidResolve = this._onDidResolve.event;
374
375
private readonly _onDidChangeSessions = this._register(new Emitter<void>());
376
readonly onDidChangeSessions = this._onDidChangeSessions.event;
377
378
private readonly _onDidChangeSessionArchivedState = this._register(new Emitter<IAgentSession>());
379
readonly onDidChangeSessionArchivedState = this._onDidChangeSessionArchivedState.event;
380
381
private _resolved = false;
382
get resolved(): boolean { return this._resolved; }
383
384
private _sessions: ResourceMap<IInternalAgentSession>;
385
get sessions(): IAgentSession[] { return Array.from(this._sessions.values()); }
386
387
private readonly resolver = this._register(new ThrottledDelayer<void>(300));
388
private readonly providersToResolve = new Set<string | undefined>();
389
390
private readonly cache: AgentSessionsCache;
391
private readonly logger: AgentSessionsLogger;
392
393
constructor(
394
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
395
@ILifecycleService private readonly lifecycleService: ILifecycleService,
396
@IInstantiationService private readonly instantiationService: IInstantiationService,
397
@IStorageService private readonly storageService: IStorageService,
398
@IProductService private readonly productService: IProductService,
399
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService
400
) {
401
super();
402
403
this._sessions = new ResourceMap<IInternalAgentSession>();
404
405
this.cache = this.instantiationService.createInstance(AgentSessionsCache);
406
for (const data of this.cache.loadCachedSessions()) {
407
const session = this.toAgentSession(data);
408
this._sessions.set(session.resource, session);
409
}
410
this.sessionStates = this.cache.loadSessionStates();
411
412
this.logger = this._register(this.instantiationService.createInstance(
413
AgentSessionsLogger,
414
() => ({
415
sessions: this._sessions.values(),
416
sessionStates: this.sessionStates,
417
})
418
));
419
this.logger.logAllStatsIfTrace('Loaded cached sessions');
420
421
this.readDateBaseline = this.resolveReadDateBaseline(); // we use this to account for bugfixes in the read/unread tracking
422
423
this.registerListeners();
424
}
425
426
private registerListeners(): void {
427
428
// Sessions changes
429
this._register(this.chatSessionsService.onDidChangeItemsProviders(({ chatSessionType }) => this.resolve(chatSessionType)));
430
this._register(this.chatSessionsService.onDidChangeAvailability(() => this.resolve(undefined)));
431
this._register(this.chatSessionsService.onDidChangeSessionItems(({ chatSessionType }) => this.updateItems([chatSessionType], CancellationToken.None)));
432
433
// State
434
this._register(this.storageService.onWillSaveState(() => {
435
this.cache.saveCachedSessions(Array.from(this._sessions.values()));
436
this.cache.saveSessionStates(this.sessionStates);
437
}));
438
}
439
440
getSession(resource: URI): IAgentSession | undefined {
441
return this._sessions.get(resource);
442
}
443
444
async resolve(provider: string | string[] | undefined): Promise<void> {
445
if (Array.isArray(provider)) {
446
for (const p of provider) {
447
this.providersToResolve.add(p);
448
}
449
} else {
450
this.providersToResolve.add(provider);
451
}
452
453
return this.resolver.trigger(async token => {
454
if (token.isCancellationRequested || this.lifecycleService.willShutdown) {
455
return;
456
}
457
458
try {
459
this._onWillResolve.fire();
460
return await this.doResolve(token);
461
} finally {
462
this._onDidResolve.fire();
463
}
464
});
465
}
466
467
private async doResolve(token: CancellationToken): Promise<void> {
468
const providersToResolve = Array.from(this.providersToResolve);
469
this.providersToResolve.clear();
470
471
const providerFilter = providersToResolve.includes(undefined) ? undefined : coalesce(providersToResolve);
472
473
await this.chatSessionsService.refreshChatSessionItems(providerFilter, token);
474
await this.updateItems(providerFilter, token);
475
}
476
477
/**
478
* Update the sessions by fetching from the service. This does not trigger an explicit refresh
479
*/
480
private async updateItems(providerFilter: readonly string[] | undefined, token: CancellationToken): Promise<void> {
481
const mapSessionContributionToType = new Map<string, IChatSessionsExtensionPoint>();
482
for (const contribution of this.chatSessionsService.getAllChatSessionContributions()) {
483
mapSessionContributionToType.set(contribution.type, contribution);
484
}
485
486
const providerResults = await this.chatSessionsService.getChatSessionItems(providerFilter, token);
487
488
const resolvedProviders = new Set<string>();
489
const sessions = new ResourceMap<IInternalAgentSession>();
490
491
for (const { chatSessionType, items: providerSessions } of providerResults) {
492
resolvedProviders.add(chatSessionType);
493
494
if (token.isCancellationRequested) {
495
return;
496
}
497
498
for (const session of providerSessions) {
499
let icon: ThemeIcon;
500
let providerLabel: string;
501
const agentSessionProvider = getAgentSessionProvider(chatSessionType);
502
if (agentSessionProvider !== undefined) {
503
providerLabel = getAgentSessionProviderName(agentSessionProvider);
504
icon = getAgentSessionProviderIcon(agentSessionProvider);
505
} else {
506
providerLabel = mapSessionContributionToType.get(chatSessionType)?.name ?? chatSessionType;
507
icon = session.iconPath ?? Codicon.terminal;
508
}
509
510
const changes = session.changes;
511
const normalizedChanges = changes && !(changes instanceof Array)
512
? { files: changes.files, insertions: changes.insertions, deletions: changes.deletions }
513
: changes;
514
515
sessions.set(session.resource, this.toAgentSession({
516
providerType: chatSessionType,
517
providerLabel,
518
resource: session.resource,
519
label: session.label.split('\n')[0], // protect against weird multi-line labels that break our layout
520
description: session.description,
521
icon,
522
badge: session.badge,
523
tooltip: session.tooltip,
524
status: session.status ?? AgentSessionStatus.Completed,
525
archived: session.archived,
526
timing: session.timing,
527
changes: normalizedChanges,
528
metadata: session.metadata,
529
}));
530
}
531
}
532
533
for (const [, session] of this._sessions) {
534
if (!resolvedProviders.has(session.providerType) && (isBuiltInAgentSessionProvider(session.providerType) || mapSessionContributionToType.has(session.providerType))) {
535
sessions.set(session.resource, session); // fill in existing sessions for providers that did not resolve if they are known or built-in
536
}
537
}
538
539
this._sessions = sessions;
540
this._resolved = true;
541
542
this.logger.logAllStatsIfTrace('Sessions resolved from providers');
543
544
this._onDidChangeSessions.fire();
545
}
546
547
private toAgentSession(data: IInternalAgentSessionData): IInternalAgentSession {
548
return {
549
...data,
550
isArchived: () => this.isArchived(data),
551
setArchived: (archived: boolean) => this.setArchived(data, archived),
552
isRead: () => this.isRead(data),
553
setRead: (read: boolean) => this.setRead(data, read),
554
};
555
}
556
557
//#region States
558
559
private static readonly UNREAD_MARKER = -1;
560
561
private readonly sessionStates: ResourceMap<IAgentSessionState>;
562
563
private isArchived(session: IInternalAgentSessionData): boolean {
564
return this.sessionStates.get(session.resource)?.archived ?? Boolean(session.archived);
565
}
566
567
private setArchived(session: IInternalAgentSessionData, archived: boolean): void {
568
if (archived) {
569
this.setRead(session, true); // mark as read when archiving
570
}
571
572
if (archived === this.isArchived(session)) {
573
return; // no change
574
}
575
576
const state = this.sessionStates.get(session.resource) ?? {};
577
this.sessionStates.set(session.resource, { ...state, archived });
578
579
const agentSession = this._sessions.get(session.resource);
580
if (agentSession) {
581
this._onDidChangeSessionArchivedState.fire(agentSession);
582
}
583
584
this._onDidChangeSessions.fire();
585
}
586
587
private isRead(session: IInternalAgentSessionData): boolean {
588
if (this.isArchived(session)) {
589
return true; // archived sessions are always read
590
}
591
592
const storedReadDate = this.sessionStates.get(session.resource)?.read;
593
if (storedReadDate === AgentSessionsModel.UNREAD_MARKER) {
594
return false;
595
}
596
597
const readDate = Math.max(storedReadDate ?? 0, this.readDateBaseline /* Use read date baseline when no read date is stored */);
598
599
// Install a heuristic to reduce false positives: a user might observe
600
// the output of a session and quickly click on another session before
601
// it is finished. Strictly speaking the session is unread, but we
602
// allow a certain threshold of time to count as read to accommodate.
603
if (readDate >= this.sessionTimeForReadStateTracking(session) - 2000) {
604
return true;
605
}
606
607
// Never consider a session as unread if its connected to a widget
608
return !!this.chatWidgetService.getWidgetBySessionResource(session.resource);
609
}
610
611
private sessionTimeForReadStateTracking(session: IInternalAgentSessionData): number {
612
return session.timing.lastRequestEnded ?? session.timing.created;
613
}
614
615
private setRead(session: IInternalAgentSessionData, read: boolean, skipEvent?: boolean): void {
616
const state = this.sessionStates.get(session.resource) ?? {};
617
618
let newRead: number;
619
if (read) {
620
newRead = Math.max(Date.now(), this.sessionTimeForReadStateTracking(session));
621
622
if (typeof state.read === 'number' && state.read >= newRead) {
623
return; // already read with a sufficient timestamp
624
}
625
} else {
626
newRead = AgentSessionsModel.UNREAD_MARKER;
627
if (state.read === AgentSessionsModel.UNREAD_MARKER) {
628
return; // already unread
629
}
630
}
631
632
this.sessionStates.set(session.resource, { ...state, read: newRead });
633
634
if (!skipEvent) {
635
this._onDidChangeSessions.fire();
636
}
637
}
638
639
private static readonly READ_DATE_BASELINE_KEY = 'agentSessions.readDateBaseline2';
640
641
private readonly readDateBaseline: number;
642
643
private resolveReadDateBaseline(): number {
644
let readDateBaseline = this.storageService.getNumber(AgentSessionsModel.READ_DATE_BASELINE_KEY, StorageScope.WORKSPACE, 0);
645
if (readDateBaseline > 0) {
646
return readDateBaseline; // already resolved
647
}
648
649
// For stable, preserve unread state for sessions from the last 7 days
650
// For other qualities, mark all sessions as read
651
readDateBaseline = this.productService.quality === 'stable'
652
? Date.now() - (7 * 24 * 60 * 60 * 1000)
653
: Date.now();
654
655
this.storageService.store(AgentSessionsModel.READ_DATE_BASELINE_KEY, readDateBaseline, StorageScope.WORKSPACE, StorageTarget.MACHINE);
656
657
return readDateBaseline;
658
}
659
660
//#endregion
661
}
662
663
//#region Sessions Cache
664
665
interface ISerializedAgentSession {
666
667
readonly providerType: string;
668
readonly providerLabel: string;
669
670
readonly resource: UriComponents /* old shape */ | string /* new shape that is more compact */;
671
672
readonly status: AgentSessionStatus;
673
674
readonly tooltip?: string | IMarkdownString;
675
676
readonly label: string;
677
readonly description?: string | IMarkdownString;
678
readonly badge?: string | IMarkdownString;
679
readonly icon: string;
680
681
readonly archived: boolean | undefined;
682
683
readonly metadata: { [key: string]: unknown } | undefined;
684
685
readonly timing: {
686
readonly created: number;
687
readonly lastRequestStarted?: number;
688
readonly lastRequestEnded?: number;
689
// Old format for backward compatibility when reading (TODO@bpasero remove eventually)
690
readonly startTime?: number;
691
readonly endTime?: number;
692
};
693
694
readonly changes?: readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[] | {
695
readonly files: number;
696
readonly insertions: number;
697
readonly deletions: number;
698
};
699
}
700
701
interface ISerializedAgentSessionState extends IAgentSessionState {
702
readonly resource: UriComponents /* old shape */ | string /* new shape that is more compact */;
703
}
704
705
class AgentSessionsCache {
706
707
private static readonly SESSIONS_STORAGE_KEY = 'agentSessions.model.cache';
708
private static readonly STATE_STORAGE_KEY = 'agentSessions.state.cache';
709
710
constructor(
711
@IStorageService private readonly storageService: IStorageService
712
) { }
713
714
//#region Sessions
715
716
saveCachedSessions(sessions: IInternalAgentSessionData[]): void {
717
const serialized: ISerializedAgentSession[] = sessions.map(session => ({
718
providerType: session.providerType,
719
providerLabel: session.providerLabel,
720
721
resource: session.resource.toString(),
722
723
icon: session.icon.id,
724
label: session.label,
725
description: session.description,
726
badge: session.badge,
727
tooltip: session.tooltip,
728
729
status: session.status,
730
archived: session.archived,
731
732
timing: session.timing,
733
734
changes: session.changes,
735
metadata: session.metadata
736
} satisfies ISerializedAgentSession));
737
738
this.storageService.store(AgentSessionsCache.SESSIONS_STORAGE_KEY, safeStringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE);
739
}
740
741
loadCachedSessions(): IInternalAgentSessionData[] {
742
const sessionsCache = this.storageService.get(AgentSessionsCache.SESSIONS_STORAGE_KEY, StorageScope.WORKSPACE);
743
if (!sessionsCache) {
744
return [];
745
}
746
747
try {
748
const cached = JSON.parse(sessionsCache) as ISerializedAgentSession[];
749
return cached.map((session): IInternalAgentSessionData => ({
750
providerType: session.providerType,
751
providerLabel: session.providerLabel,
752
753
resource: typeof session.resource === 'string' ? URI.parse(session.resource) : URI.revive(session.resource),
754
755
icon: ThemeIcon.fromId(session.icon),
756
label: session.label,
757
description: session.description,
758
badge: session.badge,
759
tooltip: session.tooltip,
760
761
status: session.status,
762
archived: session.archived,
763
764
timing: {
765
// Support loading both new and old cache formats (TODO@bpasero remove old format support after some time)
766
created: session.timing.created ?? session.timing.startTime ?? 0,
767
lastRequestStarted: session.timing.lastRequestStarted ?? session.timing.startTime,
768
lastRequestEnded: session.timing.lastRequestEnded ?? session.timing.endTime,
769
},
770
771
changes: Array.isArray(session.changes) ? session.changes.map((change: IChatSessionFileChange) => ({
772
modifiedUri: URI.revive(change.modifiedUri),
773
originalUri: change.originalUri ? URI.revive(change.originalUri) : undefined,
774
insertions: change.insertions,
775
deletions: change.deletions,
776
})) : session.changes,
777
metadata: session.metadata,
778
}));
779
} catch {
780
return []; // invalid data in storage, fallback to empty sessions list
781
}
782
}
783
784
//#endregion
785
786
//#region States
787
788
saveSessionStates(states: ResourceMap<IAgentSessionState>): void {
789
const serialized: ISerializedAgentSessionState[] = Array.from(states.entries()).map(([resource, state]) => ({
790
resource: resource.toString(),
791
archived: state.archived,
792
read: state.read
793
}));
794
795
this.storageService.store(AgentSessionsCache.STATE_STORAGE_KEY, JSON.stringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE);
796
}
797
798
loadSessionStates(): ResourceMap<IAgentSessionState> {
799
const states = new ResourceMap<IAgentSessionState>();
800
801
const statesCache = this.storageService.get(AgentSessionsCache.STATE_STORAGE_KEY, StorageScope.WORKSPACE);
802
if (!statesCache) {
803
return states;
804
}
805
806
try {
807
const cached = JSON.parse(statesCache) as ISerializedAgentSessionState[];
808
809
for (const entry of cached) {
810
states.set(typeof entry.resource === 'string' ? URI.parse(entry.resource) : URI.revive(entry.resource), {
811
archived: entry.archived,
812
read: entry.read
813
});
814
}
815
} catch {
816
// invalid data in storage, fallback to empty states
817
}
818
819
return states;
820
}
821
822
//#endregion
823
}
824
825
//#endregion
826
827