--- 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/`. ### 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`: ```typescript 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`: - **Agents** — `getCustomAgents()` returns agent objects - **Skills** — `findAgentSkills()` returns `IAgentSkill[]` - **Prompts** — `getPromptSlashCommands()` returns `IChatPromptSlashCommand[]` - **Instructions/Hooks** — `listPromptFiles()` filtered by `PromptsType` - **MCP Servers** — `mcpWorkspaceServers`/`mcpUserServers` arrays passed to `IMcpWorkbenchService` mock - **Plugins** — `IPluginMarketplaceService.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`: ```typescript 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: ```typescript await new Promise(resolve => setTimeout(resolve, 2400)); // Then optionally poll until .scrollbar.vertical loses the 'visible' class ``` ### Running unit tests ```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. ## 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: ```bash 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: ```bash 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