Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.73",
"@octokit/rest": "^21.0.0",
"@xterm/xterm": "^6.0.0",
"dotenv": "^16.4.0",
"posthog-node": "^5.28.1",
"sanitize-html": "^2.17.1",
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions services/wizard-ci/report-runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Browser runtime for the snapshot report: render each captured frame's ANSI
// into a real xterm.js terminal so Playwright can screenshot the colored TUI.
// Frames arrive as window.__FRAMES__; sets window.__ready once all have painted.
(() => {
const frames = window.__FRAMES__ || [];
const root = document.getElementById("rows");
let pending = frames.length;
const done = () => {
if (--pending === 0) requestAnimationFrame(() => (window.__ready = true));
};
if (!frames.length) window.__ready = true;

for (const f of frames) {
const section = document.createElement("section");
section.className = "row";
section.setAttribute("data-frame", f.file);
section.innerHTML =
'<h2>' + f.file + ' <span class="t">(' + f.elapsed + ')</span></h2>';
const host = document.createElement("div");
host.className = "term";
section.appendChild(host);
root.appendChild(section);

// Drop the trailing newline so writing the last row doesn't scroll the top
// row (the header bar) off into discarded scrollback.
const ansi = f.ansi.replace(/\n$/, "");
// Size the terminal to the frame: rows = line count, cols = widest line
// (measured with SGR escapes stripped, since they occupy no columns).
const lines = ansi.split("\n");
const cols = Math.max(1, ...lines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, "").length));
const term = new Terminal({
cols,
rows: Math.max(1, lines.length),
fontSize: 14,
fontFamily: 'ui-monospace,SFMono-Regular,Menlo,"DejaVu Sans Mono",monospace',
theme: { background: "#010409", foreground: "#c9d1d9" },
convertEol: true,
scrollback: 0,
disableStdin: true,
cursorInactiveStyle: "none",
});
term.open(host);
term.write(ansi, done);
}
})();
5 changes: 5 additions & 0 deletions services/wizard-ci/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ async function main(): Promise<number> {
});
await page.goto(`file://${report}`);
await page.waitForLoadState("networkidle");
// The report renders each frame into an xterm terminal; wait until they've
// all painted before screenshotting.
await page.waitForFunction(() => (window as { __ready?: boolean }).__ready === true, {
timeout: 30_000,
});

const rows = page.locator("section.row[data-frame]");
const count = await rows.count();
Expand Down
40 changes: 23 additions & 17 deletions services/wizard-ci/snapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
import "dotenv/config";
import { join, basename } from "path";
import { createRequire } from "module";
import {
existsSync,
mkdirSync,
Expand All @@ -40,6 +41,14 @@ import { findApps } from "./utils.js";
import { commandToProgram, findCommandByAppPath } from "../wizard-commands.js";
import { selectApp } from "../wizard-run/picker.js";

// xterm.js is the terminal emulator; the browser build renders the captured
// ANSI to a real colored terminal that Playwright then screenshots. Inline its
// script + CSS so the report is self-contained.
const require = createRequire(import.meta.url);
const XTERM_JS = readFileSync(require.resolve("@xterm/xterm/lib/xterm.js"), "utf8");
const XTERM_CSS = readFileSync(require.resolve("@xterm/xterm/css/xterm.css"), "utf8");
const RUNTIME_JS = readFileSync(require.resolve("./report-runtime.js"), "utf8");

/** A CI-e2e test definition: which flow runs against which app. */
interface TestDef {
name: string;
Expand Down Expand Up @@ -67,36 +76,33 @@ function readFrames(dir: string): Frame[] {
.map((file) => ({ file, text: readFileSync(join(dir, file), "utf8") }));
}

function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function reportHtml(
name: string,
frames: Frame[],
timings: Record<string, number>,
): string {
const rows = frames
.map(
(f) => `
<section class="row" data-frame="${f.file}">
<h2>${f.file} <span class="t">(${fmtElapsed(timings[f.file] ?? 0)})</span></h2>
<pre>${escapeHtml(f.text)}</pre>
</section>`,
)
.join("");
const data = JSON.stringify(
frames.map((f) => ({
file: f.file,
elapsed: fmtElapsed(timings[f.file] ?? 0),
ansi: f.text,
})),
);
return `<!doctype html><html><head><meta charset="utf-8"><title>wizard-ci snapshots — ${name}</title>
<style>
<style>${XTERM_CSS}
body{background:#0d1117;color:#c9d1d9;font:14px/1.2 ui-monospace,SFMono-Regular,Menlo,"DejaVu Sans Mono","Liberation Mono",Consolas,monospace;margin:0;padding:24px}
h1{font-size:18px} .summary{margin:8px 0 24px;color:#8b949e}
.row{background:#0d1117;border:1px solid #21262d;border-radius:8px;margin:16px 0;padding:12px 16px}
.row{background:#010409;border:1px solid #21262d;border-radius:8px;margin:16px 0;padding:12px 16px}
h2{font-size:14px;margin:0 0 10px;font-weight:600}
.t{color:#8b949e;font-weight:400}
pre{background:#010409;border:1px solid #21262d;border-radius:6px;padding:10px;margin:0;overflow:auto;white-space:pre}
.term{display:inline-block}
</style></head><body>
<h1>wizard-ci TUI snapshots — ${name}</h1>
<div class="summary">${frames.length} key-moment frames from the current run</div>
${rows}
<div id="rows"></div>
<script>window.__FRAMES__ = ${data};</script>
<script>${XTERM_JS}</script>
<script>${RUNTIME_JS}</script>
</body></html>`;
}

Expand Down