@signalcore/ui 0.0.0-stage → 0.1.0

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 (286) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +566 -2
  3. package/dist/SignalcoreProvider.d.ts +113 -0
  4. package/dist/SignalcoreProvider.js +84 -0
  5. package/dist/automations/AppMarks.d.ts +11 -0
  6. package/dist/automations/AppMarks.js +18 -0
  7. package/dist/automations/AutomationCard.d.ts +27 -0
  8. package/dist/automations/AutomationCard.js +53 -0
  9. package/dist/automations/AutomationsWindow.d.ts +38 -0
  10. package/dist/automations/AutomationsWindow.js +57 -0
  11. package/dist/automations/Card.d.ts +46 -0
  12. package/dist/automations/Card.js +45 -0
  13. package/dist/automations/ScheduleMarks.d.ts +13 -0
  14. package/dist/automations/ScheduleMarks.js +24 -0
  15. package/dist/automations/presentationIcons.d.ts +7 -0
  16. package/dist/automations/presentationIcons.js +67 -0
  17. package/dist/automations/schedule.d.ts +25 -0
  18. package/dist/automations/schedule.js +59 -0
  19. package/dist/automations/time.d.ts +3 -0
  20. package/dist/automations/time.js +25 -0
  21. package/dist/browser/BrowserView.d.ts +43 -0
  22. package/dist/browser/BrowserView.js +94 -0
  23. package/dist/browser/room.d.ts +21 -0
  24. package/dist/browser/room.js +33 -0
  25. package/dist/browser/useBrowserSession.d.ts +17 -0
  26. package/dist/browser/useBrowserSession.js +67 -0
  27. package/dist/builder/BuilderChat.d.ts +32 -0
  28. package/dist/builder/BuilderChat.js +41 -0
  29. package/dist/builder/BuilderTranscript.d.ts +49 -0
  30. package/dist/builder/BuilderTranscript.js +62 -0
  31. package/dist/builder/StatusLine.d.ts +8 -0
  32. package/dist/builder/StatusLine.js +17 -0
  33. package/dist/builder/answer.d.ts +7 -0
  34. package/dist/builder/answer.js +8 -0
  35. package/dist/builder/blocks.d.ts +31 -0
  36. package/dist/builder/blocks.js +74 -0
  37. package/dist/builder/items.d.ts +15 -0
  38. package/dist/builder/items.js +21 -0
  39. package/dist/builder/steps.d.ts +20 -0
  40. package/dist/builder/steps.js +120 -0
  41. package/dist/builder/time.d.ts +4 -0
  42. package/dist/builder/time.js +24 -0
  43. package/dist/builder/useBuildSession.d.ts +36 -0
  44. package/dist/builder/useBuildSession.js +130 -0
  45. package/dist/builder/workingOn.d.ts +7 -0
  46. package/dist/builder/workingOn.js +15 -0
  47. package/dist/chat/AgentChat.d.ts +37 -0
  48. package/dist/chat/AgentChat.js +21 -0
  49. package/dist/chat/ChatView.d.ts +25 -0
  50. package/dist/chat/ChatView.js +34 -0
  51. package/dist/chat/Composer.d.ts +20 -0
  52. package/dist/chat/Composer.js +22 -0
  53. package/dist/chat/MessageRow.d.ts +22 -0
  54. package/dist/chat/MessageRow.js +32 -0
  55. package/dist/chat/arrival.d.ts +15 -0
  56. package/dist/chat/arrival.js +25 -0
  57. package/dist/chat/history.d.ts +24 -0
  58. package/dist/chat/history.js +45 -0
  59. package/dist/chat/moments.d.ts +25 -0
  60. package/dist/chat/moments.js +72 -0
  61. package/dist/chat/types.d.ts +12 -0
  62. package/dist/chat/types.js +1 -0
  63. package/dist/chat/useComposer.d.ts +46 -0
  64. package/dist/chat/useComposer.js +62 -0
  65. package/dist/chat/useConversation.d.ts +35 -0
  66. package/dist/chat/useConversation.js +71 -0
  67. package/dist/chat/useStickToBottom.d.ts +9 -0
  68. package/dist/chat/useStickToBottom.js +33 -0
  69. package/dist/chat/useTurns.d.ts +49 -0
  70. package/dist/chat/useTurns.js +111 -0
  71. package/dist/classes.d.ts +2 -0
  72. package/dist/classes.js +4 -0
  73. package/dist/client.d.ts +65 -0
  74. package/dist/client.js +1 -0
  75. package/dist/development.d.ts +5 -0
  76. package/dist/development.js +8 -0
  77. package/dist/flow/FlowWindow.d.ts +42 -0
  78. package/dist/flow/FlowWindow.js +126 -0
  79. package/dist/flow/index.d.ts +16 -0
  80. package/dist/flow/index.js +16 -0
  81. package/dist/flow/kinds.d.ts +23 -0
  82. package/dist/flow/kinds.js +48 -0
  83. package/dist/flow/map.d.ts +44 -0
  84. package/dist/flow/map.js +225 -0
  85. package/dist/flow/nodePaths.d.ts +7 -0
  86. package/dist/flow/nodePaths.js +17 -0
  87. package/dist/flow/relations.d.ts +42 -0
  88. package/dist/flow/relations.js +174 -0
  89. package/dist/host.d.ts +124 -0
  90. package/dist/host.js +1 -0
  91. package/dist/index.d.ts +60 -0
  92. package/dist/index.js +45 -0
  93. package/dist/live/LiveView.d.ts +28 -0
  94. package/dist/live/LiveView.js +55 -0
  95. package/dist/live/capture.d.ts +20 -0
  96. package/dist/live/capture.js +100 -0
  97. package/dist/live/follow.d.ts +26 -0
  98. package/dist/live/follow.js +72 -0
  99. package/dist/live/paint.d.ts +13 -0
  100. package/dist/live/paint.js +77 -0
  101. package/dist/live/useLiveStream.d.ts +16 -0
  102. package/dist/live/useLiveStream.js +57 -0
  103. package/dist/look.d.ts +33 -0
  104. package/dist/look.js +241 -0
  105. package/dist/messages/en.d.ts +3 -0
  106. package/dist/messages/en.js +251 -0
  107. package/dist/messages/es.d.ts +3 -0
  108. package/dist/messages/es.js +251 -0
  109. package/dist/messages/index.d.ts +16 -0
  110. package/dist/messages/index.js +33 -0
  111. package/dist/messages/types.d.ts +246 -0
  112. package/dist/messages/types.js +1 -0
  113. package/dist/parts/Button.d.ts +18 -0
  114. package/dist/parts/Button.js +17 -0
  115. package/dist/parts/Icon.d.ts +39 -0
  116. package/dist/parts/Icon.js +37 -0
  117. package/dist/parts/Input.d.ts +6 -0
  118. package/dist/parts/Input.js +11 -0
  119. package/dist/parts/Spinner.d.ts +2 -0
  120. package/dist/parts/Spinner.js +7 -0
  121. package/dist/patterns.d.ts +15 -0
  122. package/dist/patterns.js +5 -0
  123. package/dist/presets.d.ts +82 -0
  124. package/dist/presets.js +81 -0
  125. package/dist/primitives.d.ts +22 -0
  126. package/dist/primitives.js +58 -0
  127. package/dist/request/RequestCard.d.ts +34 -0
  128. package/dist/request/RequestCard.js +101 -0
  129. package/dist/request/approval/ApprovalPreview.d.ts +5 -0
  130. package/dist/request/approval/ApprovalPreview.js +27 -0
  131. package/dist/request/approval/shapes.d.ts +32 -0
  132. package/dist/request/approval/shapes.js +68 -0
  133. package/dist/request/apps.d.ts +11 -0
  134. package/dist/request/apps.js +27 -0
  135. package/dist/request/cards.d.ts +12 -0
  136. package/dist/request/cards.js +24 -0
  137. package/dist/request/deliver.d.ts +12 -0
  138. package/dist/request/deliver.js +21 -0
  139. package/dist/request/handOver.d.ts +18 -0
  140. package/dist/request/handOver.js +33 -0
  141. package/dist/request/kinds/Approval.d.ts +3 -0
  142. package/dist/request/kinds/Approval.js +9 -0
  143. package/dist/request/kinds/Choice.d.ts +3 -0
  144. package/dist/request/kinds/Choice.js +8 -0
  145. package/dist/request/kinds/Connect.d.ts +9 -0
  146. package/dist/request/kinds/Connect.js +119 -0
  147. package/dist/request/kinds/FreeText.d.ts +3 -0
  148. package/dist/request/kinds/FreeText.js +20 -0
  149. package/dist/request/kinds/HandOver.d.ts +13 -0
  150. package/dist/request/kinds/HandOver.js +20 -0
  151. package/dist/request/kinds/Secret.d.ts +7 -0
  152. package/dist/request/kinds/Secret.js +49 -0
  153. package/dist/request/kinds/Upload.d.ts +6 -0
  154. package/dist/request/kinds/Upload.js +47 -0
  155. package/dist/request/types.d.ts +83 -0
  156. package/dist/request/types.js +1 -0
  157. package/dist/safeUrl.d.ts +6 -0
  158. package/dist/safeUrl.js +8 -0
  159. package/dist/stream/builder.d.ts +176 -0
  160. package/dist/stream/builder.js +202 -0
  161. package/dist/stream/reduce.d.ts +51 -0
  162. package/dist/stream/reduce.js +58 -0
  163. package/dist/stream/untilAborted.d.ts +7 -0
  164. package/dist/stream/untilAborted.js +32 -0
  165. package/dist/stream/useRunStream.d.ts +26 -0
  166. package/dist/stream/useRunStream.js +137 -0
  167. package/dist/styles.css +2318 -0
  168. package/dist/surface/HostNode.d.ts +14 -0
  169. package/dist/surface/HostNode.js +30 -0
  170. package/dist/surface/Node.d.ts +7 -0
  171. package/dist/surface/Node.js +49 -0
  172. package/dist/surface/Surface.d.ts +21 -0
  173. package/dist/surface/Surface.js +58 -0
  174. package/dist/surface/SurfaceBoundary.d.ts +26 -0
  175. package/dist/surface/SurfaceBoundary.js +20 -0
  176. package/dist/surface/bindings.d.ts +14 -0
  177. package/dist/surface/bindings.js +37 -0
  178. package/dist/surface/check.d.ts +27 -0
  179. package/dist/surface/check.js +102 -0
  180. package/dist/surface/format.d.ts +10 -0
  181. package/dist/surface/format.js +32 -0
  182. package/dist/surface/hosts.d.ts +6 -0
  183. package/dist/surface/hosts.js +11 -0
  184. package/dist/surface/scene.d.ts +21 -0
  185. package/dist/surface/scene.js +8 -0
  186. package/dist/surface/templates/Browser.d.ts +6 -0
  187. package/dist/surface/templates/Browser.js +11 -0
  188. package/dist/surface/templates/Chart.d.ts +6 -0
  189. package/dist/surface/templates/Chart.js +13 -0
  190. package/dist/surface/templates/Choices.d.ts +3 -0
  191. package/dist/surface/templates/Choices.js +12 -0
  192. package/dist/surface/templates/Form.d.ts +3 -0
  193. package/dist/surface/templates/Form.js +44 -0
  194. package/dist/surface/templates/KeyValue.d.ts +3 -0
  195. package/dist/surface/templates/KeyValue.js +7 -0
  196. package/dist/surface/templates/Layout.d.ts +5 -0
  197. package/dist/surface/templates/Layout.js +16 -0
  198. package/dist/surface/templates/Live.d.ts +6 -0
  199. package/dist/surface/templates/Live.js +9 -0
  200. package/dist/surface/templates/Media.d.ts +5 -0
  201. package/dist/surface/templates/Media.js +13 -0
  202. package/dist/surface/templates/Slots.d.ts +3 -0
  203. package/dist/surface/templates/Slots.js +19 -0
  204. package/dist/surface/templates/Table.d.ts +3 -0
  205. package/dist/surface/templates/Table.js +12 -0
  206. package/dist/surface/templates/Text.d.ts +4 -0
  207. package/dist/surface/templates/Text.js +15 -0
  208. package/dist/surface/templates/index.d.ts +7 -0
  209. package/dist/surface/templates/index.js +32 -0
  210. package/dist/surface/types.d.ts +68 -0
  211. package/dist/surface/types.js +6 -0
  212. package/dist/theme.d.ts +62 -0
  213. package/dist/theme.js +9 -0
  214. package/dist/useTokens.d.ts +7 -0
  215. package/dist/useTokens.js +41 -0
  216. package/dist/voice/Orb.d.ts +11 -0
  217. package/dist/voice/Orb.js +42 -0
  218. package/dist/voice/VoiceWindow.d.ts +18 -0
  219. package/dist/voice/VoiceWindow.js +35 -0
  220. package/dist/voice/index.d.ts +13 -0
  221. package/dist/voice/index.js +12 -0
  222. package/dist/voice/meter.d.ts +16 -0
  223. package/dist/voice/meter.js +52 -0
  224. package/dist/voice/orbColors.d.ts +16 -0
  225. package/dist/voice/orbColors.js +75 -0
  226. package/dist/voice/pushable.d.ts +16 -0
  227. package/dist/voice/pushable.js +56 -0
  228. package/dist/voice/useDictation.d.ts +24 -0
  229. package/dist/voice/useDictation.js +60 -0
  230. package/dist/voice/useReducedMotion.d.ts +5 -0
  231. package/dist/voice/useReducedMotion.js +19 -0
  232. package/dist/voice/useVoice.d.ts +70 -0
  233. package/dist/voice/useVoice.js +197 -0
  234. package/dist/voice/useVoicePrimitives.d.ts +7 -0
  235. package/dist/voice/useVoicePrimitives.js +11 -0
  236. package/dist/voice/voice.d.ts +55 -0
  237. package/dist/voice/voice.js +9 -0
  238. package/dist/voice/webSpeech.d.ts +33 -0
  239. package/dist/voice/webSpeech.js +258 -0
  240. package/dist/widget.js +13 -0
  241. package/dist/widget.js.map +1 -0
  242. package/dist/windows/AgentSpace.d.ts +78 -0
  243. package/dist/windows/AgentSpace.js +135 -0
  244. package/dist/windows/Captions.d.ts +13 -0
  245. package/dist/windows/Captions.js +22 -0
  246. package/dist/windows/Dock.d.ts +37 -0
  247. package/dist/windows/Dock.js +22 -0
  248. package/dist/windows/Journal.d.ts +15 -0
  249. package/dist/windows/Journal.js +19 -0
  250. package/dist/windows/KindMark.d.ts +7 -0
  251. package/dist/windows/KindMark.js +31 -0
  252. package/dist/windows/Pending.d.ts +8 -0
  253. package/dist/windows/Pending.js +17 -0
  254. package/dist/windows/Stage.d.ts +18 -0
  255. package/dist/windows/Stage.js +21 -0
  256. package/dist/windows/StatusLabel.d.ts +9 -0
  257. package/dist/windows/StatusLabel.js +7 -0
  258. package/dist/windows/Strip.d.ts +17 -0
  259. package/dist/windows/Strip.js +42 -0
  260. package/dist/windows/SurfaceWindow.d.ts +20 -0
  261. package/dist/windows/SurfaceWindow.js +12 -0
  262. package/dist/windows/WindowFrame.d.ts +23 -0
  263. package/dist/windows/WindowFrame.js +22 -0
  264. package/dist/windows/hearing.d.ts +11 -0
  265. package/dist/windows/hearing.js +26 -0
  266. package/dist/windows/kinds.d.ts +47 -0
  267. package/dist/windows/kinds.js +28 -0
  268. package/dist/windows/reduce.d.ts +98 -0
  269. package/dist/windows/reduce.js +151 -0
  270. package/dist/windows/registry.d.ts +18 -0
  271. package/dist/windows/registry.js +61 -0
  272. package/dist/windows/reveal.d.ts +9 -0
  273. package/dist/windows/reveal.js +6 -0
  274. package/dist/windows/space.d.ts +21 -0
  275. package/dist/windows/space.js +6 -0
  276. package/dist/windows/status.d.ts +11 -0
  277. package/dist/windows/status.js +30 -0
  278. package/dist/windows/types.d.ts +15 -0
  279. package/dist/windows/types.js +1 -0
  280. package/dist/windows/useLoaded.d.ts +44 -0
  281. package/dist/windows/useLoaded.js +58 -0
  282. package/dist/windows/useUnread.d.ts +7 -0
  283. package/dist/windows/useUnread.js +21 -0
  284. package/dist/windows/useWindows.d.ts +70 -0
  285. package/dist/windows/useWindows.js +60 -0
  286. package/package.json +81 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SignalCore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,567 @@
1
- # Temporary Holding Version
1
+ # @signalcore/ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ SignalCore's interface as React components: what an agent shows (`Surface`), what it asks the
4
+ owner (`RequestCard`), and the two chats built from them (`AgentChat` for the people an agent
5
+ serves, `BuilderChat` for the owner who shapes it), drawn exactly the way the product draws them.
6
+ The web app uses this package, so what you embed is what we ship. For a page that is not React
7
+ there is a `<signalcore-chat>` [widget](#the-widget).
8
+
9
+ ```sh
10
+ npm install @signalcore/ui @signalcore/sdk
11
+ ```
12
+
13
+ Needs React 19. The styles are one CSS file. Besides the SDK, the only dependencies are the
14
+ graph library of the agent's map (`@xyflow/react`, which only `@signalcore/ui/flow` imports, see
15
+ [Windows](#windows-of-an-agent)) and the orb of the voice window (`orb-ui`, MIT, which only
16
+ `@signalcore/ui/voice` loads).
17
+
18
+ ```tsx
19
+ import { Signalcore } from "@signalcore/sdk";
20
+ import { RequestCard, SignalcoreProvider, Surface, useRunStream } from "@signalcore/ui";
21
+ import "@signalcore/ui/styles.css";
22
+
23
+ const client = new Signalcore({ apiKey });
24
+
25
+ function Run({ runId }: { runId: string }) {
26
+ const run = useRunStream(runId);
27
+ return (
28
+ <>
29
+ {run.surfaces.map((surface) => (
30
+ <Surface key={surface.id} surface={surface} onAction={(action) => send(action)} />
31
+ ))}
32
+ {run.requests.map((card) => (
33
+ <RequestCard key={card.request_id} card={card} />
34
+ ))}
35
+ </>
36
+ );
37
+ }
38
+
39
+ <SignalcoreProvider client={client}>
40
+ <Run runId="run_..." />
41
+ </SignalcoreProvider>;
42
+ ```
43
+
44
+ ## The two chats
45
+
46
+ One renderer, two chats, split by who is talking (ADR 0009). Both reach the API through the
47
+ provider's `client`, and both are made of parts a host can compose.
48
+
49
+ **`AgentChat`** is the chat of the people an agent **serves**: their messages and the agent's, the
50
+ agent's surfaces drawn inline (with their text twin where they cannot be), and a box to write in.
51
+ It speaks to the agent as the person and can never change it.
52
+
53
+ ```tsx
54
+ import { Signalcore } from "@signalcore/sdk";
55
+ import { AgentChat, SignalcoreProvider } from "@signalcore/ui";
56
+ import "@signalcore/ui/styles.css";
57
+
58
+ // In the browser: a token your backend minted for this person (never a secret key).
59
+ const client = new Signalcore({ endUserToken });
60
+
61
+ <SignalcoreProvider client={client}>
62
+ <AgentChat agentId="agt_…" thread="order-7" placeholder="Ask about your order" />
63
+ </SignalcoreProvider>;
64
+ ```
65
+
66
+ | Prop | What |
67
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `agentId` | The agent the person talks to. |
69
+ | `thread` | The conversation to speak in; the agent remembers it. The API's default if none. |
70
+ | `placeholder` | The box's hint. `autoFocus` focuses it. |
71
+ | `onSurfaceAction` | Tells you what the person picked or submitted. The chat has already sent it. |
72
+ | `loadHistory` | `(thread) => Promise<ChatMessage[]>`. Without it the chat reads the caller's own conversation through `endUsers.myConversation` when the client has it (an end user's token or a publishable key do), and otherwise starts empty. `messagesFromConversation` turns the API's messages into `ChatMessage`s. |
73
+
74
+ - A message is sent with `agents.invoke({ input, thread })`; a pick or a form on a surface goes
75
+ back as `agents.invoke({ action, thread })`, shown as what the person said ("Martes 10:00"). Only
76
+ the last thing said takes a pick: once the person answers, a surface is a record.
77
+ - **It never shows an approval, a connection or a secret.** When the answer has `waiting_for`, the
78
+ run paused for the owner to confirm something: the person sees a quiet "waiting for a
79
+ confirmation", the chat follows the run (`useRunStream`) until it is done, and then shows what
80
+ arrived. The send button is not usable while a turn runs; the box stays free to write in.
81
+ - Accessible: new words from the agent go to a live region, the box and the conversation are
82
+ labelled, and everything works from the keyboard (Enter sends, Shift+Enter breaks the line).
83
+
84
+ **`BuilderChat`** is the chat of the owner who **shapes** the agent: their messages, what the
85
+ builder says and does as it works (steps open to what came back), the surfaces it shows, the cards
86
+ it asks them to answer in place, and what changed.
87
+
88
+ ```tsx
89
+ <BuilderChat agentId="agt_…" sessionId={sessionId} onSession={setSessionId} />
90
+ ```
91
+
92
+ A turn streams as it happens and has a stop button (Esc too); answering a card tells the builder in
93
+ the owner's words; reopening a conversation replays it with the cards still waiting. It needs a
94
+ client with the builder's calls (`build.sessions`, `requests`), which `Signalcore` with an owner's
95
+ key has. A host that draws the conversation its own way builds it from what `BuilderChat` is made
96
+ of:
97
+
98
+ - `useBuildSession({ agentId, sessionId, onSession, model, onEvent, onTurnEnd })` gives the
99
+ transcript, `loading`, `send` and `stop`;
100
+ - `reduceTranscript` is the pure fold over the builder's events behind it (live and replayed
101
+ history give the same transcript);
102
+ - `BuilderTranscript` draws the blocks, with `renderers` for the ones that are only content
103
+ (Markdown for the builder's words, a card that activates a change): the request cards and the
104
+ surfaces are always the platform's;
105
+ - `StatusLine`, `Composer` (with slots for `tools` and `context`, and a held `value`), and
106
+ `describeStep` complete it.
107
+
108
+ ## Voice
109
+
110
+ `VoiceWindow` is a conversation by voice with an agent (ADR 0011): an orb that listens, thinks and
111
+ speaks, the state in words under it, and captions of what was heard and said. The person chooses
112
+ it by pressing the button; the agent never starts it. It is its own entry, so a page that never
113
+ speaks does not load the orb:
114
+
115
+ ```tsx
116
+ import { SignalcoreProvider } from "@signalcore/ui";
117
+ import { VoiceWindow } from "@signalcore/ui/voice";
118
+
119
+ <SignalcoreProvider client={client}>
120
+ <VoiceWindow agentId="agt_…" thread="order-7" onSurfaces={(surfaces) => place(surfaces)} />
121
+ </SignalcoreProvider>;
122
+ ```
123
+
124
+ - Its turns are the chat's (`agents.invoke`, in the thread) with `modality: "voice"`, so the agent
125
+ keeps its answer short and puts the detail in a surface, which goes to `onSurfaces`: the window
126
+ draws none. Like the chat, it never shows an approval, a connection or a secret.
127
+ - `useVoice({ agentId, thread, voice, lang, continuous, onTurn, onSurfaces })` is the loop behind
128
+ it (listen, send, think, speak, listen again) for a host that lays it out itself: `state`,
129
+ `level`, `transcript`, `captions`, `start`, `stop`, `mute`.
130
+ - A `Voice` is how to listen and speak: `listen()` (an async iterable of what is said, interim
131
+ then final), `speak(text)`, their stops, and an observable `state` and `level`. `webSpeechVoice`
132
+ is the browser's own (`SpeechRecognition` and `speechSynthesis`) and the default; a provider's
133
+ transport comes behind the same interface. The level is the microphone's while listening and an
134
+ estimate from the words while speaking, since `speechSynthesis` has no audio to measure.
135
+ - `Orb` is a replaceable primitive (`components={{ Orb }}`, props `state`, `level`, `size`,
136
+ `label`, `className`). The default is the `circle` theme of `orb-ui` in your tokens; under
137
+ `prefers-reduced-motion` it is a still disc in a ring whose outline says the state.
138
+ - `useDictation({ lang, onFinal })` (from the core entry) dictates into a message box with the
139
+ same browser voice.
140
+
141
+ ## The widget
142
+
143
+ `<signalcore-chat>` is `AgentChat` as a web component in a shadow root, for pages that are not
144
+ React. One script, with React and the SDK bundled in (`dist/widget.js`, about 108 kB gzipped):
145
+
146
+ ```html
147
+ <script src="https://signalcore.ai/app/widget.js"></script>
148
+
149
+ <!-- An anonymous visitor: a publishable key, bound to the origins it lists. -->
150
+ <signalcore-chat agent="agt_…" publishable-key="pk_live_…"></signalcore-chat>
151
+
152
+ <!-- A person of your product: a token your backend minted for them. -->
153
+ <signalcore-chat agent="agt_…" end-user-token="…" thread="order-7"></signalcore-chat>
154
+ ```
155
+
156
+ | Attribute | What |
157
+ | -------------------------------- | -------------------------------------------------------------------------------------------------------- |
158
+ | `agent`, `thread`, `placeholder` | What `AgentChat` takes. `autofocus` focuses the box. |
159
+ | `end-user-token` | The SDK's `endUserToken`. |
160
+ | `publishable-key`, `visitor-id` | The SDK's `publishableKey` and `visitorId`. Without a `visitor-id` the widget keeps one in this browser. |
161
+ | `base-url` | Where the API is (`https://signalcore.ai` by default). |
162
+ | `locale` | `es` (the default) or `en`: the language of its words and dates. See [Words](#words). |
163
+
164
+ The `client` property, set from JavaScript, wins over the attributes: give it any `UiClient` (an SDK
165
+ client with other options, your own proxy). A secret key is not an option here, on purpose. The
166
+ `messages` property, also from JavaScript, changes single words (see [Words](#words)).
167
+
168
+ It draws in a shadow root with the package's styles inside, so the page and the widget do not
169
+ touch. The page themes it from outside:
170
+
171
+ - **Tokens.** `--sc-*` custom properties on the element or above it inherit in
172
+ (`signalcore-chat { --sc-accent: #b4532a; height: 600px }`), and so do the page's font and text
173
+ color. The element is `display: block; height: 32rem` until your CSS says otherwise.
174
+ - **Dark mode.** The widget follows a `data-theme` of `light` or `dark` on itself or the nearest
175
+ ancestor (usually `<html>`), and `prefers-color-scheme` without one.
176
+ - **Parts.** Every slot is a part: `signalcore-chat::part(root)`, `::part(composer)`,
177
+ `::part(message)` (and `::part(message--person)`, `::part(message--agent)`), and the rest of
178
+ `SLOTS`.
179
+ - **Events.** `surface-action` (bubbles, composed) carries what the person picked in `detail`.
180
+
181
+ **`<signalcore-space>`** is [`AgentSpace`](#the-shell-agentspace) in the same script, taking the
182
+ same credentials: `<signalcore-space agent="agt_…" end-user-token="…" windows="flow" mode="text">`.
183
+ `windows` lists the app's windows to open (the first takes the stage), `mode` is `text` or `voice`
184
+ (the element writes the person's choice back and dispatches `mode-change`), and `viewer` is
185
+ `end_user` (the default: a widget serves the people of your product) or `owner`, for a page that
186
+ sets an owner's `client`. The script carries the conversation, the browser, the live stream and the
187
+ pages the agent places, and neither the flow map nor the voice: those windows show what the agent
188
+ said they show, and the voice stays on text. It is `display: block; height: 36rem` by default.
189
+
190
+ If it cannot start (no agent, credentials that make no sense) it shows a quiet line and says why in
191
+ the console. `pnpm build` produces `dist/widget.js` after the library; `import "@signalcore/ui/widget"`
192
+ registers the element from a bundler instead. From JavaScript,
193
+ `defineSignalcoreChat({ tag, createClient })` registers it under another tag, or with your own way
194
+ of making the client.
195
+
196
+ ## What it draws
197
+
198
+ **`Surface`** draws a surface of the `signalcore-basic.v1` catalog: cards, key-values, tables,
199
+ charts, callouts, images, links, choice lists, slot pickers, forms, a live browser view and live
200
+ streams. A
201
+ surface is data, never code: it draws only catalog components (and the host's own, see
202
+ [Components](#your-own-components)), resolves their bindings, and shows only `https://` addresses.
203
+
204
+ - What the user does (a pick, a form, a button of your own component) calls
205
+ `onAction({ surface_id, name, data, label })`, the shape the API's `Action` expects. Without
206
+ `onAction` the surface is a record: nothing on it can be pressed.
207
+ - **Every surface carries a text twin** (`fallback_text`). A surface whose payload is damaged, that
208
+ names a component nobody here knows (or one of yours that this page has not registered), or whose
209
+ drawing throws shows its text twin instead, and the rest of your page carries on.
210
+ - The browser view frames a live page in a sandbox, only from an `https://` address.
211
+
212
+ **`RequestCard`** draws what an agent needs from the owner: a question, a choice, an app to
213
+ connect, a file, an approval, a secret, or the controls of its browser. These are the owner's
214
+ decisions about safety, so the platform draws them and nobody else can: an agent cannot fake an
215
+ "Aprobar" button because the catalog has no such component, and **a host cannot replace the card
216
+ either**. It follows levels 1 to 4 of [Styling](#styling) (tokens, presets, `classNames`,
217
+ `unstyled`) and takes `apps` to say how your apps look; the provider's `components` never reach it.
218
+
219
+ Answering goes through the provider's `client` (`requests.resolve`; a secret goes to `vault.put`, a
220
+ file to `documents.addFile`, an app is checked with `connections.refresh`). Without a client,
221
+ `onResolve` is how the answer gets anywhere. Secrets and files never pass through your callbacks.
222
+
223
+ **`useRunStream(runId)`** follows a run through `client.runs.stream` and gives you the events, the
224
+ surfaces and the requests (as cards) it accumulates, and `status` (`connecting`, `streaming`,
225
+ `paused`, `done`, `failed`). When the run pauses on a request it closes the stream and opens it
226
+ again after the last event once the request is answered, wherever that happened. A host that never
227
+ draws the cards passes `{ cards: false }`, and the API is not asked what they say.
228
+
229
+ ## Windows of an agent
230
+
231
+ The chats show an agent talking; a window shows the agent itself, as its owner reads it. The web
232
+ app's agent page and map are these two, drawn by the platform from facts and never generated. They
233
+ are **owner windows**: they read the business's overview and the agent's package, so they need an
234
+ owner's credential (`agents:read`), and an end user's token gets a `403`.
235
+
236
+ ```tsx
237
+ import { AutomationsWindow, SignalcoreProvider } from "@signalcore/ui";
238
+ import { FlowWindow } from "@signalcore/ui/flow";
239
+
240
+ <SignalcoreProvider client={client}>
241
+ <AutomationsWindow agentId="agt_..." onOpenFlow={showSteps} onAsk={tellTheBuilder} />
242
+ <div style={{ height: 480 }}>
243
+ <FlowWindow agentId="agt_..." onOpen={showPart} />
244
+ </div>
245
+ </SignalcoreProvider>;
246
+ ```
247
+
248
+ - **`AutomationsWindow`** is what the agent does: a card for each channel it answers on and each
249
+ automation, with what a model wrote about the part over what the platform knows (when it runs,
250
+ its apps, its state, how the last run went). `focusedId`, `attention` and `building` follow the
251
+ builder to the automation it is working on. `AutomationCard`, `ScheduleMarks` and `AppMarks` are
252
+ its parts, exported for a page you lay out yourself.
253
+ - **`FlowWindow`** is the agent as a map of its channels, automations, skills, actions, records and
254
+ apps, with a line for each call, read from its package. A part is picked with a press (`onSelect`,
255
+ `selectedId`) and opened with a second one or Enter (`onOpen`). It is in its own entry because
256
+ `@xyflow/react` and its stylesheet are not small: the core entry imports nothing from it, and a
257
+ test keeps it that way. `agentMap`, `relationsOf` and `kindOf` are exported from it as plain
258
+ functions.
259
+ - **They fetch for themselves** when given `agentId` and the client has `agents.overview` (and
260
+ `agents.get` for the map's files); they say they are loading, or that they could not load. What
261
+ is passed as props (`overview`, `files`) is what is drawn, and nothing is asked. `apps` says how
262
+ your apps look, as in `RequestCard`.
263
+ - They follow levels 1 to 4 of [Styling](#styling); their slots are `automations`, `automation`,
264
+ `schedule`, `app-marks`, `flow`, `flow__node` and `status`.
265
+
266
+ ## The shell: `AgentSpace`
267
+
268
+ Everything a person sees of an agent is a window ([ADR 0011](../../docs/adr/0011-windows.md)), and
269
+ `AgentSpace` is where they sit: a **stage** with the one that matters now, a **strip** of the open
270
+ ones and a **dock** to talk from. The conversation is always one of them. The agent adds more as it
271
+ works, placing a page of its own (`show` with a `window`: a generated window, a surface) or opening
272
+ one of the app's (`open_window`: `automations`, `flow`, `voice`), and the person closes and reopens
273
+ them.
274
+
275
+ ```tsx
276
+ import { AgentSpace, AutomationsWindow, SignalcoreProvider } from "@signalcore/ui";
277
+ import { FlowWindow } from "@signalcore/ui/flow";
278
+ import { VoiceWindow } from "@signalcore/ui/voice";
279
+
280
+ // Hoisted: a new object each render would draw the shell again for nothing.
281
+ const windows = { automations: AutomationsWindow, flow: FlowWindow, voice: VoiceWindow };
282
+
283
+ <SignalcoreProvider client={client} viewer="owner" windows={windows}>
284
+ <AgentSpace
285
+ agentId="agt_…"
286
+ thread="order-7"
287
+ windows={["automations"]}
288
+ renderVoice={(voice) => <VoiceWindow {...voice} />}
289
+ />
290
+ </SignalcoreProvider>;
291
+ ```
292
+
293
+ - **The registry** is the provider's `windows`: the component that draws each kind of default
294
+ window, laid over the package's own (`chat`, `browser`, `live`). `automations`, `flow` and
295
+ `voice` are yours to register, each from its own import, so a page that never shows the map does
296
+ not load its graph library. Every component takes the same props (`DefaultWindowProps`: the
297
+ agent, the thread, a label, the agent's words for the window, what the host opened it with), so
298
+ the windows of this package plug in as they are. A kind with none shows what the agent said the
299
+ window shows (the text twin), never an error.
300
+ - **`viewer`** says who is looking, because the client does not: `"owner"` (the default) or
301
+ `"end_user"`. An end user's stage never opens the owner's windows (`END_USER_WINDOW_KINDS`:
302
+ `chat` and `voice`), whoever asks, the host or the agent.
303
+ - **A pick on a generated window** goes back to the agent as the next turn, the way a pick in the
304
+ chat does, and the transcript says where a page went ("→ Opened in Sales") instead of drawing it
305
+ twice. A window the person closed stays closed until the agent puts something new in it. The
306
+ stage is rebuilt from the conversation's history when it opens, with no resource of its own.
307
+ - **Voice** is the dock's `renderVoice`: when the person speaks, the dock is the voice and the
308
+ conversation collapses to captions. Without `renderVoice` there is no button for it.
309
+ - **`useWindows`** is the state of the stage without the shell, for a host that lays windows out
310
+ itself: derived from facts (`place`, `restore`, `observe` a run's journal) and from what the
311
+ person does (`focus`, `close`, `reopen`). `WindowFrame`, `SurfaceWindow`, `Strip` and `Dock` are
312
+ the pieces the shell is made of; `renderStrip` and `renderDock` replace two of them.
313
+ - Tab goes strip, stage, dock; Esc closes the window on the stage (never the conversation); under
314
+ 720px of its own width the strip becomes tabs. Its slots are `space`, `strip`, `window`, `dock`
315
+ and `captions`, and `<signalcore-space>` is it as a web component (see [The widget](#the-widget)).
316
+
317
+ ## Live streams
318
+
319
+ What a run shows as it happens (the browser it drives, a preview its code renders) is a live
320
+ stream, and **`LiveView`** watches one: the pictures it sends, drawn on a canvas as they arrive.
321
+
322
+ ```tsx
323
+ <SignalcoreProvider client={client}>
324
+ <LiveView streamId="liv_..." aspect="16:9" input={false} onStatus={(status) => {}} />
325
+ </SignalcoreProvider>
326
+ ```
327
+
328
+ - **Where it comes from.** The `live` template (a `Live` component in a card) draws a `LiveView`,
329
+ and the `Browser` component and the hand-over card draw one when they name a `stream_id`, in
330
+ preference to the hosted browser's live URL and to screenshots. `client` needs `live.connect`,
331
+ which the SDK's `Signalcore` has (`client.live`); a host that reaches the API with a cookie
332
+ supplies it with `new Live({ baseUrl })` from `@signalcore/sdk`.
333
+ - **The picture.** The canvas takes the stream's size from its first frame (`aspect` holds its shape
334
+ until then), shows the newest picture and never a queue, and has a role of `img` with a label
335
+ (`label`, "Vista en directo" by default, "Live view" in English). A quiet word says how it is
336
+ ("en directo", "reconectando", "terminado", "no disponible"; "live", "reconnecting", "ended",
337
+ "unavailable" in English). It does not animate for people who prefer reduced motion.
338
+ - **Dropped connections** are opened again, after half a second and then twice as long up to ten
339
+ seconds. A stream that ended, or that the server refuses (not signed in, not allowed, no longer
340
+ there), is not. A stream that never answered is given up on after three tries: `onStatus` then
341
+ says `unavailable`, and the browser view goes back to its live URL or its screenshots.
342
+ - **Input.** With `input`, once the stream says it takes input, the pointer (clicks, double clicks,
343
+ drags), the wheel and the keys go to the stream in its own pixels, and the canvas can be reached
344
+ with the keyboard: keys are sent only while it has the focus, Tab leaves it, and keys held when it
345
+ loses the focus are let go. Whether it is the person's turn is for the stream's producer to say; a
346
+ browser session takes input only while the agent has handed the controls over, which is when the
347
+ hand-over card turns `input` on.
348
+
349
+ ## Styling
350
+
351
+ Five levels, from nothing to everything. Each one is optional, and each works without the next.
352
+
353
+ **1. Inherit.** Dropped into any page it uses **your font, your text color and your color scheme**
354
+ (`font-family: inherit`, `color: inherit`, tints of `currentColor` for lines and muted text). Dark
355
+ mode follows `prefers-color-scheme`, and an ancestor with `data-theme="dark"` (or `"light"`)
356
+ decides when you set it. This is the whole of the work for many pages.
357
+
358
+ **2. Tokens.** Everything else is a `--sc-*` custom property with a default. Set what you want
359
+ different, in CSS or through the provider, which writes them as inline custom properties on its
360
+ own wrapper element:
361
+
362
+ | Token | What |
363
+ | ----------------------------------------------- | ----------------------------------------------------------- |
364
+ | `--sc-accent`, `--sc-accent-fg` | The owner acting: primary buttons, selection, links |
365
+ | `--sc-bg`, `--sc-surface`, `--sc-sunken` | What it sits on, a card's face, a recessed area |
366
+ | `--sc-line`, `--sc-line-strong` | Borders |
367
+ | `--sc-text`, `--sc-muted`, `--sc-subtle` | Ink (default: your text color) |
368
+ | `--sc-danger`, `--sc-ok`, `--sc-signal` | State, each with a `-soft` tint |
369
+ | `--sc-radius`, `--sc-density` | The corner of a card; how much room things take (1 is ours) |
370
+ | `--sc-font`, `--sc-font-mono`, `--sc-font-size` | Default: your font; the reading size is 14px |
371
+
372
+ ```tsx
373
+ <SignalcoreProvider theme={{ accent: "var(--primary)", radius: "var(--radius)", density: 0.9 }} />
374
+ ```
375
+
376
+ **3. Presets, found for you.** By default (`theme="auto"`) the provider asks your page's root for
377
+ the variables of the design systems it knows and maps the tokens onto the first it finds:
378
+ **shadcn** (`--primary`, `--background`, `--card`, `--border`, `--foreground`…), **Radix Themes**
379
+ (`--accent-9`, `--color-background`, `--gray-6`…) and **Material 3** (`--md-sys-color-primary`,
380
+ `--md-sys-color-surface`…). So in a shadcn app the surfaces already use your primary, your card and
381
+ your radius, in light and in dark, with no configuration. A page that uses none keeps level 1.
382
+
383
+ ```tsx
384
+ <SignalcoreProvider /> {/* auto */}
385
+ <SignalcoreProvider theme="shadcn" /> {/* skip the search: Radix needs this inside its <Theme> */}
386
+ <SignalcoreProvider theme={{ accent: "var(--brand)" }} /> {/* over the detected system */}
387
+ <SignalcoreProvider theme={{ preset: "material", radius: "4px" }} />
388
+ <SignalcoreProvider theme="none" /> {/* nothing mapped */}
389
+ ```
390
+
391
+ The search reads, and never writes: the tokens go on the provider's wrapper, not on your `:root`.
392
+ It runs in the browser only (a server renders as if none were used, and the client then finds
393
+ out), and again when `data-theme` or `class` on `<html>` changes. The tables of variables are in
394
+ `src/presets.ts`; adding a system is adding a table.
395
+
396
+ **4. Class hooks.** Every element is a named slot with a stable BEM class and attributes:
397
+ `sc-card`, `sc-card__title`, `sc-button sc-button--primary`, `data-sc="button"`,
398
+ `data-sc-mod="primary sm"`. `classNames` adds yours by slot (the list is `SLOTS`), or by slot and
399
+ modifier:
400
+
401
+ ```tsx
402
+ <SignalcoreProvider
403
+ classNames={{ card: "rounded-xl border bg-card", "button--primary": "bg-primary" }}
404
+ />
405
+ ```
406
+
407
+ **`unstyled`** leaves out the shipped styles (the elements and classes stay), for hosts that style
408
+ everything through `classNames`, as a Tailwind-only app does. The wrapper is then a plain `div`;
409
+ give it `classNames={{ ui: "contents" }}`. The shipped styles sit in a CSS layer (`signalcore`)
410
+ and under the wrapper, so your own unlayered CSS wins without a fight over specificity. A host that
411
+ has layers of its own (Tailwind v4's) says where ours go, before anything fills them, so its
412
+ utilities win over our styles on what it draws around our parts:
413
+ `@layer theme, base, signalcore, components, utilities;` first in its stylesheet, then
414
+ `@import "tailwindcss";`. Layers declared later win, so without that line ours would.
415
+
416
+ **5. Your own components.** `components` takes the primitives surfaces are built from, the
417
+ catalog's components and components of your own, by name. See
418
+ [Your own components](#your-own-components).
419
+
420
+ **Patterns.** Where our components have two layouts, you choose; the defaults are the product's.
421
+ They are classes like any other (`sc-table--compact`), and apply to our components only: a
422
+ primitive you replace draws itself.
423
+
424
+ ```tsx
425
+ <SignalcoreProvider patterns={{ table: "compact", actions: "footer", card: "raised" }} />
426
+ ```
427
+
428
+ | Pattern | Values | What |
429
+ | --------- | ---------------------------------- | ----------------------------------------------------------- |
430
+ | `table` | `comfortable` (default), `compact` | Rows with room, or close together for a lot of records |
431
+ | `actions` | `inline` (default), `footer` | A form's button under its fields, or set apart on a line |
432
+ | `card` | `flat` (default), `raised` | A bordered area, or one that also lifts a hair off the page |
433
+
434
+ ## Words
435
+
436
+ Every word the components say themselves (buttons, statuses, card headings, the builder's step
437
+ verbs, the times above a conversation) comes from a dictionary. Two ship with the package, Spanish
438
+ (the default, as the product speaks) and English, and the provider picks one and lets you change
439
+ single words:
440
+
441
+ ```tsx
442
+ <SignalcoreProvider locale="en" messages={{ send: "Post", verbs: { edit: "Change" } }} />
443
+ ```
444
+
445
+ - **`locale`** is `"es"` (default) or `"en"`. It decides the words and how dates and times are
446
+ written ("Hoy 10:32" and "12 oct" in Spanish, "Today 10:32 AM" and "Oct 12" in English).
447
+ - **`messages`** is any part of the dictionary, laid over the locale's: a word you leave out stays
448
+ as it was. `Messages` is its type, and the key names are in `src/messages/types.ts`. Words that
449
+ carry a name or a number are functions (`connectWith: (app) => "Connect " + app`), and the
450
+ groups (`verbs`, what the builder is doing; `liveStatus`; `voiceStates`; and the windows'
451
+ `automations` and `flow`) can change one entry at a time, at any depth
452
+ (`{ flow: { verbs: { calls: "runs" } } }`). Define the object
453
+ outside the render, so the components are not drawn again for nothing.
454
+ - **Another language.** The package has the two. For a third, give every word as `messages` (the
455
+ type makes sure none is missing) over the `locale` whose dates are closest.
456
+ - **The widget** takes the language as an attribute and the words as a property:
457
+
458
+ ```html
459
+ <signalcore-chat agent="agt_…" publishable-key="pk_live_…" locale="en"></signalcore-chat>
460
+ <script>
461
+ document.querySelector("signalcore-chat").messages = { send: "Post" };
462
+ </script>
463
+ ```
464
+
465
+ - **Not components.** `describeStep` and `workingOn`, which a host that draws the builder's
466
+ conversation itself calls, are Spanish unless they are handed the words:
467
+ `describeStep(name, input, useMessages())`.
468
+
469
+ ## Your own components
470
+
471
+ `components` on the provider takes your own components, by name. Three kinds:
472
+
473
+ - **Primitives** (`Button`, `Card`, `Table`, `Input`, `Select`, with typed props): every surface is
474
+ built from yours, so the catalog is drawn with your design system. They add nothing to the
475
+ catalog; a surface cannot name them. The cards that ask the owner to decide never use them.
476
+ `Orb`, the voice window's, is one too (see [Voice](#voice)).
477
+ - **Catalog components** (`SlotPicker`): a surface that uses the name is drawn by your component,
478
+ given the properties the agent filled (bindings resolved), its `children` drawn, and `onAction`
479
+ to report what the user does. They are not checked: read them loosely.
480
+ - **Components of your own** (`PropertyCard`): things of your product that an agent shows in your
481
+ design, with props the API checks. See below.
482
+
483
+ ```tsx
484
+ <SignalcoreProvider components={{ Button: MyButton, SlotPicker: MySlots, PropertyCard }} />
485
+ ```
486
+
487
+ ### A component of your own
488
+
489
+ An agent can show a listing, a quote or a ticket as your product draws it, if SignalCore knows the
490
+ component: its name, a schema for its props and a text twin. It is registered twice, in the page
491
+ and with the API.
492
+
493
+ **In the page**, a React component that takes the props and, when the user can answer it, reports
494
+ with `onAction`:
495
+
496
+ ```tsx
497
+ import { AgentChat, SignalcoreProvider, type HostAction } from "@signalcore/ui";
498
+
499
+ interface PropertyCardProps {
500
+ address: string;
501
+ price: number;
502
+ onAction?: (action: HostAction) => void;
503
+ }
504
+
505
+ function PropertyCard({ address, price, onAction }: PropertyCardProps) {
506
+ const rate = (fit: boolean) =>
507
+ onAction?.({
508
+ name: "rate",
509
+ data: { address, fit },
510
+ label: `${fit ? "Good fit" : "Not a fit"}: ${address}`,
511
+ });
512
+ return (
513
+ <article>
514
+ <h3>{address}</h3>
515
+ <p>{price} €</p>
516
+ <button disabled={!onAction} onClick={() => rate(true)}>
517
+ Good fit
518
+ </button>
519
+ <button disabled={!onAction} onClick={() => rate(false)}>
520
+ Not a fit
521
+ </button>
522
+ </article>
523
+ );
524
+ }
525
+
526
+ <SignalcoreProvider components={{ PropertyCard }}>
527
+ <AgentChat agentId="agt_…" />
528
+ </SignalcoreProvider>;
529
+ ```
530
+
531
+ **With the API**, once for your organization: `client.ui.components.put(...)` in the SDKs takes the
532
+ name, a JSON Schema for the props and the text twin (see the SDK's reference for its fields). The
533
+ agent then uses the component in a surface with a node of its own kind:
534
+
535
+ ```json
536
+ {
537
+ "id": "home",
538
+ "type": "host",
539
+ "component": "PropertyCard",
540
+ "props": { "address": "Calle Mayor 3", "price": 250000 }
541
+ }
542
+ ```
543
+
544
+ What your component is given, and what it can count on:
545
+
546
+ - **`props`, exactly as the agent sent them.** The API validated them against the schema you
547
+ registered before it sent the surface, so you can rely on their shape. They are values: a
548
+ binding (`{ "path": … }`) is not looked up in them. There are no `children`.
549
+ - **`onAction({ name, data, label })`**, to report what the user does. It goes back through the
550
+ surface's action path like a pick on a list: `AgentChat` sends it to the agent as what the
551
+ person said (`label`, "Good fit: Calle Mayor 3"), and `Surface` hands your `onAction` the
552
+ `{ surface_id, name, data, label }` the API's `Action` expects. It is `undefined` when the surface
553
+ takes no actions (it is a record of what was shown), so disable what answers. The node's props
554
+ cannot replace it.
555
+ - **The text twin is already in the surface's `fallback_text`.** The API appends your component's
556
+ twin to the surface's, so where a surface cannot be drawn whole the user reads all of it in words,
557
+ never an empty box, and the rest of your page carries on. That happens when this page has not
558
+ registered the name (a development build says which in the console), when the props are not an
559
+ object, and when your component throws.
560
+ - **A primitive's name is not yours to use.** A node cannot name `Button`, `Card`, `Table`, `Input`
561
+ or `Select`, and `RequestCard` never draws your components.
562
+
563
+ ## Your own client
564
+
565
+ `client` is the part of the SDK the components call (`UiClient`): `Signalcore` has all of it. If you
566
+ reach the API another way (a session cookie instead of a key), supply the same calls (for streams,
567
+ `live: new Live({ baseUrl: location.origin })`).