blob: e407d3db05198534d7021fb3151cf43c8d424b89 [file] [view]
# 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.