@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
@@ -0,0 +1,682 @@
1
+ import { useSessionSelector, useTheme } from "./context.js";
2
+ import { a as inkTint, o as primaryTint } from "./_colors-CpZO-88A.js";
3
+ import { n as useUserTranscript } from "./use-user-transcript-uyHhzy4d.js";
4
+ import { n as ToolCallRow, t as ToolCallBlock } from "./tool-call-block-uXtMOUWN.js";
5
+ import { memo, useCallback, useMemo } from "react";
6
+ import clsx from "clsx";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ import { StickToBottom } from "use-stick-to-bottom";
9
+ import ReactMarkdown from "react-markdown";
10
+ import remarkGfm from "remark-gfm";
11
+ //#region src/components/auto-scroll.tsx
12
+ /** @jsxImportSource react */
13
+ /**
14
+ * A scroll container that stays pinned to the bottom as its content grows,
15
+ * releases when the reader scrolls up, and re-engages once they return to the
16
+ * bottom.
17
+ *
18
+ * For clients that render their own chat chrome instead of using
19
+ * {@link MessageList} — a terminal, a dispatch board, a themed transcript.
20
+ * `MessageList` already behaves this way; this is the same mechanism with no
21
+ * opinion about what goes inside it.
22
+ *
23
+ * @remarks
24
+ * The pattern this replaces is a `useEffect` that calls
25
+ * `ref.current?.scrollIntoView()` on every message change. That version has
26
+ * three faults, and they compound: it fights the reader, since scrolling up to
27
+ * re-read is undone by the next transcript delta; it misses growth that is not
28
+ * a new message, because a streamed reply, an expanding tool block or a
29
+ * markdown reflow changes height without changing the dependency array; and it
30
+ * needs a synthetic dependency (`messages.length + transcript.length`) to fire
31
+ * at all, which is where the dead `if (version < 0) return;` line comes from.
32
+ * A `ResizeObserver` on the content — what this uses — has none of those.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * import { AutoScroll, useSession } from "@alexkroman1/aai-ui";
37
+ *
38
+ * function Transcript() {
39
+ * const session = useSession();
40
+ * return (
41
+ * <AutoScroll className="flex-1 min-h-0" contentClassName="flex flex-col gap-2 p-4">
42
+ * {session.messages.map((m) => (
43
+ * <div key={m.id}>{m.content}</div>
44
+ * ))}
45
+ * </AutoScroll>
46
+ * );
47
+ * }
48
+ * ```
49
+ *
50
+ * @param props - Scroll container props.
51
+ *
52
+ * @public
53
+ */
54
+ function AutoScroll({ children, className, contentClassName, scrollClassName = "overflow-y-auto [scrollbar-width:none]", style, initial = "instant", resize = "smooth" }) {
55
+ return /* @__PURE__ */ jsx(StickToBottom, {
56
+ role: "log",
57
+ className: clsx("flex-1 min-h-0", className),
58
+ style,
59
+ initial,
60
+ resize,
61
+ children: /* @__PURE__ */ jsx(StickToBottom.Content, {
62
+ scrollClassName,
63
+ className: contentClassName,
64
+ children
65
+ })
66
+ });
67
+ }
68
+ //#endregion
69
+ //#region src/use-conversation.ts
70
+ /**
71
+ * `useConversation` — the exchange, already assembled, with nothing rendered.
72
+ *
73
+ * `<MessageList>` owns four decisions that are not obvious and are not
74
+ * derivable from the snapshot by eye: the message/tool-call INTERLEAVE, the
75
+ * streaming agent utterance, the live user transcript with its `null`-vs-`""`
76
+ * protocol distinction, and the thinking indicator's suppression rule. Its only
77
+ * prop is `className`, so the moment a client wants its own bubble markup it
78
+ * drops all four at once — and the three hand-rolled chromes in the template
79
+ * tree each shipped a strictly worse conversation for exactly that reason. One
80
+ * of them runs fifteen tools and its operator sees none of them, because the
81
+ * tool calls live in a second array nothing in that page ever reads.
82
+ *
83
+ * This is the headless half. `<MessageList>` is now a thin consumer of it —
84
+ * which is the only way to know the hook is complete, since a hook that the
85
+ * package's own list cannot be built from is a hook a custom chrome will find a
86
+ * hole in.
87
+ *
88
+ * ## It subscribes NARROWLY, and that is half the value
89
+ *
90
+ * The three custom chromes also call whole-page `useSession()`, which re-renders
91
+ * on *every* snapshot change — so a dispatch board re-renders at STT-partial
92
+ * rate. `<ChatView>` deliberately avoids that with per-field
93
+ * {@link useSessionSelector} calls, and so does this: a component that reads the
94
+ * conversation gets the conversation's own update rate, not the session's.
95
+ */
96
+ /**
97
+ * Interleave messages and tool calls, ordered by insertion time.
98
+ *
99
+ * Each tool call belongs immediately after its anchor message
100
+ * (`afterMessageId`); tool calls whose anchor slid out of the retained window —
101
+ * or that were inserted before any message existed — come first, since there is
102
+ * nothing left for them to follow.
103
+ */
104
+ function interleave(messages, toolCalls) {
105
+ const items = [];
106
+ let tci = 0;
107
+ const pushToolCallsThrough = (maxAfterId) => {
108
+ let tc = toolCalls[tci];
109
+ while (tc && tc.afterMessageId <= maxAfterId) {
110
+ items.push({
111
+ kind: "tool",
112
+ toolCall: tc
113
+ });
114
+ tci++;
115
+ tc = toolCalls[tci];
116
+ }
117
+ };
118
+ const firstMessage = messages[0];
119
+ if (firstMessage) pushToolCallsThrough(firstMessage.id - 1);
120
+ for (const message of messages) {
121
+ items.push({
122
+ kind: "message",
123
+ message
124
+ });
125
+ pushToolCallsThrough(message.id);
126
+ }
127
+ pushToolCallsThrough(Number.POSITIVE_INFINITY);
128
+ return items;
129
+ }
130
+ /**
131
+ * Whether the thinking indicator should show — see
132
+ * {@link UseConversationResult.thinking} for the rule and the argument.
133
+ */
134
+ function isThinking(state, messages, toolCalls) {
135
+ if (state !== "thinking") return false;
136
+ const last = toolCalls.at(-1);
137
+ if (last?.status === "pending") return false;
138
+ const lastMessage = messages.at(-1);
139
+ return !lastMessage || lastMessage.role === "user" || Boolean(last);
140
+ }
141
+ /**
142
+ * Subscribe to the conversation: the interleaved exchange, the streaming
143
+ * utterance, the live transcript and the thinking rule — with no markup.
144
+ *
145
+ * Must be used inside the provider `mountClient()` installs.
146
+ *
147
+ * @example A custom bubble, keeping every rule `<MessageList>` knows
148
+ * ```tsx
149
+ * import { useConversation } from "@alexkroman1/aai-ui";
150
+ *
151
+ * function Transcript() {
152
+ * const { items, streaming, transcript, thinking } = useConversation();
153
+ * return (
154
+ * <div>
155
+ * {items.map((item) =>
156
+ * item.kind === "message" ? (
157
+ * <p key={item.message.id} data-role={item.message.role}>
158
+ * {item.message.content}
159
+ * </p>
160
+ * ) : (
161
+ * <code key={item.toolCall.callId}>{item.toolCall.name}</code>
162
+ * ),
163
+ * )}
164
+ * {streaming !== null && <p data-role="assistant">{streaming}</p>}
165
+ * {transcript.speaking && <p data-role="user">{transcript.text}</p>}
166
+ * {thinking && <p>…</p>}
167
+ * </div>
168
+ * );
169
+ * }
170
+ * ```
171
+ *
172
+ * @returns See {@link UseConversationResult}.
173
+ *
174
+ * @public
175
+ */
176
+ function useConversation() {
177
+ const state = useSessionSelector((s) => s.state);
178
+ const messages = useSessionSelector((s) => s.messages);
179
+ const toolCalls = useSessionSelector((s) => s.toolCalls);
180
+ const streaming = useSessionSelector((s) => s.agentTranscript);
181
+ const transcript = useUserTranscript();
182
+ const items = useMemo(() => interleave(messages, toolCalls), [messages, toolCalls]);
183
+ const thinking = useMemo(() => isThinking(state, messages, toolCalls), [
184
+ state,
185
+ messages,
186
+ toolCalls
187
+ ]);
188
+ return useMemo(() => ({
189
+ items,
190
+ streaming,
191
+ transcript,
192
+ thinking
193
+ }), [
194
+ items,
195
+ streaming,
196
+ transcript,
197
+ thinking
198
+ ]);
199
+ }
200
+ //#endregion
201
+ //#region src/components/conversation-view.tsx
202
+ /**
203
+ * The id of the synthetic message the streaming row is rendered as. Real ids
204
+ * are assigned from zero upward at append time, so a negative one cannot
205
+ * collide with — or be mistaken for — a message in `items`.
206
+ */
207
+ const STREAMING_MESSAGE_ID = -1;
208
+ /** The id a `spoken` log entry is rendered with — negative for the same reason. */
209
+ const SPOKEN_MESSAGE_ID = -2;
210
+ function defaultTool(toolCall) {
211
+ return /* @__PURE__ */ jsx(ToolCallRow, {
212
+ title: toolCall.name,
213
+ pending: toolCall.status === "pending",
214
+ variant: "compact",
215
+ className: "self-start"
216
+ });
217
+ }
218
+ function defaultNote(entry) {
219
+ return /* @__PURE__ */ jsx("p", {
220
+ className: "text-xs text-center opacity-50",
221
+ children: entry.text
222
+ });
223
+ }
224
+ function defaultTranscript(transcript) {
225
+ return /* @__PURE__ */ jsx("p", {
226
+ className: "italic opacity-70",
227
+ children: transcript.text
228
+ });
229
+ }
230
+ const DEFAULT_INDICATOR = /* @__PURE__ */ jsx("span", {
231
+ style: { animation: "aai-pulse 1.2s ease-in-out infinite" },
232
+ children: "· · ·"
233
+ });
234
+ /**
235
+ * The conversation's skeleton over {@link useConversation}, with every row a
236
+ * render slot: a pinned scroll region holding the empty state, the interleaved
237
+ * messages and tool calls, the streaming reply and the announced thinking row,
238
+ * plus the live transcript — inside the scroll or pinned beneath it.
239
+ *
240
+ * `useConversation()` already made the DATA one thing: the interleave, the
241
+ * streaming utterance, the `null`-vs-`""` transcript distinction and the
242
+ * thinking-suppression rule. What three custom chromes then each wrote around
243
+ * it was the same fifty lines of STRUCTURE: an `AutoScroll` with a bounded
244
+ * height, an empty-state guard on `items.length === 0 && streaming === null`,
245
+ * the map with its keys, the streaming row, the thinking row with its
246
+ * `role="status"` and `aria-label` (and the same comment about screen readers
247
+ * hearing punctuation), the transcript guarded on `speaking`. The bubbles are
248
+ * the part each template exists to show, so those are slots; the order and the
249
+ * accessibility contract are this component's.
250
+ *
251
+ * {@link MessageList} is this with the stock bubbles filled in.
252
+ *
253
+ * Must be rendered inside the providers `mountClient()` installs.
254
+ *
255
+ * @example A board's radio log: its own bubbles, the stock tool row, the transcript pinned below
256
+ * ```tsx
257
+ * import { ConversationView } from "@alexkroman1/aai-ui";
258
+ *
259
+ * function RadioLog() {
260
+ * return (
261
+ * <ConversationView
262
+ * contentClassName="p-4 flex flex-col gap-2"
263
+ * empty={<p className="text-center opacity-60">Standing by.</p>}
264
+ * renderMessage={({ role, content }) => (
265
+ * <div className={role === "assistant" ? "self-start" : "self-end"}>{content}</div>
266
+ * )}
267
+ * renderTranscript={({ text }) => <div className="px-4 py-2 italic">{text}</div>}
268
+ * transcriptPosition="below"
269
+ * thinkingLabel="Dispatch is thinking"
270
+ * />
271
+ * );
272
+ * }
273
+ * ```
274
+ *
275
+ * @param props - See {@link ConversationViewProps}.
276
+ *
277
+ * @public
278
+ */
279
+ function ConversationView({ renderMessage, renderTool = defaultTool, renderStreaming, renderTranscript = defaultTranscript, transcriptPosition = "inline", log, renderNote = defaultNote, renderSessionHeader, empty, thinkingLabel = "Thinking", thinkingIndicator = DEFAULT_INDICATOR, thinkingClassName, className, contentClassName, scrollClassName, style }) {
280
+ const { items, streaming, transcript, thinking } = useConversation();
281
+ const liveItems = log ? void 0 : items;
282
+ const rows = useMemo(() => {
283
+ const renderItem = (item, key) => item.kind === "message" ? renderKeyed(`${key}m${item.message.id}`, renderMessage(item.message)) : renderKeyed(`${key}t${item.toolCall.callId}`, renderTool(item.toolCall));
284
+ if (!log) return (liveItems ?? []).map((item) => renderItem(item, ""));
285
+ return logRows(log, renderItem, renderMessage, renderNote, renderSessionHeader);
286
+ }, [
287
+ liveItems,
288
+ log,
289
+ renderMessage,
290
+ renderTool,
291
+ renderNote,
292
+ renderSessionHeader
293
+ ]);
294
+ const streamingMessage = useMemo(() => streaming === null ? null : {
295
+ id: STREAMING_MESSAGE_ID,
296
+ role: "assistant",
297
+ content: streaming
298
+ }, [streaming]);
299
+ const transcriptRow = transcript.speaking ? renderTranscript(transcript) : null;
300
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(AutoScroll, {
301
+ className,
302
+ contentClassName,
303
+ scrollClassName,
304
+ style,
305
+ children: [
306
+ (log ?? items).length === 0 && streaming === null && empty,
307
+ rows,
308
+ streamingMessage !== null && (renderStreaming ? renderStreaming(streamingMessage.content) : renderMessage(streamingMessage)),
309
+ transcriptPosition === "inline" && transcriptRow,
310
+ thinking && /* @__PURE__ */ jsx("div", {
311
+ role: "status",
312
+ "aria-label": thinkingLabel,
313
+ className: thinkingClassName,
314
+ children: thinkingIndicator
315
+ })
316
+ ]
317
+ }), transcriptPosition === "below" && transcriptRow] });
318
+ }
319
+ /**
320
+ * A log's rows. Keys are the entry's own identity (kind, time, session and run)
321
+ * plus a repeat count, not its index: the log is trimmed from the FRONT, and an
322
+ * index key would remount every row on each trim. Items within a session entry
323
+ * are keyed by position — a chain is append-only.
324
+ */
325
+ function logRows(log, renderItem, renderMessage, renderNote, renderSessionHeader) {
326
+ const seen = /* @__PURE__ */ new Map();
327
+ return log.flatMap((entry) => {
328
+ const id = entry.kind === "session" ? `s${entry.at}:${entry.sessionId}:${entry.run}` : `${entry.kind}${entry.at}`;
329
+ const n = seen.get(id) ?? 0;
330
+ seen.set(id, n + 1);
331
+ const key = `${id}#${n}`;
332
+ if (entry.kind === "note") return [renderKeyed(key, renderNote(entry))];
333
+ if (entry.kind === "spoken") return [renderKeyed(key, renderMessage({
334
+ id: SPOKEN_MESSAGE_ID,
335
+ role: "assistant",
336
+ content: entry.text
337
+ }))];
338
+ return [renderKeyed(`${key}h`, renderSessionHeader?.(entry) ?? null), ...entry.items.map((item, i) => renderItem(item, `${key}/${i}`))];
339
+ });
340
+ }
341
+ /** Give a slot's output a key without asking the renderer to remember one. */
342
+ function renderKeyed(key, node) {
343
+ return /* @__PURE__ */ jsx(RowSlot, { children: node }, key);
344
+ }
345
+ /**
346
+ * A keyed wrapper that renders nothing of its own — a `Fragment` with a key
347
+ * would do, but spelling it as a component keeps `renderKeyed` a plain call.
348
+ */
349
+ function RowSlot({ children }) {
350
+ return children;
351
+ }
352
+ //#endregion
353
+ //#region src/components/markdown.tsx
354
+ /** @jsxImportSource react */
355
+ const BARE_ORDERED_MARKER = /^(\s*)(\d{1,9})([.)])\s*$/;
356
+ const BARE_BULLET_MARKER = /^(\s*)([-*+])\s*$/;
357
+ const CODE_FENCE = /^\s*(```|~~~)/;
358
+ /**
359
+ * Escape list markers that have no content, so a terse reply renders as
360
+ * text instead of vanishing. CommonMark parses a line that is only `42.`
361
+ * as an *empty* ordered list starting at 42 — a voice agent answering
362
+ * "what's 6 times 7?" with "42." would otherwise display nothing (and a
363
+ * streaming transcript briefly ending on `1.` would blank the row).
364
+ * Lines inside fenced code blocks are left alone.
365
+ */
366
+ function escapeBareListMarkers(text) {
367
+ let inFence = false;
368
+ return text.split("\n").map((line) => {
369
+ if (CODE_FENCE.test(line)) {
370
+ inFence = !inFence;
371
+ return line;
372
+ }
373
+ if (inFence) return line;
374
+ const ordered = BARE_ORDERED_MARKER.exec(line);
375
+ if (ordered) return `${ordered[1]}${ordered[2]}\\${ordered[3]}`;
376
+ const bullet = BARE_BULLET_MARKER.exec(line);
377
+ if (bullet) return `${bullet[1]}\\${bullet[2]}`;
378
+ return line;
379
+ }).join("\n");
380
+ }
381
+ const SCALE_CLASSES = {
382
+ default: {
383
+ h1: "mt-3 mb-1.5 text-[17px] font-semibold",
384
+ h2: "mt-3 mb-1.5 text-[16px] font-semibold",
385
+ h3: "mt-3 mb-1.5 text-[15px] font-semibold",
386
+ code: "text-[12.5px]",
387
+ table: "my-1.5 w-full border-collapse text-sm"
388
+ },
389
+ compact: {
390
+ h1: "mt-3 mb-1.5 text-[15px] font-medium",
391
+ h2: "mt-3 mb-1.5 text-[14px] font-medium",
392
+ h3: "mt-3 mb-1.5 text-[13px] font-medium",
393
+ code: "text-[11px]",
394
+ table: "my-1.5 w-full border-collapse text-[12px]"
395
+ }
396
+ };
397
+ /**
398
+ * Agent prose, rendered as Markdown.
399
+ *
400
+ * Pipeline and S2S models write emphasis, lists, `code`, and links; before
401
+ * this they arrived as literal asterisks and backticks. Styling is
402
+ * per-element (theme colors via inline styles, spacing via Tailwind) so it
403
+ * stays on the default client's type scale and follows custom themes.
404
+ * The optional `variant` selects the type scale (see
405
+ * {@link MarkdownVariant}).
406
+ * GFM is on for tables and strikethrough. react-markdown does not render
407
+ * raw HTML unless rehype-raw is added — keep it that way, this text comes
408
+ * from a model.
409
+ *
410
+ * Memoized alongside `MessageBubble`: message content is referentially
411
+ * stable across snapshots, so only the streaming row re-parses.
412
+ *
413
+ * @example
414
+ * ```tsx
415
+ * import { Markdown, useSessionSelector } from "@alexkroman1/aai-ui";
416
+ *
417
+ * // The agent's reply as it streams, rendered rather than shown as literal
418
+ * // asterisks and backticks.
419
+ * function LiveReply() {
420
+ * const text = useSessionSelector((snapshot) => snapshot.agentTranscript);
421
+ * return text === null ? null : <Markdown text={text} variant="compact" />;
422
+ * }
423
+ * ```
424
+ *
425
+ * @public
426
+ */
427
+ const Markdown = memo(function Markdown({ text, variant = "default" }) {
428
+ const theme = useTheme();
429
+ const scale = SCALE_CLASSES[variant];
430
+ const components = useMemo(() => ({
431
+ p: (props) => /* @__PURE__ */ jsx("p", {
432
+ className: "my-1.5 first:mt-0 last:mb-0",
433
+ ...props
434
+ }),
435
+ ul: (props) => /* @__PURE__ */ jsx("ul", {
436
+ className: "my-1.5 list-disc pl-5",
437
+ ...props
438
+ }),
439
+ ol: (props) => /* @__PURE__ */ jsx("ol", {
440
+ className: "my-1.5 list-decimal pl-5",
441
+ ...props
442
+ }),
443
+ li: (props) => /* @__PURE__ */ jsx("li", {
444
+ className: "my-0.5",
445
+ ...props
446
+ }),
447
+ h1: (props) => /* @__PURE__ */ jsx("h1", {
448
+ className: scale.h1,
449
+ ...props
450
+ }),
451
+ h2: (props) => /* @__PURE__ */ jsx("h2", {
452
+ className: scale.h2,
453
+ ...props
454
+ }),
455
+ h3: (props) => /* @__PURE__ */ jsx("h3", {
456
+ className: scale.h3,
457
+ ...props
458
+ }),
459
+ a: ({ style, ...props }) => /* @__PURE__ */ jsx("a", {
460
+ className: "underline underline-offset-2",
461
+ style: {
462
+ color: theme.primary,
463
+ ...style
464
+ },
465
+ target: "_blank",
466
+ rel: "noreferrer noopener",
467
+ ...props
468
+ }),
469
+ code: ({ className, style, children, ...rest }) => {
470
+ return /language-/.test(className ?? "") ? /* @__PURE__ */ jsx("code", {
471
+ className: clsx("font-aai-mono", scale.code),
472
+ ...rest,
473
+ children
474
+ }) : /* @__PURE__ */ jsx("code", {
475
+ className: clsx("rounded-sm border px-1 py-0.5 font-aai-mono", scale.code),
476
+ style: {
477
+ borderColor: theme.border,
478
+ background: inkTint(theme.text, theme.surface, 3),
479
+ ...style
480
+ },
481
+ ...rest,
482
+ children
483
+ });
484
+ },
485
+ pre: ({ style, ...props }) => /* @__PURE__ */ jsx("pre", {
486
+ className: "my-1.5 overflow-x-auto rounded-md border p-2.5 whitespace-pre-wrap wrap-break-word",
487
+ style: {
488
+ borderColor: theme.border,
489
+ background: inkTint(theme.text, theme.surface, 3),
490
+ ...style
491
+ },
492
+ ...props
493
+ }),
494
+ blockquote: ({ style, ...props }) => /* @__PURE__ */ jsx("blockquote", {
495
+ className: "my-1.5 border-l-2 pl-3",
496
+ style: {
497
+ borderColor: theme.border,
498
+ color: inkTint(theme.text, theme.surface, 75),
499
+ ...style
500
+ },
501
+ ...props
502
+ }),
503
+ table: (props) => /* @__PURE__ */ jsx("table", {
504
+ className: scale.table,
505
+ ...props
506
+ }),
507
+ th: ({ style, ...props }) => /* @__PURE__ */ jsx("th", {
508
+ className: "border px-2 py-1 text-left font-medium",
509
+ style: {
510
+ borderColor: theme.border,
511
+ ...style
512
+ },
513
+ ...props
514
+ }),
515
+ td: ({ style, ...props }) => /* @__PURE__ */ jsx("td", {
516
+ className: "border px-2 py-1",
517
+ style: {
518
+ borderColor: theme.border,
519
+ ...style
520
+ },
521
+ ...props
522
+ }),
523
+ hr: ({ style, ...props }) => /* @__PURE__ */ jsx("hr", {
524
+ className: "my-2.5 border-t",
525
+ style: {
526
+ borderColor: theme.border,
527
+ ...style
528
+ },
529
+ ...props
530
+ })
531
+ }), [theme, scale]);
532
+ return /* @__PURE__ */ jsx(ReactMarkdown, {
533
+ remarkPlugins: [remarkGfm],
534
+ components,
535
+ children: escapeBareListMarkers(text)
536
+ });
537
+ });
538
+ //#endregion
539
+ //#region src/components/message-list.tsx
540
+ /** @jsxImportSource react */
541
+ const DOT_STYLES = [
542
+ 0,
543
+ .16,
544
+ .32
545
+ ].map((delay) => ({
546
+ animation: "aai-bounce 1.4s infinite ease-in-out both",
547
+ animationDelay: `${delay}s`
548
+ }));
549
+ /** The three bouncing dots alone, with no row around them. */
550
+ function Dots() {
551
+ const theme = useTheme();
552
+ const muted = inkTint(theme.text, theme.surface, 75);
553
+ return DOT_STYLES.map((style) => /* @__PURE__ */ jsx("div", {
554
+ className: "w-1.5 h-1.5 rounded-full",
555
+ style: {
556
+ ...style,
557
+ background: muted
558
+ }
559
+ }, style.animationDelay));
560
+ }
561
+ /** The row the thinking indicator sits in — `ConversationView` renders this exact element around `Dots`. */
562
+ const THINKING_ROW = "flex items-center gap-2 text-sm font-medium min-h-5";
563
+ /**
564
+ * Animated three-dot "thinking" indicator.
565
+ *
566
+ * `role="status"` with a label, for the same reason `ConsoleShell` announces
567
+ * its error banner: three animated dots are the only signal that the agent is
568
+ * working on a reply, and to a screen reader they are three empty `<div>`s.
569
+ * It is also the indicator's semantic handle — a spec asserting its presence by
570
+ * counting `.rounded-full` elements breaks when the three dots become a spinner
571
+ * (correct behaviour, red test) and again when any sibling row gains a round
572
+ * badge (wrong behaviour, green test).
573
+ *
574
+ * The trailing thinking row is `ConversationView`'s own `role="status"`
575
+ * element, so this is only rendered INSIDE the live transcript bubble, where
576
+ * speech has been detected and no words have come back yet.
577
+ *
578
+ * @internal
579
+ */
580
+ function ThinkingDots() {
581
+ return /* @__PURE__ */ jsx("div", {
582
+ role: "status",
583
+ "aria-label": "Thinking",
584
+ className: THINKING_ROW,
585
+ children: /* @__PURE__ */ jsx(Dots, {})
586
+ });
587
+ }
588
+ /**
589
+ * Right-aligned user bubble — a primary-tinted card (the design system's
590
+ * indigo-50 fill with an indigo-100 edge, derived from the theme primary so
591
+ * custom themes stay coherent). Shared by finalized messages and the live
592
+ * transcript.
593
+ */
594
+ function UserBubble({ theme, color, children }) {
595
+ return /* @__PURE__ */ jsx("div", {
596
+ className: "flex flex-col w-full items-end",
597
+ children: /* @__PURE__ */ jsx("div", {
598
+ className: "max-w-[min(78%,64ch)] border px-3.5 py-2.5 rounded-md whitespace-pre-wrap wrap-break-word text-[15px] font-normal leading-[22px]",
599
+ style: {
600
+ background: primaryTint(theme.primary, theme.surface, 7),
601
+ borderColor: primaryTint(theme.primary, theme.surface, 16),
602
+ color
603
+ },
604
+ children
605
+ })
606
+ });
607
+ }
608
+ /**
609
+ * Renders a single chat message: labeled agent prose, or a user bubble.
610
+ *
611
+ * Memoized so appending one message re-renders one row, not the whole capped
612
+ * list: message objects and the theme are referentially stable across
613
+ * snapshots, and rows are keyed on stable ids (`ChatMessage.id`) that survive
614
+ * the sliding 200-message window.
615
+ */
616
+ const MessageBubble = memo(function MessageBubble({ message, theme }) {
617
+ if (message.role === "user") return /* @__PURE__ */ jsx(UserBubble, {
618
+ theme,
619
+ color: theme.text,
620
+ children: message.content
621
+ });
622
+ return /* @__PURE__ */ jsxs("div", {
623
+ className: "flex flex-col gap-1 max-w-[82%]",
624
+ children: [/* @__PURE__ */ jsx("span", {
625
+ className: "text-[10px] font-medium tracking-[1.2px] uppercase leading-none",
626
+ style: { color: inkTint(theme.text, theme.surface, 65) },
627
+ children: "Agent"
628
+ }), /* @__PURE__ */ jsx("div", {
629
+ className: "wrap-break-word text-[15px] font-normal leading-[23px]",
630
+ style: { color: theme.text },
631
+ children: /* @__PURE__ */ jsx(Markdown, { text: message.content })
632
+ })]
633
+ });
634
+ });
635
+ /**
636
+ * Scrollable list of all chat messages, tool-call blocks, live transcript,
637
+ * streaming agent utterance, and a thinking indicator.
638
+ *
639
+ * Messages and tool calls are interleaved in the correct order. The list
640
+ * auto-scrolls to the latest content.
641
+ *
642
+ * Must be rendered inside a `SessionProvider`.
643
+ *
644
+ * @example
645
+ * ```tsx
646
+ * import { MessageList } from "@alexkroman1/aai-ui";
647
+ *
648
+ * function Conversation() {
649
+ * return <MessageList className="flex-1" />;
650
+ * }
651
+ * ```
652
+ *
653
+ * @param props - Container props.
654
+ *
655
+ * @public
656
+ */
657
+ const MessageList = memo(function MessageList({ className, log }) {
658
+ const theme = useTheme();
659
+ const renderMessage = useCallback((message) => /* @__PURE__ */ jsx(MessageBubble, {
660
+ message,
661
+ theme
662
+ }), [theme]);
663
+ const renderTool = useCallback((toolCall) => /* @__PURE__ */ jsx(ToolCallBlock, { toolCall }), []);
664
+ const renderTranscript = useCallback((transcript) => /* @__PURE__ */ jsx(UserBubble, {
665
+ theme,
666
+ color: inkTint(theme.text, theme.surface, 65),
667
+ children: transcript.partial ? transcript.partial : /* @__PURE__ */ jsx(ThinkingDots, {})
668
+ }), [theme]);
669
+ return /* @__PURE__ */ jsx(ConversationView, {
670
+ className,
671
+ log,
672
+ style: { background: theme.surface },
673
+ contentClassName: "flex flex-col gap-4 p-7",
674
+ renderMessage,
675
+ renderTool,
676
+ renderTranscript,
677
+ thinkingClassName: THINKING_ROW,
678
+ thinkingIndicator: /* @__PURE__ */ jsx(Dots, {})
679
+ });
680
+ });
681
+ //#endregion
682
+ export { useConversation as a, interleave as i, Markdown as n, AutoScroll as o, ConversationView as r, MessageList as t };