@alexkroman1/aai-ui 0.12.3 → 0.13.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 (201) hide show
  1. package/README.md +257 -0
  2. package/dist/_colors-CpZO-88A.js +78 -0
  3. package/dist/_module-url-C13kAJ87.js +15 -0
  4. package/dist/_page-shell.d.ts +11 -0
  5. package/dist/_recover-run.d.ts +79 -0
  6. package/dist/_repeat-until.d.ts +30 -0
  7. package/dist/_run-controls.d.ts +33 -0
  8. package/dist/_submission-state.d.ts +92 -0
  9. package/dist/_tap-to-talk-state.d.ts +88 -0
  10. package/dist/_utils-Mi-SCFA1.js +47 -0
  11. package/dist/_utils.d.ts +26 -0
  12. package/dist/_web-storage.d.ts +43 -0
  13. package/dist/_workflow-api-ref.d.ts +37 -0
  14. package/dist/_workflow-files.d.ts +34 -0
  15. package/dist/_workflow-test-defs.d.ts +38 -0
  16. package/dist/aai-logo-D8pMVn9_.js +85 -0
  17. package/dist/agent-state-labels.d.ts +65 -0
  18. package/dist/agent-state.d.ts +108 -0
  19. package/dist/audio/capture.d.ts +44 -0
  20. package/dist/audio/index.d.ts +18 -0
  21. package/dist/audio/index.js +431 -0
  22. package/dist/audio/preconnect.d.ts +87 -0
  23. package/dist/audio/voice-io.d.ts +112 -0
  24. package/dist/build-default-client.d.ts +1 -0
  25. package/dist/chat-view-BCbI7gD4.js +396 -0
  26. package/dist/client-config-C7fQrioM.js +139 -0
  27. package/dist/client-config.d.ts +119 -0
  28. package/dist/client-dir.d.ts +44 -0
  29. package/dist/client-dir.js +58 -0
  30. package/dist/client-identity.d.ts +131 -0
  31. package/dist/client-tool.d.ts +51 -0
  32. package/dist/components/_colors.d.ts +73 -0
  33. package/dist/components/_form-readiness.d.ts +56 -0
  34. package/dist/components/_form-values.d.ts +19 -0
  35. package/dist/components/aai-logo.d.ts +12 -0
  36. package/dist/components/audio-result.d.ts +94 -0
  37. package/dist/components/auto-scroll.d.ts +74 -0
  38. package/dist/components/bullet-list.d.ts +74 -0
  39. package/dist/components/button.d.ts +29 -21
  40. package/dist/components/button.js +62 -23
  41. package/dist/components/chat-view.d.ts +24 -18
  42. package/dist/components/chat-view.js +2 -60
  43. package/dist/components/console-shell.d.ts +85 -0
  44. package/dist/components/controls.d.ts +21 -6
  45. package/dist/components/controls.js +31 -19
  46. package/dist/components/conversation-view.d.ts +130 -0
  47. package/dist/components/eyebrow.d.ts +12 -0
  48. package/dist/components/facts.d.ts +81 -0
  49. package/dist/components/form-fields.d.ts +142 -0
  50. package/dist/components/form-types.d.ts +73 -0
  51. package/dist/components/form.d.ts +96 -0
  52. package/dist/components/markdown.d.ts +57 -0
  53. package/dist/components/message-list.d.ts +34 -7
  54. package/dist/components/message-list.js +2 -75
  55. package/dist/components/session-controls.d.ts +125 -0
  56. package/dist/components/session-error-banner.d.ts +69 -0
  57. package/dist/components/session-state-dot.d.ts +111 -0
  58. package/dist/components/sidebar-layout.d.ts +31 -10
  59. package/dist/components/sidebar-layout.js +32 -12
  60. package/dist/components/start-screen.d.ts +17 -6
  61. package/dist/components/start-screen.js +34 -14
  62. package/dist/components/tool-call-block.d.ts +15 -14
  63. package/dist/components/tool-call-block.js +2 -117
  64. package/dist/components/tool-call-row.d.ts +75 -0
  65. package/dist/components/tool-config-context.d.ts +22 -0
  66. package/dist/components/upload-progress.d.ts +76 -0
  67. package/dist/components/url-chips.d.ts +29 -0
  68. package/dist/components/workflow-fields.d.ts +46 -0
  69. package/dist/components/workflow-pending-note.d.ts +81 -0
  70. package/dist/components/workflow-progress.d.ts +81 -0
  71. package/dist/components/workflow-run-error.d.ts +48 -0
  72. package/dist/components/workflow-run-panel.d.ts +79 -0
  73. package/dist/context.d.ts +288 -0
  74. package/dist/context.js +382 -0
  75. package/dist/conversation-log.d.ts +88 -0
  76. package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
  77. package/dist/default-client/assets/audio-cGFGID-2.js +1 -0
  78. package/dist/default-client/assets/capture-processor-BUABoo3_.js +90 -0
  79. package/dist/default-client/assets/index-8ihb1Urm.css +2 -0
  80. package/dist/default-client/assets/index-BQURVARl.js +126 -0
  81. package/dist/default-client/assets/playback-processor-ABOa11j0.js +314 -0
  82. package/dist/default-client/favicon.ico +0 -0
  83. package/dist/default-client/index.html +15 -0
  84. package/dist/default-client.d.ts +23 -0
  85. package/dist/define-client.d.ts +162 -26
  86. package/dist/define-client.js +201 -55
  87. package/dist/eyebrow-UfmSz9yy.js +27 -0
  88. package/dist/hooks-DN4gHYsj.js +182 -0
  89. package/dist/hooks.d.ts +172 -0
  90. package/dist/hooks.js +3 -0
  91. package/dist/hooks.test-d.d.ts +14 -0
  92. package/dist/inbox-protocol.d.ts +128 -0
  93. package/dist/inbox.d.ts +132 -0
  94. package/dist/index.d.ts +126 -29
  95. package/dist/index.js +5500 -15
  96. package/dist/internal.d.ts +43 -0
  97. package/dist/internal.js +8 -0
  98. package/dist/linked-client.d.ts +70 -0
  99. package/dist/message-list-DHZcLhRA.js +682 -0
  100. package/dist/notice-player.d.ts +70 -0
  101. package/dist/page.d.ts +114 -0
  102. package/dist/phone.d.ts +50 -0
  103. package/dist/route-fetch.d.ts +79 -0
  104. package/dist/session/audio-effects.d.ts +31 -0
  105. package/dist/session/audio-setup.d.ts +57 -0
  106. package/dist/session/audio-state.d.ts +157 -0
  107. package/dist/session/browser-session.d.ts +30 -0
  108. package/dist/session/close.d.ts +26 -0
  109. package/dist/session/dial.d.ts +65 -0
  110. package/dist/session/handshake.d.ts +60 -0
  111. package/dist/session/index.d.ts +17 -0
  112. package/dist/session/index.js +2 -0
  113. package/dist/session/messages.d.ts +63 -0
  114. package/dist/session/mic.d.ts +64 -0
  115. package/dist/session/preconnect.d.ts +48 -0
  116. package/dist/session/reconnect.d.ts +25 -0
  117. package/dist/session/resume-store.d.ts +57 -0
  118. package/dist/session/state.d.ts +146 -0
  119. package/dist/session/ticket.d.ts +44 -0
  120. package/dist/session/types.d.ts +476 -0
  121. package/dist/session/url.d.ts +17 -0
  122. package/dist/session/user-turn.d.ts +55 -0
  123. package/dist/session-CE0Ipsb3.js +2129 -0
  124. package/dist/stored-value.d.ts +103 -0
  125. package/dist/tool-call-block-uXtMOUWN.js +204 -0
  126. package/dist/tool-config-context-Es4YUzV2.js +19 -0
  127. package/dist/types.d.ts +272 -34
  128. package/dist/types.js +35 -4
  129. package/dist/upload/files.d.ts +83 -0
  130. package/dist/upload/index.d.ts +15 -0
  131. package/dist/upload/pause.d.ts +31 -0
  132. package/dist/upload/recall.d.ts +67 -0
  133. package/dist/upload/report.d.ts +26 -0
  134. package/dist/upload/session.d.ts +99 -0
  135. package/dist/url-chips-oyy_Eiqh.js +239 -0
  136. package/dist/use-client-runs.d.ts +87 -0
  137. package/dist/use-conversation-log.d.ts +86 -0
  138. package/dist/use-conversation.d.ts +131 -0
  139. package/dist/use-copy.d.ts +74 -0
  140. package/dist/use-download-url.d.ts +84 -0
  141. package/dist/use-flash.d.ts +61 -0
  142. package/dist/use-inbox.d.ts +101 -0
  143. package/dist/use-push-to-talk.d.ts +105 -0
  144. package/dist/use-route-mutation.d.ts +113 -0
  145. package/dist/use-route.d.ts +82 -0
  146. package/dist/use-run-key.d.ts +133 -0
  147. package/dist/use-session-controls.d.ts +69 -0
  148. package/dist/use-session-id.d.ts +57 -0
  149. package/dist/use-tap-to-talk.d.ts +138 -0
  150. package/dist/use-user-transcript-uyHhzy4d.js +73 -0
  151. package/dist/use-user-transcript.d.ts +75 -0
  152. package/dist/use-workflow-form.d.ts +285 -0
  153. package/dist/use-workflow-progress.d.ts +137 -0
  154. package/dist/use-workflow-run-DdhWmbx-.js +697 -0
  155. package/dist/use-workflow-run.d.ts +100 -0
  156. package/dist/use-workflow-runs.d.ts +71 -0
  157. package/dist/use-workflow-stream.d.ts +154 -0
  158. package/dist/use-workflows.d.ts +77 -0
  159. package/dist/workflow-client.d.ts +136 -0
  160. package/dist/workflow-def-types.d.ts +31 -0
  161. package/dist/workflow-events.d.ts +39 -0
  162. package/dist/workflow-field-kind.d.ts +95 -0
  163. package/dist/workflow-status-labels.d.ts +35 -0
  164. package/dist/worklets/_module-url.d.ts +10 -0
  165. package/dist/worklets/_playback-bench-harness.d.ts +181 -0
  166. package/dist/worklets/_playback-bench-host.d.ts +63 -0
  167. package/dist/worklets/_playback-bench-page.d.ts +65 -0
  168. package/dist/worklets/_tts-trace-harness.d.ts +142 -0
  169. package/dist/worklets/_worklet-test-utils.d.ts +27 -0
  170. package/dist/worklets/capture-processor.d.ts +4 -2
  171. package/dist/worklets/capture-processor.js +73 -33
  172. package/dist/worklets/playback-processor.d.ts +4 -2
  173. package/dist/worklets/playback-processor.js +258 -42
  174. package/package.json +57 -32
  175. package/styles.css +152 -30
  176. package/dist/audio.d.ts +0 -45
  177. package/dist/audio.js +0 -121
  178. package/dist/client-context.d.ts +0 -33
  179. package/dist/client-context.js +0 -15
  180. package/dist/client-handler.d.ts +0 -43
  181. package/dist/components/app.d.ts +0 -23
  182. package/dist/components/app.js +0 -41
  183. package/dist/components/error-banner.d.ts +0 -21
  184. package/dist/components/error-banner.js +0 -27
  185. package/dist/components/message-bubble.d.ts +0 -23
  186. package/dist/components/message-bubble.js +0 -35
  187. package/dist/components/state-indicator.d.ts +0 -29
  188. package/dist/components/state-indicator.js +0 -37
  189. package/dist/components/thinking-indicator.d.ts +0 -16
  190. package/dist/components/thinking-indicator.js +0 -34
  191. package/dist/components/tool-icons.d.ts +0 -17
  192. package/dist/components/tool-icons.js +0 -128
  193. package/dist/components/transcript.d.ts +0 -25
  194. package/dist/components/transcript.js +0 -35
  195. package/dist/session-CWB7vmqz.js +0 -429
  196. package/dist/session.d.ts +0 -69
  197. package/dist/session.js +0 -2
  198. package/dist/signals.d.ts +0 -120
  199. package/dist/signals.js +0 -161
  200. package/dist/tsdown.config.d.ts +0 -2
  201. package/dist/types.test-d.d.ts +0 -7
@@ -1,74 +1,220 @@
1
- import { ClientConfigProvider } from "./client-context.js";
2
- import { SessionProvider, createSessionControls } from "./signals.js";
3
- import { t as createVoiceSession } from "./session-CWB7vmqz.js";
4
- import { render } from "preact";
5
- import { batch, signal } from "@preact/signals";
6
- import { jsx } from "preact/jsx-runtime";
7
- //#region define-client.tsx
1
+ import { t as ChatView } from "./chat-view-BCbI7gD4.js";
2
+ import { n as pageBaseUrl, r as setPageTitle } from "./_utils-Mi-SCFA1.js";
3
+ import { n as fetchClientConfig } from "./client-config-C7fQrioM.js";
4
+ import { t as createBrowserSession } from "./session-CE0Ipsb3.js";
5
+ import { SessionProvider, ThemeProvider } from "./context.js";
6
+ import { t as ToolConfigContext } from "./tool-config-context-Es4YUzV2.js";
7
+ import { SidebarLayout } from "./components/sidebar-layout.js";
8
+ import { StartScreen } from "./components/start-screen.js";
9
+ import { createElement, useEffect, useState } from "react";
10
+ import { jsx } from "react/jsx-runtime";
11
+ import { flushSync } from "react-dom";
12
+ import { createRoot } from "react-dom/client";
13
+ //#region src/define-client.tsx
14
+ /** @jsxImportSource react */
15
+ /**
16
+ * The element a mount renders into.
17
+ *
18
+ * Exported so `mountPage()` resolves its target the same way `mountClient()` does — the
19
+ * default selector and the "element not found" sentence are part of what an
20
+ * author has learned, and a second copy is how the two mounts come to disagree
21
+ * about which one they were given.
22
+ *
23
+ * @internal
24
+ */
8
25
  function resolveContainer(target = "#app") {
9
26
  if (typeof target !== "string") return target;
10
27
  const el = document.querySelector(target);
11
- if (!el) throw new Error(`Element not found: ${target}`);
28
+ if (!el) throw new Error(`Element not found: ${target} — no element matches that selector when the mount runs. Add \`<div id="app"></div>\` (or the element your selector names) to the page before the script, or pass the element itself as the mount's \`target\` option.`);
12
29
  return el;
13
30
  }
14
31
  /**
32
+ * Render `node` into `container` and hand back its teardown.
33
+ *
34
+ * Shared by {@link mountClient} and `mountPage()` because the plumbing is identical and
35
+ * the two decisions in it are not obvious enough to be re-derived: the root is
36
+ * created here rather than by the caller so nothing else can hold one, and the
37
+ * render is `flushSync` so the mount is observable to the caller's NEXT
38
+ * STATEMENT (and to a test) rather than scheduled — `mountClient()` returns a handle
39
+ * whose session is expected to be live, and `mountPage()` returns one a caller may
40
+ * dispose immediately.
41
+ *
42
+ * `onDispose` runs after the unmount, which is the order `mountClient()` needs: the
43
+ * tree comes down before the session under it goes away.
44
+ *
45
+ * @internal
46
+ */
47
+ function mountRoot(container, node, onDispose) {
48
+ const root = createRoot(container);
49
+ flushSync(() => {
50
+ root.render(node);
51
+ });
52
+ return {
53
+ dispose() {
54
+ root.unmount();
55
+ onDispose?.();
56
+ },
57
+ [Symbol.dispose]() {
58
+ this.dispose();
59
+ }
60
+ };
61
+ }
62
+ /**
63
+ * Default shell rendered in config tier.
64
+ * Wraps StartScreen → (SidebarLayout →) ChatView.
65
+ */
66
+ function DefaultShell({ name, icon, subtitle, buttonText, Sidebar, ...layout }) {
67
+ const chat = /* @__PURE__ */ jsx(ChatView, {
68
+ title: name,
69
+ icon
70
+ });
71
+ return /* @__PURE__ */ jsx(StartScreen, {
72
+ title: name,
73
+ icon,
74
+ subtitle,
75
+ buttonText,
76
+ children: Sidebar ? /* @__PURE__ */ jsx(SidebarLayout, {
77
+ sidebar: /* @__PURE__ */ jsx(Sidebar, {}),
78
+ sidebarWidth: layout.sidebarWidth,
79
+ sidebarPosition: layout.sidebarPosition,
80
+ children: chat
81
+ }) : chat
82
+ });
83
+ }
84
+ /**
85
+ * Config-tier root: fetches the server-declared display name via
86
+ * `GET client-config` and renders the chat shell. The shell renders
87
+ * immediately — optimistically — while the lookup is in flight; servers
88
+ * without the endpoint (every lookup failure resolves to the empty default)
89
+ * work exactly as before.
90
+ *
91
+ * **The lookup is SKIPPED when the caller already named the agent.** The
92
+ * response's only consumer here is the fallback below, so with an explicit
93
+ * `mountClient({ name })` the request was issued and its answer thrown away — and on
94
+ * the platform this endpoint is the BROKER, so the discarded request is one that
95
+ * can boot a sandbox. The session's own per-attempt lookup
96
+ * (`session/browser-session.ts`'s URL provider) is a different question and deliberately
97
+ * left alone: it re-brokers on every connection attempt, which is what makes a
98
+ * reconnect land on a REPLACEMENT sandbox, so it may not be served from
99
+ * anything this render already has.
100
+ */
101
+ function DefaultRoot({ platformUrl, ...display }) {
102
+ const { name } = display;
103
+ const [resolved, setResolved] = useState(null);
104
+ const needsLookup = name === void 0;
105
+ useEffect(() => {
106
+ if (!needsLookup) return;
107
+ let cancelled = false;
108
+ fetchClientConfig(platformUrl).then((cfg) => {
109
+ if (!cancelled) setResolved(cfg);
110
+ });
111
+ return () => {
112
+ cancelled = true;
113
+ };
114
+ }, [platformUrl, needsLookup]);
115
+ return /* @__PURE__ */ jsx(DefaultShell, {
116
+ ...display,
117
+ name: name ?? resolved?.name
118
+ });
119
+ }
120
+ /**
121
+ * The tree under the providers: the caller's component, or the default shell.
122
+ *
123
+ * A custom component owns the whole page, so there is no header to hang a
124
+ * `sidebar` off — but the layout the default shell uses is right here, and
125
+ * `mountClient({ component, sidebar })` names one pane and one aside, which is
126
+ * exactly what it renders. Honouring the combination rather than ignoring it is
127
+ * the same call this file already made for `name` and `tools`.
128
+ */
129
+ function rootFor(config, platformUrl) {
130
+ const Custom = config.component;
131
+ const Sidebar = config.sidebar;
132
+ if (!Custom) return /* @__PURE__ */ jsx(DefaultRoot, {
133
+ platformUrl,
134
+ name: config.name,
135
+ icon: config.icon,
136
+ subtitle: config.subtitle,
137
+ buttonText: config.buttonText,
138
+ Sidebar,
139
+ sidebarWidth: config.sidebarWidth,
140
+ sidebarPosition: config.sidebarPosition
141
+ });
142
+ if (!Sidebar) return /* @__PURE__ */ jsx(Custom, {});
143
+ return /* @__PURE__ */ jsx(SidebarLayout, {
144
+ sidebar: /* @__PURE__ */ jsx(Sidebar, {}),
145
+ sidebarWidth: config.sidebarWidth,
146
+ sidebarPosition: config.sidebarPosition,
147
+ children: /* @__PURE__ */ jsx(Custom, {})
148
+ });
149
+ }
150
+ /**
15
151
  * Define and mount a client UI for a voice agent.
16
152
  *
17
- * Creates a {@link VoiceSession}, wraps it in
18
- * {@link SessionSignals}, and renders the component
19
- * inside a {@link SessionProvider}.
153
+ * **Config only:** leave `component` out and the default shell renders
154
+ * (StartScreen + ChatView, optional sidebar).
155
+ *
156
+ * **A custom component:** pass `component` and it is rendered inside the same
157
+ * providers instead of the default shell — beside a `sidebar` if one is given,
158
+ * in the same {@link SidebarLayout}. A provided `name` then also sets
159
+ * `document.title`, there being no shell header to show it in.
160
+ *
161
+ * Mounts into `target` — a CSS selector or DOM element, defaulting to
162
+ * `"#app"` — and throws `Element not found: <target>` when the selector
163
+ * matches nothing.
164
+ *
165
+ * @example The default shell
166
+ * ```tsx
167
+ * import { mountClient } from "@alexkroman1/aai-ui";
168
+ *
169
+ * function OrderPanel() {
170
+ * return <div>Cart</div>;
171
+ * }
172
+ *
173
+ * mountClient({
174
+ * name: "Pizza Ordering",
175
+ * theme: { bg: "#1a1a1a", primary: "#e55" },
176
+ * sidebar: OrderPanel,
177
+ * tools: { add_pizza: { icon: "🍕", label: "Adding pizza" } },
178
+ * });
179
+ * ```
180
+ *
181
+ * @example A custom component
182
+ * ```tsx
183
+ * import { mountClient, useSession } from "@alexkroman1/aai-ui";
184
+ *
185
+ * function MyCustomApp() {
186
+ * const session = useSession();
187
+ * return <div>{session.state}</div>;
188
+ * }
189
+ *
190
+ * mountClient({ component: MyCustomApp });
191
+ * ```
20
192
  *
21
- * @param Component - The Preact component to render.
22
- * @param options - Client options (target element, platform URL, theme).
23
193
  * @returns A {@link ClientHandle} for cleanup.
24
194
  * @throws If the target element is not found in the DOM.
25
195
  *
26
196
  * @public
27
197
  */
28
- function defineClient(Component, options) {
29
- const container = resolveContainer(options?.target);
30
- const session = createVoiceSession({
31
- platformUrl: options?.platformUrl ?? globalThis.location.origin + globalThis.location.pathname,
32
- reactiveFactory: signal,
33
- batch,
34
- onSessionId: options?.onSessionId,
35
- resumeSessionId: options?.resumeSessionId,
36
- ...options?.WebSocket ? { WebSocket: options.WebSocket } : {}
198
+ function mountClient(config) {
199
+ const container = resolveContainer(config.target);
200
+ const platformUrl = config.platformUrl ?? pageBaseUrl();
201
+ const session = createBrowserSession({
202
+ platformUrl,
203
+ onSessionId: config.onSessionId,
204
+ resumeSessionId: config.resumeSessionId,
205
+ location: config.location,
206
+ phone: config.phone,
207
+ client: config.client,
208
+ token: config.token,
209
+ WebSocket: config.WebSocket
37
210
  });
38
- const signals = createSessionControls(session);
39
- const clientConfig = {
40
- title: options?.title,
41
- theme: options?.theme
42
- };
43
- if (options?.theme) {
44
- const t = options.theme;
45
- const el = container;
46
- if (t.bg) el.style.setProperty("--color-aai-bg", t.bg);
47
- if (t.primary) el.style.setProperty("--color-aai-primary", t.primary);
48
- if (t.text) el.style.setProperty("--color-aai-text", t.text);
49
- if (t.surface) el.style.setProperty("--color-aai-surface", t.surface);
50
- if (t.border) el.style.setProperty("--color-aai-border", t.border);
51
- }
52
- render(/* @__PURE__ */ jsx(ClientConfigProvider, {
53
- value: clientConfig,
54
- children: /* @__PURE__ */ jsx(SessionProvider, {
55
- value: signals,
56
- children: /* @__PURE__ */ jsx(Component, {})
57
- })
58
- }), container);
59
- const handle = {
211
+ setPageTitle(config.name);
212
+ const rootNode = rootFor(config, platformUrl);
213
+ const toolConfig = config.tools ?? {};
214
+ return {
60
215
  session,
61
- signals,
62
- dispose() {
63
- render(null, container);
64
- signals.dispose();
65
- session.disconnect();
66
- },
67
- [Symbol.dispose]() {
68
- handle.dispose();
69
- }
216
+ ...mountRoot(container, createElement(ToolConfigContext.Provider, { value: toolConfig }, createElement(ThemeProvider, { value: config.theme }, createElement(SessionProvider, { value: session }, rootNode))), () => session[Symbol.dispose]())
70
217
  };
71
- return handle;
72
218
  }
73
219
  //#endregion
74
- export { defineClient };
220
+ export { mountClient, mountRoot, resolveContainer };
@@ -0,0 +1,27 @@
1
+ import { useTheme } from "./context.js";
2
+ import clsx from "clsx";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/components/eyebrow.tsx
5
+ /** @jsxImportSource react */
6
+ /**
7
+ * Eyebrow label — the design system's small outlined pill with ALL-CAPS,
8
+ * tracked text ("VOICE AGENT", "LISTENING", …). Used as a section label on
9
+ * the start screen and as the live status chip in the chat header.
10
+ *
11
+ * @internal
12
+ */
13
+ function Eyebrow({ children, className, style, ...rest }) {
14
+ const theme = useTheme();
15
+ return /* @__PURE__ */ jsx("span", {
16
+ className: clsx("inline-flex items-center gap-1.5 px-2 py-1 rounded-aai border leading-none", "font-aai text-[10px] font-medium tracking-[1.2px] uppercase", className),
17
+ style: {
18
+ borderColor: theme.border,
19
+ color: theme.text,
20
+ ...style
21
+ },
22
+ ...rest,
23
+ children
24
+ });
25
+ }
26
+ //#endregion
27
+ export { Eyebrow as t };
@@ -0,0 +1,182 @@
1
+ import { a as tryParseJSON } from "./_utils-Mi-SCFA1.js";
2
+ import { useSessionSelector } from "./context.js";
3
+ import { useEffect, useMemo, useRef } from "react";
4
+ //#region src/agent-state.ts
5
+ /** One stable selector per slot name — see {@link selectAgentState}. */
6
+ const selectors = /* @__PURE__ */ new Map();
7
+ /** The whole frame — module scope, so its identity is stable. */
8
+ const selectFrame = (snapshot) => snapshot.agentState;
9
+ /**
10
+ * A `useSessionSelector` selector for ONE slot of the agent's projected state:
11
+ * its value, or `undefined` before the agent has pushed it.
12
+ *
13
+ * The same function for the same name on every call, which is load-bearing:
14
+ * `useSessionSelector` caches its selection on the selector's identity, and an
15
+ * inline arrow would re-run it on every snapshot.
16
+ *
17
+ * ```tsx
18
+ * import { selectAgentState, useSessionSelector } from "@alexkroman1/aai-ui";
19
+ *
20
+ * function CartBadge() {
21
+ * // Re-renders when the `cart` slot changes, and on nothing else.
22
+ * const cart = useSessionSelector(selectAgentState<{ count: number }>("cart"));
23
+ * return <span>{cart?.count ?? 0}</span>;
24
+ * }
25
+ * ```
26
+ *
27
+ * @public
28
+ */
29
+ function selectAgentState(slot) {
30
+ let selector = selectors.get(slot);
31
+ if (selector === void 0) {
32
+ selector = (snapshot) => snapshot.agentState?.[slot];
33
+ selectors.set(slot, selector);
34
+ }
35
+ return selector;
36
+ }
37
+ function useAgentState(source, fallback) {
38
+ const projection = typeof source === "function" ? source : void 0;
39
+ const slot = projection?.key ?? (typeof source === "string" ? source : void 0);
40
+ const selected = useSessionSelector(slot === void 0 ? selectFrame : selectAgentState(slot));
41
+ const empty = useMemo(() => projection?.(), [projection]);
42
+ if (slot === void 0) return selected ?? null;
43
+ if (selected !== void 0) return selected;
44
+ if (projection !== void 0) return empty;
45
+ return fallback === void 0 ? null : fallback;
46
+ }
47
+ //#endregion
48
+ //#region src/hooks.ts
49
+ /** Index of the first item whose sequence number is above the watermark (tail scan from the end). */
50
+ function tailStart(items, seqOf, watermark) {
51
+ let start = items.length;
52
+ for (let i = items.length - 1; i >= 0; i--) {
53
+ const item = items[i];
54
+ if (!item || seqOf(item) <= watermark) break;
55
+ start = i;
56
+ }
57
+ return start;
58
+ }
59
+ /**
60
+ * Process the unprocessed tail of `toolCalls`: fire `onNew` once per item that
61
+ * has settled (per `isSettled`), then advance the watermark past the leading
62
+ * run of settled items. Resets the cursor when the list is empty (session
63
+ * reset), matching the previous seen-set behavior.
64
+ */
65
+ function processToolCallTail(toolCalls, cursor, isSettled, onNew) {
66
+ if (toolCalls.length === 0) {
67
+ cursor.seq = 0;
68
+ cursor.fired.clear();
69
+ return;
70
+ }
71
+ const start = tailStart(toolCalls, (tc) => tc.seq, cursor.seq);
72
+ for (let i = start; i < toolCalls.length; i++) {
73
+ const tc = toolCalls[i];
74
+ if (!(tc && isSettled(tc)) || cursor.fired.has(tc.callId)) continue;
75
+ cursor.fired.add(tc.callId);
76
+ onNew(tc);
77
+ }
78
+ for (let i = start; i < toolCalls.length; i++) {
79
+ const tc = toolCalls[i];
80
+ if (!(tc && isSettled(tc))) break;
81
+ cursor.fired.delete(tc.callId);
82
+ cursor.seq = tc.seq;
83
+ }
84
+ }
85
+ /**
86
+ * The two `fire` implementations, at module scope so they are stable across
87
+ * renders — which is what lets `useToolCallEffect` name `fire` as a plain
88
+ * dependency instead of holding it in a ref.
89
+ */
90
+ const fireResult = (callback, tc, filtered) => {
91
+ const parsed = tryParseJSON(tc.result);
92
+ if (filtered) callback(parsed, tc);
93
+ else callback(tc.name, parsed, tc);
94
+ };
95
+ const fireStart = (callback, tc) => {
96
+ callback(tc);
97
+ };
98
+ /**
99
+ * Shared scaffold for the tool-call lifecycle hooks: parses the optional
100
+ * `(toolName, callback)` / `(callback)` overload args, dedups by call ID,
101
+ * and invokes `fire` once per tool call reaching `status`.
102
+ */
103
+ function useToolCallEffect(status, args, fire) {
104
+ const first = args[0];
105
+ const filterName = typeof first === "string" ? first : null;
106
+ const callback = filterName === null ? first : args[1];
107
+ const toolCalls = useSessionSelector((s) => s.toolCalls);
108
+ const cursorRef = useRef({
109
+ seq: 0,
110
+ fired: /* @__PURE__ */ new Set()
111
+ });
112
+ const mountedRef = useRef(false);
113
+ const callbackRef = useRef(callback);
114
+ callbackRef.current = callback;
115
+ useEffect(() => {
116
+ const firstRun = !mountedRef.current;
117
+ mountedRef.current = true;
118
+ processToolCallTail(toolCalls, cursorRef.current, (tc) => status === "pending" || tc.status === status, (tc) => {
119
+ if (tc.status !== status && firstRun) return;
120
+ if (filterName && tc.name !== filterName) return;
121
+ fire(callbackRef.current, tc, filterName !== null);
122
+ });
123
+ }, [
124
+ toolCalls,
125
+ filterName,
126
+ status,
127
+ fire
128
+ ]);
129
+ }
130
+ function useToolResult(...args) {
131
+ useToolCallEffect("done", args, fireResult);
132
+ }
133
+ /**
134
+ * Subscribe to custom events emitted by agent tools via
135
+ * `ctx.send(event, data)`; the callback receives each event's `data`.
136
+ *
137
+ * This is the preferred way to drive UI from tools — an explicit event beats
138
+ * inferring state from tool results with {@link useToolResult}.
139
+ *
140
+ * @example
141
+ * ```tsx
142
+ * import { useEvent } from "@alexkroman1/aai-ui";
143
+ * import { useState } from "react";
144
+ *
145
+ * type Item = { sku: string; qty: number };
146
+ *
147
+ * function Cart() {
148
+ * const [cart, setCart] = useState<Item[]>([]);
149
+ * // Tool: ctx.send("item_added", { sku, qty })
150
+ * useEvent<Item>("item_added", (data) => {
151
+ * setCart((cart) => [...cart, data]);
152
+ * });
153
+ * return <div>{cart.length} items</div>;
154
+ * }
155
+ * ```
156
+ *
157
+ * @public
158
+ */
159
+ function useEvent(event, callback) {
160
+ const customEvents = useSessionSelector((s) => s.customEvents);
161
+ const watermarkRef = useRef(0);
162
+ const callbackRef = useRef(callback);
163
+ callbackRef.current = callback;
164
+ useEffect(() => {
165
+ if (customEvents.length === 0) {
166
+ watermarkRef.current = 0;
167
+ return;
168
+ }
169
+ const start = tailStart(customEvents, (ce) => ce.id, watermarkRef.current);
170
+ for (let i = start; i < customEvents.length; i++) {
171
+ const ce = customEvents[i];
172
+ if (!ce) continue;
173
+ watermarkRef.current = ce.id;
174
+ if (ce.event === event) callbackRef.current(ce.data);
175
+ }
176
+ }, [customEvents, event]);
177
+ }
178
+ function useToolCallStart(...args) {
179
+ useToolCallEffect("pending", args, fireStart);
180
+ }
181
+ //#endregion
182
+ export { useAgentState as a, selectAgentState as i, useToolCallStart as n, useToolResult as r, useEvent as t };
@@ -0,0 +1,172 @@
1
+ import type { ToolCallInfo } from "./types.ts";
2
+ export { selectAgentState, useAgentState } from "./agent-state.ts";
3
+ /**
4
+ * Fire a callback when ONE named tool settles, with its parsed JSON result.
5
+ *
6
+ * For new code prefer explicit events — `ctx.send(event, data)` in the tool
7
+ * paired with {@link useEvent} here — over listening to tool results.
8
+ *
9
+ * A component that mounts late still receives the results of calls that
10
+ * already completed, because a result is a value the UI is driven from rather
11
+ * than a moment. Each call fires exactly once per hook instance.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { useState } from "react";
16
+ * import { useToolResult } from "@alexkroman1/aai-ui";
17
+ *
18
+ * type Quote = { symbol: string; price: number };
19
+ *
20
+ * function QuoteCard() {
21
+ * const [quote, setQuote] = useState<Quote>();
22
+ * useToolResult<Quote>("get_quote", (result) => setQuote(result));
23
+ * return quote ? <p>{quote.symbol}: {quote.price}</p> : null;
24
+ * }
25
+ * ```
26
+ *
27
+ * @typeParam R - The result shape. Defaults to `unknown`, NOT to
28
+ * `DefaultToolResult` (`any`): the return type is inferred perfectly
29
+ * at `tool()` and this hook is the one place a client reads it, so an `any`
30
+ * default threw the whole inference away exactly where it was wanted —
31
+ * `useToolResult("get_order", (r) => r.a.b.c.d.e)` reported nothing. It is
32
+ * the tool's own shape that belongs here, and the spelling that costs a
33
+ * browser bundle nothing is a TYPE-ONLY import of the tool module:
34
+ * `import type getOrder from "./tools/get_order.ts"` is erased, so
35
+ * `useToolResult<InferToolOutput<typeof getOrder>>(…)` pulls no host code
36
+ * into the client graph. `useToolResult<Quote>(…)` against a hand-written
37
+ * shape is the other spelling. `DefaultToolResult` itself stays `any`
38
+ * — see `ToolCallInfo.args` for why a value the framework cannot see is
39
+ * typed that way at REST; the argument does not extend to a call site whose
40
+ * whole job is to name the shape.
41
+ * @param toolName - Only calls of this tool fire the callback.
42
+ * @param callback - Called with the parsed result and the call itself.
43
+ *
44
+ * @public
45
+ */
46
+ export declare function useToolResult<R = unknown>(toolName: string, callback: (result: R, toolCall: ToolCallInfo) => void): void;
47
+ /**
48
+ * Fire a callback when ANY tool call settles — the tool's name is the
49
+ * callback's first argument.
50
+ *
51
+ * The unfiltered form, for a chrome rendering a log of everything the agent
52
+ * did rather than reacting to one tool.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * import { useState } from "react";
57
+ * import { useToolResult } from "@alexkroman1/aai-ui";
58
+ *
59
+ * function ToolLog() {
60
+ * const [lines, setLines] = useState<string[]>([]);
61
+ * useToolResult((name, result) => {
62
+ * setLines((prev) => [...prev, `${name}: ${JSON.stringify(result)}`]);
63
+ * });
64
+ * return <pre>{lines.join("\n")}</pre>;
65
+ * }
66
+ * ```
67
+ *
68
+ * @typeParam R - The result shape. Defaults to `unknown`, for the reason the
69
+ * filtered overload's `@typeParam` gives. A log renderer is the one caller
70
+ * that legitimately wants no shape, and `unknown` is what it should say:
71
+ * `JSON.stringify(result)` takes it unchanged.
72
+ * @param callback - Called with the tool's name, the parsed result, and the
73
+ * call itself.
74
+ *
75
+ * @public
76
+ */
77
+ export declare function useToolResult<R = unknown>(callback: (name: string, result: R, toolCall: ToolCallInfo) => void): void;
78
+ /**
79
+ * Subscribe to custom events emitted by agent tools via
80
+ * `ctx.send(event, data)`; the callback receives each event's `data`.
81
+ *
82
+ * This is the preferred way to drive UI from tools — an explicit event beats
83
+ * inferring state from tool results with {@link useToolResult}.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * import { useEvent } from "@alexkroman1/aai-ui";
88
+ * import { useState } from "react";
89
+ *
90
+ * type Item = { sku: string; qty: number };
91
+ *
92
+ * function Cart() {
93
+ * const [cart, setCart] = useState<Item[]>([]);
94
+ * // Tool: ctx.send("item_added", { sku, qty })
95
+ * useEvent<Item>("item_added", (data) => {
96
+ * setCart((cart) => [...cart, data]);
97
+ * });
98
+ * return <div>{cart.length} items</div>;
99
+ * }
100
+ * ```
101
+ *
102
+ * @public
103
+ */
104
+ export declare function useEvent<T = unknown>(event: string, callback: (data: T) => void): void;
105
+ /**
106
+ * Fire a callback when ONE named tool starts, before its result arrives.
107
+ *
108
+ * A start is a MOMENT rather than a value, so unlike {@link useToolResult}
109
+ * this never replays: a component that mounts mid-session learns nothing about
110
+ * calls that started before it.
111
+ *
112
+ * @example
113
+ * ```tsx
114
+ * import { useState } from "react";
115
+ * import { useToolCallStart } from "@alexkroman1/aai-ui";
116
+ *
117
+ * function Searching() {
118
+ * const [busy, setBusy] = useState(false);
119
+ * useToolCallStart("search_catalog", () => setBusy(true));
120
+ * return busy ? <p>Searching the catalog…</p> : null;
121
+ * }
122
+ * ```
123
+ *
124
+ * @typeParam A - The tool's ARGUMENT shape. Defaults to
125
+ * `ToolCallInfo["args"]`, which is `Record<string, any>` — so an
126
+ * un-parameterized call behaves exactly as it always has, and
127
+ * `toolCall.args.totally_made_up_field` still compiles. That default is a
128
+ * property of {@link ToolCallInfo} rather than a choice made here (its doc
129
+ * carries the argument, and the escape hatch it recommends —
130
+ * `args as { url: string }` — is what this type parameter replaces); until
131
+ * that field is tightened there is nothing stricter for this hook to fall
132
+ * back to. What was missing was any way to opt IN: there was no type
133
+ * parameter at all, so a custom client could not check args even when it
134
+ * knew the shape. Name it — `useToolCallStart<{ query: string }>(…)` — or
135
+ * derive it from the tool with a TYPE-ONLY import, which is erased and so
136
+ * pulls no host code into the browser graph:
137
+ * `useToolCallStart<InferToolInput<typeof search>>("search", …)`.
138
+ * @param toolName - Only calls of this tool fire the callback.
139
+ * @param callback - Called with the pending call.
140
+ *
141
+ * @public
142
+ */
143
+ export declare function useToolCallStart<A = ToolCallInfo["args"]>(toolName: string, callback: (toolCall: Omit<ToolCallInfo, "args"> & {
144
+ args: A;
145
+ }) => void): void;
146
+ /**
147
+ * Fire a callback when ANY tool call starts — read the tool's name off the
148
+ * call itself (`toolCall.name`).
149
+ *
150
+ * @example
151
+ * ```tsx
152
+ * import { useState } from "react";
153
+ * import { useToolCallStart } from "@alexkroman1/aai-ui";
154
+ *
155
+ * function Activity() {
156
+ * const [now, setNow] = useState<string>();
157
+ * useToolCallStart((toolCall) => setNow(toolCall.name));
158
+ * return now ? <p>Running {now}…</p> : null;
159
+ * }
160
+ * ```
161
+ *
162
+ * @typeParam A - The tool's ARGUMENT shape; see the filtered overload. On the
163
+ * unfiltered form every tool's call arrives, so naming one shape here is
164
+ * only right for a page that switches on `toolCall.name` and narrows it
165
+ * itself — the default is the honest answer for a log renderer.
166
+ * @param callback - Called with the pending call.
167
+ *
168
+ * @public
169
+ */
170
+ export declare function useToolCallStart<A = ToolCallInfo["args"]>(callback: (toolCall: Omit<ToolCallInfo, "args"> & {
171
+ args: A;
172
+ }) => void): void;
package/dist/hooks.js ADDED
@@ -0,0 +1,3 @@
1
+ import "./context.js";
2
+ import { a as useAgentState, i as selectAgentState, n as useToolCallStart, r as useToolResult, t as useEvent } from "./hooks-DN4gHYsj.js";
3
+ export { selectAgentState, useAgentState, useEvent, useToolCallStart, useToolResult };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Type-level contract for the four `aai-ui` hooks a custom client is written
3
+ * against. All four are generic, all four are typed BY THE CALLER, and every
4
+ * one of them was unpinned — the root guide claimed `aai-ui` (`.`) had
5
+ * type-level coverage and no `.test-d.ts` existed in this package at all.
6
+ *
7
+ * The reason these need a type test rather than a runtime one: nothing here
8
+ * has runtime behaviour to observe. `useAgentState` is one `useSessionSelector`
9
+ * call over a per-slot selector, and `useToolResult`'s two overloads erase to the same
10
+ * `(...args: unknown[])` implementation. What a consumer actually depends on
11
+ * is the SIGNATURE, and a signature is exactly what a runtime suite cannot
12
+ * assert.
13
+ */
14
+ export {};