mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-29 18:09:01 +01:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user