mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
chat customizations: component fixtures, developer skill, spec updates (#303309)
* component explorer fixture for chat customization tabs * chat customizations: full editor fixture + developer skill * Refine AI customization management editor fixtures * fix: update DOM element creation to use shorthand syntax --------- Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
This commit is contained in:
42
.github/skills/chat-customizations-editor/SKILL.md
vendored
Normal file
42
.github/skills/chat-customizations-editor/SKILL.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: chat-customizations-editor
|
||||
description: Use when working on the Chat Customizations editor — the management UI for agents, skills, instructions, hooks, prompts, MCP servers, and plugins.
|
||||
---
|
||||
|
||||
# Chat Customizations Editor
|
||||
|
||||
Split-view management pane for AI customization items across workspace, user, extension, and plugin storage. Supports harness-based filtering (Local, Copilot CLI, Claude).
|
||||
|
||||
## Spec
|
||||
|
||||
**`src/vs/sessions/AI_CUSTOMIZATIONS.md`** — always read before making changes, always update after.
|
||||
|
||||
## Key Folders
|
||||
|
||||
| Folder | What |
|
||||
|--------|------|
|
||||
| `src/vs/workbench/contrib/chat/common/` | `ICustomizationHarnessService`, `ISectionOverride`, `IStorageSourceFilter` — shared interfaces and filter helpers |
|
||||
| `src/vs/workbench/contrib/chat/browser/aiCustomization/` | Management editor, list widgets (prompts, MCP, plugins), harness service registration |
|
||||
| `src/vs/sessions/contrib/chat/browser/` | Sessions-window overrides (harness service, workspace service) |
|
||||
| `src/vs/sessions/contrib/sessions/browser/` | Sessions tree view counts and toolbar |
|
||||
|
||||
When changing harness descriptor interfaces or factory functions, verify both core and sessions registrations compile.
|
||||
|
||||
## Key Interfaces
|
||||
|
||||
- **`IHarnessDescriptor`** — drives all UI behavior declaratively (hidden sections, button overrides, file filters, agent gating). See spec for full field reference.
|
||||
- **`ISectionOverride`** — per-section button customization (command invocation, root file creation, type labels, file extensions).
|
||||
- **`IStorageSourceFilter`** — controls which storage sources and user roots are visible per harness/type.
|
||||
|
||||
Principle: the UI widgets read everything from the descriptor — no harness-specific conditionals in widget code.
|
||||
|
||||
## Testing
|
||||
|
||||
Component explorer fixtures (see `component-fixtures` skill): `aiCustomizationListWidget.fixture.ts`, `aiCustomizationManagementEditor.fixture.ts` under `src/vs/workbench/test/browser/componentFixtures/`.
|
||||
|
||||
```bash
|
||||
./scripts/test.sh --grep "applyStorageSourceFilter|customizationCounts"
|
||||
npm run compile-check-ts-native && npm run valid-layers-check
|
||||
```
|
||||
|
||||
See the `sessions` skill for sessions-window specific guidance.
|
||||
@@ -15,18 +15,21 @@ src/vs/workbench/contrib/chat/browser/aiCustomization/
|
||||
├── aiCustomizationManagementEditor.ts # SplitView list/editor
|
||||
├── aiCustomizationManagementEditorInput.ts # Singleton input
|
||||
├── aiCustomizationListWidget.ts # Search + grouped list + harness toggle
|
||||
├── aiCustomizationListWidgetUtils.ts # List item helpers (truncation, etc.)
|
||||
├── aiCustomizationDebugPanel.ts # Debug diagnostics panel
|
||||
├── aiCustomizationWorkspaceService.ts # Core VS Code workspace service impl
|
||||
├── customizationHarnessService.ts # Core harness service impl (VS Code harness)
|
||||
├── customizationHarnessService.ts # Core harness service impl (agent-gated)
|
||||
├── customizationCreatorService.ts # AI-guided creation flow
|
||||
├── mcpListWidget.ts # MCP servers section
|
||||
├── customizationGroupHeaderRenderer.ts # Collapsible group header renderer
|
||||
├── mcpListWidget.ts # MCP servers section (Extensions + Built-in groups)
|
||||
├── pluginListWidget.ts # Agent plugins section
|
||||
├── aiCustomizationIcons.ts # Icons
|
||||
└── media/
|
||||
└── aiCustomizationManagement.css
|
||||
|
||||
src/vs/workbench/contrib/chat/common/
|
||||
├── aiCustomizationWorkspaceService.ts # IAICustomizationWorkspaceService + IStorageSourceFilter
|
||||
└── customizationHarnessService.ts # ICustomizationHarnessService + CustomizationHarness enum
|
||||
└── customizationHarnessService.ts # ICustomizationHarnessService + ISectionOverride + helpers
|
||||
```
|
||||
|
||||
The tree view and overview live in `vs/sessions` (sessions window only):
|
||||
@@ -46,9 +49,10 @@ Sessions-specific overrides:
|
||||
```
|
||||
src/vs/sessions/contrib/chat/browser/
|
||||
├── aiCustomizationWorkspaceService.ts # Sessions workspace service override
|
||||
├── customizationHarnessService.ts # Sessions harness service (CLI + Claude harnesses)
|
||||
├── customizationHarnessService.ts # Sessions harness service (CLI harness only)
|
||||
└── promptsService.ts # AgenticPromptsService (CLI user roots)
|
||||
src/vs/sessions/contrib/sessions/browser/
|
||||
├── aiCustomizationShortcutsWidget.ts # Shortcuts widget
|
||||
├── customizationCounts.ts # Source count utilities (type-aware)
|
||||
└── customizationsToolbar.contribution.ts # Sidebar customization links
|
||||
```
|
||||
@@ -59,7 +63,7 @@ The `IAICustomizationWorkspaceService` interface controls per-window behavior:
|
||||
|
||||
| Property / Method | Core VS Code | Sessions Window |
|
||||
|----------|-------------|----------|
|
||||
| `managementSections` | All sections except Models | Same minus MCP |
|
||||
| `managementSections` | All sections except Models | All sections except Models |
|
||||
| `getStorageSourceFilter(type)` | Delegates to `ICustomizationHarnessService` | Delegates to `ICustomizationHarnessService` |
|
||||
| `isSessionsWindow` | `false` | `true` |
|
||||
| `activeProjectRoot` | First workspace folder | Active session worktree |
|
||||
@@ -71,19 +75,34 @@ Storage answers "where did this come from?"; harness answers "who consumes it?".
|
||||
|
||||
The service is defined in `common/customizationHarnessService.ts` which also provides:
|
||||
- **`CustomizationHarnessServiceBase`** — reusable base class handling active-harness state, the observable list, and `getStorageSourceFilter` dispatch.
|
||||
- **Factory functions** — `createVSCodeHarnessDescriptor`, `createCliHarnessDescriptor`, `createClaudeHarnessDescriptor` — parameterized by an `extras` array (the additional storage sources beyond `local`, `user`, `plugin`). Core passes `[PromptsStorage.extension]`; sessions passes `[BUILTIN_STORAGE]`.
|
||||
- **Well-known root helpers** — `getCliUserRoots(userHome)` and `getClaudeUserRoots(userHome)` centralize the `~/.copilot`, `~/.claude`, `~/.agents` path knowledge so it isn't duplicated.
|
||||
- **`ISectionOverride`** — per-section UI customization: `commandId` (command invocation), `rootFile` + `label` (root-file creation), `typeLabel` (custom type name), `fileExtension` (override default), `rootFileShortcuts` (dropdown shortcuts).
|
||||
- **Factory functions** — `createVSCodeHarnessDescriptor`, `createCliHarnessDescriptor`, `createClaudeHarnessDescriptor`. The VS Code harness receives `[PromptsStorage.extension]` as extras; CLI and Claude in core receive `[]` (no extension source). Sessions CLI receives `[BUILTIN_STORAGE]`.
|
||||
- **Well-known root helpers** — `getCliUserRoots(userHome)` and `getClaudeUserRoots(userHome)` centralize the `~/.copilot`, `~/.claude`, `~/.agents` path knowledge.
|
||||
- **Filter helpers** — `matchesWorkspaceSubpath()` for segment-safe subpath matching; `matchesInstructionFileFilter()` for filename/path-prefix pattern matching.
|
||||
|
||||
Available harnesses:
|
||||
|
||||
| Harness | Label | Description |
|
||||
|---------|-------|-------------|
|
||||
| `vscode` | VS Code | Shows all storage sources (default in core) |
|
||||
| `vscode` | Local | Shows all storage sources (default in core) |
|
||||
| `cli` | Copilot CLI | Restricts user roots to `~/.copilot`, `~/.claude`, `~/.agents` |
|
||||
| `claude` | Claude | Restricts user roots to `~/.claude` |
|
||||
| `claude` | Claude | Restricts user roots to `~/.claude`; hides Prompts + Plugins sections |
|
||||
|
||||
In core VS Code, all three harnesses are registered; VS Code is the default.
|
||||
In sessions, `cli` and `claude` harnesses are registered with a toggle bar above the list.
|
||||
In core VS Code, all three harnesses are registered but CLI and Claude only appear when their respective agents are registered (`requiredAgentId` checked via `IChatAgentService`). VS Code is the default.
|
||||
In sessions, only CLI is registered (single harness, toggle bar hidden).
|
||||
|
||||
### IHarnessDescriptor
|
||||
|
||||
Key properties on the harness descriptor:
|
||||
|
||||
| Property | Purpose |
|
||||
|----------|--------|
|
||||
| `hiddenSections` | Sidebar sections to hide (e.g. Claude: `[Prompts, Plugins]`) |
|
||||
| `workspaceSubpaths` | Restrict file creation/display to directories (e.g. Claude: `['.claude']`) |
|
||||
| `hideGenerateButton` | Replace "Generate X" sparkle button with "New X" |
|
||||
| `sectionOverrides` | Per-section `ISectionOverride` map for button behavior |
|
||||
| `requiredAgentId` | Agent ID that must be registered for harness to appear |
|
||||
| `instructionFileFilter` | Filename/path patterns to filter instruction items |
|
||||
|
||||
### IStorageSourceFilter
|
||||
|
||||
@@ -99,9 +118,7 @@ interface IStorageSourceFilter {
|
||||
|
||||
The shared `applyStorageSourceFilter()` helper applies this filter to any `{uri, storage}` array.
|
||||
|
||||
**Sessions filter behavior by harness and type:**
|
||||
|
||||
CLI harness:
|
||||
**Sessions filter behavior (CLI harness):**
|
||||
|
||||
| Type | sources | includedUserFileRoots |
|
||||
|------|---------|----------------------|
|
||||
@@ -109,15 +126,31 @@ CLI harness:
|
||||
| Prompts | `[local, user, plugin, builtin]` | `undefined` (all roots) |
|
||||
| Agents, Skills, Instructions | `[local, user, plugin, builtin]` | `[~/.copilot, ~/.claude, ~/.agents]` |
|
||||
|
||||
Claude harness:
|
||||
**Core VS Code filter behavior:**
|
||||
|
||||
Local harness: all types use `[local, user, extension, plugin]` with no user root filter.
|
||||
|
||||
CLI harness (core):
|
||||
|
||||
| Type | sources | includedUserFileRoots |
|
||||
|------|---------|----------------------|
|
||||
| Hooks | `[local, plugin]` | N/A |
|
||||
| Prompts | `[local, user, plugin, builtin]` | `undefined` (all roots) |
|
||||
| Agents, Skills, Instructions | `[local, user, plugin, builtin]` | `[~/.claude]` |
|
||||
| Prompts | `[local, user, plugin]` | `undefined` (all roots) |
|
||||
| Agents, Skills, Instructions | `[local, user, plugin]` | `[~/.copilot, ~/.claude, ~/.agents]` |
|
||||
|
||||
**Core VS Code:** All types use `[local, user, extension, plugin]` with no user root filter.
|
||||
Claude harness (core):
|
||||
|
||||
| Type | sources | includedUserFileRoots |
|
||||
|------|---------|----------------------|
|
||||
| Hooks | `[local, plugin]` | N/A |
|
||||
| Prompts | `[local, user, plugin]` | `undefined` (all roots) |
|
||||
| Agents, Skills, Instructions | `[local, user, plugin]` | `[~/.claude]` |
|
||||
|
||||
Claude additionally applies:
|
||||
- `hiddenSections: [Prompts, Plugins]`
|
||||
- `instructionFileFilter: ['CLAUDE.md', 'CLAUDE.local.md', '.claude/rules/', 'copilot-instructions.md']`
|
||||
- `workspaceSubpaths: ['.claude']` (instruction files matching `instructionFileFilter` are exempt)
|
||||
- `sectionOverrides`: Hooks → `copilot.claude.hooks` command; Instructions → "Add CLAUDE.md" primary, "Rule" type label, `.md` file extension
|
||||
|
||||
### AgenticPromptsService (Sessions)
|
||||
|
||||
@@ -149,7 +182,7 @@ Prompt files bundled with the Sessions app live in `src/vs/sessions/prompts/`. T
|
||||
| Skills | `findAgentSkills()` | Parsed skills with frontmatter |
|
||||
| Prompts | `getPromptSlashCommands()` | Filters out skill-type commands |
|
||||
| Instructions | `listPromptFiles()` + `listAgentInstructions()` | Includes AGENTS.md, CLAUDE.md etc. |
|
||||
| Hooks | `listPromptFiles()` | Raw hook files |
|
||||
| Hooks | `listPromptFiles()` | Individual hooks parsed via `parseHooksFromFile()` |
|
||||
|
||||
### Debug Panel
|
||||
|
||||
@@ -175,8 +208,9 @@ All commands and UI respect `ChatContextKeys.enabled` and the `chat.customizatio
|
||||
|
||||
## Settings
|
||||
|
||||
Settings use the `chat.customizationsMenu.` namespace:
|
||||
Settings use the `chat.customizationsMenu.` and `chat.customizations.` namespaces:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `chat.customizationsMenu.enabled` | `true` | Show the Chat Customizations editor in the Command Palette |
|
||||
| `chat.customizations.harnessSelector.enabled` | `true` | Show the harness selector dropdown in the sidebar |
|
||||
|
||||
@@ -74,6 +74,8 @@ import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
|
||||
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
|
||||
import { Action } from '../../../../../base/common/actions.js';
|
||||
import { McpServerEditorInput } from '../../../mcp/browser/mcpServerEditorInput.js';
|
||||
import { McpServerEditor } from '../../../mcp/browser/mcpServerEditor.js';
|
||||
import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
|
||||
@@ -331,6 +333,7 @@ export class AICustomizationManagementEditor extends EditorPane {
|
||||
@IHoverService private readonly hoverService: IHoverService,
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IQuickInputService private readonly quickInputService: IQuickInputService,
|
||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService,
|
||||
@@ -606,7 +609,7 @@ export class AICustomizationManagementEditor extends EditorPane {
|
||||
this.updateHarnessDropdown();
|
||||
|
||||
this.editorDisposables.add(DOM.addDisposableListener(this.harnessDropdownButton, 'click', () => {
|
||||
this.showHarnessPicker();
|
||||
this.showHarnessMenu();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -627,31 +630,26 @@ export class AICustomizationManagementEditor extends EditorPane {
|
||||
}
|
||||
}
|
||||
|
||||
private showHarnessPicker(): void {
|
||||
private showHarnessMenu(): void {
|
||||
if (!this.harnessDropdownButton) {
|
||||
return;
|
||||
}
|
||||
const harnesses = this.harnessService.availableHarnesses.get();
|
||||
const activeId = this.harnessService.activeHarness.get();
|
||||
|
||||
const items = harnesses.map(h => ({
|
||||
label: h.label,
|
||||
iconClass: ThemeIcon.asClassName(h.icon),
|
||||
id: h.id,
|
||||
picked: h.id === activeId,
|
||||
}));
|
||||
|
||||
const picker = this.quickInputService.createQuickPick();
|
||||
picker.items = items;
|
||||
picker.placeholder = localize('selectTarget', "Select customization target");
|
||||
picker.canSelectMany = false;
|
||||
picker.activeItems = items.filter(i => i.picked);
|
||||
picker.onDidAccept(() => {
|
||||
const selected = picker.activeItems[0] as typeof items[0] | undefined;
|
||||
if (selected) {
|
||||
this.harnessService.setActiveHarness(selected.id);
|
||||
}
|
||||
picker.dispose();
|
||||
const actions = harnesses.map(h => {
|
||||
const action = new Action(h.id, h.label, ThemeIcon.asClassName(h.icon), true, () => {
|
||||
this.harnessService.setActiveHarness(h.id);
|
||||
});
|
||||
action.checked = h.id === activeId;
|
||||
return action;
|
||||
});
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => this.harnessDropdownButton!,
|
||||
getActions: () => actions,
|
||||
getCheckedActionsRepresentation: () => 'radio',
|
||||
});
|
||||
picker.onDidHide(() => picker.dispose());
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private createFolderPicker(sidebarContent: HTMLElement): void {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { ResourceSet } from '../../../../base/common/map.js';
|
||||
import { ResourceMap, ResourceSet } from '../../../../base/common/map.js';
|
||||
import { observableValue } from '../../../../base/common/observable.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { mock } from '../../../../base/test/common/mock.js';
|
||||
@@ -17,7 +17,7 @@ import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../..
|
||||
import { CustomizationHarness, ICustomizationHarnessService, IHarnessDescriptor, createVSCodeHarnessDescriptor } from '../../../contrib/chat/common/customizationHarnessService.js';
|
||||
import { IAgentPluginService } from '../../../contrib/chat/common/plugins/agentPluginService.js';
|
||||
import { PromptsType } from '../../../contrib/chat/common/promptSyntax/promptTypes.js';
|
||||
import { IPromptsService, IResolvedAgentFile, AgentFileType, PromptsStorage, IPromptPath, IExtensionPromptPath } from '../../../contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { IPromptsService, IResolvedAgentFile, AgentFileType, PromptsStorage } from '../../../contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { AICustomizationManagementSection } from '../../../contrib/chat/browser/aiCustomization/aiCustomizationManagement.js';
|
||||
import { AICustomizationListWidget } from '../../../contrib/chat/browser/aiCustomization/aiCustomizationListWidget.js';
|
||||
import { IPathService } from '../../../services/path/common/pathService.js';
|
||||
@@ -33,16 +33,28 @@ import '../../../../platform/theme/common/colors/listColors.js';
|
||||
// ============================================================================
|
||||
|
||||
const defaultFilter: IStorageSourceFilter = {
|
||||
sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin, PromptsStorage.extension],
|
||||
sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension, PromptsStorage.plugin],
|
||||
};
|
||||
|
||||
interface IFixtureInstructionFile {
|
||||
readonly promptPath: IPromptPath;
|
||||
readonly uri: URI;
|
||||
readonly storage: PromptsStorage;
|
||||
readonly type: PromptsType;
|
||||
readonly name?: string;
|
||||
readonly description?: string;
|
||||
/** If set, this instruction file has an applyTo pattern (on-demand). */
|
||||
readonly applyTo?: string;
|
||||
}
|
||||
|
||||
function createMockPromptsService(instructionFiles: IFixtureInstructionFile[], agentInstructionFiles: IResolvedAgentFile[] = []): IPromptsService {
|
||||
// Build a map from URI to applyTo for parseNew
|
||||
const applyToMap = new ResourceMap<string | undefined>();
|
||||
const descriptionMap = new ResourceMap<string | undefined>();
|
||||
for (const file of instructionFiles) {
|
||||
applyToMap.set(file.uri, file.applyTo);
|
||||
descriptionMap.set(file.uri, file.description);
|
||||
}
|
||||
|
||||
return new class extends mock<IPromptsService>() {
|
||||
override readonly onDidChangeCustomAgents = Event.None;
|
||||
override readonly onDidChangeSlashCommands = Event.None;
|
||||
@@ -50,14 +62,26 @@ function createMockPromptsService(instructionFiles: IFixtureInstructionFile[], a
|
||||
override getDisabledPromptFiles(): ResourceSet { return new ResourceSet(); }
|
||||
override async listPromptFiles(type: PromptsType) {
|
||||
if (type === PromptsType.instructions) {
|
||||
return instructionFiles.map(f => f.promptPath);
|
||||
return instructionFiles.map(f => ({
|
||||
uri: f.uri,
|
||||
storage: f.storage as PromptsStorage.local,
|
||||
type: f.type,
|
||||
name: f.name,
|
||||
description: f.description,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
override async listAgentInstructions() { return agentInstructionFiles; }
|
||||
override async getCustomAgents() { return []; }
|
||||
override async parseNew(uri: URI, _token: CancellationToken): Promise<ParsedPromptFile> {
|
||||
return new ParsedPromptFile(uri);
|
||||
const applyTo = applyToMap.get(uri);
|
||||
const description = descriptionMap.get(uri);
|
||||
const header = {
|
||||
get applyTo() { return applyTo; },
|
||||
get description() { return description; },
|
||||
};
|
||||
return new ParsedPromptFile(uri, header as never);
|
||||
}
|
||||
}();
|
||||
}
|
||||
@@ -154,20 +178,20 @@ async function renderInstructionsTab(ctx: ComponentFixtureContext, instructionFi
|
||||
// Fixtures
|
||||
// ============================================================================
|
||||
|
||||
export default defineThemedFixtureGroup({ path: 'chat/' }, {
|
||||
export default defineThemedFixtureGroup({ path: 'chat/aiCustomizations/' }, {
|
||||
|
||||
InstructionsTabWithItems: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: ctx => renderInstructionsTab(ctx, [
|
||||
// Always-active instructions (no applyTo)
|
||||
{ promptPath: { uri: URI.file('/workspace/.github/instructions/coding-standards.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Coding Standards', description: 'Repository-wide coding standards' } },
|
||||
{ promptPath: { uri: URI.file('/home/dev/.copilot/instructions/my-style.instructions.md'), storage: PromptsStorage.user, type: PromptsType.instructions, name: 'My Style', description: 'Personal coding style preferences' } },
|
||||
{ uri: URI.file('/workspace/.github/instructions/coding-standards.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Coding Standards', description: 'Repository-wide coding standards' },
|
||||
{ uri: URI.file('/home/dev/.copilot/instructions/my-style.instructions.md'), storage: PromptsStorage.user, type: PromptsType.instructions, name: 'My Style', description: 'Personal coding style preferences' },
|
||||
// Always-included instruction (applyTo: **)
|
||||
{ promptPath: { uri: URI.file('/workspace/.github/instructions/general-guidelines.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'General Guidelines', description: 'General development guidelines' }, applyTo: '**' },
|
||||
{ uri: URI.file('/workspace/.github/instructions/general-guidelines.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'General Guidelines', description: 'General development guidelines', applyTo: '**' },
|
||||
// On-demand instructions (with applyTo pattern)
|
||||
{ promptPath: { uri: URI.file('/workspace/.github/instructions/testing-guidelines.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Testing Guidelines', description: 'Testing best practices' }, applyTo: '**/*.test.ts' },
|
||||
{ promptPath: { uri: URI.file('/workspace/.github/instructions/security-review.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Security Review', description: 'Security review checklist' }, applyTo: 'src/auth/**' },
|
||||
{ promptPath: { uri: URI.file('/home/dev/.copilot/instructions/typescript-rules.instructions.md'), storage: PromptsStorage.extension, type: PromptsType.instructions, name: 'TypeScript Rules', description: 'TypeScript conventions', extension: undefined!, source: undefined! } satisfies IExtensionPromptPath, applyTo: '**/*.ts' },
|
||||
{ uri: URI.file('/workspace/.github/instructions/testing-guidelines.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Testing Guidelines', description: 'Testing best practices', applyTo: '**/*.test.ts' },
|
||||
{ uri: URI.file('/workspace/.github/instructions/security-review.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Security Review', description: 'Security review checklist', applyTo: 'src/auth/**' },
|
||||
{ uri: URI.file('/home/dev/.copilot/instructions/typescript-rules.instructions.md'), storage: PromptsStorage.user, type: PromptsType.instructions, name: 'Typescript Rules', description: 'TypeScript conventions', applyTo: '**/*.ts' },
|
||||
], [
|
||||
// Agent instruction files (AGENTS.md, copilot-instructions.md)
|
||||
{ uri: URI.file('/workspace/AGENTS.md'), realPath: undefined, type: AgentFileType.agentsMd },
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from '../../../../base/browser/dom.js';
|
||||
import { Dimension } from '../../../../base/browser/dom.js';
|
||||
import { IRenderedMarkdown } from '../../../../base/browser/markdownRenderer.js';
|
||||
import { mainWindow } from '../../../../base/browser/window.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { ResourceMap, ResourceSet } from '../../../../base/common/map.js';
|
||||
import { constObservable, observableValue } from '../../../../base/common/observable.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { mock } from '../../../../base/test/common/mock.js';
|
||||
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
|
||||
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IListService, ListService } from '../../../../platform/list/browser/listService.js';
|
||||
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
|
||||
import { IRequestService } from '../../../../platform/request/common/request.js';
|
||||
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { IWorkspace, IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
|
||||
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
|
||||
import { IPathService } from '../../../services/path/common/pathService.js';
|
||||
import { IWorkingCopyService } from '../../../services/workingCopy/common/workingCopyService.js';
|
||||
import { IWebviewService } from '../../../contrib/webview/browser/webview.js';
|
||||
import { IAICustomizationWorkspaceService, AICustomizationManagementSection } from '../../../contrib/chat/common/aiCustomizationWorkspaceService.js';
|
||||
import { CustomizationHarness, ICustomizationHarnessService, IHarnessDescriptor, createVSCodeHarnessDescriptor, createClaudeHarnessDescriptor, createCliHarnessDescriptor, getCliUserRoots, getClaudeUserRoots } from '../../../contrib/chat/common/customizationHarnessService.js';
|
||||
import { PromptsType } from '../../../contrib/chat/common/promptSyntax/promptTypes.js';
|
||||
import { IPromptsService, IResolvedAgentFile, AgentFileType, PromptsStorage } from '../../../contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { ParsedPromptFile } from '../../../contrib/chat/common/promptSyntax/promptFileParser.js';
|
||||
import { IAgentPluginService } from '../../../contrib/chat/common/plugins/agentPluginService.js';
|
||||
import { IPluginMarketplaceService } from '../../../contrib/chat/common/plugins/pluginMarketplaceService.js';
|
||||
import { IPluginInstallService } from '../../../contrib/chat/common/plugins/pluginInstallService.js';
|
||||
import { AICustomizationManagementEditor } from '../../../contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js';
|
||||
import { AICustomizationManagementEditorInput } from '../../../contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js';
|
||||
import { IMcpWorkbenchService, IWorkbenchMcpServer, IMcpService, McpServerInstallState } from '../../../contrib/mcp/common/mcpTypes.js';
|
||||
import { IMcpRegistry } from '../../../contrib/mcp/common/mcpRegistryTypes.js';
|
||||
import { IWorkbenchLocalMcpServer, LocalMcpServerScope } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
|
||||
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from './fixtureUtils.js';
|
||||
|
||||
// Ensure theme colors & widget CSS are loaded
|
||||
import '../../../../platform/theme/common/colors/inputColors.js';
|
||||
import '../../../../platform/theme/common/colors/listColors.js';
|
||||
import '../../../contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css';
|
||||
|
||||
// ============================================================================
|
||||
// Mock helpers
|
||||
// ============================================================================
|
||||
|
||||
const userHome = URI.file('/home/dev');
|
||||
const BUILTIN_STORAGE = 'builtin';
|
||||
|
||||
interface IFixtureFile {
|
||||
readonly uri: URI;
|
||||
readonly storage: PromptsStorage;
|
||||
readonly type: PromptsType;
|
||||
readonly name?: string;
|
||||
readonly description?: string;
|
||||
readonly applyTo?: string;
|
||||
}
|
||||
|
||||
function createMockEditorGroup(): IEditorGroup {
|
||||
return new class extends mock<IEditorGroup>() {
|
||||
override windowId = mainWindow.vscodeWindowId;
|
||||
}();
|
||||
}
|
||||
|
||||
function createMockPromptsService(files: IFixtureFile[], agentInstructions: IResolvedAgentFile[]): IPromptsService {
|
||||
const applyToMap = new ResourceMap<string | undefined>();
|
||||
const descriptionMap = new ResourceMap<string | undefined>();
|
||||
for (const f of files) { applyToMap.set(f.uri, f.applyTo); descriptionMap.set(f.uri, f.description); }
|
||||
return new class extends mock<IPromptsService>() {
|
||||
override readonly onDidChangeCustomAgents = Event.None;
|
||||
override readonly onDidChangeSlashCommands = Event.None;
|
||||
override readonly onDidChangeSkills = Event.None;
|
||||
override readonly onDidChangeInstructions = Event.None;
|
||||
override getDisabledPromptFiles(): ResourceSet { return new ResourceSet(); }
|
||||
override async listPromptFiles(type: PromptsType) {
|
||||
return files.filter(f => f.type === type).map(f => ({
|
||||
uri: f.uri, storage: f.storage as PromptsStorage.local, type: f.type, name: f.name, description: f.description,
|
||||
}));
|
||||
}
|
||||
override async listAgentInstructions() { return agentInstructions; }
|
||||
override async getCustomAgents() {
|
||||
return files.filter(f => f.type === PromptsType.agent).map(a => ({
|
||||
uri: a.uri, name: a.name ?? 'agent', description: a.description, storage: a.storage,
|
||||
source: { storage: a.storage },
|
||||
})) as never[];
|
||||
}
|
||||
override async parseNew(uri: URI, _token: CancellationToken): Promise<ParsedPromptFile> {
|
||||
const header = {
|
||||
get applyTo() { return applyToMap.get(uri); },
|
||||
get description() { return descriptionMap.get(uri); },
|
||||
};
|
||||
return new ParsedPromptFile(uri, header as never);
|
||||
}
|
||||
override async getSourceFolders() { return [] as never[]; }
|
||||
override async findAgentSkills() { return [] as never[]; }
|
||||
override async getPromptSlashCommands() { return [] as never[]; }
|
||||
}();
|
||||
}
|
||||
|
||||
function createMockHarnessService(activeHarness: CustomizationHarness, descriptors: readonly IHarnessDescriptor[]): ICustomizationHarnessService {
|
||||
const active = observableValue('activeHarness', activeHarness);
|
||||
return new class extends mock<ICustomizationHarnessService>() {
|
||||
override readonly activeHarness = active;
|
||||
override readonly availableHarnesses = constObservable(descriptors);
|
||||
override getStorageSourceFilter(type: PromptsType) {
|
||||
const d = descriptors.find(h => h.id === active.get()) ?? descriptors[0];
|
||||
return d.getStorageSourceFilter(type);
|
||||
}
|
||||
override getActiveDescriptor() {
|
||||
return descriptors.find(h => h.id === active.get()) ?? descriptors[0];
|
||||
}
|
||||
override setActiveHarness(id: CustomizationHarness) { active.set(id, undefined); }
|
||||
}();
|
||||
}
|
||||
|
||||
function makeLocalMcpServer(id: string, label: string, scope: LocalMcpServerScope, description?: string): IWorkbenchMcpServer {
|
||||
return new class extends mock<IWorkbenchMcpServer>() {
|
||||
override readonly id = id;
|
||||
override readonly name = id;
|
||||
override readonly label = label;
|
||||
override readonly description = description ?? '';
|
||||
override readonly installState = McpServerInstallState.Installed;
|
||||
override readonly local = new class extends mock<IWorkbenchLocalMcpServer>() {
|
||||
override readonly id = id;
|
||||
override readonly scope = scope;
|
||||
}();
|
||||
}();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Realistic test data — a project that has Copilot + Claude customizations
|
||||
// ============================================================================
|
||||
|
||||
const allFiles: IFixtureFile[] = [
|
||||
// Copilot instructions
|
||||
{ uri: URI.file('/workspace/.github/instructions/coding-standards.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Coding Standards', description: 'Repository-wide coding standards' },
|
||||
{ uri: URI.file('/workspace/.github/instructions/testing.instructions.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Testing', description: 'Testing best practices', applyTo: '**/*.test.ts' },
|
||||
{ uri: URI.file('/home/dev/.copilot/instructions/my-style.instructions.md'), storage: PromptsStorage.user, type: PromptsType.instructions, name: 'My Style', description: 'Personal coding style' },
|
||||
// Claude rules
|
||||
{ uri: URI.file('/workspace/.claude/rules/code-style.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Code Style', description: 'Claude code style rules' },
|
||||
{ uri: URI.file('/workspace/.claude/rules/testing.md'), storage: PromptsStorage.local, type: PromptsType.instructions, name: 'Testing', description: 'Claude testing conventions' },
|
||||
{ uri: URI.file('/home/dev/.claude/rules/personal.md'), storage: PromptsStorage.user, type: PromptsType.instructions, name: 'Personal', description: 'Personal rules' },
|
||||
// Agents
|
||||
{ uri: URI.file('/workspace/.github/agents/reviewer.agent.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'Reviewer', description: 'Code review agent' },
|
||||
{ uri: URI.file('/workspace/.github/agents/documenter.agent.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'Documenter', description: 'Documentation agent' },
|
||||
{ uri: URI.file('/workspace/.claude/agents/planner.md'), storage: PromptsStorage.local, type: PromptsType.agent, name: 'Planner', description: 'Project planning agent' },
|
||||
// Skills
|
||||
{ uri: URI.file('/workspace/.github/skills/deploy/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'Deploy', description: 'Deployment automation' },
|
||||
{ uri: URI.file('/workspace/.github/skills/refactor/SKILL.md'), storage: PromptsStorage.local, type: PromptsType.skill, name: 'Refactor', description: 'Code refactoring patterns' },
|
||||
// Prompts
|
||||
{ uri: URI.file('/workspace/.github/prompts/explain.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Explain', description: 'Explain selected code' },
|
||||
{ uri: URI.file('/workspace/.github/prompts/review.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Review', description: 'Review changes' },
|
||||
];
|
||||
|
||||
const agentInstructions: IResolvedAgentFile[] = [
|
||||
{ uri: URI.file('/workspace/AGENTS.md'), realPath: undefined, type: AgentFileType.agentsMd },
|
||||
{ uri: URI.file('/workspace/CLAUDE.md'), realPath: undefined, type: AgentFileType.claudeMd },
|
||||
{ uri: URI.file('/workspace/.github/copilot-instructions.md'), realPath: undefined, type: AgentFileType.copilotInstructionsMd },
|
||||
];
|
||||
|
||||
const mcpWorkspaceServers = [
|
||||
makeLocalMcpServer('mcp-postgres', 'PostgreSQL', LocalMcpServerScope.Workspace, 'Database access'),
|
||||
makeLocalMcpServer('mcp-github', 'GitHub', LocalMcpServerScope.Workspace, 'GitHub API'),
|
||||
];
|
||||
const mcpUserServers = [
|
||||
makeLocalMcpServer('mcp-web-search', 'Web Search', LocalMcpServerScope.User, 'Search the web'),
|
||||
];
|
||||
const mcpRuntimeServers = [
|
||||
{ definition: { id: 'github-copilot-mcp', label: 'GitHub Copilot' }, collection: { id: 'ext.github.copilot/mcp', label: 'ext.github.copilot/mcp' }, enablement: constObservable(2), connectionState: constObservable({ state: 2 }) },
|
||||
];
|
||||
|
||||
interface IRenderEditorOptions {
|
||||
readonly harness: CustomizationHarness;
|
||||
readonly isSessionsWindow?: boolean;
|
||||
readonly managementSections?: readonly AICustomizationManagementSection[];
|
||||
readonly availableHarnesses?: readonly IHarnessDescriptor[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Render helper — creates the full management editor
|
||||
// ============================================================================
|
||||
|
||||
async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditorOptions): Promise<void> {
|
||||
const width = 900;
|
||||
const height = 600;
|
||||
ctx.container.style.width = `${width}px`;
|
||||
ctx.container.style.height = `${height}px`;
|
||||
|
||||
const isSessionsWindow = options.isSessionsWindow ?? false;
|
||||
const managementSections = options.managementSections ?? [
|
||||
AICustomizationManagementSection.Agents,
|
||||
AICustomizationManagementSection.Skills,
|
||||
AICustomizationManagementSection.Instructions,
|
||||
AICustomizationManagementSection.Hooks,
|
||||
AICustomizationManagementSection.Prompts,
|
||||
AICustomizationManagementSection.McpServers,
|
||||
AICustomizationManagementSection.Plugins,
|
||||
];
|
||||
const availableHarnesses = options.availableHarnesses ?? [
|
||||
createVSCodeHarnessDescriptor([PromptsStorage.extension]),
|
||||
createCliHarnessDescriptor(getCliUserRoots(userHome), []),
|
||||
createClaudeHarnessDescriptor(getClaudeUserRoots(userHome), []),
|
||||
];
|
||||
|
||||
const allMcpServers = [...mcpWorkspaceServers, ...mcpUserServers];
|
||||
|
||||
const instantiationService = createEditorServices(ctx.disposableStore, {
|
||||
colorTheme: ctx.theme,
|
||||
additionalServices: (reg) => {
|
||||
const harnessService = createMockHarnessService(options.harness, availableHarnesses);
|
||||
registerWorkbenchServices(reg);
|
||||
reg.define(IListService, ListService);
|
||||
reg.defineInstance(IPromptsService, createMockPromptsService(allFiles, agentInstructions));
|
||||
reg.defineInstance(IAICustomizationWorkspaceService, new class extends mock<IAICustomizationWorkspaceService>() {
|
||||
override readonly isSessionsWindow = isSessionsWindow;
|
||||
override readonly activeProjectRoot = observableValue('root', URI.file('/workspace'));
|
||||
override readonly hasOverrideProjectRoot = observableValue('hasOverride', false);
|
||||
override getActiveProjectRoot() { return URI.file('/workspace'); }
|
||||
override getStorageSourceFilter(type: PromptsType) { return harnessService.getStorageSourceFilter(type); }
|
||||
override clearOverrideProjectRoot() { }
|
||||
override setOverrideProjectRoot() { }
|
||||
override readonly managementSections = managementSections;
|
||||
override async generateCustomization() { }
|
||||
}());
|
||||
reg.defineInstance(ICustomizationHarnessService, harnessService);
|
||||
reg.defineInstance(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() {
|
||||
override readonly onDidChangeWorkspaceFolders = Event.None;
|
||||
override getWorkspace(): IWorkspace { return { id: 'test', folders: [] }; }
|
||||
override getWorkbenchState(): WorkbenchState { return WorkbenchState.WORKSPACE; }
|
||||
}());
|
||||
reg.defineInstance(IFileService, new class extends mock<IFileService>() {
|
||||
override readonly onDidFilesChange = Event.None;
|
||||
}());
|
||||
reg.defineInstance(IPathService, new class extends mock<IPathService>() {
|
||||
override readonly defaultUriScheme = 'file';
|
||||
override userHome(): URI;
|
||||
override userHome(): Promise<URI>;
|
||||
override userHome(): URI | Promise<URI> { return userHome; }
|
||||
}());
|
||||
reg.defineInstance(ITextModelService, new class extends mock<ITextModelService>() { }());
|
||||
reg.defineInstance(IWorkingCopyService, new class extends mock<IWorkingCopyService>() {
|
||||
override readonly onDidChangeDirty = Event.None;
|
||||
}());
|
||||
reg.defineInstance(IFileDialogService, new class extends mock<IFileDialogService>() { }());
|
||||
reg.defineInstance(IExtensionService, new class extends mock<IExtensionService>() { }());
|
||||
reg.defineInstance(IQuickInputService, new class extends mock<IQuickInputService>() { }());
|
||||
reg.defineInstance(IRequestService, new class extends mock<IRequestService>() { }());
|
||||
reg.defineInstance(IMarkdownRendererService, new class extends mock<IMarkdownRendererService>() {
|
||||
override render() {
|
||||
const rendered: IRenderedMarkdown = {
|
||||
element: DOM.$('span'),
|
||||
dispose() { },
|
||||
};
|
||||
return rendered;
|
||||
}
|
||||
}());
|
||||
reg.defineInstance(IWebviewService, new class extends mock<IWebviewService>() { }());
|
||||
reg.defineInstance(IMcpWorkbenchService, new class extends mock<IMcpWorkbenchService>() {
|
||||
override readonly onChange = Event.None;
|
||||
override readonly onReset = Event.None;
|
||||
override readonly local = allMcpServers;
|
||||
override async queryLocal() { return allMcpServers; }
|
||||
override canInstall() { return true as const; }
|
||||
}());
|
||||
reg.defineInstance(IMcpService, new class extends mock<IMcpService>() {
|
||||
override readonly servers = constObservable(mcpRuntimeServers as never[]);
|
||||
}());
|
||||
reg.defineInstance(IMcpRegistry, new class extends mock<IMcpRegistry>() {
|
||||
override readonly collections = constObservable([]);
|
||||
override readonly delegates = constObservable([]);
|
||||
override readonly onDidChangeInputs = Event.None;
|
||||
}());
|
||||
reg.defineInstance(IAgentPluginService, new class extends mock<IAgentPluginService>() {
|
||||
override readonly plugins = constObservable([]);
|
||||
override readonly enablementModel = undefined as never;
|
||||
}());
|
||||
reg.defineInstance(IPluginMarketplaceService, new class extends mock<IPluginMarketplaceService>() {
|
||||
override readonly installedPlugins = constObservable([]);
|
||||
override readonly onDidChangeMarketplaces = Event.None;
|
||||
}());
|
||||
reg.defineInstance(IPluginInstallService, new class extends mock<IPluginInstallService>() { }());
|
||||
},
|
||||
});
|
||||
|
||||
const editor = ctx.disposableStore.add(
|
||||
instantiationService.createInstance(AICustomizationManagementEditor, createMockEditorGroup())
|
||||
);
|
||||
editor.create(ctx.container);
|
||||
editor.layout(new Dimension(width, height));
|
||||
|
||||
// setInput may fail on unmocked service calls — catch to still show the editor shell
|
||||
try {
|
||||
await editor.setInput(AICustomizationManagementEditorInput.getOrCreate(), undefined, {}, CancellationToken.None);
|
||||
} catch {
|
||||
// Expected in fixture — some services are partially mocked
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Fixtures
|
||||
// ============================================================================
|
||||
|
||||
export default defineThemedFixtureGroup({ path: 'chat/aiCustomizations/' }, {
|
||||
|
||||
// Full editor with Local (VS Code) harness — all sections visible, harness dropdown,
|
||||
// Generate buttons, AGENTS.md shortcut, all storage groups
|
||||
LocalHarness: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: ctx => renderEditor(ctx, { harness: CustomizationHarness.VSCode }),
|
||||
}),
|
||||
|
||||
// Full editor with Copilot CLI harness — no prompts section, CLI-specific
|
||||
// root files and instruction filtering under .github/.copilot paths.
|
||||
CliHarness: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: ctx => renderEditor(ctx, { harness: CustomizationHarness.CLI }),
|
||||
}),
|
||||
|
||||
// Full editor with Claude harness — Prompts+Plugins hidden, Agents visible,
|
||||
// "Add CLAUDE.md" button, "New Rule" dropdown, instruction filtering, bridged MCP badge
|
||||
ClaudeHarness: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: ctx => renderEditor(ctx, { harness: CustomizationHarness.Claude }),
|
||||
}),
|
||||
|
||||
// Sessions-window variant of the full editor with workspace override UX
|
||||
// and sessions section ordering.
|
||||
Sessions: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: ctx => renderEditor(ctx, {
|
||||
harness: CustomizationHarness.CLI,
|
||||
isSessionsWindow: true,
|
||||
availableHarnesses: [
|
||||
createCliHarnessDescriptor(getCliUserRoots(userHome), [BUILTIN_STORAGE]),
|
||||
],
|
||||
managementSections: [
|
||||
AICustomizationManagementSection.Agents,
|
||||
AICustomizationManagementSection.Skills,
|
||||
AICustomizationManagementSection.Instructions,
|
||||
AICustomizationManagementSection.Prompts,
|
||||
AICustomizationManagementSection.Hooks,
|
||||
AICustomizationManagementSection.McpServers,
|
||||
AICustomizationManagementSection.Plugins,
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user