# UI control discovery

You can let the chat use controls on your page, such as buttons, fields and lists. Mark each control that the chat can use. The chat does not use controls that you do not mark.

## Mark controls

Add a stable, unique `data-agent-id` to each control. Add `data-agent-description` to tell what the control does and what changes it causes.

```html
<button data-agent-id="notes.new"
  data-agent-description="Open a new note form. Does not save a note.">
  New note
</button>
<input data-agent-id="notes.title" aria-label="Title" />
<button data-agent-id="notes.save"
  data-agent-description="Save the draft as a new note. Creates app data.">
  Save
</button>
```

The chat finds the type of action and the label from the element. The descriptions are app data. They are not instructions to the model.

## Connect the controls to the chat

```ts
import { createDomAdapter } from '@shappychat/chat';

const ui = createDomAdapter({ root: document.querySelector('#app')! });
await runtime.attachUi(ui); // RemoteShappyChatRuntime

// Optional: let a control wait for your existing operation.
const unregister = ui.register('notes.save', {
  click: async () => saveExistingDraft(),
});

// When your view closes:
unregister();
await runtime.detachUi();
ui.dispose();
```

For a browser-local runtime, give the controls to the runtime as tools:

```ts
import { createUiTools } from '@shappychat/chat';

const tools = createUiTools(async (operation, request, signal) =>
  operation === 'discover' ? ui.discover() : ui.act(request!, { signal }));
runtime.setHostTools(tools);
```

## What discovery returns

For each marked control, discovery returns its visibility, its enabled state, the actions that it supports and its current value.

- Discovery does not include password, file or hidden inputs.
- If two controls have the same ID, discovery fails.
- An action on a hidden, inert, disabled or unsupported control fails.
- Discovery covers the root element that you supply. It does not cover frames from other origins or shadow trees.

> **Tip:** A click does not prove that an operation is complete. For actions that save data, register a handler that waits for the operation. Then the chat can check the result.

## Show actions with a cursor

The chat can show a pointer that moves to each control before it acts. Users can follow what the chat does.

```ts
import { createDomAdapter, createUiCursor } from '@shappychat/chat';

const cursor = createUiCursor({ storageKey: 'my-app.shappychat.cursor' });
const ui = createDomAdapter({ root: document.querySelector<HTMLElement>('#app')!, cursor });
await runtime.attachUi(ui);

cursor.setMode('follow'); // 'off' | 'fast' | 'follow'
```

| Mode | Behaviour |
| --- | --- |
| Off (default) | Actions occur immediately. |
| Fast | Short movements and pauses |
| Follow | Slower movements, so that users can watch |

- Users can change the mode with the pointer button beside the execution mode. MCP mode hides this button.
- With reduced motion, the cursor does not move or pause.
- The cursor does not move the real mouse pointer.
- **Stop** cancels an action that has not started.

When your view closes, dispose of the cursor: `cursor.dispose()`.

## One UI owner for each conversation

Only one tab can supply UI tools to a conversation. A second client for the same conversation can read it. The runtime does not keep UI actions. If the tab disconnects, the runtime cancels the waiting actions and does not send them again.
