> ## Documentation Index
> Fetch the complete documentation index at: https://docs.surfacearea.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Give a world a UI

> Every schema world with routes can serve a UI. Declare it in connector.toml, let compile generate it from the contract or bring a frontend built with any framework, then open a session with the ui and browser surfaces.

A world with routes can serve a page over them. The page is one more view of the same session
state: a row created in a form is the row the API lists and the row the grader scores.

The shipped connector templates declare a UI, so a world made from one has a page as soon as it
compiles. A world you wrote yourself gets one with a single command, or serves a frontend you built
with any framework: the world holds static files, and the host serves them.

## Declare it

`[ui]` lives in `connector.toml`, the file that already declares the world's routes.

```toml theme={null}
[ui]
static = "ui/static"   # the directory holding index.html and its assets
entry = "/"            # the page an agent lands on first
generated = true       # compile writes the directory from the contract
```

| Key | Default | Meaning |
| - | - | - |
| `static` | required | Directory inside the world with `index.html` at its root: the generated page, or your build's output. Not under `dist`, `build` or `outputs`, which `bench push` drops. |
| `entry` | `/` | Path the agent lands on first. |
| `api_path` | `/__world-api` | Same-origin prefix the page calls the API at. |
| `generated` | unset | `true`: compile writes the directory and refuses to overwrite a page it did not write. `false`: hand-written, compile never touches it. Unset: compile writes the directory only when it holds no page yet. |

`[browser]` is optional and defaults to enabled with a 1280x800 viewport; `enabled = false`
turns the browser surface off for the world.

## Generate it

```bash theme={null}
gateway worlds schema ui init ./vendor-world
```

`ui init` adds the `[ui]` section when `connector.toml` has none and compiles. Compile writes
three files into `ui/static/`:

| File | What it is |
| - | - |
| `index.html` | The generated page: plain HTML, no framework, no CDN, so it needs no build step. |
| `app.js` | The app. The same file for every world. |
| `contract.json` | This world: every entity with its fields, types, keys and relationships, the route that lists, reads, creates, updates and deletes it, and the API's envelope and paging keys. |

`gateway worlds schema compile` rewrites the three files whenever the contract changes, so the
page shows the current schema. `ui init` needs a `connector.toml`, which declares the routes the
page calls.

The page renders what the routes serve:

* **Home** lists every entity with the route behind each verb.
* **List** is the entity's `GET` list route, with the world's own query parameters as filters and
  the world's paging (cursor or offset) for the next page.
* **Detail** is the `GET` by id. A foreign key links to the related entity's detail view.
* **New** and **Edit** are the `POST` and `PATCH`/`PUT` routes. The form asks for the fields the
  route's body mapping names, or every field the server does not mint itself. Enums are selects,
  booleans and numbers are typed, nested objects are JSON. A handler route may carry a `body`
  mapping too (`body = { channel_id = "channel", text = "text" }`): the handler reads the request
  itself, the mapping tells the form which fields to ask for and what the vendor calls them. A
  `2xx` that carries no row (Slack's `{"ok": false, "error": ...}`) keeps the form up and shows
  the answer.
* **Delete** is the `DELETE` route.
* Every view prints the request it made and the status it got, so an agent reading the page sees
  the API call behind it.

## Bring your own frontend

Any frontend that builds to static files is a world UI: React, Vue, Svelte, Angular, Solid, plain
HTML, a WASM app. The host serves files and rewrites one API prefix; it does not care what produced
them.

1. Set `generated = false` so compile never touches the directory.
2. Build into `static` with `index.html` at its root: Vite `build.outDir`, Create React App
   `BUILD_PATH`, Angular `outputPath`, all pointed at `ui/static`; a Next.js `output: "export"`
   build is copied there from `out/`. Keep the build's default base of `/`.
3. Call the API at `api_path` on the page's own origin with plain `fetch`, no credential.

That is all. The page is served at the root of its own origin, on the host and locally, so absolute
asset URLs (`/assets/index-3c4d.js`) resolve. A path that names no file and has no extension
returns `index.html`, so a client-side router's deep links load. Hashed files under `assets/` are
cached as immutable; everything else is revalidated on every request.

Export a frontend that renders on a server (Next.js without `output: "export"`, a Django or Rails
view) as static files, or keep the server outside the world and point it at the session's API URL.

The `slack` template is a worked example. Its `ui/static` is a Vite + React build of a Slack
client, copied in unchanged, and `connector.toml` declares the routes that client calls the way it
calls them: every Web API method accepts `POST` as well as `GET` (`methods = ["GET", "POST"]`),
`/events` answers the page's `EventSource` with the events since a sequence number, and
`/avatars/{name}` serves the profile images the seed names as `/avatars/ada_72.png`. Assets a
frontend loads through `api_path` (avatars, files, thumbnails) are routes, not files under `static`:
the host rewrites the prefix onto the world's API, so a `[[operations]]` handler has to answer
them.

## How the page authorizes

The page holds no credential. A browser cannot set an `Authorization` header on a page load, so the
World Host admits the UI by cookie:

1. The first page load carries the session token as `?token=`, the URL `session open` prints as
   `ui.entry`.
2. The host answers with an `HttpOnly` cookie and a redirect to the same page without the token.
3. Every request after that (pages, assets, calls to `api_path`) rides the cookie. The host strips
   `api_path` and answers the route as it would for a caller holding the session token.

Each `api_path` call lands in the session's call log with `via: "ui"`, so a grader can require
that a call went through the page's door (see [the call log](/worlds/sessions#look-at-what-the-world-holds)).
It records the door, not the client: a script holding the session token can call the same prefix.

Under `gateway worlds serve` the page has its own port (`--ui-port`, by default the one after
`--port`) with the same layout as the host: the page at `/` and its `api_path` calls answered with
the local pin. The same files are at `/__ui/` on `--port`. The bare routes still require the key, and
the prefix answers the page only: a browser call from another site is refused. Only the UI port's
prefix counts as `via: "ui"`; the same prefix on `--port` is the API origin and logs `api`.

## Open a session with it

```bash theme={null}
gateway worlds session open vendor-world --task default --surface ui,browser
```

The descriptor adds `ui.url`, `ui.entry` (with the one-time token) and `browser.wsUrl`. `ui`
without `browser` serves the page for a browser you run yourself; `browser` needs `ui`.

```bash theme={null}
gateway worlds session browse <sessionId>                    # open the entry page, print title and text
gateway worlds session browse <sessionId> --path /#/accounts --screenshot page.png
```

`browse` drives the hosted browser when the session has one and a local Playwright otherwise, and
prints what the agent would read. From TypeScript, [Drive a world UI](/sdk-ts/browser) covers the
same session.

A schema world that declares `[ui]` opens on the World Host with `tools`, `api`, `ui` and, when
asked for, `browser`.

## Verify

```bash theme={null}
gateway worlds serve ./vendor-world --port 8080                   # the ui on 8081
curl -s http://127.0.0.1:8081/ | head -2                          # the page
curl -s http://127.0.0.1:8081/contract.json | jq .entities        # what the generated page renders
curl -s http://127.0.0.1:8081/__world-api/accounts                # the page's list call
```

Ship the checks as [world tests](/worlds/tests): an HTTP case on `/__ui/` and one on
`/__world-api/<route>` with `"auth": false`, and `gateway worlds test` runs them with the rest.

## Where to go next

* [Spin worlds up and down](/worlds/sessions) for the session the page belongs to.
* [Drive a world UI](/sdk-ts/browser) to hand the page to a model as computer-use tools.
* [Native computer use](/worlds/computer-use) to hand it to Claude's or OpenAI's own pixel-based tool.
* [The files a world is made of](/worlds/contract) for the rest of `connector.toml`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.