From 66d440dd9127adcfc6b46517e9b5fa693baea9b0 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:51:15 +0000 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=A4=96=20ci:=20migrate=20visual=20r?= =?UTF-8?q?egression=20testing=20from=20Chromatic=20to=20Coder=20Pixel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Chromatic workflow with @coder/pixel-storybook 0.2.0 uploading to pixel.coder.com. Story-level parameters.chromatic (modes/delay/ disableSnapshot/hasTouch) become parameters.pixel (matrix/exclude), runtime detection switches to window.__PIXEL__, and PR readiness scripts now ignore the Pixel / Review commit status instead of Chromatic statuses. The CI job skips cleanly until the PIXEL_KEY secret and a real platform project ID exist. --- .github/workflows/chromatic.yml | 39 ------ .github/workflows/pixel.yml | 46 +++++++ .../generate-readme-screenshots/SKILL.md | 8 +- .storybook/preview.tsx | 30 ++--- Makefile | 6 +- bun.lock | 31 ++++- chromatic.config.json | 8 -- docs/AGENTS.md | 4 +- package.json | 1 + pixel.jsonc | 19 +++ rfc/dead-code-cleanup-roadmap.md | 2 +- scripts/extract_pr_logs.sh | 12 +- scripts/lib/pr_check_filters.sh | 12 +- scripts/wait_pr_checks.sh | 32 ++--- scripts/wait_pr_ready.sh | 8 +- .../components/AIView/AIView.stories.tsx | 4 +- .../AgentListItem/AgentListItem.stories.tsx | 4 +- src/browser/components/ChatPane/ChatPane.tsx | 8 +- .../DebugLlmRequestModal.stories.tsx | 4 +- .../LeftSidebar/LeftSidebar.stories.tsx | 12 +- .../ProjectCreateModal.stories.tsx | 4 +- .../ProjectPage/ProjectPage.stories.tsx | 33 ++--- .../ProjectSidebar/ProjectSidebar.stories.tsx | 12 +- .../components/TitleBar/TitleBar.stories.tsx | 11 +- .../WorkspaceHeartbeatModal.stories.tsx | 34 +---- .../WorkspaceMCPModal.stories.tsx | 3 - .../WorkspaceMenuBar.stories.tsx | 17 +-- src/browser/features/AIElements/Shimmer.tsx | 1 - .../features/ChatInput/ChatInput.stories.tsx | 2 +- .../ChatInput/placeholderTips.test.ts | 4 +- .../features/ChatInput/placeholderTips.ts | 4 +- .../InterruptedBarrier.stories.tsx | 4 +- .../StreamingBarrierView.stories.tsx | 2 +- .../Messages/CompactionBackground.tsx | 1 - .../Messages/MessageRenderer.stories.tsx | 8 +- .../Messages/TranscriptDensity.stories.tsx | 6 +- .../CodeReview/HunkViewer.stories.tsx | 8 +- .../ImmersiveReviewView.stories.tsx | 11 +- .../DevToolsTab/DevToolsStepCard.stories.tsx | 4 +- .../features/RightSidebar/GoalTab.stories.tsx | 4 +- .../RightSidebar/PlanFileDialog.stories.tsx | 9 +- .../RightSidebar/RightSidebar.stories.tsx | 11 +- .../Sections/GovernorSection.stories.tsx | 2 +- .../Sections/MCPSettingsSection.stories.tsx | 3 - .../Sections/ProvidersSection.stories.tsx | 6 +- .../Settings/Sections/settingsStoryUtils.tsx | 2 +- .../Tools/AgentSkillListToolCall.stories.tsx | 8 +- .../Tools/AttachFileToolCall.stories.tsx | 2 +- .../Tools/CodeExecutionToolCall.stories.tsx | 2 +- .../Tools/GoogleSearchToolCall.stories.tsx | 2 +- .../Tools/HeartbeatToolCall.stories.tsx | 10 +- .../ProposePlanToolCall.stories.tsx | 34 +++-- .../Tools/ToolSearchToolCall.stories.tsx | 6 +- .../WorkflowRunToolCall.timeout.stories.tsx | 4 +- .../WorkspaceLifecycleToolCall.stories.tsx | 10 +- .../stories/App.commandPalette.stories.tsx | 4 +- .../stories/App.phoneViewports.stories.tsx | 58 +++------ .../stories/App.readmeScreenshots.stories.tsx | 22 +--- src/browser/stories/meta.tsx | 24 +--- src/browser/stories/mocks/version.ts | 2 +- src/browser/styles/globals.css | 4 +- src/browser/utils/ui/workspaceFiltering.ts | 2 +- .../builtInSkillContent.generated.ts | 4 +- tests/ui/storybook/budget.test.ts | 121 +++++------------- tests/ui/storybook/coverage.test.ts | 63 ++++----- 65 files changed, 351 insertions(+), 527 deletions(-) delete mode 100644 .github/workflows/chromatic.yml create mode 100644 .github/workflows/pixel.yml delete mode 100644 chromatic.config.json create mode 100644 pixel.jsonc diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml deleted file mode 100644 index eaca9c5f6e..0000000000 --- a/.github/workflows/chromatic.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Chromatic - -on: - push: - branches: - - main - pull_request: - branches: ["**"] - -jobs: - chromatic: - name: Visual Regression Testing - runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} - # Skip for forked PRs since they don't have access to secrets - if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' - steps: - - name: Checkout code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 # Required for Chromatic to track changes - persist-credentials: false - - - uses: ./.github/actions/setup-mux - - - name: Generate version file - run: ./scripts/generate-version.sh - - - name: Mock version for stable visual testing - run: cp src/browser/stories/mocks/version.ts src/version.ts - - - name: Build Storybook - run: bun x storybook build --stats-json - - - name: Run Chromatic - uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5 - with: - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - exitZeroOnChanges: true - onlyChanged: true diff --git a/.github/workflows/pixel.yml b/.github/workflows/pixel.yml new file mode 100644 index 0000000000..96fe76a064 --- /dev/null +++ b/.github/workflows/pixel.yml @@ -0,0 +1,46 @@ +name: Pixel + +on: + push: + branches: + - main + pull_request: + branches: ["**"] + +jobs: + pixel: + name: Visual Regression Testing + runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} + # Skip for forked PRs since they don't have access to secrets + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' + env: + PIXEL_KEY: ${{ secrets.PIXEL_KEY }} + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - uses: ./.github/actions/setup-mux + + - uses: ./.github/actions/setup-playwright + + - name: Generate version file + run: ./scripts/generate-version.sh + + - name: Mock version for stable visual testing + run: cp src/browser/stories/mocks/version.ts src/version.ts + + - name: Snapshot + if: env.PIXEL_KEY != '' + run: bun x pixel-storybook + env: + # On pull_request events GITHUB_SHA is the merge commit; report the + # head SHA so the Pixel commit status lands on the PR. + PIXEL_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + # Auto-approve main builds so they become baselines without review. + PIXEL_AUTO_REVIEW: ${{ github.event_name == 'push' && 'true' || 'false' }} + + - name: Notice when Pixel is not configured + if: env.PIXEL_KEY == '' + run: echo "PIXEL_KEY secret not configured; skipping Pixel snapshot upload." diff --git a/.mux/skills/generate-readme-screenshots/SKILL.md b/.mux/skills/generate-readme-screenshots/SKILL.md index 9fdcfff330..f81cd46743 100644 --- a/.mux/skills/generate-readme-screenshots/SKILL.md +++ b/.mux/skills/generate-readme-screenshots/SKILL.md @@ -2,9 +2,9 @@ name: generate-readme-screenshots description: >- Regenerate high-resolution README screenshots from Storybook stories. - Use this skill when Chromatic detects visual diffs in any story under + Use this skill when visual diffs are detected in any story under "Docs/README Screenshots", or when story data/layout changes require - updated documentation assets. Triggers on: Chromatic visual regressions + updated documentation assets. Triggers on: visual regressions in readme screenshot stories, changes to App.readmeScreenshots.stories.tsx, changes to mockFactory.ts that affect screenshot stories, or explicit user request to update README images. @@ -17,7 +17,7 @@ description: >- The README screenshots in `docs/img/*.webp` are **not manually captured** — they are deterministically generated from Storybook stories defined in `src/browser/stories/App.readmeScreenshots.stories.tsx`. When any of these stories -change visually (detected by Chromatic or local inspection), the corresponding WebP +change visually (detected in review or local inspection), the corresponding WebP assets must be regenerated and committed. ## Quick Reference @@ -174,7 +174,7 @@ git commit -m "regenerate README screenshots at 3800px" ## When to Run This -1. **Chromatic flags visual changes** in any `Docs/README Screenshots` story +1. **Visual changes are flagged** in any `Docs/README Screenshots` story 2. **Story data changes** in `App.readmeScreenshots.stories.tsx` or `mockFactory.ts` 3. **UI component changes** that affect the visual appearance of screenshot stories 4. **Explicit user request** to refresh README images diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 6946d07464..1ec7d158c3 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -13,7 +13,7 @@ import { NOW } from "../src/browser/stories/storyTime"; import { updatePersistedState } from "../src/browser/hooks/usePersistedState"; import { configure } from "storybook/test"; -// Signal Storybook runtime to modules that need to stabilize for Chromatic +// Signal Storybook runtime to modules that need to stabilize for visual snapshots // (e.g. the ChatInput placeholder tip carousel pins to its lead tip so // tip-list reorders don't cascade into baseline diffs across every story). // Set as early as possible so it precedes any story-module import that might @@ -39,7 +39,7 @@ function ensureStorybookFontsReady(): Promise { const fonts = document.fonts; - // Trigger load of layout-affecting fonts so Chromatic doesn't snapshot mid font-swap. + // Trigger load of layout-affecting fonts so snapshots aren't captured mid font-swap. await Promise.allSettled([ fonts.load("400 14px 'Geist'"), fonts.load("600 14px 'Geist'"), @@ -130,10 +130,10 @@ const preview: Preview = { decorators: [ // Theme provider (Story, context) => { - // Default to dark if mode not set (e.g., Chromatic headless browser defaults to light) + // Default to dark if mode not set (headless snapshot browsers default to light) const mode = (context.globals.theme as ThemeMode | undefined) ?? "dark"; - // Apply theme synchronously before React renders - critical for Chromatic snapshots + // Apply theme synchronously before React renders - critical for visual snapshots if (typeof document !== "undefined") { document.documentElement.dataset.theme = mode; document.documentElement.style.colorScheme = mode; @@ -208,21 +208,13 @@ const preview: Preview = { }, }, }, - // Chromatic modes STACK across project → component → story levels (they are - // NOT overridden by more specific levels). A project-level `modes` map here - // would therefore be added to EVERY story's snapshot set on top of whatever - // each meta/story declares, multiplying the whole budget. Concretely, a - // global { dark, light } stacked onto appMeta's `dark-desktop` produced 3 - // snapshots per App story (dark + light + dark-desktop) instead of the 1 the - // meta intended — silently inflating the Chromatic snapshot budget. - // - // We intentionally declare NO project-level modes. Stories then snapshot in: - // - the modes their meta/story explicitly declare (e.g. CHROMATIC_SINGLE_MODE, - // CHROMATIC_SMOKE_MODES), or - // - a single default snapshot (the default `theme: "dark"` global) when none - // are declared. - // Dual-theme coverage is opt-in per file/story via CHROMATIC_SMOKE_MODES, which - // matches the documented policy in src/browser/stories/meta.tsx. + // Pixel snapshots each story once by default: pixel.jsonc declares a single + // chrome/laptop variant with no themes, so stories render with the default + // `theme: "dark"` global. Dual-theme or viewport coverage is opt-in per + // meta/story via parameters.pixel.matrix (e.g. PIXEL_DUAL_THEME in + // src/browser/stories/meta.tsx). Story-level matrix axes REPLACE the + // top-level axes rather than stacking, so opting one story in never + // multiplies other stories' snapshot counts. }, }; diff --git a/Makefile b/Makefile index db3eade06e..67f58c387d 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ include fmt.mk .PHONY: dist dist-mac dist-win dist-linux install-mac-arm64 check-appimage-icons check-mac-attach-file-runtime .PHONY: vscode-ext vscode-ext-install .PHONY: docs-server check-docs-links -.PHONY: storybook storybook-run storybook-build test-storybook chromatic +.PHONY: storybook storybook-run storybook-build test-storybook .PHONY: benchmark-terminal .PHONY: ensure-deps rebuild-native mux .PHONY: check-eager-imports check-bundle-size check-startup @@ -530,10 +530,6 @@ test-storybook: node_modules/.installed ## Run Storybook interaction tests (requ @# Storybook story transitions can exceed Jest's default 15s timeout on loaded CI runners. @bun x test-storybook --testTimeout 30000 -chromatic: node_modules/.installed ## Run Chromatic for visual regression testing - $(check_node_version) - @bun x chromatic --exit-zero-on-changes - ## Benchmarks benchmark-terminal: ## Run Terminal-Bench 2.0 with Harbor (use TB_HARBOR_PACKAGE/TB_HARBOR_DAYTONA_PACKAGE/TB_DATASET/TB_CONCURRENCY/TB_TIMEOUT/TB_ENV/TB_MODEL/TB_ARGS to customize) @# Pin Harbor with the Daytona extra so scheduled ingestion does not break on future CLI or adapter API drift. diff --git a/bun.lock b/bun.lock index 5928c1c5a5..156e39eea3 100644 --- a/bun.lock +++ b/bun.lock @@ -110,6 +110,7 @@ "@babel/preset-env": "^7.28.5", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", + "@coder/pixel-storybook": "0.2.0", "@electron/rebuild": "^4.0.4", "@eslint/js": "^9.36.0", "@playwright/test": "^1.56.0", @@ -559,6 +560,8 @@ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + "@coder/pixel-storybook": ["@coder/pixel-storybook@0.2.0", "", { "dependencies": { "get-port-please": "3.1.2", "jsonc-parser": "^3.3.1", "serve-handler": "6.1.7", "zod": "3.23.8" }, "peerDependencies": { "playwright-core": ">=1.47.2", "storybook": ">=8.0.0" }, "bin": { "pixel-storybook": "build/bin.js" } }, "sha512-TrZ5Xp6az5NUmp8jjmySwBBY9NBT/PL9eE2M0N3FvQC31BhaaH2Jyk6aDyJ88zGLO0DWvGueimDI1GTDDO5ZZg=="], + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], @@ -1761,7 +1764,7 @@ "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="], "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], @@ -2317,6 +2320,8 @@ "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + "get-port-please": ["get-port-please@3.1.2", "", {}, "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], @@ -3065,13 +3070,15 @@ "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "path-is-inside": ["path-is-inside@1.0.2", "", {}, "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "path-to-regexp": ["path-to-regexp@3.3.0", "", {}, "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="], "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], @@ -3353,6 +3360,8 @@ "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "serve-handler": ["serve-handler@6.1.7", "", { "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", "mime-types": "2.1.18", "minimatch": "3.1.5", "path-is-inside": "1.0.2", "path-to-regexp": "3.3.0", "range-parser": "1.2.0" } }, "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg=="], + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], @@ -3803,6 +3812,8 @@ "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@coder/pixel-storybook/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], + "@develar/schema-utils/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], @@ -4031,6 +4042,8 @@ "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "body-parser/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "body-parser/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], "browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw=="], @@ -4275,6 +4288,8 @@ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "raw-body/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], "react-docgen/doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], @@ -4295,8 +4310,18 @@ "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "serve-handler/content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="], + + "serve-handler/mime-types": ["mime-types@2.1.18", "", { "dependencies": { "mime-db": "~1.33.0" } }, "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ=="], + + "serve-handler/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "serve-handler/range-parser": ["range-parser@1.2.0", "", {}, "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A=="], + "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "spawn-wrap/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -4725,6 +4750,8 @@ "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "serve-handler/mime-types/mime-db": ["mime-db@1.33.0", "", {}, "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="], + "spawn-wrap/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "storybook/@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], diff --git a/chromatic.config.json b/chromatic.config.json deleted file mode 100644 index a7be7bbabe..0000000000 --- a/chromatic.config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "projectId": "68e30fca49979473fc9abc73", - "buildScriptName": "storybook:build", - "storybookBuildDir": "storybook-static", - "exitZeroOnChanges": true, - "exitOnceUploaded": true, - "autoAcceptChanges": false -} diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 7e5340816a..d267b63b89 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -128,7 +128,7 @@ HistoryService is pure local disk I/O with a single dependency (`getSessionDir`) - For incrementing numeric UI (costs, timers, token counts, percentages), use semantic numeric typography utilities (`counter-nums` / `counter-nums-mono`) to prevent width jitter. - Choose `counter-nums-mono` only when monospace is an intentional visual style (e.g., terminal/telemetry), not merely as a workaround. - Use `min-w-[Nch]` only when reserving layout width is intentional and separate from tabular numeral stability. -- **Always verify UI at mobile widths.** When adding or changing UI, check how it renders in a narrow/mobile viewport (~375px), not just desktop. Tool cards size via `@container` queries, so test the narrow container layout (Storybook `mobile1` viewport or a resized window). Watch for: badges/labels stretching to fill grid cells, `auto` grid columns inflated by `whitespace-nowrap` text (starves sibling columns and pushes content off-screen), and right-edge overflow. Truncating text belongs in `minmax(0,1fr)` cells. Add a pinned-viewport story per the Storybook responsive/Chromatic validation rule below when a breakpoint matters. +- **Always verify UI at mobile widths.** When adding or changing UI, check how it renders in a narrow/mobile viewport (~375px), not just desktop. Tool cards size via `@container` queries, so test the narrow container layout (Storybook `mobile1` viewport or a resized window). Watch for: badges/labels stretching to fill grid cells, `auto` grid columns inflated by `whitespace-nowrap` text (starves sibling columns and pushes content off-screen), and right-edge overflow. Truncating text belongs in `minmax(0,1fr)` cells. Add a pinned-viewport story per the Storybook responsive/Pixel validation rule below when a breakpoint matters. ## Security: Renderer HTML & XSS @@ -222,7 +222,7 @@ Freely make breaking changes, and reorganize / cleanup IPC as needed. - **Use conditional rendering for testability:** Components like `AgentModePicker` use `{isOpen &&
...}` instead of Radix Portal. This renders inline and works in happy-dom. - When adding new dropdown/popover components that need tests/ui coverage, prefer the conditional rendering pattern over Radix Portal. - E2E tests (tests/e2e) work with Radix but are slow (~2min startup); reserve for scenarios that truly need real Electron. -- **Storybook responsive/Chromatic validation:** Do not prove responsive snapshots by only resizing `iframe.html`; that bypasses Storybook/Chromatic viewport mode configuration. If a story depends on a breakpoint (wide gutters, mobile, touch), pin an explicit `parameters.chromatic.modes[*].viewport`, mirror it with story `globals.viewport` for local viewing, and validate through the Storybook manager or an equivalent Chromatic-mode check. Add a play/static contract when a missing mode would silently snapshot the wrong UI. Caveat: the Storybook test-runner (CI `Test / Storybook`) applies neither `globals.viewport` nor Chromatic modes — plays execute at desktop window size — so breakpoint-dependent play assertions must force the narrow width themselves (fixed-width wrapper/decorator, as in `App.phoneViewports.stories.tsx`) or guard on the rendered width before asserting. +- **Storybook responsive/Pixel validation:** Do not prove responsive snapshots by only resizing `iframe.html`; that bypasses the Pixel viewport matrix configuration. If a story depends on a breakpoint (wide gutters, mobile), pin an explicit `parameters.pixel.matrix.viewports` variant (named widths: phone 390, tablet 744, laptop 1200, desktop 1900), mirror it with story `globals.viewport` for local viewing, and validate through the Storybook manager or an equivalent viewport-pinned check. Pixel does not emulate touch, so `pointer: coarse` media queries never match in snapshots; touch-only affordances need play/static contracts instead. Add a play/static contract when a missing variant would silently snapshot the wrong UI. Caveat: the Storybook test-runner (CI `Test / Storybook`) applies neither `globals.viewport` nor Pixel matrix variants — plays execute at desktop window size — so breakpoint-dependent play assertions must force the narrow width themselves (fixed-width wrapper/decorator, as in `App.phoneViewports.stories.tsx`) or guard on the rendered width before asserting. - Only use `validateApiKeys()` in tests that actually make AI API calls. ## Tool: todo_write diff --git a/package.json b/package.json index a70b0d0a54..32a67618c2 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ "@babel/preset-env": "^7.28.5", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", + "@coder/pixel-storybook": "0.2.0", "@electron/rebuild": "^4.0.4", "@eslint/js": "^9.36.0", "@playwright/test": "^1.56.0", diff --git a/pixel.jsonc b/pixel.jsonc new file mode 100644 index 0000000000..d90b95fc49 --- /dev/null +++ b/pixel.jsonc @@ -0,0 +1,19 @@ +{ + // Coder Pixel visual regression testing (replaces Chromatic). + // The CLI builds Storybook, captures screenshots across the matrix below, + // and uploads them to the Pixel platform for baseline comparison + review. + "platformAccessUrl": "https://pixel.coder.com", + // TODO: replace with the real project ID once the mux project is created + // on the Pixel platform. CI skips the snapshot step until PIXEL_KEY is set. + "projectId": "00000000-0000-0000-0000-000000000000", + "storybook": { + "buildCommand": "bun x storybook build", + "outputDirectory": "storybook-static/" + }, + // Default variant: chrome at laptop width (1200px) rendering the preview's + // default dark theme. Stories opt into more variants via parameters.pixel.matrix. + "matrix": { + "browsers": ["chrome"], + "viewports": ["laptop"] + } +} diff --git a/rfc/dead-code-cleanup-roadmap.md b/rfc/dead-code-cleanup-roadmap.md index d4db09f59a..3a98917105 100644 --- a/rfc/dead-code-cleanup-roadmap.md +++ b/rfc/dead-code-cleanup-roadmap.md @@ -44,7 +44,7 @@ Remove confirmed dead code from the repository while preserving entrypoints, Sto - `src/browser/utils/runtimeUi.ts: Partial` — TypeScript utility type syntax, not an export. - `satisfies`, `Record`, `Readonly` entries in schema/config files — TypeScript syntax artifacts. - Intentional entrypoints: - - Storybook story exports (`*.stories.tsx`) are discovered by Storybook/Chromatic. + - Storybook story exports (`*.stories.tsx`) are discovered by Storybook/Pixel. - Shared schema barrels (`src/common/orpc/schemas.ts`, `src/common/config/schemas/index.ts`) are API/schema surfaces and contain many type-only false positives. ## Completion Criteria diff --git a/scripts/extract_pr_logs.sh b/scripts/extract_pr_logs.sh index 225b70c305..3841280c45 100755 --- a/scripts/extract_pr_logs.sh +++ b/scripts/extract_pr_logs.sh @@ -47,16 +47,16 @@ if [[ "$INPUT" =~ ^[0-9]{1,5}$ ]]; then PR_NUMBER="$INPUT" echo "🔍 Finding latest failed run for PR #$PR_NUMBER..." >&2 - # Get the latest failed non-Chromatic run for this PR. Chromatic UI statuses are + # Get the latest failed non-visual-review run for this PR. Pixel review statuses are # intentionally ignored for merge readiness, so they should not drive log extraction. - JQ_DEFS=$(chromatic_check_jq_defs) - RUN_ID=$(gh pr checks "$PR_NUMBER" --json name,link,state,bucket,workflow,description --jq "$JQ_DEFS .[] | select((is_chromatic_related_check | not) and is_failed_check) | .link" | sed -nE 's|.*/runs/([0-9]+).*|\1|p' | head -1 || echo "") + JQ_DEFS=$(visual_check_jq_defs) + RUN_ID=$(gh pr checks "$PR_NUMBER" --json name,link,state,bucket,workflow,description --jq "$JQ_DEFS .[] | select((is_visual_review_check | not) and is_failed_check) | .link" | sed -nE 's|.*/runs/([0-9]+).*|\1|p' | head -1 || echo "") if [[ -z "$RUN_ID" ]]; then - echo "❌ No failed non-Chromatic runs found for PR #$PR_NUMBER" >&2 + echo "❌ No failed non-visual-review runs found for PR #$PR_NUMBER" >&2 echo "" >&2 - echo "Current non-Chromatic check status:" >&2 - gh pr checks "$PR_NUMBER" --json name,state,bucket,workflow,link,description --jq "$JQ_DEFS .[] | select(is_chromatic_related_check | not) | check_line" 2>&1 || true + echo "Current non-visual-review check status:" >&2 + gh pr checks "$PR_NUMBER" --json name,state,bucket,workflow,link,description --jq "$JQ_DEFS .[] | select(is_visual_review_check | not) | check_line" 2>&1 || true exit 1 fi diff --git a/scripts/lib/pr_check_filters.sh b/scripts/lib/pr_check_filters.sh index 579d55c587..de3ad29f3b 100644 --- a/scripts/lib/pr_check_filters.sh +++ b/scripts/lib/pr_check_filters.sh @@ -1,16 +1,14 @@ #!/usr/bin/env bash # Shared PR check filters for scripts that gate merge readiness. -chromatic_check_jq_defs() { +visual_check_jq_defs() { cat <<'JQ' def check_text($field): (.[$field] // "" | tostring | ascii_downcase); -def is_chromatic_related_check: - (check_text("workflow") == "chromatic") +def is_visual_review_check: + (check_text("workflow") == "pixel") or (check_text("name") == "visual regression testing") - or (check_text("name") == "ui review") - or (check_text("name") == "ui tests") - or (check_text("link") | contains("chromatic.com")) - or (check_text("description") | contains("chromatic")); + or (check_text("name") | startswith("pixel /")) + or (check_text("link") | contains("pixel.coder.com")); def is_unready_check: (.bucket == "fail") or (.bucket == "cancel") diff --git a/scripts/wait_pr_checks.sh b/scripts/wait_pr_checks.sh index 3b8f6573fb..ef28244abb 100755 --- a/scripts/wait_pr_checks.sh +++ b/scripts/wait_pr_checks.sh @@ -174,8 +174,8 @@ CHECK_PR_CHECKS_ONCE() { return 1 fi - # Get check status. Chromatic posts persistent UI Review/UI Tests statuses - # outside the PR workflow; they are useful visual-signal, but not a merge-readiness gate. + # Get check status. Pixel posts a persistent "Pixel / Review" status outside + # the PR workflow; it is useful visual-signal, but not a merge-readiness gate. local checks_json local jq_defs local filtered_checks @@ -191,11 +191,11 @@ CHECK_PR_CHECKS_ONCE() { return 1 fi - jq_defs=$(chromatic_check_jq_defs) - filtered_checks=$(echo "$checks_json" | jq "$jq_defs [ .[] | select(is_chromatic_related_check | not) ]") - ignored_checks=$(echo "$checks_json" | jq "$jq_defs [ .[] | select(is_chromatic_related_check) ]") + jq_defs=$(visual_check_jq_defs) + filtered_checks=$(echo "$checks_json" | jq "$jq_defs [ .[] | select(is_visual_review_check | not) ]") + ignored_checks=$(echo "$checks_json" | jq "$jq_defs [ .[] | select(is_visual_review_check) ]") filtered_count=$(echo "$filtered_checks" | jq 'length') - checks=$(echo "$filtered_checks" | jq -r "$jq_defs if length == 0 then \"(no non-Chromatic checks reported)\" else .[] | check_line end") + checks=$(echo "$filtered_checks" | jq -r "$jq_defs if length == 0 then \"(no non-visual-review checks reported)\" else .[] | check_line end") ignored_unready_count=$(echo "$ignored_checks" | jq "$jq_defs [ .[] | select(is_unready_check) ] | length") ignored_unready_checks=$(echo "$ignored_checks" | jq -r "$jq_defs [ .[] | select(is_unready_check) ] | .[]? | check_line") @@ -222,19 +222,19 @@ CHECK_PR_CHECKS_ONCE() { fi # Sometimes the only early status is a skipped informational check; wait for - # at least one pass/fail/pending non-Chromatic signal before deciding. + # at least one pass/fail/pending non-visual-review signal before deciding. if [ "$has_fail" -eq 0 ] && [ "$has_pending" -eq 0 ] && [ "$has_pass" -eq 0 ]; then return 10 fi # Check for failures if [ "$has_fail" -eq 1 ]; then - echo "❌ Some non-Chromatic checks failed:" + echo "❌ Some non-visual-review checks failed:" echo "" echo "$checks" if [ "$ignored_unready_count" -gt 0 ]; then echo "" - echo "ℹ️ Ignoring Chromatic-related unready checks:" + echo "ℹ️ Ignoring visual-review unready checks:" echo "$ignored_unready_checks" fi echo "" @@ -265,22 +265,22 @@ CHECK_PR_CHECKS_ONCE() { return 1 fi - # If a Chromatic status is required in branch protection, GitHub reports - # the aggregate merge state as BLOCKED. At this point non-Chromatic checks, + # If a Pixel review status is required in branch protection, GitHub reports + # the aggregate merge state as BLOCKED. At this point non-visual-review checks, # review-thread checks, and GitHub's reviewDecision gate have passed, so an # ignored unready status is the only remaining blocker this script can see. - local merge_state_blocked_by_ignored_chromatic=0 + local merge_state_blocked_by_ignored_visual=0 if { [ "$merge_state" = "UNSTABLE" ] || [ "$merge_state" = "BLOCKED" ]; } && [ "$ignored_unready_count" -gt 0 ]; then - merge_state_blocked_by_ignored_chromatic=1 + merge_state_blocked_by_ignored_visual=1 fi - if [ "$merge_state" = "CLEAN" ] || [ "$merge_state_blocked_by_ignored_chromatic" -eq 1 ]; then - echo "✅ All non-Chromatic checks passed!" + if [ "$merge_state" = "CLEAN" ] || [ "$merge_state_blocked_by_ignored_visual" -eq 1 ]; then + echo "✅ All non-visual-review checks passed!" echo "" echo "$checks" if [ "$ignored_unready_count" -gt 0 ]; then echo "" - echo "ℹ️ Ignoring Chromatic-related unready checks:" + echo "ℹ️ Ignoring visual-review unready checks:" echo "$ignored_unready_checks" fi echo "" diff --git a/scripts/wait_pr_ready.sh b/scripts/wait_pr_ready.sh index 0747ca2159..41e5877d68 100755 --- a/scripts/wait_pr_ready.sh +++ b/scripts/wait_pr_ready.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# Wait for a PR to become merge-ready by enforcing the Codex + optional coder-agents-review + non-Chromatic CI loop. +# Wait for a PR to become merge-ready by enforcing the Codex + optional coder-agents-review + non-visual-review CI loop. # Usage: ./scripts/wait_pr_ready.sh # # This script orchestrates Codex + optional coder-agents-review + checks in one polling loop: @@ -13,7 +13,7 @@ set -euo pipefail # 6) check_codex_comments.sh (only when all active gates pass) # # It exits immediately on the first terminal failure and succeeds only when -# all required and active optional gates report success. Chromatic UI Review/UI Tests +# all required and active optional gates report success. Pixel review # statuses are intentionally ignored by wait_pr_checks.sh. if [ $# -ne 1 ]; then @@ -147,7 +147,7 @@ trap cleanup EXIT export MUX_PR_DATA_FILE="$PR_DATA_FILE" export MUX_REACTIONS_SCAN_CACHE_FILE="$REACTIONS_SCAN_CACHE_FILE" -echo "🚦 Waiting for PR #$PR_NUMBER to become ready (Codex + optional coder-agents-review + non-Chromatic CI, fail-fast)..." +echo "🚦 Waiting for PR #$PR_NUMBER to become ready (Codex + optional coder-agents-review + non-visual-review CI, fail-fast)..." echo "" while true; do @@ -330,7 +330,7 @@ while true; do if CODEX_COMMENTS_OUT=$("$CHECK_CODEX_COMMENTS_SCRIPT" "$PR_NUMBER" 2>&1); then echo "" echo "" - echo "🎉 PR #$PR_NUMBER is ready: Codex approved, ${CODER_AGENTS_READY_SUMMARY}, non-Chromatic checks passed, and no unresolved Codex comments remain." + echo "🎉 PR #$PR_NUMBER is ready: Codex approved, ${CODER_AGENTS_READY_SUMMARY}, non-visual-review checks passed, and no unresolved Codex comments remain." exit 0 fi diff --git a/src/browser/components/AIView/AIView.stories.tsx b/src/browser/components/AIView/AIView.stories.tsx index 89f038482c..8c32e9d82b 100644 --- a/src/browser/components/AIView/AIView.stories.tsx +++ b/src/browser/components/AIView/AIView.stories.tsx @@ -1,5 +1,5 @@ import type { AppStory } from "@/browser/stories/meta.js"; -import { CHROMATIC_SMOKE_MODES, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; +import { PIXEL_DUAL_THEME, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; import { collapseLeftSidebar, selectWorkspace } from "@/browser/stories/helpers/uiState"; import { createMockORPCClient } from "@/browser/stories/mocks/orpc"; import { @@ -18,7 +18,7 @@ export default meta; // Integration: story renders full app with incompatible workspace to test AIView error state. export const IncompatibleWorkspace: AppStory = { parameters: { - chromatic: { modes: CHROMATIC_SMOKE_MODES }, + pixel: { matrix: PIXEL_DUAL_THEME }, }, render: () => ( { diff --git a/src/browser/components/ChatPane/ChatPane.tsx b/src/browser/components/ChatPane/ChatPane.tsx index 4490393c16..26ac539d8b 100644 --- a/src/browser/components/ChatPane/ChatPane.tsx +++ b/src/browser/components/ChatPane/ChatPane.tsx @@ -159,7 +159,7 @@ function PerfRenderMarker(props: { id: string; children: React.ReactNode }): Rea return <>{props.children}; } -function isChromaticStorybookEnvironment(): boolean { +function isPixelSnapshotEnvironment(): boolean { if (typeof window === "undefined") { return false; } @@ -170,8 +170,8 @@ function isChromaticStorybookEnvironment(): boolean { return false; } - const chromaticRuntimeFlag = (window as Window & { chromatic?: boolean }).chromatic; - return /Chromatic/i.test(window.navigator.userAgent) || chromaticRuntimeFlag === true; + // Pixel injects window.__PIXEL__ into every snapshot capture context. + return typeof (window as Window & { __PIXEL__?: object }).__PIXEL__ === "object"; } interface ChatPaneProps { @@ -423,7 +423,7 @@ const ChatPaneContent: React.FC = (props) => { } = workspaceState; const shouldShowPinnedTodoList = workspaceState.todos.length > 0; const shouldShowReviewsBanner = reviews.reviews.length > 0; - const shouldRenderLoadOlderMessagesButton = hasOlderHistory && !isChromaticStorybookEnvironment(); + const shouldRenderLoadOlderMessagesButton = hasOlderHistory && !isPixelSnapshotEnvironment(); const loadOlderMessagesShortcutLabel = formatKeybind(KEYBINDS.LOAD_OLDER_MESSAGES); const { diff --git a/src/browser/components/DebugLlmRequestModal/DebugLlmRequestModal.stories.tsx b/src/browser/components/DebugLlmRequestModal/DebugLlmRequestModal.stories.tsx index da5826b46d..8fe2e6c394 100644 --- a/src/browser/components/DebugLlmRequestModal/DebugLlmRequestModal.stories.tsx +++ b/src/browser/components/DebugLlmRequestModal/DebugLlmRequestModal.stories.tsx @@ -2,7 +2,7 @@ import { userEvent, waitFor } from "@storybook/test"; import type { WorkspaceChatMessage } from "@/common/orpc/types"; import type { DebugLlmRequestSnapshot } from "@/common/types/debugLlmRequest"; import type { AppStory } from "@/browser/stories/meta.js"; -import { CHROMATIC_SMOKE_MODES, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; +import { PIXEL_DUAL_THEME, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; import { createOnChatAdapter } from "@/browser/stories/helpers/chatSetup"; import { collapseLeftSidebar, @@ -85,7 +85,7 @@ const createDebugLlmRequestSnapshot = (workspaceId: string): DebugLlmRequestSnap // Integration: story renders full app with debug snapshot + chat error to trigger the Debug LLM Request modal. export const DebugLlmRequestModal: AppStory = { parameters: { - chromatic: { modes: CHROMATIC_SMOKE_MODES }, + pixel: { matrix: PIXEL_DUAL_THEME }, }, render: () => ( = { component: LeftSidebar, parameters: { layout: "fullscreen", - chromatic: { delay: 500 }, }, decorators: [ (Story: () => JSX.Element) => { @@ -269,7 +268,7 @@ function createGitStatusExecutor(gitStatus?: Map) { /** Single project with multiple workspaces including SSH */ export const SingleProject: AppStory = { parameters: { - chromatic: { modes: CHROMATIC_SMOKE_MODES }, + pixel: { matrix: PIXEL_DUAL_THEME }, }, render: () => ( ( diff --git a/src/browser/components/ProjectCreateModal/ProjectCreateModal.stories.tsx b/src/browser/components/ProjectCreateModal/ProjectCreateModal.stories.tsx index 7c0bc60b33..b247388ecd 100644 --- a/src/browser/components/ProjectCreateModal/ProjectCreateModal.stories.tsx +++ b/src/browser/components/ProjectCreateModal/ProjectCreateModal.stories.tsx @@ -11,7 +11,7 @@ import { expandProjects, selectWorkspace } from "@/browser/stories/helpers/uiSta import { createMockORPCClient } from "@/browser/stories/mocks/orpc"; import { createWorkspace, groupWorkspacesByProject } from "@/browser/stories/mocks/workspaces"; import type { AppStory } from "@/browser/stories/meta.js"; -import { CHROMATIC_SMOKE_MODES, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; +import { PIXEL_DUAL_THEME, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; import { within, userEvent, waitFor } from "@storybook/test"; const meta = { @@ -55,7 +55,7 @@ async function openNewProjectModal(canvasElement: HTMLElement): Promise { /** Default "Local folder" tab of the Add Project modal. */ export const LocalFolder: AppStory = { parameters: { - chromatic: { modes: CHROMATIC_SMOKE_MODES }, + pixel: { matrix: PIXEL_DUAL_THEME }, }, // Integration: stories navigate via sidebar → "Add project" button to open the modal portal. render: () => , diff --git a/src/browser/components/ProjectPage/ProjectPage.stories.tsx b/src/browser/components/ProjectPage/ProjectPage.stories.tsx index 887af74eac..668aba1bc8 100644 --- a/src/browser/components/ProjectPage/ProjectPage.stories.tsx +++ b/src/browser/components/ProjectPage/ProjectPage.stories.tsx @@ -5,12 +5,7 @@ import { within, userEvent, waitFor, expect } from "@storybook/test"; import { expandProjects } from "@/browser/stories/helpers/uiState"; -import { - CHROMATIC_SMOKE_MODES, - appMeta, - AppWithMocks, - type AppStory, -} from "@/browser/stories/meta.js"; +import { PIXEL_DUAL_THEME, appMeta, AppWithMocks, type AppStory } from "@/browser/stories/meta.js"; import { createMockORPCClient, type MockSessionUsage } from "@/browser/stories/mocks/orpc"; import { createArchivedWorkspace, NOW } from "@/browser/stories/mocks/workspaces"; import { LEFT_SIDEBAR_COLLAPSED_KEY } from "@/common/constants/storage"; @@ -80,7 +75,7 @@ function projectWithNoWorkspaces(path: string): [string, ProjectConfig] { */ export const CreateWorkspace: AppStory = { parameters: { - chromatic: { modes: CHROMATIC_SMOKE_MODES }, + pixel: { matrix: PIXEL_DUAL_THEME }, }, render: () => ( ( @@ -136,19 +127,15 @@ export const CreateWorkspaceMultipleProjects: AppStory = { * Creation view with sub-projects configured. * Sub-projects are shown in the sidebar rather than in a creation-time section selector. * - * Includes mobile chromatic modes: the sidebar starts expanded via + * Includes mobile snapshot variants: the sidebar starts expanded via * localStorage so the play function can click the project row, then * collapses it so the creation form is the main visible content. */ export const CreateWorkspaceWithSections: AppStory = { parameters: { - chromatic: { - modes: { - // Two snapshots cover both themes AND both viewports without paying for - // the full 2x2 matrix: a dark desktop render plus a light mobile render. - dark: { theme: "dark" }, - "light-mobile": { theme: "light", viewport: "mobile1", hasTouch: true }, - }, + pixel: { + // Cover both themes and both form factors (laptop + phone widths). + matrix: { themes: ["dark", "light"], viewports: ["laptop", "phone"] }, }, }, render: () => ( @@ -204,7 +191,7 @@ export const CreateWorkspaceWithSections: AppStory = { // On mobile, handleAddWorkspace auto-collapses the sidebar after the // project click. On desktop it stays open — collapse it so the creation - // form dominates the Chromatic screenshot. + // form dominates the snapshot. const sidebar = storyRoot.querySelector("[data-testid='left-sidebar']"); const sidebarIsExpanded = sidebar && sidebar.getBoundingClientRect().width > 40; if (sidebarIsExpanded) { diff --git a/src/browser/components/ProjectSidebar/ProjectSidebar.stories.tsx b/src/browser/components/ProjectSidebar/ProjectSidebar.stories.tsx index 13ca22d8b3..a0bbd9b1c0 100644 --- a/src/browser/components/ProjectSidebar/ProjectSidebar.stories.tsx +++ b/src/browser/components/ProjectSidebar/ProjectSidebar.stories.tsx @@ -1,6 +1,6 @@ import { fireEvent, userEvent, waitFor } from "@storybook/test"; import type { AppStory } from "@/browser/stories/meta.js"; -import { CHROMATIC_SMOKE_MODES, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; +import { PIXEL_DUAL_THEME, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; import { expandProjects } from "@/browser/stories/helpers/uiState"; import { createMockORPCClient } from "@/browser/stories/mocks/orpc"; import { createWorkspace, groupWorkspacesByProject } from "@/browser/stories/mocks/workspaces"; @@ -17,7 +17,7 @@ export default meta; // Integration: story renders full app to test project removal confirmation flow via sidebar context menu. export const ProjectRemovalDisabled: AppStory = { parameters: { - chromatic: { modes: CHROMATIC_SMOKE_MODES }, + pixel: { matrix: PIXEL_DUAL_THEME }, }, render: () => ( ( ( ( ( , }; diff --git a/src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.stories.tsx b/src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.stories.tsx index c3c794078a..f540554eb5 100644 --- a/src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.stories.tsx +++ b/src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.stories.tsx @@ -124,7 +124,6 @@ const meta = { }, parameters: { layout: "fullscreen", - chromatic: { delay: 500 }, }, } satisfies Meta; @@ -136,13 +135,6 @@ export const LongMessageDesktop: Story = { globals: { viewport: { value: "desktop", isRotated: false }, }, - parameters: { - chromatic: { - modes: { - "dark-desktop": { theme: "dark", viewport: { width: 1280, height: 800 } }, - }, - }, - }, render: () => renderOpenModal(), play: async ({ canvasElement }) => { await assertHeartbeatModalLoaded(canvasElement); @@ -154,11 +146,7 @@ export const LongMessageMobile: Story = { viewport: { value: "mobile1", isRotated: false }, }, parameters: { - chromatic: { - modes: { - "dark-mobile": { theme: "dark", viewport: { width: 375, height: 667 } }, - }, - }, + pixel: { matrix: { viewports: ["phone"] } }, }, render: () => renderOpenModal(), play: async ({ canvasElement }) => { @@ -170,13 +158,6 @@ export const DisabledLongMessageDesktop: Story = { globals: { viewport: { value: "desktop", isRotated: false }, }, - parameters: { - chromatic: { - modes: { - "dark-desktop-disabled": { theme: "dark", viewport: { width: 1280, height: 800 } }, - }, - }, - }, render: () => renderOpenModal(disabledLongMessageSettings), play: async ({ canvasElement }) => { await assertHeartbeatModalLoaded(canvasElement); @@ -187,13 +168,6 @@ export const FixedScheduleDesktop: Story = { globals: { viewport: { value: "desktop", isRotated: false }, }, - parameters: { - chromatic: { - modes: { - "dark-desktop-fixed-schedule": { theme: "dark", viewport: { width: 1280, height: 800 } }, - }, - }, - }, render: () => renderOpenModal(fixedScheduleSettings), play: async ({ canvasElement }) => { await assertHeartbeatModalLoaded(canvasElement); @@ -220,11 +194,7 @@ export const DisabledLongMessageMobile: Story = { viewport: { value: "mobile1", isRotated: false }, }, parameters: { - chromatic: { - modes: { - "dark-mobile-disabled": { theme: "dark", viewport: { width: 375, height: 667 } }, - }, - }, + pixel: { matrix: { viewports: ["phone"] } }, }, render: () => renderOpenModal(disabledLongMessageSettings), play: async ({ canvasElement }) => { diff --git a/src/browser/components/WorkspaceMCPModal/WorkspaceMCPModal.stories.tsx b/src/browser/components/WorkspaceMCPModal/WorkspaceMCPModal.stories.tsx index 01e050d9d0..b581ad9024 100644 --- a/src/browser/components/WorkspaceMCPModal/WorkspaceMCPModal.stories.tsx +++ b/src/browser/components/WorkspaceMCPModal/WorkspaceMCPModal.stories.tsx @@ -194,9 +194,6 @@ const meta: Meta = { component: WorkspaceMCPModal, parameters: { layout: "fullscreen", - chromatic: { - delay: 500, - }, }, }; diff --git a/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.stories.tsx b/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.stories.tsx index 756465da26..b573e48143 100644 --- a/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.stories.tsx +++ b/src/browser/components/WorkspaceMenuBar/WorkspaceMenuBar.stories.tsx @@ -1,9 +1,4 @@ -import { - CHROMATIC_SMOKE_MODES, - appMeta, - AppWithMocks, - type AppStory, -} from "@/browser/stories/meta.js"; +import { PIXEL_DUAL_THEME, appMeta, AppWithMocks, type AppStory } from "@/browser/stories/meta.js"; import { createGitStatusExecutor } from "@/browser/stories/helpers/git"; import { collapseRightSidebar, @@ -106,7 +101,7 @@ function createDevcontainerClient(runtimeStatus: "running" | "stopped" | "unknow */ export const DevcontainerRunning: AppStory = { parameters: { - chromatic: { modes: CHROMATIC_SMOKE_MODES }, + pixel: { matrix: PIXEL_DUAL_THEME }, }, render: () => createDevcontainerClient("running")} />, }; @@ -129,11 +124,9 @@ export const ScratchWorkspace: AppStory = { viewport: { value: "mobile1", isRotated: false }, }, parameters: { - chromatic: { - modes: { - desktop: { theme: "dark" }, - mobile: { theme: "light", viewport: "mobile1", hasTouch: true }, - }, + pixel: { + // Cover both themes and both form factors (laptop + phone widths). + matrix: { themes: ["dark", "light"], viewports: ["laptop", "phone"] }, }, }, render: () => ( diff --git a/src/browser/features/AIElements/Shimmer.tsx b/src/browser/features/AIElements/Shimmer.tsx index 3c7c4402e5..7e4d834e96 100644 --- a/src/browser/features/AIElements/Shimmer.tsx +++ b/src/browser/features/AIElements/Shimmer.tsx @@ -38,7 +38,6 @@ const ShimmerComponent = ({ return ( { const workspaceId = "ws-editing"; - // Ensure a deterministic starting state (Chromatic/Storybook can preserve localStorage + // Ensure a deterministic starting state (Storybook can preserve localStorage // across story runs in the same session). setWorkspaceInput(workspaceId, ""); diff --git a/src/browser/features/ChatInput/placeholderTips.test.ts b/src/browser/features/ChatInput/placeholderTips.test.ts index d62e4ae942..9d064f9900 100644 --- a/src/browser/features/ChatInput/placeholderTips.test.ts +++ b/src/browser/features/ChatInput/placeholderTips.test.ts @@ -18,7 +18,7 @@ describe("PLACEHOLDER_TIPS", () => { // tip carousel is one of the few discovery surfaces users will see it on. // Placing it at the lead slot has two consequences this assertion locks in: // 1) It's the tip a user sees on degenerate-timer fallback. - // 2) It's the tip every Chromatic story renders via the Storybook pin. + // 2) It's the tip every visual snapshot renders via the Storybook pin. // Demoting it from index 0 would silently regress both surfaces, so we // assert the position rather than just the presence. expect(PLACEHOLDER_TIPS[0]).toMatch(/\/orchestrate\b/); @@ -68,7 +68,7 @@ describe("getPlaceholderTip", () => { }); test("pins the default-arg call to the lead tip when running under Storybook", () => { - // Storybook/Chromatic renders 100+ stories that include ChatInput. Without + // Storybook visual snapshots render 100+ stories that include ChatInput. Without // pinning, every reorder or insertion into PLACEHOLDER_TIPS shifts the // tip the wall-clock bucket lands on and forces a baseline re-accept on // every one of those stories. The fix is a runtime flag set by diff --git a/src/browser/features/ChatInput/placeholderTips.ts b/src/browser/features/ChatInput/placeholderTips.ts index 83621780ca..01f3b4402b 100644 --- a/src/browser/features/ChatInput/placeholderTips.ts +++ b/src/browser/features/ChatInput/placeholderTips.ts @@ -30,12 +30,12 @@ import { KEYBINDS, formatKeybind } from "@/browser/utils/ui/keybinds"; const TIP_ROTATION_INTERVAL_MS = 20 * 60 * 1000; // 20 minutes /** - * Tip index pinned for Storybook/Chromatic snapshots. + * Tip index pinned for Storybook visual snapshots. * * Without pinning, every story that renders ChatInput would resolve a tip via * `floor(NOW / 20min) mod PLACEHOLDER_TIPS.length` — so any reorder of or * insertion into PLACEHOLDER_TIPS shifts the displayed tip and cascades into - * a fresh Chromatic baseline diff on every ChatInput story (currently 100+). + * a fresh visual baseline diff on every ChatInput story (currently 100+). * * Pinning to index 0 means tip-list edits only affect snapshots when the lead * tip's text itself changes, which is the rare, intentional case. /orchestrate diff --git a/src/browser/features/Messages/ChatBarrier/InterruptedBarrier.stories.tsx b/src/browser/features/Messages/ChatBarrier/InterruptedBarrier.stories.tsx index 271e87c160..43ff956ac5 100644 --- a/src/browser/features/Messages/ChatBarrier/InterruptedBarrier.stories.tsx +++ b/src/browser/features/Messages/ChatBarrier/InterruptedBarrier.stories.tsx @@ -1,6 +1,6 @@ import type { WorkspaceChatMessage } from "@/common/orpc/types"; import type { AppStory } from "@/browser/stories/meta.js"; -import { CHROMATIC_SMOKE_MODES, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; +import { PIXEL_DUAL_THEME, appMeta, AppWithMocks } from "@/browser/stories/meta.js"; import { setupCustomChatStory } from "@/browser/stories/helpers/chatSetup"; import { collapseLeftSidebar } from "@/browser/stories/helpers/uiState"; import { createUserMessage } from "@/browser/stories/mocks/messages"; @@ -16,7 +16,7 @@ export default meta; // Integration: story uses full app chat streaming to trigger context-exceeded error in InterruptedBarrier. export const ContextExceededSuggestion: AppStory = { parameters: { - chromatic: { modes: CHROMATIC_SMOKE_MODES }, + pixel: { matrix: PIXEL_DUAL_THEME }, }, render: () => ( { translateX 53.33% (of 300%) = 160%, moving highlight to 130% (off-screen right). */}