From e29566697724ce70f1df58c840bbe20b3746f9f1 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 8 Jun 2026 14:39:16 +0200 Subject: [PATCH 1/4] Add screenshot-dev-app skill for web preview UI verification. Documents how agents navigate the Vite preview (?previewMode=true), capture screenshots via cursor-ide-browser, and extend mocks in index.html when fixtures are insufficient. --- .claude/skills/screenshot-dev-app/SKILL.md | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .claude/skills/screenshot-dev-app/SKILL.md diff --git a/.claude/skills/screenshot-dev-app/SKILL.md b/.claude/skills/screenshot-dev-app/SKILL.md new file mode 100644 index 0000000000..a2d971586f --- /dev/null +++ b/.claude/skills/screenshot-dev-app/SKILL.md @@ -0,0 +1,97 @@ +--- +name: screenshot-dev-app +description: Take a screenshot of the PostHog Code renderer via the Vite web preview (localhost:5173 with ?previewMode=true). Navigate with hash routes, capture with cursor-ide-browser, and verify the PNG. Use when the user asks to screenshot, capture, or visually verify the dev app UI. +--- + +# Screenshot the PostHog Code dev app (web preview) + +Navigate in a browser → capture with `browser_take_screenshot` → read the PNG. + +**Prerequisites:** `pnpm dev:mprocs` or `pnpm dev:code` running (Vite on **localhost:5173**). Use **cursor-ide-browser** — do not control Electron or use `screencapture`. + +## Workflow + +1. **Navigate** — open the preview URL for the target hash route (step 1 below). +2. **Wait** — `browser_snapshot` shows sidebar buttons and page content, not an empty `#root`. +3. **Capture** — `browser_take_screenshot` (step 2 below). +4. **Verify** — read the PNG. + +## URL shape + +The renderer uses **TanStack Router with hash history** (`apps/code/src/renderer/router.ts`): + +```text +http://localhost:5173/?previewMode=true# +``` + +`?previewMode=true` is required. It activates the dev shim in `apps/code/index.html` that mocks tRPC/auth so the UI mounts in a regular browser. Without it, the app cannot bootstrap outside Electron. + +### Route map + +| View | Hash route | +| --- | --- | +| New task (home) | `#/code` | +| Scouts & Responders | `#/code/agents` | +| Inbox → pulls | `#/code/inbox/pulls` | +| Inbox → reports | `#/code/inbox/reports` | +| Inbox → runs | `#/code/inbox/runs` | +| Inbox PR detail | `#/code/inbox/pulls/` | +| Inbox report detail | `#/code/inbox/reports/` | +| Inbox run detail | `#/code/inbox/runs/` | +| Task detail | `#/code/tasks/` | Needs a real task id | +| Skills | `#/skills` | +| MCP servers | `#/mcp-servers` | +| Command Center | `#/command-center` | +| Archived | `#/code/archived` | +| Settings | `#/settings/` | +| Folder settings | `#/folders/` | + +Settings categories: `general`, `plan-usage`, `workspaces`, `worktrees`, `environments`, `cloud-environments`, `personalization`, `terminal`, `claude-code`, `shortcuts`, `github`, `slack`, `signals`, `updates`, `advanced`. + +Mock report ids for inbox detail screenshots: `r-1` … `r-8` (defined in `apps/code/index.html`). Example: `http://localhost:5173/?previewMode=true#/code/inbox/pulls/r-2`. + +## 1. Navigate + +With **cursor-ide-browser**: + +1. `browser_tabs` with `action: "list"` — reuse an existing tab or note the `viewId`. +2. `browser_navigate` to the preview URL. **Omit `position`** so automation stays in the background and does not steal focus. +3. `browser_snapshot` — confirm the target view rendered (e.g. tab bar on Inbox, section headings on Scouts & Responders). + +Examples: + +```text +http://localhost:5173/?previewMode=true#/code/agents +http://localhost:5173/?previewMode=true#/code/inbox/pulls +http://localhost:5173/?previewMode=true#/code/inbox/pulls/r-2 +http://localhost:5173/?previewMode=true#/settings/signals +``` + +To exercise sidebar highlight / nav UX, `browser_click` a sidebar button (e.g. `Scouts & Responders`, `Inbox`) after landing on `#/code`. + +## 2. Capture + +`browser_take_screenshot`: + +- Use `fullPage: true` for long pages (Scouts & Responders, settings). +- Set `filename` when saving a deliverable (e.g. `scouts-responders-verify.png`). +- Pass `viewId` if multiple tabs are open. + +Then read the PNG to verify the correct route and content. + +## Extending mock data + +If the fixtures don't cover the state you need (empty list, missing field, connected GitHub, etc.), **edit the preview shim** — don't fall back to Electron. All preview mocks live in **`apps/code/index.html`** inside the `?previewMode=true` block: + +- **`mockReports` / `fakeReport()`** — inbox list and detail cards (`r-1` … `r-8`) +- **`mocks`** — tRPC responses keyed by procedure name (e.g. `auth.getState`, `inbox.getSignalReports`) +- **`window.fetch` interceptor** — PostHog Cloud HTTP endpoints (report lists, artefacts, processing state) + +Reload the preview URL after editing; Vite hot-reloads `index.html` on save. + +## Gotchas + +- **Preview mode = mock data.** GitHub/Slack show "Connect …" stubs unless you add mocks for them. Inbox uses fixture reports. Layout and component checks only — not live integrations. +- **Navigate before capture.** A screenshot of the wrong hash is a false pass. +- **`previewMode` must stay in the query string** when changing hash routes. If navigation drops it, re-add `?previewMode=true`. +- **Do not use Electron capture** (`screencapture`, Chrome DevTools Protocol on `:9222`) for this skill — the web preview is the supported path. From 7636be53f123ef7b312fe7bc3874a7d9825ccdd8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 8 Jun 2026 13:09:06 +0000 Subject: [PATCH 2/4] Fix route map table column count for Task detail row Co-authored-by: Michael Matloka --- .claude/skills/screenshot-dev-app/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/screenshot-dev-app/SKILL.md b/.claude/skills/screenshot-dev-app/SKILL.md index a2d971586f..9ccd986337 100644 --- a/.claude/skills/screenshot-dev-app/SKILL.md +++ b/.claude/skills/screenshot-dev-app/SKILL.md @@ -38,7 +38,7 @@ http://localhost:5173/?previewMode=true# | Inbox PR detail | `#/code/inbox/pulls/` | | Inbox report detail | `#/code/inbox/reports/` | | Inbox run detail | `#/code/inbox/runs/` | -| Task detail | `#/code/tasks/` | Needs a real task id | +| Task detail | `#/code/tasks/` (needs a real task id) | | Skills | `#/skills` | | MCP servers | `#/mcp-servers` | | Command Center | `#/command-center` | From acdcea552df65bd3a011c699d1d8bd2ce7550683 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Tue, 9 Jun 2026 13:44:44 +0100 Subject: [PATCH 3/4] Add Playwright screenshot script and streamline screenshot-dev-app skill. Ship screenshot:preview commands and a persistent-server capture script so agents can verify the Vite preview without cursor-ide-browser. --- .claude/skills/screenshot-dev-app/SKILL.md | 106 ++---- apps/code/package.json | 2 + apps/code/scripts/screenshot-dev-preview.ts | 392 ++++++++++++++++++++ 3 files changed, 420 insertions(+), 80 deletions(-) create mode 100644 apps/code/scripts/screenshot-dev-preview.ts diff --git a/.claude/skills/screenshot-dev-app/SKILL.md b/.claude/skills/screenshot-dev-app/SKILL.md index 9ccd986337..2c11aabed5 100644 --- a/.claude/skills/screenshot-dev-app/SKILL.md +++ b/.claude/skills/screenshot-dev-app/SKILL.md @@ -1,97 +1,43 @@ --- name: screenshot-dev-app -description: Take a screenshot of the PostHog Code renderer via the Vite web preview (localhost:5173 with ?previewMode=true). Navigate with hash routes, capture with cursor-ide-browser, and verify the PNG. Use when the user asks to screenshot, capture, or visually verify the dev app UI. +description: Take a screenshot of the PostHog Code renderer via the Vite web preview (localhost:5173 with ?previewMode=true). Navigate with hash routes, capture with Playwright (screenshot-dev-preview.ts), and verify the PNG. Use when the user asks to screenshot, capture, or visually verify the dev app UI. --- -# Screenshot the PostHog Code dev app (web preview) +# Screenshot the PostHog Code dev app -Navigate in a browser → capture with `browser_take_screenshot` → read the PNG. +Capture via Playwright only — not cursor-ide-browser, Electron, or `screencapture`. -**Prerequisites:** `pnpm dev:mprocs` or `pnpm dev:code` running (Vite on **localhost:5173**). Use **cursor-ide-browser** — do not control Electron or use `screencapture`. +**Needs:** Vite on localhost:5173 (`pnpm dev:code` / `pnpm dev:mprocs`). First Playwright use: `pnpm exec playwright install chromium`. -## Workflow +## Capture -1. **Navigate** — open the preview URL for the target hash route (step 1 below). -2. **Wait** — `browser_snapshot` shows sidebar buttons and page content, not an empty `#root`. -3. **Capture** — `browser_take_screenshot` (step 2 below). -4. **Verify** — read the PNG. +```bash +# one shot +pnpm --filter code screenshot:preview -- --route /code/inbox/pulls -o out.png -## URL shape - -The renderer uses **TanStack Router with hash history** (`apps/code/src/renderer/router.ts`): - -```text -http://localhost:5173/?previewMode=true# +# batch (start once — first capture ~5s, later ones ~3s via hash navigation) +pnpm --filter code screenshot:preview:serve # background +pnpm --filter code screenshot:preview -- --route /code/inbox/reports -o reports.png +pnpm --filter code screenshot:preview -- --route /code/inbox/runs -o runs.png ``` -`?previewMode=true` is required. It activates the dev shim in `apps/code/index.html` that mocks tRPC/auth so the UI mounts in a regular browser. Without it, the app cannot bootstrap outside Electron. - -### Route map - -| View | Hash route | -| --- | --- | -| New task (home) | `#/code` | -| Scouts & Responders | `#/code/agents` | -| Inbox → pulls | `#/code/inbox/pulls` | -| Inbox → reports | `#/code/inbox/reports` | -| Inbox → runs | `#/code/inbox/runs` | -| Inbox PR detail | `#/code/inbox/pulls/` | -| Inbox report detail | `#/code/inbox/reports/` | -| Inbox run detail | `#/code/inbox/runs/` | -| Task detail | `#/code/tasks/` (needs a real task id) | -| Skills | `#/skills` | -| MCP servers | `#/mcp-servers` | -| Command Center | `#/command-center` | -| Archived | `#/code/archived` | -| Settings | `#/settings/` | -| Folder settings | `#/folders/` | - -Settings categories: `general`, `plan-usage`, `workspaces`, `worktrees`, `environments`, `cloud-environments`, `personalization`, `terminal`, `claude-code`, `shortcuts`, `github`, `slack`, `signals`, `updates`, `advanced`. - -Mock report ids for inbox detail screenshots: `r-1` … `r-8` (defined in `apps/code/index.html`). Example: `http://localhost:5173/?previewMode=true#/code/inbox/pulls/r-2`. - -## 1. Navigate +Read the printed PNG path and verify content. Flags: `-o`, `--full-page`, `--wait-for `, `--url` (full URL), `--help`. -With **cursor-ide-browser**: +Preview URLs are `http://localhost:5173/?previewMode=true#`. `--route` builds that automatically; `?previewMode=true` loads mocks from `apps/code/index.html`. -1. `browser_tabs` with `action: "list"` — reuse an existing tab or note the `viewId`. -2. `browser_navigate` to the preview URL. **Omit `position`** so automation stays in the background and does not steal focus. -3. `browser_snapshot` — confirm the target view rendered (e.g. tab bar on Inbox, section headings on Scouts & Responders). +## Routes -Examples: - -```text -http://localhost:5173/?previewMode=true#/code/agents -http://localhost:5173/?previewMode=true#/code/inbox/pulls -http://localhost:5173/?previewMode=true#/code/inbox/pulls/r-2 -http://localhost:5173/?previewMode=true#/settings/signals -``` - -To exercise sidebar highlight / nav UX, `browser_click` a sidebar button (e.g. `Scouts & Responders`, `Inbox`) after landing on `#/code`. - -## 2. Capture - -`browser_take_screenshot`: - -- Use `fullPage: true` for long pages (Scouts & Responders, settings). -- Set `filename` when saving a deliverable (e.g. `scouts-responders-verify.png`). -- Pass `viewId` if multiple tabs are open. - -Then read the PNG to verify the correct route and content. - -## Extending mock data - -If the fixtures don't cover the state you need (empty list, missing field, connected GitHub, etc.), **edit the preview shim** — don't fall back to Electron. All preview mocks live in **`apps/code/index.html`** inside the `?previewMode=true` block: - -- **`mockReports` / `fakeReport()`** — inbox list and detail cards (`r-1` … `r-8`) -- **`mocks`** — tRPC responses keyed by procedure name (e.g. `auth.getState`, `inbox.getSignalReports`) -- **`window.fetch` interceptor** — PostHog Cloud HTTP endpoints (report lists, artefacts, processing state) +| View | `--route` | +| --- | --- | +| Home | `/code` | +| Responders | `/code/agents` | +| Inbox pulls / reports / runs | `/code/inbox/pulls`, `/code/inbox/reports`, `/code/inbox/runs` | +| Inbox detail | `/code/inbox/pulls/`, `/code/inbox/reports/`, `/code/inbox/runs/` | +| Settings | `/settings/` | +| Skills, MCP, archived, tasks | `/skills`, `/mcp-servers`, `/code/archived`, `/code/tasks/` | -Reload the preview URL after editing; Vite hot-reloads `index.html` on save. +Inbox mock ids: `r-1` … `r-8`. Settings categories include `signals`, `github`, `slack`, `general`, … -## Gotchas +## When fixtures aren't enough -- **Preview mode = mock data.** GitHub/Slack show "Connect …" stubs unless you add mocks for them. Inbox uses fixture reports. Layout and component checks only — not live integrations. -- **Navigate before capture.** A screenshot of the wrong hash is a false pass. -- **`previewMode` must stay in the query string** when changing hash routes. If navigation drops it, re-add `?previewMode=true`. -- **Do not use Electron capture** (`screencapture`, Chrome DevTools Protocol on `:9222`) for this skill — the web preview is the supported path. +Edit the `?previewMode=true` block in `apps/code/index.html` (`mockReports`, tRPC `mocks`, `fetch` interceptor). Re-run capture after save. Preview data is mocked — layout checks only, not live GitHub/Slack. diff --git a/apps/code/package.json b/apps/code/package.json index 7264fcdf4c..7cab63fec6 100644 --- a/apps/code/package.json +++ b/apps/code/package.json @@ -27,6 +27,8 @@ "test": "vitest run", "test:e2e": "playwright test --config=tests/e2e/playwright.config.ts", "test:e2e:headed": "playwright test --config=tests/e2e/playwright.config.ts --headed", + "screenshot:preview": "tsx scripts/screenshot-dev-preview.ts", + "screenshot:preview:serve": "tsx scripts/screenshot-dev-preview.ts --serve", "postinstall": "bash scripts/postinstall.sh", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", diff --git a/apps/code/scripts/screenshot-dev-preview.ts b/apps/code/scripts/screenshot-dev-preview.ts new file mode 100644 index 0000000000..4aa94d5f05 --- /dev/null +++ b/apps/code/scripts/screenshot-dev-preview.ts @@ -0,0 +1,392 @@ +/** + * Fast Playwright captures of the PostHog Code Vite preview (?previewMode=true). + * + * Batch / repeated captures (fast — one browser, hash navigation between routes): + * pnpm --filter code screenshot:preview:serve # background + * pnpm --filter code screenshot:preview -- --route /code/inbox/pulls -o a.png + * + * One-shot (launches Chromium once, then exits): + * pnpm --filter code screenshot:preview -- --route /code/inbox/pulls -o a.png + */ + +import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; +import { createServer, type IncomingMessage } from "node:http"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { type Browser, chromium, type Page } from "@playwright/test"; + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); +const SERVER_FILE = resolve( + SCRIPT_DIR, + "../node_modules/.cache/screenshot-preview-server.json", +); +const DEFAULT_BASE = "http://localhost:5173/?previewMode=true"; +const DEFAULT_VIEWPORT = { width: 1280, height: 900 }; +const DEFAULT_TIMEOUT_MS = 10_000; +const LOADING_TIMEOUT_MS = 2_000; + +interface CaptureRequest { + baseUrl: string; + route: string | null; + url: string | null; + output: string; + fullPage: boolean; + waitFor: string | null; + timeoutMs: number; +} + +interface ServerInfo { + port: number; +} + +interface CliOptions extends CaptureRequest { + mode: "capture" | "serve"; +} + +function printUsage(): void { + process.stderr.write(`Usage: + screenshot-dev-preview.ts --route [-o ] [options] + screenshot-dev-preview.ts --url [-o ] [options] + screenshot-dev-preview.ts --serve + +Options: + --route, --url, -o/--output, --full-page, --wait-for, --base-url, --timeout + --serve Persistent browser + HTTP capture API (use screenshot:preview:serve) + -h, --help +`); +} + +function parseArgs(argv: string[]): CliOptions { + const args = argv[0] === "--" ? argv.slice(1) : argv; + let mode: CliOptions["mode"] = "capture"; + let baseUrl = DEFAULT_BASE; + let route: string | null = null; + let url: string | null = null; + let output = `screenshot-${Date.now()}.png`; + let fullPage = false; + let waitFor: string | null = null; + let timeoutMs = DEFAULT_TIMEOUT_MS; + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + const next = args[i + 1]; + + switch (arg) { + case "--help": + case "-h": + printUsage(); + process.exit(0); + break; + case "--serve": + mode = "serve"; + break; + case "--route": + route = next ?? null; + i += 1; + break; + case "--url": + url = next ?? null; + i += 1; + break; + case "--output": + case "-o": + output = next ?? output; + i += 1; + break; + case "--full-page": + fullPage = true; + break; + case "--wait-for": + waitFor = next ?? null; + i += 1; + break; + case "--base-url": + baseUrl = next ?? baseUrl; + i += 1; + break; + case "--timeout": + timeoutMs = Number(next ?? timeoutMs); + i += 1; + break; + default: + process.stderr.write(`Unknown argument: ${arg}\n`); + printUsage(); + process.exit(1); + } + } + + if (mode === "capture" && !route && !url) { + process.stderr.write("Provide --route or --url (or --serve).\n"); + printUsage(); + process.exit(1); + } + + if (route && url) { + process.stderr.write("Use only one of --route or --url.\n"); + process.exit(1); + } + + return { + mode, + baseUrl, + route, + url, + output, + fullPage, + waitFor, + timeoutMs, + }; +} + +function buildPreviewUrl(request: CaptureRequest): string { + if (request.url) { + return request.url; + } + + const normalizedRoute = request.route?.startsWith("#") + ? request.route.slice(1) + : (request.route ?? ""); + const hashPath = normalizedRoute.startsWith("/") + ? normalizedRoute + : `/${normalizedRoute}`; + + return `${request.baseUrl}#${hashPath}`; +} + +function previewBaseKey(baseUrl: string): string { + const url = new URL(baseUrl); + return `${url.origin}${url.pathname}${url.search}`; +} + +function readServerInfo(): ServerInfo | null { + try { + return JSON.parse(readFileSync(SERVER_FILE, "utf8")) as ServerInfo; + } catch { + return null; + } +} + +function writeServerInfo(info: ServerInfo): void { + mkdirSync(dirname(SERVER_FILE), { recursive: true }); + writeFileSync(SERVER_FILE, JSON.stringify(info), "utf8"); +} + +function clearServerInfo(): void { + try { + unlinkSync(SERVER_FILE); + } catch { + // already gone + } +} + +async function readJsonBody(req: IncomingMessage): Promise { + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); + } + return JSON.parse(Buffer.concat(chunks).toString("utf8")) as T; +} + +async function waitForPaint(page: Page): Promise { + await page.evaluate( + () => + new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(() => resolve()); + }); + }), + ); +} + +async function waitForReady( + page: Page, + request: CaptureRequest, +): Promise { + await page.waitForSelector("#root > *", { timeout: request.timeoutMs }); + + const loading = page.locator("text=Loading").first(); + if (await loading.isVisible().catch(() => false)) { + await loading + .waitFor({ state: "hidden", timeout: LOADING_TIMEOUT_MS }) + .catch(() => {}); + } + + if (request.waitFor) { + await page + .getByText(request.waitFor, { exact: false }) + .first() + .waitFor({ state: "visible", timeout: request.timeoutMs }); + } + + await waitForPaint(page); +} + +async function navigatePreview( + page: Page, + targetUrl: string, + request: CaptureRequest, +): Promise { + const target = new URL(targetUrl); + const baseKey = previewBaseKey(request.baseUrl); + const current = page.url(); + const onPreviewBase = + current.startsWith(baseKey) || current.startsWith(`${baseKey}#`); + + if (onPreviewBase && target.hash) { + const currentHash = new URL(current).hash; + if (currentHash !== target.hash) { + await page.evaluate((hash) => { + window.location.hash = hash; + }, target.hash); + } + await waitForReady(page, request); + return; + } + + await page.goto(targetUrl, { + waitUntil: "commit", + timeout: request.timeoutMs, + }); + await waitForReady(page, request); +} + +async function captureToFile( + page: Page, + request: CaptureRequest, +): Promise { + const targetUrl = buildPreviewUrl(request); + const outputPath = resolve(process.cwd(), request.output); + mkdirSync(dirname(outputPath), { recursive: true }); + + await navigatePreview(page, targetUrl, request); + await page.screenshot({ path: outputPath, fullPage: request.fullPage }); + return outputPath; +} + +async function captureViaServer( + request: CaptureRequest, +): Promise { + const info = readServerInfo(); + if (!info) { + return null; + } + + try { + const response = await fetch(`http://127.0.0.1:${info.port}/capture`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request), + signal: AbortSignal.timeout(request.timeoutMs + 5_000), + }); + + if (!response.ok) { + throw new Error(await response.text()); + } + + return (await response.text()).trim(); + } catch { + clearServerInfo(); + return null; + } +} + +async function captureOneShot(request: CaptureRequest): Promise { + const browser = await chromium.launch({ + headless: true, + args: ["--disable-dev-shm-usage"], + }); + const context = await browser.newContext({ viewport: DEFAULT_VIEWPORT }); + const page = await context.newPage(); + + try { + return await captureToFile(page, request); + } finally { + await context.close(); + await browser.close(); + } +} + +async function runServe(): Promise { + const browser: Browser = await chromium.launch({ + headless: true, + args: ["--disable-dev-shm-usage"], + }); + const context = await browser.newContext({ viewport: DEFAULT_VIEWPORT }); + const page = await context.newPage(); + + const server = createServer(async (req, res) => { + if (req.method !== "POST" || req.url !== "/capture") { + res.writeHead(404); + res.end(); + return; + } + + try { + const request = await readJsonBody(req); + const outputPath = await captureToFile(page, request); + res.writeHead(200, { "content-type": "text/plain" }); + res.end(outputPath); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + res.writeHead(500, { "content-type": "text/plain" }); + res.end(message); + } + }); + + await new Promise((resolve, reject) => { + server.listen(0, "127.0.0.1", () => resolve()); + server.on("error", reject); + }); + + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Failed to bind screenshot preview server"); + } + + writeServerInfo({ port: address.port }); + process.stderr.write( + `screenshot preview server on http://127.0.0.1:${address.port}\n`, + ); + + const shutdown = async () => { + clearServerInfo(); + server.close(); + await context.close().catch(() => {}); + await browser.close().catch(() => {}); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + await new Promise(() => {}); +} + +async function runCapture(options: CliOptions): Promise { + if (options.url && !options.url.includes("previewMode=true")) { + process.stderr.write( + "Warning: URL missing ?previewMode=true — app may not boot.\n", + ); + } + + const outputPath = + (await captureViaServer(options)) ?? (await captureOneShot(options)); + process.stdout.write(`${outputPath}\n`); +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)); + + if (options.mode === "serve") { + await runServe(); + return; + } + + await runCapture(options); +} + +main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`screenshot-dev-preview failed: ${message}\n`); + process.exit(1); +}); From ce91cab28854efd759bad1d10b310618bd96367c Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Tue, 9 Jun 2026 14:09:59 +0100 Subject: [PATCH 4/4] chore(code): expose renderer debug port on dev/start Makes Electron renderer attachable on :9222 during normal dev, alongside the Playwright preview screenshot tooling. --- apps/code/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/code/package.json b/apps/code/package.json index 7cab63fec6..9e63a92501 100644 --- a/apps/code/package.json +++ b/apps/code/package.json @@ -10,8 +10,8 @@ }, "scripts": { "setup": "bash bin/setup", - "dev": "electron-forge start", - "start": "electron-forge start", + "dev": "electron-forge start -- --remote-debugging-port=9222", + "start": "electron-forge start -- --remote-debugging-port=9222", "start:debug": "electron-forge start -- --inspect=5858 --remote-debugging-port=9222", "package": "electron-forge package", "package:dev": "FORCE_DEV_MODE=1 SKIP_NOTARIZE=1 electron-forge package",