Infinite Mac Construction Set

6 hours ago 3

tl;dr: You can now embed any OS from Infinite Mac into your website, from 1984’s System 1.0 through 2005’s Mac OS X 10.4. There’s documentation for customizing and controlling embedded instances programmatically. As a demo of what’s possible, Infinite Monkey hooks up an emulated Mac 128K to OpenAI’s and Anthropic’s computer-using models, letting the technologies of 1984 and 2025 to finally meet. The instigator behind all this was Marcin Wichary, whose recent Frame of preference article is another showcase of the embedding capabilities.

A Kindred Spirit

Marcin’s articles – with their attention to detail and passion that they convey – have always struck a chord with me, whether they’re about underrated fonts or underrated movies. I’ve also appreciated that he strives for interactivity, from his Google Doodles to his Config talks. A few months ago, Marcin approached me about doing a modern take on his GUIdebook site, something with a story to tell and a way for readers to experience the material first-hand. Specifically, he wanted a way to embed emulated Mac instances in an article, ideally in as seamless of a way as possible.

Infinite Mac was already used by sites like Classic Macintosh Game Demos, DiscMaster, and Macintosh Repository to host runnable custom instances. But those were full-screen experiences and, in some cases, required a custom fork of the site. I wanted the experience to be closer to a YouTube or Google Maps embed – a snippet of HTML that’s easy to drop into any site, but still controllable via query parameters. To enable this, I added a new /embed endpoint that’s better suited to iframing. In addition to hiding the screen bezel and other chrome, it lets the embedding site control the screen resolution, get notified of the screen contents changing, and send mouse and keyboard events to the emulated Mac.

To keep embedded instances lightweight, I added an option for auto-pausing when they’re hidden. This uses both in-page (IntersectionObserver) and cross-page (visibilitychange) signals to pause and resume the emulator. Fortunately, I didn’t need to implement pausing in each emulator – instead I hooked into their input-reading loop and used Atomics.wait to cheaply suspend execution until the “paused” bit is cleared.

There were a couple of surprises along the way. First, while Chrome lets SharedArrayBuffer work in iframes (via the allow="cross-origin-isolated" attribute), this is not yet supported in Safari/WebKit-based browsers. That made my 2021-era Safari workaround newly relevant. It mostly worked, but it assumed a single global emulator instance which in Marcin’s tests had the hilarious effect of making every Mac on the page receive the same input. I added per-instance tracking so that each one gets its own event stream.

Infinite Mac network request waterfalling

Second, Marcin noticed his custom disk images loaded much more slowly than the built-in ones. Some of that was due to them bypassing Cloudflare’s cache, but even when that was enabled, they were slower. It turned out that prefetching – added in the earliest days of the site, when all it ran was System 7 – had become even more important for more modern Mac OS versions. They read a lot more data at startup, and being blocked for 50-100ms for each chunk adds up when they need to get through a couple of hundred. Extending the prefetching to remote disks too (and caching these prefetched chunks via the service worker) shaved a lot of time off booting the Mac OS 8.5 and NeXTStep images from Marcin’s article.

Infinite Mac embed builder dialog

I wanted to make embedding as approachable as possible (well, approachable for those interested in computing platforms from 20 to 40 years ago). There is an embed HTML builder that uses a variant of the custom instance dialog to generate the <iframe> markup for a specific instance (a construction set if you will). I also wrote documentation for the query parameters and message events to control the instance and receive state-change notifications.

Making a Demo Site

Marcin’s article is a kind of demo, and I had a crude testbed, but I wanted to more thoroughly dogfood the embedding support. It occurred to me that its capabilities (get screen content updates, send synthetic input events) made it a perfect fit for the computer use models recently launched by OpenAI and Anthropic. Their demos control a Docker container or other remote environment; having something in the browser is both satisfying (nothing to set up) and safe (the emulation sandbox already limits what the model can do).

The Mac actually has a long history of being controlled by another program, thus the Infinite Monkey site was born (as an aside, the original Monkey desk accessory is installed in the System 1.0 image if you’d like to try it out). Hooking it up was relatively straightforward, the useComputer and useChat hooks have the core glue logic. I even had Claude Code write most of the Anthropic glue code. I went with a “bring your own API key” approach – the models are somewhat expensive, and I did not want the donors to foot the bill. This does make it somewhat less accessible (the OpenAI version of the model is only available for Tier 3 accounts, and possibly not even then). The demo video should help those who don’t have access.

The actual experience of letting an LLM drive System 1.0 is a bit like a dog walking on its hind legs – impressive that it works at all, but objectively a bit underwhelming. The models are slow, it’s definitely faster to use the computer yourself (Anthropic’s own documentation calls this out). They also struggle with the UI conventions of older platforms, especially the press-and-hold mechanics of pull-down menus. OpenAI’s model can’t use them at all, since it can only request click and drag actions. Anthropic’s fares better with its separate left_mouse_down and left_mouse_up, but it often tries to click first, even when instructed otherwise.

OpenAI vs. Anthropic's handling of pull-down menus

This is just one possibility, I’m curious what other things could be built using embedded instances. And if there’s a capability that you wish they had, feel free to file an issue suggesting it.

Odds and Ends

I modernized the site’s Cloudflare setup, adopting both static assets and the official Vite plugin. Both worked as advertised: I have less code to maintain, and the local dev experience is closer to production. It was a welcome change from the usual experience on the frontend dependency treadmill.

The auto-pausing work above also suggested how I might implement a speed setting for more machine types. This fulfills a long-standing feature request to allow older software to run more accurately. It’s somewhat amusing that even in this many-layered environment (an emulator compiled to WebAssembly, which is in turn interpreted or compiled to the native platform) some things can be too fast.

Epilogue: Doctor. Manhattan has nothing on me

  • It is 2006. I am working on making one website (Google Reader) embeddable in another (Gmail).
  • It is 2007. I am working on making one website (Google Reader) embeddable in another (Blogger).
  • It is 2008. I am working on making one website (Google Reader) embeddable in another (iGoogle).
  • It is 2012. I am working on making any web app (Chrome Apps) embeddable in a host environment (Chrome).
  • It is 2017. I am working on making any web app (Live Apps) embeddable in another (Quip documents).
  • It is 2020. I am working on making one website (Quip chat) embeddable in another (Salesforce Lightning).
  • It is 2021. I am working on making one website (Quip documents) embeddable in another (Slack).
  • It is 2022. I am working on making one service (Tailscale SSH) embeddable in another (Tailscale Admin Panel).
  • It is 2023. I am working on making one service (Sierra agents) embeddable in any website or iOS app.
  • It is 2024. I am working on making one service (Sierra agents) embeddable in any Android app.
  • It is 2025. I am working on making one website (Infinite Mac) embeddable in any other.
Read Entire Article