Write Storybook stories and visual regression tests for the Kilo VS Code extension webview UI
Use this skill when the user asks you to add visual regression tests, screenshot tests, or Storybook stories for components in packages/kilo-vscode/.
Architecture
The VS Code extension uses Storybook + Playwright for visual regression testing:
Storybook stories define UI scenarios using SolidJS components with mock contexts
Playwright auto-discovers all stories, renders each in headless Chromium, and compares screenshots against baseline PNGs using toHaveScreenshot()
Baselines are Linux-only Chromium PNGs stored in tests/visual-regression.spec.ts-snapshots/ (tracked via Git LFS)
The test runner at tests/visual-regression.spec.ts is fully automatic — it fetches ALL stories from the Storybook index and creates one Playwright test per story. You do NOT write Playwright test code. You only write stories.
How to add a visual regression test
Step 1: Decide which story file to use
Stories live in packages/kilo-vscode/webview-ui/src/stories/. Existing files and their scope:
AssistantMessage with tool cards, permissions, questions
prompt-input.stories.tsx
PromptInput (sidebar prompt bar)
settings.stories.tsx
Settings panel, ProvidersTab
history.stories.tsx
SessionList
shared.stories.tsx
ModelSelector and shared controls
Add to an existing file if the component fits. Create a new file only for a genuinely new component area.
Step 2: Write the story
Every story file follows this exact structure:
/** @jsxImportSourcesolid-js *//**
* Stories for [ComponentName].
*/importtype { Meta, StoryObj } from"storybook-solidjs-vite"import { StoryProviders } from"./StoryProviders"// Import the component(s) under testimport { MyComponent } from"../components/path/MyComponent"constmeta: Meta = {
title: "MyCategory", // Becomes the snapshot subdirectory name (lowercased)parameters: { layout: "padded" }, // or "fullscreen"
}
exportdefault meta
typeStory = StoryObjexportconstMyStoryName: Story = {
name: "MyComponent — description of variant",
render: () => (
<StoryProviders><divstyle={{ "max-height": "400px", overflow: "auto" }}><MyComponentsomeProp="value" /></div></StoryProviders>
),
}
Key rules
**Always start with /** @jsxImportSource solid-js \*/** — required for SolidJS JSX compilation.
Always wrap in <StoryProviders> — provides all required contexts (VSCode, Server, Config, Provider, Session, I18n, Dialog, Marked, Data, Diff, Code). Without it, components that call useVSCode(), useSession(), etc. will throw.
Do NOT set an explicit width on the wrapper div. The Playwright viewport is already 420px wide (or 200px for narrow stories). Setting width: "420px" leaves no room for a vertical scrollbar and causes right-side cropping in screenshots. Let the viewport control the width.
Use max-height not height for the wrapper div when you need to constrain vertical size. A fixed height forces a scrollbar even when content is short; max-height avoids unnecessary scrollbars that would eat into the available horizontal space.
Meta title determines the snapshot subdirectory. Use PascalCase or slash-notation (e.g., "Composite/Webview"). Playwright transforms it: "Composite/Webview" becomes composite-webview/ in the snapshots folder.
Export name determines the story ID. The Storybook ID is {lowercase-title}--{kebab-export-name}. For example, title: "Chat" + export const ChatViewIdle produces ID chat--chat-view-idle.
Snapshot path is derived automatically: tests/visual-regression.spec.ts-snapshots/{title-slug}/{variant-slug}.png. Example: chat/chat-view-idle-chromium-linux.png.
StoryProviders props
interfaceStoryProvidersProps {
data?: any// Override mock data (messages, parts, permissions, etc.)permissions?: PermissionRequest[] // Active permission requestsquestions?: QuestionRequest[] // Active question requestsstatus?: string// Session status: "idle" | "busy"sessionID?: string// Custom session IDnoPadding?: boolean// Skip the default 12px padding wrapper
}
Overriding session state
For stories that need custom session behavior (messages, agents, model overrides), use mockSessionValue() and override the SessionContext:
For components that should be tested at multiple widths, create separate stories. Stories whose Storybook ID ends in -200 are automatically rendered at 200px width by the test runner:
exportconstDefault420: Story = {
name: "Default — 420px",
render: () => (
<StoryProviders><MyComponent /></StoryProviders>
),
}
exportconstDefault200: Story = {
name: "Default — 200px", // Storybook ID will end in -200render: () => (
<StoryProviders><MyComponent /></StoryProviders>
),
}
The naming convention with -200 suffix on the export name (e.g., Default200) produces the ID mycategory--default-200, which the test runner detects and uses a 200px viewport for.
The test runner injects CSS to disable all animations and transitions. If a story still produces non-deterministic frames (e.g., a spinner captured at a random rotation), add the story ID to the SKIP set in tests/visual-regression.spec.ts:
Only skip stories as a last resort. Prefer making the story deterministic (e.g., use a static state instead of an animated one).
Step 5: Generate baseline images
Baselines are generated on Linux CI only (font rendering differs on macOS). The CI workflow at .github/workflows/visual-regression.yml auto-runs bun run test:visual:update and commits new baselines to the PR branch.
You do NOT need to generate baseline PNGs locally. Just write the story, push, and CI handles the rest.
To preview stories locally:
# From packages/kilo-vscode/
bun run storybook
# Opens at http://localhost:6007
Reference: snapshot directory structure
Snapshots live at packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/:
The title-slug is derived from the meta title (lowercased, slashes become hyphens). The variant-slug is derived from the story export name (kebab-cased). Chromium and Linux suffixes are appended by Playwright.
The .storybook/preview.tsx applies these via a decorator that calls applyVscodeTheme() / applyKiloTheme() from kilo-ui. Stories do NOT need to handle theming — it happens automatically.
Reference: tool override registration
If your story renders AssistantMessage with tool parts, you may need to register VS Code tool overrides at the top of the file (outside any story), as done in composite.stories.tsx: