Path: blob/main/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/aiCustomizationManagement.css';6import * as DOM from '../../../../base/browser/dom.js';7import { CancellationToken } from '../../../../base/common/cancellation.js';8import { autorun } from '../../../../base/common/observable.js';9import { ThemeIcon } from '../../../../base/common/themables.js';10import { localize } from '../../../../nls.js';11import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';12import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';13import { IViewDescriptorService } from '../../../../workbench/common/views.js';14import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';15import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';16import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';17import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';18import { IOpenerService } from '../../../../platform/opener/common/opener.js';19import { IThemeService } from '../../../../platform/theme/common/themeService.js';20import { IHoverService } from '../../../../platform/hover/browser/hover.js';21import { ResourceSet } from '../../../../base/common/map.js';22import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';23import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';24import { AICustomizationManagementSection, AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js';25import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js';26import { agentIcon, instructionsIcon, mcpServerIcon, pluginIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js';27import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';28import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';29import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';30import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js';31import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js';3233const $ = DOM.$;3435export const AI_CUSTOMIZATION_OVERVIEW_VIEW_ID = 'workbench.view.aiCustomizationOverview';3637function isWelcomePageEditor(editor: unknown): editor is { showWelcomePage(): void } {38return typeof (editor as { showWelcomePage?: unknown })?.showWelcomePage === 'function';39}4041interface ISectionSummary {42readonly id: AICustomizationManagementSection;43readonly label: string;44readonly icon: ThemeIcon;45count: number;46}4748/**49* A compact overview view that shows a snapshot of AI customizations50* and provides deep-links to the management editor sections.51*/52export class AICustomizationOverviewView extends ViewPane {5354private bodyElement!: HTMLElement;55private container!: HTMLElement;56private sectionsContainer!: HTMLElement;57private readonly sections: ISectionSummary[] = [];58private readonly countElements = new Map<AICustomizationManagementSection, HTMLElement>();5960constructor(61options: IViewPaneOptions,62@IKeybindingService keybindingService: IKeybindingService,63@IContextMenuService contextMenuService: IContextMenuService,64@IConfigurationService configurationService: IConfigurationService,65@IContextKeyService contextKeyService: IContextKeyService,66@IViewDescriptorService viewDescriptorService: IViewDescriptorService,67@IInstantiationService instantiationService: IInstantiationService,68@IOpenerService openerService: IOpenerService,69@IThemeService themeService: IThemeService,70@IHoverService hoverService: IHoverService,71@IEditorService private readonly editorService: IEditorService,72@IPromptsService private readonly promptsService: IPromptsService,73@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,74@IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService,75@IMcpService private readonly mcpService: IMcpService,76@IAgentPluginService private readonly agentPluginService: IAgentPluginService,77) {78super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);7980// Initialize sections81this.sections.push(82{ id: AICustomizationManagementSection.Agents, label: localize('agents', "Agents"), icon: agentIcon, count: 0 },83{ id: AICustomizationManagementSection.Skills, label: localize('skills', "Skills"), icon: skillIcon, count: 0 },84{ id: AICustomizationManagementSection.Instructions, label: localize('instructions', "Instructions"), icon: instructionsIcon, count: 0 },85{ id: AICustomizationManagementSection.McpServers, label: localize('mcpServers', "MCP Servers"), icon: mcpServerIcon, count: 0 },86{ id: AICustomizationManagementSection.Plugins, label: localize('plugins', "Plugins"), icon: pluginIcon, count: 0 },87);8889// Listen to changes90this._register(this.promptsService.onDidChangeCustomAgents(() => this.loadCounts()));91this._register(this.promptsService.onDidChangeSlashCommands(() => this.loadCounts()));9293// Listen to workspace folder changes to update counts94this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.loadCounts()));95this._register(autorun(reader => {96this.workspaceService.activeProjectRoot.read(reader);97this.loadCounts();98}));99100}101102protected override renderBody(container: HTMLElement): void {103super.renderBody(container);104105this.bodyElement = container;106this.container = DOM.append(container, $('.ai-customization-overview'));107this.sectionsContainer = DOM.append(this.container, $('.overview-sections'));108109this.renderSections();110void this.loadCounts();111112// Force initial layout113this.layoutBody(this.bodyElement.offsetHeight, this.bodyElement.offsetWidth);114}115116private renderSections(): void {117DOM.clearNode(this.sectionsContainer);118this.countElements.clear();119120for (const section of this.sections) {121const sectionElement = DOM.append(this.sectionsContainer, $('.overview-section'));122sectionElement.tabIndex = 0;123sectionElement.setAttribute('role', 'button');124sectionElement.setAttribute('aria-label', `${section.label}: ${section.count} items`);125126const iconElement = DOM.append(sectionElement, $('.section-icon'));127iconElement.classList.add(...ThemeIcon.asClassNameArray(section.icon));128129const textContainer = DOM.append(sectionElement, $('.section-text'));130const labelElement = DOM.append(textContainer, $('.section-label'));131labelElement.textContent = section.label;132133const countElement = DOM.append(sectionElement, $('.section-count'));134countElement.textContent = `${section.count}`;135this.countElements.set(section.id, countElement);136137// Click handler to open the management editor overview138this._register(DOM.addDisposableListener(sectionElement, 'click', () => {139this.openOverview();140}));141142// Keyboard support143this._register(DOM.addDisposableListener(sectionElement, 'keydown', (e: KeyboardEvent) => {144if (e.key === 'Enter' || e.key === ' ') {145e.preventDefault();146this.openOverview();147}148}));149150// Hover tooltip151this._register(this.hoverService.setupDelayedHoverAtMouse(sectionElement, () => ({152content: localize('openOverview', "Open Chat Customizations editor"),153appearance: { compact: true, skipFadeInAnimation: true }154})));155}156}157158private async loadCounts(): Promise<void> {159const sectionPromptTypes: Array<{ section: AICustomizationManagementSection; type: PromptsType }> = [160{ section: AICustomizationManagementSection.Agents, type: PromptsType.agent },161{ section: AICustomizationManagementSection.Skills, type: PromptsType.skill },162{ section: AICustomizationManagementSection.Instructions, type: PromptsType.instructions },163];164165await Promise.all(sectionPromptTypes.map(async ({ section, type }) => {166let count = 0;167if (type === PromptsType.skill) {168const skills = await this.promptsService.findAgentSkills(CancellationToken.None);169if (skills) {170count = skills.length;171}172} else {173const allItems = await this.promptsService.listPromptFiles(type, CancellationToken.None);174count = allItems.length;175176// For instructions, also count agent instructions (AGENTS.md, copilot-instructions.md, CLAUDE.md, etc.)177if (type === PromptsType.instructions) {178const existingUris = new ResourceSet(allItems.map(item => item.uri));179const agentInstructions = await this.promptsService.listAgentInstructions(CancellationToken.None);180for (const file of agentInstructions) {181if (!existingUris.has(file.uri)) {182count++;183}184}185}186}187188const sectionData = this.sections.find(s => s.id === section);189if (sectionData) {190sectionData.count = count;191}192}));193194// Update MCP server count reactively195const mcpSection = this.sections.find(s => s.id === AICustomizationManagementSection.McpServers);196if (mcpSection) {197this._register(autorun(reader => {198const servers = this.mcpService.servers.read(reader);199mcpSection.count = servers.length;200this.updateCountElements();201}));202}203204// Update plugin count reactively205const pluginSection = this.sections.find(s => s.id === AICustomizationManagementSection.Plugins);206if (pluginSection) {207this._register(autorun(reader => {208const plugins = this.agentPluginService.plugins.read(reader);209pluginSection.count = plugins.length;210this.updateCountElements();211}));212}213214this.updateCountElements();215}216217private updateCountElements(): void {218for (const section of this.sections) {219const countElement = this.countElements.get(section.id);220if (countElement) {221countElement.textContent = `${section.count}`;222}223}224}225226private async openOverview(): Promise<void> {227const input = AICustomizationManagementEditorInput.getOrCreate();228const editor = await this.editorService.openEditor(input, { pinned: true });229230// Always reset to the welcome page when opening from the sidebar,231// so we don't restore the previously selected section.232if (editor?.getId() === AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID && isWelcomePageEditor(editor)) {233editor.showWelcomePage();234}235}236237protected override layoutBody(height: number, width: number): void {238super.layoutBody(height, width);239this.container.style.height = `${height}px`;240}241}242243244