diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts index 9bdcdd03e029..4c3887db7ba3 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts @@ -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 } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css index 541e3bf02a5a..0b7063507a4b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css @@ -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); +} diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index c9510266cbcd..3fb5d98607c7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -756,6 +756,9 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ public async getChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise> { const results: Array<{ readonly chatSessionType: string; readonly items: IChatSessionItem[] }> = []; + const resolvedProviderTypes = new Set(); + + // 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; }