open-standards/joule-ui/docs/create.md
Transaction Science 608cd843e9 wai-app: Create's crate, a library with no binary, whose export wai create writes
wai/wai-ui becomes wai/wai-app, the library `wai_app`, with no binary and no `cli`
feature: `wai`, built with its feature `app`, is its command line and its MCP
server. Its served files are named wai-app-* (the WebAssembly package wai_app),
the recordings wai-app-*.json.

- export.rs gives the files of a starter project for a program and the facts its
  project file holds (title, summary, `wai dev`'s mode for the app's room, a watch
  room's file, a show's audience); `wai create --from app.oui` writes them, the
  project file and the licence. The export no longer writes app.json, room.toml,
  relay.toml, package.json, check.mjs or serve.mjs: wai.json names the room, `wai
  dev` runs it and `wai app check` checks the project. Its README and AGENTS.md
  say so, and the README its licence.
- runtime.rs reads a Create runtime by its listing (create/wai-app-runtime.json),
  checking each file's SHA-256.
- bridge.rs keeps the bridge on 127.0.0.1 a coding agent streams a program into
  the page through, with a stream helper; the stdio server, its tools and the
  binary's test go (`wai mcp --toolset app` serves them).
- The page's Export hands over app.oui, the program in its canonical form, and the
  command that makes it a project; the ZIP writer and the page's export in
  WebAssembly go. The page names `wai mcp --toolset app` for the coding agent.
- joule-ui/docs/create.md §4 and §5: the export as `wai create --from app.oui`, and
  the agent's tools app_guide, app_check, app_stream and app_export.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 05:13:27 -04:00

133 lines
8.6 KiB
Markdown

# Create: describing an app, and the line language joule-ui takes in
A design note. The normative text is [`SPEC.md`](../SPEC.md) §11 (the line language) and
the conformance pack in [`conformance/lang/`](../conformance/lang/). The WAI side is
[`wai/wai-app/`](../../wai/wai-app/).
**Create** is a page where a person describes a WAI app ("a show for my channel, with
chat and a live poll") and watches it build itself from WAI components while a model
streams its answer. The room it builds is a real room on a room service. The person then
exports it as a starter project.
## 1. The line language onto joule-ui's tree
A model writes OpenUI Lang (the open-source project's line-per-component language, MIT
licensed; see [`NOTICE`](../NOTICE)): one statement per line, `name = Component(arg, ...)`,
names usable before they are defined. joule-ui takes it in the way it takes in A2UI
(`joule-ui-a2ui`): the language is a way to write a joule-ui **widget tree** against a
host's **registry**, and what comes out is an ordinary `Widget`, checked by joule-ui's
own `validate`.
| The language | joule-ui |
|---|---|
| the library | the `Registry`, plus the name of its root widget |
| `Component(a, b, c)` | a `Widget`; the arguments map to props in the order the widget's schema lists (`WidgetSchema::positional`, new in core) |
| the prop named `children` in that order | `Widget::children`; a list of components |
| strings, numbers, booleans, `null`, lists, objects | `Text`, `Number`, `Bool`, (absent), `List`, `Object` |
| a reference to another statement | that statement's value (hoisting) |
| `@Name(args)` in a prop | `PropValue::Action { id: "Name", payload }` |
| `$state`, `Query`, `Mutation`, operators, `? :`, member access, built-in functions, a sequence of actions | not carried: joule-ui puts no expression and no behaviour on the wire. Each is parsed (the grammar is parsed whole) and refused `not-declarative`, the smallest piece dropped |
The crate is `joule-ui-lang`: a streaming parser, an ingest into widget trees validated
against a registry, and a canonical writer back to the language. Every error names the
line and the reason (`unknown-component`, `missing-required`, `type-mismatch`,
`disallowed-child`, `not-declarative`, ...; the table is SPEC §11.8).
## 2. Streaming
The text arrives in chunks. After each chunk the parser gives a **snapshot**: the tree as
far as it can be built, the errors so far, and the names still unresolved.
- **Complete and pending.** A statement is complete once its newline has arrived and the
next non-blank character has too and is not `?` (a line starting `?` would continue a
ternary). Complete statements never change. The last statement is pending: it is
closed for the moment (an open string closed, open brackets closed in reverse order)
and built, so a title shows letter by letter as it streams.
- **What renders before the tree is whole.** The tree starts at `root`. A reference to a
statement not yet written is unresolved: in a list it is left out, in a prop it is
absent. A widget whose required prop is absent only because something has not arrived
is held back, silently, until it can render. So the first line, `root = App([room],
"Friday show")`, renders an empty app at once, and each later line fills it in.
- **How validation works on a stream.** Every widget is validated against the registry
as it is built, by joule-ui's `validate`; a widget that fails is dropped, and its error
is reported once its statement is complete (a pending statement's problems may be gone
by its next chunk). The snapshot's whole tree passes `validate` before it is handed to
a renderer: no snapshot ever holds a widget the registry refuses. At the end of the
stream the held-back cases become errors (`missing-required`, `no-root`), and the
result equals parsing the whole text at once.
## 3. The WAI registry and the renderer
`wai-app` publishes a joule-ui registry of WAI widgets, each backed by the room client
of `joule-moq-web` (the page that joins rooms, verifies every object against its receipt,
watches together and keeps co-performance):
| Widget | What it is |
|---|---|
| `App(children, title)` | the root |
| `Room(children, title)` | a live room: join with camera and microphone, or watch |
| `WatchTogether(source, children, title)` | a watch room: everyone watches one file in sync |
| `Show(children, title, audience)` | a room with co-performance: hosts on camera, an audience that votes |
| `Player(title)` | the room's video, laid out from its signed record |
| `Chat(title)` | the room's chat events, each verified; a participant sends |
| `Poll(question, options, seconds)` | a vote: the room's clock opens the window and signs the selection |
| `Participants()` | who is present, from the room's lifecycle |
| `Receipts()` | this page's sealed interaction log, verified, and the room's verified objects |
| `EnergyMeter()` | what each interaction cost, with how the figure was obtained |
| `Text(text)` | a paragraph |
Each room kind accepts only the widgets its service keeps (a watch room carries co-watch
statements alone, so it accepts no `Chat` and no `Poll`); joule-ui's `AllowedChildren`
says so, and a line that puts a `Poll` in a `WatchTogether` is refused with its line and
`disallowed-child`. An app holds one room.
The renderer (`wai/wai-app/web/`) reconciles each snapshot into the page by key (a
statement's name), so a room already joined is never torn down by the next chunk. Every
action a person fires (join, leave, send a chat, vote, play) goes through
`joule-ui-interaction` in the page's WebAssembly build: authorized, metered (a page has
no meter, so the figure is the estimator's, from the handler's wall time, and labelled
so), and sealed into the page's hash-chained log under a key the page makes. `Receipts`
shows the log and the page's own `verify_chain` of it. A browser handler is asynchronous,
so the interaction crate gains a two-step form of its lifecycle (`begin_interaction`, then
`Pending::finish`) with the same three phases; a denied action yields no `Pending`, so
its handler cannot run.
## 4. Export
The page hands the person the program, `app.oui`, in the canonical form of the writer, and
the command that makes it a project on their machine: `wai create <dir> --from app.oui`
(the WAI toolchain's operation `app.export`). The project is an ordinary WAI project, the
same kind `wai create --template` writes. Its project file, `wai.json`, is written by the
one writer every WAI project file has, and names the program (`"app": "app.oui"`) and the
room `wai dev` runs for it (`"dev": {"mode": "room" | "watch" | "show", ...}`: a watch room's
file, a show's audience inputs). Beside it are the program, the page that renders it with
the same renderer and components, their runtime under `vendor/` (each file with its SHA-256
in `vendor.json`), a README, an AGENTS.md for the next person or coding agent, and
`LICENSE`: every project `wai create` writes is Apache-2.0, and says so. No key is in it.
It runs with `wai dev` (the relay, the room service and the page on one loopback origin, the
keys made in `.wai/`, which is never committed and never served), and `wai app check`
(`app.check`) checks it: `app.oui` against the registry, the room `wai.json` names against
the app's own, and the runtime file by file. The app's files are made in Rust by one module
(`wai_app::export`), and the project file by `wai`'s, whoever asks (`wai create --from`, or
the MCP server's `app_export`), so the project format changes in one place.
## 5. How a page talks to a model
Create spends nobody's money and holds no one's key on a server. Two ways in, and the
page says which is in use:
- **Your own model.** The visitor names an endpoint that streams the common
`chat/completions` server-sent-events format (a model server they run on their own
machine, or a provider they hold a key for), a model name and a key. The page calls
it from the browser. The key stays in the page's memory; it is never stored and never
sent anywhere else.
- **Your coding agent.** `wai mcp --toolset app` is the WAI MCP server the visitor's own
agent runs on their machine, with Create's toolset. Its tools give the agent the registry
and the language (`app_guide`), check a program (`app_check`), stream a program into the
visitor's open Create page through a bridge on `127.0.0.1` the page connects to with a
pairing code (`app_stream`), and write the starter project into the agent's workspace
(`app_export`, as `wai create --from app.oui` does).
A third mode replays a recorded stream, which is how the end-to-end test drives the
page. A model hosted by the site is out of scope until the owner decides it.