Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 38 additions & 20 deletions src/vs/sessions/AI_CUSTOMIZATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ src/vs/workbench/contrib/chat/browser/aiCustomization/
├── aiCustomizationManagementEditor.ts # SplitView list/editor
├── aiCustomizationManagementEditorInput.ts # Singleton input
├── aiCustomizationListWidget.ts # Search + grouped list + harness toggle
├── aiCustomizationItemSource.ts # Item pipeline: ICustomizationItem → IAICustomizationListItem view model
├── promptsServiceCustomizationItemProvider.ts # Adapts IPromptsService → ICustomizationItemProvider
├── aiCustomizationListWidgetUtils.ts # List item helpers (truncation, etc.)
├── aiCustomizationDebugPanel.ts # Debug diagnostics panel
├── aiCustomizationWorkspaceService.ts # Core VS Code workspace service impl
Expand All @@ -29,7 +31,7 @@ src/vs/workbench/contrib/chat/browser/aiCustomization/

src/vs/workbench/contrib/chat/common/
├── aiCustomizationWorkspaceService.ts # IAICustomizationWorkspaceService + IStorageSourceFilter + BUILTIN_STORAGE
└── customizationHarnessService.ts # ICustomizationHarnessService + ISectionOverride + helpers
└── customizationHarnessService.ts # ICustomizationHarnessService + ICustomizationItem + ICustomizationItemProvider + helpers
```

The tree view and overview live in `vs/sessions` (agent sessions window only):
Expand Down Expand Up @@ -98,6 +100,8 @@ Key properties on the harness descriptor:

| Property | Purpose |
|----------|--------|
| `itemProvider` | `ICustomizationItemProvider` supplying items; when absent, falls back to `PromptsServiceCustomizationItemProvider` |
| `syncProvider` | `ICustomizationSyncProvider` enabling local→remote sync checkboxes |
| `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" |
Expand Down Expand Up @@ -157,16 +161,38 @@ Claude additionally applies:

In core VS Code, customization items contributed by the default chat extension (`productService.defaultChatAgent.chatExtensionId`, typically `GitHub.copilot-chat`) are grouped under the "Built-in" header in the management editor list widget, separate from third-party "Extensions".

This follows the same pattern as the MCP list widget, which determines grouping at the UI layer by inspecting collection sources. The list widget uses `IProductService` to identify the chat extension and sets `groupKey: BUILTIN_STORAGE` on matching items:
`PromptsServiceCustomizationItemProvider` handles this via `applyBuiltinGroupKeys()`: it builds a URI→extension-ID lookup from prompt file metadata, then sets `groupKey: BUILTIN_STORAGE` on items whose extension matches the chat extension ID (checked via the shared `isChatExtensionItem()` utility). The underlying `storage` remains `PromptsStorage.extension` — the grouping is a `groupKey` override that keeps `applyStorageSourceFilter` working while visually distinguishing chat-extension items from third-party extension items.

- **Agents**: checks `agent.source.extensionId` against the chat extension ID
- **Skills**: builds a URI→ExtensionIdentifier lookup from `listPromptFiles(PromptsType.skill)`, then checks each skill's URI
- **Prompts**: checks `command.promptPath.extension?.identifier`
- **Instructions/Hooks**: checks `item.extension?.identifier` via `IPromptPath`
`BUILTIN_STORAGE` is defined in `aiCustomizationWorkspaceService.ts` (common layer) and re-exported by both `aiCustomizationManagement.ts` (browser) and `builtinPromptsStorage.ts` (sessions) for backward compatibility.

The underlying `storage` remains `PromptsStorage.extension` — the grouping is a UI-level override via `groupKey` that keeps `applyStorageSourceFilter` working with existing storage types while visually distinguishing chat-extension items from third-party extension items.
### Management Editor Item Pipeline

`BUILTIN_STORAGE` is defined in `aiCustomizationWorkspaceService.ts` (common layer) and re-exported by both `aiCustomizationManagement.ts` (browser) and `builtinPromptsStorage.ts` (sessions) for backward compatibility.
All customization sources — `IPromptsService`, extension-contributed providers, and AHP remote servers — produce items conforming to the same `ICustomizationItem` contract (defined in `customizationHarnessService.ts`). This contract carries `uri`, `type`, `name`, `description`, optional `storage`, `groupKey`, `badge`, and status fields.

```
promptsService ──→ PromptsServiceCustomizationItemProvider ──→ ICustomizationItem[]
Extension Provider ───────────────────────────────────────→ ICustomizationItem[]
AHP Remote Server ────────────────────────────────────────→ ICustomizationItem[]
CustomizationItemSource (aiCustomizationItemSource.ts)
├── normalizes → IAICustomizationListItem[]
├── expands hooks from file content
└── blends sync overlays when syncProvider present
List Widget renders
```

**Key files:**

- **`aiCustomizationItemSource.ts`** — The browser-side pipeline: `IAICustomizationListItem` (view model), `IAICustomizationItemSource` (data contract), `AICustomizationItemNormalizer` (maps `ICustomizationItem` → view model, inferring storage/grouping from URIs when the provider doesn't supply them), `ProviderCustomizationItemSource` (orchestrates provider + sync + normalizer), and shared utilities (`expandHookFileItems`, `getFriendlyName`, `isChatExtensionItem`).

- **`promptsServiceCustomizationItemProvider.ts`** — Adapts `IPromptsService` to `ICustomizationItemProvider`. Reads agents, skills, instructions, hooks, and prompts from the core service, expands instruction categories and hook entries, applies harness-specific filters (storage sources, workspace subpaths, instruction file patterns), and returns `ICustomizationItem[]` with `storage` set from the authoritative promptsService metadata. Used as the default item provider for harnesses that don't supply their own.

- **`customizationHarnessService.ts`** (common layer) — Defines `ICustomizationItem`, `ICustomizationItemProvider`, `ICustomizationSyncProvider`, and `IHarnessDescriptor`. A harness descriptor optionally carries an `itemProvider`; when absent, the widget falls back to `PromptsServiceCustomizationItemProvider`.

### AgenticPromptsService (Sessions)

Expand Down Expand Up @@ -194,26 +220,18 @@ Skills that are directly invoked by UI elements (toolbar buttons, menu items) ar

### Count Consistency

`customizationCounts.ts` uses the **same data sources** as the list widget's `loadItems()`:

| Type | Data Source | Notes |
|------|-------------|-------|
| Agents | `getCustomAgents()` | Parsed agents, not raw files |
| Skills | `findAgentSkills()` | Parsed skills with frontmatter |
| Prompts | `getPromptSlashCommands()` | Filters out skill-type commands |
| Instructions | `listPromptFiles()` + `listAgentInstructions()` | Includes AGENTS.md, CLAUDE.md etc. |
| Hooks | `listPromptFiles()` | Individual hooks parsed via `parseHooksFromFile()` |
`customizationCounts.ts` uses the **same data sources** as the list widget. Both go through the active harness's `ICustomizationItemProvider` (or the `PromptsServiceCustomizationItemProvider` fallback), ensuring counts match what the list displays.

### Item Badges

`IAICustomizationListItem.badge` is an optional string that renders as a small inline tag next to the item name (same visual style as the MCP "Bridged" badge). For context instructions, this badge shows the raw `applyTo` pattern (e.g. a glob like `**/*.ts`), while the tooltip (`badgeTooltip`) explains the behavior. For skills with UI integrations, the badge reads "UI Integration" with a tooltip describing which UI surface invokes the skill. The badge text is also included in search filtering.

### Debug Panel

Toggle via Command Palette: "Toggle Customizations Debug Panel". Shows a 4-stage pipeline view:
Toggle via Command Palette: "Toggle Customizations Debug Panel". Shows a diagnostic view of the item pipeline:

1. **Raw PromptsService data** — per-storage file lists + type-specific extras
2. **After applyStorageSourceFilter** — what was removed and why
1. **Provider data** — items returned by the active `ICustomizationItemProvider`
2. **After filtering** — what was removed by storage source and workspace subpath filters
3. **Widget state** — allItems vs displayEntries with group counts
4. **Source/resolved folders** — creation targets and discovery order

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js';
import { AICustomizationManagementSection, type IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
import { type IHarnessDescriptor, type IExternalCustomizationItem, type IExternalCustomizationItemProvider } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { type IHarnessDescriptor, type ICustomizationItem, type ICustomizationItemProvider } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import type { IAgentConnection } from '../../../../platform/agentHost/common/agentService.js';
import { ActionType } from '../../../../platform/agentHost/common/state/sessionActions.js';
import { type IAgentInfo, type ICustomizationRef, type ISessionCustomization, CustomizationStatus } from '../../../../platform/agentHost/common/state/sessionState.js';
Expand All @@ -23,7 +23,7 @@ export { AgentCustomizationSyncProvider as RemoteAgentSyncProvider } from '../..

/**
* Maps a {@link CustomizationStatus} enum value to the string literal
* expected by {@link IExternalCustomizationItem.status}.
* expected by {@link ICustomizationItem.status}.
*/
function toStatusString(status: CustomizationStatus | undefined): 'loading' | 'loaded' | 'degraded' | 'error' | undefined {
switch (status) {
Expand All @@ -37,14 +37,14 @@ function toStatusString(status: CustomizationStatus | undefined): 'loading' | 'l

/**
* Provider that exposes a remote agent's customizations as
* {@link IExternalCustomizationItem} entries for the list widget.
* {@link ICustomizationItem} entries for the list widget.
*
* Baseline items come from {@link IAgentInfo.customizations} (available
* without an active session). When a session is active, the provider
* overlays {@link ISessionCustomization} data, which includes loading
* status and enabled state.
*/
export class RemoteAgentCustomizationItemProvider extends Disposable implements IExternalCustomizationItemProvider {
export class RemoteAgentCustomizationItemProvider extends Disposable implements ICustomizationItemProvider {
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;

Expand Down Expand Up @@ -79,7 +79,7 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements
this._onDidChange.fire();
}

async provideChatSessionCustomizations(_token: CancellationToken): Promise<IExternalCustomizationItem[]> {
async provideChatSessionCustomizations(_token: CancellationToken): Promise<ICustomizationItem[]> {
// When a session is active, prefer session-level data (includes status)
if (this._sessionCustomizations) {
return this._sessionCustomizations.map(sc => ({
Expand Down Expand Up @@ -108,7 +108,7 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements
* the agent host protocol.
*
* The descriptor exposes the agent's server-provided customizations through
* an {@link IExternalCustomizationItemProvider} and allows the user to
* an {@link ICustomizationItemProvider} and allows the user to
* select local customizations for syncing via an {@link ICustomizationSyncProvider}.
*/
export function createRemoteAgentHarnessDescriptor(
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/api/browser/mainThreadChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { Dto } from '../../services/extensions/common/proxyIdentifier.js';
import { ExtHostChatAgentsShape2, ExtHostContext, IChatAgentInvokeResult, IChatSessionCustomizationItemDto, IChatSessionCustomizationProviderMetadataDto, IChatNotebookEditDto, IChatParticipantMetadata, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, IHookDto, IInstructionDto, IPluginDto, ISkillDto, ISlashCommandDto, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js';
import { NotebookDto } from './mainThreadNotebookDto.js';
import { isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.js';
import { ICustomizationHarnessService, IExternalCustomizationItem, IExternalCustomizationItemProvider, IHarnessDescriptor } from '../../contrib/chat/common/customizationHarnessService.js';
import { ICustomizationHarnessService, ICustomizationItem, ICustomizationItemProvider, IHarnessDescriptor } from '../../contrib/chat/common/customizationHarnessService.js';
import { AICustomizationManagementSection, BUILTIN_STORAGE } from '../../contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IAgentPluginService } from '../../contrib/chat/common/plugins/agentPluginService.js';
import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';
Expand Down Expand Up @@ -762,14 +762,14 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._customizationProviderEmitters.set(handle, emitter);

// Build the item provider that calls back to the ExtHost
const itemProvider: IExternalCustomizationItemProvider = {
const itemProvider: ICustomizationItemProvider = {
onDidChange: emitter.event,
provideChatSessionCustomizations: async (token) => {
const items = await this._proxy.$provideChatSessionCustomizations(handle, token);
if (!items) {
return undefined;
}
return items.map((item: IChatSessionCustomizationItemDto): IExternalCustomizationItem => ({
return items.map((item: IChatSessionCustomizationItemDto): ICustomizationItem => ({
uri: URI.revive(item.uri),
type: item.type,
name: item.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IPromptsService, PromptsStorage, IPromptPath } from '../../common/promp
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js';
import { AICustomizationManagementSection } from './aiCustomizationManagement.js';
import { IExternalCustomizationItemProvider, IHarnessDescriptor } from '../../common/customizationHarnessService.js';
import { ICustomizationItemProvider, IHarnessDescriptor } from '../../common/customizationHarnessService.js';

/**
* Maps section ID to prompt type. Duplicated from aiCustomizationListWidget
Expand Down Expand Up @@ -100,7 +100,7 @@ export async function generateCustomizationDebugReport(
return lines.join('\n');
}

async function appendExternalProviderData(lines: string[], provider: IExternalCustomizationItemProvider, promptType: PromptsType): Promise<void> {
async function appendExternalProviderData(lines: string[], provider: ICustomizationItemProvider, promptType: PromptsType): Promise<void> {
lines.push('--- External Provider Data ---');

const allItems = await provider.provideChatSessionCustomizations(CancellationToken.None);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
*--------------------------------------------------------------------------------------------*/

import { Codicon } from '../../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { localize } from '../../../../../nls.js';
import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js';
import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';

/**
* Icon for the AI Customization view container (sidebar).
Expand Down Expand Up @@ -76,3 +78,16 @@ export const builtinIcon = registerIcon('ai-customization-builtin', Codicon.star
* Icon for MCP servers.
*/
export const mcpServerIcon = registerIcon('ai-customization-mcp-server', Codicon.server, localize('aiCustomizationMcpServerIcon', "Icon for MCP servers."));

/**
* Returns the icon for a given storage type.
*/
export function storageToIcon(storage: PromptsStorage): ThemeIcon {
switch (storage) {
case PromptsStorage.local: return workspaceIcon;
case PromptsStorage.user: return userIcon;
case PromptsStorage.extension: return extensionIcon;
case PromptsStorage.plugin: return pluginIcon;
default: return instructionsIcon;
}
}
Loading
Loading