Files
Josh Spicer 7efa1c5c0d chatCustomizations: support grouping and badges for external provider items (#305813)
* feat: enhance AICustomizationListWidget with grouping and badge support for external customization items

* feat: add user data profile service and infer storage from URI in AICustomizationListWidget

* Copilot CLI session 8af2fd4a-10fe-4bba-b408-f1b90cebc8dc changes

* docs: add chatSessionCustomizationProvider API chain to customizations editor skill

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address PR review feedback

- Remove duplicate sectionToIcon, reuse getSectionIcon instance method
- Use Map for O(1) groupKey lookups instead of O(n²) includes/find
- Check active project root in inferStorageFromUri for Sessions window
- Set pluginUri on provider items and use it for storage inference
- Remove redundant plugin check from inferStorageFromUri (handled by caller)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 21:48:27 +00:00

12 KiB
Raw Permalink Blame History

name, description
name description
chat-customizations-editor 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.
  • IExternalCustomizationItemProvider / IExternalCustomizationItem — internal interfaces (in customizationHarnessService.ts) for extension-contributed providers that supply items directly. These mirror the proposed extension API types.

Principle: the UI widgets read everything from the descriptor — no harness-specific conditionals in widget code.

Extension API (chatSessionCustomizationProvider)

The proposed API in src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts lets extensions register customization providers. Changes to IExternalCustomizationItem or IExternalCustomizationItemProvider must be kept in sync across the full chain:

Layer File Type
Extension API vscode.proposed.chatSessionCustomizationProvider.d.ts ChatSessionCustomizationItem
IPC DTO extHost.protocol.ts IChatSessionCustomizationItemDto
ExtHost mapping extHostChatAgents2.ts $provideChatSessionCustomizations()
MainThread mapping mainThreadChatAgents2.ts provideChatSessionCustomizations callback
Internal interface customizationHarnessService.ts IExternalCustomizationItem

When adding fields to IExternalCustomizationItem, update all five layers. The proposed API .d.ts is additive-only (new optional fields are backward-compatible and do not require a version bump).

Testing

Component explorer fixtures (see component-fixtures skill): aiCustomizationListWidget.fixture.ts, aiCustomizationManagementEditor.fixture.ts under src/vs/workbench/test/browser/componentFixtures/.

Screenshotting specific tabs

The management editor fixture supports a selectedSection option to render any tab. Each tab has Dark/Light variants auto-generated by defineThemedFixtureGroup.

Available fixture IDs (use with mcp_component-exp_screenshot):

Fixture ID pattern Tab shown
chat/aiCustomizations/aiCustomizationManagementEditor/AgentsTab/{Dark,Light} Agents
chat/aiCustomizations/aiCustomizationManagementEditor/SkillsTab/{Dark,Light} Skills
chat/aiCustomizations/aiCustomizationManagementEditor/InstructionsTab/{Dark,Light} Instructions
chat/aiCustomizations/aiCustomizationManagementEditor/HooksTab/{Dark,Light} Hooks
chat/aiCustomizations/aiCustomizationManagementEditor/PromptsTab/{Dark,Light} Prompts
chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTab/{Dark,Light} MCP Servers
chat/aiCustomizations/aiCustomizationManagementEditor/PluginsTab/{Dark,Light} Plugins
chat/aiCustomizations/aiCustomizationManagementEditor/LocalHarness/{Dark,Light} Default (Agents, Local harness)
chat/aiCustomizations/aiCustomizationManagementEditor/CliHarness/{Dark,Light} Default (Agents, CLI harness)
chat/aiCustomizations/aiCustomizationManagementEditor/ClaudeHarness/{Dark,Light} Default (Agents, Claude harness)
chat/aiCustomizations/aiCustomizationManagementEditor/Sessions/{Dark,Light} Sessions window variant

Adding a new tab fixture: Add a variant to the defineThemedFixtureGroup in aiCustomizationManagementEditor.fixture.ts:

MyNewTab: defineComponentFixture({
    labels: { kind: 'screenshot' },
    render: ctx => renderEditor(ctx, {
        harness: CustomizationHarness.VSCode,
        selectedSection: AICustomizationManagementSection.MySection,
    }),
}),

The selectedSection calls editor.selectSectionById() after setInput, which navigates to the specified tab and re-layouts.

Populating test data

Each customization type requires its own mock path in createMockPromptsService:

  • AgentsgetCustomAgents() returns agent objects
  • SkillsfindAgentSkills() returns IAgentSkill[]
  • PromptsgetPromptSlashCommands() returns IChatPromptSlashCommand[]
  • Instructions/HookslistPromptFiles() filtered by PromptsType
  • MCP ServersmcpWorkspaceServers/mcpUserServers arrays passed to IMcpWorkbenchService mock
  • PluginsIPluginMarketplaceService.installedPlugins and IAgentPluginService.plugins observables

All test data lives in allFiles (prompt-based items) and the mcpWorkspace/UserServers arrays. Add enough items per category (8+) to invoke scrolling.

Exercising built-in grouping

The list widget regroups items from the default chat extension under a "Built-in" header. Three things must be in place for fixtures to exercise this:

  1. Include BUILTIN_STORAGE in the harness descriptor's visible sources
  2. Mock IProductService.defaultChatAgent.chatExtensionId (e.g., 'GitHub.copilot-chat')
  3. Give mock items extension provenance via extensionId / extensionDisplayName matching that ID

Without all three, built-in regrouping silently doesn't run and the fixture only shows flat lists.

Editor contribution service mocks

The management editor embeds a CodeEditorWidget. Electron-side editor contributions (e.g., AgentFeedbackEditorWidgetContribution) are instantiated automatically and crash if their injected services aren't registered. The fixture must mock at minimum:

  • IAgentFeedbackService — needs onDidChangeFeedback, onDidChangeNavigation as Event.None
  • ICodeReviewService — needs getReviewState() / getPRReviewState() returning idle observables
  • IChatEditingService — needs editingSessionsObs as empty observable
  • IAgentSessionsService — needs model.sessions as empty array

These are cross-layer imports from vs/sessions/ — use // eslint-disable-next-line local/code-import-patterns on the import lines.

CI regression gates

Key fixtures have blocksCi: true in their labels. The screenshot-test.yml GitHub Action captures screenshots on every PR to main and fails the CI status check if any blocks-ci-labeled fixture's screenshot changes. This catches layout regressions automatically.

Currently gated fixtures: LocalHarness, McpServersTab, McpServersTabNarrow, AgentsTabNarrow. When adding a new section or layout-critical fixture, add blocksCi: true:

MyFixture: defineComponentFixture({
    labels: { kind: 'screenshot', blocksCi: true },
    render: ctx => renderEditor(ctx, { ... }),
}),

Don't add blocksCi to every fixture — only ones that cover critical layout paths (default view, section with list + footer, narrow viewport). Too many gated fixtures creates noisy CI.

Screenshot stability

Scrollbar fade transitions cause screenshot instability — the scrollbar shifts from visible to invisible fade class ~2 seconds after a programmatic scroll. After calling revealLastItem() or any scroll action, wait for the transition to complete before the fixture's render promise resolves:

await new Promise(resolve => setTimeout(resolve, 2400));
// Then optionally poll until .scrollbar.vertical loses the 'visible' class

Running unit tests

./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.

Debugging Layout in the Real Product

Component fixtures use mock data and a fixed container size. Layout bugs caused by reflow timing, real data shapes, or narrow window sizes often don't reproduce in fixtures. When a user reports a broken layout, debug in the live Code OSS product.

For launching Code OSS with CDP and connecting agent-browser, see the launch skill. Use --user-data-dir /tmp/code-oss-debug to avoid colliding with an already-running instance from another worktree.

Navigating to the customizations editor

After connecting, use snapshot -i to find the "Open Customizations" button (in the Chat panel header), then click it. To switch sections, use eval with a DOM click since sidebar items aren't interactive refs:

npx agent-browser eval "const items = [...document.querySelectorAll('.section-list-item')]; \
  items.find(el => el.textContent?.includes('MCP'))?.click();"

Inspecting widget layout

agent-browser eval doesn't always print return values. Use document.title as a return channel:

npx agent-browser eval "const w = document.querySelector('.mcp-list-widget'); \
  const lc = w?.querySelector('.mcp-list-container'); \
  const rows = lc?.querySelectorAll('.monaco-list-row'); \
  document.title = 'DBG:rows=' + (rows?.length ?? -1) \
    + ',listH=' + (lc?.offsetHeight ?? -1) \
    + ',seStH=' + (lc?.querySelector('.monaco-scrollable-element')?.style?.height ?? '') \
    + ',wH=' + (w?.offsetHeight ?? -1);"
npx agent-browser eval "document.title" 2>&1

Key diagnostics:

  • rows — fewer than expected means list.layout() never received the correct viewport height.
  • seStH — empty means the list was never properly laid out.
  • listH vs wH — list container height should be widget height minus search bar minus footer.

Common layout issues

Symptom Root cause Fix pattern
List shows 0-1 rows in a tall container layout() bailed out because offsetHeight returned 0 during display:none → visible transition Defer layout via DOM.getWindow(this.element).requestAnimationFrame(...)
Badge or row content clips at right edge Widget container missing overflow: hidden Add overflow: hidden to the widget's CSS class
Items visible in fixture but not in product Fixture uses many mock items; real product has few Add fixture variants with fewer items or narrower dimensions (width/height options)

Fixture vs real product gaps

Fixtures render at a fixed size (default 900×600) with many mock items. They won't catch:

  • Reflow timing — the real product's display:none → visible transition may not have reflowed before layout() fires
  • Narrow windows — add narrow fixture variants (e.g., width: 550, height: 400)
  • Real data counts — a user with 1 MCP server sees very different layout than a fixture with 12