Skip to content

Release: UI 2.11.10 + Solid 2.0.0-rc.4, capture viewport, and runtime script fetching - #30

Closed
pathscale wants to merge 7 commits into
masterfrom
release/corpus-fixes
Closed

Release: UI 2.11.10 + Solid 2.0.0-rc.4, capture viewport, and runtime script fetching#30
pathscale wants to merge 7 commits into
masterfrom
release/corpus-fixes

Conversation

@pathscale

@pathscale pathscale commented Aug 31, 2026

Copy link
Copy Markdown
Owner

This is the one to merge. Six commits, everything stacked: it now includes #27's frontend work as well as the corpus fixes. Supersedes #27, #28 and #29.

12ca80d fix(browser): fetch runtime-discovered scripts in the window too
5f3ea4f fix(capture): fetch the scripts a page discovers while running
34fce86 fix(capture): honour the capture viewport environment variables
d55e84c chore(deps): take @pathscale/ui 2.11.10
6c7cdbd docs(theme): drop the stale daisyUI reference from the layer comment
3d50ae0 chore(deps): move the frontend to @pathscale/ui 2.11.9 and Solid 2.0.0-rc.4

The bottom three are #27, unchanged and rebased forward. The top three are new, found by capturing a 104-site corpus headlessly and comparing against a reference browser. Sites are referred to by opaque id; the id-to-host map is local and not committed.

Capture viewport

scripts/render-check.sh has documented and exported CHUZZ_CAPTURE_WIDTH and CHUZZ_CAPTURE_HEIGHT since it was written, and the capture entry point read neither — it passed a literal 1440, 960.

Found because a re-capture at width 1507 produced a byte-identical tree dump to the 1440 run, which is not something a working viewport knob can do. It matters beyond tidiness: two engines laying the same page out at different widths disagree on every percentage width, centred box and responsive breakpoint, so an unhonoured viewport turns a rendering diff into noise that reads exactly like a rendering fault.

Scripts a page discovers while running

The prefetch walks the parsed HTML and loads every script it names. A page that builds a <script> from JavaScript asks for a URL nobody knew about until the page was running, and that request reached DefaultScriptFetcher, which serves file: and data: only. The script was dropped with unsupported URL scheme for script: https — reading like a policy decision rather than the missing capability it is.

The most common engine defect in the corpus: 26 of 104 sites, a quarter.

It hides well. Scripts the parser found load perfectly, so a page fails only in the parts it assembles itself, and jQuery/$ coming out undefined on 7 and 6 sites looks like its own bug until you notice the library was never fetched.

One commit fixes the capture path; the next fixes the window and moves the fetcher into script_fetch.rs so both share it rather than keeping two copies in step.

The deadlines differ per caller, deliberately

ScriptFetcher::fetch is synchronous, because classic scripts execute in document order. In the window, page scripts run on the UI thread, so this blocks everything — other tabs included — for as long as it waits. Five seconds there, ten in the capture: a capture is unattended and a dropped script costs it the fidelity it exists to provide, while a window has someone watching the frame.

Neither number is the real answer. The real answer is an asynchronous script-loading path in blitz-script, which would not have to choose. That is an engine change, not this one. This is a real tradeoff against the previous behaviour of silently dropping the script, and worth a second opinion before tagging.

Verification

Re-captured the six worst-affected sites: dropped scripts 12, 6, 4, 2, 2, 2 → 0 on every one, and still 0 after the refactor.

Rendering barely moved, which is the honest result rather than a disappointing one. On one site the error count rose 9 → 14 with a new kind, unescape is not defined: the scripts now run and reach the next missing global. This unblocks a layer; the missing web APIs behind it — fetch (4 sites), XMLHttpRequest (6), Image (4), getComputedStyle (3) — are what turn it into pixels.

The test serves a script from a real socket and asserts a synchronous fetch completes a round trip from inside the runtime driving the page. Its accept loop carries a deadline rather than blocking: restoring the defect means no connection is ever made, and a blocking accept hung the run instead of failing it — which it did, until the test was fixed. Confirmed by restoring the defect: fails in 10.02s.

Green on the stacked branch: cargo fmt --check, cargo clippy -D warnings, 34 Rust tests, and the frontend typecheck including the local @chuzz/ui layouts regeneration.

Not verified

The window path is the same code the tests cover and the capture exercises, but it is not itself exercised in a window: this machine has no screen access, and the control socket sees the chrome document rather than the page's sub-document. Worth a human look before tagging.

meh added 3 commits August 31, 2026 09:42
…0-rc.4

local-ui/package.json still asked for @pathscale/ui ^2.5.0, solid-js ^1.9.5 and
solid-layouts ^0.1.3, from before the chrome was ported. Those are the peer
ranges of a workspace member, so bun resolved them for the whole workspace and
installed solid-js 1.9.14 and @pathscale/ui 2.5.0 -- the app has been building
against Solid 1 while its own package.json asked for 2.0.0-rc.0. Widening the
member's ranges is what makes the app's pins take effect.

Also adds the @iconify-json sets. index.css @sources @pathscale/ui, which uses
mdi-- tokens internally, and nothing installed them, so those icons resolved to
nothing.

babel-preset-solid moves to rc.2 behind a caret; the exact rc.0 pin could not
take a fix.

Verified: typecheck, biome, the 15 frontend tests, a production build, and the
built chrome rendering its address bar, title bar and tabs.

The scripts/solid-2-boundary.ts workaround stays. Its exit condition is
solid-layouts-oxc consulting boundaryFor, which 0.2.2 now does -- but
rsbuild-plugin-solid-layouts 0.2.1 carries its own 0.2.1 copy of the validator,
and a bun override does not dedupe it. The pair goes when that plugin
publishes against 0.2.2.
Nothing here uses daisyUI; the comment described the cascade problem in terms
of a package this repository does not install.
A patch release: no exports added or removed against 2.11.9, and the same peer
range. The caret already admitted it; this pins the lockfile to it.
meh added 3 commits August 31, 2026 18:09
`scripts/render-check.sh` has documented and exported CHUZZ_CAPTURE_WIDTH and
CHUZZ_CAPTURE_HEIGHT since it was written, and the capture entry point read
neither: it passed a literal 1440 by 960. Every capture was that size whatever
the caller asked for, and a run at another width produced a byte-identical
tree, which is how this surfaced.

That matters now because captures are about to be compared against a reference
browser. Two engines laying the same page out at different widths disagree on
every percentage width, every centred box and every responsive breakpoint, so
an unhonoured viewport turns a diff into noise that reads exactly like a
rendering fault.

Only CHUZZ_CAPTURE_SCALE was wired up, so the fix follows its shape.
The prefetch walks the parsed HTML and loads every script it names. A page
that builds a `<script>` from JavaScript asks for a URL nobody knew about
until the page was already running, and that request reached
`DefaultScriptFetcher`, which serves `file:` and `data:` only. The script was
dropped with `unsupported URL scheme for script: https`, which reads like a
policy decision rather than the missing capability it is.

Measured over a hundred-site corpus, this is the single most common engine
defect: **26 sites**, a quarter of the corpus. It hides well, because scripts
the parser found load perfectly, so a page fails only in the parts it
assembles itself, and jQuery going undefined on seven sites looks like its own
bug rather than a consequence of never having been fetched.

`ScriptFetcher::fetch` is synchronous, because classic scripts execute in
document order, so the network call blocks. It runs on the page's own provider
and so keeps the per-origin connection cap the rest of the loads obey, and it
is bounded by a timeout: a server that accepts and never answers would
otherwise hang the capture, and a missing script is better than a run that
never ends.

Verified by re-capturing the six worst-affected sites: dropped scripts went
12, 6, 4, 2, 2, 2 to zero everywhere. Rendering barely moved, which is the
honest result rather than a disappointing one. On one site the error count
rose from 9 to 14 with a new kind, `unescape is not defined`: the scripts now
run and reach the *next* missing global. This unblocks a layer; the missing
web APIs behind it are what turn that into pixels.

The test serves a script from a real socket and asserts a synchronous fetch
completes a round trip from inside the runtime driving the page. Its accept
loop has a deadline rather than blocking: restoring the defect makes no
connection at all, and a blocking accept hangs the run instead of failing it.
Confirmed by restoring the defect, which fails the test in ten seconds.

Only the capture path. `browser.rs` has the same fetcher and is reached from a
synchronous poll with no runtime handle to hand, so it needs a stored handle
and is left for its own change.
The previous change fixed the capture path and left the window with the same
defect, so the browser people actually use still dropped a quarter of the
corpus's scripts while the tool measuring it did not. Measuring correctly is
not the point of the exercise.

The fetcher moves into `script_fetch.rs` and both paths share it, rather than
the window growing a second copy of logic that must then be kept in step. The
prefetch-then-fall-back shape was already duplicated between the two; it is now
in one place.

The deadline is per caller, and the two differ for a reason worth stating.
`ScriptFetcher::fetch` is synchronous and page scripts run on the UI thread, so
in the window this blocks everything, other tabs included, for as long as it
waits. Five seconds there against the capture's ten: a capture is unattended
and a dropped script costs it the fidelity it exists to provide, while a window
has someone watching the frame. Neither number is the real answer. The real
answer is an asynchronous script-loading path in the engine, which would not
have to choose, and that is a change to blitz-script rather than to this.

The window path is the same code the tests cover and the capture exercises, but
it is not itself verified in a window: this machine has no screen access, and
the control socket sees the chrome document rather than the page's
sub-document. Re-captured after the refactor to confirm the tested path did not
move: dropped scripts still zero on the worst-affected site.
@pathscale
pathscale force-pushed the release/corpus-fixes branch from 038c913 to 12ca80d Compare August 31, 2026 11:10
@pathscale pathscale changed the title Corpus fixes: capture viewport, and the scripts a page discovers while running Release: UI 2.11.10 + Solid 2.0.0-rc.4, capture viewport, and runtime script fetching Aug 31, 2026
The browser advertises two tools over the same socket. `chuzz-inspect` only
ever called `blitz.agent.control`, so `blitz.diagnostics` — DOM and layout
snapshots, renderer metrics, idle settlement, and the console and
runtime-error streams — was unreachable from the client that exists to read
the browser.

That gap has a cost. A page that throws during evaluation reports it only to
stdout, mixed in with the renderer's own logging, and a JS error is then
indistinguishable from a paint trace. Driving honey.id here is what surfaced
it: the page rendered blank, and the reason was a TypeError only visible by
grepping the process output.

`call` keeps its shape and routes to the agent tool, `diagnostics` is the same
round trip against the other one, and both share `call_tool`. On top of that
the CLI grows `console`, `metrics`, `settle`, `dom` and a verbatim `diag`.

Two limits worth stating, both in the runtime rather than here:

- `console` answers `streamingUnavailable`: diagnostic subscriptions are not
  implemented, and the runtime says so rather than pretending.
- `dom` reports the chrome window. A page lives in a sub-document on its
  `<web-view>` mount and is still not in the tree, so page content remains
  reachable only by screenshot.
@pathscale

Copy link
Copy Markdown
Owner Author

Added feat(inspect): reach the diagnostics tool, not just agent control.

The browser advertises two tools over the same socket. chuzz-inspect only ever called blitz.agent.control, so blitz.diagnostics — DOM and layout snapshots, renderer metrics, idle settlement, and the console and runtime-error streams — was unreachable from the client whose whole job is reading the browser.

That gap has a cost. Driving honey.id here is what surfaced it: the page rendered blank, and the only way to learn why was to grep the process stdout for Uncaught JS error, where it sits mixed in with the renderer's own paint logging.

call keeps its shape and routes to the agent tool, diagnostics is the same round trip against the other one, and both share call_tool. The CLI grows console, metrics, settle, dom and a verbatim diag.

Verified against a running browser: metrics returns real renderer timings, dom returns a full DOM and layout snapshot with attributes.

Two limits, both upstream in the runtime rather than here, and both worth knowing:

  • console answers streamingUnavailable — diagnostic subscriptions are not implemented. It says so rather than pretending, but it means neither the page nor an agent can observe a JS error.
  • dom reports the chrome window. A page lives in a sub-document on its <web-view> mount and is not in the tree, so page content is still screenshot-only. Checked rather than assumed: of 87 nodes, the only matches for a loaded page were the address bar's value and the tab title.

cargo fmt --check, clippy and the 15 tests pass.

@pathscale

Copy link
Copy Markdown
Owner Author

Superseded by #33, which consolidates all chuzz work into one PR.

@pathscale pathscale closed this Aug 31, 2026
@pathscale
pathscale deleted the release/corpus-fixes branch September 1, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant