updates component explorer

This commit is contained in:
Henning Dieterichs
2026-02-23 20:45:02 +01:00
committed by Henning Dieterichs
parent 0f688387c1
commit 5c4204e60d
12 changed files with 282 additions and 52 deletions

View File

@@ -24,6 +24,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
- name: Setup Node.js
uses: actions/setup-node@v4

9
.vscode/launch.json vendored
View File

@@ -604,12 +604,13 @@
},
{
"name": "Component Explorer",
"type": "chrome",
"type": "msedge",
"port": 9230,
"request": "launch",
"url": "http://localhost:5199/___explorer",
"preLaunchTask": "Launch Monaco Editor Vite",
"url": "http://localhost:5337/___explorer",
"preLaunchTask": "Launch Component Explorer",
"presentation": {
"group": "monaco",
"group": "1_component_explorer",
"order": 4
}
},

20
.vscode/mcp.json vendored
View File

@@ -3,9 +3,25 @@
"vscode-automation-mcp": {
"type": "stdio",
"command": "npm",
// Look at the [README](../test/mcp/README.md) to see what arguments are supported
"args": ["run", "start-stdio"],
"args": [
"run",
"start-stdio"
],
"cwd": "${workspaceFolder}/test/mcp"
},
"component-explorer": {
"type": "stdio",
"command": "npx",
"cwd": "${workspaceFolder}",
"args": [
"component-explorer",
"mcp",
"-c",
"./test/componentFixtures/component-explorer.json",
"--no-daemon-autostart",
"--no-daemon-hint",
"Start the daemon by running the 'Launch Component Explorer' VS Code task (use the run_task tool). When you start the task, try up to 4 times to give the daemon enough time to start."
]
}
},
"inputs": []

7
.vscode/tasks.json vendored
View File

@@ -357,6 +357,13 @@
"problemMatcher": [
"$tsc"
]
},
{
"label": "Launch Component Explorer",
"type": "shell",
"command": "npx component-explorer serve -c ./test/componentFixtures/component-explorer.json",
"isBackground": true,
"problemMatcher": []
}
]
}

View File

@@ -72,7 +72,7 @@ interface RenderAiStatsOptions extends ComponentFixtureContext {
data: IAiStatsHoverData;
}
function renderAiStatsHover({ container, disposableStore, data }: RenderAiStatsOptions): HTMLElement {
function renderAiStatsHover({ container, disposableStore, data }: RenderAiStatsOptions): void {
container.style.width = '320px';
container.style.padding = '8px';
container.style.backgroundColor = 'var(--vscode-editorHoverWidget-background)';
@@ -87,6 +87,4 @@ function renderAiStatsHover({ container, disposableStore, data }: RenderAiStatsO
const elem = hover.keepUpdated(disposableStore).element;
container.appendChild(elem);
return container;
}

View File

@@ -113,7 +113,7 @@ const themedProgressBarOptions = {
// Buttons
// ============================================================================
function renderButtons({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
function renderButtons({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
@@ -162,11 +162,9 @@ function renderButtons({ container, disposableStore }: ComponentFixtureContext):
const disabledSecondary = disposableStore.add(new Button(disabledSection, { ...themedButtonStyles, secondary: true, title: 'Disabled Secondary', disabled: true }));
disabledSecondary.label = 'Disabled Secondary';
disabledSecondary.enabled = false;
return container;
}
function renderButtonBar({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
function renderButtonBar({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
@@ -193,8 +191,6 @@ function renderButtonBar({ container, disposableStore }: ComponentFixtureContext
const buttonWithDesc = disposableStore.add(new ButtonWithDescription(descContainer, { ...themedButtonStyles, title: 'Install Extension', supportIcons: true }));
buttonWithDesc.label = '$(extensions) Install Extension';
buttonWithDesc.description = 'This will install the extension and enable it globally';
return container;
}
@@ -202,7 +198,7 @@ function renderButtonBar({ container, disposableStore }: ComponentFixtureContext
// Toggles and Checkboxes
// ============================================================================
function renderToggles({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
function renderToggles({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
@@ -266,8 +262,6 @@ function renderToggles({ container, disposableStore }: ComponentFixtureContext):
checkboxSection.appendChild(createCheckboxRow('Enable auto-save', true));
checkboxSection.appendChild(createCheckboxRow('Show line numbers', true));
checkboxSection.appendChild(createCheckboxRow('Word wrap', false));
return container;
}
@@ -275,7 +269,7 @@ function renderToggles({ container, disposableStore }: ComponentFixtureContext):
// Input Boxes
// ============================================================================
function renderInputBoxes({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
function renderInputBoxes({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
@@ -321,8 +315,6 @@ function renderInputBoxes({ container, disposableStore }: ComponentFixtureContex
}));
errorInput.value = 'invalid-email';
errorInput.validate();
return container;
}
@@ -330,7 +322,7 @@ function renderInputBoxes({ container, disposableStore }: ComponentFixtureContex
// Count Badges
// ============================================================================
function renderCountBadges({ container }: ComponentFixtureContext): HTMLElement {
function renderCountBadges({ container }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.gap = '12px';
@@ -353,8 +345,6 @@ function renderCountBadges({ container }: ComponentFixtureContext): HTMLElement
new CountBadge(badgeContainer, { count }, themedBadgeStyles);
container.appendChild(badgeContainer);
}
return container;
}
@@ -362,7 +352,7 @@ function renderCountBadges({ container }: ComponentFixtureContext): HTMLElement
// Action Bar
// ============================================================================
function renderActionBar({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
function renderActionBar({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
@@ -410,8 +400,6 @@ function renderActionBar({ container, disposableStore }: ComponentFixtureContext
new Action('action.disabled', 'Disabled', ThemeIcon.asClassName(Codicon.debugPause), false, async () => { }),
new Action('action.enabled2', 'Enabled', ThemeIcon.asClassName(Codicon.debugStop), true, async () => { }),
]);
return container;
}
@@ -419,7 +407,7 @@ function renderActionBar({ container, disposableStore }: ComponentFixtureContext
// Progress Bar
// ============================================================================
function renderProgressBars({ container, disposableStore }: ComponentFixtureContext): HTMLElement {
function renderProgressBars({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
@@ -470,8 +458,6 @@ function renderProgressBars({ container, disposableStore }: ComponentFixtureCont
const doneBar = disposableStore.add(new ProgressBar(doneSection, themedProgressBarOptions));
doneBar.total(100);
doneBar.worked(100);
return container;
}
@@ -479,7 +465,7 @@ function renderProgressBars({ container, disposableStore }: ComponentFixtureCont
// Highlighted Label
// ============================================================================
function renderHighlightedLabels({ container }: ComponentFixtureContext): HTMLElement {
function renderHighlightedLabels({ container }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
@@ -511,6 +497,4 @@ function renderHighlightedLabels({ container }: ComponentFixtureContext): HTMLEl
container.appendChild(createHighlightedLabel('inlineCompletionsController.ts', [{ start: 6, end: 10 }])); // "Comp"
container.appendChild(createHighlightedLabel('diffEditorViewModel.ts', [{ start: 0, end: 4 }, { start: 10, end: 14 }])); // "diff" and "View"
container.appendChild(createHighlightedLabel('workbenchTestServices.ts', [{ start: 9, end: 13 }])); // "Test"
return container;
}

View File

@@ -31,7 +31,7 @@ console.log(greet('World'));
console.log(\`Count: \${counter.count}\`);
`;
function renderCodeEditor({ container, disposableStore, theme }: ComponentFixtureContext): HTMLElement {
function renderCodeEditor({ container, disposableStore, theme }: ComponentFixtureContext): void {
container.style.width = '600px';
container.style.height = '400px';
container.style.border = '1px solid var(--vscode-editorWidget-border)';
@@ -66,8 +66,6 @@ function renderCodeEditor({ container, disposableStore, theme }: ComponentFixtur
));
editor.setModel(model);
return container;
}
export default defineThemedFixtureGroup({

View File

@@ -95,6 +95,7 @@ import '../../../../platform/theme/common/colors/editorColors.js';
import '../../../../platform/theme/common/colors/listColors.js';
import '../../../../platform/theme/common/colors/miscColors.js';
import '../../../common/theme.js';
import { isThenable } from '../../../../base/common/async.js';
/**
* A storage service that never stores anything and always returns the default/fallback value.
@@ -478,7 +479,7 @@ export interface ComponentFixtureContext {
}
export interface ComponentFixtureOptions {
render: (context: ComponentFixtureContext) => HTMLElement | Promise<HTMLElement>;
render: (context: ComponentFixtureContext) => void | Promise<void>;
}
type ThemedFixtures = ReturnType<typeof defineFixtureVariants>;
@@ -486,6 +487,10 @@ type ThemedFixtures = ReturnType<typeof defineFixtureVariants>;
/**
* Creates Dark and Light fixture variants from a single render function.
* The render function receives a context with container and disposableStore.
*
* Note: If render returns a Promise, the async work will run in background.
* Component-explorer waits 2 animation frames after sync render returns,
* which should be sufficient for most async setup, but timing is not guaranteed.
*/
export function defineComponentFixture(options: ComponentFixtureOptions): ThemedFixtures {
const createFixture = (theme: typeof darkTheme | typeof lightTheme) => defineFixture({
@@ -496,8 +501,9 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed
render: (container: HTMLElement) => {
const disposableStore = new DisposableStore();
setupTheme(container, theme);
options.render({ container, disposableStore, theme });
return disposableStore;
// Start render (may be async) - component-explorer will wait 2 rAF after this returns
const result = options.render({ container, disposableStore, theme });
return isThenable(result) ? result.then(() => disposableStore) : disposableStore;
},
});

View File

@@ -33,7 +33,7 @@ interface InlineEditOptions extends ComponentFixtureContext {
editorOptions?: IEditorOptions;
}
function renderInlineEdit(options: InlineEditOptions): HTMLElement {
function renderInlineEdit(options: InlineEditOptions): void {
const { container, disposableStore, theme } = options;
container.style.width = options.width ?? '500px';
container.style.height = options.height ?? '170px';
@@ -100,8 +100,6 @@ function renderInlineEdit(options: InlineEditOptions): HTMLElement {
// Trigger inline completions
const controller = InlineCompletionsController.get(editor);
controller?.model?.get();
return container;
}

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { mainWindow } from '../../../../base/browser/window.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Event } from '../../../../base/common/event.js';
import { ResourceSet } from '../../../../base/common/map.js';
@@ -85,15 +86,7 @@ export default defineThemedFixtureGroup({
}),
});
async function renderPromptFilePickerFixture({ container, disposableStore, theme, type, placeholder, seedData }: RenderPromptPickerOptions): Promise<HTMLElement> {
container.style.width = 'fit-content';
container.style.minHeight = '0';
container.style.padding = '8px';
container.style.boxSizing = 'border-box';
container.style.background = 'var(--vscode-editor-background)';
container.style.border = '1px solid var(--vscode-editorWidget-border)';
container.style.position = 'relative';
async function renderPromptFilePickerFixture({ container, disposableStore, theme, type, placeholder, seedData }: RenderPromptPickerOptions): Promise<void> {
const quickInputHost = document.createElement('div');
quickInputHost.style.position = 'relative';
const hostWidth = 800;
@@ -207,5 +200,45 @@ async function renderPromptFilePickerFixture({ container, disposableStore, theme
type,
});
return container;
// Wait for the quickpick widget to render and have dimensions
const quickInputWidget = await waitForElement<HTMLElement>(
quickInputHost,
'.quick-input-widget',
el => el.offsetWidth > 0 && el.offsetHeight > 0
);
if (quickInputWidget) {
// Reset positioning
quickInputWidget.style.position = 'relative';
quickInputWidget.style.top = '0';
quickInputWidget.style.left = '0';
// Move widget to container and remove host
container.appendChild(quickInputWidget);
quickInputHost.remove();
// Set explicit dimensions on container to match widget
const rect = quickInputWidget.getBoundingClientRect();
container.style.width = `${rect.width}px`;
container.style.height = `${rect.height}px`;
}
}
async function waitForElement<T extends HTMLElement>(
root: HTMLElement,
selector: string,
condition: (el: T) => boolean,
timeout = 2000
): Promise<T | null> {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = root.querySelector<T>(selector);
if (el && condition(el)) {
// Wait one more frame to ensure layout is complete
await new Promise(resolve => mainWindow.requestAnimationFrame(resolve));
return el;
}
await new Promise(resolve => setTimeout(resolve, 10));
}
return root.querySelector<T>(selector);
}

View File

@@ -0,0 +1,183 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"screenshotDir": {
"description": "Directory for storing screenshots (default: .screenshots)",
"type": "string"
},
"sessions": {
"minItems": 1,
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Unique session name"
},
"source": {
"anyOf": [
{
"type": "string",
"const": "current"
},
{
"type": "object",
"properties": {
"worktree": {
"type": "object",
"properties": {
"ref": {
"type": "string",
"description": "Git ref (branch, tag, or commit) to check out"
},
"name": {
"description": "Directory name for the worktree (default: component-explorer-baseline)",
"type": "string"
},
"install": {
"anyOf": [
{
"type": "string",
"const": "auto"
},
{
"type": "string",
"const": "npm"
},
{
"type": "string",
"const": "pnpm"
},
{
"type": "string",
"const": "yarn"
},
{
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Custom install command to run in the worktree"
}
},
"required": [
"command"
],
"additionalProperties": false
},
{
"type": "boolean",
"const": false
}
],
"description": "Dependency install strategy for the worktree"
}
},
"required": [
"ref"
],
"additionalProperties": false,
"description": "Git worktree configuration for a baseline session"
}
},
"required": [
"worktree"
],
"additionalProperties": false
}
],
"description": "Session source: \"current\" for the working tree, or a worktree config for a baseline"
},
"viteConfig": {
"description": "Path to vite config file, relative to this config (overrides top-level viteConfig)",
"type": "string"
}
},
"required": [
"name"
],
"additionalProperties": false,
"description": "A component explorer session"
},
"description": "List of explorer sessions"
},
"compare": {
"type": "object",
"properties": {
"baseline": {
"type": "string",
"description": "Session name to use as the baseline for comparisons"
},
"current": {
"type": "string",
"description": "Session name to use as the current version for comparisons"
}
},
"required": [
"baseline",
"current"
],
"additionalProperties": false,
"description": "Screenshot comparison configuration"
},
"viteConfig": {
"description": "Default vite config file path, relative to this config (default: vite.config.ts)",
"type": "string"
},
"vite": {
"type": "object",
"properties": {
"hmr": {
"type": "object",
"properties": {
"allowedPaths": {
"type": "array",
"items": {
"type": "string"
},
"description": "Glob patterns for files that keep HMR; everything else triggers a full reload"
}
},
"required": [
"allowedPaths"
],
"additionalProperties": false,
"description": "Vite HMR configuration"
}
},
"additionalProperties": false,
"description": "Vite configuration overrides"
},
"redirection": {
"type": "object",
"properties": {
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "Port for the redirection HTTP server"
},
"host": {
"description": "Host to bind the redirection server to (default: localhost)",
"type": "string"
}
},
"required": [
"port"
],
"additionalProperties": false,
"description": "HTTP redirection server that redirects to the session URL"
},
"$schema": {
"type": "string",
"description": "URL of the JSON Schema for this config file"
}
},
"required": [
"sessions"
],
"additionalProperties": false,
"description": "Component Explorer configuration"
}

View File

@@ -1,10 +1,14 @@
{
"$schema": "./component-explorer-config.schema.json",
"screenshotDir": ".screenshots",
"sessions": [
{
"name": "current"
}
],
"redirection": {
"port": 5337
},
"viteConfig": "../../build/vite/vite.config.ts",
"vite": {
"hmr": {