mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-03 13:06:06 +01:00
feat(copilot): enable lazy loading for chat session items and update related configurations (#312047)
* feat(copilot): enable lazy loading for chat session items and update related configurations * updates
This commit is contained in:
+2
@@ -59,4 +59,6 @@ export interface IChatSessionWorkspaceFolderService {
|
||||
* Returns the affected session IDs.
|
||||
*/
|
||||
clearWorkspaceChanges(folderUri: vscode.Uri): string[];
|
||||
|
||||
hasCachedChanges(sessionId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export interface IChatSessionWorktreeService {
|
||||
|
||||
getWorktreeChanges(sessionId: string): Promise<readonly vscode.ChatSessionChangedFile[] | undefined>;
|
||||
|
||||
hasWorktreeChanges(sessionId: string): Promise<boolean>;
|
||||
hasCachedChanges(sessionId: string): Promise<boolean>;
|
||||
|
||||
handleRequestCompleted(sessionId: string): Promise<void>;
|
||||
|
||||
|
||||
+6
@@ -103,6 +103,12 @@ export class ChatSessionWorkspaceFolderService extends Disposable implements ICh
|
||||
this.invalidateSessionCache(sessionId);
|
||||
}
|
||||
|
||||
async hasCachedChanges(sessionId: string): Promise<boolean> {
|
||||
const existingRepoKey = this.sessionRepoKeys.get(sessionId);
|
||||
const cachedChanges = existingRepoKey ? this.workspaceFolderChanges.get(existingRepoKey) : undefined;
|
||||
return !!cachedChanges;
|
||||
}
|
||||
|
||||
async getWorkspaceChanges(sessionId: string): Promise<readonly ChatSessionWorktreeFile[] | undefined> {
|
||||
return this.workspaceChangesSequencer.queue(sessionId, async () => {
|
||||
|
||||
|
||||
+1
-1
@@ -318,7 +318,7 @@ export class ChatSessionWorktreeService extends Disposable implements IChatSessi
|
||||
}
|
||||
}
|
||||
|
||||
async hasWorktreeChanges(sessionId: string): Promise<boolean> {
|
||||
async hasCachedChanges(sessionId: string): Promise<boolean> {
|
||||
const worktreeProperties = await this.getWorktreeProperties(sessionId);
|
||||
if (!worktreeProperties || typeof worktreeProperties === 'string') {
|
||||
return false;
|
||||
|
||||
@@ -131,7 +131,6 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
|
||||
|
||||
private readonly controller: vscode.ChatSessionItemController;
|
||||
private readonly newSessions = new ResourceMap<vscode.ChatSessionItem>();
|
||||
private readonly previouslyCachedChanges = new Map<string, vscode.ChatSessionChangedFile[]>();
|
||||
|
||||
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(
|
||||
|
||||
+5
-6
@@ -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<string, vscode.ChatSessionChangedFile[]>();
|
||||
|
||||
|
||||
public resolveChatSessionItem?: (item: vscode.ChatSessionItem, token: vscode.CancellationToken) => Promise<vscode.ChatSessionItem | undefined>;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -71,6 +71,8 @@ Registry.as<IConfigurationRegistry>(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,
|
||||
|
||||
@@ -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<SessionListItem, FuzzyScore,
|
||||
private readonly contextKeyService: IContextKeyService,
|
||||
private readonly markdownRendererService: IMarkdownRendererService,
|
||||
private readonly hoverService: IHoverService,
|
||||
private readonly agentSessionsService: IAgentSessionsService,
|
||||
) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): ISessionItemTemplate {
|
||||
@@ -213,6 +215,12 @@ class SessionItemRenderer implements ITreeRenderer<SessionListItem, FuzzyScore,
|
||||
private renderSession(element: ISession, template: ISessionItemTemplate, matches?: IMatch[]): void {
|
||||
template.elementDisposables.clear();
|
||||
|
||||
// Trigger lazy resolve for expensive session properties (e.g. changes)
|
||||
// so that providers which populate them on demand deliver fresh data
|
||||
// by the time the row renders. Only fires for sessions that become
|
||||
// visible in the viewport (O(visible rows), not O(all sessions)).
|
||||
this.agentSessionsService.model.observeSession(element.resource);
|
||||
|
||||
// Toolbar context
|
||||
template.titleToolbar.context = element;
|
||||
|
||||
@@ -717,6 +725,7 @@ export class SessionsList extends Disposable implements ISessionsList {
|
||||
const approvalModel = this._register(instantiationService.createInstance(AgentSessionApprovalModel));
|
||||
const markdownRendererService = instantiationService.invokeFunction(accessor => 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();
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<IAgentSession | undefined>;
|
||||
|
||||
resolve(provider: string | string[] | undefined): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -527,6 +541,35 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode
|
||||
return this._sessions.get(resource);
|
||||
}
|
||||
|
||||
private _changedSignal: IObservable<void> | undefined;
|
||||
private readonly _sessionObservables = new ResourceMap<IObservable<IAgentSession | undefined>>();
|
||||
private readonly _resolvedResources = new ResourceSet();
|
||||
|
||||
observeSession(resource: URI): IObservable<IAgentSession | undefined> {
|
||||
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<void> {
|
||||
const providers = Array.isArray(provider)
|
||||
? provider
|
||||
|
||||
@@ -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<IAgentSession, FuzzyScore>, 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<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): boolean {
|
||||
|
||||
+6
@@ -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();
|
||||
|
||||
+2
-1
@@ -125,12 +125,13 @@ suite('AgentSessionsDataSource', () => {
|
||||
sessions,
|
||||
resolved: true,
|
||||
getSession: () => undefined,
|
||||
observeSession: () => { throw new Error('Not implemented'); },
|
||||
onWillResolve: Event.None as Event<string>,
|
||||
onDidResolve: Event.None as Event<string>,
|
||||
onDidChangeSessions: Event.None,
|
||||
onDidChangeSessionArchivedState: Event.None,
|
||||
resolve: async () => { },
|
||||
};
|
||||
} satisfies IAgentSessionsModel;
|
||||
}
|
||||
|
||||
function createMockFilter(options: {
|
||||
|
||||
Reference in New Issue
Block a user