# UiNode

> Talos 0.6 · Since 0.6

One node of a screen description.

This is the frozen wire contract between the TSX compiler and the engine: `{ format: 1, screen, root }`, with a node carrying its role, its identity, its classes, its data attributes, its text and its children. It is deliberately data rather than a builder — a screen an agent can print, diff and post is a screen an agent can debug.

## Signature

```ts
export interface UiNode {
  role: UiRole
  /**
   * Stable author id, flat within the screen. `GET /ui` reports it qualified: `pause:resume`.
   *
   * May not contain `/` or `:` — those are the id grammar's own separators, so an id using one
   * could forge a sibling's derived id. The engine refuses it and falls back to the derived
   * form rather than letting two nodes answer to one name.
   */
  id?: string
  /** Diffing key. A keyed child keeps its identity across a rebuild even if its position moves. */
  key?: string
  classes?: string[]
  /** Data attributes without the `data-` prefix. Variants are data, not generated class strings. */
  data?: Record<string, string | number | boolean>
  /** A literal string. Mutually exclusive with `textKey`. */
  text?: string
  /** A `meta.strings` key. §2.11 makes this the default and a literal the marked case. */
  textKey?: string
  /**
   * Per-node props. On the **root** these configure the screen itself — `inputContext`,
   * `modal`, `dismissible`, `clickThrough`, `cursor`, `pausesWorld`. On any other node they are accepted,
   * kept and reported as `ui.screen.props-ignored` until the binding table lands; they are
   * never silently dropped.
   */
  props?: Record<string, unknown>
  children?: UiNode[]
}
```

Import: `import type { UiNode } from "@argus/api"`

## Members

| Member | Signature | Description |
|---|---|---|
| `role` | `role: UiRole` |  |
| `id` | `id?: string` | Stable author id, flat within the screen. `GET /ui` reports it qualified: `pause:resume`. May not contain `/` or `:` — those are the id grammar's own separators, so an id using one could forge a sibling's derived id. The engine refuses it and falls back to the derived form rather than letting two nodes answer to one name. |
| `key` | `key?: string` | Diffing key. A keyed child keeps its identity across a rebuild even if its position moves. |
| `classes` | `classes?: string[]` |  |
| `data` | `data?: Record<string, string \| number \| boolean>` | Data attributes without the `data-` prefix. Variants are data, not generated class strings. |
| `text` | `text?: string` | A literal string. Mutually exclusive with `textKey`. |
| `textKey` | `textKey?: string` | A `meta.strings` key. §2.11 makes this the default and a literal the marked case. |
| `props` | `props?: Record<string, unknown>` | Per-node props. On the **root** these configure the screen itself — `inputContext`, `modal`, `dismissible`, `clickThrough`, `cursor`, `pausesWorld`. On any other node they are accepted, kept and reported as `ui.screen.props-ignored` until the binding table lands; they are never silently dropped. |
| `children` | `children?: UiNode[]` |  |

## Example

The tested fixture `fixtures/app/agent.ts`.

```ts
import type { Behavior } from "@argus/api"

// Sends a nav agent to `props.target` once, then reports arrival through ctx.state so the
// test can read it without polling the crowd directly.
export default {
  init(ctx) {
    ctx.state.arrived = false
    ctx.nav?.goTo(ctx.props.target as [number, number, number])
  },
  update(ctx) {
    if (ctx.nav?.arrived) ctx.state.arrived = true
  },
} satisfies Behavior
```
