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]>
133 lines
8.6 KiB
Markdown
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.
|