diff --git a/apps/web/package.json b/apps/web/package.json index 1c59bd9..2cee383 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,7 +8,8 @@ "build": "vite build", "preview": "vite preview", "lint": "tsc --noEmit", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "screenshots": "playwright test --config=playwright.screenshots.config.ts" }, "dependencies": { "@fluffylabs/shared-ui": "^0.4.6", diff --git a/apps/web/playwright.screenshots.config.ts b/apps/web/playwright.screenshots.config.ts new file mode 100644 index 0000000..f2163c6 --- /dev/null +++ b/apps/web/playwright.screenshots.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "@playwright/test"; + +/** + * Dedicated Playwright config for regenerating the usage-guide screenshots. + * + * Run with: npm run screenshots + * + * Specs live in ./screenshots and drive the UI into specific states before + * calling page.screenshot(). Output is written to docs/usage-screenshots/. + */ +export default defineConfig({ + testDir: "./screenshots", + testMatch: /.*\.screenshot\.ts$/, + timeout: 60_000, + retries: 0, + fullyParallel: false, + workers: 1, + reporter: "list", + use: { + baseURL: "http://localhost:4199", + viewport: { width: 1440, height: 900 }, + deviceScaleFactor: 2, + colorScheme: "dark", + }, + webServer: { + command: "npx vite preview --port 4199", + port: 4199, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/apps/web/screenshots/01-load.screenshot.ts b/apps/web/screenshots/01-load.screenshot.ts new file mode 100644 index 0000000..816001a --- /dev/null +++ b/apps/web/screenshots/01-load.screenshot.ts @@ -0,0 +1,52 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { capture, expect, settle, test } from "./helpers"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const FIXTURES = path.resolve(HERE, "..", "..", "..", "fixtures"); + +test.describe("Load screens", () => { + test("load-examples: bundled example browser", async ({ page }) => { + await page.goto("/#/load"); + await page.getByTestId("load-page").waitFor(); + // Expand WAT and JSON categories so more examples are visible in the shot. + await page.getByTestId("category-toggle-wat").click(); + await page.getByTestId("category-toggle-json-test-vectors").click(); + await settle(page); + await capture(page, "load-examples.png"); + }); + + test("load-upload: uploaded file selected", async ({ page }) => { + await page.goto("/#/load"); + await page.getByTestId("load-page").waitFor(); + const input = page.getByTestId("file-upload-input"); + await input.setInputFiles(path.join(FIXTURES, "generic", "branch.pvm")); + await expect(page.getByTestId("file-upload-selected")).toBeVisible(); + await settle(page); + await capture(page, "load-upload.png"); + }); + + test("load-url: URL field filled", async ({ page }) => { + await page.goto("/#/load"); + await page.getByTestId("load-page").waitFor(); + await page + .getByTestId("url-input-field") + .fill( + "https://github.com/FluffyLabs/pvm-debugger/blob/main/fixtures/generic/branch.pvm", + ); + await settle(page); + await capture(page, "load-url.png"); + }); + + test("load-manual: hex input filled", async ({ page }) => { + await page.goto("/#/load"); + await page.getByTestId("load-page").waitFor(); + await page + .getByTestId("manual-input-field") + .fill("0x00 03 00 01 00 0d 00 08 00 02 00 07 00 01"); + // Blur so the byte count appears. + await page.getByTestId("manual-input-field").blur(); + await settle(page); + await capture(page, "load-manual.png"); + }); +}); diff --git a/apps/web/screenshots/02-config.screenshot.ts b/apps/web/screenshots/02-config.screenshot.ts new file mode 100644 index 0000000..a95e473 --- /dev/null +++ b/apps/web/screenshots/02-config.screenshot.ts @@ -0,0 +1,23 @@ +import { capture, expect, settle, test } from "./helpers"; + +test.describe("Config step", () => { + test("config-jam-builder: JAM SPI builder mode", async ({ page }) => { + await page.goto("/#/load"); + await page.getByTestId("category-toggle-wat").click(); + await page.getByTestId("example-card-add-jam").click(); + await expect(page.getByTestId("config-step")).toBeVisible(); + await settle(page); + await capture(page, "config-jam-builder.png"); + }); + + test("config-jam-raw: JAM SPI raw mode", async ({ page }) => { + await page.goto("/#/load"); + await page.getByTestId("category-toggle-wat").click(); + await page.getByTestId("example-card-add-jam").click(); + await expect(page.getByTestId("config-step")).toBeVisible(); + await page.getByTestId("spi-raw-mode-switch").click(); + await expect(page.getByTestId("spi-raw-hex")).toBeVisible(); + await settle(page); + await capture(page, "config-jam-raw.png"); + }); +}); diff --git a/apps/web/screenshots/03-debugger.screenshot.ts b/apps/web/screenshots/03-debugger.screenshot.ts new file mode 100644 index 0000000..8a02b42 --- /dev/null +++ b/apps/web/screenshots/03-debugger.screenshot.ts @@ -0,0 +1,59 @@ +import type { Page } from "@playwright/test"; +import { capture, expect, settle, test } from "./helpers"; + +/** Load a generic PVM example that goes straight to the debugger. */ +async function loadGeneric(page: Page, id: string) { + await page.goto("/#/load"); + await page.getByTestId(`example-card-${id}`).click(); + await expect(page.getByTestId("debugger-page")).toBeVisible({ + timeout: 15000, + }); +} + +test.describe("Debugger screen", () => { + test("debugger-full: layout with instructions/registers/memory", async ({ + page, + }) => { + await loadGeneric(page, "fibonacci"); + await settle(page); + await capture(page, "debugger-full.png"); + }); + + test("debugger-stepping: after several steps", async ({ page }) => { + await loadGeneric(page, "fibonacci"); + // Advance a few instructions so register deltas and the PC marker + // move off the initial state. + for (let i = 0; i < 5; i++) { + await page.getByTestId("next-button").click(); + await page.waitForTimeout(60); + } + await settle(page); + await capture(page, "debugger-stepping.png"); + }); + + test("registers-edit: inline register editor open", async ({ page }) => { + await loadGeneric(page, "fibonacci"); + // Click the hex value to reveal the inline editor input. + await page.getByTestId("register-hex-7").click(); + await expect(page.getByTestId("register-edit-7")).toBeVisible(); + await settle(page); + await capture(page, "registers-edit.png"); + }); + + test("memory-panel: expanded range with bytes", async ({ page }) => { + // Game of Life has a writable memory page mapped. + await loadGeneric(page, "game-of-life"); + // Take a few steps so memory has non-zero bytes to show. + for (let i = 0; i < 30; i++) { + await page.getByTestId("next-button").click(); + await page.waitForTimeout(20); + } + // Expand the first memory range by clicking its header. + const firstHeader = page + .locator("[data-testid^='memory-range-header-']") + .first(); + await firstHeader.click(); + await settle(page); + await capture(page, "memory-panel.png"); + }); +}); diff --git a/apps/web/screenshots/04-settings.screenshot.ts b/apps/web/screenshots/04-settings.screenshot.ts new file mode 100644 index 0000000..a49fb57 --- /dev/null +++ b/apps/web/screenshots/04-settings.screenshot.ts @@ -0,0 +1,46 @@ +import { capture, expect, settle, test } from "./helpers"; + +test.describe("Settings and multi-PVM", () => { + test("settings: drawer showing PVM toggles and stepping modes", async ({ + page, + }) => { + await page.goto("/#/load"); + await page.getByTestId("example-card-fibonacci").click(); + await expect(page.getByTestId("debugger-page")).toBeVisible({ + timeout: 15000, + }); + await page.getByTestId("drawer-tab-settings").click(); + await expect(page.getByTestId("settings-tab")).toBeVisible(); + await settle(page); + await capture(page, "settings.png"); + }); + + test("multiple-pvms: both typeberry and ananas enabled", async ({ page }) => { + await page.goto("/#/load"); + await page.getByTestId("example-card-fibonacci").click(); + await expect(page.getByTestId("debugger-page")).toBeVisible({ + timeout: 15000, + }); + await page.getByTestId("drawer-tab-settings").click(); + await expect(page.getByTestId("settings-tab")).toBeVisible(); + // Enable ananas alongside typeberry. + const ananasSwitch = page.getByTestId("pvm-switch-ananas"); + const isChecked = await ananasSwitch.isChecked().catch(() => false); + if (!isChecked) { + await ananasSwitch.click(); + } + // Close drawer so PVM tabs in the top-right are in frame. + await page.getByTestId("drawer-close-button").click(); + // Both PVM tabs should appear. + await expect(page.getByTestId("pvm-tab-typeberry")).toBeVisible(); + await expect(page.getByTestId("pvm-tab-ananas")).toBeVisible(); + // Advance a few steps to produce deltas between the two PVMs (usually none + // for a clean example, but stepping gives the UI something real to show). + for (let i = 0; i < 3; i++) { + await page.getByTestId("next-button").click(); + await page.waitForTimeout(100); + } + await settle(page); + await capture(page, "multiple-pvms.png"); + }); +}); diff --git a/apps/web/screenshots/05-persistence.screenshot.ts b/apps/web/screenshots/05-persistence.screenshot.ts new file mode 100644 index 0000000..7102ad0 --- /dev/null +++ b/apps/web/screenshots/05-persistence.screenshot.ts @@ -0,0 +1,25 @@ +import { capture, expect, settle, test } from "./helpers"; + +test.describe("Persistence", () => { + test("persistence-restored: state preserved across reload", async ({ + page, + }) => { + await page.goto("/#/load"); + await page.getByTestId("example-card-fibonacci").click(); + await expect(page.getByTestId("debugger-page")).toBeVisible({ + timeout: 15000, + }); + // Take a few steps so the restored state looks "live". + for (let i = 0; i < 4; i++) { + await page.getByTestId("next-button").click(); + await page.waitForTimeout(60); + } + // Reload the page — persistence should reinstall the program. + await page.reload(); + await expect(page.getByTestId("debugger-page")).toBeVisible({ + timeout: 15000, + }); + await settle(page); + await capture(page, "persistence-restored.png"); + }); +}); diff --git a/apps/web/screenshots/06-host-calls.screenshot.ts b/apps/web/screenshots/06-host-calls.screenshot.ts new file mode 100644 index 0000000..566e03b --- /dev/null +++ b/apps/web/screenshots/06-host-calls.screenshot.ts @@ -0,0 +1,88 @@ +import type { Page } from "@playwright/test"; +import { + advanceToNextHostCall, + capture, + expect, + runToHostCall, + setAutoContinueNever, + settle, + test, +} from "./helpers"; + +/** + * Screenshots of the sprint-42 redesigned host-call drawer. + * + * Fixture choices: + * - `io-trace` — short trace, first host call is `fetch`. Used for the + * two-column overview and the pending-changes banner. + * - `all-ecalli-accumulate` — exercises every host-call kind; hits a `log` + * call within the first few pauses. + */ + +async function loadTraceExample(page: Page, id: "io-trace") { + await page.goto("/#/load"); + await page.getByTestId(`example-card-${id}`).click(); + await expect(page.getByTestId("debugger-page")).toBeVisible({ + timeout: 15000, + }); +} + +async function loadAllEcalliAccumulate(page: Page) { + await page.goto("/#/load"); + await page.getByTestId("example-card-all-ecalli-accumulate").click(); + await expect(page.getByTestId("config-step")).toBeVisible({ timeout: 15000 }); + const loadBtn = page.getByTestId("config-step-load"); + await expect(loadBtn).toBeEnabled({ timeout: 10000 }); + await loadBtn.click(); + await expect(page.getByTestId("debugger-page")).toBeVisible({ + timeout: 30000, + }); +} + +test.describe("Host call drawer", () => { + test("host-call-overview: redesigned two-column layout", async ({ page }) => { + await loadTraceExample(page, "io-trace"); + await setAutoContinueNever(page); + await runToHostCall(page); + await settle(page); + await capture(page, "host-call-overview.png"); + }); + + test("host-call-log: decoded log message", async ({ page }) => { + await loadAllEcalliAccumulate(page); + await setAutoContinueNever(page); + await runToHostCall(page); + for (let i = 0; i < 30; i++) { + const logVisible = await page + .getByTestId("log-host-call") + .isVisible() + .catch(() => false); + if (logVisible) break; + if (!(await advanceToNextHostCall(page))) break; + } + await expect(page.getByTestId("log-host-call")).toBeVisible(); + await settle(page); + await capture(page, "host-call-log.png"); + }); + + test("host-call-pending-changes: banner with arrow notation", async ({ + page, + }) => { + await loadTraceExample(page, "io-trace"); + await setAutoContinueNever(page); + await runToHostCall(page); + // Pending-changes is debounced; give it a moment before checking. + for (let i = 0; i < 20; i++) { + await page.waitForTimeout(500); + const pendingVisible = await page + .getByTestId("pending-changes") + .isVisible() + .catch(() => false); + if (pendingVisible) break; + if (!(await advanceToNextHostCall(page))) break; + } + await expect(page.getByTestId("pending-changes")).toBeVisible(); + await settle(page); + await capture(page, "host-call-pending-changes.png"); + }); +}); diff --git a/apps/web/screenshots/07-fetch-trace.screenshot.ts b/apps/web/screenshots/07-fetch-trace.screenshot.ts new file mode 100644 index 0000000..4faf146 --- /dev/null +++ b/apps/web/screenshots/07-fetch-trace.screenshot.ts @@ -0,0 +1,67 @@ +import type { Page } from "@playwright/test"; +import { + advanceToNextHostCall, + capture, + expect, + runToHostCall, + setAutoContinueNever, + settle, + test, +} from "./helpers"; + +/** + * Fetch host-call and ecalli-trace screenshots. The `io-trace` example is a + * short typeberry test trace that starts with a fetch (ecalli=1) — good for + * showing the redesigned fetch handler on the first pause. + */ + +async function loadIoTrace(page: Page) { + await page.goto("/#/load"); + await page.getByTestId("example-card-io-trace").click(); + await expect(page.getByTestId("debugger-page")).toBeVisible({ + timeout: 15000, + }); +} + +test.describe("Fetch host call and trace", () => { + test("host-call-fetch: struct-mode editor with slice preview", async ({ + page, + }) => { + await loadIoTrace(page); + await setAutoContinueNever(page); + await runToHostCall(page); + // Advance until a fetch host call is active. + for (let i = 0; i < 30; i++) { + const fetchVisible = await page + .getByTestId("fetch-host-call") + .isVisible() + .catch(() => false); + if (fetchVisible) break; + if (!(await advanceToNextHostCall(page))) break; + } + await expect(page.getByTestId("fetch-host-call")).toBeVisible(); + // Prefer struct mode if available. + const structTab = page.getByTestId("fetch-mode-struct"); + if (await structTab.isVisible().catch(() => false)) { + const disabled = await structTab.isDisabled().catch(() => false); + if (!disabled) await structTab.click(); + } + await settle(page); + await capture(page, "host-call-fetch.png"); + }); + + test("trace-comparison: side-by-side execution vs reference", async ({ + page, + }) => { + await loadIoTrace(page); + await setAutoContinueNever(page); + // Advance through one host call so the execution column has at least one + // entry alongside the reference trace. + await runToHostCall(page); + await advanceToNextHostCall(page); + await page.getByTestId("drawer-tab-ecalli_trace").click(); + await expect(page.getByTestId("ecalli-trace-tab")).toBeVisible(); + await settle(page); + await capture(page, "trace-comparison.png"); + }); +}); diff --git a/apps/web/screenshots/README.md b/apps/web/screenshots/README.md new file mode 100644 index 0000000..00ea01d --- /dev/null +++ b/apps/web/screenshots/README.md @@ -0,0 +1,70 @@ +# Usage-guide screenshot capture + +Playwright scripts that drive the app into specific states and save PNGs to +`docs/usage-screenshots/`. These back the images referenced from +`docs/usage-guide.md`. + +## Running + +From the repo root (builds the app first so the preview server has something to serve): + +```bash +npm run build +cd apps/web && npm run screenshots +``` + +Or, if a dev/preview server is already running on port 4199, just: + +```bash +cd apps/web && npm run screenshots +``` + +## Layout + +- `helpers.ts` — shared fixture that forces dark mode, font-ready waits, and a + `capture()` helper that writes into `docs/usage-screenshots/`. +- `*.screenshot.ts` — one file per feature area. Each test corresponds to one + screenshot referenced from the usage guide. + +## Conventions + +- Fixed viewport 1440×900 @ deviceScaleFactor 2 (see + `playwright.screenshots.config.ts`). +- Dark theme, enforced by `localStorage.theme-mode = "dark"` injected before + first paint. +- Animations disabled at capture time so drawer transitions don't blur output. +- Scripts pick real, visually informative states (e.g. factorial stepped a few + times, game-of-life running) rather than empty initial states, unless the + initial state is the point of the shot. + +## Adding a new screenshot + +1. Add a test in an existing `*.screenshot.ts` file, or create a new one for a + new feature area. +2. Use `capture(page, "filename.png")` for full-viewport shots, or pass a + locator for element-scoped shots. +3. Reference the new image from `docs/usage-guide.md`. +4. Run `npm run screenshots` and commit the PNG. + +## Gotchas when capturing host-call states + +Driving the debugger between host-call pauses has two traps. `helpers.ts` +provides shared utilities — prefer them over rolling your own: + +- **`Next` is one instruction, not one host call.** After you resume a host + call via Next, the next instruction is almost never another ecalli, so you + land on a normal paused state. Use `advanceToNextHostCall(page)` instead — + it clicks Run, which (with `auto-continue=never`) stops at the next host + call automatically. +- **`pvm-status-typeberry` can stay on "Host Call" across a transition.** The + text doesn't always leave "Host Call" before the PVM pauses on the next + one. Detect advancement by diffing the PC value (`pc-value` testid). + `advanceToNextHostCall` does this for you. +- **Always call `setAutoContinueNever(page)` before `runToHostCall`.** The + default policy (`continue_when_trace_matches`) blasts through matching + host calls on trace-backed examples, and your capture will never pause. +- **React error #185 (infinite render cycle)** can still trigger on rapid Run + clicks for some trace programs, despite sprint-47's fix. `advanceToNextHostCall` + adds a short settle-delay before each click as a workaround. If you see the + "Something went wrong / Reload" error boundary during a capture, rerun — it + is usually transient. diff --git a/apps/web/screenshots/helpers.ts b/apps/web/screenshots/helpers.ts new file mode 100644 index 0000000..8de6a53 --- /dev/null +++ b/apps/web/screenshots/helpers.ts @@ -0,0 +1,183 @@ +import { existsSync, mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { Locator, Page } from "@playwright/test"; +import { test as base } from "@playwright/test"; + +const HERE = dirname(fileURLToPath(import.meta.url)); + +/** + * Root folder where screenshots end up. Resolved once so all specs agree. + * `HERE` is apps/web/screenshots; climb three levels to the repo root. + */ +export const SCREENSHOT_DIR = resolve( + HERE, + "..", + "..", + "..", + "docs", + "usage-screenshots", +); + +/** + * Resolve a screenshot filename to its final on-disk path. + */ +export function screenshotPath(name: string): string { + const path = resolve(SCREENSHOT_DIR, name); + const dir = dirname(path); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + return path; +} + +/** + * Wait for web fonts to finish loading so Poppins/Inconsolata render + * consistently between runs. + */ +export async function waitForFontsReady(page: Page): Promise { + await page.evaluate(async () => { + if (document.fonts?.ready) { + await document.fonts.ready; + } + }); +} + +/** + * A small settle delay for animated UI (Vaul drawers, tab underlines). + * Playwright's screenshot stabilization handles most of this, but a tiny + * buffer avoids flaky captures of mid-transition states. + */ +export async function settle(page: Page, ms = 250): Promise { + await page.waitForTimeout(ms); +} + +/** + * Playwright test fixture preset to dark mode. + * + * Injects `localStorage.theme-mode = "dark"` before any page code runs, + * so the shared-ui `initializeTheme()` picks it up on boot. + */ +export const test = base.extend({ + page: async ({ page }, use) => { + await page.addInitScript(() => { + window.localStorage.setItem("theme-mode", "dark"); + }); + await use(page); + }, +}); + +export { expect } from "@playwright/test"; + +/** + * Capture either the full page or a specific locator, writing under + * docs/usage-screenshots/ with the given filename. + */ +export async function capture( + target: Page | Locator, + name: string, +): Promise { + const path = screenshotPath(name); + if ("screenshot" in target && "evaluate" in target) { + const page = target as Page; + await waitForFontsReady(page); + await page.screenshot({ path, fullPage: false, animations: "disabled" }); + } else { + await (target as Locator).screenshot({ path, animations: "disabled" }); + } +} + +// --------------------------------------------------------------------------- +// Host-call navigation helpers. +// +// Why these live here and not inline in each spec: advancing between +// host-call pauses has two non-obvious traps that bit me during this sprint. +// +// 1. `Next` advances ONE instruction. After resuming a host call, the next +// instruction is rarely another ecalli, so `Next` lands on a regular +// paused state instead of the next host-call. Use `Run` with +// `autoContinuePolicy=never` — Run stops automatically at every host +// call. +// 2. `pvm-status-typeberry` text can stay on "Host Call" across the +// transition from one pause to the next. To detect that execution +// actually advanced, diff the PC value from `pc-value`. +// --------------------------------------------------------------------------- + +/** Read the current PC from the status header; null if it isn't rendered. */ +export async function currentPc(page: Page): Promise { + return await page + .getByTestId("pc-value") + .textContent() + .catch(() => null); +} + +/** + * Set the host-call auto-continue policy to "Never (Manual)". Opens the + * Settings drawer if needed and verifies the radio is checked after the + * click. + */ +export async function setAutoContinueNever(page: Page): Promise { + await page.getByTestId("drawer-tab-settings").click(); + const radio = page.getByTestId("auto-continue-radio-never"); + await radio.click(); + // Fail fast if the click didn't take effect — otherwise captures silently + // run with the default policy and blast through all host calls. + if (!(await radio.isChecked())) { + throw new Error("auto-continue=never did not stick"); + } +} + +/** + * Click Run and wait until the PVM pauses on the first host call. + * + * Assumes `setAutoContinueNever` has been called. If the program has no host + * calls at all, this will time out — by design. + */ +export async function runToHostCall( + page: Page, + timeoutMs = 20_000, +): Promise { + await page.getByTestId("run-button").click(); + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const status = await page + .getByTestId("pvm-status-typeberry") + .textContent() + .catch(() => null); + if (status === "Host Call") return; + await page.waitForTimeout(100); + } + throw new Error("Timed out waiting for first host-call pause"); +} + +/** + * Advance from the current host-call pause to the next one. + * + * Returns `true` if a new pause was reached (different PC), `false` if the + * program terminated instead. + * + * Uses Run (not Next) — see the header comment above. + * + * The leading settle-delay is a workaround for a flaky React-render cycle + * (error #185) that can occur when Run is clicked rapidly on some trace + * programs. Sprint-47 narrowed the common trigger but did not eliminate it + * entirely; a proper fix is tracked separately. + */ +export async function advanceToNextHostCall(page: Page): Promise { + const before = await currentPc(page); + await page.waitForTimeout(400); + await page.getByTestId("run-button").click(); + const deadline = Date.now() + 15_000; + while (Date.now() < deadline) { + const status = await page + .getByTestId("pvm-status-typeberry") + .textContent() + .catch(() => null); + if (status === "Host Call") { + const now = await currentPc(page); + if (now && now !== before) return true; + } else if (status && status !== "Running") { + return false; + } + await page.waitForTimeout(100); + } + return false; +} diff --git a/apps/web/src/components/debugger/PendingChanges.tsx b/apps/web/src/components/debugger/PendingChanges.tsx index 340292c..0dd92da 100644 --- a/apps/web/src/components/debugger/PendingChanges.tsx +++ b/apps/web/src/components/debugger/PendingChanges.tsx @@ -131,7 +131,7 @@ export function PendingChanges({ pending }: PendingChangesProps) { {[...pending.registerWrites.entries()].map(([regIdx, val]) => (
- {"\u03C9"} + {"\u03C6"} {regIdx} {"\u2190"} {formatRegister(val).hex}
diff --git a/apps/web/src/components/load/DetectionSummary.tsx b/apps/web/src/components/load/DetectionSummary.tsx index 7112aff..6c56caf 100644 --- a/apps/web/src/components/load/DetectionSummary.tsx +++ b/apps/web/src/components/load/DetectionSummary.tsx @@ -282,7 +282,7 @@ function RegisterPreview({ registers }: { registers: bigint[] }) { const preview = nonZero .slice(0, 4) - .map(({ i, v }) => `\u03C9${i}=0x${v.toString(16)}`) + .map(({ i, v }) => `\u03C6${i}=0x${v.toString(16)}`) .join(", "); const suffix = nonZero.length > 4 ? `, +${nonZero.length - 4} more` : ""; diff --git a/docs/usage-guide.md b/docs/usage-guide.md index c4014c5..84940d3 100644 --- a/docs/usage-guide.md +++ b/docs/usage-guide.md @@ -1,6 +1,6 @@ # PVM Debugger Usage Guide -PVM Debugger is a browser-based debugger for Polkavm Virtual Machine programs. You can load bundled examples, upload local files, inspect machine state, compare trace-backed host calls, and switch between multiple PVM implementations from one UI. +PVM Debugger is a browser-based debugger for PolkaVM programs. You can load bundled examples, upload local files, inspect machine state, compare trace-backed host calls, edit pending host-call effects before resuming, and run multiple PVM implementations side by side. ## 1. Getting Started @@ -8,11 +8,15 @@ Start the app locally with `npm run dev`, then open `http://localhost:5173`. ![Load screen with bundled examples and input sources](usage-screenshots/load-examples.png) +The left sidebar links to sibling Fluffy Labs tools. The bottom-left icon toggles dark/light mode. The debugger is the main content area. + ## 2. Loading a Program +The loader offers four input sources. Non-SPI programs jump straight to the debugger; JAM SPI programs stop at a configuration step first. + ### From Examples -Use the examples column on the right to open bundled programs. The six categories are Generic PVM, WAT to PVM, AssemblyScript, Large Programs, JSON Test Vectors, and Trace Files. Click any example to jump straight to the configuration step. +The Examples column on the right groups bundled programs by category: Generic PVM, WAT → PVM, AssemblyScript, Large Programs, JSON Test Vectors, and Trace Files. Categories can be expanded or collapsed; Generic, AssemblyScript, and Trace Files are expanded by default. Click any example to load it. ![Loader showing example categories and bundled examples](usage-screenshots/load-examples.png) @@ -20,7 +24,7 @@ Use the examples column on the right to open bundled programs. The six categorie Drop a file or use the browse button to load `.jam`, `.pvm`, `.bin`, `.log`, `.trace`, or `.json` input from disk. -![Loader with an uploaded branch.pvm file selected](usage-screenshots/load-upload.png) +![Loader with an uploaded .pvm file selected](usage-screenshots/load-upload.png) ### From URL @@ -36,80 +40,159 @@ Paste raw hex bytes when you want to debug a tiny program without creating a fil ## 3. Configuring the Program -After source selection, review the detected format summary and adjust any format-specific options before loading the debugger. For JAM SPI programs, Builder mode exposes the entrypoint form and RAW mode lets you edit the encoded argument bytes directly. +JAM SPI programs show a Program Summary with detection info and an SPI Entrypoint editor before loading into the debugger. -![JAM SPI configuration in builder mode with accumulate parameters](usage-screenshots/config-jam-builder.png) +![JAM SPI configuration in builder mode](usage-screenshots/config-jam-builder.png) -Use `Refine`, `Accumulate`, or `Is Authorized` depending on which SPI entrypoint you want to invoke. Gas is not configured here; you can edit gas later from the debugger. +Choose `Refine`, `Accumulate`, or `Is Authorized` depending on which SPI entrypoint you want to invoke. Fill in the entrypoint-specific fields; the SPI Arguments hex is regenerated automatically from your inputs. + +Toggle the RAW switch to edit the encoded argument bytes directly instead of using the builder form. ![JAM SPI configuration in RAW mode](usage-screenshots/config-jam-raw.png) +Press **Load Program** to send the program to the debugger. Gas is not set here; you can edit gas later while paused. + ## 4. The Debugger Screen -Once the program loads, the debugger shows three main columns: Instructions on the left, Registers and Status in the center, and Memory on the right. The execution controls stay in the top bar, and the bottom drawer holds Settings, Ecalli Trace, Host Call, and Logs. +Once the program loads, the debugger shows three columns: Instructions on the left, Registers and Status in the middle, and Memory on the right. Execution controls sit in the top toolbar, and the bottom drawer holds four tabs: Settings, Ecalli Trace, Host Call, and Logs. The active drawer tab is highlighted with a brand-colored underline. -![Debugger layout with instructions, registers, memory, and the bottom drawer open](usage-screenshots/debugger-full.png) +![Debugger layout with instructions, registers, and memory](usage-screenshots/debugger-full.png) + +If a render error ever escapes the app, an error boundary catches it and shows a "Reload" button that returns you to the loader. ## 5. Stepping Through Code -Use `Next` to advance one instruction, `Step` to use the configured stepping mode, `Run` to continue until a stop condition, `Pause` to stop a run loop, and `Reset` to reload the initial program state. Keyboard shortcuts are: +Use `Next` to advance according to the current stepping mode, `Step` to advance a single basic step, `Run` to continue until a stop condition, `Pause` to stop a run loop, and `Reset` to reload the program's initial state. Keyboard shortcuts: - `F10` for `Next` - `F5` for `Run` / `Pause` - `Ctrl+Shift+R` for `Reset` -Choose the `Instruction`, `Block`, or `N-Instructions` stepping mode from the Settings drawer. +Choose `Instruction`, `Block`, or `N-Instructions` stepping from the Settings drawer. Breakpoints can be toggled by clicking the gutter of any instruction row in the Instructions panel. -![Debugger after stepping, with the current PC highlighted and changed state markers visible](usage-screenshots/debugger-stepping.png) +![Debugger after stepping, with the current PC highlighted](usage-screenshots/debugger-stepping.png) ## 6. Working With Registers and State -When execution is paused, you can edit the PC, gas, and all 13 registers inline. Registers always show a fixed-width hex encoding and a decimal value, PC is displayed in hex, and gas is displayed in decimal with a hex tooltip. +When execution is paused, you can edit the PC, gas, and all 13 registers inline. Click a register's hex value to open its editor. Registers show a fixed-width hex encoding with their decimal value in parentheses; the labels use the φ (phi) notation matching the Gray Paper spec. PC is displayed in hex; gas is displayed in decimal with a hex tooltip on hover. Changed values flash once and keep a small brand-colored dot until the next step. ![Registers panel with inline editing enabled for a register](usage-screenshots/registers-edit.png) +When execution is paused at a host call, a **Pending Host Call Changes** banner appears inside the Registers panel. It previews the register writes, memory writes (coalesced into ranges with a short hex preview), and gas update that will be applied on resume. Use it to verify the effects before stepping forward. + ## 7. Viewing Memory -Expand any mapped memory range to inspect bytes in a hex dump. Ranges are lazy-loaded, collapsed by default, and changed bytes are highlighted after execution mutates memory. +Expand any mapped memory range to inspect bytes in a hex dump. Ranges are lazy-loaded, collapsed by default, and labeled with their role (RO Data, RW Data, Heap, Stack, Arguments) alongside the hex start address. Bytes that changed between the last two paused states are highlighted. -![Memory panel with an expanded range and highlighted post-execution bytes](usage-screenshots/memory-panel.png) +![Memory panel with an expanded range](usage-screenshots/memory-panel.png) ## 8. Host Calls -When execution pauses on an `ecalli`, the Host Call drawer opens automatically. Review the decoded call, then use the normal `Next`, `Step`, or `Run` controls to continue. There is no separate resume button. +When execution pauses on an `ecalli`, the Host Call drawer opens automatically. Sprint 42 redesigned the drawer as a two-column layout: a sidebar on the left with host-call metadata, and a handler-specific editor on the right. + +![Host Call drawer paused on a fetch call](usage-screenshots/host-call-overview.png) + +### Sidebar + +- **Badge** with the host-call name (`fetch`, `log`, `write`, `read`, …) and its index. +- **Input registers** listed with labels defined per host call (e.g. `dest`, `maxLen`, `kind` for fetch). +- **Output preview** showing the value that will be written to `φ₇` on resume. +- **Memory write count** when the pending effects include memory writes. + +### Editor (right column) + +The right column adapts to the host call kind: + +- **Fetch** (index 1) shows the fetch-handler editor with three modes — Trace, Raw, and Struct — plus a slice preview bar. See [Fetch Handler](#fetch-handler) below. +- **Storage read/write** (indices 3/4) shows the session-scoped key/value table so you can inspect or seed storage before resuming. +- **Log** (index 100) shows the decoded log level, target, and message content, with a raw hex fallback. + + ![Host Call drawer showing a decoded log message](usage-screenshots/host-call-log.png) + +- **Other host calls** (gas, read, info, write, and anything not specifically handled) fall back to a generic text editor. Enter one command per line; blank lines and `#` comments are allowed. Parse errors are reported with the offending line number. + + ``` + # Return value in φ7 + setreg φ7 <- 0x2a + + # Write 4 bytes to memory + memwrite 0x100 len=4 <- 0xdeadbeef -![Debugger paused on a host call with the Host Call drawer open](usage-screenshots/host-call-overview.png) + # Set gas after the call + setgas <- 500000 + ``` -Log host calls render decoded text and raw payload data when available. +### Sticky bar -![Host Call drawer showing a decoded log message](usage-screenshots/host-call-log.png) +At the bottom of the drawer, a sticky bar shows: -Storage host calls expose an editable key/value table so you can inspect or seed storage-backed effects before resuming. +- A **NONE** checkbox for host calls that accept it (fetch, lookup, read, info). Checking it returns `φ₇ = 2⁶⁴ − 1` and suppresses memory writes; the handler editor is hidden while NONE is active. +- **Use Trace Data** (amber pill) appears when you have edited the effects and a matching reference trace proposal exists. Clicking it resets the editor to the trace's proposed effects. +- **Changes auto-applied** — the drawer auto-applies edits on the fly, so there is no separate Apply button. Errors in the editor are surfaced here in red. -![Host Call drawer showing the editable storage table](usage-screenshots/host-call-storage.png) +Use the normal `Next`, `Step`, or `Run` controls to resume past the host call. If your source includes a reference trace and the Host Call Policy allows it, matching host calls auto-continue without stopping. -If your source includes a reference trace and the Host Call Policy allows it, matching host calls can auto-continue without opening the drawer. +### Fetch Handler -## 9. Trace Comparison +The fetch handler (sprint 43) supports all 16 fetch kinds (Protocol Constants, Entropy, Work Package, Operand, Transfer, Authorizer Info, …). Switch between three modes; the underlying bytes are preserved when you switch. -Load a trace-backed example or trace file to compare the live execution trace against a reference trace. The Ecalli Trace drawer shows execution and reference columns side by side. When a value diverges, the differing row is highlighted. +![Fetch handler in struct mode with encoded output and slice preview](usage-screenshots/host-call-fetch.png) -![Ecalli Trace drawer comparing execution output with a reference trace](usage-screenshots/trace-comparison.png) +- **Trace** — read-only view of the reference trace's bytes, shown when the loaded source has a matching trace entry. +- **Raw** — a hex textarea for free-form editing of the full encoded response. +- **Struct** — per-kind structured form (for example, the Protocol Constants editor exposes the 134-byte constants schema with individual fields). The encoded output and a slice preview update as you edit. -## 10. Settings +A slice preview bar visualizes the `(offset, maxLen)` window the caller requested against the full encoded response. -Open the Settings drawer to choose active PVMs, set the stepping mode, and decide when trace-backed host calls should auto-continue. +## 9. Pending Changes + +When paused at a host call, the Pending Host Call Changes banner in the Registers panel summarizes everything that will apply on resume: register writes (`φ ← value`), gas (`Gas ← value`), and memory writes as coalesced address ranges with a short hex preview and total byte count (e.g. `[0x327a0] ← 0a 00 00 … (134B)`). + +![Pending changes banner with register and memory writes](usage-screenshots/host-call-pending-changes.png) + +Edits you make in the Host Call drawer — manual register overrides, memory edits, storage overrides, fetch mode changes — feed back into this banner so you can double-check before stepping. + +## 10. Trace Comparison + +Load a trace-backed example or trace file to compare the live execution against a reference trace. The Ecalli Trace drawer shows the Execution Trace and Reference Trace columns side by side; when a value diverges, the differing row is highlighted. + +![Ecalli Trace drawer comparing execution and reference](usage-screenshots/trace-comparison.png) + +Above the columns: + +- **Formatted** / **Raw** — switch between a decoded view and the raw trace bytes. +- **Link scroll** — when enabled, scrolling one column scrolls the other in lock-step. +- **Download** — save the current execution trace as a `.log` file. + +## 11. Settings + +Open the Settings drawer to choose active PVMs, set the stepping mode, and decide how trace-backed host calls should behave during Run. ![Settings drawer showing PVM selection, stepping mode, and host call policy](usage-screenshots/settings.png) -## 11. Multiple PVMs +- **PVM Selection** — pick one or more PVMs. Changing selection resets the debugger state. +- **Stepping Mode** — `Instruction`, `Block`, or `N-Instructions` (with an adjustable count). +- **Host Call Policy** — `Always` (auto-continue past every host call using trace data), `When Trace Matches` (default; auto-continue only when the live host call matches the reference), or `Never (Manual)` (always pause for manual review). + +## 12. Multiple PVMs + +Enable more than one PVM in Settings to compare implementations side by side. The two available interpreters today are **typeberry** (reference, from `@typeberry/lib`) and **ananas**. The PVM tabs in the top-right corner switch which machine is focused in the main panels, and a divergence badge appears when they disagree on PC, gas, status, or registers. + +![Debugger with both typeberry and ananas PVM tabs active](usage-screenshots/multiple-pvms.png) + +## 13. Persistence + +The app remembers the currently loaded program across page refreshes and restores it at its initial loaded state. Settings persist independently, so stepping mode, PVM selection, and host-call policy survive reloads. Use `Load` to return to the loader and clear the persisted program session. -Enable more than one PVM in Settings to compare implementations side by side. Use the PVM tabs in the top-right corner to switch the focused machine, and watch the divergence badge for disagreements in PC, gas, status, or registers. +![Debugger restored after a page refresh](usage-screenshots/persistence-restored.png) -![Debugger with multiple PVM tabs and a divergence indicator](usage-screenshots/multiple-pvms.png) +## Regenerating Screenshots -## 12. Persistence +The screenshots in this guide are produced by Playwright scripts in `apps/web/screenshots/`. To refresh them after UI changes, rebuild the app and rerun the capture scripts: -The app remembers the currently loaded program across page refreshes and restores it at the initial loaded state. Settings persist independently, so stepping mode and active PVM choices survive reloads too. Use `Load` to return to the loader and clear the persisted program session. +```bash +npm run build +cd apps/web && npm run screenshots +``` -![Debugger restored after a page refresh with the previous settings still visible](usage-screenshots/persistence-restored.png) +Each screenshot is one test that drives the app into the right state before calling `page.screenshot()`. Add new captures by extending an existing file in `apps/web/screenshots/` or creating a new `*.screenshot.ts` file; see the directory's README for conventions. diff --git a/docs/usage-screenshots/config-jam-builder.png b/docs/usage-screenshots/config-jam-builder.png index 7db1d80..7a36cfa 100644 Binary files a/docs/usage-screenshots/config-jam-builder.png and b/docs/usage-screenshots/config-jam-builder.png differ diff --git a/docs/usage-screenshots/config-jam-raw.png b/docs/usage-screenshots/config-jam-raw.png index 8b1c999..9b92c3f 100644 Binary files a/docs/usage-screenshots/config-jam-raw.png and b/docs/usage-screenshots/config-jam-raw.png differ diff --git a/docs/usage-screenshots/debugger-full.png b/docs/usage-screenshots/debugger-full.png index 0d91f52..1dfcd07 100644 Binary files a/docs/usage-screenshots/debugger-full.png and b/docs/usage-screenshots/debugger-full.png differ diff --git a/docs/usage-screenshots/debugger-stepping.png b/docs/usage-screenshots/debugger-stepping.png index fdb81c9..73bfec5 100644 Binary files a/docs/usage-screenshots/debugger-stepping.png and b/docs/usage-screenshots/debugger-stepping.png differ diff --git a/docs/usage-screenshots/host-call-fetch.png b/docs/usage-screenshots/host-call-fetch.png new file mode 100644 index 0000000..7349f2b Binary files /dev/null and b/docs/usage-screenshots/host-call-fetch.png differ diff --git a/docs/usage-screenshots/host-call-log.png b/docs/usage-screenshots/host-call-log.png index 8a4454c..39ce27d 100644 Binary files a/docs/usage-screenshots/host-call-log.png and b/docs/usage-screenshots/host-call-log.png differ diff --git a/docs/usage-screenshots/host-call-overview.png b/docs/usage-screenshots/host-call-overview.png index ee003c8..b038c92 100644 Binary files a/docs/usage-screenshots/host-call-overview.png and b/docs/usage-screenshots/host-call-overview.png differ diff --git a/docs/usage-screenshots/host-call-pending-changes.png b/docs/usage-screenshots/host-call-pending-changes.png new file mode 100644 index 0000000..d4d40cb Binary files /dev/null and b/docs/usage-screenshots/host-call-pending-changes.png differ diff --git a/docs/usage-screenshots/host-call-storage.png b/docs/usage-screenshots/host-call-storage.png deleted file mode 100644 index 494f18e..0000000 Binary files a/docs/usage-screenshots/host-call-storage.png and /dev/null differ diff --git a/docs/usage-screenshots/load-examples.png b/docs/usage-screenshots/load-examples.png index 58adb40..1c1dbb4 100644 Binary files a/docs/usage-screenshots/load-examples.png and b/docs/usage-screenshots/load-examples.png differ diff --git a/docs/usage-screenshots/load-manual.png b/docs/usage-screenshots/load-manual.png index a419cdb..c90fb05 100644 Binary files a/docs/usage-screenshots/load-manual.png and b/docs/usage-screenshots/load-manual.png differ diff --git a/docs/usage-screenshots/load-upload.png b/docs/usage-screenshots/load-upload.png index 7434d99..811a123 100644 Binary files a/docs/usage-screenshots/load-upload.png and b/docs/usage-screenshots/load-upload.png differ diff --git a/docs/usage-screenshots/load-url.png b/docs/usage-screenshots/load-url.png index ec2ba96..11dde43 100644 Binary files a/docs/usage-screenshots/load-url.png and b/docs/usage-screenshots/load-url.png differ diff --git a/docs/usage-screenshots/memory-panel.png b/docs/usage-screenshots/memory-panel.png index e776bb1..9606c00 100644 Binary files a/docs/usage-screenshots/memory-panel.png and b/docs/usage-screenshots/memory-panel.png differ diff --git a/docs/usage-screenshots/multiple-pvms.png b/docs/usage-screenshots/multiple-pvms.png index 30d0d43..bfc7603 100644 Binary files a/docs/usage-screenshots/multiple-pvms.png and b/docs/usage-screenshots/multiple-pvms.png differ diff --git a/docs/usage-screenshots/persistence-restored.png b/docs/usage-screenshots/persistence-restored.png index f915b50..0e9a795 100644 Binary files a/docs/usage-screenshots/persistence-restored.png and b/docs/usage-screenshots/persistence-restored.png differ diff --git a/docs/usage-screenshots/registers-edit.png b/docs/usage-screenshots/registers-edit.png index d964b14..4cdc261 100644 Binary files a/docs/usage-screenshots/registers-edit.png and b/docs/usage-screenshots/registers-edit.png differ diff --git a/docs/usage-screenshots/settings.png b/docs/usage-screenshots/settings.png index d63547c..be9ac14 100644 Binary files a/docs/usage-screenshots/settings.png and b/docs/usage-screenshots/settings.png differ diff --git a/docs/usage-screenshots/trace-comparison.png b/docs/usage-screenshots/trace-comparison.png index 9638862..17b2f33 100644 Binary files a/docs/usage-screenshots/trace-comparison.png and b/docs/usage-screenshots/trace-comparison.png differ diff --git a/spec/ui/sprint-49-register-symbol-omega-to-phi.md b/spec/ui/sprint-49-register-symbol-omega-to-phi.md index 20f0f20..e12f2ee 100644 --- a/spec/ui/sprint-49-register-symbol-omega-to-phi.md +++ b/spec/ui/sprint-49-register-symbol-omega-to-phi.md @@ -63,6 +63,22 @@ Unicode escape in detail line changed from `\u03C9` (ω) to `\u03C6` (φ) for re Log host call register comment updated (φ8=target_ptr, φ9=target_len, etc.). +### 9. Unicode Escape Form (added in sprint 50 follow-up) + +Two files stored the symbol as the Unicode escape `\u03C9` rather than the +literal `ω` character, so the original verification grep missed them. Both +were found while refreshing the usage-guide screenshots in sprint 50 and +updated to `\u03C6`: + +- `apps/web/src/components/debugger/PendingChanges.tsx` — pending register + write prefix (`{"\u03C9"}` → `{"\u03C6"}`). +- `apps/web/src/components/load/DetectionSummary.tsx` — registers-preview + template literal (`\u03C9${i}=…` → `\u03C6${i}=…`). + +If you re-implement this rename from scratch, remember to search for BOTH +representations: the literal character and the `\u03C9` escape sequence. +The verification section below has been updated accordingly. + ## Files Changed | File | Changes | @@ -90,6 +106,8 @@ Log host call register comment updated (φ8=target_ptr, φ9=target_len, etc.). | `apps/web/e2e/sprint-04-registers.spec.ts` | E2E label assertions match φ | | `apps/web/e2e/sprint-28-asm-raw-popover.spec.ts` | E2E ASM mode assertions match φ | | `apps/web/e2e/integration-smoke.spec.ts` | Comment uses φ7 | +| `apps/web/src/components/debugger/PendingChanges.tsx` | (sprint 50 follow-up) `\u03C9` escape → `\u03C6` | +| `apps/web/src/components/load/DetectionSummary.tsx` | (sprint 50 follow-up) `\u03C9` escape → `\u03C6` | ## Acceptance Criteria @@ -98,7 +116,9 @@ Log host call register comment updated (φ8=target_ptr, φ9=target_len, etc.). - Trace display uses `φ7` in host call headers and `φN` in register writes. - Divergence detail strings use `φN:` prefix. - Host call output preview shows `φ₇ ←`. -- No remaining references to ω in `.ts` or `.tsx` source or test files. +- **No remaining references to ω in source or test files, in ANY form:** + the literal character (`ω`), the Unicode code-point escape (`\u03C9`), + the hex-code phrase (`U+03C9`), or the word "omega". - All 683 unit tests pass. - The spec/ directory documentation files are not updated (they are historical records of prior sprints). @@ -107,6 +127,14 @@ Log host call register comment updated (φ8=target_ptr, φ9=target_len, etc.). ```bash npm run build npm test -# Confirm no ω remains in source/test files: -grep -r 'ω' --include='*.ts' --include='*.tsx' apps/ packages/ +# Confirm no ω remains in source/test files — check BOTH the literal char +# and the Unicode-escape form, since both appear in the code base: +grep -r -E '(ω|\\u03C9|\\u03c9|U\+03C9|omega)' \ + --include='*.ts' --include='*.tsx' apps/ packages/ ``` + +The original sprint-49 verification only grepped for the literal `ω` +character, which missed `PendingChanges.tsx` and `DetectionSummary.tsx` — +both stored the symbol as `\u03C9`. Those files were fixed in the sprint-50 +follow-up. If you re-run this rename for any future notation change, use +the broader regex above. diff --git a/spec/ui/sprint-50-usage-guide-refresh.md b/spec/ui/sprint-50-usage-guide-refresh.md new file mode 100644 index 0000000..3bc1902 --- /dev/null +++ b/spec/ui/sprint-50-usage-guide-refresh.md @@ -0,0 +1,143 @@ +# Sprint 50 — Usage Guide Refresh and Automated Screenshot Pipeline + +Status: Implemented + +## Goal + +Refresh `docs/usage-guide.md` so it matches the current UI (sprints 38–49), replace every stale screenshot with a new dark-mode capture, and add a reproducible Playwright pipeline so screenshots can be regenerated in the future without hand-authoring them. + +The previous guide was written in sprint 37 and had drifted substantially — the entire Host Call drawer was rewritten in sprint 42, fetch handling was added in sprints 43/44, host-call editing and the pending-changes banner shipped in sprint 41, and ananas PVM arrived in sprint 39. None of that was documented. + +## Prior Sprint Dependencies + +- Sprint 37 — initial usage guide and screenshot set (this sprint replaces its artifacts) +- Sprint 39 — ananas PVM +- Sprint 40 — drawer polish, trace link-scroll, hex-labeled memory pages +- Sprint 41 — host-call editing, pending-changes banner +- Sprint 42 — host-call UX redesign (two-column layout, NONE toggle, auto-apply, sticky bar) +- Sprint 43 — fetch host call handler with all 16 kinds and three modes +- Sprint 44 — eager default blob for Constants fetch, `Use Trace Data` behavior +- Sprint 47 — error boundary +- Sprint 49 — register symbol ω → φ (partial; see Known Gaps below) + +## What Works After This Sprint + +### Documentation + +1. **`docs/usage-guide.md` rewritten** to cover the current UI: + - Section 4 (Debugger Screen) describes the new drawer tab-bar styling and mentions the error boundary. + - Section 6 (Registers) documents the inline editor (click the hex value) and the pending-changes banner. + - Section 8 (Host Calls) is largely new: two-column layout, sidebar contents (badge, input registers, output preview, memory write count), handler-specific editors (fetch/storage/log/generic-text), sticky bar with NONE toggle and Use Trace Data button, and the auto-apply flow. + - A new subsection **Fetch Handler** describes Trace/Raw/Struct modes, per-kind editors, encoded output, and the slice preview bar. + - A new section **9. Pending Changes** describes the `φ ← value` banner with coalesced memory writes. + - Section 10 (Trace Comparison) mentions link-scroll and download. + - Section 11 (Settings) enumerates PVM selection, stepping modes, and the host-call policy options. + - Section 12 (Multiple PVMs) names typeberry and ananas explicitly. + - Register notation is φ throughout prose (matching sprint 49 rename in most UI locations). + - A new trailing section explains how to regenerate screenshots. + +2. **Every screenshot reference resolves.** The sprint-37 verification grep passes with no `MISSING:` lines. + +### Screenshot Pipeline + +3. **New Playwright pipeline** in `apps/web/screenshots/`: + - `helpers.ts` — shared fixture that forces dark mode via `localStorage.theme-mode = "dark"` injected with `addInitScript`, a `waitForFontsReady` helper, a `capture(target, name)` wrapper that writes into `docs/usage-screenshots/`, and host-call navigation helpers (`currentPc`, `setAutoContinueNever`, `runToHostCall`, `advanceToNextHostCall`) with inline comments documenting the `Next`-vs-`Run` and PC-diff gotchas. + - `01-load.screenshot.ts` — loaders (examples/upload/URL/manual hex). + - `02-config.screenshot.ts` — JAM SPI builder + RAW modes. + - `03-debugger.screenshot.ts` — full layout, stepping, register editing, memory panel. + - `04-settings.screenshot.ts` — settings tab, multi-PVM (typeberry + ananas). + - `05-persistence.screenshot.ts` — persistence after reload. + - `06-host-calls.screenshot.ts` — host-call overview, log, pending-changes banner. + - `07-fetch-trace.screenshot.ts` — fetch handler and trace comparison. + - `README.md` — conventions for adding new captures, plus a "Gotchas" section covering the host-call navigation traps. + +4. **Dedicated Playwright config** `apps/web/playwright.screenshots.config.ts`: + - `testDir: "./screenshots"`, `testMatch: /.*\.screenshot\.ts$/`. + - Fixed viewport 1440×900 at `deviceScaleFactor: 2`. + - `colorScheme: "dark"` plus the localStorage injection. + - Auto-starts `vite preview --port 4199` as its `webServer`. + - `workers: 1`, `fullyParallel: false` — captures are serial so PVM state is deterministic per file. + +5. **`npm run screenshots`** (in `apps/web/package.json`) runs the pipeline. Full capture takes under 30 s on a warm build. + +### Fixture choices and gotchas (captured for reproducibility) + +6. **io-trace** example (bundled trace file) is used for: + - `host-call-overview.png` — first host call is `fetch`, renders the fetch editor out of the box. + - `host-call-pending-changes.png` — same first host call produces a pending `φ7 ← ...` register write and a memory write. + - `host-call-fetch.png` — struct mode selected on the Constants (kind 0) encoding. + - `trace-comparison.png` — after one Run and one advance-to-next-host-call, both columns have entries. + +7. **all-ecalli-accumulate** (JAM SPI) is used for `host-call-log.png`: the program emits a log (ecalli=100) within the first few host calls, which renders the `LogHostCall` view with decoded text. + +8. **`Next` vs `Run` for advancing between host-call pauses.** `Next` is one instruction with the default stepping mode, which typically does not land on the next host call. The capture helpers use `run-button` with `auto-continue-radio-never`, which pauses on every host call and makes "advance to next call" a single Run click. Detection uses the PC value (`pc-value` testid) — the `host-call-header` badge alone repeats for same-kind calls. This is encapsulated in `advanceToNextHostCall(page)` in `helpers.ts`; new captures should use that rather than rolling their own. + +9. **React error #185 ("Maximum update depth exceeded") can still be triggered** by rapid successive Run-to-next-host-call cycles on some trace examples, despite sprint 47's targeted fix. The capture specs work around this by keeping the number of Run iterations small per test and by adding a short settle delay before each Run click. If the error boundary ever shows during capture, the offending screenshot file will still be produced but contain the fallback UI — rerunning usually recovers. + +### Screenshots retired + +10. **`host-call-storage.png` is removed from the guide.** The redesigned layout is adequately conveyed by `host-call-overview.png` (fetch) and `host-call-log.png` (log); a separate storage shot would be redundant and was brittle to capture (see gotcha 9). Storage host-call behavior is described in prose in section 8. + +### Sprint 49 Follow-up Fix + +11. **Completed sprint-49's ω → φ rename.** Two files stored the symbol as + the Unicode escape `\u03C9` rather than the literal `ω` character, so + sprint-49's verification grep (`grep -r 'ω'`) missed them: + - `apps/web/src/components/debugger/PendingChanges.tsx` — pending + register-write prefix. + - `apps/web/src/components/load/DetectionSummary.tsx` — registers-preview + line in the Program Summary. + + Both were updated to `\u03C6` and the sprint-49 spec's verification + regex was broadened to catch `ω`, `\u03C9`, `U+03C9`, and `omega` so + future rename sprints don't repeat the miss. + +## Known Gaps (not addressed in this sprint) + +- **React error #185 root cause.** Sprint 47 suppressed host-call state while the run loop is executing, which fixed the common trace-replay crash. Manual rapid Run clicks with `auto-continue=never` can still trip the infinite-render cycle on some examples; a deeper investigation into the pending-changes / resume interaction is warranted. + +## Files Changed + +| File | Changes | +|------|---------| +| `docs/usage-guide.md` | Rewritten to match current UI. New Host Calls section, new Pending Changes section, new Fetch Handler subsection, regeneration instructions. | +| `docs/usage-screenshots/*.png` | All 18 screenshots regenerated in dark mode at 1440×900@2x. `host-call-storage.png` removed. | +| `apps/web/playwright.screenshots.config.ts` | **New.** Dedicated Playwright config for screenshot captures. | +| `apps/web/screenshots/helpers.ts` | **New.** Shared fixture, dark-mode localStorage injection, `capture()` helper. | +| `apps/web/screenshots/01-load.screenshot.ts` | **New.** Loader captures. | +| `apps/web/screenshots/02-config.screenshot.ts` | **New.** JAM SPI builder/raw captures. | +| `apps/web/screenshots/03-debugger.screenshot.ts` | **New.** Debugger layout, stepping, registers, memory captures. | +| `apps/web/screenshots/04-settings.screenshot.ts` | **New.** Settings and multi-PVM captures. | +| `apps/web/screenshots/05-persistence.screenshot.ts` | **New.** Persistence-after-reload capture. | +| `apps/web/screenshots/06-host-calls.screenshot.ts` | **New.** Host-call overview, log, pending-changes captures. | +| `apps/web/screenshots/07-fetch-trace.screenshot.ts` | **New.** Fetch handler and trace-comparison captures. | +| `apps/web/screenshots/README.md` | **New.** Conventions for adding new captures. | +| `apps/web/package.json` | Added `"screenshots"` script. | +| `apps/web/src/components/debugger/PendingChanges.tsx` | Sprint-49 follow-up: `\u03C9` → `\u03C6`. | +| `apps/web/src/components/load/DetectionSummary.tsx` | Sprint-49 follow-up: `\u03C9` → `\u03C6`. | +| `spec/ui/sprint-49-register-symbol-omega-to-phi.md` | Added missed files, broadened verification regex, noted the follow-up. | + +## Acceptance Criteria + +- `docs/usage-guide.md` covers the full debugger workflow, including the sprint-38-through-sprint-49 features listed above. +- Every screenshot referenced from the guide exists at the referenced path. +- Running `cd apps/web && npm run screenshots` regenerates all screenshots from scratch with 18/18 tests passing. +- Captured PNGs are produced in dark mode at 1440×900 with deviceScaleFactor 2. +- The guide explains how to regenerate screenshots after further UI changes. + +## Verification + +```bash +# Build + full capture run (end-to-end). +npm run build +cd apps/web && npm run screenshots + +# Verify every referenced screenshot exists. +grep -oE 'usage-screenshots/[^)]+' ../../docs/usage-guide.md \ + | sort -u \ + | while read f; do + [ -f "../../docs/$f" ] || echo "MISSING: docs/$f" + done +``` + +Both commands should complete with no errors and no `MISSING:` lines.