| # Developing a {name} application | |
| 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. | |
| ## What the runtime is | |
| `{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. | |
| ## Application layout | |
| ```text | |
| 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` | |
| ```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. | |
| ## The smallest application | |
| ```js | |
| 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"), | |
| ); | |
| } | |
| } | |
| ``` | |
| ## Built-in modules | |
| {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. | |
| ## Rules that are easy to get wrong | |
| - **Host methods are snake_case** (`items_center`, `on_click`, `text_sm`, | |
| `Button.new("id")`), exactly as in Rust. Your own code is camelCase. | |
| - **Construct with `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. | |
| - **Assign state in `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. | |
| - **Call `cx.notify()` after changing state** in an event handler or task. | |
| Calling it inside `render` throws. | |
| - **Elements are single-use.** Build them inside `render`; never store one on | |
| `this` or reuse it in two places. | |
| - **Ids must be stable and unique among siblings** for interactive controls: | |
| `Button.new("save")`, `Checkbox.new(`todo-${item.id}`)`. | |
| - **Controls are controlled.** `Checkbox`/`Switch` take `.checked(value)` and | |
| report `.on_change((checked, cx) => …)`; store the new value and notify. | |
| - **Text state reports its own events.** Create an `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. | |
| - **Overlays are window calls made from an event handler or a task:** | |
| `window.open_dialog(() => element)`, `window.open_sheet(...)`, | |
| `window.push_toast({ title })`. `init` runs before the window can show one, | |
| so they throw there. | |
| - **Base controls draw nothing by themselves.** `Button.new("x")` has no | |
| padding, colour or radius; every visual decision is written in the script. | |
| - **Use theme tokens for colour**: `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. | |
| - A number length is pixels; a string length is `"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. | |
| - Use `.when(condition, (el) => el.…)` to keep a builder chain in one piece. | |
| ## State, events and asynchrony | |
| - `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: | |
| ```json | |
| "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"] }`. | |
| - An application with a manifest keeps its data under its `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: | |
| ```js | |
| 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}`. | |
| - Heavy or rarely used code belongs in modules loaded with dynamic | |
| `import()` from the handler that needs it; keep the entry module light. | |
| ## Workflow | |
| ```bash | |
| {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. |