These instructions were generated by {name} {version} for coding agents and people writing an application for it. They describe the runtime that generated them; regenerate them with {name} agents --dir <directory> after upgrading.
{name} runs JavaScript applications natively on GPUI. There is no DOM, no HTML, no CSS, no browser and no Node.js. A script describes its interface with builder calls; Rust turns that description into real GPUI elements and renders it on the GPU. Do not reach for web APIs, React, JSX, npm packages that touch the DOM, or document/window.addEventListener — none of them exist.
The language is modern JavaScript (ES2022 modules) on QuickJS. Write .js files with JSDoc types; there is no build step and no bundler.
my-app/ ├── gpui-shell.json # identity, entry, dependencies and capabilities ├── main.js # entry module: default-exports a View subclass ├── jsconfig.json # editor type checking (written on first run) ├── gpui-kit.d.ts # generated declarations — never edit, never commit └── icons/ # assets, resolved against the application root
{name} runs one application per process. An installed application is a directory under {home}: {name} run clock runs {home}/clock. Any other directory runs with {name} run -d <directory>. To ship the application as a macOS .app or a Linux package, place this directory as app beside the {name} executable (or Contents/Resources/app in a macOS bundle) and start the executable with no arguments.
gpui-shell.json{ "id": "com.example.clock", "name": "Clock", "version": "1.0.0", "shell-version": "{version}", "entry": "main.js", "capabilities": { "storage": true } }
id, name and entry are required. id is the application‘s identity and keys its data directory, so keep it stable. shell-version is the oldest runtime the application supports. window is the size the window opens at, in logical pixels — "window": { "width": 360, "height": 520 } — and the host’s default when absent. Unknown fields are rejected.
import { View, div } from "gpui-kit"; import { v_flex, Button } from "gpui-base"; export default class Counter extends View { init(_props, _cx) { this.count = 0; } render(cx) { return v_flex() .size_full() .items_center() .justify_center() .gap(12) .bg(cx.theme().colors.background) .child(div().text_color(cx.theme().colors.foreground).child(`Count: ${this.count}`)) .child( Button.new("increment") .px(14) .h(32) .rounded(6) .bg(cx.theme().colors.primary) .text_color(cx.theme().colors.primary_foreground) .on_click((_event, cx) => { this.count += 1; cx.notify(); }) .child("Increment"), ); } }
{modules}
Import only these names, plus relative modules (./ui.js) and the bare dependency names declared in gpui-shell.json. The exact API — every element, style method, argument type and component — is in gpui-kit.d.ts, generated by {name} types <directory>. Read it before writing code and treat it as the source of truth over this guide.
items_center, on_click, text_sm, Button.new("id")), exactly as in Rust. Your own code is camelCase.Type.new(...) in every module — Button.new("save") from "gpui-base" and from "gpui-component" alike — never new Type(...). The arguments differ by module: gpui-base's InputState.new({ placeholder }) takes options, gpui-component's InputState.new(placeholder, value) takes them in order. gpui-kit.d.ts has each signature.init, not as class fields. View's constructor calls init from inside super(); a class field declaration runs afterwards and overwrites it with undefined.render(cx) returns one element (or a string, or a retained view). It runs only when the view is invalidated, not every frame.cx.notify() after changing state in an event handler or task. Calling it inside render throws.render; never store one on this or reuse it in two places.Button.new("save"), Checkbox.new(todo-${item.id}).Checkbox/Switch take .checked(value) and report .on_change((checked, cx) => …); store the new value and notify.InputState in init, pass it to Input.new(state) from the same module, and subscribe with state.on("change" | "submit" | "focus" | "blur", (event, cx) => …); submit is Enter, and event.shift says whether Shift was held. A TextareaState inserts the line break before it reports submit; gpui-base‘s has set_submit_on_enter(true) to make Enter only submit (Shift+Enter still inserts a line break, and still reports submit with shift: true). Both modules’ InputState and TextareaState answer on the same way, but each module‘s Input takes only its own module’s state.window.open_dialog(() => element), window.open_sheet(...), window.push_toast({ title }). init runs before the window can show one, so they throw there.Button.new("x") has no padding, colour or radius; every visual decision is written in the script.cx.theme().colors.surface, cx.theme().spacing.md. Read them at the use site; do not destructure the theme. #rrggbb literals bypass the theme and break dark mode."auto", "50%", "12px" or "1rem", as far as the Rust signature allows..bg(...) takes a colour or a Background from "gpui-kit": .bg(Background.linear_gradient(180, Background.stop(top, 0), Background.stop(bottom, 1))) — stop positions are fractions from 0 to 1. .shadow([...]) takes CSS-like records, { x, y, blur, spread, color, inset }, with CSS blur and CSS stacking (first on top); [] clears.svg(path) and image(path) resolve against the application root, not the importing file. Keep assets in a directory named after the application..when(condition, (el) => el.…) to keep a builder chain in one piece.cx.spawn(async (cx) => { … }) runs async work whose cx survives await; cx.sleep(ms), cx.timer.after and cx.timer.every schedule on the UI thread. There is no background thread for script.localStorage / sessionStorage persist per application (needs "storage": true).TextEncoder / TextDecoder (UTF-8) are globals and need no grant.fetch (HTTP), WebSocket from "websocket", fs/promises, clipboard and process.run exist but are denied unless the manifest grants them. Ask for the narrowest grant that works:"capabilities": { "fs": { "read": ["${pluginDir}"], "write": ["${dataDir}"] }, "network": { "http": [{ "host": "api.example.com", "methods": ["GET"], "path_prefixes": ["/v1/"] }] }, "storage": true, "clipboard": { "write": true } }
fs roots may name the user's files with ${homeDir}, as in "read": ["${homeDir}/.config/my-app"], or be absolute paths.os.homedir(), process.dataDir (the ${dataDir} directory) and import.meta.url (a file: URL) tell an application where things are. process.env holds common variables (HOME, USER, HOSTNAME, LANG, TZ, PATH, EDITOR, DISPLAY, HTTP_PROXY, the XDG directories…) by default, matched regardless of case; any other variable must be listed in "process": { "env": ["MY_APP_MODE"] }.id, whether run with -d, installed or packaged.process.run(command, args, { cwd, timeoutMs }) waits for a command to finish (30 s unless timeoutMs says otherwise). For one that runs longer, reads stdin, or should be read as it works — an AI command line in JSON-lines mode — use process.spawn. Same grant (capabilities.fs.execute), same empty environment; handlers run on the UI thread with a cx, exit fires once after the last output, and the child is killed when the view goes away, the application reloads or the window closes:import process from "process"; const child = process.spawn("claude", ["-p", prompt, "--output-format", "stream-json"], { cwd: projectDir, // inside capabilities.fs.read lines: true, }); child .on("stdout", (line, cx) => { if (!line.trim()) return; this.events.push(JSON.parse(line)); cx.notify(); }) .on("exit", ({ code, signal, error }, cx) => { this.running = false; this.failure = error ?? (code === 0 ? null : `exited with ${code ?? signal}`); cx.notify(); }); // child.kill() to stop it; { stdin: "pipe" } and child.write(text) / child.end() to talk to it.
console.log / console.error go to the terminal running {name}.import() from the handler that needs it; keep the entry module light.{name} types -d my-app # write gpui-kit.d.ts — do this first {name} check -d my-app # load, render and materialize once; exit 1 on error {name} dev -d my-app # run it, reloading on save {name} list # what is installed under {home} {name} run my-app # run the installed copy
After every change, run {name} check -d <directory> and fix what it reports before declaring the work done. check does not exercise layout, paint, later interactions or asynchronous states, so also run the application with {name} dev when behaviour matters.
To install an application, copy or link its directory into {home}; the directory name is the name {name} runs it by. An installed application keeps its data under its manifest id, so a reinstall does not lose it.