Embeddable UI

Drop a Lifo terminal and a live preview browser into your own app with the framework-agnostic @lifo-sh/ui package.

@lifo-sh/ui gives you the playground's building blocks as reusable, framework- agnostic components — mount them into any DOM element, wire them to a box, done. No React required (though they work fine inside it).

  • Terminal — a themeable xterm.js terminal bound to a box's shell.
  • PreviewBrowser — an iframe view bound to an in-VM port, with optional Chrome-like chrome (back / forward / reload, a friendly address bar, and open-in-new-tab).
  • FileExplorer — a file tree over the box's filesystem (optionally with an editor).
npm install @lifo-sh/core @lifo-sh/ui

Run a project from scratch

Here's a complete, minimal app: boot a box, attach a terminal, start a server inside the VM, and show it live in a preview browser — all in the browser, no backend.

<div id="terminal" style="height: 300px"></div>
<div id="preview" style="height: 400px"></div>
import { Sandbox, ServiceWorkerBridge } from "@lifo-sh/core";
import { Terminal, PreviewBrowser } from "@lifo-sh/ui";

// 1. Boot a box with a terminal attached. The terminal is wired to the box's
//    shell — you get an interactive prompt immediately.
const term = new Terminal(document.getElementById("terminal")!);
const sandbox = await Sandbox.create({
  terminal: term,
  cwd: "/home/user/app",
  files: {
    "/home/user/app/server.js": `
      const http = require("http");
      http.createServer((req, res) => {
        res.setHeader("content-type", "text/html");
        res.end("<h1>Hello from a Lifo box</h1>");
      }).listen(3000, () => console.log("listening on :3000"));
    `,
  },
});

// 2. Route the box's in-VM ports through a service worker so they can be
//    previewed in an iframe. (Serve the Lifo service worker at /sw.js — see
//    Live previews.)
const bridge = new ServiceWorkerBridge(sandbox.kernel.portRegistry);
await bridge.connect("/sw.js", "/");

// 3. Start the server. It's long-running, so don't await it — it keeps serving
//    until the box is torn down.
sandbox.commands.run("node server.js");

// 4. Mount a live preview bound to the in-VM port.
new PreviewBrowser(document.getElementById("preview")!, { bridge, port: 3000 });

That's the whole thing. The preview renders whatever the in-VM server returns, and any client-side navigation or HMR flows back through the bridge. Swap server.js for a real project — seed a Vite or Expo app, run npm install && npm run dev in the terminal (or via sandbox.commands.run), and point PreviewBrowser at the dev server's port.

Note

Live previews need the Lifo service worker served at /sw.js with a Service-Worker-Allowed: / header so it can control the whole origin. See Live previews for how the service-worker bridge works and Protocols for the wire format.

Terminal

const term = new Terminal(container, {
  fontSize: 14,       // default 14
  webgl: true,        // WebGL renderer (default true)
  theme,              // an xterm ITheme (defaults to Tokyo Night)
});

Pass it to Sandbox.create({ terminal: term }) to bind it to the box's shell. Methods include write, writeln, onData, focus, fit/refit (call refit() when a hidden terminal becomes visible), setFontSize, and setTheme. Sandbox.create also accepts an HTMLElement or a CSS selector directly and creates the terminal for you.

PreviewBrowser

const preview = new PreviewBrowser(container, {
  port: 3000,         // in-VM port to preview (required)
  bridge,             // the box's ServiceWorkerBridge (preferred)
  // boxId: "box_…",  // or an explicit box id, if you manage the SW yourself
  path: "/",          // initial in-app path (default "/")
  chrome: true,       // show back/forward/reload + address bar (default true)
});

It points an iframe at `/_sw/<boxId>/<port>/` (the service-worker route) and reads the box id from the bridge. Methods:

  • reload() — reload in place, preserving the current in-app route.
  • navigate(path) — go to a different in-VM path.
  • setPort(port) — point at a different in-VM port.
  • back() / forward() — drive the iframe's history.
  • route() — the current absolute in-VM route.
  • destroy() — tear down (stops URL polling, removes the DOM).

Set chrome: false for a bare iframe with no browser chrome — useful when you want to supply your own controls.

Theming

The components are styled with CSS variables (--tk-*, Tokyo Night by default). Override them on a parent element to reskin — e.g. --tk-bg, --tk-border, --tk-fg, --tk-blue. The Terminal additionally takes an xterm theme option.

Tip

These are the same building blocks the demo playground is made of. Richer browser chrome (tabs, history) is on the roadmap; the core terminal and preview views are here today.