# Generated UI v1

Impo publishes interactive cards through `impo_publish_ui`. Rebyte Agents
generate the content. The API does not call a model or render a page. iOS and
Android render the same A2UI v0.9 document using the pinned AGenUI 1.6.0 release.
Web renders the same catalog with React and Astryx components. The existing
`impo.mobile.v1` identifier is retained for compatibility across all three clients.

## Agent contract

- **Spec:** `generatedUIParameters` in `server/src/tools/generated-ui.ts` is the
  tool's JSON schema. It lists the complete supported component catalog.
- **Prompt:** `generatedUIInstructions` explains when to publish a card, data
  binding, action semantics, HTML restrictions and the required text fallback.
  Main Chat and task agents receive both the instructions and tool definition.
- **Tool:** `impo_publish_ui` validates a complete snapshot and returns a
  server-owned surface ID. It has no external side effects. The existing durable
  worker executes it and freezes the receipt before provider delivery.
- **Renderer:** native AGenUI surfaces and the compatible Web renderer display
  the document. A skill is not
  required; a future skill could package authoring examples without changing
  this transport or execution boundary.

The input has `title`, `summary`, `height` (160–720), `components` and `dataModel`.
The catalog `impo.mobile.v1` includes Column, Row, Text, Divider, Button,
TextField, CheckBox, Slider and the custom ImpoHTML component. Components form
one tree rooted at `root`, with unique IDs, no shared children or cycles, at
most 64 nodes and depth 12. Bindings use top-level scalar dataModel keys.
There are at most 32 values and 16 action context values (6 KB after resolving bindings). Input is limited to
96 KB, HTML to 64,000 characters. Unknown properties, URL widgets, local
function calls and caller-supplied styles are rejected. The server supplies
spacing, readable type, touch targets and forest-green buttons.

The successful tool output is:

```json
{
  "kind": "generated_ui",
  "schemaVersion": 1,
  "catalog": "impo.mobile.v1",
  "surfaceId": "server-tool-invocation-id",
  "title": "Trip planner",
  "summary": "Choose a city and ask Impo to plan a trip.",
  "height": 300,
  "document": "A2UI v0.9 JSONL: createSurface, updateDataModel, updateComponents"
}
```

`document` uses catalog ID `urn:impo:catalog:mobile:v1`. A complete example is
in [fixtures/generated-ui-v1.json](fixtures/generated-ui-v1.json). Each tool
call creates one immutable card. Cards appear when a tool result arrives,
while the agent may still be streaming its answer. Partial tool-argument JSON
is never executed. A follow-up reply can publish a new card; patching an old
card and persisting unsent form/game state are outside v1.

## Persistence, replay and interaction

The existing `tool-output-available` SSE chunk carries the output; no new SSE
lifetime or core event semantics are introduced. All clients derive cards
from successful `impo_publish_ui` results. The worker atomically saves each
`dynamic-tool` history part in `message_generated_ui`, keyed by invocation ID
and protected by the message's composite ownership foreign key. History reads
hydrate these records after Rebyte text projection cleanup. Replaying or
opening history does not execute the tool or send an action. Deleting the
owning message cascades to its UI snapshots.

A Button uses `action.event.name` and optional scalar/bound `context`.
Its event must identify this surface and fit in 8 KB. A tap formats the action
name and form values as **ordinary user input**, then uses the existing main
Chat/task message path, authentication, idempotency and subscription. It does
not call an arbitrary server endpoint or native tool. UI callbacks are disabled
while the conversation is busy; stale disposed surfaces cannot submit input.
Form values do not grant authority to access another user's resources.

Web validates the complete document before rendering its allowlisted components.
Native styles are not applied to browser elements. Unsupported versions or
malformed documents display the card's summary. Actions use the account-scoped
outbox and preserve the composer's draft and attachments. Reloading, leaving a
conversation, virtualization or changing the HTML theme can reset unsent local
form/game state; submitted messages remain in history.

## HTML games

ImpoHTML is an Impo-owned AGenUI custom component, not the SDK's unrestricted
Web component. Its HTML/CSS/JavaScript runs in an opaque-origin iframe with
`sandbox="allow-scripts"`. CSP denies networking, external assets, forms and
base URLs. Native navigation policies deny external navigation. Android also
blocks network requests, file/content access and downloads. iOS uses an
ephemeral WKWebsiteDataStore. Web nests the game inside a trusted sandboxed frame
whose CSP restricts child navigation to `about:` documents. Both browser frames
have opaque origins and deny device permissions. No client registers a game-to-host
bridge or injects credentials. Local scripts, inline styles, canvas and inline
data images work; external libraries, account access and durable browser
storage do not. Games must include touch controls and responsive layouts.

## Setup and validation

Run root workspace commands:

```sh
npm run setup:agenui
npm run db:push
npm run build:ios
npm run build:android
npm run test:server
npm run test:rebyte
npm run test:swift
npm run test:android:client
npm run typecheck:web
npm run test:web
npm run build:web
```

`setup:agenui` verifies pinned release SHA-256 digests; native build helpers run
it automatically. Archives live in `.local/vendor/agenui-sdk`; extracted iOS
binaries live in ignored `ios/Vendor/AGenUI`. No binary or signing output is
committed. Regenerate Xcode after adding native sources with `npm run generate:ios`.
The upstream Android release only contains arm64-v8a; unsupported native
libraries produce the card summary instead of crashing the chat.

Focused native tests are `InstantNativeTests/GeneratedUIRenderingTests` and
`ai.impo.ui.GeneratedUIInstrumentedTest`. Run them with:

```sh
IMPO_IOS_TEST_CLASS=InstantNativeTests/GeneratedUIRenderingTests npm run test:ios:app -- --simulator <UDID>
IMPO_ANDROID_TEST_CLASS=ai.impo.ui.GeneratedUIInstrumentedTest npm run test:android:ui
```

They exercise the actual SDK, changed
form values returning through a button, local JavaScript, blocked network
access and blocked origin storage. The Rebyte integration uses isolated
PostgreSQL, real API/worker processes and a provider protocol double. It checks
streaming output, durable replay after cleanup and account isolation. These
checks do not establish physical-device, production-account or remote-model
authoring acceptance. Apply the schema before starting an updated server.

Web tests parse the shared native fixture, exercise all catalog components and
reject invalid trees/bindings/actions. HTTP/SSE fixture tests cover streamed cards,
main/task follow-ups, history replay, idempotency and account isolation. For local
browser acceptance, start `npm run dev:web:fixture -- --generated-ui`, then use
the fixture-mode Web command in [the Web guide](../web/README.md). Verify actual
game interaction and network/storage/navigation isolation in the browser as well
as the generated sandbox markup.
