Path: blob/main/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.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/aiCustomizationTreeView.css';6import * as dom from '../../../../base/browser/dom.js';7import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';8import { CancellationToken } from '../../../../base/common/cancellation.js';9import { DisposableStore } from '../../../../base/common/lifecycle.js';10import { autorun } from '../../../../base/common/observable.js';11import { basename, dirname } from '../../../../base/common/resources.js';12import { ThemeIcon } from '../../../../base/common/themables.js';13import { URI } from '../../../../base/common/uri.js';14import { localize } from '../../../../nls.js';15import { createActionViewItem, getContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';16import { IMenuService } from '../../../../platform/actions/common/actions.js';17import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';18import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';19import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';20import { IHoverService } from '../../../../platform/hover/browser/hover.js';21import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';22import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';23import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';24import { IOpenerService } from '../../../../platform/opener/common/opener.js';25import { IThemeService } from '../../../../platform/theme/common/themeService.js';26import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';27import { IViewDescriptorService } from '../../../../workbench/common/views.js';28import { IPromptsService, PromptsStorage, IAgentSkill, IPromptPath } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';29import { ResourceSet } from '../../../../base/common/map.js';30import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';31import { agentIcon, extensionIcon, instructionsIcon, mcpServerIcon, pluginIcon, promptIcon, skillIcon, userIcon, workspaceIcon, builtinIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js';32import { AICustomizationItemMenuId } from './aiCustomizationTreeView.js';33import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js';34import { AICustomizationPromptsStorage, BUILTIN_STORAGE } from '../../chat/common/builtinPromptsStorage.js';35import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js';36import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js';37import { IAsyncDataSource, ITreeNode, ITreeRenderer, ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js';38import { FuzzyScore } from '../../../../base/common/filters.js';39import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';40import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';41import { ILogService } from '../../../../platform/log/common/log.js';42import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';43import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';4445//#region Context Keys4647/**48* Context key indicating whether the AI Customization view has no items.49*/50export const AICustomizationIsEmptyContextKey = new RawContextKey<boolean>('aiCustomization.isEmpty', true);5152/**53* Context key for the current item's prompt type in context menus.54*/55export const AICustomizationItemTypeContextKey = new RawContextKey<string>('aiCustomizationItemType', '');5657/**58* Context key indicating whether the current item is disabled.59*/60export const AICustomizationItemDisabledContextKey = new RawContextKey<boolean>('aiCustomizationItemDisabled', false);6162/**63* Context key for the current item's storage type in context menus.64*/65export const AICustomizationItemStorageContextKey = new RawContextKey<string>('aiCustomizationItemStorage', '');6667//#endregion6869//#region Tree Item Types7071/**72* Root element marker for the tree.73*/74const ROOT_ELEMENT = Symbol('root');75type RootElement = typeof ROOT_ELEMENT;7677/**78* Represents a type category in the tree (e.g., "Custom Agents", "Skills").79*/80interface IAICustomizationTypeItem {81readonly type: 'category';82readonly id: string;83readonly label: string;84readonly promptType: PromptsType;85readonly icon: ThemeIcon;86}8788/**89* Represents a storage group header in the tree (e.g., "Workspace", "User", "Extensions").90*/91interface IAICustomizationGroupItem {92readonly type: 'group';93readonly id: string;94readonly label: string;95readonly storage: AICustomizationPromptsStorage;96readonly promptType: PromptsType;97readonly icon: ThemeIcon;98}99100/**101* Represents an individual AI customization item (agent, skill, instruction, or prompt).102*/103interface IAICustomizationFileItem {104readonly type: 'file';105readonly id: string;106readonly uri: URI;107readonly name: string;108readonly description?: string;109readonly storage: AICustomizationPromptsStorage;110readonly promptType: PromptsType;111readonly disabled: boolean;112}113114/**115* Represents a link item that navigates to the management editor.116*/117interface IAICustomizationLinkItem {118readonly type: 'link';119readonly id: string;120readonly label: string;121readonly icon: ThemeIcon;122readonly section: AICustomizationManagementSection;123}124125type AICustomizationTreeItem = IAICustomizationTypeItem | IAICustomizationGroupItem | IAICustomizationFileItem | IAICustomizationLinkItem;126127//#endregion128129//#region Tree Infrastructure130131class AICustomizationTreeDelegate implements IListVirtualDelegate<AICustomizationTreeItem> {132getHeight(_element: AICustomizationTreeItem): number {133return 22;134}135136getTemplateId(element: AICustomizationTreeItem): string {137switch (element.type) {138case 'category':139case 'link':140return 'category';141case 'group':142return 'group';143case 'file':144return 'file';145}146}147}148149interface ICategoryTemplateData {150readonly container: HTMLElement;151readonly icon: HTMLElement;152readonly label: HTMLElement;153}154155interface IGroupTemplateData {156readonly container: HTMLElement;157readonly label: HTMLElement;158}159160interface IFileTemplateData {161readonly container: HTMLElement;162readonly icon: HTMLElement;163readonly name: HTMLElement;164readonly actionBar: ActionBar;165readonly elementDisposables: DisposableStore;166readonly templateDisposables: DisposableStore;167}168169class AICustomizationCategoryRenderer implements ITreeRenderer<IAICustomizationTypeItem | IAICustomizationLinkItem, FuzzyScore, ICategoryTemplateData> {170readonly templateId = 'category';171172renderTemplate(container: HTMLElement): ICategoryTemplateData {173const element = dom.append(container, dom.$('.ai-customization-category'));174const icon = dom.append(element, dom.$('.icon'));175const label = dom.append(element, dom.$('.label'));176return { container: element, icon, label };177}178179renderElement(node: ITreeNode<IAICustomizationTypeItem | IAICustomizationLinkItem, FuzzyScore>, _index: number, templateData: ICategoryTemplateData): void {180templateData.icon.className = 'icon';181templateData.icon.classList.add(...ThemeIcon.asClassNameArray(node.element.icon));182templateData.label.textContent = node.element.label;183}184185disposeTemplate(_templateData: ICategoryTemplateData): void { }186}187188class AICustomizationGroupRenderer implements ITreeRenderer<IAICustomizationGroupItem, FuzzyScore, IGroupTemplateData> {189readonly templateId = 'group';190191renderTemplate(container: HTMLElement): IGroupTemplateData {192const element = dom.append(container, dom.$('.ai-customization-group-header'));193const label = dom.append(element, dom.$('.label'));194return { container: element, label };195}196197renderElement(node: ITreeNode<IAICustomizationGroupItem, FuzzyScore>, _index: number, templateData: IGroupTemplateData): void {198templateData.label.textContent = node.element.label;199}200201disposeTemplate(_templateData: IGroupTemplateData): void { }202}203204class AICustomizationFileRenderer implements ITreeRenderer<IAICustomizationFileItem, FuzzyScore, IFileTemplateData> {205readonly templateId = 'file';206207constructor(208private readonly menuService: IMenuService,209private readonly contextKeyService: IContextKeyService,210private readonly instantiationService: IInstantiationService,211) { }212213renderTemplate(container: HTMLElement): IFileTemplateData {214const element = dom.append(container, dom.$('.ai-customization-tree-item'));215const icon = dom.append(element, dom.$('.icon'));216const name = dom.append(element, dom.$('.name'));217const actionsContainer = dom.append(element, dom.$('.actions'));218219const templateDisposables = new DisposableStore();220const actionBar = templateDisposables.add(new ActionBar(actionsContainer, {221actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService),222}));223224return { container: element, icon, name, actionBar, elementDisposables: new DisposableStore(), templateDisposables };225}226227renderElement(node: ITreeNode<IAICustomizationFileItem, FuzzyScore>, _index: number, templateData: IFileTemplateData): void {228const item = node.element;229templateData.elementDisposables.clear();230231// Set icon based on prompt type232let icon: ThemeIcon;233switch (item.promptType) {234case PromptsType.agent:235icon = agentIcon;236break;237case PromptsType.skill:238icon = skillIcon;239break;240case PromptsType.instructions:241icon = instructionsIcon;242break;243case PromptsType.prompt:244default:245icon = promptIcon;246break;247}248249templateData.icon.className = 'icon';250templateData.icon.classList.add(...ThemeIcon.asClassNameArray(icon));251252templateData.name.textContent = item.name;253254// Apply disabled styling255templateData.container.classList.toggle('disabled', item.disabled);256257// Set tooltip with name and description258const tooltip = item.description ? `${item.name} - ${item.description}` : item.name;259templateData.container.title = tooltip;260261// Build context for menu actions262const context = {263uri: item.uri.toString(),264name: item.name,265promptType: item.promptType,266storage: item.storage,267};268269// Create scoped context key service with item type for when-clause filtering270const overlay = this.contextKeyService.createOverlay([271[AICustomizationItemTypeContextKey.key, item.promptType],272[AICustomizationItemDisabledContextKey.key, item.disabled],273[AICustomizationItemStorageContextKey.key, item.storage],274]);275276// Create menu and extract inline actions277const menu = templateData.elementDisposables.add(278this.menuService.createMenu(AICustomizationItemMenuId, overlay)279);280281const updateActions = () => {282const actions = menu.getActions({ arg: context, shouldForwardArgs: true });283const { primary } = getContextMenuActions(actions, 'inline');284templateData.actionBar.clear();285templateData.actionBar.push(primary, { icon: true, label: false });286};287updateActions();288templateData.elementDisposables.add(menu.onDidChange(updateActions));289290templateData.actionBar.context = context;291}292293disposeElement(_node: ITreeNode<IAICustomizationFileItem, FuzzyScore>, _index: number, templateData: IFileTemplateData): void {294templateData.elementDisposables.clear();295}296297disposeTemplate(templateData: IFileTemplateData): void {298templateData.templateDisposables.dispose();299templateData.elementDisposables.dispose();300}301}302303/**304* Cached data for a specific prompt type.305*/306interface ICachedTypeData {307skills?: IAgentSkill[];308files?: Map<string, readonly IPromptPath[]>;309}310311/**312* Data source for the AI Customization tree with efficient caching.313* Caches data per-type to avoid redundant fetches when expanding groups.314*/315class UnifiedAICustomizationDataSource implements IAsyncDataSource<RootElement, AICustomizationTreeItem> {316private cache = new Map<PromptsType, ICachedTypeData>();317private totalItemCount = 0;318319constructor(320private readonly promptsService: IPromptsService,321private readonly logService: ILogService,322private readonly onItemCountChanged: (count: number) => void,323) { }324325/**326* Clears the cache. Should be called when the view refreshes.327*/328clearCache(): void {329this.cache.clear();330this.totalItemCount = 0;331}332333hasChildren(element: RootElement | AICustomizationTreeItem): boolean {334if (element === ROOT_ELEMENT) {335return true;336}337if (element.type === 'link') {338return false;339}340return element.type === 'category' || element.type === 'group';341}342343async getChildren(element: RootElement | AICustomizationTreeItem): Promise<AICustomizationTreeItem[]> {344try {345if (element === ROOT_ELEMENT) {346return this.getTypeCategories();347}348349if (element.type === 'category') {350return this.getStorageGroups(element.promptType);351}352353if (element.type === 'group') {354return this.getFilesForStorageAndType(element.storage, element.promptType);355}356357return [];358} catch (error) {359this.logService.error('[AICustomization] Error fetching tree children:', error);360return [];361}362}363364private getTypeCategories(): (IAICustomizationTypeItem | IAICustomizationLinkItem)[] {365return [366{367type: 'category',368id: 'category-agents',369label: localize('customAgents', "Custom Agents"),370promptType: PromptsType.agent,371icon: agentIcon,372},373{374type: 'category',375id: 'category-skills',376label: localize('skills', "Skills"),377promptType: PromptsType.skill,378icon: skillIcon,379},380{381type: 'category',382id: 'category-instructions',383label: localize('instructions', "Instructions"),384promptType: PromptsType.instructions,385icon: instructionsIcon,386},387{388type: 'link',389id: 'link-mcp-servers',390label: localize('mcpServers', "MCP Servers"),391icon: mcpServerIcon,392section: AICustomizationManagementSection.McpServers,393},394];395}396397/**398* Fetches and caches data for a prompt type, returning storage groups with items.399*/400private async getStorageGroups(promptType: PromptsType): Promise<IAICustomizationGroupItem[]> {401const groups: IAICustomizationGroupItem[] = [];402403// Check cache first404let cached = this.cache.get(promptType);405if (!cached) {406cached = {};407this.cache.set(promptType, cached);408}409410// For skills, use findAgentSkills which has the proper names from frontmatter411if (promptType === PromptsType.skill) {412if (!cached.skills) {413const skills = await this.promptsService.findAgentSkills(CancellationToken.None);414cached.skills = skills || [];415this.totalItemCount += cached.skills.length;416this.onItemCountChanged(this.totalItemCount);417}418419const workspaceSkills = cached.skills.filter(s => s.storage === PromptsStorage.local);420const userSkills = cached.skills.filter(s => s.storage === PromptsStorage.user);421const extensionSkills = cached.skills.filter(s => s.storage === PromptsStorage.extension);422const builtinSkills = cached.skills.filter(s => s.storage === BUILTIN_STORAGE);423424if (workspaceSkills.length > 0) {425groups.push(this.createGroupItem(promptType, PromptsStorage.local, workspaceSkills.length));426}427if (userSkills.length > 0) {428groups.push(this.createGroupItem(promptType, PromptsStorage.user, userSkills.length));429}430if (extensionSkills.length > 0) {431groups.push(this.createGroupItem(promptType, PromptsStorage.extension, extensionSkills.length));432}433if (builtinSkills.length > 0) {434groups.push(this.createGroupItem(promptType, BUILTIN_STORAGE, builtinSkills.length));435}436437return groups;438}439440// For other types, fetch once and cache grouped by storage441if (!cached.files) {442const allItems: IPromptPath[] = [...await this.promptsService.listPromptFiles(promptType, CancellationToken.None)];443444// For instructions, also include agent instructions (AGENTS.md, copilot-instructions.md, CLAUDE.md, etc.)445if (promptType === PromptsType.instructions) {446const existingUris = new ResourceSet(allItems.map(item => item.uri));447const agentInstructions = await this.promptsService.listAgentInstructions(CancellationToken.None);448for (const file of agentInstructions) {449if (!existingUris.has(file.uri)) {450allItems.push({ uri: file.uri, storage: PromptsStorage.local, type: PromptsType.instructions });451}452}453}454455const workspaceItems = allItems.filter(item => item.storage === PromptsStorage.local);456const userItems = allItems.filter(item => item.storage === PromptsStorage.user);457const extensionItems = allItems.filter(item => item.storage === PromptsStorage.extension);458const builtinItems = allItems.filter(item => item.storage === BUILTIN_STORAGE);459460cached.files = new Map<string, readonly IPromptPath[]>([461[PromptsStorage.local, workspaceItems],462[PromptsStorage.user, userItems],463[PromptsStorage.extension, extensionItems],464[BUILTIN_STORAGE, builtinItems],465]);466467const itemCount = allItems.length;468this.totalItemCount += itemCount;469this.onItemCountChanged(this.totalItemCount);470}471472const workspaceItems = cached.files!.get(PromptsStorage.local) || [];473const userItems = cached.files!.get(PromptsStorage.user) || [];474const extensionItems = cached.files!.get(PromptsStorage.extension) || [];475const builtinItems = cached.files!.get(BUILTIN_STORAGE) || [];476477if (workspaceItems.length > 0) {478groups.push(this.createGroupItem(promptType, PromptsStorage.local, workspaceItems.length));479}480if (userItems.length > 0) {481groups.push(this.createGroupItem(promptType, PromptsStorage.user, userItems.length));482}483if (extensionItems.length > 0) {484groups.push(this.createGroupItem(promptType, PromptsStorage.extension, extensionItems.length));485}486if (builtinItems.length > 0) {487groups.push(this.createGroupItem(promptType, BUILTIN_STORAGE, builtinItems.length));488}489490return groups;491}492493/**494* Creates a group item with consistent structure.495*/496private createGroupItem(promptType: PromptsType, storage: AICustomizationPromptsStorage, count: number): IAICustomizationGroupItem {497const storageLabels: Record<string, string> = {498[PromptsStorage.local]: localize('workspaceWithCount', "Workspace ({0})", count),499[PromptsStorage.user]: localize('userWithCount', "User ({0})", count),500[PromptsStorage.extension]: localize('extensionsWithCount', "Extensions ({0})", count),501[PromptsStorage.plugin]: localize('pluginsWithCount', "Plugins ({0})", count),502[BUILTIN_STORAGE]: localize('builtinWithCount', "Built-in ({0})", count),503};504505const storageIcons: Record<string, ThemeIcon> = {506[PromptsStorage.local]: workspaceIcon,507[PromptsStorage.user]: userIcon,508[PromptsStorage.extension]: extensionIcon,509[PromptsStorage.plugin]: pluginIcon,510[BUILTIN_STORAGE]: builtinIcon,511};512513const storageSuffixes: Record<string, string> = {514[PromptsStorage.local]: 'workspace',515[PromptsStorage.user]: 'user',516[PromptsStorage.extension]: 'extensions',517[PromptsStorage.plugin]: 'plugins',518[BUILTIN_STORAGE]: 'builtin',519};520521return {522type: 'group',523id: `group-${promptType}-${storageSuffixes[storage]}`,524label: storageLabels[storage],525storage,526promptType,527icon: storageIcons[storage],528};529}530531/**532* Returns files for a specific storage/type combination from cache.533* getStorageGroups must be called first to populate the cache.534*/535private async getFilesForStorageAndType(storage: AICustomizationPromptsStorage, promptType: PromptsType): Promise<IAICustomizationFileItem[]> {536const cached = this.cache.get(promptType);537const disabledUris = this.promptsService.getDisabledPromptFiles(promptType);538539// For skills, use the cached skills data and merge in disabled skills540if (promptType === PromptsType.skill) {541const skills = cached?.skills || [];542const filtered = skills.filter(skill => skill.storage === storage);543const seenUris = new Set<string>();544const result: IAICustomizationFileItem[] = filtered545.map(skill => {546seenUris.add(skill.uri.toString());547// Use skill name from frontmatter, or fallback to parent folder name548const skillName = skill.name || basename(dirname(skill.uri)) || basename(skill.uri);549return {550type: 'file' as const,551id: skill.uri.toString(),552uri: skill.uri,553name: skillName,554description: skill.description,555storage: skill.storage,556promptType,557disabled: disabledUris.has(skill.uri),558};559});560561// Include disabled skills not already in the enabled list562if (disabledUris.size > 0) {563const allSkillFiles = await this.promptsService.listPromptFiles(PromptsType.skill, CancellationToken.None);564for (const file of allSkillFiles) {565if (file.storage === storage && !seenUris.has(file.uri.toString()) && disabledUris.has(file.uri)) {566result.push({567type: 'file' as const,568id: file.uri.toString(),569uri: file.uri,570name: file.name || basename(dirname(file.uri)) || basename(file.uri),571description: file.description,572storage: file.storage,573promptType,574disabled: true,575});576}577}578}579580return result;581}582583// Use cached files data (already fetched in getStorageGroups)584const items = [...(cached?.files?.get(storage) || [])];585return items.map(item => ({586type: 'file' as const,587id: item.uri.toString(),588uri: item.uri,589name: item.name || basename(item.uri),590description: item.description,591storage: item.storage,592promptType,593disabled: disabledUris.has(item.uri),594}));595}596}597598//#endregion599600//#region Unified View Pane601602/**603* Unified view pane for all AI Customization items (agents, skills, instructions, prompts).604*/605export class AICustomizationViewPane extends ViewPane {606static readonly ID = 'aiCustomization.view';607608private tree: WorkbenchAsyncDataTree<RootElement, AICustomizationTreeItem, FuzzyScore> | undefined;609private dataSource: UnifiedAICustomizationDataSource | undefined;610private treeContainer: HTMLElement | undefined;611private readonly treeDisposables = this._register(new DisposableStore());612613// Context keys for controlling menu visibility and welcome content614private readonly isEmptyContextKey: IContextKey<boolean>;615private readonly itemTypeContextKey: IContextKey<string>;616private readonly itemDisabledContextKey: IContextKey<boolean>;617private readonly itemStorageContextKey: IContextKey<string>;618619constructor(620options: IViewPaneOptions,621@IKeybindingService keybindingService: IKeybindingService,622@IContextMenuService contextMenuService: IContextMenuService,623@IConfigurationService configurationService: IConfigurationService,624@IContextKeyService contextKeyService: IContextKeyService,625@IViewDescriptorService viewDescriptorService: IViewDescriptorService,626@IInstantiationService instantiationService: IInstantiationService,627@IOpenerService openerService: IOpenerService,628@IThemeService themeService: IThemeService,629@IHoverService hoverService: IHoverService,630@IPromptsService private readonly promptsService: IPromptsService,631@IEditorService private readonly editorService: IEditorService,632@IMenuService private readonly menuService: IMenuService,633@ILogService private readonly logService: ILogService,634@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,635@IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService,636) {637super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);638639// Initialize context keys640this.isEmptyContextKey = AICustomizationIsEmptyContextKey.bindTo(contextKeyService);641this.itemTypeContextKey = AICustomizationItemTypeContextKey.bindTo(contextKeyService);642this.itemDisabledContextKey = AICustomizationItemDisabledContextKey.bindTo(contextKeyService);643this.itemStorageContextKey = AICustomizationItemStorageContextKey.bindTo(contextKeyService);644645// Subscribe to prompt service events to refresh tree646this._register(this.promptsService.onDidChangeCustomAgents(() => this.refresh()));647this._register(this.promptsService.onDidChangeSlashCommands(() => this.refresh()));648649// Listen to workspace folder changes to refresh tree650this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.refresh()));651this._register(autorun(reader => {652this.workspaceService.activeProjectRoot.read(reader);653this.refresh();654}));655656}657658protected override renderBody(container: HTMLElement): void {659super.renderBody(container);660661container.classList.add('ai-customization-view');662this.treeContainer = dom.append(container, dom.$('.tree-container'));663664this.createTree();665}666667private createTree(): void {668if (!this.treeContainer) {669return;670}671672// Create data source with callback for tracking item count673this.dataSource = new UnifiedAICustomizationDataSource(674this.promptsService,675this.logService,676(count) => this.isEmptyContextKey.set(count === 0),677);678679this.tree = this.treeDisposables.add(this.instantiationService.createInstance(680WorkbenchAsyncDataTree<RootElement, AICustomizationTreeItem, FuzzyScore>,681'AICustomization',682this.treeContainer,683new AICustomizationTreeDelegate(),684[685new AICustomizationCategoryRenderer(),686new AICustomizationGroupRenderer(),687new AICustomizationFileRenderer(this.menuService, this.contextKeyService, this.instantiationService),688],689this.dataSource,690{691identityProvider: {692getId: (element: AICustomizationTreeItem) => element.id,693},694accessibilityProvider: {695getAriaLabel: (element: AICustomizationTreeItem) => {696if (element.type === 'category' || element.type === 'link') {697return element.label;698}699if (element.type === 'group') {700return element.label;701}702// For files, include description and disabled state703const nameAndDesc = element.description704? localize('fileAriaLabel', "{0}, {1}", element.name, element.description)705: element.name;706return element.disabled707? localize('fileAriaLabelDisabled', "{0}, disabled", nameAndDesc)708: nameAndDesc;709},710getWidgetAriaLabel: () => localize('aiCustomizationTree', "Chat Customization Items"),711},712keyboardNavigationLabelProvider: {713getKeyboardNavigationLabel: (element: AICustomizationTreeItem) => {714if (element.type === 'file') {715return element.name;716}717return element.label;718},719},720}721));722723// Handle double-click to open file or navigate to section724this.treeDisposables.add(this.tree.onDidOpen(async e => {725if (e.element && e.element.type === 'file') {726this.editorService.openEditor({727resource: e.element.uri,728});729} else if (e.element && e.element.type === 'link') {730const input = AICustomizationManagementEditorInput.getOrCreate();731const editor = await this.editorService.openEditor(input, { pinned: true });732if (editor instanceof AICustomizationManagementEditor) {733editor.selectSectionById(e.element.section);734}735}736}));737738// Handle context menu739this.treeDisposables.add(this.tree.onContextMenu(e => this.onContextMenu(e)));740741// Initial load and auto-expand category nodes742void this.tree.setInput(ROOT_ELEMENT).then(() => this.autoExpandCategories());743}744745private async autoExpandCategories(): Promise<void> {746if (!this.tree) {747return;748}749// Auto-expand all category nodes to show storage groups750const rootNode = this.tree.getNode(ROOT_ELEMENT);751for (const child of rootNode.children) {752if (child.element !== ROOT_ELEMENT) {753await this.tree.expand(child.element);754}755}756}757758protected override layoutBody(height: number, width: number): void {759super.layoutBody(height, width);760this.tree?.layout(height, width);761}762763public refresh(): void {764// Clear the cache before refreshing765this.dataSource?.clearCache();766this.isEmptyContextKey.set(true); // Reset until we know the count767void this.tree?.setInput(ROOT_ELEMENT).then(() => this.autoExpandCategories());768}769770public collapseAll(): void {771this.tree?.collapseAll();772}773774public expandAll(): void {775this.tree?.expandAll();776}777778private onContextMenu(e: ITreeContextMenuEvent<AICustomizationTreeItem | null>): void {779// Only show context menu for file items780if (!e.element || e.element.type !== 'file') {781return;782}783784const element = e.element;785786// Set context keys for the item so menu items can use `when` clauses787this.itemTypeContextKey.set(element.promptType);788this.itemDisabledContextKey.set(element.disabled);789this.itemStorageContextKey.set(element.storage);790791// Get menu actions from the menu service792const context = {793uri: element.uri.toString(),794name: element.name,795promptType: element.promptType,796disabled: element.disabled,797};798const menu = this.menuService.getMenuActions(AICustomizationItemMenuId, this.contextKeyService, { arg: context, shouldForwardArgs: true });799const { secondary } = getContextMenuActions(menu, 'inline');800801// Show the context menu802if (secondary.length > 0) {803this.contextMenuService.showContextMenu({804getAnchor: () => e.anchor,805getActions: () => secondary,806getActionsContext: () => context,807onHide: () => {808// Clear the context keys when menu closes809this.itemTypeContextKey.reset();810this.itemDisabledContextKey.reset();811this.itemStorageContextKey.reset();812},813});814}815}816}817818//#endregion819820821