Files
PaperClipAI/tools/process-opencode-transcript-screenshots/capture.mjs
T
Jannes StubbemannandClaude Fable 5 35b4769136 docs(screenshots): add before/after transcript screenshots for PR review
Captures the mixed-stream fixture (two plain bootstrap lines followed by
OpenCode JSONL) rendered through the real process adapter module via a
dev-only route at /tests/screenshots/process-opencode-transcript (gated
on import.meta.env.DEV so it tree-shakes out of production builds). The
companion script at
tools/process-opencode-transcript-screenshots/capture.mjs boots vite,
navigates to the route, and writes one PNG per scenario:

  01-before-raw-jsonl-passthrough — previous pass-through parser dumping
                                    OpenCode JSONL as raw stdout
  02-after-rich-opencode-transcript — same stream with detection: plain
                                      lines stay raw, JSONL renders as
                                      the rich transcript

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:01:26 +02:00

121 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Capture before/after screenshots of the process-adapter OpenCode transcript
* rendering for PR review.
*
* Boots the UI Vite dev server and uses Playwright (chromium) to render the
* `/tests/screenshots/process-opencode-transcript` dev-only route (see
* `ui/src/pages/__screenshots__/ProcessOpencodeTranscriptScreenshots.tsx`).
* One PNG per `[data-screenshot-id]` section is written to
* `docs/screenshots/process-opencode-transcript/`.
*
* Usage:
* node tools/process-opencode-transcript-screenshots/capture.mjs
*
* Idempotent — overwrites existing PNGs.
*/
import { spawn } from "node:child_process";
import { mkdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { chromium } from "@playwright/test";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const REPO_ROOT = path.resolve(__dirname, "..", "..");
const UI_DIR = path.join(REPO_ROOT, "ui");
const OUTPUT_DIR = path.join(REPO_ROOT, "docs", "screenshots", "process-opencode-transcript");
const PORT = Number(process.env.SCREENSHOT_PORT ?? 5173);
const BASE_URL = `http://127.0.0.1:${PORT}`;
const SCREENSHOT_PATH = "/tests/screenshots/process-opencode-transcript";
const SECTIONS = [
"01-before-raw-jsonl-passthrough",
"02-after-rich-opencode-transcript",
];
async function waitForServer(url, timeoutMs = 60_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const res = await fetch(url, { method: "GET" });
if (res.ok || res.status === 404) return;
} catch {
// not ready
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw new Error(`Server at ${url} never became ready`);
}
async function startDevServer() {
const proc = spawn(
"pnpm",
["vite", "--port", String(PORT), "--host", "127.0.0.1", "--strictPort"],
{
cwd: UI_DIR,
env: { ...process.env, NODE_ENV: "development" },
stdio: ["ignore", "pipe", "pipe"],
},
);
proc.stdout?.on("data", (chunk) => {
process.stdout.write(`[vite] ${chunk}`);
});
proc.stderr?.on("data", (chunk) => {
process.stderr.write(`[vite] ${chunk}`);
});
return proc;
}
async function captureScreenshots() {
mkdirSync(OUTPUT_DIR, { recursive: true });
const browser = await chromium.launch();
try {
const context = await browser.newContext({
viewport: { width: 1280, height: 1600 },
deviceScaleFactor: 2,
});
const page = await context.newPage();
page.on("console", (msg) => {
if (msg.type() === "error") {
console.error(`[browser console error] ${msg.text()}`);
}
});
await page.goto(`${BASE_URL}${SCREENSHOT_PATH}`, { waitUntil: "networkidle" });
await page.waitForSelector(`[data-screenshot-id="${SECTIONS[0]}"]`, { timeout: 15_000 });
for (const section of SECTIONS) {
const locator = page.locator(`[data-screenshot-id="${section}"]`);
await locator.scrollIntoViewIfNeeded();
await page.waitForTimeout(200);
const filename = path.join(OUTPUT_DIR, `${section}.png`);
await locator.screenshot({ path: filename });
console.log(`Wrote ${path.relative(REPO_ROOT, filename)}`);
}
} finally {
await browser.close();
}
}
async function main() {
console.log(`[capture] booting vite dev server on ${BASE_URL}`);
const server = await startDevServer();
try {
await waitForServer(BASE_URL);
console.log(`[capture] vite ready, capturing screenshots`);
await captureScreenshots();
} finally {
if (server.exitCode === null) {
server.kill("SIGTERM");
await new Promise((resolve) => setTimeout(resolve, 500));
if (server.exitCode === null) server.kill("SIGKILL");
}
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});