ai-customizations: improve list visual scannability (#300551)

* ai-customizations: improve list visual scannability (#299211)

- Add type-specific icon to each list item (agent, skill, instructions, prompt, hook)
- Format item names: convert dashes/underscores to spaces and apply title case
- Truncate descriptions to first sentence (max 120 chars fallback) to reduce visual noise
- Make item name font-weight 500 so titles pop against secondary text

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

* ai-customizations: add type icon + name/description polish for MCP servers and plugins (#299211)

- Export formatDisplayName and truncateToFirstSentence helpers from aiCustomizationListWidget
- Add mcpServerIcon to McpServerItemRenderer (local + builtin items)
- Add pluginIcon to PluginInstalledItemRenderer
- Apply formatDisplayName (dash/underscore → spaces, title case) to names
- Apply truncateToFirstSentence to descriptions
- Set font-weight: 500 on mcp-server-name to match AI customization list style
- Remove left-indent padding on mcp-server-item now that the icon fills that space

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

* ai-customizations: mute group count badges (#299211)

Replace badge-background/foreground pill styling with plain descriptionForeground
text (opacity 0.8) on both the group-header count and the sidebar section count.
This lets the section label dominate visually.

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

* ai-customizations: restore badge pill with reduced opacity (#299211)

Keep badge-background/foreground colors but apply opacity: 0.6 so the
pill is still visible but clearly secondary to the section label.

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

* ai-customizations: use keybindingLabel tokens for count badges (#299211)

Switch from badge-background (bright accent) to keybindingLabel-background/
foreground/border tokens, which are designed for subtle inline labels.
No opacity hacks needed — the color itself is naturally muted.

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

* ai-customizations: use list.inactiveSelection tokens for count badges (#299211)

Switch to list.inactiveSelectionBackground/Foreground — the semantically
closest tokens for a secondary/muted count pill in a list/tree context.
No opacity hacks needed and the name directly reflects the role.

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

* ai-customizations: strip trailing .md from display names (#299211)

formatDisplayName now strips a case-insensitive .md suffix before
applying the title-case transform, so 'my-file.Md' no longer
appears as a title.

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

* ai-customizations: remove explicit font-weight from item titles (#299211)

Drop the font-weight: 500 on item-name and mcp-server-name. The visual
hierarchy is already established by the 13px full-foreground title vs the
11px muted descriptionForeground description below it, without needing an
explicit weight bump.

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

* ai-customizations: use star icon for built-in MCP server items (#299211)

Built-in MCP items now show builtinIcon (star) instead of mcpServerIcon,
consistent with the prompts built-in group. Icon is now set per-element
in renderElement so the two item types can show different icons.

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

* Revert "ai-customizations: use star icon for built-in MCP server items (#299211)"

This reverts commit 6b08675a22.

* ai-customizations: use star icon for Built-in MCP group header (#299211)

Change the Built-in group header icon from extensionIcon to builtinIcon
(starFull), consistent with the Built-in group in the prompts list.

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

* ai-customizations: remove unused extensionIcon import from mcpListWidget (#299211)

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

* prompts: add create-pr prompt with compile-check reminder

Ensures the TypeScript compile check is run before opening a PR,
catching unused import and type errors that tsgo would flag in CI.

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

* ai-customizations: fix IMatch highlight positions for formatted names (#299211)

nameMatches are now computed against formatDisplayName(item.name) in
filterItems so highlight positions align with the displayed string.
The .md stripping in formatDisplayName changes string length, so matches
against the raw name would produce incorrect highlight spans.
Also removed the outdated '1:1 transformation' claim from the JSDoc.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Josh Spicer
2026-03-10 14:43:53 -07:00
committed by GitHub
parent 4863c500a8
commit a200e8b166
4 changed files with 94 additions and 22 deletions

View File

@@ -115,6 +115,7 @@ class AICustomizationListDelegate implements IListVirtualDelegate<IListEntry> {
interface IAICustomizationItemTemplateData {
readonly container: HTMLElement;
readonly actionsContainer: HTMLElement;
readonly typeIcon: HTMLElement;
readonly nameLabel: HighlightedLabel;
readonly description: HighlightedLabel;
readonly disposables: DisposableStore;
@@ -194,6 +195,47 @@ class GroupHeaderRenderer implements IListRenderer<IGroupHeaderEntry, IGroupHead
}
}
/**
* Returns the icon for a given prompt type.
*/
function promptTypeToIcon(type: PromptsType): ThemeIcon {
switch (type) {
case PromptsType.agent: return agentIcon;
case PromptsType.skill: return skillIcon;
case PromptsType.instructions: return instructionsIcon;
case PromptsType.prompt: return promptIcon;
case PromptsType.hook: return hookIcon;
default: return promptIcon;
}
}
/**
* Formats a name for display: strips a trailing .md extension, converts dashes/underscores
* to spaces and applies title case.
* Note: callers that pass IMatch highlight ranges must compute those ranges against the
* formatted string (not the raw input), since .md stripping changes string length.
*/
export function formatDisplayName(name: string): string {
return name
.replace(/\.md$/i, '')
.replace(/[-_]/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
}
/**
* Truncates a description string to the first sentence, with a maximum character fallback.
*/
export function truncateToFirstSentence(text: string, maxChars = 120): string {
const match = text.match(/^[^.!?]*[.!?]/);
if (match && match[0].length <= maxChars) {
return match[0];
}
if (text.length > maxChars) {
return text.substring(0, maxChars).trimEnd() + '\u2026';
}
return text;
}
/**
* Renderer for AI customization list items.
*/
@@ -212,6 +254,7 @@ class AICustomizationItemRenderer implements IListRenderer<IFileItemEntry, IAICu
container.classList.add('ai-customization-list-item');
const leftSection = DOM.append(container, $('.item-left'));
const typeIcon = DOM.append(leftSection, $('.item-type-icon'));
const textContainer = DOM.append(leftSection, $('.item-text'));
const nameLabel = disposables.add(new HighlightedLabel(DOM.append(textContainer, $('.item-name'))));
const description = disposables.add(new HighlightedLabel(DOM.append(textContainer, $('.item-description'))));
@@ -222,6 +265,7 @@ class AICustomizationItemRenderer implements IListRenderer<IFileItemEntry, IAICu
return {
container,
actionsContainer,
typeIcon,
nameLabel,
description,
disposables,
@@ -233,6 +277,10 @@ class AICustomizationItemRenderer implements IListRenderer<IFileItemEntry, IAICu
templateData.elementDisposables.clear();
const element = entry.item;
// Type icon based on prompt type
templateData.typeIcon.className = 'item-type-icon';
templateData.typeIcon.classList.add(...ThemeIcon.asClassNameArray(promptTypeToIcon(element.promptType)));
// Hover tooltip: name + full path
templateData.elementDisposables.add(this.hoverService.setupDelayedHover(templateData.container, () => {
const uriLabel = this.labelService.getUriLabel(element.uri, { relative: false });
@@ -245,11 +293,12 @@ class AICustomizationItemRenderer implements IListRenderer<IFileItemEntry, IAICu
};
}));
// Name with highlights
templateData.nameLabel.set(element.name, element.nameMatches);
// Name with highlights — nameMatches are pre-computed against the formatted display name
const displayName = formatDisplayName(element.name);
templateData.nameLabel.set(displayName, element.nameMatches);
// Description - show either description or filename as secondary text
const secondaryText = element.description || element.filename;
// Description - show either truncated description or filename as secondary text
const secondaryText = element.description ? truncateToFirstSentence(element.description) : element.filename;
if (secondaryText) {
templateData.description.set(secondaryText, element.description ? element.descriptionMatches : undefined);
templateData.description.element.style.display = '';
@@ -963,7 +1012,10 @@ export class AICustomizationListWidget extends Disposable {
matchedItems = [];
for (const item of this.allItems) {
const nameMatches = matchesContiguousSubString(query, item.name);
// Compute matches against the formatted display name so highlight positions
// are correct even after .md stripping and title-casing.
const displayName = formatDisplayName(item.name);
const nameMatches = matchesContiguousSubString(query, displayName);
const descriptionMatches = item.description ? matchesContiguousSubString(query, item.description) : null;
const filenameMatches = matchesContiguousSubString(query, item.filename);

View File

@@ -29,7 +29,8 @@ import { Delayer } from '../../../../../base/common/async.js';
import { IAction, Separator } from '../../../../../base/common/actions.js';
import { getContextMenuActions } from '../../../../contrib/mcp/browser/mcpServerActions.js';
import { LocalMcpServerScope } from '../../../../services/mcp/common/mcpWorkbenchManagementService.js';
import { workspaceIcon, userIcon, extensionIcon } from './aiCustomizationIcons.js';
import { workspaceIcon, userIcon, mcpServerIcon, builtinIcon } from './aiCustomizationIcons.js';
import { formatDisplayName, truncateToFirstSentence } from './aiCustomizationListWidget.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js';
import { CustomizationGroupHeaderRenderer, ICustomizationGroupHeaderEntry, CUSTOMIZATION_GROUP_HEADER_HEIGHT, CUSTOMIZATION_GROUP_HEADER_HEIGHT_WITH_SEPARATOR } from './customizationGroupHeaderRenderer.js';
@@ -93,6 +94,7 @@ class McpServerItemDelegate implements IListVirtualDelegate<IMcpListEntry> {
interface IMcpServerItemTemplateData {
readonly container: HTMLElement;
readonly typeIcon: HTMLElement;
readonly name: HTMLElement;
readonly description: HTMLElement;
readonly status: HTMLElement;
@@ -113,13 +115,16 @@ class McpServerItemRenderer implements IListRenderer<IMcpServerItemEntry | IMcpB
renderTemplate(container: HTMLElement): IMcpServerItemTemplateData {
container.classList.add('mcp-server-item');
const typeIcon = DOM.append(container, $('.mcp-server-icon'));
typeIcon.classList.add(...ThemeIcon.asClassNameArray(mcpServerIcon));
const details = DOM.append(container, $('.mcp-server-details'));
const name = DOM.append(details, $('.mcp-server-name'));
const description = DOM.append(details, $('.mcp-server-description'));
const status = DOM.append(container, $('.mcp-server-status'));
return { container, name, description, status, disposables: new DisposableStore() };
return { container, typeIcon, name, description, status, disposables: new DisposableStore() };
}
renderElement(element: IMcpServerItemEntry | IMcpBuiltinItemEntry, index: number, templateData: IMcpServerItemTemplateData): void {
@@ -127,9 +132,9 @@ class McpServerItemRenderer implements IListRenderer<IMcpServerItemEntry | IMcpB
if (element.type === 'builtin-item') {
templateData.container.classList.add('builtin');
templateData.name.textContent = element.label;
templateData.name.textContent = formatDisplayName(element.label);
if (element.description) {
templateData.description.textContent = element.description;
templateData.description.textContent = truncateToFirstSentence(element.description);
templateData.description.style.display = '';
} else {
templateData.description.style.display = 'none';
@@ -139,9 +144,9 @@ class McpServerItemRenderer implements IListRenderer<IMcpServerItemEntry | IMcpB
}
templateData.container.classList.remove('builtin');
templateData.name.textContent = element.server.label;
templateData.name.textContent = formatDisplayName(element.server.label);
if (element.server.description) {
templateData.description.textContent = element.server.description;
templateData.description.textContent = truncateToFirstSentence(element.server.description);
templateData.description.style.display = '';
} else {
templateData.description.style.display = 'none';
@@ -674,7 +679,7 @@ export class McpListWidget extends Disposable {
id: 'mcp-group-builtin',
scope: 'builtin',
label: localize('builtInGroup', "Built-in"),
icon: extensionIcon,
icon: builtinIcon,
count: builtinServers.length,
isFirst,
description: localize('builtInGroupDescription', "MCP servers built into VS Code. These are available automatically."),

View File

@@ -306,8 +306,8 @@
flex-shrink: 0;
font-size: 10px;
font-weight: 500;
background-color: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
background-color: var(--vscode-list-inactiveSelectionBackground);
color: var(--vscode-list-inactiveSelectionForeground, var(--vscode-foreground));
padding: 0 5px;
border-radius: 8px;
min-width: 14px;
@@ -397,6 +397,17 @@
opacity: 0.6;
}
.ai-customization-list-item .item-type-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.8;
font-size: 14px;
}
.ai-customization-list-item .item-text {
display: flex;
flex-direction: column;
@@ -543,12 +554,11 @@
.ai-customization-overview .overview-section .section-count {
flex-shrink: 0;
font-size: 9px;
font-weight: 600;
font-weight: 500;
padding: 1px 5px;
background-color: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
background-color: var(--vscode-list-inactiveSelectionBackground);
color: var(--vscode-list-inactiveSelectionForeground, var(--vscode-foreground));
border-radius: 8px;
border: 1px solid transparent;
min-width: 14px;
text-align: center;
}
@@ -703,7 +713,7 @@
.mcp-server-item {
display: flex;
align-items: center;
padding: 6px 8px 6px 24px;
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
margin: 2px 0;

View File

@@ -32,6 +32,7 @@ import { IMarketplacePlugin, IPluginMarketplaceService } from '../../common/plug
import { IPluginInstallService } from '../../common/plugins/pluginInstallService.js';
import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem, IMarketplacePluginItem } from '../agentPluginEditor/agentPluginItems.js';
import { pluginIcon } from './aiCustomizationIcons.js';
import { formatDisplayName, truncateToFirstSentence } from './aiCustomizationListWidget.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { CustomizationGroupHeaderRenderer, ICustomizationGroupHeaderEntry, CUSTOMIZATION_GROUP_HEADER_HEIGHT, CUSTOMIZATION_GROUP_HEADER_HEIGHT_WITH_SEPARATOR } from './customizationGroupHeaderRenderer.js';
@@ -100,6 +101,7 @@ class PluginItemDelegate implements IListVirtualDelegate<IPluginListEntry> {
interface IPluginInstalledItemTemplateData {
readonly container: HTMLElement;
readonly typeIcon: HTMLElement;
readonly name: HTMLElement;
readonly description: HTMLElement;
readonly status: HTMLElement;
@@ -112,21 +114,24 @@ class PluginInstalledItemRenderer implements IListRenderer<IPluginInstalledItemE
renderTemplate(container: HTMLElement): IPluginInstalledItemTemplateData {
container.classList.add('mcp-server-item');
const typeIcon = DOM.append(container, $('.mcp-server-icon'));
typeIcon.classList.add(...ThemeIcon.asClassNameArray(pluginIcon));
const details = DOM.append(container, $('.mcp-server-details'));
const name = DOM.append(details, $('.mcp-server-name'));
const description = DOM.append(details, $('.mcp-server-description'));
const status = DOM.append(container, $('.mcp-server-status'));
return { container, name, description, status, disposables: new DisposableStore() };
return { container, typeIcon, name, description, status, disposables: new DisposableStore() };
}
renderElement(element: IPluginInstalledItemEntry, _index: number, templateData: IPluginInstalledItemTemplateData): void {
templateData.disposables.clear();
templateData.name.textContent = element.item.name;
templateData.name.textContent = formatDisplayName(element.item.name);
if (element.item.description) {
templateData.description.textContent = element.item.description;
templateData.description.textContent = truncateToFirstSentence(element.item.description);
templateData.description.style.display = '';
} else {
templateData.description.style.display = 'none';