Skip to content

refactor(export): build static snapshot from shared live modules#60

Merged
setkyar merged 3 commits into
mainfrom
refactor/unify-export-build
Jun 5, 2026
Merged

refactor(export): build static snapshot from shared live modules#60
setkyar merged 3 commits into
mainfrom
refactor/unify-export-build

Conversation

@setkyar
Copy link
Copy Markdown
Contributor

@setkyar setkyar commented Jun 5, 2026

Summary

Collapses a long-standing duplication in the static export/Gist renderer and fixes a blank-page regression it surfaced.

  • Unify export with the live app. The Gist export runtime was a ~2000-line hand-maintained copy of the live session renderer (internal/ui/live_templates/export/app/*.js), kept in sync by discipline and prone to drift. It's replaced by a Vite-built bundle from a new web/src/export/export-entry.js that imports the same web/src/session/ rendering modules as the live app, omitting all live-only code (SSE, chat, artifacts, annotations). export.go now embeds one built file instead of concatenating nine.
  • Fix sandboxed-iframe crash. Gist previews load the snapshot in a sandboxed iframe without allow-same-origin, where even reading window.localStorage throws SecurityError — which aborted the unified bootstrap and left a blank page. The export now guards localStorage behind an in-memory shim.
  • Add real share coverage. A new GET /share?id=…&preview=1 returns the rendered export HTML directly (no gh/gist) for local preview; the share E2E now loads that snapshot in a sandboxed iframe and asserts the conversation renders (verified to fail without the fix).
  • De-flake the load-earlier E2E. Made the large-session truncation thresholds env-configurable (defaults unchanged) so the spec triggers pagination with a tiny session instead of rendering 1600 messages, which flaked under parallel CPU contention.

Related issue

Closes #

Type of change

  • feat — new feature (local ?preview=1 export mode)
  • fix — bug fix (sandboxed-iframe blank page)
  • docs — documentation only
  • refactor — code change that neither fixes a bug nor adds a feature
  • style — formatting / UI styling, no behavior change
  • test — adding or updating tests
  • chore — build, tooling, or maintenance

Live vs. Export

  • Not applicable — this PR doesn't touch session rendering
  • Considered both the live app and the export snapshot
  • Export now builds from web/src/session/ rather than a hand-kept copy, so the two render paths can no longer drift (replaces the old manual-sync rule)
  • No live-only chrome (Vite scripts, active composer, SSE/API) leaked into export — guarded by TestExportBundleIsSelfContained

Testing

  • go test ./... — pass
  • go vet ./... — pass
  • cd web && npm test (vitest) — 466 pass
  • make build (frontend build incl. export bundle + go build) — pass
  • make e2e — full Playwright matrix green; share + load-earlier specs verified across all browser projects
  • Verified the new sandboxed-iframe E2E fails with the localStorage fix reverted and passes with it

setkyar added 3 commits June 5, 2026 16:52
The Gist export runtime was a ~2000-line hand-maintained parallel copy of
the live session renderer (internal/ui/live_templates/export/app/*.js),
kept in sync by discipline alone and prone to silent drift.

Replace it with a Vite-built bundle from a new web/src/export/export-entry.js
that imports the exact same web/src/session/ rendering modules as the live
app (data, tree, filter, format, render, navigation, ui), omitting all
live-only code (SSE, chat, artifacts, annotations). vite.config.export.js
emits a single self-contained IIFE that reads window.marked/window.hljs from
the inlined vendor scripts; export.go now embeds that one built file instead
of concatenating nine hand-written ones.

- Delete export/app/*.js; export.go shrinks to a single //go:embed.
- Add TestExportBundleIsSelfContained to fail the build if a live-only
  symbol (EventSource/runLiveReload) ever leaks into the export graph.
- Repoint the tests that grepped the old unminified bundle at the canonical
  web/src source files (the minified bundle is no longer greppable).
- export.js + dist-export are generated; gitignore them. npm run build now
  runs build:export after the live build, so make build/check stay green.
The load-earlier spec built a 1600-message session; each "load earlier"
click re-renders the whole conversation on the browser main thread. Under
the full E2E matrix (8+ browsers + Node runner + one shared pi-web server)
CPU starvation intermittently delayed that work — and even Playwright's own
poll — past the assertion timeout. The load always completed; the test just
gave up first. It flaked locally (workers=undefined, retries=0); CI masked
it via retries.

Make the work cheap and bound the residual flake:
- session_page.go: LargeSessionThreshold/LargeSessionTailEntries become
  env-configurable (PI_WEB_LARGE_SESSION_THRESHOLD / _TAIL_ENTRIES),
  defaults unchanged. The existing comment already invited this seam.
- e2e/lib/server.ts: lower them to 100/50 so the spec triggers the identical
  pagination path with a ~150-entry session that renders instantly. Chosen
  well above every other spec's session size (max ~34) so nothing else
  truncates.
- load-earlier.spec.ts: 150-entry session, modest 15s waits, and retries: 2
  to absorb rare contention spikes — a real regression still fails every
  attempt.
- pagination_test.go: const n -> n := (threshold is now a var, not a const).
Gist previews load the export HTML in a sandboxed iframe without
allow-same-origin, where even reading window.localStorage throws
SecurityError. The unified export bootstrap read target.localStorage eagerly
to pass into setupSessionUi, so that throw aborted the whole bundle and left
a blank page. (The old hand-written export only touched localStorage inside
try/catch, so it never surfaced.)

- export-entry.js: wrap localStorage access in a safeLocalStorage() helper
  that falls back to an in-memory shim when the property access throws — a
  static snapshot has nothing to persist anyway. Returning a shim (never
  undefined) also keeps the shared modules off their globalThis.localStorage
  default, which throws the same way.
- share.go: add a GET ?preview=1 mode that returns the rendered export HTML
  directly, skipping the gh/gist round-trip. Useful for eyeballing a snapshot
  before sharing, and gives tests a network-free way to load the real page.
- share.spec.ts: load the previewed snapshot into a sandboxed (allow-scripts,
  no allow-same-origin) iframe and assert the conversation renders — the
  regression guard. Verified it fails without the localStorage fix.
- share_test.go: cover preview mode (returns HTML, no gist; requires id).
@setkyar setkyar merged commit 531336e into main Jun 5, 2026
2 checks passed
@setkyar setkyar deleted the refactor/unify-export-build branch June 5, 2026 12:35
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