Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/aiCustomizationManagement.css';
7
import * as DOM from '../../../../base/browser/dom.js';
8
import { CancellationToken } from '../../../../base/common/cancellation.js';
9
import { autorun } from '../../../../base/common/observable.js';
10
import { ThemeIcon } from '../../../../base/common/themables.js';
11
import { localize } from '../../../../nls.js';
12
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
13
import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';
14
import { IViewDescriptorService } from '../../../../workbench/common/views.js';
15
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
16
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
17
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
18
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
19
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
20
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
21
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
22
import { ResourceSet } from '../../../../base/common/map.js';
23
import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
24
import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
25
import { AICustomizationManagementSection, AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js';
26
import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js';
27
import { agentIcon, instructionsIcon, mcpServerIcon, pluginIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js';
28
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
29
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
30
import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';
31
import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js';
32
import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js';
33
34
const $ = DOM.$;
35
36
export const AI_CUSTOMIZATION_OVERVIEW_VIEW_ID = 'workbench.view.aiCustomizationOverview';
37
38
function isWelcomePageEditor(editor: unknown): editor is { showWelcomePage(): void } {
39
return typeof (editor as { showWelcomePage?: unknown })?.showWelcomePage === 'function';
40
}
41
42
interface ISectionSummary {
43
readonly id: AICustomizationManagementSection;
44
readonly label: string;
45
readonly icon: ThemeIcon;
46
count: number;
47
}
48
49
/**
50
* A compact overview view that shows a snapshot of AI customizations
51
* and provides deep-links to the management editor sections.
52
*/
53
export class AICustomizationOverviewView extends ViewPane {
54
55
private bodyElement!: HTMLElement;
56
private container!: HTMLElement;
57
private sectionsContainer!: HTMLElement;
58
private readonly sections: ISectionSummary[] = [];
59
private readonly countElements = new Map<AICustomizationManagementSection, HTMLElement>();
60
61
constructor(
62
options: IViewPaneOptions,
63
@IKeybindingService keybindingService: IKeybindingService,
64
@IContextMenuService contextMenuService: IContextMenuService,
65
@IConfigurationService configurationService: IConfigurationService,
66
@IContextKeyService contextKeyService: IContextKeyService,
67
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
68
@IInstantiationService instantiationService: IInstantiationService,
69
@IOpenerService openerService: IOpenerService,
70
@IThemeService themeService: IThemeService,
71
@IHoverService hoverService: IHoverService,
72
@IEditorService private readonly editorService: IEditorService,
73
@IPromptsService private readonly promptsService: IPromptsService,
74
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
75
@IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService,
76
@IMcpService private readonly mcpService: IMcpService,
77
@IAgentPluginService private readonly agentPluginService: IAgentPluginService,
78
) {
79
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
80
81
// Initialize sections
82
this.sections.push(
83
{ id: AICustomizationManagementSection.Agents, label: localize('agents', "Agents"), icon: agentIcon, count: 0 },
84
{ id: AICustomizationManagementSection.Skills, label: localize('skills', "Skills"), icon: skillIcon, count: 0 },
85
{ id: AICustomizationManagementSection.Instructions, label: localize('instructions', "Instructions"), icon: instructionsIcon, count: 0 },
86
{ id: AICustomizationManagementSection.McpServers, label: localize('mcpServers', "MCP Servers"), icon: mcpServerIcon, count: 0 },
87
{ id: AICustomizationManagementSection.Plugins, label: localize('plugins', "Plugins"), icon: pluginIcon, count: 0 },
88
);
89
90
// Listen to changes
91
this._register(this.promptsService.onDidChangeCustomAgents(() => this.loadCounts()));
92
this._register(this.promptsService.onDidChangeSlashCommands(() => this.loadCounts()));
93
94
// Listen to workspace folder changes to update counts
95
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.loadCounts()));
96
this._register(autorun(reader => {
97
this.workspaceService.activeProjectRoot.read(reader);
98
this.loadCounts();
99
}));
100
101
}
102
103
protected override renderBody(container: HTMLElement): void {
104
super.renderBody(container);
105
106
this.bodyElement = container;
107
this.container = DOM.append(container, $('.ai-customization-overview'));
108
this.sectionsContainer = DOM.append(this.container, $('.overview-sections'));
109
110
this.renderSections();
111
void this.loadCounts();
112
113
// Force initial layout
114
this.layoutBody(this.bodyElement.offsetHeight, this.bodyElement.offsetWidth);
115
}
116
117
private renderSections(): void {
118
DOM.clearNode(this.sectionsContainer);
119
this.countElements.clear();
120
121
for (const section of this.sections) {
122
const sectionElement = DOM.append(this.sectionsContainer, $('.overview-section'));
123
sectionElement.tabIndex = 0;
124
sectionElement.setAttribute('role', 'button');
125
sectionElement.setAttribute('aria-label', `${section.label}: ${section.count} items`);
126
127
const iconElement = DOM.append(sectionElement, $('.section-icon'));
128
iconElement.classList.add(...ThemeIcon.asClassNameArray(section.icon));
129
130
const textContainer = DOM.append(sectionElement, $('.section-text'));
131
const labelElement = DOM.append(textContainer, $('.section-label'));
132
labelElement.textContent = section.label;
133
134
const countElement = DOM.append(sectionElement, $('.section-count'));
135
countElement.textContent = `${section.count}`;
136
this.countElements.set(section.id, countElement);
137
138
// Click handler to open the management editor overview
139
this._register(DOM.addDisposableListener(sectionElement, 'click', () => {
140
this.openOverview();
141
}));
142
143
// Keyboard support
144
this._register(DOM.addDisposableListener(sectionElement, 'keydown', (e: KeyboardEvent) => {
145
if (e.key === 'Enter' || e.key === ' ') {
146
e.preventDefault();
147
this.openOverview();
148
}
149
}));
150
151
// Hover tooltip
152
this._register(this.hoverService.setupDelayedHoverAtMouse(sectionElement, () => ({
153
content: localize('openOverview', "Open Chat Customizations editor"),
154
appearance: { compact: true, skipFadeInAnimation: true }
155
})));
156
}
157
}
158
159
private async loadCounts(): Promise<void> {
160
const sectionPromptTypes: Array<{ section: AICustomizationManagementSection; type: PromptsType }> = [
161
{ section: AICustomizationManagementSection.Agents, type: PromptsType.agent },
162
{ section: AICustomizationManagementSection.Skills, type: PromptsType.skill },
163
{ section: AICustomizationManagementSection.Instructions, type: PromptsType.instructions },
164
];
165
166
await Promise.all(sectionPromptTypes.map(async ({ section, type }) => {
167
let count = 0;
168
if (type === PromptsType.skill) {
169
const skills = await this.promptsService.findAgentSkills(CancellationToken.None);
170
if (skills) {
171
count = skills.length;
172
}
173
} else {
174
const allItems = await this.promptsService.listPromptFiles(type, CancellationToken.None);
175
count = allItems.length;
176
177
// For instructions, also count agent instructions (AGENTS.md, copilot-instructions.md, CLAUDE.md, etc.)
178
if (type === PromptsType.instructions) {
179
const existingUris = new ResourceSet(allItems.map(item => item.uri));
180
const agentInstructions = await this.promptsService.listAgentInstructions(CancellationToken.None);
181
for (const file of agentInstructions) {
182
if (!existingUris.has(file.uri)) {
183
count++;
184
}
185
}
186
}
187
}
188
189
const sectionData = this.sections.find(s => s.id === section);
190
if (sectionData) {
191
sectionData.count = count;
192
}
193
}));
194
195
// Update MCP server count reactively
196
const mcpSection = this.sections.find(s => s.id === AICustomizationManagementSection.McpServers);
197
if (mcpSection) {
198
this._register(autorun(reader => {
199
const servers = this.mcpService.servers.read(reader);
200
mcpSection.count = servers.length;
201
this.updateCountElements();
202
}));
203
}
204
205
// Update plugin count reactively
206
const pluginSection = this.sections.find(s => s.id === AICustomizationManagementSection.Plugins);
207
if (pluginSection) {
208
this._register(autorun(reader => {
209
const plugins = this.agentPluginService.plugins.read(reader);
210
pluginSection.count = plugins.length;
211
this.updateCountElements();
212
}));
213
}
214
215
this.updateCountElements();
216
}
217
218
private updateCountElements(): void {
219
for (const section of this.sections) {
220
const countElement = this.countElements.get(section.id);
221
if (countElement) {
222
countElement.textContent = `${section.count}`;
223
}
224
}
225
}
226
227
private async openOverview(): Promise<void> {
228
const input = AICustomizationManagementEditorInput.getOrCreate();
229
const editor = await this.editorService.openEditor(input, { pinned: true });
230
231
// Always reset to the welcome page when opening from the sidebar,
232
// so we don't restore the previously selected section.
233
if (editor?.getId() === AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID && isWelcomePageEditor(editor)) {
234
editor.showWelcomePage();
235
}
236
}
237
238
protected override layoutBody(height: number, width: number): void {
239
super.layoutBody(height, width);
240
this.container.style.height = `${height}px`;
241
}
242
}
243
244