Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Capture headless-Chrome screenshots of the tingly-box frontend (running locally in mock mode) so frontend changes can be visually verified in environments without a real browser. Use when the user asks to "preview", "screenshot", "see the page", "show me the UI", "verify visually", or when frontend layout / component / styling changes need a sanity-check before review. Works in restricted/cloud sandboxes where Playwright's normal Chromium install is blocked.
.claude/skills/tingly-dev-ui-preview/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 54% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 11% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 7% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 4% | 0% |
Designed for the remote-execution container where:
cdn.playwright.dev / dl.google.com are blocked — use Chrome for Testing via storage.googleapis.com.chromium-browser is a snap stub and won't run.> ⚠️ This project uses pnpm (frontend/pnpm-lock.yaml is the source of truth). > Do NOT run npm i here — npm ignores the pnpm lockfile and re-resolves > transitive deps, which pulls broken versions (e.g. es-toolkit@1.47.0 vs the > pinned 1.46.1) that crash the whole SPA at load. See Troubleshooting below.
playwright is already a committed devDependency in frontend/package.json, so a plain install provides it — no ad-hoc add/revert needed. @emotion/react / @emotion/styled are likewise already declared.
bash# 1. Install deps from the lockfile (also fixes a node_modules previously # polluted by npm — wipe it first with `rm -rf node_modules` if you suspect that). cd frontend pnpm install --frozen-lockfile # 2. Download Chrome for Testing (Playwright's own Chromium download is blocked) mkdir -p /tmp/chrome && cd /tmp/chrome curl -fsSL -o chrome.zip \ "https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.96/linux64/chrome-linux64.zip" unzip -q chrome.zip # → /tmp/chrome/chrome-linux64/chrome
Do NOT modify frontend/package.json / frontend/pnpm-lock.yaml for tooling anymore — everything needed is already committed. If a previous session left them dirty (or npm polluted the tree), reset with:
bashcd <repo> && git checkout -- frontend/package.json frontend/pnpm-lock.yaml pnpm -C frontend install --frozen-lockfile
bashcd frontend USE_MOCK=true node_modules/.bin/vite --mode mock --port 3000 & until curl -fs http://localhost:3000 >/dev/null; do sleep 1; done
USE_MOCK=true must be a shell env var (read by vite.config.ts before .env.mock).
Every script must seed localStorage.user_auth_token via addInitScript or the app redirects to the login screen:
jsawait page.addInitScript(() => { localStorage.setItem('user_auth_token', 'mock-token-for-screenshots'); });
Mock mode defaults these off (src/mocks/mockConfig.ts), so a fresh browser context no longer pops the "Direct Routing Guide" dialog and steals focus/clicks — no Escape-key dismissal needed before interacting with the page. If a script specifically needs to screenshot or assert the first-run guide itself, force it on per-navigation with a query param (no code change, no shared state between runs):
jsawait page.goto('http://localhost:3000/agent/openai?mockOnboarding=on', { waitUntil: 'networkidle' });
?mockOnboarding=off is the (redundant) explicit default, useful when a test wants to assert the off state regardless of what a prior run left in localStorage.
All scripts live here and are run from frontend/. They use createRequire(cwd) to resolve playwright from frontend/node_modules regardless of the script's location:
jsimport { createRequire } from 'module'; const { chromium } = createRequire('file://' + process.cwd() + '/')('playwright');
| Script | Mode | Purpose | |--------|------|---------| | screenshot.mjs | mock | Ad-hoc template — copy here and customise; do NOT commit | | docs-screenshots.mjs | mock | All 9 docs/images/ product screenshots + theme previews | | regression-credentials.mjs | mock | Assertion-based regression for /credentials Add API Key flow | | scenario-routing-graph.mjs | real backend | Codex routing graph screenshots (requires running Go server) |
Each script is self-documenting — see its file header for usage, outputs, and known issues.
Screenshots are captured with Playwright's plain page.screenshot() — the skill deliberately does not compress images internally, because optimizers depend on host tooling that is not guaranteed in the run environment. If a PNG is large and you want to trim it before committing, run any optimizer you have available on the captured file afterwards. None of these are required:
pngquant --quality 80-95 --strip --force --out foo.png foo.png (lossy, smallest PNGs)oxipng -o 4 --strip safe foo.png (lossless)sharp(file).png({ quality: 80, palette: true }).toFile(out)Pillow → img.save(out, optimize=True)Prefer keeping the original capture when quality matters more than size.
SendUserFile the PNGs so the user can review them.screenshot.mjs: delete it before committing — the stop hook will flag it.docs/ is gitignored; force-add images: git add -f docs/images/.fuser -k 3000/tcp or pkill -f "vite --mode mock".Blank white page / root is empty, console shows TypeError: require_isUnsafeProperty is not a function (stack points at es-toolkit/dist/compat/object/get.js ← recharts).
Cause: node_modules was installed/resolved by npm instead of pnpm, so a transitive dep (es-toolkit) drifted to a version whose CJS→ESM interop esbuild mis-bundles. It breaks the React vendor chunk, so every route renders blank — not just the page you're testing. Tweaking optimizeDeps (include/exclude/ keepNames) does not fix it.
Fix — reinstall the pinned tree with pnpm:
bashcd frontend rm -rf node_modules node_modules/.vite # drop the npm-polluted tree + dep cache pnpm install --frozen-lockfile # verify the pinned (working) version is what's on disk: ls node_modules/.pnpm | grep es-toolkit # expect es-toolkit@1.46.1, NOT 1.47.0 pnpm add -D playwright # re-add the tool
Then restart the dev server. If you edited vite.config.ts while chasing this, revert it — the config is not the problem.
Dev-server process exits immediately (e.g. exit 144) on --force: usually a follow-on of the crash above (the unhandled client error). Once the pnpm tree is correct it starts cleanly; --force is only needed once to drop a stale node_modules/.vite optimize cache.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→fail | 11,075 | 4,802 | -57% | 1 | 1 | 0% | 2,081 | 1,858 | -11% | 0 | 0 | — |
case-01 | fail→fail | 8,183 | 4,597 | -44% | 1 | 1 | 0% | 1,423 | 1,955 | +37% | 0 | 0 | — |
case-02 | fail→fail | 13,031 | 5,477 | -58% | 1 | 1 | 0% | 2,707 | 1,830 | -32% | 0 | 0 | — |
case-03 | fail→fail | 9,125 | 6,472 | -29% | 1 | 1 | 0% | 1,559 | 2,128 | +36% | 0 | 0 | — |
case-04 | pass→pass | 17,707 | 22,132 | +25% | 1 | 1 | 0% | 3,602 | 6,178 | +72% | 0 | 0 | — |
case-06 | pass→fail | 11,289 | 5,471 | -52% | 1 | 1 | 0% | 2,377 | 1,934 | -19% | 0 | 0 | — |
case-07 | fail→pass | 8,171 | 3,458 | -58% | 1 | 1 | 0% | 1,446 | 2,230 | +54% | 0 | 0 | — |
case-08 | fail→pass | 21,636 | 5,589 | -74% | 1 | 1 | 0% | 1,176 | 2,219 | +89% | 0 | 0 | — |
case-09 | fail→pass | 10,508 | 2,755 | -74% | 1 | 1 | 0% | 1,912 | 2,115 | +11% | 0 | 0 | — |
case-10 | fail→pass | 13,524 | 5,112 | -62% | 1 | 1 | 0% | 2,344 | 2,514 | +7% | 0 | 0 | — |
case-11 | fail→pass | 11,737 | 7,024 | -40% | 1 | 1 | 0% | 2,153 | 2,230 | +4% | 0 | 0 | — |
case-12 | fail→pass | 9,670 | 2,495 | -74% | 1 | 1 | 0% | 1,491 | 2,061 | +38% | 0 | 0 | — |
case-13 | fail→pass | 11,847 | 6,946 | -41% | 1 | 1 | 0% | 2,040 | 2,845 | +39% | 0 | 0 | — |
case-14 | pass→pass | 8,907 | 2,711 | -70% | 1 | 1 | 0% | 1,552 | 1,993 | +28% | 0 | 0 | — |
case-15 | pass→pass | 4,640 | 1,810 | -61% | 1 | 1 | 0% | 831 | 1,924 | +132% | 0 | 0 | — |
case-16 | pass→pass | 2,383 | 2,466 | +3% | 1 | 1 | 0% | 351 | 2,028 | +478% | 0 | 0 | — |
case-17 | pass→pass | 10,927 | 4,590 | -58% | 1 | 1 | 0% | 1,722 | 2,494 | +45% | 0 | 0 | — |
case-18 | fail→pass | 6,086 | 3,779 | -38% | 1 | 1 | 0% | 1,032 | 2,308 | +124% | 0 | 0 | — |
case-19 | fail→pass | 16,151 | 2,299 | -86% | 1 | 1 | 0% | 2,900 | 1,932 | -33% | 0 | 0 | — |
case-20 | fail→pass | 8,989 | 2,055 | -77% | 1 | 1 | 0% | 1,440 | 1,968 | +37% | 0 | 0 | — |
case-21 | fail→pass | 16,635 | 2,171 | -87% | 1 | 1 | 0% | 958 | 1,999 | +109% | 0 | 0 | — |
case-22 | fail→fail | 14,661 | 4,344 | -70% | 1 | 1 | 0% | 2,363 | 2,324 | -2% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 17 counted toward the lift figure. The other 5 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +41 percentage points is the difference between those two pass rates over the 17 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
The publisher has shipped newer versions since this run, so these numbers describe v1, not the version currently listed.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.