diff --git a/extensions/copilot/src/extension/chatSessions/common/chatSessionWorkspaceFolderService.ts b/extensions/copilot/src/extension/chatSessions/common/chatSessionWorkspaceFolderService.ts index 82e0b0316a7..f3e4d58d673 100644 --- a/extensions/copilot/src/extension/chatSessions/common/chatSessionWorkspaceFolderService.ts +++ b/extensions/copilot/src/extension/chatSessions/common/chatSessionWorkspaceFolderService.ts @@ -59,4 +59,6 @@ export interface IChatSessionWorkspaceFolderService { * Returns the affected session IDs. */ clearWorkspaceChanges(folderUri: vscode.Uri): string[]; + + hasCachedChanges(sessionId: string): Promise; } diff --git a/extensions/copilot/src/extension/chatSessions/common/chatSessionWorktreeService.ts b/extensions/copilot/src/extension/chatSessions/common/chatSessionWorktreeService.ts index 6b442ce04ec..e26a50bf2c4 100644 --- a/extensions/copilot/src/extension/chatSessions/common/chatSessionWorktreeService.ts +++ b/extensions/copilot/src/extension/chatSessions/common/chatSessionWorktreeService.ts @@ -75,7 +75,7 @@ export interface IChatSessionWorktreeService { getWorktreeChanges(sessionId: string): Promise; - hasWorktreeChanges(sessionId: string): Promise; + hasCachedChanges(sessionId: string): Promise; handleRequestCompleted(sessionId: string): Promise; diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorkspaceFolderServiceImpl.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorkspaceFolderServiceImpl.ts index 8c7cdd79937..ce6d0acfd45 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorkspaceFolderServiceImpl.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorkspaceFolderServiceImpl.ts @@ -103,6 +103,12 @@ export class ChatSessionWorkspaceFolderService extends Disposable implements ICh this.invalidateSessionCache(sessionId); } + async hasCachedChanges(sessionId: string): Promise { + const existingRepoKey = this.sessionRepoKeys.get(sessionId); + const cachedChanges = existingRepoKey ? this.workspaceFolderChanges.get(existingRepoKey) : undefined; + return !!cachedChanges; + } + async getWorkspaceChanges(sessionId: string): Promise { return this.workspaceChangesSequencer.queue(sessionId, async () => { diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorktreeServiceImpl.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorktreeServiceImpl.ts index 6ff3e2b232c..5c2f599121f 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorktreeServiceImpl.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorktreeServiceImpl.ts @@ -318,7 +318,7 @@ export class ChatSessionWorktreeService extends Disposable implements IChatSessi } } - async hasWorktreeChanges(sessionId: string): Promise { + async hasCachedChanges(sessionId: string): Promise { const worktreeProperties = await this.getWorktreeProperties(sessionId); if (!worktreeProperties || typeof worktreeProperties === 'string') { return false; diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts index 23cad19203e..3b678188f73 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts @@ -131,7 +131,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements private readonly controller: vscode.ChatSessionItemController; private readonly newSessions = new ResourceMap(); - private readonly previouslyCachedChanges = new Map(); constructor( @ICopilotCLISessionService private readonly sessionService: ICopilotCLISessionService, @@ -228,7 +227,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements } this._register(this.sessionService.onDidDeleteSession(async (e) => { controller.items.delete(SessionIdForCLI.getResource(e)); - this.previouslyCachedChanges.delete(e); })); this._register(this.sessionService.onDidChangeSession(async (e) => { const item = await this.toChatSessionItem(e); @@ -318,7 +316,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements if (refreshOptions.reason === 'delete') { const uri = SessionIdForCLI.getResource(refreshOptions.sessionId); this.controller.items.delete(uri); - this.previouslyCachedChanges.delete(refreshOptions.sessionId); } else if (refreshOptions.reason === 'update' && hasKey(refreshOptions, { 'sessionIds': true })) { await Promise.allSettled(refreshOptions.sessionIds.map(async sessionId => { const item = await this.sessionService.getSessionItem(sessionId, CancellationToken.None); @@ -351,8 +348,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements } item.timing = session.timing; item.status = session.status ?? vscode.ChatSessionStatus.Completed; - // This way, when user refreshes everything, they get the cached changes immediately. - item.changes = this.previouslyCachedChanges.get(session.id); // `buildChanges` runs `git diff` and is the slow leg of populating an item. Skip it on the // eager pass and let `resolveChatSessionItem` fill it in lazily for visible items. @@ -372,7 +367,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements } item.changes = changes; - this.previouslyCachedChanges.set(session.id, changes); } if (token.isCancellationRequested) { @@ -420,11 +414,12 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements if (!worktreeProperties?.repositoryPath) { return false; } - const [trusted, available] = await Promise.all([ + const [trusted, hasCachedWorktreeChanges, hasCachedWorkspaceChanges] = await Promise.all([ vscode.workspace.isResourceTrusted(vscode.Uri.file(worktreeProperties.repositoryPath)), - this.copilotCLIWorktreeManagerService.hasWorktreeChanges(sessionId) + this.copilotCLIWorktreeManagerService.hasCachedChanges(sessionId), + this._workspaceFolderService.hasCachedChanges(sessionId) ]); - return trusted && available; + return trusted && (hasCachedWorktreeChanges || hasCachedWorkspaceChanges); } private async buildChanges( diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index 216f4003bd3..62c6e2bb764 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -170,7 +170,6 @@ export class CopilotCLIChatSessionItemProvider extends Disposable implements vsc private readonly _onDidCommitChatSessionItem = this._register(new Emitter<{ original: vscode.ChatSessionItem; modified: vscode.ChatSessionItem }>()); public readonly onDidCommitChatSessionItem: Event<{ original: vscode.ChatSessionItem; modified: vscode.ChatSessionItem }> = this._onDidCommitChatSessionItem.event; - private readonly previouslyCachedChanges = new Map(); public resolveChatSessionItem?: (item: vscode.ChatSessionItem, token: vscode.CancellationToken) => Promise; @@ -297,10 +296,9 @@ export class CopilotCLIChatSessionItemProvider extends Disposable implements vsc // `buildChanges` runs `git diff` and is the slow leg of populating an item. Skip it on the // eager pass and let `resolveChatSessionItem` fill it in lazily for visible items. // But if computing changes is easy (cached or the like), then include them right away to avoid a second update pass. - let changes: vscode.ChatSessionChangedFile[] | undefined = this.previouslyCachedChanges.get(session.id); + let changes: vscode.ChatSessionChangedFile[] | undefined; if (!token.isCancellationRequested && (options?.includeChanges || (await this.canBuildChangesFast(session.id, worktreeProperties)))) { changes = await this.buildChanges(session.id, worktreeProperties, workingDirectory, token); - this.previouslyCachedChanges.set(session.id, changes); // We need to get an updated version of worktree properties here because when the // changes are being computed, the worktree properties are also updated with the @@ -415,11 +413,12 @@ export class CopilotCLIChatSessionItemProvider extends Disposable implements vsc if (!worktreeProperties?.repositoryPath) { return false; } - const [trusted, available] = await Promise.all([ + const [trusted, hasCachedWorktreeChanges, hasCachedWorkspaceChanges] = await Promise.all([ vscode.workspace.isResourceTrusted(vscode.Uri.file(worktreeProperties.repositoryPath)), - this.worktreeManager.hasWorktreeChanges(sessionId) + this.worktreeManager.hasCachedChanges(sessionId), + this.workspaceFolderService.hasCachedChanges(sessionId) ]); - return trusted && available; + return trusted && (hasCachedWorktreeChanges || hasCachedWorkspaceChanges); } diff --git a/extensions/copilot/test/e2e/cli.stest.ts b/extensions/copilot/test/e2e/cli.stest.ts index d0550535c7b..e4c5017f78c 100644 --- a/extensions/copilot/test/e2e/cli.stest.ts +++ b/extensions/copilot/test/e2e/cli.stest.ts @@ -280,6 +280,7 @@ async function registerChatServices(testingServiceCollection: TestingServiceColl async getRepositoryProperties() { return undefined; }, async handleRequestCompleted() { }, async getWorkspaceChanges() { return undefined; }, + async hasCachedChanges() { return false; }, clearWorkspaceChanges() { return []; }, } as IChatSessionWorkspaceFolderService); testingServiceCollection.define(IChatSessionWorktreeService, { @@ -298,7 +299,7 @@ async function registerChatServices(testingServiceCollection: TestingServiceColl async handleRequestCompletedForWorktree() { }, async cleanupWorktreeOnArchive() { return { cleaned: false }; }, async recreateWorktreeOnUnarchive() { return { recreated: false }; }, - async hasWorktreeChanges() { return false; }, + async hasCachedChanges() { return false; }, } as IChatSessionWorktreeService); testingServiceCollection.define(IPromptVariablesService, new SyncDescriptor(NullPromptVariablesService)); testingServiceCollection.define(IChatDebugFileLoggerService, new NullChatDebugFileLoggerService()); diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 6aa6cb99ef3..06dc034f439 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -71,6 +71,8 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'github.copilot.chat.cli.autoCommit.enabled': false, 'github.copilot.chat.cli.branchSupport.enabled': true, 'github.copilot.chat.cli.isolationOption.enabled': true, + 'github.copilot.chat.cli.sessionController.enabled': false, + 'github.copilot.chat.cli.lazyLoadSessionItem.enabled': false, 'github.copilot.chat.cli.mcp.enabled': true, 'github.copilot.chat.cli.remote.enabled': false, 'github.copilot.chat.githubMcpServer.enabled': true, diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index 774a30c6a34..e32f285a867 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -41,6 +41,7 @@ import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { HoverStyle } from '../../../../../base/browser/ui/hover/hover.js'; import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; import { ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js'; +import { IAgentSessionsService } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { ISessionsListModelService } from './sessionsListModelService.js'; import { IAgentHostFilterService } from '../../../remoteAgentHost/common/agentHostFilter.js'; @@ -173,6 +174,7 @@ class SessionItemRenderer implements ITreeRenderer accessor.get(IMarkdownRendererService)); const hoverService = instantiationService.invokeFunction(accessor => accessor.get(IHoverService)); + const agentSessionsService = instantiationService.invokeFunction(accessor => accessor.get(IAgentSessionsService)); const sessionRenderer = new SessionItemRenderer( { grouping: this.options.grouping, sorting: this.options.sorting, isPinned: s => this.isSessionPinned(s), isRead: s => this.isSessionRead(s) }, approvalModel, @@ -724,6 +733,7 @@ export class SessionsList extends Disposable implements ISessionsList { contextKeyService, markdownRendererService, hoverService, + agentSessionsService, ); const showMoreRenderer = new SessionShowMoreRenderer(); diff --git a/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts index cc00fbeccd1..3b4c281795b 100644 --- a/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/services/sessions/browser/sessionsManagementService.ts @@ -11,6 +11,7 @@ import { IContextKey, IContextKeyService } from '../../../../platform/contextkey import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; +import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { ActiveSessionProviderIdContext, ActiveSessionTypeContext, IsActiveSessionArchivedContext, IsActiveSessionBackgroundProviderContext, IsNewChatInSessionContext, IsNewChatSessionContext } from '../../../common/contextkeys.js'; import { ActiveSessionSupportsMultiChatContext, IActiveSession, ISessionsChangeEvent, ISessionsManagementService } from '../common/sessionsManagement.js'; @@ -57,6 +58,7 @@ class SessionsManagementService extends Disposable implements ISessionsManagemen @ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, ) { super(); @@ -380,6 +382,13 @@ class SessionsManagementService extends Disposable implements ISessionsManagemen if (session) { this.logService.info(`[ActiveSessionService] Active session changed: ${session.resource.toString()}`); + + // Trigger lazy resolve for expensive session properties (e.g. changes, + // badge). This is fire-and-forget — the resolve result flows back through + // the model's onDidChangeSessions → _refreshSessionCache → adapter.update() + // chain, updating observables reactively. Safe for providers without a + // resolve handler (returns undefined). + this.agentSessionsService.model.observeSession(session.resource); } else { this.logService.trace('[ActiveSessionService] Active session cleared'); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts index 2b05a00898d..a39d1915dc3 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionHoverWidget.ts @@ -11,6 +11,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { fromNow, getDurationString } from '../../../../../base/common/date.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -21,6 +22,7 @@ import { ChatViewModel } from '../../common/model/chatViewModel.js'; import { IChatWidgetService } from '../chat.js'; import { ChatListWidget } from '../widget/chatListWidget.js'; import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; +import { IAgentSessionsService } from './agentSessionsService.js'; import { AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession } from './agentSessionsModel.js'; import './media/agentSessionHoverWidget.css'; @@ -44,6 +46,7 @@ export class AgentSessionHoverWidget extends Disposable { @IChatService private readonly chatService: IChatService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, ) { super(); @@ -178,21 +181,37 @@ export class AgentSessionHoverWidget extends Disposable { dom.append(detailsRow, dom.$('span', undefined, fromNow(startTime, true, true))); } - // Diff information - const diff = getAgentChangesSummary(session.changes); - if (diff && hasValidDiff(session.changes)) { - dom.append(detailsRow, dom.$('span.separator', undefined, '•')); - const diffContainer = dom.append(detailsRow, dom.$('.agent-session-hover-diff')); - if (diff.files > 0) { - dom.append(diffContainer, dom.$('span', undefined, diff.files === 1 ? localize('tooltip.file', "1 file") : localize('tooltip.files', "{0} files", diff.files))); + // Diff information - rendered reactively because `changes` may be lazily + // resolved by the provider (see IAgentSessionsModel.observeSession). We + // reserve a separator + container slot here and update them whenever the + // observed session emits a fresh value. + const diffSeparator = dom.append(detailsRow, dom.$('span.separator', undefined, '•')); + const diffContainer = dom.append(detailsRow, dom.$('.agent-session-hover-diff')); + diffSeparator.style.display = 'none'; + diffContainer.style.display = 'none'; + + const observed = this.agentSessionsService.model.observeSession(session.resource); + this._register(autorun(reader => { + const latest = observed.read(reader) ?? session; + const diff = getAgentChangesSummary(latest.changes); + dom.clearNode(diffContainer); + if (diff && hasValidDiff(latest.changes)) { + diffSeparator.style.display = ''; + diffContainer.style.display = ''; + if (diff.files > 0) { + dom.append(diffContainer, dom.$('span', undefined, diff.files === 1 ? localize('tooltip.file', "1 file") : localize('tooltip.files', "{0} files", diff.files))); + } + if (diff.insertions > 0) { + dom.append(diffContainer, dom.$('span.insertions', undefined, `+${diff.insertions}`)); + } + if (diff.deletions > 0) { + dom.append(diffContainer, dom.$('span.deletions', undefined, `-${diff.deletions}`)); + } + } else { + diffSeparator.style.display = 'none'; + diffContainer.style.display = 'none'; } - if (diff.insertions > 0) { - dom.append(diffContainer, dom.$('span.insertions', undefined, `+${diff.insertions}`)); - } - if (diff.deletions > 0) { - dom.append(diffContainer, dom.$('span.deletions', undefined, `-${diff.deletions}`)); - } - } + })); // Status (only show if not completed) if (session.status !== AgentSessionStatus.Completed) { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index a23f358a8cf..f28419d86b9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -9,9 +9,10 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; +import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; import { MarshalledId } from '../../../../../base/common/marshallingIds.js'; import { safeStringify } from '../../../../../base/common/objects.js'; +import { derived, IObservable, observableSignalFromEvent } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; @@ -47,6 +48,19 @@ export interface IAgentSessionsModel { readonly sessions: IAgentSession[]; getSession(resource: URI): IAgentSession | undefined; + /** + * Returns an observable that emits the latest {@link IAgentSession} for the + * given resource (or `undefined` if no session is currently known). + * + * The observable updates whenever the underlying session collection changes. + * The first call for a given resource lazily triggers + * {@link IChatSessionsService.resolveChatSessionItem} so consumers reading + * lazy properties (e.g. `changes`) see fresh values once the provider has + * resolved them. In-flight resolves are deduplicated by the chat sessions + * service. + */ + observeSession(resource: URI): IObservable; + resolve(provider: string | string[] | undefined): Promise; } @@ -527,6 +541,35 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode return this._sessions.get(resource); } + private _changedSignal: IObservable | undefined; + private readonly _sessionObservables = new ResourceMap>(); + private readonly _resolvedResources = new ResourceSet(); + + observeSession(resource: URI): IObservable { + let observable = this._sessionObservables.get(resource); + if (!observable) { + // Lazily trigger a resolve for this resource so consumers reading + // lazy properties (e.g. `changes`) get fresh data without needing + // to wait for a tree row to scroll into view. The chat sessions + // service deduplicates in-flight resolves by resource. + if (!this._resolvedResources.has(resource)) { + this._resolvedResources.add(resource); + const sessionType = getChatSessionType(resource); + this.chatSessionsService.resolveChatSessionItem(sessionType, resource, CancellationToken.None) + .catch(error => this.logger.logIfTrace(`observeSession: resolve failed for ${resource.toString()}: ${error instanceof Error ? error.message : String(error)}`)); + } + + this._changedSignal ??= observableSignalFromEvent('agentSessionsChanged', this.onDidChangeSessions); + const signal = this._changedSignal; + observable = derived(reader => { + signal.read(reader); + return this._sessions.get(resource); + }); + this._sessionObservables.set(resource, observable); + } + return observable; + } + async resolve(provider: string | string[] | undefined): Promise { const providers = Array.isArray(provider) ? provider diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 81cc3a4354c..01aa7ab5208 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -51,6 +51,8 @@ import { defaultButtonStyles } from '../../../../../platform/theme/browser/defau import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { compareIgnoreCase } from '../../../../../base/common/strings.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { IChatSessionsService } from '../../common/chatSessionsService.js'; export type AgentSessionListItem = IAgentSession | IAgentSessionSection | IAgentSessionShowMore | IAgentSessionShowLess; @@ -128,6 +130,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre @IHoverService private readonly hoverService: IHoverService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, ) { super(); } @@ -310,6 +313,18 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre if (this._approvalModel) { this.renderApprovalRow(session, template); } + + // Lazily resolve item details (timing, changes, badge, etc.) + this.triggerResolve(session, template); + } + + private triggerResolve(session: ITreeNode, template: IAgentSessionItemTemplate): void { + const cts = new CancellationTokenSource(); + template.elementDisposable.add({ dispose() { cts.dispose(true); } }); + + this.chatSessionsService.resolveChatSessionItem(session.element.providerType, session.element.resource, cts.token).catch(() => { + // Resolve failures are non-fatal — the item continues to display with whatever data is available + }); } private renderBadge(session: ITreeNode, template: IAgentSessionItemTemplate): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts index 8b8e25bb3d1..3b2ef4ca300 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts @@ -166,6 +166,12 @@ class AgentSessionReadyContribution extends Disposable implements IWorkbenchCont return; } + // Trigger a lazy resolve so providers that populate `changes` on + // demand (see IAgentSessionsModel.observeSession) deliver fresh data. + // Re-evaluation happens via the onDidChangeSessions listener in the + // constructor. + this.agentSessionsService.model.observeSession(sessionResource); + // Check if this is a projection-capable provider if (!AGENT_SESSION_PROJECTION_ENABLED_PROVIDERS.has(session.providerType)) { this._clearEntriesWatcher(); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts index b9d98566520..16ca999555b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts @@ -125,12 +125,13 @@ suite('AgentSessionsDataSource', () => { sessions, resolved: true, getSession: () => undefined, + observeSession: () => { throw new Error('Not implemented'); }, onWillResolve: Event.None as Event, onDidResolve: Event.None as Event, onDidChangeSessions: Event.None, onDidChangeSessionArchivedState: Event.None, resolve: async () => { }, - }; + } satisfies IAgentSessionsModel; } function createMockFilter(options: {