The problem I was staring at
A product manager on the team wanted an interactive demo of a UI flow, the kind of thing that you drop into a customer email or a Slack channel and the recipient can click through. The existing option was: record a Loom, wait for a UI change, record it again. Every button rename, every layout shift, every color tweak invalidated the recording. The PM was on their fourth take of the same 40 second flow that week.
The obvious fix was to stop recording pixels and start recording steps. Store the flow as a list of actions against a real running app, replay those actions on demand, and let a machine describe what happened in text alongside the visual replay. If the underlying app changed shape, only the selectors and the narration would need updating, not the whole video.
Yokoso is what came out of that. It is an AI-powered demo generator that runs the target app inside a WebContainer, drives a timeline of user actions against it, and exports the result either as a stepped interactive demo or as an MP4 built with FFmpeg.wasm. The repo is private under kaushiksaravanan/yokoso. I own it.
What runs where
The project sits in a Turborepo monorepo. The web app is Next.js 14 with the app router. The timeline editor is a React surface but the heavy drawing is a PixiJS canvas layered underneath, because at around 200 keyframes React reconciliation started dropping frames on the scroll and the drag interactions. Moving the frame rendering to Pixi got the interaction back to 60 fps on a mid-range laptop.
The runtime piece is a WebContainer. When you load a Yokoso project, the browser boots a Node environment in-process, mounts the target app source, runs npm install inside the container, and starts the dev server. The URL you see in the preview iframe is a service worker route into the container. You get a real, hot-reloading Next.js dev server running inside your tab. No servers of mine touch this. The target app is whatever code the user uploads.
The export pipeline uses FFmpeg.wasm. Once a run is recorded, frames get pulled off the preview iframe and stitched into an MP4 in a worker. The whole export path stays in the browser, which matters because customer apps sometimes contain data that the customer does not want touching a third party server.
Thirty one tools over MCP
The other half of Yokoso is a Model Context Protocol server. I wrote 31 tools that a client like Claude Desktop or Cursor can call to drive a demo end to end. The tool set is split across six files under packages/mcp-server/src/tools/:
Environment (6): boot the WebContainer, run npm install, start the dev server, stop the dev server, run an arbitrary shell command, read the current environment status.
Recording (5): start, stop, pause, resume, and read the current status of a recording.
Automation (10): generate a demo script from a natural language prompt, execute a script step by step, take a DOM snapshot of the running app, capture a screenshot, analyze the app structure, click, type, scroll, wait, and wait for a specific element.
Editor (4): read, write, list, and delete files in the WebContainer filesystem.
Repo (3): load a GitHub repository into the workspace, get info on the currently loaded repo, search GitHub for a repo.
AI (3): analyze a screenshot with a vision model, suggest what actions to take next given the current app context, generate demo ideas for the loaded repo.
The 31 count is not marketing. It is the sum of server.tool( calls across the six files in the tools directory. Every tool is a real function that the WebContainer or the workspace store already exposed. The MCP surface is a thin translation layer, not a new codebase.
The point of the MCP surface is that a demo brief in plain English becomes a demo without me clicking through the editor. A PM types "record a two minute demo of the invoice creation flow, focus on the split payment case, narrate each step" into Claude Desktop and the tool sequence writes itself.
Why Transformers.js came out
Yokoso needed embeddings for two things: matching a user narration prompt against past demos, and clustering similar steps across projects so the editor could suggest reusable snippets. I started on Transformers.js, running a small MiniLM model inside the browser and inside the Vercel serverless function that backs the search API.
The browser side worked. The serverless side did not. Transformers.js in a Vercel serverless function had a cold start that regularly blew past the 10 second function timeout on the free tier and did not stay under it reliably on the Pro tier either. The model weights had to come off disk on every cold container. The WASM runtime for ONNX would sometimes fail to initialize with a stack trace that was hard to reproduce locally, because locally the cold start looks nothing like the Vercel cold start.
I gave it a real try. I tried lazy loading the model, sharing model instances across warm invocations (which Vercel does not really let you do deterministically), trimming the model, quantizing more aggressively. None of it turned into a reliable p99. Every three or four demos, a user would hit a cold container and wait fifteen seconds for a search that should take under a second.
The migration was to OpenAI embeddings. One HTTP call, no model in the function, cold start becomes indistinguishable from any other API-backed endpoint. The downside is that embeddings now cost money and require an API key, so the search API sits behind auth and rate limits. The upside is that the search actually works every time. The browser path still uses Transformers.js for the local suggestion feature that runs while you are editing, because in the browser the cold start is a one-time model download, not a per-invocation cost.
I do not think Transformers.js is bad. I think Vercel serverless functions are the wrong host for it.
The auto-injected wait problem
The interesting bug in the action replay path was action validation. The tool calls looked like "click the submit button, then read the confirmation banner." The click succeeded. The read failed, because the banner appeared 300 ms later after an async request came back. The replay engine would report a failed step even though the underlying flow was healthy.
The first fix was to let the AI add its own waits. That did not work. The model would sometimes add a wait, sometimes not, sometimes add a wait for the wrong element. Human demo authors would forget too.
The second fix was a script validator that checks every action pair. After a click, submit, or navigate, the next action must be a wait, a waitForElement, or a waitForNavigation, otherwise the validator flags it. The action language already had those wait types as first-class actions, so the fix was to make the missing ones a hard warning at authoring time and to teach the script generator to always emit an appropriate wait after a state-changing action.
The recording still stores the human-authored steps. The validator runs before replay and refuses to run a script that has a click followed immediately by an assert or a screenshot. This split matters because a demo file recorded a month ago still replays correctly today even if the target app is now slightly slower on a particular request, because the wait is a condition, not a fixed sleep.
Reliable action validation was the last thing keeping the tool from being usable by a non-engineer. Once the waits went in, the PM I built it for could actually author a demo without me on a call.
Why it lives inside Atomix now
Yokoso started as a standalone Next.js app under its own subdomain. It had its own auth, its own settings page, its own storage. Every improvement to the shared shell had to be ported over, and every improvement to Yokoso had to be re-explained to anyone who lived mostly in Atomix, the shared internal shell for our tooling.
I migrated Yokoso into Atomix as a native page. The timeline editor, the WebContainer preview, the MCP server, the export pipeline, all of it is now mounted under an Atomix route. Auth comes from Atomix. The project list is one query into Atomix's shared storage. When a user goes from a Yokoso demo to a different Atomix page, the sidebar does not change and the session does not reset.
The MCP server still runs as a separate process so it can be pointed at Claude Desktop, but the tools it exposes now call into Atomix APIs instead of Yokoso's private ones. The 31 tools survived the migration without a rename.
The migration also killed a class of bugs where the standalone Yokoso and Atomix had diverging versions of the same helper. There is now one version of the DOM snapshot code, one version of the FFmpeg export wrapper, one version of the WebContainer boot sequence. If a fix lands in Atomix, Yokoso gets it on the next deploy.
Yokoso being a page inside Atomix is the correct shape for it. It was never a product on its own. It was a workflow that happened to need a lot of scaffolding, and the scaffolding belonged in the shared shell all along.