rebuild agentStatusWidget (#288192)

* rebuild agentStatusWidget

* Update src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Josh Spicer
2026-01-16 00:30:02 +00:00
committed by GitHub
co-authored by Copilot
parent 2baec29920
commit 94bd7d47f9
3 changed files with 397 additions and 104 deletions
@@ -17,7 +17,7 @@ import { ICommandService } from '../../../../../platform/commands/common/command
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
import { ExitAgentSessionProjectionAction } from './agentSessionProjectionActions.js';
import { IAgentSessionsService } from './agentSessionsService.js';
import { IAgentSession, isSessionInProgressStatus } from './agentSessionsModel.js';
import { AgentSessionStatus, IAgentSession, isSessionInProgressStatus } from './agentSessionsModel.js';
import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';
import { IAction } from '../../../../../base/common/actions.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
@@ -55,6 +55,9 @@ export class AgentStatusWidget extends BaseActionViewItem {
/** The currently displayed in-progress session (if any) - clicking pill opens this */
private _displayedSession: IAgentSession | undefined;
/** Cached render state to avoid unnecessary DOM rebuilds */
private _lastRenderState: string | undefined;
constructor(
action: IAction,
options: IBaseActionViewItemOptions | undefined,
@@ -113,6 +116,45 @@ export class AgentStatusWidget extends BaseActionViewItem {
return;
}
// Compute current render state to avoid unnecessary DOM rebuilds
const mode = this.agentStatusService.mode;
const sessionInfo = this.agentStatusService.sessionInfo;
const { activeSessions, unreadSessions, attentionNeededSessions } = this._getSessionStats();
// Get attention session info for state computation
const attentionSession = attentionNeededSessions.length > 0
? [...attentionNeededSessions].sort((a, b) => {
const timeA = a.timing.lastRequestStarted ?? a.timing.created;
const timeB = b.timing.lastRequestStarted ?? b.timing.created;
return timeB - timeA;
})[0]
: undefined;
const attentionText = attentionSession?.description
? (typeof attentionSession.description === 'string'
? attentionSession.description
: renderAsPlaintext(attentionSession.description))
: attentionSession?.label;
const label = this._getLabel();
// Build state key for comparison
const stateKey = JSON.stringify({
mode,
sessionTitle: sessionInfo?.title,
activeCount: activeSessions.length,
unreadCount: unreadSessions.length,
attentionCount: attentionNeededSessions.length,
attentionText,
label,
});
// Skip re-render if state hasn't changed
if (this._lastRenderState === stateKey) {
return;
}
this._lastRenderState = stateKey;
// Clear existing content
reset(this._container);
@@ -128,80 +170,113 @@ export class AgentStatusWidget extends BaseActionViewItem {
}
}
// #region Session Statistics
/**
* Get computed session statistics for rendering.
*/
private _getSessionStats(): {
activeSessions: IAgentSession[];
unreadSessions: IAgentSession[];
attentionNeededSessions: IAgentSession[];
hasActiveSessions: boolean;
hasUnreadSessions: boolean;
hasAttentionNeeded: boolean;
} {
const sessions = this.agentSessionsService.model.sessions;
const activeSessions = sessions.filter(s => isSessionInProgressStatus(s.status));
const unreadSessions = sessions.filter(s => !s.isRead());
// Sessions that need user attention (approval/confirmation/input)
const attentionNeededSessions = sessions.filter(s => s.status === AgentSessionStatus.NeedsInput);
return {
activeSessions,
unreadSessions,
attentionNeededSessions,
hasActiveSessions: activeSessions.length > 0,
hasUnreadSessions: unreadSessions.length > 0,
hasAttentionNeeded: attentionNeededSessions.length > 0,
};
}
// #endregion
// #region Mode Renderers
private _renderChatInputMode(disposables: DisposableStore): void {
if (!this._container) {
return;
}
// Get agent session statistics
const sessions = this.agentSessionsService.model.sessions;
const activeSessions = sessions.filter(s => isSessionInProgressStatus(s.status));
const unreadSessions = sessions.filter(s => !s.isRead());
const hasActiveSessions = activeSessions.length > 0;
const hasUnreadSessions = unreadSessions.length > 0;
const { activeSessions, unreadSessions, attentionNeededSessions, hasAttentionNeeded } = this._getSessionStats();
// Create pill - add 'has-active' class when sessions are in progress
// Create pill
const pill = $('div.agent-status-pill.chat-input-mode');
if (hasActiveSessions) {
pill.classList.add('has-active');
} else if (hasUnreadSessions) {
pill.classList.add('has-unread');
if (hasAttentionNeeded) {
pill.classList.add('needs-attention');
}
pill.setAttribute('role', 'button');
pill.setAttribute('aria-label', localize('openQuickChat', "Open Quick Chat"));
pill.tabIndex = 0;
this._container.appendChild(pill);
// Left side indicator (status)
const leftIndicator = $('span.agent-status-indicator');
if (hasActiveSessions) {
// Running indicator when there are active sessions
const runningIcon = $('span.agent-status-icon');
reset(runningIcon, renderIcon(Codicon.sessionInProgress));
leftIndicator.appendChild(runningIcon);
const runningCount = $('span.agent-status-text');
runningCount.textContent = String(activeSessions.length);
leftIndicator.appendChild(runningCount);
} else if (hasUnreadSessions) {
// Unread indicator when there are unread sessions
const unreadIcon = $('span.agent-status-icon');
reset(unreadIcon, renderIcon(Codicon.circleFilled));
leftIndicator.appendChild(unreadIcon);
const unreadCount = $('span.agent-status-text');
unreadCount.textContent = String(unreadSessions.length);
leftIndicator.appendChild(unreadCount);
// Left icon container (sparkle by default, report+count when attention needed, search on hover)
const leftIcon = $('span.agent-status-left-icon');
if (hasAttentionNeeded) {
// Show report icon + count when sessions need attention
const reportIcon = renderIcon(Codicon.report);
const countSpan = $('span.agent-status-attention-count');
countSpan.textContent = String(attentionNeededSessions.length);
reset(leftIcon, reportIcon, countSpan);
leftIcon.classList.add('has-attention');
} else {
// Keyboard shortcut when idle (show quick chat keybinding - matches click action)
const kb = this.keybindingService.lookupKeybinding(ACTION_ID)?.getLabel();
if (kb) {
const kbLabel = $('span.agent-status-keybinding');
kbLabel.textContent = kb;
leftIndicator.appendChild(kbLabel);
}
reset(leftIcon, renderIcon(Codicon.searchSparkle));
}
pill.appendChild(leftIndicator);
pill.appendChild(leftIcon);
// Show label - either progress from most recent active session, or workspace name
// Label (workspace name by default, placeholder on hover)
// Show attention progress or default label
const label = $('span.agent-status-label');
const { session: activeSession, progress: progressText } = this._getMostRecentActiveSession(activeSessions);
this._displayedSession = activeSession;
const { session: attentionSession, progress: progressText } = this._getSessionNeedingAttention(attentionNeededSessions);
this._displayedSession = attentionSession;
const defaultLabel = progressText ?? this._getLabel();
if (progressText) {
// Show progress with fade-in animation
label.classList.add('has-progress');
label.textContent = progressText;
} else {
label.textContent = this._getLabel();
}
const hoverLabel = localize('askAnythingPlaceholder', "Ask anything or describe what to build next");
label.textContent = defaultLabel;
pill.appendChild(label);
// Send icon (right side) - only show when not streaming progress
// Send icon (hidden by default, shown on hover - only when not showing attention message)
const sendIcon = $('span.agent-status-send');
reset(sendIcon, renderIcon(Codicon.send));
sendIcon.classList.add('hidden');
pill.appendChild(sendIcon);
// Hover behavior - swap icon and label (only when showing default state).
// When progressText is defined (e.g. sessions need attention), keep the attention/progress
// message visible and do not replace it with the generic placeholder on hover.
if (!progressText) {
const sendIcon = $('span.agent-status-send');
reset(sendIcon, renderIcon(Codicon.send));
pill.appendChild(sendIcon);
disposables.add(addDisposableListener(pill, EventType.MOUSE_ENTER, () => {
reset(leftIcon, renderIcon(Codicon.searchSparkle));
leftIcon.classList.remove('has-attention');
label.textContent = hoverLabel;
label.classList.remove('has-progress');
sendIcon.classList.remove('hidden');
}));
disposables.add(addDisposableListener(pill, EventType.MOUSE_LEAVE, () => {
reset(leftIcon, renderIcon(Codicon.searchSparkle));
label.textContent = defaultLabel;
sendIcon.classList.add('hidden');
}));
}
// Setup hover - show session name when displaying progress, otherwise show keybinding
// Setup hover tooltip
const hoverDelegate = getDefaultHoverDelegate('mouse');
disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, () => {
if (this._displayedSession) {
@@ -229,8 +304,8 @@ export class AgentStatusWidget extends BaseActionViewItem {
}
}));
// Search button (right of pill)
this._renderSearchButton(disposables);
// Status badge (separate rectangle on right) - always rendered for smooth transitions
this._renderStatusBadge(disposables, activeSessions, unreadSessions);
}
private _renderSessionMode(disposables: DisposableStore): void {
@@ -238,68 +313,53 @@ export class AgentStatusWidget extends BaseActionViewItem {
return;
}
const { activeSessions, unreadSessions } = this._getSessionStats();
const pill = $('div.agent-status-pill.session-mode');
this._container.appendChild(pill);
// Session title (left/center)
// Search button (left side, inside pill)
this._renderSearchButton(disposables, pill);
// Session title (center)
const titleLabel = $('span.agent-status-title');
const sessionInfo = this.agentStatusService.sessionInfo;
titleLabel.textContent = sessionInfo?.title ?? localize('agentSessionProjection', "Agent Session Projection");
pill.appendChild(titleLabel);
// Escape button (right side) - serves as both keybinding hint and close button
const escButton = $('span.agent-status-esc-button');
escButton.textContent = 'Esc';
escButton.setAttribute('role', 'button');
escButton.setAttribute('aria-label', localize('exitAgentSessionProjection', "Exit Agent Session Projection"));
escButton.tabIndex = 0;
pill.appendChild(escButton);
// Escape button (right side)
this._renderEscapeButton(disposables, pill);
// Setup hovers
// Setup pill hover
const hoverDelegate = getDefaultHoverDelegate('mouse');
disposables.add(this.hoverService.setupManagedHover(hoverDelegate, escButton, localize('exitAgentSessionProjectionTooltip', "Exit Agent Session Projection (Escape)")));
disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, () => {
const sessionInfo = this.agentStatusService.sessionInfo;
return sessionInfo ? localize('agentSessionProjectionTooltip', "Agent Session Projection: {0}", sessionInfo.title) : localize('agentSessionProjection', "Agent Session Projection");
}));
// Esc button click handler
disposables.add(addDisposableListener(escButton, EventType.MOUSE_DOWN, (e) => {
e.preventDefault();
e.stopPropagation();
this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID);
}));
disposables.add(addDisposableListener(escButton, EventType.CLICK, (e) => {
e.preventDefault();
e.stopPropagation();
this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID);
}));
// Esc button keyboard handler
disposables.add(addDisposableListener(escButton, EventType.KEY_DOWN, (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID);
}
}));
// Search button (right of pill)
this._renderSearchButton(disposables);
// Status badge (separate rectangle on right) - always rendered for smooth transitions
this._renderStatusBadge(disposables, activeSessions, unreadSessions);
}
private _renderSearchButton(disposables: DisposableStore): void {
if (!this._container) {
// #endregion
// #region Reusable Components
/**
* Render the search button. If parent is provided, appends to parent; otherwise appends to container.
*/
private _renderSearchButton(disposables: DisposableStore, parent?: HTMLElement): void {
const container = parent ?? this._container;
if (!container) {
return;
}
const searchButton = $('span.agent-status-search');
reset(searchButton, renderIcon(Codicon.search));
reset(searchButton, renderIcon(Codicon.searchSparkle));
searchButton.setAttribute('role', 'button');
searchButton.setAttribute('aria-label', localize('openQuickOpen', "Open Quick Open"));
searchButton.tabIndex = 0;
this._container.appendChild(searchButton);
container.appendChild(searchButton);
// Setup hover
const hoverDelegate = getDefaultHoverDelegate('mouse');
@@ -326,6 +386,110 @@ export class AgentStatusWidget extends BaseActionViewItem {
}));
}
/**
* Render the status badge showing in-progress and/or unread session counts.
* Shows split UI with both indicators when both types exist.
* Always renders for smooth fade transitions - uses visibility classes.
*/
private _renderStatusBadge(disposables: DisposableStore, activeSessions: IAgentSession[], unreadSessions: IAgentSession[]): void {
if (!this._container) {
return;
}
const hasActiveSessions = activeSessions.length > 0;
const hasUnreadSessions = unreadSessions.length > 0;
const hasContent = hasActiveSessions || hasUnreadSessions;
const badge = $('div.agent-status-badge');
if (!hasContent) {
badge.classList.add('empty');
}
this._container.appendChild(badge);
// Unread section (blue dot + count)
if (hasUnreadSessions) {
const unreadSection = $('span.agent-status-badge-section.unread');
const unreadIcon = $('span.agent-status-icon');
reset(unreadIcon, renderIcon(Codicon.circleFilled));
unreadSection.appendChild(unreadIcon);
const unreadCount = $('span.agent-status-text');
unreadCount.textContent = String(unreadSessions.length);
unreadSection.appendChild(unreadCount);
badge.appendChild(unreadSection);
}
// In-progress section (session-in-progress icon + count)
if (hasActiveSessions) {
const activeSection = $('span.agent-status-badge-section.active');
const runningIcon = $('span.agent-status-icon');
reset(runningIcon, renderIcon(Codicon.sessionInProgress));
activeSection.appendChild(runningIcon);
const runningCount = $('span.agent-status-text');
runningCount.textContent = String(activeSessions.length);
activeSection.appendChild(runningCount);
badge.appendChild(activeSection);
}
// Setup hover with combined tooltip
const hoverDelegate = getDefaultHoverDelegate('mouse');
disposables.add(this.hoverService.setupManagedHover(hoverDelegate, badge, () => {
const parts: string[] = [];
if (hasUnreadSessions) {
parts.push(unreadSessions.length === 1
? localize('unreadSessionsTooltip1', "{0} unread session", unreadSessions.length)
: localize('unreadSessionsTooltip', "{0} unread sessions", unreadSessions.length));
}
if (hasActiveSessions) {
parts.push(activeSessions.length === 1
? localize('activeSessionsTooltip1', "{0} session in progress", activeSessions.length)
: localize('activeSessionsTooltip', "{0} sessions in progress", activeSessions.length));
}
return parts.join(', ');
}));
}
/**
* Render the escape button for exiting session projection mode.
*/
private _renderEscapeButton(disposables: DisposableStore, parent: HTMLElement): void {
const escButton = $('span.agent-status-esc-button');
escButton.textContent = 'Esc';
escButton.setAttribute('role', 'button');
escButton.setAttribute('aria-label', localize('exitAgentSessionProjection', "Exit Agent Session Projection"));
escButton.tabIndex = 0;
parent.appendChild(escButton);
// Setup hover
const hoverDelegate = getDefaultHoverDelegate('mouse');
disposables.add(this.hoverService.setupManagedHover(hoverDelegate, escButton, localize('exitAgentSessionProjectionTooltip', "Exit Agent Session Projection (Escape)")));
// Click handler
disposables.add(addDisposableListener(escButton, EventType.MOUSE_DOWN, (e) => {
e.preventDefault();
e.stopPropagation();
this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID);
}));
disposables.add(addDisposableListener(escButton, EventType.CLICK, (e) => {
e.preventDefault();
e.stopPropagation();
this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID);
}));
// Keyboard handler
disposables.add(addDisposableListener(escButton, EventType.KEY_DOWN, (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
this.commandService.executeCommand(ExitAgentSessionProjectionAction.ID);
}
}));
}
// #endregion
// #region Click Handlers
/**
* Handle pill click - opens the displayed session if showing progress, otherwise executes default action
*/
@@ -337,17 +501,21 @@ export class AgentStatusWidget extends BaseActionViewItem {
}
}
// #endregion
// #region Session Helpers
/**
* Get the most recently interacted active session and its progress text.
* Returns undefined session if no active sessions.
* Get the session most urgently needing user attention (approval/confirmation/input).
* Returns undefined if no sessions need attention.
*/
private _getMostRecentActiveSession(activeSessions: IAgentSession[]): { session: IAgentSession | undefined; progress: string | undefined } {
if (activeSessions.length === 0) {
private _getSessionNeedingAttention(attentionNeededSessions: IAgentSession[]): { session: IAgentSession | undefined; progress: string | undefined } {
if (attentionNeededSessions.length === 0) {
return { session: undefined, progress: undefined };
}
// Sort by most recently started request
const sorted = [...activeSessions].sort((a, b) => {
const sorted = [...attentionNeededSessions].sort((a, b) => {
const timeA = a.timing.lastRequestStarted ?? a.timing.created;
const timeB = b.timing.lastRequestStarted ?? b.timing.created;
return timeB - timeA;
@@ -355,7 +523,7 @@ export class AgentStatusWidget extends BaseActionViewItem {
const mostRecent = sorted[0];
if (!mostRecent.description) {
return { session: mostRecent, progress: undefined };
return { session: mostRecent, progress: mostRecent.label };
}
// Convert markdown to plain text if needed
@@ -366,6 +534,10 @@ export class AgentStatusWidget extends BaseActionViewItem {
return { session: mostRecent, progress };
}
// #endregion
// #region Label Helpers
/**
* Compute the label to display, matching the command center behavior.
* Includes prefix and suffix decorations (remote host, extension dev host, etc.)
@@ -419,4 +591,6 @@ export class AgentStatusWidget extends BaseActionViewItem {
return { prefix, suffix };
}
// #endregion
}
@@ -76,7 +76,7 @@ Agent Status Widget - Titlebar control
}
.agent-status-pill.chat-input-mode.has-active .agent-status-label {
color: var(--vscode-progressBar-background);
color: var(--vscode-foreground);
opacity: 1;
}
@@ -85,6 +85,22 @@ Agent Status Widget - Titlebar control
font-size: 8px;
}
/* Needs attention state - session requires user approval/confirmation/input */
.agent-status-pill.chat-input-mode.needs-attention {
background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent);
border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent);
}
.agent-status-pill.chat-input-mode.needs-attention:hover {
background-color: color-mix(in srgb, var(--vscode-progressBar-background) 25%, transparent);
border-color: color-mix(in srgb, var(--vscode-progressBar-background) 70%, transparent);
}
.agent-status-pill.chat-input-mode.needs-attention .agent-status-label {
color: var(--vscode-foreground);
opacity: 1;
}
/* Session mode (viewing a session) */
.agent-status-pill.session-mode {
background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent);
@@ -111,7 +127,7 @@ Agent Status Widget - Titlebar control
/* Progress label - fade in animation when showing session progress */
.agent-status-label.has-progress {
animation: agentStatusFadeIn 0.3s ease-out;
color: var(--vscode-progressBar-background);
color: var(--vscode-foreground);
opacity: 1;
}
@@ -159,6 +175,11 @@ Agent Status Widget - Titlebar control
align-items: center;
color: var(--vscode-foreground);
opacity: 0.7;
flex-shrink: 0;
}
.agent-status-send.hidden {
display: none;
}
.agent-status-pill.has-active .agent-status-send {
@@ -166,6 +187,28 @@ Agent Status Widget - Titlebar control
opacity: 1;
}
/* Left icon (sparkle default, report+count when attention needed, search on hover) */
.agent-status-left-icon {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
color: var(--vscode-foreground);
opacity: 0.7;
flex-shrink: 0;
}
/* Left icon with attention - show report icon + count */
.agent-status-left-icon.has-attention {
color: var(--vscode-foreground);
opacity: 1;
}
.agent-status-left-icon.has-attention .agent-status-attention-count {
font-size: 11px;
font-weight: 500;
}
/* Session title */
.agent-status-title {
flex: 1;
@@ -208,26 +251,78 @@ Agent Status Widget - Titlebar control
outline-offset: 1px;
}
/* Search button (right of pill) */
/* Search button (inside pill on left) */
.agent-status-search {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 4px;
cursor: pointer;
color: var(--vscode-foreground);
opacity: 0.7;
-webkit-app-region: no-drag;
flex-shrink: 0;
}
.agent-status-search:hover {
opacity: 1;
background-color: var(--vscode-commandCenter-activeBackground, rgba(0, 0, 0, 0.1));
}
.agent-status-search:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
/* Status badge (separate rectangle on right of pill) */
.agent-status-badge {
display: flex;
align-items: center;
gap: 0;
height: 22px;
border-radius: 6px;
overflow: hidden;
background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent);
border: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 50%, transparent);
flex-shrink: 0;
-webkit-app-region: no-drag;
transition: opacity 0.2s ease-in-out, background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
opacity: 1;
/* Reserve minimum width to prevent layout shift */
min-width: 50px;
justify-content: center;
}
/* Empty badge - invisible but reserves space to prevent layout shift */
.agent-status-badge.empty {
opacity: 0;
pointer-events: none;
background-color: transparent;
border-color: transparent;
}
/* Badge section (for split UI) */
.agent-status-badge-section {
display: flex;
align-items: center;
gap: 4px;
padding: 0 8px;
height: 100%;
}
/* Separator between sections */
.agent-status-badge-section + .agent-status-badge-section {
border-left: 1px solid color-mix(in srgb, var(--vscode-progressBar-background) 40%, transparent);
}
/* Unread section styling */
.agent-status-badge-section.unread {
color: var(--vscode-foreground);
}
.agent-status-badge-section.unread .agent-status-icon {
font-size: 8px;
color: var(--vscode-notificationsInfoIcon-foreground);
}
/* Active/in-progress section styling */
.agent-status-badge-section.active {
color: var(--vscode-foreground);
}
@@ -756,6 +756,9 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
public async getChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise<Array<{ readonly chatSessionType: string; readonly items: IChatSessionItem[] }>> {
const results: Array<{ readonly chatSessionType: string; readonly items: IChatSessionItem[] }> = [];
const resolvedProviderTypes = new Set<string>();
// First, iterate over extension point contributions
for (const contrib of this.getAllChatSessionContributions()) {
if (providersToResolve && !providersToResolve.includes(contrib.type)) {
continue; // skip: not considered for resolving
@@ -774,6 +777,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
const providerSessions = await raceCancellationError(provider.provideChatSessionItems(token), token);
this._logService.trace(`[ChatSessionsService] Resolved ${providerSessions.length} sessions for provider ${provider.chatSessionType}`);
results.push({ chatSessionType: provider.chatSessionType, items: providerSessions });
resolvedProviderTypes.add(provider.chatSessionType);
} catch (error) {
// Log error but continue with other providers
this._logService.error(`[ChatSessionsService] Failed to resolve sessions for provider ${provider.chatSessionType}`, error);
@@ -781,6 +785,26 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
}
}
// Also include registered items providers that don't have corresponding contributions
// (e.g., the local session provider which is built-in and not an extension contribution)
for (const [chatSessionType, provider] of this._itemsProviders) {
if (resolvedProviderTypes.has(chatSessionType)) {
continue; // already resolved via contribution
}
if (providersToResolve && !providersToResolve.includes(chatSessionType)) {
continue; // skip: not considered for resolving
}
try {
const providerSessions = await raceCancellationError(provider.provideChatSessionItems(token), token);
this._logService.trace(`[ChatSessionsService] Resolved ${providerSessions.length} sessions for built-in provider ${chatSessionType}`);
results.push({ chatSessionType, items: providerSessions });
} catch (error) {
this._logService.error(`[ChatSessionsService] Failed to resolve sessions for built-in provider ${chatSessionType}`, error);
continue;
}
}
return results;
}