Repository navigation
HyperLab in a browser: the website, and a WebAssembly shell - #4
Merged
Merged
Conversation
…ystem or an HTTP client The browser build needs both: it brings its own transport (fetch), so the provider crate's encode/decode become public free functions behind a default 'native' feature that keeps reqwest and the clients exactly as they were; and it has no disk, so a stack can now round-trip through a string with single_file_string and stack_from_single_file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
…ompiled to WebAssembly One exported function per desktop command, so the React renderer cannot tell which shell it is behind. Dialogs cross a JsHost object the page supplies — the worker blocks where the desktop blocked a thread — a stack travels as single-file JSON instead of a path, keys live in browser storage behind the same one-way rule as the OS keychain, and AI requests go through the browser's own fetch straight to the configured provider. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
…he browser apps/web serves two pages from GitHub Pages. The landing page tells HyperCard's story — 1987, the teachers, the museums, Myst — dressed in the Neo Classic theme's own values. The playground is the desktop's React renderer, unchanged, over the WebAssembly runtime: Vite swaps one module (api.ts) and everything else — cards, inspector, assistant, map, dialogs — is the same code the desktop draws. The runtime lives in a Web Worker so a script can block on a real, HyperCard-shaped dialog: the worker parks on Atomics.wait while the page shows it, which is the desktop's blocking-thread arrangement rebuilt from browser parts. GitHub Pages cannot send the isolation headers that allows, so a vendored coi-serviceworker (MIT) adds them; without isolation the runtime falls back to the main thread and the browser's own prompt. Files became uploads and downloads of the single-file stack JSON, pictures arrive through a file picker, and the six examples are packed by a new persistence example and fetched from the site. API keys live in browser storage behind the same one-way rule as the OS keychain and travel only to the configured provider — the site is static files, so there is no server for them to visit. The stack crate's clock became pluggable along the way: SystemTime traps on wasm32, so the web shell names the browser's clock, and the interpreter's time and randomness follow it. The Todo example also gained the trailing newline its Add button always assumed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
pages.yml builds the wasm runtime, packs the examples, and deploys apps/web to GitHub Pages on every push to main; a matching CI job runs the same build on pull requests so it cannot rot. docs/website.md explains the arrangement — one swapped module, a worker parked on Atomics.wait behind real dialogs, keys that never leave the browser — and the README and changelog point at the site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
Plain section titles, shorter paragraphs, and fewer slogans: Examples instead of a counted headline, AI and The desktop application instead of coinages, one sentence on keys instead of four. Your history text is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
wasm-pack's release profile downloads a binaryen release to run wasm-opt, and a failed download fails the build — which is how the Pages deploy would have broken on a bad day. The optimiser was worth about a tenth of a 313 KB artifact, so the build gives it up and stays hermetic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
Both crates already compiled to WebAssembly untouched; what they could not do was find a typeface. Artwork carries words — Cluedo's board names its rooms — and a page cannot read the machine's fonts, so those labels would have gone missing from every export. So export and decker each take a font from their host, the way the stack takes a clock: registered fonts join the machine's own, and stand in for the generic families only where the platform found none, which leaves a desktop export exactly as it was. The site fetches Liberation Sans (OFL) the first time someone exports, so the 400 KB never lands on an ordinary visit. A PDF crosses the worker boundary as bytes rather than base64 through the JSON channel. Driving the real page, a Cluedo PDF exported in the browser matches the desktop's checked-in one: three pages, thirteen pieces of artwork, the same words, an embedded face for the labels. The MCP client is now native-only. It spawns a program, which a browser cannot do, and compiling it for wasm left a warning that -D warnings would have failed CI on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
`cargo doc` runs on the host, where crates/web's api module does not exist — it is compiled for wasm32 only — so a link to it cannot resolve, and ProviderConfig was never in scope to link at all. CI documents with -D warnings, which is how both were caught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
The pre-pull-request list named the desktop app and stopped there, so somebody touching a core crate had no way to know they had just broken a wasm build. It now covers the website, and the documentation build that caught two links good everywhere but there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
CI caught this and my machine could not: the test asserted that a picture's words were drawn, which held on a laptop full of fonts and failed on a bare runner. The rule was wrong, not just the test. usvg falls back to the generic families, and those resolve through names of their own — sans-serif means Arial until something says otherwise. Keying the stand-in on "the platform found no fonts at all" missed the commoner case: a machine with fonts but no Arial, which is a stock Linux box as much as it is a browser, and which was losing its labels either way. So the stand-in now turns on whether the family resolves, which fixes both and still leaves a machine that can already draw sans-serif alone. The test now asserts only what holds anywhere — the document comes out whole. That a registered font reaches the converter, and that the generic family it stands in for really was unresolvable, are unit tests against a database built in the test. That a browser ends up with the labels is checked by driving the page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A retro landing page telling HyperCard's story, and the whole application
running on it — the same renderer the desktop draws, over the same runtime
compiled to WebAssembly.
The playground is the application
The desktop keeps its shell behind one module (
apps/desktop/src/api.ts, atyped wrapper per command), and the renderer never learns which shell
answers. Vite swaps that one module for
apps/web/src/api.ts; nothing elsein the desktop tree is touched, so the two interfaces cannot drift apart —
they are the same files.
apps/desktop/has no changes in this diff at all.Behind it,
crates/webwraps the same crates the Tauri shell wraps, oneexported function per desktop command.
What a browser makes hard, and what was done about it
A script has to block on a dialog.
askstops the script and the answerlands in
iton the next line. The runtime lives in a Web Worker; when ascript asks, the worker parks on
Atomics.waitwhile the page — stillresponsive — shows the same dialog component the desktop shows, and writes
the reply into a
SharedArrayBuffer. The runtime is none the wiser. GitHubPages cannot send the isolation headers that needs, so a vendored
coi-serviceworker.js(MIT) adds them, at the cost of one reload on firstvisit; without isolation the runtime falls back to the main thread and
window.prompt.There is no file system. A stack travels as the single-file JSON
hyperlab-persistencealready speaks — the same file the desktop opens.There is no clock.
SystemTime::nowtraps onwasm32, sohyperlab_stack::set_clocklets a host name one and the web shell names thebrowser's. The interpreter's
the dateandrandomfollow it.There are no fonts. Artwork carries words — Cluedo's board names its
rooms — and both exporters asked the operating system for a typeface. They
now take one from their host too. Chasing this found a real bug beyond the
browser: usvg resolves the generic families through names of their own, and
sans-serifmeans Arial until told otherwise, so a stock Linux box with noArial was losing labels as surely as a page was. The stand-in now turns on
whether the family resolves, which fixes both and leaves a well-stocked
machine alone.
Keys never leave the browser
The desktop keeps API keys in the OS keychain, one-way. The web keeps the
same shape with the parts a page has: a key goes into this browser's
storage, no command reads one back out, and requests go straight from the
browser to the configured provider. The site is static files, so there is no
server of ours for a key to visit. A mock provider answers locally, so the
whole AI surface can be tried with no key at all.
Feature parity
Everything on the desktop's File menu is here, including a PDF of the cards,
a PNG of the map, a self-contained web page, and a Decker deck. A Cluedo PDF
exported in the browser matches the desktop's checked-in
docs/cluedo.pdf:three pages, thirteen pieces of artwork, the same words, an embedded face
for the labels. The one deliberate difference is
Save/Save As…collapsing into
Download Stack.Enabling changes to existing crates
hyperlab-ai-providersgained a default-onnativefeature. Without itthe crate is only the wire protocol, for a host that brings its own
transport; with it, nothing changes.
hyperlab-persistencegainedsingle_file_stringandstack_from_single_file.hyperlab-mcp's client is native-only — it spawns a program, which abrowser cannot do.
assumed.
Verified
CI is green on
036fe74, all seven jobs. Beyond that, the built site wasdriven in Chromium: all six examples open and draw, a script's
askblocksand its reply reaches
it, undo works, the mock assistant answers,ai("…")answers mid-script, and both new exports produce valid files.
One thing left for a human
Settings ▸ Pages ▸ Source must be set to GitHub Actions before the
deploy workflow has anywhere to publish.
docs/website.mdexplains the arrangement in full.🤖 Generated with Claude Code
https://claude.ai/code/session_01R7eaxUAqy2gKQSBNnYZoFr