Path: blob/main/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts
5262 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 { ThrottledDelayer } from '../../../../../base/common/async.js';7import { CancellationToken } from '../../../../../base/common/cancellation.js';8import { Codicon } from '../../../../../base/common/codicons.js';9import { Emitter, Event } from '../../../../../base/common/event.js';10import { IMarkdownString } from '../../../../../base/common/htmlContent.js';11import { Disposable } from '../../../../../base/common/lifecycle.js';12import { ResourceMap } from '../../../../../base/common/map.js';13import { MarshalledId } from '../../../../../base/common/marshallingIds.js';14import { safeStringify } from '../../../../../base/common/objects.js';15import { ThemeIcon } from '../../../../../base/common/themables.js';16import { URI, UriComponents } from '../../../../../base/common/uri.js';17import { localize } from '../../../../../nls.js';18import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';19import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js';20import { IProductService } from '../../../../../platform/product/common/productService.js';21import { Registry } from '../../../../../platform/registry/common/platform.js';22import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';23import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';24import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';25import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js';26import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionFileChange2, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';27import { IChatWidgetService } from '../chat.js';28import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName, isBuiltInAgentSessionProvider } from './agentSessions.js';2930//#region Interfaces, Types3132export { ChatSessionStatus as AgentSessionStatus, isSessionInProgressStatus } from '../../common/chatSessionsService.js';3334export interface IAgentSessionsModel {3536readonly onWillResolve: Event<void>;37readonly onDidResolve: Event<void>;3839readonly onDidChangeSessions: Event<void>;40readonly onDidChangeSessionArchivedState: Event<IAgentSession>;4142readonly resolved: boolean;4344readonly sessions: IAgentSession[];45getSession(resource: URI): IAgentSession | undefined;4647resolve(provider: string | string[] | undefined): Promise<void>;48}4950interface IAgentSessionData extends Omit<IChatSessionItem, 'archived' | 'iconPath'> {5152readonly providerType: string;53readonly providerLabel: string;5455readonly resource: URI;5657readonly status: AgentSessionStatus;5859readonly tooltip?: string | IMarkdownString;6061readonly label: string;62readonly description?: string | IMarkdownString;63readonly badge?: string | IMarkdownString;64readonly icon: ThemeIcon;6566readonly timing: IChatSessionItem['timing'];6768readonly changes?: IChatSessionItem['changes'];69}7071/**72* Checks if the provided changes object represents valid diff information.73*/74export function hasValidDiff(changes: IAgentSession['changes']): boolean {75if (!changes) {76return false;77}7879if (changes instanceof Array) {80return changes.length > 0;81}8283return changes.files > 0 || changes.insertions > 0 || changes.deletions > 0;84}8586/**87* Gets a summary of agent session changes, converting from array format to object format if needed.88*/89export function getAgentChangesSummary(changes: IAgentSession['changes']) {90if (!changes) {91return;92}9394if (!(changes instanceof Array)) {95return changes;96}9798let insertions = 0;99let deletions = 0;100for (const change of changes) {101insertions += change.insertions;102deletions += change.deletions;103}104105return { files: changes.length, insertions, deletions };106}107108export interface IAgentSession extends IAgentSessionData {109isArchived(): boolean;110setArchived(archived: boolean): void;111112isRead(): boolean;113setRead(read: boolean): void;114}115116interface IInternalAgentSessionData extends IAgentSessionData {117118/**119* The `archived` property is provided by the session provider120* and will be used as the initial value if the user has not121* changed the archived state for the session previously. It122* is kept internal to not expose it publicly. Use `isArchived()`123* and `setArchived()` methods instead.124*/125readonly archived: boolean | undefined;126}127128interface IInternalAgentSession extends IAgentSession, IInternalAgentSessionData { }129130export function isLocalAgentSessionItem(session: IAgentSession): boolean {131return session.providerType === AgentSessionProviders.Local;132}133134export function isAgentSession(obj: unknown): obj is IAgentSession {135const session = obj as IAgentSession | undefined;136137return URI.isUri(session?.resource) && typeof session.setArchived === 'function' && typeof session.setRead === 'function';138}139140export function isAgentSessionsModel(obj: unknown): obj is IAgentSessionsModel {141const sessionsModel = obj as IAgentSessionsModel | undefined;142143return Array.isArray(sessionsModel?.sessions) && typeof sessionsModel?.getSession === 'function';144}145146interface IAgentSessionState {147readonly archived?: boolean;148readonly read?: number /* last date turned read */;149}150151export const enum AgentSessionSection {152153// Default Grouping (by date)154InProgress = 'inProgress',155Today = 'today',156Yesterday = 'yesterday',157Week = 'week',158Older = 'older',159Archived = 'archived',160161// Capped Grouping162More = 'more',163}164165export interface IAgentSessionSection {166readonly section: AgentSessionSection;167readonly label: string;168readonly sessions: IAgentSession[];169}170171export function isAgentSessionSection(obj: unknown): obj is IAgentSessionSection {172const candidate = obj as IAgentSessionSection;173174return typeof candidate.section === 'string' && Array.isArray(candidate.sessions);175}176177export interface IMarshalledAgentSessionContext {178readonly $mid: MarshalledId.AgentSessionContext;179180readonly session: IAgentSession;181readonly sessions: IAgentSession[]; // support for multi-selection182}183184export function isMarshalledAgentSessionContext(thing: unknown): thing is IMarshalledAgentSessionContext {185if (typeof thing === 'object' && thing !== null) {186const candidate = thing as IMarshalledAgentSessionContext;187return candidate.$mid === MarshalledId.AgentSessionContext && typeof candidate.session === 'object' && candidate.session !== null;188}189190return false;191}192193//#endregion194195//#region Sessions Logger196197const agentSessionsOutputChannelId = 'agentSessionsOutput';198const agentSessionsOutputChannelLabel = localize('agentSessionsOutput', "Agent Sessions");199200function statusToString(status: AgentSessionStatus): string {201switch (status) {202case AgentSessionStatus.Failed: return 'Failed';203case AgentSessionStatus.Completed: return 'Completed';204case AgentSessionStatus.InProgress: return 'InProgress';205case AgentSessionStatus.NeedsInput: return 'NeedsInput';206default: return `Unknown(${status})`;207}208}209210class AgentSessionsLogger extends Disposable {211212private isChannelRegistered = false;213214constructor(215private readonly getSessionsData: () => {216sessions: Iterable<IInternalAgentSession>;217sessionStates: ResourceMap<IAgentSessionState>;218},219@ILogService private readonly logService: ILogService,220@IOutputService private readonly outputService: IOutputService,221@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService222) {223super();224225this.updateChannelRegistration();226this.registerListeners();227}228229private updateChannelRegistration(): void {230const chatDisabled = this.chatEntitlementService.sentiment.hidden;231232if (chatDisabled && this.isChannelRegistered) {233Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).removeChannel(agentSessionsOutputChannelId);234this.isChannelRegistered = false;235} else if (!chatDisabled && !this.isChannelRegistered) {236Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({237id: agentSessionsOutputChannelId,238label: agentSessionsOutputChannelLabel,239log: false240});241this.isChannelRegistered = true;242}243}244245private registerListeners(): void {246this._register(this.logService.onDidChangeLogLevel(level => {247if (level === LogLevel.Trace) {248this.logAllStatsIfTrace('Log level changed to trace');249}250}));251252this._register(this.chatEntitlementService.onDidChangeSentiment(() => {253this.updateChannelRegistration();254}));255}256257logIfTrace(msg: string): void {258if (this.logService.getLevel() !== LogLevel.Trace) {259return;260}261262this.trace(`[Agent Sessions] ${msg}`);263}264265logAllStatsIfTrace(reason: string): void {266if (this.logService.getLevel() !== LogLevel.Trace) {267return;268}269270this.logAllSessions(reason);271this.logSessionStates();272}273274private logAllSessions(reason: string): void {275const { sessions, sessionStates } = this.getSessionsData();276277const lines: string[] = [];278lines.push(`=== Agent Sessions (${reason}) ===`);279280let count = 0;281for (const session of sessions) {282count++;283const state = sessionStates.get(session.resource);284285lines.push(`--- Session: ${session.label} ---`);286lines.push(` Resource: ${session.resource.toString()}`);287lines.push(` Provider Type: ${session.providerType}`);288lines.push(` Provider Label: ${session.providerLabel}`);289lines.push(` Status: ${statusToString(session.status)}`);290lines.push(` Icon: ${session.icon.id}`);291292if (session.description) {293lines.push(` Description: ${typeof session.description === 'string' ? session.description : session.description.value}`);294}295if (session.badge) {296lines.push(` Badge: ${typeof session.badge === 'string' ? session.badge : session.badge.value}`);297}298if (session.tooltip) {299lines.push(` Tooltip: ${typeof session.tooltip === 'string' ? session.tooltip : session.tooltip.value}`);300}301302// Timing info303lines.push(` Timing:`);304lines.push(` Created: ${session.timing.created ? new Date(session.timing.created).toISOString() : 'N/A'}`);305lines.push(` Last Request Started: ${session.timing.lastRequestStarted ? new Date(session.timing.lastRequestStarted).toISOString() : 'N/A'}`);306lines.push(` Last Request Ended: ${session.timing.lastRequestEnded ? new Date(session.timing.lastRequestEnded).toISOString() : 'N/A'}`);307308// Changes info309if (session.changes) {310const summary = getAgentChangesSummary(session.changes);311if (summary) {312lines.push(` Changes: ${summary.files} files, +${summary.insertions} -${summary.deletions}`);313}314}315316// Our state (read/unread, archived)317lines.push(` State:`);318lines.push(` Archived (provider): ${session.archived ?? 'N/A'}`);319lines.push(` Archived (computed): ${session.isArchived()}`);320lines.push(` Archived (stored): ${state?.archived ?? 'N/A'}`);321lines.push(` Read: ${session.isRead()}`);322lines.push(` Read date (stored): ${state?.read ? new Date(state.read).toISOString() : 'N/A'}`);323324lines.push('');325}326327lines.unshift(`Total sessions: ${count}`, '');328329lines.push(`=== End Agent Sessions ===`);330331this.trace(lines.join('\n'));332}333334private logSessionStates(): void {335const { sessionStates } = this.getSessionsData();336337const lines: string[] = [];338lines.push(`=== Session States ===`);339lines.push(`Total stored states: ${sessionStates.size}`);340lines.push('');341342for (const [resource, state] of sessionStates) {343lines.push(`URI: ${resource.toString()}`);344lines.push(` Archived: ${state.archived}`);345lines.push(` Read: ${state.read ? new Date(state.read).toISOString() : '0 (unread)'}`);346lines.push('');347}348349lines.push(`=== End Session States ===`);350351this.trace(lines.join('\n'));352}353354private trace(msg: string): void {355const channel = this.outputService.getChannel(agentSessionsOutputChannelId);356if (!channel) {357return;358}359360channel.append(`${msg}\n`);361}362}363364//#endregion365366export class AgentSessionsModel extends Disposable implements IAgentSessionsModel {367368private readonly _onWillResolve = this._register(new Emitter<void>());369readonly onWillResolve = this._onWillResolve.event;370371private readonly _onDidResolve = this._register(new Emitter<void>());372readonly onDidResolve = this._onDidResolve.event;373374private readonly _onDidChangeSessions = this._register(new Emitter<void>());375readonly onDidChangeSessions = this._onDidChangeSessions.event;376377private readonly _onDidChangeSessionArchivedState = this._register(new Emitter<IAgentSession>());378readonly onDidChangeSessionArchivedState = this._onDidChangeSessionArchivedState.event;379380private _resolved = false;381get resolved(): boolean { return this._resolved; }382383private _sessions: ResourceMap<IInternalAgentSession>;384get sessions(): IAgentSession[] { return Array.from(this._sessions.values()); }385386private readonly resolver = this._register(new ThrottledDelayer<void>(300));387private readonly providersToResolve = new Set<string | undefined>();388389private readonly cache: AgentSessionsCache;390private readonly logger: AgentSessionsLogger;391392constructor(393@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,394@ILifecycleService private readonly lifecycleService: ILifecycleService,395@IInstantiationService private readonly instantiationService: IInstantiationService,396@IStorageService private readonly storageService: IStorageService,397@IProductService private readonly productService: IProductService,398@IChatWidgetService private readonly chatWidgetService: IChatWidgetService399) {400super();401402this._sessions = new ResourceMap<IInternalAgentSession>();403404this.cache = this.instantiationService.createInstance(AgentSessionsCache);405for (const data of this.cache.loadCachedSessions()) {406const session = this.toAgentSession(data);407this._sessions.set(session.resource, session);408}409this.sessionStates = this.cache.loadSessionStates();410411this.logger = this._register(this.instantiationService.createInstance(412AgentSessionsLogger,413() => ({414sessions: this._sessions.values(),415sessionStates: this.sessionStates,416})417));418this.logger.logAllStatsIfTrace('Loaded cached sessions');419420this.readDateBaseline = this.resolveReadDateBaseline(); // we use this to account for bugfixes in the read/unread tracking421422this.registerListeners();423}424425private registerListeners(): void {426427// Sessions changes428this._register(this.chatSessionsService.onDidChangeItemsProviders(({ chatSessionType }) => this.resolve(chatSessionType)));429this._register(this.chatSessionsService.onDidChangeAvailability(() => this.resolve(undefined)));430this._register(this.chatSessionsService.onDidChangeSessionItems(({ chatSessionType }) => this.updateItems([chatSessionType], CancellationToken.None)));431432// State433this._register(this.storageService.onWillSaveState(() => {434this.cache.saveCachedSessions(Array.from(this._sessions.values()));435this.cache.saveSessionStates(this.sessionStates);436}));437}438439getSession(resource: URI): IAgentSession | undefined {440return this._sessions.get(resource);441}442443async resolve(provider: string | string[] | undefined): Promise<void> {444if (Array.isArray(provider)) {445for (const p of provider) {446this.providersToResolve.add(p);447}448} else {449this.providersToResolve.add(provider);450}451452return this.resolver.trigger(async token => {453if (token.isCancellationRequested || this.lifecycleService.willShutdown) {454return;455}456457try {458this._onWillResolve.fire();459return await this.doResolve(token);460} finally {461this._onDidResolve.fire();462}463});464}465466private async doResolve(token: CancellationToken): Promise<void> {467const providersToResolve = Array.from(this.providersToResolve);468this.providersToResolve.clear();469470const providerFilter = providersToResolve.includes(undefined) ? undefined : coalesce(providersToResolve);471472await this.chatSessionsService.refreshChatSessionItems(providerFilter, token);473await this.updateItems(providerFilter, token);474}475476/**477* Update the sessions by fetching from the service. This does not trigger an explicit refresh478*/479private async updateItems(providerFilter: readonly string[] | undefined, token: CancellationToken): Promise<void> {480const mapSessionContributionToType = new Map<string, IChatSessionsExtensionPoint>();481for (const contribution of this.chatSessionsService.getAllChatSessionContributions()) {482mapSessionContributionToType.set(contribution.type, contribution);483}484485const providerResults = await this.chatSessionsService.getChatSessionItems(providerFilter, token);486487const resolvedProviders = new Set<string>();488const sessions = new ResourceMap<IInternalAgentSession>();489490for (const { chatSessionType, items: providerSessions } of providerResults) {491resolvedProviders.add(chatSessionType);492493if (token.isCancellationRequested) {494return;495}496497for (const session of providerSessions) {498let icon: ThemeIcon;499let providerLabel: string;500const agentSessionProvider = getAgentSessionProvider(chatSessionType);501if (agentSessionProvider !== undefined) {502providerLabel = getAgentSessionProviderName(agentSessionProvider);503icon = getAgentSessionProviderIcon(agentSessionProvider);504} else {505providerLabel = mapSessionContributionToType.get(chatSessionType)?.name ?? chatSessionType;506icon = session.iconPath ?? Codicon.terminal;507}508509const changes = session.changes;510const normalizedChanges = changes && !(changes instanceof Array)511? { files: changes.files, insertions: changes.insertions, deletions: changes.deletions }512: changes;513514sessions.set(session.resource, this.toAgentSession({515providerType: chatSessionType,516providerLabel,517resource: session.resource,518label: session.label.split('\n')[0], // protect against weird multi-line labels that break our layout519description: session.description,520icon,521badge: session.badge,522tooltip: session.tooltip,523status: session.status ?? AgentSessionStatus.Completed,524archived: session.archived,525timing: session.timing,526changes: normalizedChanges,527metadata: session.metadata,528}));529}530}531532for (const [, session] of this._sessions) {533if (!resolvedProviders.has(session.providerType) && (isBuiltInAgentSessionProvider(session.providerType) || mapSessionContributionToType.has(session.providerType))) {534sessions.set(session.resource, session); // fill in existing sessions for providers that did not resolve if they are known or built-in535}536}537538this._sessions = sessions;539this._resolved = true;540541this.logger.logAllStatsIfTrace('Sessions resolved from providers');542543this._onDidChangeSessions.fire();544}545546private toAgentSession(data: IInternalAgentSessionData): IInternalAgentSession {547return {548...data,549isArchived: () => this.isArchived(data),550setArchived: (archived: boolean) => this.setArchived(data, archived),551isRead: () => this.isRead(data),552setRead: (read: boolean) => this.setRead(data, read),553};554}555556//#region States557558private static readonly UNREAD_MARKER = -1;559560private readonly sessionStates: ResourceMap<IAgentSessionState>;561562private isArchived(session: IInternalAgentSessionData): boolean {563return this.sessionStates.get(session.resource)?.archived ?? Boolean(session.archived);564}565566private setArchived(session: IInternalAgentSessionData, archived: boolean): void {567if (archived) {568this.setRead(session, true); // mark as read when archiving569}570571if (archived === this.isArchived(session)) {572return; // no change573}574575const state = this.sessionStates.get(session.resource) ?? {};576this.sessionStates.set(session.resource, { ...state, archived });577578const agentSession = this._sessions.get(session.resource);579if (agentSession) {580this._onDidChangeSessionArchivedState.fire(agentSession);581}582583this._onDidChangeSessions.fire();584}585586private isRead(session: IInternalAgentSessionData): boolean {587if (this.isArchived(session)) {588return true; // archived sessions are always read589}590591const storedReadDate = this.sessionStates.get(session.resource)?.read;592if (storedReadDate === AgentSessionsModel.UNREAD_MARKER) {593return false;594}595596const readDate = Math.max(storedReadDate ?? 0, this.readDateBaseline /* Use read date baseline when no read date is stored */);597598// Install a heuristic to reduce false positives: a user might observe599// the output of a session and quickly click on another session before600// it is finished. Strictly speaking the session is unread, but we601// allow a certain threshold of time to count as read to accommodate.602if (readDate >= this.sessionTimeForReadStateTracking(session) - 2000) {603return true;604}605606// Never consider a session as unread if its connected to a widget607return !!this.chatWidgetService.getWidgetBySessionResource(session.resource);608}609610private sessionTimeForReadStateTracking(session: IInternalAgentSessionData): number {611return session.timing.lastRequestEnded ?? session.timing.created;612}613614private setRead(session: IInternalAgentSessionData, read: boolean, skipEvent?: boolean): void {615const state = this.sessionStates.get(session.resource) ?? {};616617let newRead: number;618if (read) {619newRead = Math.max(Date.now(), this.sessionTimeForReadStateTracking(session));620621if (typeof state.read === 'number' && state.read >= newRead) {622return; // already read with a sufficient timestamp623}624} else {625newRead = AgentSessionsModel.UNREAD_MARKER;626if (state.read === AgentSessionsModel.UNREAD_MARKER) {627return; // already unread628}629}630631this.sessionStates.set(session.resource, { ...state, read: newRead });632633if (!skipEvent) {634this._onDidChangeSessions.fire();635}636}637638private static readonly READ_DATE_BASELINE_KEY = 'agentSessions.readDateBaseline2';639640private readonly readDateBaseline: number;641642private resolveReadDateBaseline(): number {643let readDateBaseline = this.storageService.getNumber(AgentSessionsModel.READ_DATE_BASELINE_KEY, StorageScope.WORKSPACE, 0);644if (readDateBaseline > 0) {645return readDateBaseline; // already resolved646}647648// For stable, preserve unread state for sessions from the last 7 days649// For other qualities, mark all sessions as read650readDateBaseline = this.productService.quality === 'stable'651? Date.now() - (7 * 24 * 60 * 60 * 1000)652: Date.now();653654this.storageService.store(AgentSessionsModel.READ_DATE_BASELINE_KEY, readDateBaseline, StorageScope.WORKSPACE, StorageTarget.MACHINE);655656return readDateBaseline;657}658659//#endregion660}661662//#region Sessions Cache663664interface ISerializedAgentSession {665666readonly providerType: string;667readonly providerLabel: string;668669readonly resource: UriComponents /* old shape */ | string /* new shape that is more compact */;670671readonly status: AgentSessionStatus;672673readonly tooltip?: string | IMarkdownString;674675readonly label: string;676readonly description?: string | IMarkdownString;677readonly badge?: string | IMarkdownString;678readonly icon: string;679680readonly archived: boolean | undefined;681682readonly metadata: { [key: string]: unknown } | undefined;683684readonly timing: {685readonly created: number;686readonly lastRequestStarted?: number;687readonly lastRequestEnded?: number;688// Old format for backward compatibility when reading (TODO@bpasero remove eventually)689readonly startTime?: number;690readonly endTime?: number;691};692693readonly changes?: readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[] | {694readonly files: number;695readonly insertions: number;696readonly deletions: number;697};698}699700interface ISerializedAgentSessionState extends IAgentSessionState {701readonly resource: UriComponents /* old shape */ | string /* new shape that is more compact */;702}703704class AgentSessionsCache {705706private static readonly SESSIONS_STORAGE_KEY = 'agentSessions.model.cache';707private static readonly STATE_STORAGE_KEY = 'agentSessions.state.cache';708709constructor(710@IStorageService private readonly storageService: IStorageService711) { }712713//#region Sessions714715saveCachedSessions(sessions: IInternalAgentSessionData[]): void {716const serialized: ISerializedAgentSession[] = sessions.map(session => ({717providerType: session.providerType,718providerLabel: session.providerLabel,719720resource: session.resource.toString(),721722icon: session.icon.id,723label: session.label,724description: session.description,725badge: session.badge,726tooltip: session.tooltip,727728status: session.status,729archived: session.archived,730731timing: session.timing,732733changes: session.changes,734metadata: session.metadata735} satisfies ISerializedAgentSession));736737this.storageService.store(AgentSessionsCache.SESSIONS_STORAGE_KEY, safeStringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE);738}739740loadCachedSessions(): IInternalAgentSessionData[] {741const sessionsCache = this.storageService.get(AgentSessionsCache.SESSIONS_STORAGE_KEY, StorageScope.WORKSPACE);742if (!sessionsCache) {743return [];744}745746try {747const cached = JSON.parse(sessionsCache) as ISerializedAgentSession[];748return cached.map((session): IInternalAgentSessionData => ({749providerType: session.providerType,750providerLabel: session.providerLabel,751752resource: typeof session.resource === 'string' ? URI.parse(session.resource) : URI.revive(session.resource),753754icon: ThemeIcon.fromId(session.icon),755label: session.label,756description: session.description,757badge: session.badge,758tooltip: session.tooltip,759760status: session.status,761archived: session.archived,762763timing: {764// Support loading both new and old cache formats (TODO@bpasero remove old format support after some time)765created: session.timing.created ?? session.timing.startTime ?? 0,766lastRequestStarted: session.timing.lastRequestStarted ?? session.timing.startTime,767lastRequestEnded: session.timing.lastRequestEnded ?? session.timing.endTime,768},769770changes: Array.isArray(session.changes) ? session.changes.map((change: IChatSessionFileChange) => ({771modifiedUri: URI.revive(change.modifiedUri),772originalUri: change.originalUri ? URI.revive(change.originalUri) : undefined,773insertions: change.insertions,774deletions: change.deletions,775})) : session.changes,776metadata: session.metadata,777}));778} catch {779return []; // invalid data in storage, fallback to empty sessions list780}781}782783//#endregion784785//#region States786787saveSessionStates(states: ResourceMap<IAgentSessionState>): void {788const serialized: ISerializedAgentSessionState[] = Array.from(states.entries()).map(([resource, state]) => ({789resource: resource.toString(),790archived: state.archived,791read: state.read792}));793794this.storageService.store(AgentSessionsCache.STATE_STORAGE_KEY, JSON.stringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE);795}796797loadSessionStates(): ResourceMap<IAgentSessionState> {798const states = new ResourceMap<IAgentSessionState>();799800const statesCache = this.storageService.get(AgentSessionsCache.STATE_STORAGE_KEY, StorageScope.WORKSPACE);801if (!statesCache) {802return states;803}804805try {806const cached = JSON.parse(statesCache) as ISerializedAgentSessionState[];807808for (const entry of cached) {809states.set(typeof entry.resource === 'string' ? URI.parse(entry.resource) : URI.revive(entry.resource), {810archived: entry.archived,811read: entry.read812});813}814} catch {815// invalid data in storage, fallback to empty states816}817818return states;819}820821//#endregion822}823824//#endregion825826827