docs: refresh usage guide and add Playwright screenshot pipeline#12
docs: refresh usage guide and add Playwright screenshot pipeline#12
Conversation
Rewrite docs/usage-guide.md to match the current UI (sprints 38-49): redesigned host-call drawer, fetch handler, pending-changes banner, Ananas PVM, error boundary, and link-scroll. Every screenshot regenerated in dark mode. Add apps/web/screenshots/ with a dedicated Playwright config so the screenshot set can be regenerated via `npm run screenshots` after future UI changes. Shared helpers cover the non-obvious traps (Next vs Run, PC-diff for pause advancement). Also complete sprint-49's ω → φ rename in PendingChanges.tsx and DetectionSummary.tsx (both stored the symbol as \u03C9, which the original verification grep missed) and broaden sprint-49's verification regex so future rename sprints don't repeat the miss. Session captured in spec/ui/sprint-50-usage-guide-refresh.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-push hook caught two issues: - helpers.ts: collapse `a && a.b` to `a?.b`. - 03-debugger.screenshot.ts: import sort order and line length. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for pvm-debugger ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis PR introduces a comprehensive Playwright-based screenshot capture system for the web app, enabling automated visual testing and documentation generation. It adds dedicated test files for load, config, debugger, settings, persistence, host-calls, and fetch-trace features, shared test utilities, a dedicated Playwright configuration, an npm script, updated usage documentation, and minor UI symbol updates from omega to phi. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
apps/web/screenshots/06-host-calls.screenshot.ts (1)
22-28: Simplify the single-value trace loader helper.
loadTraceExampleis currently parameterized but only accepts"io-trace". A dedicated helper name makes intent clearer and avoids a needless argument.♻️ Proposed cleanup
-async function loadTraceExample(page: Page, id: "io-trace") { +async function loadIoTrace(page: Page) { await page.goto("/#/load"); - await page.getByTestId(`example-card-${id}`).click(); + await page.getByTestId("example-card-io-trace").click(); await expect(page.getByTestId("debugger-page")).toBeVisible({ timeout: 15000, }); } @@ - await loadTraceExample(page, "io-trace"); + await loadIoTrace(page); @@ - await loadTraceExample(page, "io-trace"); + await loadIoTrace(page);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/screenshots/06-host-calls.screenshot.ts` around lines 22 - 28, The helper loadTraceExample is parameterized but only ever called with the literal "io-trace"; replace it with a dedicated, no-argument helper (e.g., loadIoTraceExample) that removes the id parameter and hardcodes the test id string, update its implementation to navigate to "/#/load", click page.getByTestId("example-card-io-trace"), and wait for page.getByTestId("debugger-page") to be visible with the same timeout; also update any call sites that invoke loadTraceExample("io-trace") to call the new loadIoTraceExample instead.apps/web/screenshots/03-debugger.screenshot.ts (1)
5-11: Narrow example id type for safer fixture selection.Typing
idasstringallows silent typos in test ids. A narrow union keeps this helper self-validating.🔧 Suggested type tightening
-async function loadGeneric(page: Page, id: string) { +type GenericExampleId = "fibonacci" | "game-of-life"; + +async function loadGeneric(page: Page, id: GenericExampleId) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/screenshots/03-debugger.screenshot.ts` around lines 5 - 11, The helper loadGeneric currently accepts id: string which permits silent typos when selecting test fixtures; change the id parameter to a narrowed union type (or an exported type alias, e.g., ExampleId = 'exampleA' | 'exampleB' | ...) listing the valid example ids used in your tests, update the function signature of loadGeneric(page: Page, id: ExampleId) and any call sites to use those exact constants, and keep the existing selectors (example-card-${id} and debugger-page) unchanged so the test only accepts known valid ids.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/screenshots/01-load.screenshot.ts`:
- Around line 45-47: The hex payload string passed into
.getByTestId("manual-input-field").fill(...) is hardcoded; move that payload
into a fixtures file (e.g., fixtures/manual-hex-payload.hex), update the test to
read the fixture (via your test fixture helper or a simple fs.readFileSync/await
readFile) and pass the file contents to .fill, trimming any trailing newline;
replace the literal string in the .fill call with the variable holding the
fixture content so the test complies with the repo's test-data rule and is
reusable.
In `@apps/web/screenshots/04-settings.screenshot.ts`:
- Around line 29-31: After clicking the switch when isChecked is false,
explicitly assert the control is now checked to avoid flakiness: after await
ananasSwitch.click() (in the code that references isChecked and ananasSwitch)
add an await that verifies the switch state (e.g., await
ananasSwitch.isChecked() asserted to true or using your test framework's
expect(await ananasSwitch.isChecked()).toBe(true)) before proceeding to close
the drawer so the enabled state is deterministic.
In `@apps/web/screenshots/07-fetch-trace.screenshot.ts`:
- Around line 61-63: The test ignores the boolean returned by
advanceToNextHostCall which can be false and cause the trace capture to be
out-of-sync; update the flow to capture its return value (e.g., const advanced =
await advanceToNextHostCall(page)) and assert it before proceeding (use an
expect or throw if advanced is false) so the subsequent click on
page.getByTestId("drawer-tab-ecalli_trace") and
expect(page.getByTestId("ecalli-trace-tab")).toBeVisible() only run when
advancement succeeded.
In `@apps/web/screenshots/helpers.ts`:
- Around line 79-85: The type guard in capture() is unsafe because both Page and
Locator have screenshot()/evaluate(); change the discriminator to check for
"goto" in target (since goto exists only on Page) so the Page branch is only
taken for actual Page instances; then call waitForFontsReady(page) and
page.screenshot({ path, fullPage: false, animations: "disabled" }) inside the
"goto" branch, and call (target as Locator).screenshot({ path, animations:
"disabled" }) in the else branch to avoid passing fullPage to Locator; update
any casts and references to Page and Locator accordingly (capture, Page,
Locator, waitForFontsReady).
In `@docs/usage-guide.md`:
- Around line 114-123: The fenced command block containing the example commands
(setreg φ7 <- 0x2a, memwrite 0x100 len=4 <- 0xdeadbeef, setgas <- 500000) is
missing a language tag and triggers MD040; update the triple-backtick fence to
include a language identifier (e.g., ```text) so the block is explicitly marked
as plaintext. Locate the block around the commands in docs/usage-guide.md and
change the opening fence to include the tag without altering the example lines
or semantics.
---
Nitpick comments:
In `@apps/web/screenshots/03-debugger.screenshot.ts`:
- Around line 5-11: The helper loadGeneric currently accepts id: string which
permits silent typos when selecting test fixtures; change the id parameter to a
narrowed union type (or an exported type alias, e.g., ExampleId = 'exampleA' |
'exampleB' | ...) listing the valid example ids used in your tests, update the
function signature of loadGeneric(page: Page, id: ExampleId) and any call sites
to use those exact constants, and keep the existing selectors
(example-card-${id} and debugger-page) unchanged so the test only accepts known
valid ids.
In `@apps/web/screenshots/06-host-calls.screenshot.ts`:
- Around line 22-28: The helper loadTraceExample is parameterized but only ever
called with the literal "io-trace"; replace it with a dedicated, no-argument
helper (e.g., loadIoTraceExample) that removes the id parameter and hardcodes
the test id string, update its implementation to navigate to "/#/load", click
page.getByTestId("example-card-io-trace"), and wait for
page.getByTestId("debugger-page") to be visible with the same timeout; also
update any call sites that invoke loadTraceExample("io-trace") to call the new
loadIoTraceExample instead.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f482a662-bb84-4b60-b570-67279a957657
⛔ Files ignored due to path filters (19)
docs/usage-screenshots/config-jam-builder.pngis excluded by!**/*.pngdocs/usage-screenshots/config-jam-raw.pngis excluded by!**/*.pngdocs/usage-screenshots/debugger-full.pngis excluded by!**/*.pngdocs/usage-screenshots/debugger-stepping.pngis excluded by!**/*.pngdocs/usage-screenshots/host-call-fetch.pngis excluded by!**/*.pngdocs/usage-screenshots/host-call-log.pngis excluded by!**/*.pngdocs/usage-screenshots/host-call-overview.pngis excluded by!**/*.pngdocs/usage-screenshots/host-call-pending-changes.pngis excluded by!**/*.pngdocs/usage-screenshots/host-call-storage.pngis excluded by!**/*.pngdocs/usage-screenshots/load-examples.pngis excluded by!**/*.pngdocs/usage-screenshots/load-manual.pngis excluded by!**/*.pngdocs/usage-screenshots/load-upload.pngis excluded by!**/*.pngdocs/usage-screenshots/load-url.pngis excluded by!**/*.pngdocs/usage-screenshots/memory-panel.pngis excluded by!**/*.pngdocs/usage-screenshots/multiple-pvms.pngis excluded by!**/*.pngdocs/usage-screenshots/persistence-restored.pngis excluded by!**/*.pngdocs/usage-screenshots/registers-edit.pngis excluded by!**/*.pngdocs/usage-screenshots/settings.pngis excluded by!**/*.pngdocs/usage-screenshots/trace-comparison.pngis excluded by!**/*.png
📒 Files selected for processing (16)
apps/web/package.jsonapps/web/playwright.screenshots.config.tsapps/web/screenshots/01-load.screenshot.tsapps/web/screenshots/02-config.screenshot.tsapps/web/screenshots/03-debugger.screenshot.tsapps/web/screenshots/04-settings.screenshot.tsapps/web/screenshots/05-persistence.screenshot.tsapps/web/screenshots/06-host-calls.screenshot.tsapps/web/screenshots/07-fetch-trace.screenshot.tsapps/web/screenshots/README.mdapps/web/screenshots/helpers.tsapps/web/src/components/debugger/PendingChanges.tsxapps/web/src/components/load/DetectionSummary.tsxdocs/usage-guide.mdspec/ui/sprint-49-register-symbol-omega-to-phi.mdspec/ui/sprint-50-usage-guide-refresh.md
| .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. |
There was a problem hiding this comment.
Move embedded manual hex payload to a fixture file.
Line 46 hardcodes a hex payload in source, which breaks the repo’s test-data rule and makes this scenario harder to maintain/reuse.
Proposed fix
+import { readFile } from "node:fs/promises";
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");
+const MANUAL_HEX_FIXTURE = path.join(FIXTURES, "generic", "load-manual.hex");
@@
await page
.getByTestId("manual-input-field")
- .fill("0x00 03 00 01 00 0d 00 08 00 02 00 07 00 01");
+ .fill((await readFile(MANUAL_HEX_FIXTURE, "utf8")).trim());As per coding guidelines, Use fixtures/ directory for example programs and test data; do NOT embed hex strings in source code.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .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. | |
| import { readFile } from "node:fs/promises"; | |
| 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"); | |
| const MANUAL_HEX_FIXTURE = path.join(FIXTURES, "generic", "load-manual.hex"); | |
| const hexPayload = (await readFile(MANUAL_HEX_FIXTURE, "utf8")).trim(); | |
| await page | |
| .getByTestId("manual-input-field") | |
| .fill(hexPayload); | |
| // Blur so the byte count appears. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/screenshots/01-load.screenshot.ts` around lines 45 - 47, The hex
payload string passed into .getByTestId("manual-input-field").fill(...) is
hardcoded; move that payload into a fixtures file (e.g.,
fixtures/manual-hex-payload.hex), update the test to read the fixture (via your
test fixture helper or a simple fs.readFileSync/await readFile) and pass the
file contents to .fill, trimming any trailing newline; replace the literal
string in the .fill call with the variable holding the fixture content so the
test complies with the repo's test-data rule and is reusable.
| if (!isChecked) { | ||
| await ananasSwitch.click(); | ||
| } |
There was a problem hiding this comment.
Assert toggle state after enabling ananas to reduce flakiness.
After clicking the switch, assert it is checked before closing the drawer; this makes capture state deterministic.
✅ Suggested reliability fix
if (!isChecked) {
await ananasSwitch.click();
+ await expect(ananasSwitch).toBeChecked();
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!isChecked) { | |
| await ananasSwitch.click(); | |
| } | |
| if (!isChecked) { | |
| await ananasSwitch.click(); | |
| await expect(ananasSwitch).toBeChecked(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/screenshots/04-settings.screenshot.ts` around lines 29 - 31, After
clicking the switch when isChecked is false, explicitly assert the control is
now checked to avoid flakiness: after await ananasSwitch.click() (in the code
that references isChecked and ananasSwitch) add an await that verifies the
switch state (e.g., await ananasSwitch.isChecked() asserted to true or using
your test framework's expect(await ananasSwitch.isChecked()).toBe(true)) before
proceeding to close the drawer so the enabled state is deterministic.
| await advanceToNextHostCall(page); | ||
| await page.getByTestId("drawer-tab-ecalli_trace").click(); | ||
| await expect(page.getByTestId("ecalli-trace-tab")).toBeVisible(); |
There was a problem hiding this comment.
Assert host-call advancement before capturing trace comparison.
The boolean result from advanceToNextHostCall is currently ignored. If it returns false, the capture can drift from the intended scenario.
🧪 Suggested guard
- await advanceToNextHostCall(page);
+ const advanced = await advanceToNextHostCall(page);
+ await expect(advanced).toBe(true);
await page.getByTestId("drawer-tab-ecalli_trace").click();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await advanceToNextHostCall(page); | |
| await page.getByTestId("drawer-tab-ecalli_trace").click(); | |
| await expect(page.getByTestId("ecalli-trace-tab")).toBeVisible(); | |
| const advanced = await advanceToNextHostCall(page); | |
| await expect(advanced).toBe(true); | |
| await page.getByTestId("drawer-tab-ecalli_trace").click(); | |
| await expect(page.getByTestId("ecalli-trace-tab")).toBeVisible(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/screenshots/07-fetch-trace.screenshot.ts` around lines 61 - 63, The
test ignores the boolean returned by advanceToNextHostCall which can be false
and cause the trace capture to be out-of-sync; update the flow to capture its
return value (e.g., const advanced = await advanceToNextHostCall(page)) and
assert it before proceeding (use an expect or throw if advanced is false) so the
subsequent click on page.getByTestId("drawer-tab-ecalli_trace") and
expect(page.getByTestId("ecalli-trace-tab")).toBeVisible() only run when
advancement succeeded.
| 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" }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Playwright v1.50, does Locator expose evaluate(), and does Locator.screenshot support a fullPage option?
💡 Result:
In Playwright v1.50, Locator exposes an evaluate method, but Locator.screenshot does not support a fullPage option. Locator.evaluate is listed in the official API documentation among the methods available on the Locator class. This has been a standard method since earlier versions and remains in v1.50, as confirmed by the release notes which do not indicate its removal. Locator.screenshot captures a screenshot of the specific element or bounding box represented by the locator. The fullPage option is exclusively for Page.screenshot, which captures the full scrollable page. Official screenshots documentation explicitly shows page.screenshot({ fullPage: true }) for full page captures and locator.screenshot for element screenshots without mentioning fullPage for locators. Older API docs (e.g., v1.22) and recent issues confirm Locator.ScreenshotOptions lacks fullPage, and v1.50 release notes introduce no such change.
Citations:
- 1: https://playwright.dev/docs/api/class-locator
- 2: https://playwright.dev/docs/next/api/class-locator
- 3: https://playwright.dev/docs/screenshots
- 4: https://playwright.dev/docs/next/screenshots
- 5: https://playwright.dev/docs/release-notes
- 6: https://github.com/microsoft/playwright/releases/tag/v1.50.0
- 7: https://javadoc.io/static/com.microsoft.playwright/playwright/1.22.0/com/microsoft/playwright/Locator.ScreenshotOptions.html
Fix Page/Locator type guard in capture() (currently ambiguous).
Both Page and Locator expose screenshot() and evaluate(), so the discriminator is unsafe. If a Locator is misclassified as Page and routed through the first branch, it will receive the fullPage option, which Locator.screenshot() does not support, causing a failure.
Use "goto" in target instead, since goto() exists only on Page:
Proposed fix
export async function capture(
target: Page | Locator,
name: string,
): Promise<void> {
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" });
+ if ("goto" in target) {
+ await waitForFontsReady(target);
+ await target.screenshot({ path, fullPage: false, animations: "disabled" });
} else {
- await (target as Locator).screenshot({ path, animations: "disabled" });
+ await target.screenshot({ path, animations: "disabled" });
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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" }); | |
| } | |
| if ("goto" in target) { | |
| await waitForFontsReady(target); | |
| await target.screenshot({ path, fullPage: false, animations: "disabled" }); | |
| } else { | |
| await target.screenshot({ path, animations: "disabled" }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/screenshots/helpers.ts` around lines 79 - 85, The type guard in
capture() is unsafe because both Page and Locator have screenshot()/evaluate();
change the discriminator to check for "goto" in target (since goto exists only
on Page) so the Page branch is only taken for actual Page instances; then call
waitForFontsReady(page) and page.screenshot({ path, fullPage: false, animations:
"disabled" }) inside the "goto" branch, and call (target as
Locator).screenshot({ path, animations: "disabled" }) in the else branch to
avoid passing fullPage to Locator; update any casts and references to Page and
Locator accordingly (capture, Page, Locator, waitForFontsReady).
| ``` | ||
| # Return value in φ7 | ||
| setreg φ7 <- 0x2a | ||
|
|
||
| # Write 4 bytes to memory | ||
| memwrite 0x100 len=4 <- 0xdeadbeef | ||
|
|
||
|  | ||
| # Set gas after the call | ||
| setgas <- 500000 | ||
| ``` |
There was a problem hiding this comment.
Add a language tag to the fenced command block.
The fenced block in this section is missing a language identifier (MD040), which can fail lint/docs checks.
📝 Minimal fix
- ```
+ ```text
# Return value in φ7
setreg φ7 <- 0x2a
@@
# Set gas after the call
setgas <- 500000</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 114-114: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/usage-guide.md` around lines 114 - 123, The fenced command block
containing the example commands (setreg φ7 <- 0x2a, memwrite 0x100 len=4 <-
0xdeadbeef, setgas <- 500000) is missing a language tag and triggers MD040;
update the triple-backtick fence to include a language identifier (e.g.,
```text) so the block is explicitly marked as plaintext. Locate the block around
the commands in docs/usage-guide.md and change the opening fence to include the
tag without altering the example lines or semantics.
Summary
docs/usage-guide.mdto match the current UI (sprints 38–49): redesigned host-call drawer with two-column layout, fetch handler with three modes, pending-changes banner, generic text editor DSL, Ananas PVM, error boundary recovery, and trace link-scroll. Every screenshot regenerated in dark mode.apps/web/screenshots/— a Playwright-based capture pipeline so the screenshot set can be refreshed vianpm run screenshotsafter future UI changes. Shared helpers document the non-obvious traps (NextvsRunfor host-call advancement, PC-diff for pause detection).PendingChanges.tsxandDetectionSummary.tsx— both stored the symbol as\u03C9, which sprint-49's literal-character grep missed. Sprint-49's verification regex is broadened to catch both forms so the next rename sprint doesn't repeat the miss.spec/ui/sprint-50-usage-guide-refresh.md.Test plan
npm test— all 683 unit tests passnpm run build— builds cleancd apps/web && npm run screenshots— 18/18 captures pass in ~25sdocs/usage-guide.mdresolves to an existing filegrep -rE '(ω|\\u03C9|\\u03c9|U\+03C9|omega)' --include='*.ts' --include='*.tsx' apps/ packages/returns nothingdocs/usage-guide.mdin a preview (VS CodeCmd+Shift+Vornpx markserv docs/usage-guide.md) to verify screenshots render correctlycd apps/web && npm run screenshotsto confirm the pipeline reproduces the committed PNGsNotes
host-call-storage.png) was retired: the redesigned layout is adequately shown byhost-call-overview(fetch) andhost-call-log(log), and driving the app to a storage pause was brittle enough to repeatedly trip a residual React error #185 render loop.auto-continue=nevercan still hit it. Out of scope here.🤖 Generated with Claude Code