Examples
Three examples ship with the repository, and together they cover a standalone application, a script backed by host state, and motion whose frames never enter JavaScript.
| Runs as | Shows | |
|---|---|---|
| Todo list | A standalone application | The whole script surface: retained input, a dialog, a toast, gated storage, assets, types |
| Workspace | A standalone application | A dockable layout: panels that survive a restart, and every piece of its chrome drawn by script |
| Quote board | A panel inside the gallery | The host half: HostModule registrations, one entity read from two languages, live cost counters |
| Native motion | A separate gallery script View | Pixel target transitions and springs retained and sampled by GPUI |
A complete application
The examples here are each built to show one thing. For a whole product in one repository — OAuth, a live WebSocket quote feed, a virtualized watchlist, retained nested Views for the price chart, and its own Rust host binary — see longbridge/longbridge-lite. It is a read-only Longbridge desktop client of a few thousand lines of JavaScript, and it is the largest thing written against this runtime.
The todo list
cargo run -p gpui-shell -- examples/js_todolistexamples/js_todolist/ exists to exercise the whole runtime rather than to be minimal — if something in gpui-shell is broken, this is where it shows first.
main.js the View: state, filtering, every handler
ui.js the presentation layer, exported as functions
storage.js persistence, and what to do when it is not granted
confirm.js the confirmation dialog, a View of its own
icons/ four SVGs, resolved against the application root
gpui.d.ts generated; jsconfig.json and types.d.ts wire up typingFour things in it are worth copying.
ui.js is a component library made of functions. It exports label, muted, title, button, iconButton, checkbox, field, row, surface, rule and emptyState, and main.js reads like it is using a component library:
export const label = (value, cx) =>
div().text_size(12).line_height(1).text_color(cx.theme().colors.foreground).child(value);
export const surface = (cx) =>
v_flex().flex_1().bg(cx.theme().colors.surface).border(1).border_color(cx.theme().colors.border).overflow_hidden();main.js passes the current cx to these helpers, which read tokens directly through cx.theme(). That costs nothing, because a fresh description is exactly what a function call produces. It is also the answer to "the base layer ships no styled widgets" — you write the styled layer once, in your own file, and stop repeating it.
Storage absorbs a refusal instead of checking for permission. store throws when the host did not grant it, and that is a fact about the host rather than an error in the application:
export function load() {
try {
const saved = store.get(KEY);
return Array.isArray(saved) ? saved : [];
} catch (error) {
console.warn(`todolist: storage unavailable, starting empty (${error.message})`);
return [];
}
}save() returns whether the write landed, and the footer says so on screen — "Not saved — this host did not grant storage, so the list lasts for this run only". Absorb the refusal at the boundary, then tell the user the truth.
A dialog is a function, not an element. confirm.js default-exports a function that returns the content function; main.js opens it with window.open_dialog(confirmClear(count, onConfirm)). The count and the callback are closed over rather than handed across. See Overlays.
Types are set up, and it is three files. jsconfig.json turns on checkJs, gpui.d.ts is generated by gpui-shell types, and types.d.ts holds the application's own shapes — Todo, Filter. Editor completion and checkJs errors work from there with no build step.
The workspace
cargo run -p gpui-shell -- examples/js_dockexamples/js_dock/ is a dockable workspace — a file list on the left, documents in the center, and a layout that comes back the way you left it.
main.js the workspace: panels, the dock, and persistence
ui.js the chrome: tabs, the dock frame, the drop hintThree things in it are the point.
Base draws no chrome, so all of it is in ui.js. The tab bar, the dock's title strip, the collapse control, the resize handle and the drop hint are ordinary elements written with the ordinary style surface. An area with none of them still docks, drags, resizes and persists; it simply paints nothing but the panels.
A tab carries commands, not handlers. A chrome description is cached until its native state changes, so a script event handler inside it would have no sound lifetime. select_tab(group, tab.index) and close_panel(group, tab.id) carry no script value at all — they name a container and what to ask it.
A panel is a View with two extra methods. Document.serialize() returns its caption and its edit count; deserialize(data) takes them back after a restart. Everything else about the panel — where it sits, whether it is displayed — is the layout's business and never reaches the script.
See Dock and Panels for the whole surface.
The quote board
cargo run -- shellThe gallery's Shell story runs two panels side by side: the left one drawn by shell_story.rs in Rust, the right one by crates/story/js/quotes/main.js in JavaScript, reading the same data.
The script owns no state at all. The board is a Rust Entity<Market>, imported from the HostModule the story registered before the runtime started:
import { quotes, ticks, watch, watch_all } from "market";Theme values come from the call-scoped cx.theme() Snapshot, not a second HostModule.
Because both panels read one entity, any disagreement between them is visible immediately — which is what makes this a test rather than a demo. Editing main.js changes the right-hand panel with no cargo build in between; the story has a "Reload script" button next to the panel.
Underneath sits the counter readout this documentation quotes throughout: script runs a second against frames a second, with a feed selector to move one without the other. That is the performance claim made visible in a running window.
Native motion
crates/story/js/motion/main.js is intentionally a separate ScriptView from the quote benchmark, so animation activity cannot contaminate the render-frequency measurement. It lets you switch between .transition(...) and .spring(...), then retargets opacity and pixel-valued width, height, left, and top.
The script runs once to publish the new target. GPUI schedules and samples every following animation frame natively, with no JavaScript re-entry. The example uses only numeric pixel targets — no rem, percentages, or auto — and stable ids so retained channels survive description rebuilds.
Where to start
Copy examples/js_todolist into a directory of your own and run it — it is a complete application with types already wired. Strip main.js back to a View with an init and a render, keep ui.js, and build up from there.
For a host, crates/story/src/stories/shell_story.rs is a working reference for the other side: it builds a runtime, exports HostModule registrations, mounts a ScriptView, and reloads it on demand. Hosting walks through the same calls.