130 lines
8.2 KiB
Markdown
130 lines
8.2 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-ui/`](../../wai/wai-ui/).
|
|
|
|
**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-ui` 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-ui/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
|
|
|
|
Export writes a starter project from the tree: the program (`app.oui`, in the canonical
|
|
form of the writer), the tree (`app.json`), the room service's configuration
|
|
(`room.toml`, derived from the room widget: a watch room's source, a show's
|
|
audience inputs) and the relay's (`relay.toml`), the page that renders the program with
|
|
the same renderer and components and their runtime under `vendor/` (each file named with
|
|
its SHA-256 in `vendor.json`), a same-origin front for local development (`serve.mjs`;
|
|
`--up` starts the relay and the room service, their keys made in `.wai-dev/`, which is
|
|
never committed), a check (`check.mjs`, the project's build) that parses `app.oui`
|
|
against the registry with the runtime's own parser, refuses a project without its room
|
|
configuration, with one that does not match the app, or with a runtime file changed, and
|
|
writes `dist/`; and a README and an AGENTS.md for the next person or coding agent. No key
|
|
is in it. The file set is made in Rust by one writer (`wai_ui::export`), whoever asks
|
|
(the page, which zips it; `wai-ui export`; the MCP server), 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-ui mcp` is an MCP server the visitor's own agent runs
|
|
on their machine. Its tools give the agent the registry and the language
|
|
(`create_guide`), check a program (`create_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 (`create_stream`), and write the starter project into the agent's
|
|
workspace (`create_export`).
|
|
|
|
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.
|