Skip to content

Embed the selector engine in the standalone report viewer - #243

Merged
handstandsam merged 2 commits into
mainfrom
viewer-shell-selector-engine
Aug 20, 2026
Merged

Embed the selector engine in the standalone report viewer#243
handstandsam merged 2 commits into
mainfrom
viewer-shell-selector-engine

Conversation

@handstandsam

Copy link
Copy Markdown
Collaborator

The UI Inspector's selector suggestions were unavailable in the standalone report viewer — the one the docs site publishes and anyone can self-host. The shell embeds every line of Inspector code (Inspect UI, selectorSuggestion*, the #tb-selector-engine reader) but nothing ever embedded the engine for it to run, so loadSelectorEngineFromChunk returned null and the panel opened with no suggestions. Silently: a null engine is a designed degrade path.

Exported reports were never affected. There the Kotlin side supplies it — RunReportGenerator stages the JAR resource, run-report-cli.ts packs it into the chunk. The shell is built by bun alone, with no run baked in, and had no equivalent step.

Verified on today's docs-report-clock artifact — the exported report carries the payload and the published viewer does not:

Inspector code engine payload
report-interactive.html
hosted viewer shell

The fix

A bun macro reads the Kotlin/JS bundle at transpile time and buildViewerShellHtml embeds it as the same #tb-selector-engine chunk a report uses. The macro imports packSelectorEngine rather than re-implementing the transport, so the two homes can't drift.

The shell embeds it unconditionally, unlike a report, which embeds it only when a session carries an analyzable hierarchy: the shell has no session at build time, any archive dropped later may need it, and an offline single-file viewer gets no second chance to fetch one.

Size: 416,116 → 531,155 bytes (+115 KB — gzip+base64 of the ~320 KB bundle, which always lands on the gz side of the threshold). The macro is replaced by its value at transpile time, so no driver code enters the bundle; the delta is the payload and its wrapper, nothing else.

Delivery. build-viewer-shell.sh builds the bundle on demand when ./gradlew is present, so the default path produces a complete viewer. A JDK stays optional: without one you get today's viewer plus a warning naming the gradle task. --require-engine turns that warning into a failure, and the Pages deploy passes it — a missing engine is invisible in a rendered page, so this is the one payload worth failing a deploy over. That step needs a JVM, so the deploy gains setup-java (JDK 17, gradle cache) used for nothing else.

Test plan

  • Built the shell, loaded a real CI session archive, clicked Inspect UI, selected a node — engine loaded (TrailblazeSelectorEngine exposing resolveTapTarget / resolveSelector / computeSelectorAnalysis) and suggestions rendered: textRegex: ALARM marked UNIQUE/BEST, plus the structural content-free suggestion
  • --require-engine with the bundle absent exits 1; without the flag exits 0 and emits a shell byte-identical to today's (416,116 bytes), so the degrade path is unchanged
  • tsc --noEmit clean; bun test 440 pass / 0 fail
  • No driver code leaked into the shell (packSelectorEngine, readSelectorEngineSource, anyAnalyzableHierarchy all absent from the output)
  • Bundle is a gitignored build artifact — nothing generated is committed
  • CI green
  • After merge: Inspect UI shows suggestions on the gallery runs

The viewer shell shipped every line of UI Inspector code and no engine to run it, so
"Inspect UI" opened a panel that could never show a selector suggestion. An exported
report gets the engine from the Kotlin generator path; the shell is built by bun alone
and had no equivalent step.

A build-time macro now embeds the bundle through the same transport the report uses.
build-viewer-shell.sh builds it on demand and takes --require-engine, which the docs
deploy passes so a published viewer can't silently lose the feature.
This workflow publishes: it holds `pages: write` + `id-token: write` and
deploys what it builds to the live site. A restored dependency cache in a
publishing workflow is the cache-poisoning vector zizmor flags, and it would
land on the very payload the JDK step exists to produce — the selector engine
bundle embedded in the published viewer.

build-desktop.yml and release.yml already take this posture; this was the only
`cache:` left in any workflow. Cost is a cold compile of one small Kotlin/JS
module per deploy.
@handstandsam
handstandsam merged commit d6bd343 into main Aug 20, 2026
6 checks passed
@handstandsam
handstandsam deleted the viewer-shell-selector-engine branch August 20, 2026 15:58
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