File icon display handling in chat sessions (#268364)

* Re-add the chat session icons

* update comment

* override global file icon hiding for chat sessions

* Consolidate font-family

* Remove !important for font

* move back to use custom icon

* Switch to use IconLabel.

* remove comment

* Changes based on PR feedback to remove color and custom spinning rule

* update comment

* small nits

---------

Co-authored-by: vijay upadya <vj@example.com>
Co-authored-by: BeniBenj <besimmonds@microsoft.com>
This commit is contained in:
Vijay Upadya
2025-09-29 02:35:06 -07:00
committed by GitHub
parent 5aa87724f6
commit 7460c38f95
3 changed files with 37 additions and 85 deletions
@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import * as DOM from '../../../../../../base/browser/dom.js';
import { $, append, getActiveWindow } from '../../../../../../base/browser/dom.js';
import { $, append } from '../../../../../../base/browser/dom.js';
import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js';
import { ActionBar } from '../../../../../../base/browser/ui/actionbar/actionbar.js';
import { InputBox, MessageType } from '../../../../../../base/browser/ui/inputbox/inputBox.js';
import { IAsyncDataSource, ITreeNode, ITreeRenderer } from '../../../../../../base/browser/ui/tree/tree.js';
import { timeout } from '../../../../../../base/common/async.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { FuzzyScore } from '../../../../../../base/common/filters.js';
import { FuzzyScore, createMatches } from '../../../../../../base/common/filters.js';
import { createSingleCallFunction } from '../../../../../../base/common/functional.js';
import { isMarkdownString } from '../../../../../../base/common/htmlContent.js';
import { KeyCode } from '../../../../../../base/common/keyCodes.js';
@@ -19,7 +19,6 @@ import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../..
import { MarshalledId } from '../../../../../../base/common/marshallingIds.js';
import Severity from '../../../../../../base/common/severity.js';
import { ThemeIcon } from '../../../../../../base/common/themables.js';
import { URI } from '../../../../../../base/common/uri.js';
import { MarkdownRenderer } from '../../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
import * as nls from '../../../../../../nls.js';
import { getActionBarActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js';
@@ -29,11 +28,10 @@ import { IContextKeyService } from '../../../../../../platform/contextkey/common
import { IContextViewService } from '../../../../../../platform/contextview/browser/contextView.js';
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../../../platform/log/common/log.js';
import product from '../../../../../../platform/product/common/product.js';
import { defaultInputBoxStyles } from '../../../../../../platform/theme/browser/defaultStyles.js';
import { IThemeService } from '../../../../../../platform/theme/common/themeService.js';
import { IResourceLabel, ResourceLabels } from '../../../../../browser/labels.js';
import { IconLabel } from '../../../../../../base/browser/ui/iconLabel/iconLabel.js';
import { IEditableData } from '../../../../../common/views.js';
import { IEditorGroupsService } from '../../../../../services/editor/common/editorGroupsService.js';
import { IChatService } from '../../../common/chatService.js';
@@ -51,13 +49,14 @@ import { getLocalHistoryDateFormatter } from '../../../../localHistory/browser/l
interface ISessionTemplateData {
readonly container: HTMLElement;
readonly resourceLabel: IResourceLabel;
readonly iconLabel: IconLabel;
readonly actionBar: ActionBar;
readonly elementDisposable: DisposableStore;
readonly timestamp: HTMLElement;
readonly descriptionRow: HTMLElement;
readonly descriptionLabel: HTMLElement;
readonly statisticsLabel: HTMLElement;
readonly customIcon: HTMLElement;
}
export interface IGettingStartedItem {
@@ -110,13 +109,9 @@ export class GettingStartedRenderer implements IListRenderer<IGettingStartedItem
export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatSessionItem, FuzzyScore, ISessionTemplateData> {
static readonly TEMPLATE_ID = 'session';
private appliedIconColorStyles = new Set<string>();
private markdownRenderer: MarkdownRenderer;
constructor(
private readonly labels: ResourceLabels,
@IThemeService private readonly themeService: IThemeService,
@ILogService private readonly logService: ILogService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@@ -130,56 +125,9 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
) {
super();
// Listen for theme changes to clear applied styles
this._register(this.themeService.onDidColorThemeChange(() => {
this.appliedIconColorStyles.clear();
}));
this.markdownRenderer = instantiationService.createInstance(MarkdownRenderer, {});
}
private applyIconColorStyle(iconId: string, colorId: string): void {
const styleKey = `${iconId}-${colorId}`;
if (this.appliedIconColorStyles.has(styleKey)) {
return; // Already applied
}
const colorTheme = this.themeService.getColorTheme();
const color = colorTheme.getColor(colorId);
if (color) {
// Target the ::before pseudo-element where the actual icon is rendered
const css = `.monaco-workbench .chat-session-item .monaco-icon-label.codicon-${iconId}::before { color: ${color} !important; }`;
const activeWindow = getActiveWindow();
const styleId = `chat-sessions-icon-${styleKey}`;
const existingStyle = activeWindow.document.getElementById(styleId);
if (existingStyle) {
existingStyle.textContent = css;
} else {
const styleElement = activeWindow.document.createElement('style');
styleElement.id = styleId;
styleElement.textContent = css;
activeWindow.document.head.appendChild(styleElement);
// Clean up on dispose
this._register({
dispose: () => {
const activeWin = getActiveWindow();
const style = activeWin.document.getElementById(styleId);
if (style) {
style.remove();
}
}
});
}
this.appliedIconColorStyles.add(styleKey);
} else {
this.logService.debug('No color found for colorId:', colorId);
}
}
get templateId(): string {
return SessionsRenderer.TEMPLATE_ID;
}
@@ -189,7 +137,9 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
// Create a container that holds the label, timestamp, and actions
const contentContainer = append(element, $('.session-content'));
const resourceLabel = this.labels.create(contentContainer, { supportHighlights: true });
// Custom icon element rendered separately from label text
const customIcon = append(contentContainer, $('.chat-session-custom-icon'));
const iconLabel = new IconLabel(contentContainer, { supportHighlights: true, supportIcons: true });
const descriptionRow = append(element, $('.description-row'));
const descriptionLabel = append(descriptionRow, $('span.description'));
const statisticsLabel = append(descriptionRow, $('span.statistics'));
@@ -204,7 +154,8 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
return {
container: element,
resourceLabel,
iconLabel,
customIcon,
actionBar,
elementDisposable,
timestamp,
@@ -217,7 +168,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
statusToIcon(status?: ChatSessionStatus) {
switch (status) {
case ChatSessionStatus.InProgress:
return Codicon.loading;
return ThemeIcon.modify(Codicon.loading, 'spin');
case ChatSessionStatus.Completed:
return Codicon.pass;
case ChatSessionStatus.Failed:
@@ -253,7 +204,6 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
templateData.actionBar.clear();
// Handle different icon types
let iconResource: URI | undefined;
let iconTheme: ThemeIcon | undefined;
if (!session.iconPath && session.id !== LocalChatSessionsProvider.HISTORY_NODE_ID) {
iconTheme = this.statusToIcon(session.status);
@@ -261,11 +211,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
iconTheme = session.iconPath;
}
if (iconTheme?.color?.id) {
this.applyIconColorStyle(iconTheme.id, iconTheme.color.id);
}
const renderDescriptionOnSecondRow = this.configurationService.getValue(ChatConfiguration.ShowAgentSessionsViewDescription) && session.provider.chatSessionType !== 'local';
const renderDescriptionOnSecondRow = this.configurationService.getValue<boolean>(ChatConfiguration.ShowAgentSessionsViewDescription) && session.provider.chatSessionType !== 'local';
if (renderDescriptionOnSecondRow && session.description) {
templateData.container.classList.toggle('multiline', true);
@@ -305,17 +251,17 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
} : undefined) :
undefined;
// Set the resource label
templateData.resourceLabel.setResource({
name: session.label,
description: !renderDescriptionOnSecondRow && 'description' in session && typeof session.description === 'string' ? session.description : '',
resource: iconResource
}, {
fileKind: undefined,
icon: iconTheme,
// Set tooltip on resourceLabel only for single-row items
title: !renderDescriptionOnSecondRow || !session.description ? tooltipContent : undefined
});
templateData.customIcon.className = iconTheme ? `chat-session-custom-icon ${ThemeIcon.asClassName(iconTheme)}` : '';
// Set the icon label
templateData.iconLabel.setLabel(
session.label,
!renderDescriptionOnSecondRow && typeof session.description === 'string' ? session.description : undefined,
{
title: !renderDescriptionOnSecondRow || !session.description ? tooltipContent : undefined,
matches: createMatches(element.filterData)
}
);
// For two-row items, set tooltip on the container instead
if (renderDescriptionOnSecondRow && session.description && tooltipContent) {
@@ -398,7 +344,6 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
disposeElement(_element: ITreeNode<IChatSessionItem, FuzzyScore>, _index: number, templateData: ISessionTemplateData): void {
templateData.elementDisposable.clear();
templateData.resourceLabel.clear();
templateData.actionBar.clear();
}
@@ -531,7 +476,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
disposeTemplate(templateData: ISessionTemplateData): void {
templateData.elementDisposable.dispose();
templateData.resourceLabel.dispose();
templateData.iconLabel.dispose();
templateData.actionBar.dispose();
}
}
@@ -295,8 +295,7 @@ export class SessionsViewPane extends ViewPane {
const accessibilityProvider = new SessionsAccessibilityProvider();
// Use the existing ResourceLabels service for consistent styling
const labels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
const renderer = this.instantiationService.createInstance(SessionsRenderer, labels);
const renderer = this.instantiationService.createInstance(SessionsRenderer);
this._register(renderer);
const getResourceForElement = (element: ChatSessionItemWithProvider): URI | null => {
@@ -169,8 +169,14 @@
text-align: center;
}
.chat-sessions-tree-container .chat-session-item .monaco-icon-label.codicon-loading::before {
animation: codicon-spin 1.5s steps(30) infinite;
/* Chat session icon */
.chat-sessions-tree-container .chat-session-item .chat-session-custom-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
margin-right: 6px;
font-size: 16px;
display: flex;
}
/* Timestamp styling - similar to timeline pane */
@@ -229,8 +235,10 @@
/* Hide twisties for elements that don't have children */
.chat-sessions-tree-container .monaco-list-row .monaco-tl-twistie {
visibility: hidden;
width: 0;
/* Ultra-small indent to separate parent/child without large gutter */
visibility: hidden; /* keep layout space */
width: 3px;
min-width: 3px;
padding: 0;
margin: 0;
}