@ui-sdk/react 0.0.0-stage → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +10 -2
  3. package/dist/accordion.d.ts +331 -0
  4. package/dist/accordion.js +25 -0
  5. package/dist/agent.d.ts +2 -0
  6. package/dist/agent.js +8 -0
  7. package/dist/alert-dialog.d.ts +56 -0
  8. package/dist/alert-dialog.js +45 -0
  9. package/dist/async-options.d.ts +22 -0
  10. package/dist/async-options.js +56 -0
  11. package/dist/autocomplete.d.ts +102 -0
  12. package/dist/autocomplete.js +195 -0
  13. package/dist/avatar.d.ts +301 -0
  14. package/dist/avatar.js +17 -0
  15. package/dist/button.d.ts +21 -0
  16. package/dist/button.js +45 -0
  17. package/dist/calendar-context.d.ts +10 -0
  18. package/dist/calendar-context.js +9 -0
  19. package/dist/calendar.d.ts +80 -0
  20. package/dist/calendar.js +257 -0
  21. package/dist/carousel.d.ts +52 -0
  22. package/dist/carousel.js +151 -0
  23. package/dist/checkbox.d.ts +24 -0
  24. package/dist/checkbox.js +98 -0
  25. package/dist/children.d.ts +38 -0
  26. package/dist/children.js +63 -0
  27. package/dist/collapsible.d.ts +302 -0
  28. package/dist/collapsible.js +19 -0
  29. package/dist/color-picker.d.ts +72 -0
  30. package/dist/color-picker.js +133 -0
  31. package/dist/color.d.ts +2 -0
  32. package/dist/color.js +22 -0
  33. package/dist/combobox.d.ts +152 -0
  34. package/dist/combobox.js +338 -0
  35. package/dist/command.d.ts +88 -0
  36. package/dist/command.js +233 -0
  37. package/dist/controller-target.d.ts +3 -0
  38. package/dist/controller-target.js +59 -0
  39. package/dist/date-input.d.ts +19 -0
  40. package/dist/date-input.js +58 -0
  41. package/dist/date-picker.d.ts +47 -0
  42. package/dist/date-picker.js +310 -0
  43. package/dist/date.d.ts +2 -0
  44. package/dist/date.js +60 -0
  45. package/dist/dialog-parts.d.ts +23 -0
  46. package/dist/dialog-parts.js +142 -0
  47. package/dist/dialog.d.ts +57 -0
  48. package/dist/dialog.js +32 -0
  49. package/dist/drawer.d.ts +70 -0
  50. package/dist/drawer.js +158 -0
  51. package/dist/editor-context.d.ts +4 -0
  52. package/dist/editor-context.js +9 -0
  53. package/dist/field-context.d.ts +47 -0
  54. package/dist/field-context.js +87 -0
  55. package/dist/field.d.ts +49 -0
  56. package/dist/field.js +169 -0
  57. package/dist/file-input.d.ts +46 -0
  58. package/dist/file-input.js +170 -0
  59. package/dist/form-inputs.d.ts +5 -0
  60. package/dist/form-inputs.js +22 -0
  61. package/dist/gesture.d.ts +1 -0
  62. package/dist/gesture.js +16 -0
  63. package/dist/history.d.ts +2 -0
  64. package/dist/history.js +12 -0
  65. package/dist/hover-card.d.ts +33 -0
  66. package/dist/hover-card.js +84 -0
  67. package/dist/image-cropper.d.ts +333 -0
  68. package/dist/image-cropper.js +37 -0
  69. package/dist/image.d.ts +2 -0
  70. package/dist/image.js +6 -0
  71. package/dist/input.d.ts +22 -0
  72. package/dist/input.js +51 -0
  73. package/dist/label.d.ts +294 -0
  74. package/dist/label.js +9 -0
  75. package/dist/list-items.d.ts +15 -0
  76. package/dist/list-items.js +39 -0
  77. package/dist/list-text.d.ts +4 -0
  78. package/dist/list-text.js +15 -0
  79. package/dist/listbox.d.ts +56 -0
  80. package/dist/listbox.js +122 -0
  81. package/dist/menu.d.ts +116 -0
  82. package/dist/menu.js +426 -0
  83. package/dist/menubar-context.d.ts +4 -0
  84. package/dist/menubar-context.js +7 -0
  85. package/dist/menubar.d.ts +11 -0
  86. package/dist/menubar.js +32 -0
  87. package/dist/meter.d.ts +312 -0
  88. package/dist/meter.js +30 -0
  89. package/dist/navigation-menu.d.ts +61 -0
  90. package/dist/navigation-menu.js +169 -0
  91. package/dist/number-input.d.ts +44 -0
  92. package/dist/number-input.js +185 -0
  93. package/dist/number.d.ts +2 -0
  94. package/dist/number.js +40 -0
  95. package/dist/observe.d.ts +2 -0
  96. package/dist/observe.js +8 -0
  97. package/dist/otp-input.d.ts +29 -0
  98. package/dist/otp-input.js +94 -0
  99. package/dist/overlays.d.ts +26 -0
  100. package/dist/overlays.js +82 -0
  101. package/dist/pagination.d.ts +336 -0
  102. package/dist/pagination.js +71 -0
  103. package/dist/password-input.d.ts +23 -0
  104. package/dist/password-input.js +65 -0
  105. package/dist/popover.d.ts +54 -0
  106. package/dist/popover.js +151 -0
  107. package/dist/popup.d.ts +41 -0
  108. package/dist/popup.js +180 -0
  109. package/dist/preferences.d.ts +35 -0
  110. package/dist/preferences.js +93 -0
  111. package/dist/primitive.d.ts +2 -0
  112. package/dist/primitive.js +14 -0
  113. package/dist/progress.d.ts +312 -0
  114. package/dist/progress.js +30 -0
  115. package/dist/radio.d.ts +22 -0
  116. package/dist/radio.js +86 -0
  117. package/dist/rating.d.ts +20 -0
  118. package/dist/rating.js +94 -0
  119. package/dist/render-element.d.ts +35 -0
  120. package/dist/render-element.js +103 -0
  121. package/dist/resizable.d.ts +302 -0
  122. package/dist/resizable.js +23 -0
  123. package/dist/scroll-area.d.ts +324 -0
  124. package/dist/scroll-area.js +49 -0
  125. package/dist/segmented-control.d.ts +24 -0
  126. package/dist/segmented-control.js +78 -0
  127. package/dist/segmented-input.d.ts +15 -0
  128. package/dist/segmented-input.js +48 -0
  129. package/dist/select.d.ts +125 -0
  130. package/dist/select.js +330 -0
  131. package/dist/separator.d.ts +290 -0
  132. package/dist/separator.js +9 -0
  133. package/dist/shortcuts.d.ts +32 -0
  134. package/dist/shortcuts.js +61 -0
  135. package/dist/slider.d.ts +38 -0
  136. package/dist/slider.js +163 -0
  137. package/dist/switch.d.ts +15 -0
  138. package/dist/switch.js +39 -0
  139. package/dist/tabs.d.ts +331 -0
  140. package/dist/tabs.js +28 -0
  141. package/dist/text-control.d.ts +22 -0
  142. package/dist/text-control.js +24 -0
  143. package/dist/text-match.d.ts +2 -0
  144. package/dist/text-match.js +8 -0
  145. package/dist/textarea.d.ts +10 -0
  146. package/dist/textarea.js +48 -0
  147. package/dist/time-input.d.ts +19 -0
  148. package/dist/time-input.js +63 -0
  149. package/dist/timeline.d.ts +327 -0
  150. package/dist/timeline.js +31 -0
  151. package/dist/toast-parts.d.ts +63 -0
  152. package/dist/toast-parts.js +144 -0
  153. package/dist/toast.d.ts +2 -0
  154. package/dist/toast.js +22 -0
  155. package/dist/toggle.d.ts +23 -0
  156. package/dist/toggle.js +76 -0
  157. package/dist/token-input.d.ts +35 -0
  158. package/dist/token-input.js +100 -0
  159. package/dist/toolbar-context.d.ts +6 -0
  160. package/dist/toolbar-context.js +16 -0
  161. package/dist/toolbar.d.ts +38 -0
  162. package/dist/toolbar.js +63 -0
  163. package/dist/tooltip.d.ts +39 -0
  164. package/dist/tooltip.js +108 -0
  165. package/dist/tree.d.ts +51 -0
  166. package/dist/tree.js +150 -0
  167. package/dist/trigger-menu.d.ts +83 -0
  168. package/dist/trigger-menu.js +167 -0
  169. package/dist/use-primitive.d.ts +178 -0
  170. package/dist/use-primitive.js +674 -0
  171. package/dist/waterfall.d.ts +80 -0
  172. package/dist/waterfall.js +108 -0
  173. package/docs/README.md +89 -0
  174. package/docs/agents.md +1523 -0
  175. package/docs/components/accordion.md +1391 -0
  176. package/docs/components/alert-dialog.md +850 -0
  177. package/docs/components/autocomplete.md +1963 -0
  178. package/docs/components/avatar.md +252 -0
  179. package/docs/components/button.md +444 -0
  180. package/docs/components/calendar.md +1687 -0
  181. package/docs/components/carousel.md +1244 -0
  182. package/docs/components/checkbox.md +1069 -0
  183. package/docs/components/collapsible.md +567 -0
  184. package/docs/components/color-picker.md +736 -0
  185. package/docs/components/combobox.md +3647 -0
  186. package/docs/components/command.md +1573 -0
  187. package/docs/components/date-input.md +608 -0
  188. package/docs/components/date-picker.md +1266 -0
  189. package/docs/components/dialog.md +1469 -0
  190. package/docs/components/drawer.md +1197 -0
  191. package/docs/components/field.md +549 -0
  192. package/docs/components/file-input.md +609 -0
  193. package/docs/components/hover-card.md +514 -0
  194. package/docs/components/image-cropper.md +579 -0
  195. package/docs/components/input.md +301 -0
  196. package/docs/components/label.md +217 -0
  197. package/docs/components/listbox.md +1130 -0
  198. package/docs/components/menu.md +1286 -0
  199. package/docs/components/menubar.md +455 -0
  200. package/docs/components/meter.md +304 -0
  201. package/docs/components/navigation-menu.md +1581 -0
  202. package/docs/components/number-input.md +1086 -0
  203. package/docs/components/otp-input.md +565 -0
  204. package/docs/components/pagination.md +648 -0
  205. package/docs/components/password-input.md +737 -0
  206. package/docs/components/popover.md +1552 -0
  207. package/docs/components/preferences.md +220 -0
  208. package/docs/components/progress.md +443 -0
  209. package/docs/components/radio.md +846 -0
  210. package/docs/components/rating.md +458 -0
  211. package/docs/components/resizable.md +982 -0
  212. package/docs/components/scroll-area.md +980 -0
  213. package/docs/components/segmented-control.md +465 -0
  214. package/docs/components/select.md +2133 -0
  215. package/docs/components/separator.md +243 -0
  216. package/docs/components/slider.md +1169 -0
  217. package/docs/components/switch.md +613 -0
  218. package/docs/components/tabs.md +1014 -0
  219. package/docs/components/textarea.md +318 -0
  220. package/docs/components/time-input.md +231 -0
  221. package/docs/components/timeline.md +677 -0
  222. package/docs/components/toast.md +1395 -0
  223. package/docs/components/toggle.md +541 -0
  224. package/docs/components/token-input.md +1007 -0
  225. package/docs/components/toolbar.md +787 -0
  226. package/docs/components/tooltip.md +884 -0
  227. package/docs/components/tree.md +1346 -0
  228. package/docs/components/trigger-menu.md +1257 -0
  229. package/docs/components/waterfall.md +754 -0
  230. package/docs/controllers.md +89 -0
  231. package/docs/core.md +8027 -0
  232. package/docs/custom-parts.md +120 -0
  233. package/docs/first-component.md +83 -0
  234. package/docs/forms.md +148 -0
  235. package/docs/hooks/async-options.md +366 -0
  236. package/docs/hooks/shortcuts.md +221 -0
  237. package/docs/installation.md +45 -0
  238. package/docs/introduction.md +32 -0
  239. package/docs/lists-and-menus.md +170 -0
  240. package/docs/llms.txt +1844 -0
  241. package/docs/parts-and-composition.md +93 -0
  242. package/docs/popups.md +117 -0
  243. package/docs/rendering.md +121 -0
  244. package/docs/server-rendering.md +253 -0
  245. package/docs/state-and-events.md +105 -0
  246. package/docs/styling-and-animation.md +176 -0
  247. package/package.json +333 -3
package/docs/agents.md ADDED
@@ -0,0 +1,1523 @@
1
+ <!-- ui-sdk Agents docs, as one file. -->
2
+
3
+ # Built for Agents
4
+
5
+ ui-sdk gives agents the building blocks to create components and the tools to use them.
6
+
7
+ ## Agents Build Components
8
+
9
+ You can ask an agent to build a component for your app, from a toggle button to an emoji picker or a kanban board.
10
+
11
+ `@ui-sdk/core` gives it **behaviors**: tested building blocks for focus, keyboard navigation, selection, popups, drag and drop, and more. The agent combines them to handle the interactions your component needs.
12
+
13
+ A **primitive** brings a component's state, actions and parts together. Here's a small one: a mute button.
14
+
15
+ ```ts
16
+ import { definePrimitive } from "@ui-sdk/core"
17
+
18
+ const muteButton = definePrimitive({
19
+ name: "mute-button",
20
+ state: () => ({ muted: false }),
21
+ api: (context) => ({
22
+ toggle: () => context.store.set({ muted: !context.store.get().muted }),
23
+ }),
24
+ slots: (slot) => ({
25
+ root: slot({
26
+ attrs: (state) => ({
27
+ type: "button",
28
+ "aria-pressed": String(state.muted),
29
+ }),
30
+ press: ({ api }) => api.toggle(),
31
+ }),
32
+ }),
33
+ })
34
+
35
+ export { muteButton }
36
+ ```
37
+
38
+ Pressing the button changes `muted`, and `aria-pressed` follows it. The same primitive can render in React, Vue, Svelte, Solid and Web Components. [Build a Component](./core.md#build-a-component) walks through connecting it to your framework and adding styles.
39
+
40
+ The [component skill](./agents.md#skills) gives the agent a workflow to follow. Each package includes Markdown docs for the installed version, and the [testing kit](./core.md#testing) helps it check keyboard behavior, focus and ARIA in the finished component.
41
+
42
+ ## Agents Use Your Components
43
+
44
+ Connect an agent to a UI built with ui-sdk and it can read component state and call component actions directly. For example, it can rename a project by calling the input's `setValue` action.
45
+
46
+ Create the bridge before the page renders. Import it from the same package as your components; this example uses React:
47
+
48
+ ```ts
49
+ import { createAgent } from "@ui-sdk/react/agent"
50
+
51
+ const agent = createAgent()
52
+ ```
53
+
54
+ Once the page has rendered, `agent.snapshot()` lists its components, their state and their available actions. For a project-name input, it includes:
55
+
56
+ ```json
57
+ {
58
+ "ref": "c1",
59
+ "component": "input",
60
+ "label": "Project name",
61
+ "value": "Apollo",
62
+ "actions": ["setValue", "clear"]
63
+ }
64
+ ```
65
+
66
+ The agent can use that label to change the value:
67
+
68
+ ```ts
69
+ await agent.act({
70
+ steps: [{ ref: "Project name", action: "setValue", args: ["Atlas"] }],
71
+ })
72
+ ```
73
+
74
+ The change goes through the input's API and change callback. Your app can provide an `allow` callback to approve or refuse requested actions. Change callbacks receive `source: "agent"` and can cancel a change too.
75
+
76
+ You can give these tools to your own agent, or register them with a supported browser through [WebMCP](./agents.md#webmcp).
77
+
78
+ ## Benchmark
79
+
80
+ In a project-settings benchmark, agents renamed a project, updated members and notification settings, then saved. One used browser tools on a page built with another component library; the other used the agent bridge on the same screen built with ui-sdk.
81
+
82
+ | | Browser tools | ui-sdk + agent bridge | Delta |
83
+ | ---------- | ------------- | --------------------- | ----- |
84
+ | Tool calls | 16 | **5** | −69% |
85
+ | Text read | 39.8k | **6.8k** | −83% |
86
+ | Time | 41 s | **16 s** | −61% |
87
+ | Cost | $0.073 | **$0.032** | −56% |
88
+
89
+ These are medians from three runs per approach. Both agents used Sonnet through Claude Code with the same task and system prompt, plus instructions for their available tools. Every run completed the task. Text read counts characters returned by the tools. Time and cost are what Claude Code reported for those runs.
90
+
91
+ ## Where to Start
92
+
93
+ - **[Build with an Agent](./agents.md#build-with-an-agent):** give it the docs, describe the component, and check its work.
94
+ - **[Connect an Agent](./agents.md#get-started):** set up the bridge, read your components, and call their actions.
95
+
96
+
97
+ # Build with an Agent
98
+
99
+ Give an agent the right context, build a small version, and check how it behaves.
100
+
101
+ Tell the agent what you're building, which framework you use, and how the component should behave. It can use ui-sdk's components and behaviors to put it together.
102
+
103
+ ## Give It the Docs
104
+
105
+ Each package includes Markdown docs that match the version you installed. Choose your framework in the page header, then start with:
106
+
107
+ **React**
108
+
109
+ ```text
110
+ This project uses React and ui-sdk. Read
111
+ node_modules/@ui-sdk/react/docs/llms.txt to see what's available.
112
+
113
+ For a custom component, read the relevant sections of docs/core.md
114
+ and docs/rendering.md in the same package.
115
+ ```
116
+
117
+ **Vue**
118
+
119
+ ```text
120
+ This project uses Vue and ui-sdk. Read
121
+ node_modules/@ui-sdk/vue/docs/llms.txt to see what's available.
122
+
123
+ For a custom component, read the relevant sections of docs/core.md
124
+ and docs/rendering.md in the same package.
125
+ ```
126
+
127
+ **Svelte**
128
+
129
+ ```text
130
+ This project uses Svelte and ui-sdk. Read
131
+ node_modules/@ui-sdk/svelte/docs/llms.txt to see what's available.
132
+
133
+ For a custom component, read the relevant sections of docs/core.md
134
+ and docs/rendering.md in the same package.
135
+ ```
136
+
137
+ **Solid**
138
+
139
+ ```text
140
+ This project uses Solid and ui-sdk. Read
141
+ node_modules/@ui-sdk/solid/docs/llms.txt to see what's available.
142
+
143
+ For a custom component, read the relevant sections of docs/core.md
144
+ and docs/rendering.md in the same package.
145
+ ```
146
+
147
+ **Web Components**
148
+
149
+ ```text
150
+ This project uses Web Components and ui-sdk. Read
151
+ node_modules/@ui-sdk/web/docs/llms.txt to see what's available.
152
+
153
+ For a custom component, read the relevant sections of docs/core.md
154
+ and docs/rendering.md in the same package.
155
+ ```
156
+
157
+ For a page with no build step, like an Artifact, point it at web.md instead. It's the same docs in one file, for a page that loads `web.js` and `web.css`:
158
+
159
+ ```text
160
+ This page loads ui-sdk with web.js and web.css. Read
161
+ https://ui-sdk.dev/web.md to see what's available.
162
+ ```
163
+
164
+ `llms.txt` covers your framework's APIs. `core.md` covers the behaviors, and `rendering.md` explains how to render a primitive in your framework.
165
+
166
+ The [Core guides](./core.md#build-a-component) walk through the code. [All Behaviors](./core.md#all-behaviors) helps the agent find a building block for each interaction.
167
+
168
+ ## Add the Skill
169
+
170
+ If your agent supports skills, install `ui-sdk`:
171
+
172
+ ```bash
173
+ npx skills add vercel-labs/ui --skill ui-sdk
174
+ ```
175
+
176
+ It gives the agent a workflow for choosing behaviors, defining a primitive, rendering it and testing it. The same skill ships with `@ui-sdk/core` in `node_modules/@ui-sdk/core/skills/`, so an agent without skills can read it there. [Skills](./agents.md#skills) shows how to use it. You can also work directly from the docs.
177
+
178
+ ## Describe the Interaction
179
+
180
+ Give the agent a concrete example of what someone should be able to do. For an emoji picker:
181
+
182
+ **React**
183
+
184
+ ```text
185
+ Build an emoji picker in React using ui-sdk.
186
+
187
+ Add a search input and a grid of emoji. Down from the search input
188
+ should move focus to the first result. Arrow keys move through the
189
+ grid, and Enter selects an emoji. Announce the selection to screen readers.
190
+
191
+ Use our existing styles.
192
+ ```
193
+
194
+ **Vue**
195
+
196
+ ```text
197
+ Build an emoji picker in Vue using ui-sdk.
198
+
199
+ Add a search input and a grid of emoji. Down from the search input
200
+ should move focus to the first result. Arrow keys move through the
201
+ grid, and Enter selects an emoji. Announce the selection to screen readers.
202
+
203
+ Use our existing styles.
204
+ ```
205
+
206
+ **Svelte**
207
+
208
+ ```text
209
+ Build an emoji picker in Svelte using ui-sdk.
210
+
211
+ Add a search input and a grid of emoji. Down from the search input
212
+ should move focus to the first result. Arrow keys move through the
213
+ grid, and Enter selects an emoji. Announce the selection to screen readers.
214
+
215
+ Use our existing styles.
216
+ ```
217
+
218
+ **Solid**
219
+
220
+ ```text
221
+ Build an emoji picker in Solid using ui-sdk.
222
+
223
+ Add a search input and a grid of emoji. Down from the search input
224
+ should move focus to the first result. Arrow keys move through the
225
+ grid, and Enter selects an emoji. Announce the selection to screen readers.
226
+
227
+ Use our existing styles.
228
+ ```
229
+
230
+ **Web Components**
231
+
232
+ ```text
233
+ Build an emoji picker as a web component using ui-sdk.
234
+
235
+ Add a search input and a grid of emoji. Down from the search input
236
+ should move focus to the first result. Arrow keys move through the
237
+ grid, and Enter selects an emoji. Announce the selection to screen readers.
238
+
239
+ Use our existing styles.
240
+ ```
241
+
242
+ Include details that matter to your app, such as whether the picker opens in a popup or whether a parent component owns the selected value. The agent can choose the behaviors from those requirements.
243
+
244
+ ## Start Small
245
+
246
+ Ask for one working example first:
247
+
248
+ ```text
249
+ Start with a small, fixed list of emoji and one example page.
250
+ Reuse public ui-sdk components and behaviors where they fit.
251
+ ```
252
+
253
+ Try that version, then ask for a specific change:
254
+
255
+ ```text
256
+ When a search has no results, show "No emoji found" and keep focus
257
+ in the search input. Keep the input in the same position as results change.
258
+ ```
259
+
260
+ This gives you something to review as the component takes shape.
261
+
262
+ ## Check the Result
263
+
264
+ Ask the agent to test the interactions you described:
265
+
266
+ ```text
267
+ Test moving from search into the grid, navigating with arrow keys,
268
+ and selecting with Enter. Check the empty search state and the
269
+ selection announcement too.
270
+
271
+ Run the project's typecheck and relevant tests. Tell me what passed
272
+ and anything you couldn't check.
273
+ ```
274
+
275
+ The [testing guide](./core.md#testing) shows how to check a primitive with `@ui-sdk/core/testing`.
276
+
277
+ Try the example yourself too: Tab into the search input, search for an emoji, press Down, move with the arrows, and select with Enter. Then try a search with no results. These are the same interactions the agent's tests should cover.
278
+
279
+
280
+ # Skills
281
+
282
+ Help your agent build custom components with @ui-sdk/core.
283
+
284
+ `ui-sdk` helps your agent build a component ui-sdk doesn't ship, such as a rating, a stepper, or an emoji picker.
285
+
286
+ ## Install the Skill
287
+
288
+ If your agent supports skills, install it with:
289
+
290
+ ```bash
291
+ npx skills add vercel-labs/ui --skill ui-sdk
292
+ ```
293
+
294
+ The skill also ships inside `@ui-sdk/core`, at `node_modules/@ui-sdk/core/skills/ui-sdk/SKILL.md`. That copy always matches the version you installed. If your agent doesn't support skills, point it there instead.
295
+
296
+ ## Use the Skill
297
+
298
+ Mention the skill, your framework, and the interaction you want:
299
+
300
+ ```text
301
+ Use ui-sdk to build a five-star rating in React.
302
+ Arrow keys should move between stars and choose a rating.
303
+ Use our existing styles and test the keyboard behavior.
304
+ ```
305
+
306
+ The skill guides the agent through reading the installed docs, choosing behaviors, defining a primitive, rendering it in your framework, and testing the result. It supports React, Vue, Svelte, Solid, and Web Components.
307
+
308
+ [Build with an Agent](./agents.md#build-with-an-agent) shows how to give the agent context, refine the component, and check its work. The [Core guides](./core.md#build-a-component) explain the code it builds.
309
+
310
+
311
+ # Get Started
312
+
313
+ Let an agent read your components and call their actions.
314
+
315
+ `createAgent` connects an agent to the ui-sdk components in your app. It can read their state, find the actions they support, and call those actions through the component's API.
316
+
317
+ In this example, an agent reads a text field labeled "Project name" and changes it from "Apollo" to "Atlas".
318
+
319
+ For a complete app you can run, follow the [Guide](./agents.md#guide). It takes you from a Vite project to using a Select through the console and ChatGPT.
320
+
321
+ ## Create the Bridge
322
+
323
+ Create the bridge once. These examples use React:
324
+
325
+ ```ts title="agent.ts"
326
+ import { createAgent } from "@ui-sdk/react/agent"
327
+
328
+ const agent = createAgent()
329
+
330
+ export { agent }
331
+ ```
332
+
333
+ The bridge finds the components already on the page when you create it, and registers new ones as they're created, so you can create it at startup or later, when an assistant opens.
334
+
335
+ Use the agent entry from the same package as your components:
336
+
337
+ | Framework | Import From |
338
+ | -------------- | ---------------------- |
339
+ | React | `@ui-sdk/react/agent` |
340
+ | Vue | `@ui-sdk/vue/agent` |
341
+ | Svelte | `@ui-sdk/svelte/agent` |
342
+ | Solid | `@ui-sdk/solid/agent` |
343
+ | Web Components | `@ui-sdk/web/agent` |
344
+
345
+ ## Add a Component
346
+
347
+ Render a ui-sdk input with an accessible name:
348
+
349
+ ```tsx
350
+ import { Input } from "@ui-sdk/react/input"
351
+ import "./agent"
352
+
353
+ function ProjectName() {
354
+ return <Input aria-label="Project name" defaultValue="Apollo" />
355
+ }
356
+
357
+ export { ProjectName }
358
+ ```
359
+
360
+ The name helps both people using a screen reader and agents identify the input. A [Field label](./components/field.md) works too.
361
+
362
+ ## Read the Page
363
+
364
+ After the component renders, call `snapshot`:
365
+
366
+ ```ts
367
+ import { agent } from "./agent"
368
+
369
+ const components = agent.snapshot()
370
+ ```
371
+
372
+ For this page, it returns:
373
+
374
+ ```json
375
+ [
376
+ {
377
+ "ref": "c1",
378
+ "component": "input",
379
+ "label": "Project name",
380
+ "value": "Apollo",
381
+ "actions": ["setValue", "clear"]
382
+ }
383
+ ]
384
+ ```
385
+
386
+ `ref` identifies this component. `actions` lists what the agent can ask it to do now.
387
+
388
+ The snapshot includes ui-sdk components. It can also include components that left the page, such as a field in a closed tab, marked with `offPage: true`. A page with more than 100 components lists the first 100 and ends with an entry that says how many more there are; use `query` to find the rest.
389
+
390
+ ## Change a Value
391
+
392
+ Use `act` to call one of the input's actions:
393
+
394
+ ```ts
395
+ const result = await agent.act({
396
+ steps: [{ ref: "Project name", action: "setValue", args: ["Atlas"] }],
397
+ })
398
+ ```
399
+
400
+ The input now reads **Atlas**. Its `onValueChange` callback runs with `details.source === "agent"`, so your app can respond to the change or cancel it. [Permissions](./agents.md#permissions) covers those checks.
401
+
402
+ You can use the label or the `ref` from the snapshot. A label matches whole words regardless of case and accents, so `"project name"` works too. If a label is ambiguous, the bridge returns an error with the matches. Use a specific `ref` to choose one.
403
+
404
+ `result.steps` reports what each step changed, and `result.values` contains the current field values. A callback can cancel a change, so check those values when you need to know what took effect.
405
+
406
+ Put several steps in one call to run them in order. If a step fails, `result.ok` is `false` and `result.error` tells you which step failed and why. Earlier steps stay applied.
407
+
408
+ ## Find List Items
409
+
410
+ For components with choices, such as a Select or Combobox, `query` with `within` lists the available values. For a Select labeled **Region**:
411
+
412
+ ```ts
413
+ const [region] = await agent.query({ within: "Region", text: "Europe" })
414
+ ```
415
+
416
+ `region.items` holds up to 50 matching choices, each with a `value` and `label`, and `region.more` says how many matched beyond that. Leave out `text` to list the first 50. Depending on how the component supplies its items, the bridge may briefly open it to read them.
417
+
418
+ `query` also finds components without listing the page. `agent.query({ text: "Europe" })` returns the components whose label or row text has that word, and lists whose choices match, with those choices under them. `kind` keeps one kind, such as `"select"`.
419
+
420
+ ## Connect the Tools
421
+
422
+ `agentTools` describes `snapshot`, `act`, and `query` with names, descriptions, and JSON Schemas:
423
+
424
+ ```ts
425
+ import { agentTools } from "@ui-sdk/react/agent"
426
+ ```
427
+
428
+ Use these definitions when setting up your agent's tools. Route each tool call to the matching method on `agent` and return its result to the model. Your model SDK determines how you register and handle those calls.
429
+
430
+ What the tools return comes from the page, so it can include text your users wrote, like a name in a table row. Their descriptions tell the model to treat it as data. Before an agent changes something that matters, have a person confirm it with [Permissions](./agents.md#permissions).
431
+
432
+ For a browser that supports WebMCP, [register the tools with the browser](./agents.md#webmcp).
433
+
434
+ ## Your Own Components
435
+
436
+ A component built with [`definePrimitive`](./core.md#define-a-primitive) joins the bridge too. Its `agent` key lists the API methods an agent can call, like `agent: { actions: ["setValue"] }`. Without one, methods named like changes, such as `setValue` or `toggle`, become its actions, and event handlers such as `keydown` stay internal.
437
+
438
+ The snapshot reads the primitive's `controlled` state keys. If none are declared, it reads `open`, `value`, and `checked` when present. [Your Own Components](./agents.md#your-own-components) shows how to add the key and what each part of it does.
439
+
440
+ ## Next Steps
441
+
442
+ - [Permissions](./agents.md#permissions): approve, refuse, or cancel an agent's changes.
443
+ - [Observe Changes](./agents.md#observe-changes): keep the agent up to date with what the person changed.
444
+ - [History and Undo](./agents.md#history-and-undo): record changes, undo them, and preview suggestions.
445
+ - [WebMCP](./agents.md#webmcp): make the tools available to a browser agent.
446
+ - [Reference](./agents.md#reference): look up options, methods, and return values.
447
+
448
+
449
+ # Guide
450
+
451
+ Build a Vite app, try the agent bridge in the console, and let ChatGPT use it through WebMCP.
452
+
453
+ We'll build a Select for choosing a region. First, you'll read and change it from the browser console. Then you'll give ChatGPT the same tools and ask it to choose a region for you.
454
+
455
+ ## Create a Vite App
456
+
457
+ Start with Vite's React and TypeScript template:
458
+
459
+ ```bash
460
+ npm create vite@latest agent-demo -- --template react-ts
461
+ cd agent-demo
462
+ npm install
463
+ npm install @ui-sdk/react @ui-sdk/core
464
+ ```
465
+
466
+ You'll need a Node.js version supported by [Vite](https://vite.dev/guide/). If the scaffolder offers to start the app, finish the files below first.
467
+
468
+ ## Add a Select
469
+
470
+ Replace `src/App.tsx` with:
471
+
472
+ ```tsx title="src/App.tsx"
473
+ import {
474
+ Select,
475
+ SelectContent,
476
+ SelectItem,
477
+ SelectLabel,
478
+ SelectTrigger,
479
+ SelectValue,
480
+ } from "@ui-sdk/react/select"
481
+
482
+ const regions = [
483
+ { value: "europe", label: "Europe" },
484
+ { value: "north-america", label: "North America" },
485
+ { value: "asia-pacific", label: "Asia Pacific" },
486
+ ]
487
+
488
+ function App() {
489
+ return (
490
+ <main>
491
+ <h1>Project Settings</h1>
492
+ <Select defaultValue="europe" items={regions}>
493
+ <SelectLabel className="select-label">Region</SelectLabel>
494
+ <SelectTrigger className="select-trigger">
495
+ <SelectValue />
496
+ <svg
497
+ width="16"
498
+ height="16"
499
+ viewBox="0 0 16 16"
500
+ fill="none"
501
+ aria-hidden="true"
502
+ >
503
+ <path d="m4 6 4 4 4-4" stroke="currentColor" strokeWidth="1.5" />
504
+ </svg>
505
+ </SelectTrigger>
506
+ <SelectContent className="select-content">
507
+ {regions.map((region) => (
508
+ <SelectItem
509
+ key={region.value}
510
+ value={region.value}
511
+ className="select-item"
512
+ >
513
+ {region.label}
514
+ </SelectItem>
515
+ ))}
516
+ </SelectContent>
517
+ </Select>
518
+ </main>
519
+ )
520
+ }
521
+
522
+ export { App }
523
+ ```
524
+
525
+ `SelectLabel` names the component **Region**. We'll use that name in the console and in our prompts. Passing `items` tells the Select every choice before its list renders, so the trigger shows **Europe** right away and the bridge can read the choices while the popup is closed.
526
+
527
+ Replace `src/index.css` with a few neutral styles:
528
+
529
+ ```css title="src/index.css"
530
+ body {
531
+ margin: 0;
532
+ background: #fff;
533
+ color: #171717;
534
+ font:
535
+ 14px/1.5 system-ui,
536
+ sans-serif;
537
+ }
538
+
539
+ main {
540
+ max-width: 320px;
541
+ margin: 80px auto;
542
+ padding: 0 24px;
543
+ }
544
+
545
+ h1 {
546
+ margin: 0 0 24px;
547
+ font-size: 20px;
548
+ }
549
+
550
+ .select-label {
551
+ display: block;
552
+ margin-bottom: 8px;
553
+ }
554
+
555
+ .select-trigger {
556
+ display: flex;
557
+ width: 100%;
558
+ align-items: center;
559
+ justify-content: space-between;
560
+ padding: 8px 12px;
561
+ border: 1px solid #d4d4d4;
562
+ border-radius: 8px;
563
+ background: #fff;
564
+ color: inherit;
565
+ font: inherit;
566
+ }
567
+
568
+ .select-content {
569
+ min-width: var(--anchor-width);
570
+ max-height: var(--available-height);
571
+ overflow-y: auto;
572
+ padding: 4px;
573
+ border: 1px solid #d4d4d4;
574
+ border-radius: 8px;
575
+ background: #fff;
576
+ }
577
+
578
+ .select-item {
579
+ padding: 8px;
580
+ border-radius: 4px;
581
+ cursor: default;
582
+ }
583
+
584
+ .select-item[data-highlighted] {
585
+ background: #f5f5f5;
586
+ }
587
+
588
+ .select-trigger:focus-visible,
589
+ .select-item:focus-visible {
590
+ outline: 2px solid #171717;
591
+ outline-offset: 2px;
592
+ }
593
+ ```
594
+
595
+ ## Initialize the Agent
596
+
597
+ Create `src/agent.ts`:
598
+
599
+ ```ts title="src/agent.ts"
600
+ import { createAgent } from "@ui-sdk/react/agent"
601
+
602
+ const agent = createAgent()
603
+
604
+ Object.assign(window, { agent })
605
+
606
+ export { agent }
607
+ ```
608
+
609
+ `Object.assign` makes the bridge available as `window.agent` in the console. It doesn't require a TypeScript declaration on `Window`.
610
+
611
+ Now replace `src/main.tsx`. Import `agent.ts` before rendering the app so the bridge sees the Select as it's created:
612
+
613
+ ```tsx title="src/main.tsx"
614
+ import "./agent"
615
+ import { StrictMode } from "react"
616
+ import { createRoot } from "react-dom/client"
617
+ import { App } from "./App"
618
+ import "./index.css"
619
+
620
+ createRoot(document.getElementById("root")!).render(
621
+ <StrictMode>
622
+ <App />
623
+ </StrictMode>
624
+ )
625
+ ```
626
+
627
+ Start the app on port 5173:
628
+
629
+ ```bash
630
+ npm run dev -- --port 5173 --strictPort
631
+ ```
632
+
633
+ Open [localhost:5173](http://localhost:5173). **Europe** should be selected. Open the Select and choose another region, or focus it and use the arrow keys to try it yourself.
634
+
635
+ ## Read It from the Console
636
+
637
+ Open your browser's developer tools and select **Console**. Run:
638
+
639
+ ```js
640
+ window.agent.snapshot()
641
+ ```
642
+
643
+ You'll see a component with `component: "select"`, `label: "Region"`, its current `value`, and its available `actions`. Its `ref` is an identifier assigned by the bridge; we'll use the label **Region** in this guide.
644
+
645
+ List its choices:
646
+
647
+ ```js
648
+ await window.agent.query({ within: "Region" })
649
+ ```
650
+
651
+ This returns the Region Select with its three regions under `items`, each with a `value` and `label`. To filter the list, add some text:
652
+
653
+ ```js
654
+ await window.agent.query({ within: "Region", text: "Asia" })
655
+ ```
656
+
657
+ ## Change the Selection
658
+
659
+ Run this in the same console:
660
+
661
+ ```js
662
+ await window.agent.act({
663
+ steps: [{ ref: "Region", action: "setValue", args: ["north-america"] }],
664
+ })
665
+ ```
666
+
667
+ The Select should now show **North America**. The result includes `ok`, the changes made by each step, and the current field values.
668
+
669
+ You can also pass an item's label:
670
+
671
+ ```js
672
+ await window.agent.act({
673
+ steps: [{ ref: "Region", action: "setValue", args: ["Asia Pacific"] }],
674
+ })
675
+ ```
676
+
677
+ Choose **Europe** yourself, then run `window.agent.snapshot()` again. The bridge reads the same selection you see on the page.
678
+
679
+ ## Register the WebMCP Tools
680
+
681
+ Replace `src/agent.ts` with this version:
682
+
683
+ ```ts title="src/agent.ts"
684
+ import { createAgent, registerTools } from "@ui-sdk/react/agent"
685
+
686
+ const agent = createAgent()
687
+
688
+ Object.assign(window, { agent })
689
+
690
+ const unregister = registerTools(agent, {
691
+ allow: (step) => step.action !== "press" || confirm(`Press ${step.args[0]}?`),
692
+ })
693
+
694
+ if (import.meta.hot) {
695
+ import.meta.hot.dispose(unregister)
696
+ }
697
+
698
+ export { agent }
699
+ ```
700
+
701
+ `registerTools` publishes `snapshot`, `query`, and `act` through WebMCP when the browser supports it. Its `allow` asks you before the browser's agent presses a button. The cleanup removes the old registration when Vite replaces the module during development.
702
+
703
+ Reload the page after saving. `window.agent` is for the console exercise; WebMCP calls the bridge directly.
704
+
705
+ ## Open the App in ChatGPT
706
+
707
+ Keep Vite running. In the **ChatGPT desktop app** on the same computer:
708
+
709
+ 1. Start a Work conversation with a model that supports site tools.
710
+ 2. Open the built-in browser and visit `http://localhost:5173`.
711
+ 3. Select **Site tools** in the address bar, then **Available site tools**. Look for `snapshot`, `query`, and `act`.
712
+
713
+ ChatGPT discovers tools from the page open in that browser. Model and workspace availability are listed in [ChatGPT's site tools guide](https://learn.chatgpt.com/docs/webmcp).
714
+
715
+ Start with a prompt that only reads the page:
716
+
717
+ ```text
718
+ Find the WebMCP tools available on this page. Use snapshot to tell me
719
+ which component is on the page and which region is selected.
720
+ ```
721
+
722
+ Then ask it to read the choices:
723
+
724
+ ```text
725
+ Use the page's query tool to list the choices for Region.
726
+ ```
727
+
728
+ Finally, ask it to change the selection:
729
+
730
+ ```text
731
+ Use the page's act tool to change Region to North America. Tell me
732
+ the selected value returned by the tool.
733
+ ```
734
+
735
+ Review any access prompt from ChatGPT, then watch the Select update. In **Site tools**, use **Recently used** to check the calls it made.
736
+
737
+ You can also give it the whole task at once:
738
+
739
+ ```text
740
+ Find this page's WebMCP tools, check the available regions, and select
741
+ Asia Pacific. Use the tool result to confirm the selected value.
742
+ ```
743
+
744
+ ## If the Tools Don't Appear
745
+
746
+ Check that you're viewing the Vite app in ChatGPT's built-in browser and that **Enable site tools** is on under **Settings → Browser → Permissions**. Open the app as the page itself; ChatGPT doesn't discover tools inside an iframe. See the [current requirements](https://learn.chatgpt.com/docs/webmcp) for supported models and workspaces.
747
+
748
+ In a browser's console, you can check whether the registration API exists:
749
+
750
+ ```js
751
+ typeof document.modelContext?.registerTool
752
+ ```
753
+
754
+ It should return `"function"` in a browser that supports registration. If it returns `"undefined"`, the console examples still work, but that browser won't receive WebMCP tools from `registerTools`.
755
+
756
+ ## Keep Going
757
+
758
+ [Permissions](./agents.md#permissions) shows how to limit the actions an agent can run. [Observe Changes](./agents.md#observe-changes) keeps it informed when you change the UI. Use the [Reference](./agents.md#reference) to look up the bridge's options and results.
759
+
760
+
761
+ # Your Own Components
762
+
763
+ Let an agent use the components you build with definePrimitive, wherever they're rendered.
764
+
765
+ A component you build with [`definePrimitive`](./core.md#define-a-primitive) shows up in the agent's snapshot next to every Select and Dialog on the page. Add an `agent` key to say which of its methods an agent can call.
766
+
767
+ You write the key once, in the component. Anyone who renders the component gets it: an app that installs your rating from npm [creates the bridge](./agents.md#create-the-bridge), renders `<Rating>`, and its agent can rate, with no tool or schema to write.
768
+
769
+ ## Try It
770
+
771
+ This rating has `agent: { actions: ["rate"] }`. Press **Read the page** to see what an agent sees, then **Ask for 4 stars** to send the step an agent would send.
772
+
773
+ ## Add the Agent Key
774
+
775
+ Here's the rating's primitive. Its API has one method, `rate`:
776
+
777
+ ```ts title="rating.ts"
778
+ const rating = definePrimitive({
779
+ name: "rating",
780
+ state: (options: { defaultValue?: number }) => ({
781
+ value: options.defaultValue ?? 0,
782
+ }),
783
+ api: (context) => {
784
+ const change = createChange(context)
785
+ return {
786
+ rate: (value: number, event?: Event) =>
787
+ change("value", value, { reason: "item-press", event }),
788
+ }
789
+ },
790
+ slots: (slot) => ({
791
+ root: slot({ attrs: () => ({ role: "group" }) }),
792
+ }),
793
+ agent: { actions: ["rate"] },
794
+ })
795
+ ```
796
+
797
+ `actions` lists the API methods an agent can call. Put `agent` last, after `slots`, so TypeScript has read your API by then.
798
+
799
+ With the key, the snapshot lists `rate`:
800
+
801
+ ```json
802
+ [
803
+ {
804
+ "ref": "c1",
805
+ "component": "rating",
806
+ "actions": ["rate"],
807
+ "label": "Your rating",
808
+ "value": 0
809
+ }
810
+ ]
811
+ ```
812
+
813
+ And an agent can call it with the arguments the method takes:
814
+
815
+ ```ts
816
+ await agent.act({ steps: [{ ref: "Your rating", action: "rate", args: [4] }] })
817
+ // { ok: true, steps: [{ ref: "c1", changes: [{ key: "value", value: 4, ... }] }], ... }
818
+ ```
819
+
820
+ The change goes through `createChange`, so the app's `onValueChange` sees `details.source === "agent"` and can cancel it, like any other change. [Permissions](./agents.md#permissions) covers that.
821
+
822
+ ## Choose What an Agent Calls
823
+
824
+ Only the methods in `actions` are offered. The rest of your API, like a `keydown` handler or a roving focus helper, stays private to your component, and a step that names one is refused.
825
+
826
+ If `actions` names a method your API doesn't have, development builds warn, so a typo shows up right away.
827
+
828
+ Without an `agent` key, the bridge guesses from method names: `set` followed by a capital letter, like `setValue`, and verbs like `open`, `toggle` or `clear`. It can't guess `rate`, so the rating would show `"actions": []`, and a step that asks for `rate` would get:
829
+
830
+ ```txt
831
+ rating c1 takes no rate now. It takes: nothing.
832
+ ```
833
+
834
+ Listing your actions makes the component say exactly what it does, whatever its methods are called.
835
+
836
+ ## Lists
837
+
838
+ When your component's value is always one of its items, like a size picker, add `closed: true`:
839
+
840
+ ```ts
841
+ agent: { actions: ["setValue"], closed: true },
842
+ ```
843
+
844
+ The agent can name an item by its label or its value. If it names something that isn't there, the step is refused with the real items, so it can try again:
845
+
846
+ ```txt
847
+ No item "XL" in Size (c1). Its items: Small, Medium, Large.
848
+ ```
849
+
850
+ The bridge reads the items from your state's `items`. If they live somewhere else, return them from `items`:
851
+
852
+ ```ts
853
+ agent: { actions: ["setValue"], items: (api) => api.allItems() },
854
+ ```
855
+
856
+ The snapshot shows the items with the component when there are 10 or fewer and none are on the page. That works for any action, not only `setValue`, so a stepper's `goTo` shows its steps. Longer lists come back from `query({ within })`.
857
+
858
+ ## Content Behind Items
859
+
860
+ Tabs and accordions hide other components behind their closed items. Add `reveals: true` and, when an agent looks for something it can't find, the error lists the closed items it might be behind, so the agent can open one first:
861
+
862
+ ```ts
863
+ agent: { actions: ["setValue"], closed: true, reveals: true },
864
+ ```
865
+
866
+ ## Show It When It Matters
867
+
868
+ `shown` decides whether an agent sees the component right now. A field wrapper, for example, only needs to show while it has errors, since its input already carries the label:
869
+
870
+ ```ts
871
+ agent: { shown: (state) => state.errors.length > 0 },
872
+ ```
873
+
874
+ ## Secret Values
875
+
876
+ For a value an agent should never read, like a PIN, add `secret: true`. The agent can still fill it in, and the snapshot shows `"[redacted]"` instead of the value:
877
+
878
+ ```ts
879
+ agent: { actions: ["setValue", "clear"], secret: true },
880
+ ```
881
+
882
+ To hide one particular field instead, the app can mark it with `data-agent-secret`. See [Permissions](./agents.md#permissions).
883
+
884
+ ## API Reference
885
+
886
+ | Key | Type | Description |
887
+ | --------- | -------------------- | ------------------------------------------------------------------------------------------------------ |
888
+ | `actions` | `string[]` | The API methods an agent can call. Without it, methods named like changes, such as `setValue`. |
889
+ | `closed` | `boolean` | The value is always one of the items, so a step that names another item is refused. |
890
+ | `items` | `(api) => unknown` | The items as data, when they aren't the state's `items`. |
891
+ | `reveals` | `boolean` | Closed items hide other components. A component the agent can't find is reported as maybe behind them. |
892
+ | `shown` | `(state) => boolean` | Whether an agent sees the component right now. |
893
+ | `secret` | `boolean` | The value is always redacted, in the snapshot, in step results and in `observe`. |
894
+
895
+ The `agent` key is experimental.
896
+
897
+
898
+ # Permissions
899
+
900
+ Choose what an agent can change, what it can see, and when to ask the person.
901
+
902
+ Use `allow` to choose which actions an agent can run. Use a component's change callback to reject a value before it's applied. These checks also apply when the agent uses [WebMCP](./agents.md#webmcp).
903
+
904
+ The examples below use the **Region** Select from the [Guide](./agents.md#guide).
905
+
906
+ ## Choose the Actions
907
+
908
+ Without an `allow` callback, the bridge accepts supported actions. Add one to the `createAgent` call in `src/agent.ts` to let the agent change only Region:
909
+
910
+ ```ts
911
+ import { createAgent } from "@ui-sdk/react/agent"
912
+
913
+ const agent = createAgent({
914
+ allow: ({ component, label, action }) =>
915
+ component === "select" && label === "Region" && action === "setValue",
916
+ })
917
+ ```
918
+
919
+ Return `true` to let an action run or `false` to refuse it. On a refusal, `act` returns `ok: false` and stops. Any earlier steps stay applied.
920
+
921
+ The callback also receives `ref` and `args`. If the agent asks for an item by label, the bridge resolves it to a value first. For example, `"North America"` becomes `"north-america"`. A list whose items show only while it's open has to open to resolve a label, so the callback is asked first with the label, before it opens, and then with the value.
922
+
923
+ The callback runs before the step changes anything. A `press` that opens a closed menu or dialog to find its button asks before it opens, and closes it again when the answer is no.
924
+
925
+ ## Ask First
926
+
927
+ To ask the person before changing Region, return their answer from `allow`:
928
+
929
+ ```ts
930
+ import { createAgent } from "@ui-sdk/react/agent"
931
+
932
+ const agent = createAgent({
933
+ allow: ({ component, label, action, args }) => {
934
+ if (component !== "select" || label !== "Region" || action !== "setValue") {
935
+ return false
936
+ }
937
+
938
+ return window.confirm(`Change Region to ${args[0]}?`)
939
+ },
940
+ })
941
+ ```
942
+
943
+ Try changing Region through the console or ChatGPT again. **OK** allows the change; **Cancel** refuses it.
944
+
945
+ For your own confirmation UI, return a `Promise<boolean>` that resolves with the person's answer. The bridge waits before running the action. Later `act` and `query` calls on the same bridge wait too. If nobody answers within 5 minutes, the step fails and the `signal` passed to `allow` aborts, so you can close your prompt. Set `timeout` on `createAgent` to change the wait.
946
+
947
+ ## Mark What Needs a Person
948
+
949
+ Some controls should always wait for the person, whatever your `allow` rules say. Add `data-agent-confirm` with a sentence that says what the control does:
950
+
951
+ ```tsx
952
+ <Button
953
+ data-agent-confirm="Charges $20 to your card for 1,000 credits"
954
+ onClick={buyCredits}
955
+ >
956
+ Buy credits
957
+ </Button>
958
+ ```
959
+
960
+ The snapshot lists the button with `confirm: "Charges $20 to your card for 1,000 credits"`, so the agent knows before it tries. Without an `allow` callback, pressing it fails with:
961
+
962
+ ```text
963
+ Needs a person: "Charges $20 to your card for 1,000 credits". Ask them to press "Buy credits" themselves.
964
+ ```
965
+
966
+ With `allow`, the step waits for your callback, and `step.confirm` carries the sentence. Show it to the person and return their answer:
967
+
968
+ ```ts
969
+ const agent = createAgent({
970
+ allow: (step, { signal }) =>
971
+ step.confirm ? showConfirm(step.confirm, { signal }) : true,
972
+ })
973
+ ```
974
+
975
+ If the person says no, the step fails with `The person said no to "…"`.
976
+
977
+ The mark covers its element and everything inside it, for any action. Put it on a form or section to cover every control in it, or on a dialog's content to cover the buttons inside, wherever the dialog renders.
978
+
979
+ ## Keep Parts of the Page from the Agent
980
+
981
+ Two more marks change what the agent sees:
982
+
983
+ | Attribute | What the Agent Gets |
984
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
985
+ | `data-agent-hidden` | Nothing. Components inside are left out of `snapshot` and `query`, and a step that names one is told to ask the person. |
986
+ | `data-agent-secret` | The component, with its value shown as `"[redacted]"` once it's filled. The agent can still type into it. |
987
+
988
+ ```tsx
989
+ <section data-agent-hidden>
990
+ <RecoveryCodes />
991
+ </section>
992
+
993
+ <Field data-agent-secret>
994
+ <FieldLabel>Tax ID</FieldLabel>
995
+ <Input />
996
+ </Field>
997
+ ```
998
+
999
+ Changes inside `data-agent-hidden` still reach [Observe](./agents.md#observe-changes), so your own logs see them, while `agent.subscribe` stays quiet. A secret value stays out of `snapshot`, `act` results and change events, and the field's error text reads `"[redacted]"` too. Password, card and one-time-code fields are treated as secret without a mark.
1000
+
1001
+ In development, a misspelled mark such as `data-agent-confim` logs a warning with the name it meant.
1002
+
1003
+ ## Cancel a Value Change
1004
+
1005
+ An agent's changes reach the same callbacks as a person's changes. Check `details.source` to tell them apart, and call `details.cancel()` to keep the current value.
1006
+
1007
+ For example, add this prop to the Select in `src/App.tsx` to refuse an agent's request to choose Asia Pacific:
1008
+
1009
+ ```tsx
1010
+ onValueChange={(value, details) => {
1011
+ if (details.source === "agent" && value === "asia-pacific") {
1012
+ details.cancel()
1013
+ }
1014
+ }}
1015
+ ```
1016
+
1017
+ You can still choose Asia Pacific yourself. To apply the rule to every change, remove the `details.source === "agent"` check.
1018
+
1019
+ Canceling a value change can leave `act` with `ok: true` and no change for that step. Check each step's `changes` and the returned `values` to confirm what happened.
1020
+
1021
+ Put value checks in these change callbacks. Actions such as `setValue` call the component's API directly, so checks in keyboard or pointer handlers won't run.
1022
+
1023
+ ## What `allow` Covers
1024
+
1025
+ `allow` checks actions. Components and their actions remain visible in `snapshot()`, and the agent can still read choices with `query()`. Use `data-agent-hidden` to keep a region out of view.
1026
+
1027
+ Finding a component or its choices can open a section or popup before the requested action is checked. `query()` may also briefly open a popup to read its choices. Use the component's `onOpenChange` or equivalent callback if you need to reject those changes too.
1028
+
1029
+ The [Reference](./agents.md#reference) lists the fields passed to `allow` and the results returned by `act`.
1030
+
1031
+
1032
+ # Observe Changes
1033
+
1034
+ Keep an agent informed when someone changes the UI.
1035
+
1036
+ `agent.subscribe` reports changes to ui-sdk components. Collect them between turns so the agent knows what changed since it last read the page.
1037
+
1038
+ Reuse the bridge from the [Guide](./agents.md#guide). The examples below go in a module that runs when your app starts.
1039
+
1040
+ ## Collect Changes
1041
+
1042
+ This subscription collects changes made outside the bridge. That includes a person's interactions and changes made by your application:
1043
+
1044
+ ```ts
1045
+ import type { ObservedChange } from "@ui-sdk/react/observe"
1046
+ import { agent } from "./agent"
1047
+
1048
+ const changes: ObservedChange[] = []
1049
+
1050
+ const stop = agent.subscribe((change) => {
1051
+ if (change.source !== "agent") {
1052
+ changes.push(change)
1053
+ }
1054
+ })
1055
+ ```
1056
+
1057
+ Before the agent's next turn, take the queued changes:
1058
+
1059
+ ```ts
1060
+ const recentChanges = changes.splice(0)
1061
+ ```
1062
+
1063
+ Include `recentChanges` in the context you send to your model. `splice(0)` returns the collected changes and empties the queue. Leave out the source check above if you want to collect the agent's changes too.
1064
+
1065
+ A change a Button's handler makes after an `await` reports `"programmatic"`, even when the agent pressed the button, unless the handler passes the press's source on with `withSource`. [Observe](./core.md#keep-the-source-after-an-await) shows how.
1066
+
1067
+ ## Read a Change
1068
+
1069
+ Choosing **North America** in the Guide's Select produces a change that includes:
1070
+
1071
+ ```json
1072
+ {
1073
+ "label": "Region",
1074
+ "key": "value",
1075
+ "previous": "europe",
1076
+ "value": "north-america",
1077
+ "reason": "item-press",
1078
+ "source": "mouse"
1079
+ }
1080
+ ```
1081
+
1082
+ This describes clicking the option while Europe was selected. Its `ref` identifies the component in a snapshot and can be used in an `act` step. The [Reference](./agents.md#subscribelistener) lists all the fields.
1083
+
1084
+ Changes canceled with `details.cancel()` aren't reported. For a controlled component, the notification reports the requested change before the owner passes the value back. It isn't confirmation that the owner accepted it.
1085
+
1086
+ Password, card and one-time-code fields report `"[redacted]"` for both `value` and `previous`.
1087
+
1088
+ ## When Changes Arrive
1089
+
1090
+ A slider drag reports one change when it finishes, with `previous` set to the value before the drag. State that doesn't commit, such as a text field's input, reports each change. Changes made by the agent or application arrive immediately.
1091
+
1092
+ The subscription gives you one stream of changes. It doesn't emit a separate commit notification. For intermediate steps or a subscription limited to part of the page, use [Observe](./core.md#observe).
1093
+
1094
+ ## Stop Listening
1095
+
1096
+ When you no longer need the subscription, call the function it returned:
1097
+
1098
+ ```ts
1099
+ stop()
1100
+ ```
1101
+
1102
+ To record changes you can step back through, see [History and Undo](./agents.md#history-and-undo).
1103
+
1104
+
1105
+ # History and Undo
1106
+
1107
+ Record component values, step through changes and try suggestions you can revert.
1108
+
1109
+ `createHistory` records changes to component values. `stageChanges` applies a set of values and gives you a way to restore the previous ones. Both use the agent bridge to change the controls.
1110
+
1111
+ For undo inside one component, like a drawing or a board, record its value with [`createUndoStack`](./core.md#undo) instead.
1112
+
1113
+ These helpers are experimental. The examples reuse the bridge from the [Guide](./agents.md#guide), including its permission policy. Import the helpers from your framework's `history` entry or `@ui-sdk/core/history`.
1114
+
1115
+ ## Start Recording
1116
+
1117
+ Start recording before the changes you want to keep:
1118
+
1119
+ ```ts
1120
+ import { createHistory } from "@ui-sdk/react/history"
1121
+ import { agent } from "./agent"
1122
+
1123
+ const history = createHistory(agent)
1124
+ ```
1125
+
1126
+ By default, the history records `value`, `checked` and `pressed` changes across the page, from people, agents, and your app. Use `within` to limit it to one element, such as the Guide's `<main>`:
1127
+
1128
+ ```ts
1129
+ const history = createHistory(agent, {
1130
+ within: () => document.querySelector("main"),
1131
+ keys: ["value", "checked"],
1132
+ })
1133
+ ```
1134
+
1135
+ `within` accepts an element or a function that returns one. `keys` replaces the list of state keys to record. Restoring a key uses its `set<Key>` action, such as `setValue` or `setChecked`, so choose keys the component can set through the bridge.
1136
+
1137
+ Keep password, card and one-time-code fields outside the recorded panel. Their values are redacted, so the history can't restore the originals: their entries carry `redacted: true`, and seeking or undoing leaves those fields as they are and reports them among the changes it couldn't put back.
1138
+
1139
+ ## Read the History
1140
+
1141
+ `history.entries()` returns entries from oldest to newest. Each has an `id`, component `ref`, `label`, state `key`, `value`, `previous`, `source` and `reason`.
1142
+
1143
+ Use `describeEntry` when you want a short sentence for each entry:
1144
+
1145
+ ```ts
1146
+ import { describeEntry } from "@ui-sdk/react/history"
1147
+
1148
+ const labels = history.entries().map(describeEntry)
1149
+ ```
1150
+
1151
+ For example: "Agent set Project name to Atlas". `history.position()` tells you how far you are through the entries. It starts at `0`; the end is `history.entries().length`.
1152
+
1153
+ `history.subscribe(listener)` calls a listener when the entries or position change. Read them inside the listener to update your history UI. It returns a function that unsubscribes.
1154
+
1155
+ ## Step Back and Forward
1156
+
1157
+ Pass an entry count to `seek`. These calls return to the start of the recorded history, then apply it again:
1158
+
1159
+ ```ts
1160
+ await history.seek(0)
1161
+ await history.seek(history.entries().length)
1162
+ ```
1163
+
1164
+ `seek` clamps the position to the available history. If someone makes a new recorded change after seeking back, the history drops the entries after that position, like undo in an editor.
1165
+
1166
+ The history restores recorded component values. It doesn't undo a network request, recreate a removed component or reverse an action such as saving a form. Restoration goes through `agent.act`, one control at a time, so current permissions, disabled state and change callbacks still apply, and a control that's refused or has left the page doesn't stop the others.
1167
+
1168
+ `seek` resolves with the changes it couldn't put back, each as `{ ref, key, value }`, and with an empty array when everything went back. If nothing went back, the position stays where it was.
1169
+
1170
+ The history doesn't record its own restoring, but it records every other change made meanwhile. When someone changes a control the history hasn't restored yet, their change stays, and the control is in the result.
1171
+
1172
+ ## Undo the Agent's Changes
1173
+
1174
+ Use `undoSource` to remove the effect of one source from the applied history:
1175
+
1176
+ ```ts
1177
+ await history.undoSource("agent")
1178
+ ```
1179
+
1180
+ It restores each affected key to its latest value from the other sources, or to its starting value if there isn't one. For example, if a person changes the project name after the agent does, that person's value stays.
1181
+
1182
+ The entries it put back remain in the history with `reverted: true`, and seeking through the history continues to skip them. Like `seek`, `undoSource` resolves with the changes it couldn't put back, and their entries stay applied. The same method accepts other source names, such as `"keyboard"` or `"programmatic"`.
1183
+
1184
+ A Button whose handler awaits needs one more step. A change the handler makes after the `await` happens once the press is over, so it's recorded as `"programmatic"` and `undoSource("agent")` leaves it. Wrap it in `withSource` with the source the handler receives:
1185
+
1186
+ ```tsx
1187
+ import { Button } from "@ui-sdk/react/button"
1188
+ import { withSource } from "@ui-sdk/react/observe"
1189
+
1190
+ function SaveButton() {
1191
+ return (
1192
+ <Button
1193
+ onClick={async (event, { source }) => {
1194
+ await save()
1195
+ withSource(source, () => status.setValue("Saved"))
1196
+ }}
1197
+ >
1198
+ Save
1199
+ </Button>
1200
+ )
1201
+ }
1202
+ ```
1203
+
1204
+ Here `status` is an Input's controller. When the agent presses Save, the new status is recorded as the agent's. See [Observe](./core.md#keep-the-source-after-an-await) for how sources work.
1205
+
1206
+ ## Keep Interactions Together
1207
+
1208
+ A drag that commits arrives as one entry. The history also combines consecutive changes with the same component, key, source and reason, except for `item-press` and `imperative` changes.
1209
+
1210
+ To record every intermediate step instead, turn coalescing off:
1211
+
1212
+ ```ts
1213
+ const history = createHistory(agent, { coalesce: false })
1214
+ ```
1215
+
1216
+ Call `history.clear()` to forget every entry and keep the controls as they are. Call `history.destroy()` when you're done to stop recording and remove its listeners.
1217
+
1218
+ ## Try a Suggestion
1219
+
1220
+ `stageChanges` puts suggested values on the real controls. Try a different region in the Guide's Select:
1221
+
1222
+ ```ts
1223
+ import { stageChanges } from "@ui-sdk/react/history"
1224
+
1225
+ const staged = await stageChanges(agent, [
1226
+ { ref: "Region", value: "north-america" },
1227
+ ])
1228
+ ```
1229
+
1230
+ Each change is `{ ref, key?, value }`. `key` defaults to `"value"`; use `"checked"` for a checkbox. The component must be mounted, expose that key in its snapshot, and advertise the matching `set<Key>` action.
1231
+
1232
+ Staging accepts a ref or an exact label. It uses the first component with a matching label, so use a ref from `snapshot()` when labels repeat.
1233
+
1234
+ Missing components, unavailable setters and refused actions are skipped, and the remaining suggestions are still attempted. `staged.applied` lists successful calls as `{ ref, key, before, value }`. As with `act`, a successful call doesn't prove a controlled owner accepted the value or that a change callback didn't cancel it; read the component state when you need to confirm.
1235
+
1236
+ If the person rejects the suggestion, restore the captured values:
1237
+
1238
+ ```ts
1239
+ await staged.revert()
1240
+ ```
1241
+
1242
+ `revert` runs once, in reverse order, through the same action policy and callbacks. It attempts to restore the captured values even if the person has edited those controls since the preview, and resolves with the entries of `staged.applied` it couldn't put back. A filled password, card or one-time-code field's `before` reads `"[redacted]"`, so `revert` leaves that field as staged and includes it in the result. If the person accepts the suggestion, leave the values in place.
1243
+
1244
+ To point at a suggested control, use `elementOf(ref)` from [Observe](./core.md#observe) with one of the refs in `staged.applied`.
1245
+
1246
+
1247
+ # WebMCP
1248
+
1249
+ Let the agent built into a browser read and use your components.
1250
+
1251
+ `registerTools` exposes `snapshot`, `act`, and `query` through the browser's `document.modelContext` API. It uses the bridge you created in [Get Started](./agents.md#get-started), so you register the tools once for your app.
1252
+
1253
+ For a complete example with ChatGPT prompts, follow the [Guide](./agents.md#guide).
1254
+
1255
+ ## Register the Tools
1256
+
1257
+ Import `registerTools` from the same package as your bridge:
1258
+
1259
+ ```ts
1260
+ import { registerTools } from "@ui-sdk/react/agent"
1261
+ import { agent } from "./agent"
1262
+
1263
+ const unregister = registerTools(agent, {
1264
+ allow: (step) => step.action !== "press" || confirm(`Press ${step.args[0]}?`),
1265
+ })
1266
+ ```
1267
+
1268
+ The browser's agent can now read components and call their actions. You didn't write that agent, so `registerTools` asks for an `allow` of its own. It runs for every step the browser's agent asks for, after your bridge's [permissions](./agents.md#permissions), and works the same way: return `false` to refuse a step. Here it asks you before every press. To let every step run, pass `allow: () => true`. Your change callbacks still apply.
1269
+
1270
+ Registration requires a browser that provides `document.modelContext.registerTool`. In a browser without it, `registerTools` does nothing.
1271
+
1272
+ ## Remove the Tools
1273
+
1274
+ Call the returned function when your integration is no longer needed:
1275
+
1276
+ ```ts
1277
+ unregister()
1278
+ ```
1279
+
1280
+ This removes the browser tools. You can still call methods on `agent` directly.
1281
+
1282
+ ## Tool Annotations
1283
+
1284
+ `snapshot` and `query` have `readOnlyHint: true`. All three tools have `untrustedContentHint: true` because their results include page content, such as labels and text people entered.
1285
+
1286
+ These are hints for the tool consumer. `snapshot` and `query` never open a component. Use [Permissions](./agents.md#permissions) to decide which requested actions your app accepts.
1287
+
1288
+
1289
+ # Reference
1290
+
1291
+ The bridge's methods, tool definitions, and the data they return.
1292
+
1293
+ Import these APIs from your framework's agent entry, such as `@ui-sdk/react/agent`. Vue, Svelte, Solid, and Web Components use `@ui-sdk/vue/agent`, `@ui-sdk/svelte/agent`, `@ui-sdk/solid/agent`, and `@ui-sdk/web/agent`.
1294
+
1295
+ Start with [Get Started](./agents.md#get-started) for the basic calls, or follow the [Guide](./agents.md#guide) to try them in a Vite app and ChatGPT.
1296
+
1297
+ ## createAgent(options?)
1298
+
1299
+ Creates a bridge with `snapshot`, `act`, `query`, and `subscribe`:
1300
+
1301
+ ```ts
1302
+ import { createAgent } from "@ui-sdk/react/agent"
1303
+
1304
+ const agent = createAgent()
1305
+ ```
1306
+
1307
+ Create it once, in the browser, at startup or later. It finds the components already on the page and registers new ones as they're created; importing the entry alone doesn't start registration. Use the same package as your components so they share one copy of the core.
1308
+
1309
+ | Option | Type | What It Does |
1310
+ | --------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
1311
+ | `allow` | `(step: AgentAction, context: { signal: AbortSignal }) => boolean \| Promise<boolean>` | Decides whether a resolved action may run. Returning `false` refuses it. |
1312
+ | `timeout` | `number` | How long a step waits for `allow`, in milliseconds. 5 minutes by default. Then the step fails and `signal` aborts. |
1313
+
1314
+ `AgentAction` has `ref`, `component`, optional `label`, `action`, `args`, and `confirm`, the text of a `data-agent-confirm` mark on the target. The arguments may already be resolved, such as an item's label becoming its value. Without `allow`, a step with `confirm` fails with "Needs a person". See [Permissions](./agents.md#mark-what-needs-a-person).
1315
+
1316
+ Finding a target or its choices may open UI before the final action is checked. See [Permissions](./agents.md#permissions) for the scope of `allow` and change cancellation.
1317
+
1318
+ The bridge has no `destroy` method. Stop subscriptions with their returned cleanup functions, and remove WebMCP tools with the cleanup from `registerTools`.
1319
+
1320
+ ## snapshot()
1321
+
1322
+ Returns an `AgentComponent[]` synchronously. Call it after the components have mounted:
1323
+
1324
+ ```ts
1325
+ const components = agent.snapshot()
1326
+ ```
1327
+
1328
+ Each entry describes one registered component with bound elements, or a remembered component that left the page. Components inside `data-agent-hidden` are left out. Past 100 entries, the snapshot keeps open popups, fills up to 100, and ends with `{ ref: "more", component: "more", more, use: "query", actions: [] }`, where `more` is how many it left out.
1329
+
1330
+ | Field | What It Contains |
1331
+ | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
1332
+ | `ref` | The component's identifier, such as `"c1"`. Use it in `act` and `query`. |
1333
+ | `component` | Its kind, such as `"input"`, `"select"`, or a custom primitive's name. |
1334
+ | `label` | Its accessible name, when available. |
1335
+ | `row` | Nearby row text that helps identify it, such as the person beside a menu. |
1336
+ | State keys | Its exposed state, commonly `value`, `checked`, or `open`. See [Custom Primitives](#custom-primitives). |
1337
+ | `min`, `max`, `step`, `placeholder`, `query`, `multiple`, `required`, `errors` | Additional state when present and meaningful. |
1338
+ | `items` | Included choices as `{ value, label }[]`. Use `query()` when the snapshot doesn't include the choices you need. |
1339
+ | `text` | Text from its title, description, and error parts. |
1340
+ | `buttons` | Button text inside an open component. |
1341
+ | `pending`, `error` | Whether a component action is still running, or its error as text. |
1342
+ | `disabled` | `true` when the bridge considers the component unavailable. Its `actions` are then empty. |
1343
+ | `offPage` | `true` for a remembered component that left the page. Its `actions` are empty. |
1344
+ | `confirm` | The text of a `data-agent-confirm` mark. A step on it needs the person. |
1345
+ | `source` | Where the app writes it, such as `"components/header.tsx:24"`. Development builds only; see below. |
1346
+ | `actions` | Action names available to request, such as `setValue`, `setChecked`, or `press`. |
1347
+
1348
+ A password, card, or one-time-code field's value, or one inside `data-agent-secret`, shows as `"[redacted]"` once it's filled, here, in `act`'s values and changes, and in `subscribe`. Password Input and OTP Input stay redacted while their text is shown.
1349
+
1350
+ In development, `source` comes from a `data-review-loc="file:line"` attribute on the component's element or the nearest element around it, or from Svelte's own element locations. Add the attribute at build time to let an agent go from a component to the code that renders it. Production builds leave `source` out.
1351
+
1352
+ Remembered entries keep their last exposed state. The bridge retains up to 50 of them; password, card, one-time-code, and `data-agent-secret` fields aren't kept after they leave. An off-page entry can't be acted on until its component is available again.
1353
+
1354
+ ## act(\{ steps \})
1355
+
1356
+ Runs actions in order and returns a promise of the result:
1357
+
1358
+ ```ts
1359
+ const result = await agent.act({
1360
+ steps: [
1361
+ { ref: "Project name", action: "setValue", args: ["Atlas"] },
1362
+ { ref: "Save", action: "press" },
1363
+ ],
1364
+ })
1365
+ ```
1366
+
1367
+ ### Steps
1368
+
1369
+ Each step is an `AgentStep`:
1370
+
1371
+ | Field | Type | What It Does |
1372
+ | -------- | ----------- | ------------------------------------------------------------------- |
1373
+ | `ref` | `string` | Identifies a component by its ref, label, nearby row text, or kind. |
1374
+ | `action` | `string` | Names an action, usually taken from a snapshot entry’s `actions`. |
1375
+ | `args` | `unknown[]` | Supplies the action's arguments. Defaults to an empty array. |
1376
+
1377
+ Use a specific ref when a name is ambiguous. A step can reach a component revealed by an earlier step. The bridge can also look for a target inside closed accordion sections.
1378
+
1379
+ For choices, an argument can be the item's value or its label. Labels match regardless of case and accents, like component names; values match exactly. Ambiguous item labels are refused. When a list's items show only while it's open, `setValue` with a label asks `allow` with the label before it opens the list to read them, then again with the item's value. Numeric `setValue` actions also accept text that starts with a number, such as `"90 days"`.
1380
+
1381
+ A `set` action's first argument is checked against the kind of the option it sets in the primitive's schema, like `setChecked` against `checked`. A `"boolean"` option takes only `true` or `false`, a `"number"` option takes a number or text that is one, like `"3"`, and an option read by an attribute parser takes what that parser reads, so a resizable group takes `"30,70"` as its layout but a checkbox refuses `"yes"`. A value the kind doesn't take fails the step, with the type it expects, and changes nothing.
1382
+
1383
+ `press` without arguments clicks the component's own button. `press` with a button's text finds and clicks that button inside the component, opening a closed menu or dialog if needed. `allow` decides the press before anything opens; if the button it finds carries a `data-agent-confirm` mark, `allow` is asked again with `confirm`, and a refusal closes what the press opened. The text matches a button's whole text, or whole words in it; when two buttons match, the step fails. It waits up to 15 seconds for a ui-sdk Button's action, and any other action waits the same way for its component's work. An action error fails the step; an action still running after that limit is reported as pending in the step's result.
1384
+
1385
+ Other actions call the component's API directly. They don't simulate pointer or keyboard input. Text inputs can still emit an `input` event, and the bridge triggers blur validation on a containing Field. A method returning a promise is reported as `"pending"`; the bridge doesn't wait for arbitrary API promises to resolve. A promise that rejects before the step ends fails the step with its message.
1386
+
1387
+ ### How a Step Finds Its Component
1388
+
1389
+ Every component in a snapshot has a ref, such as `c3`, that stays the same while it's on the page. A step can use that ref, or name the component in the words of the task:
1390
+
1391
+ ```ts
1392
+ const step = { ref: "Region", action: "setValue", args: ["North America"] }
1393
+ ```
1394
+
1395
+ The bridge tries these in order and uses the first that fits:
1396
+
1397
+ 1. A ref, such as `"c3"`.
1398
+ 2. A label, or whole words in a label or row text, such as `"Region"`. `"Name"` finds "Display name", not "Username".
1399
+ 3. An item the component offers, such as an accordion's "Email" section.
1400
+ 4. The component's kind, such as `"slider"`.
1401
+
1402
+ Names match regardless of case and accents, so `"Region"` finds "Région" and `"strasse"` finds "Straße".
1403
+
1404
+ When a name fits more than one component, the step fails and lists each one's ref, label and row text, such as `"Jane Doe" names 2 components: c1 Admin (Jane Doe), c4 Jane Doe. Use a ref.` Retry the step with one of those refs. That holds even when one label matches exactly, since labels and rows are page text anyone can write, like a person's display name. If exactly one of them takes the step's action, like a Textarea and the Trigger Menu on the same field for `setValue`, the step uses that one. Otherwise, if exactly one of them is an open popup, the step uses that one.
1405
+
1406
+ A returning component can reuse a remembered ref when its kind, label, and row text match. The bridge remembers refs for the last 500 that left. Identical components receive remembered refs in the order they return. Read a fresh snapshot after reordering or renaming components instead of assuming each ref still names the same control.
1407
+
1408
+ Each entry in the result's `steps` names the ref it acted on, so you can see which component changed.
1409
+
1410
+ ### Result
1411
+
1412
+ | Field | What It Contains |
1413
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1414
+ | `ok` | Whether every step ran without a bridge error. |
1415
+ | `error` | When a step fails: `{ step, message }`. The step index starts at zero. |
1416
+ | `steps` | Completed steps as `{ step, ref, result?, changes?, gone? }`. |
1417
+ | `open` | Components acted on that remain open, when any do. |
1418
+ | `appeared` | Components that appeared during the call and remain on the page. |
1419
+ | `values` | A record of exposed `value` and `checked` fields, keyed by ref and label, such as `"c1 Project name"`. Includes remembered fields; currently open components are omitted. On a page with more than 100 fields, it keeps the ones the steps acted on or changed, then others up to 100, and `more` says how many were left out. |
1420
+
1421
+ Each change is `{ component, label?, key, value }`. `gone` lists components that left the page, using their refs with labels when available.
1422
+
1423
+ A failed step stops the call. Earlier steps stay applied; there is no rollback. A callback can also cancel a change without making the step fail, so `ok: true` doesn't mean every requested value was accepted. Inspect each step's `changes` and the returned `values` to see what took effect.
1424
+
1425
+ `act` and `query` calls on the same bridge are queued in the order they're called.
1426
+
1427
+ ## query(\{ text?, within?, kind? \})
1428
+
1429
+ Finds components and their choices without listing the page, and returns a promise:
1430
+
1431
+ ```ts
1432
+ const [region] = await agent.query({ within: "Region", text: "Europe" })
1433
+ ```
1434
+
1435
+ | Input | What It Does |
1436
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
1437
+ | `text` | Matches a component's label or row text by whole words, regardless of case and accents. A list whose choices match comes back with only those choices. |
1438
+ | `within` | Names one list by its ref or label, and returns it with its choices, matching `text` when given. |
1439
+ | `kind` | Keeps one kind of component, such as `"select"`. |
1440
+
1441
+ Pass at least one. Returns an `AgentComponent[]` of up to 100 entries, ending with a `more` entry like the snapshot's when there were more. A list's `items` hold up to 50 matching choices as `{ value, label }`, with `more` set to how many matched beyond that. A choice matches `text` by a case- and accent-insensitive substring of its label or value. An unknown or ambiguous `within`, or a call with no input, returns `{ error: string }`. Components that left the page are included with `offPage: true`.
1442
+
1443
+ With `within`, the bridge reads item data or rendered parts, and never opens the component. If neither supplies choices, `items` is empty and `note` says that `act`'s `setValue` takes an item's label and opens the list to find it. Tree data includes closed branches. This method doesn't search inside closed accordion sections for the component itself.
1444
+
1445
+ ## subscribe(listener)
1446
+
1447
+ Calls the listener when a component reports a settled change and returns an unsubscribe function:
1448
+
1449
+ ```ts
1450
+ const unsubscribe = agent.subscribe((change) => {
1451
+ console.log(change.label, change.key, change.value)
1452
+ })
1453
+ ```
1454
+
1455
+ The listener receives an `ObservedChange`:
1456
+
1457
+ | Fields | What They Identify |
1458
+ | -------------------------- | ----------------------------------------------------------------------- |
1459
+ | `ref`, `id`, `component` | The bridge ref, primitive instance id, and component kind. |
1460
+ | `label`, `row` | Optional accessible name and nearby row text. |
1461
+ | `key`, `value`, `previous` | The state key, requested value, and previous value. |
1462
+ | `reason`, `source` | Why the change happened and which input caused it, including `"agent"`. |
1463
+
1464
+ Both people's and agents' changes are reported. A committed interaction, such as a slider drag, arrives once at the end. A controlled component's parent may not accept the requested value. Sensitive field values are reported as `"[redacted]"`.
1465
+
1466
+ Call `unsubscribe()` when you're done. See [Observe Changes](./agents.md#observe-changes) for keeping an agent up to date, or [History and Undo](./agents.md#history-and-undo) for recording and reverting changes.
1467
+
1468
+ ## agentTools
1469
+
1470
+ The definitions of the three tools: `snapshot`, `act`, and `query`.
1471
+
1472
+ ```ts
1473
+ import { agentTools } from "@ui-sdk/react/agent"
1474
+ ```
1475
+
1476
+ Each definition is `{ name, description, inputSchema }`. Adapt it to your model SDK's tool format, route calls to the matching bridge method in the page, and return the result to the model. These definitions don't create a model connection or run a tool loop for you.
1477
+
1478
+ ## registerTools(agent, \{ allow \})
1479
+
1480
+ Registers the three tools with `document.modelContext.registerTool` when the browser provides it:
1481
+
1482
+ ```ts
1483
+ import { registerTools } from "@ui-sdk/react/agent"
1484
+
1485
+ const unregister = registerTools(agent, {
1486
+ allow: (step) => step.action !== "press" || confirm(`Press ${step.args[0]}?`),
1487
+ })
1488
+ ```
1489
+
1490
+ `allow` is required. It decides each step the browser's agent asks for, after the bridge's own `allow`, and works the same way: return `false` to refuse the step, or `true` to let every step run. It returns a cleanup function. In a browser without WebMCP, registration does nothing. See [WebMCP](./agents.md#webmcp) for registration, cleanup, and tool annotations.
1491
+
1492
+ ## Custom Primitives
1493
+
1494
+ A primitive created with `definePrimitive` can join the bridge without a separate tool definition. Give it an accessible name, and list what an agent can do in its `agent` key:
1495
+
1496
+ ```ts
1497
+ const sizePicker = definePrimitive({
1498
+ name: "size-picker",
1499
+ // state, api and slots
1500
+ agent: { actions: ["setValue", "clear"], closed: true },
1501
+ })
1502
+ ```
1503
+
1504
+ | Key | Description |
1505
+ | --------- | --------------------------------------------------------------------------------------------------------------------- |
1506
+ | `actions` | The api methods an agent can call. Development builds warn about a name the api doesn't have. |
1507
+ | `closed` | Its value is always one of its items, so the bridge refuses text that names none of them. |
1508
+ | `items` | A function from the api to its items, when they aren't in the state's `items`. |
1509
+ | `reveals` | Its closed items hide other components, like tabs. When a step names a component it can't find, the error lists them. |
1510
+ | `shown` | A function from the state that says whether the agent sees it now, like a field only while it has errors. |
1511
+ | `secret` | Its value is always redacted, like a password's. |
1512
+
1513
+ [Your Own Components](./agents.md#your-own-components) walks through each key with examples.
1514
+
1515
+ Without `actions`, the bridge picks methods by name: `set` followed by a capital letter, such as `setValue`, and these verbs alone or followed by a capital letter:
1516
+
1517
+ `open`, `close`, `toggle`, `select`, `clear`, `next`, `previous`, `first`, `last`, `play`, `pause`, `show`, `hide`, `expand`, `collapse`, `increment`, `decrement`, `reset`, `submit`, `add`, `remove`, `insert`.
1518
+
1519
+ Methods such as `keydown`, `focus`, and `moveBy` aren't picked. Some aliases, such as `open` when `setOpen` is an action, are left out of `actions` but remain callable.
1520
+
1521
+ An action receives the supplied `args` as its method arguments. Only a `set` action's first argument whose option has a `"boolean"` or `"number"` kind or an attribute parser is checked; the rest, and every argument of an action with no option of its name, like `rate`, arrive unchecked, so check them in the method. Make optional details usable without a DOM event, and route user-facing changes through `createChange` so callbacks can report their source and cancel them.
1522
+
1523
+ The snapshot exposes the state keys named in `controlled`. Without that declaration, it includes `open`, `value`, and `checked` when present, along with the status fields listed above. Declare custom keys such as `muted` in `controlled` when they belong in the snapshot. This is separate from whether a parent is currently controlling the value.