@alexkroman1/aai-ui 3.1.0 → 4.0.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 (33) hide show
  1. package/dist/audio.d.ts +0 -33
  2. package/dist/audio.js +2 -20
  3. package/dist/{chat-view-C1oJxsWz.js → chat-view-CW-tZCMm.js} +1 -4
  4. package/dist/components/chat-view.js +1 -1
  5. package/dist/components/console-shell.d.ts +0 -9
  6. package/dist/components/markdown.d.ts +21 -0
  7. package/dist/components/message-list.d.ts +0 -8
  8. package/dist/components/message-list.js +3 -230
  9. package/dist/components/url-chips.d.ts +0 -15
  10. package/dist/context.d.ts +1 -1
  11. package/dist/default-client/assets/audio-DQseUQnN.js +1 -0
  12. package/dist/default-client/assets/{capture-processor-BLPsqnGl.js → capture-processor-DKFNcEhw.js} +1 -1
  13. package/dist/default-client/assets/index-CRfdZPH1.css +2 -0
  14. package/dist/default-client/assets/index-CkZ6t_eJ.js +417 -0
  15. package/dist/default-client/assets/{playback-processor-B_T_1KQP.js → playback-processor-HeyncQUX.js} +1 -1
  16. package/dist/default-client/index.html +2 -2
  17. package/dist/{define-client-Cyf1jqAl.js → define-client-DqRs1XPd.js} +2 -2
  18. package/dist/define-client.d.ts +1 -1
  19. package/dist/define-client.js +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +6 -7
  22. package/dist/message-list-q2lTiFhF.js +381 -0
  23. package/dist/{session-core-BM3WHeoY.js → session-core-CkZ8LyEA.js} +30 -24
  24. package/dist/session-core-audio-setup.d.ts +3 -6
  25. package/dist/session-core-types.d.ts +0 -9
  26. package/dist/session-core.d.ts +0 -1
  27. package/dist/session-core.js +1 -1
  28. package/dist/types.d.ts +1 -1
  29. package/dist/types.js +2 -2
  30. package/package.json +4 -2
  31. package/dist/default-client/assets/audio-BGHiiDY_.js +0 -1
  32. package/dist/default-client/assets/index-DXf7-M0Q.js +0 -244
  33. package/dist/default-client/assets/index-Dv-Q5VRL.css +0 -2
@@ -1,4 +1,4 @@
1
- import{i as e}from"./index-DXf7-M0Q.js";import{t}from"./_module-url-BX0RuRU2.js";var n=t(`
1
+ import{i as e}from"./index-CkZ6t_eJ.js";import{t}from"./_module-url-BX0RuRU2.js";var n=t(`
2
2
  class PlaybackProcessor extends AudioWorkletProcessor {
3
3
  constructor(options) {
4
4
  super();
@@ -6,8 +6,8 @@
6
6
  <title>aai</title>
7
7
  <link rel="icon" href="./favicon.ico" />
8
8
  <style>html, body { background: #FBF8F2; margin: 0; }</style>
9
- <script type="module" crossorigin src="./assets/index-DXf7-M0Q.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-Dv-Q5VRL.css">
9
+ <script type="module" crossorigin src="./assets/index-CkZ6t_eJ.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-CRfdZPH1.css">
11
11
  </head>
12
12
  <body>
13
13
  <main id="app"></main>
@@ -1,9 +1,9 @@
1
1
  import { SessionProvider, ThemeProvider } from "./context.js";
2
- import { t as ChatView } from "./chat-view-C1oJxsWz.js";
2
+ import { t as ChatView } from "./chat-view-CW-tZCMm.js";
3
3
  import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
4
4
  import { SidebarLayout } from "./components/sidebar-layout.js";
5
5
  import { StartScreen } from "./components/start-screen.js";
6
- import { t as createSessionCore } from "./session-core-BM3WHeoY.js";
6
+ import { t as createSessionCore } from "./session-core-CkZ8LyEA.js";
7
7
  import { CLIENT_CONFIG_PATH, ClientConfigResponseSchema } from "@alexkroman1/aai/protocol";
8
8
  import { createElement, useEffect, useState } from "react";
9
9
  import { jsx } from "react/jsx-runtime";
@@ -1,7 +1,7 @@
1
1
  /** @jsxImportSource react */
2
2
  import { type ComponentType } from "react";
3
3
  import { type ToolDisplayConfig } from "./components/tool-config-context.ts";
4
- import { type SessionCore } from "./session-core.ts";
4
+ import type { SessionCore } from "./session-core-types.ts";
5
5
  import type { ClientTheme, WebSocketConstructor } from "./types.ts";
6
6
  /**
7
7
  * Base options shared by both client tiers.
@@ -1,4 +1,4 @@
1
- import { t as client } from "./define-client-Cyf1jqAl.js";
1
+ import { t as client } from "./define-client-DqRs1XPd.js";
2
2
  import "./context.js";
3
3
  import "./components/sidebar-layout.js";
4
4
  import "./components/start-screen.js";
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { floatToPcm16 } from "./audio.ts";
2
1
  export { buildAgentUrl, type ClientConfigResponse, fetchClientConfig, } from "./client-config.ts";
3
2
  export { Button } from "./components/button.tsx";
4
3
  export { ChatView } from "./components/chat-view.tsx";
5
4
  export { Controls } from "./components/controls.tsx";
5
+ export { Markdown } from "./components/markdown.tsx";
6
6
  export { MessageList } from "./components/message-list.tsx";
7
7
  export { SidebarLayout } from "./components/sidebar-layout.tsx";
8
8
  export { StartScreen } from "./components/start-screen.tsx";
@@ -14,7 +14,7 @@ export { SessionProvider, ThemeProvider, useSession, useSessionSelector, useThem
14
14
  export type { ClientConfig, ClientHandle, } from "./define-client.tsx";
15
15
  export { client } from "./define-client.tsx";
16
16
  export { useEvent, useToolCallStart, useToolResult } from "./hooks.ts";
17
- export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core.ts";
18
17
  export { createSessionCore } from "./session-core.ts";
18
+ export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core-types.ts";
19
19
  export type { AgentState, ChatMessage, ClientTheme, SessionError, SessionErrorCode, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor, } from "./types.ts";
20
20
  export { VOICE_CAPTURE_CONSTRAINTS } from "./types.ts";
package/dist/index.js CHANGED
@@ -1,14 +1,13 @@
1
- import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
2
- import { floatToPcm16 } from "./audio.js";
3
- import { n as buildAgentUrl, r as fetchClientConfig, t as client } from "./define-client-Cyf1jqAl.js";
1
+ import { n as buildAgentUrl, r as fetchClientConfig, t as client } from "./define-client-DqRs1XPd.js";
4
2
  import { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
5
3
  import { Button } from "./components/button.js";
6
- import { t as ChatView } from "./chat-view-C1oJxsWz.js";
4
+ import { t as ChatView } from "./chat-view-CW-tZCMm.js";
7
5
  import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-DV368uhb.js";
6
+ import { n as Markdown, t as MessageList } from "./message-list-q2lTiFhF.js";
8
7
  import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
9
- import { MessageList } from "./components/message-list.js";
10
8
  import { SidebarLayout } from "./components/sidebar-layout.js";
11
9
  import { StartScreen } from "./components/start-screen.js";
12
- import { t as createSessionCore } from "./session-core-BM3WHeoY.js";
10
+ import { t as createSessionCore } from "./session-core-CkZ8LyEA.js";
11
+ import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
13
12
  import { useEvent, useToolCallStart, useToolResult } from "./hooks.js";
14
- export { ApiUrlChip, Button, ChatView, Controls, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, buildAgentUrl, client, createSessionCore, fetchClientConfig, floatToPcm16, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult };
13
+ export { ApiUrlChip, Button, ChatView, Controls, Markdown, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, buildAgentUrl, client, createSessionCore, fetchClientConfig, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult };
@@ -0,0 +1,381 @@
1
+ import { useSessionSelector, useTheme } from "./context.js";
2
+ import { a as primaryTint, i as TEXT_MUTED, n as SURFACE_TINT, r as TEXT_FAINT } from "./_colors-DYX7XRTr.js";
3
+ import { t as ToolCallBlock } from "./tool-call-block-DIxpG8GM.js";
4
+ import clsx from "clsx";
5
+ import { memo, useCallback, useEffect, useMemo, useRef } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import ReactMarkdown from "react-markdown";
8
+ import remarkGfm from "remark-gfm";
9
+ //#region components/markdown.tsx
10
+ /** @jsxImportSource react */
11
+ const BARE_ORDERED_MARKER = /^(\s*)(\d{1,9})([.)])\s*$/;
12
+ const BARE_BULLET_MARKER = /^(\s*)([-*+])\s*$/;
13
+ const CODE_FENCE = /^\s*(```|~~~)/;
14
+ /**
15
+ * Escape list markers that have no content, so a terse reply renders as
16
+ * text instead of vanishing. CommonMark parses a line that is only `42.`
17
+ * as an *empty* ordered list starting at 42 — a voice agent answering
18
+ * "what's 6 times 7?" with "42." would otherwise display nothing (and a
19
+ * streaming transcript briefly ending on `1.` would blank the row).
20
+ * Lines inside fenced code blocks are left alone.
21
+ */
22
+ function escapeBareListMarkers(text) {
23
+ let inFence = false;
24
+ return text.split("\n").map((line) => {
25
+ if (CODE_FENCE.test(line)) {
26
+ inFence = !inFence;
27
+ return line;
28
+ }
29
+ if (inFence) return line;
30
+ const ordered = BARE_ORDERED_MARKER.exec(line);
31
+ if (ordered) return `${ordered[1]}${ordered[2]}\\${ordered[3]}`;
32
+ const bullet = BARE_BULLET_MARKER.exec(line);
33
+ if (bullet) return `${bullet[1]}\\${bullet[2]}`;
34
+ return line;
35
+ }).join("\n");
36
+ }
37
+ /**
38
+ * Agent prose, rendered as Markdown.
39
+ *
40
+ * Pipeline and S2S models write emphasis, lists, `code`, and links; before
41
+ * this they arrived as literal asterisks and backticks. Styling is
42
+ * per-element (theme colors via inline styles, spacing via Tailwind) so it
43
+ * stays on the default client's type scale and follows custom themes.
44
+ * GFM is on for tables and strikethrough. react-markdown does not render
45
+ * raw HTML unless rehype-raw is added — keep it that way, this text comes
46
+ * from a model.
47
+ *
48
+ * Memoized alongside `MessageBubble`: message content is referentially
49
+ * stable across snapshots, so only the streaming row re-parses.
50
+ *
51
+ * @public
52
+ */
53
+ const Markdown = memo(function Markdown({ text }) {
54
+ const theme = useTheme();
55
+ return /* @__PURE__ */ jsx(ReactMarkdown, {
56
+ remarkPlugins: [remarkGfm],
57
+ components: {
58
+ p: (props) => /* @__PURE__ */ jsx("p", {
59
+ className: "my-1.5 first:mt-0 last:mb-0",
60
+ ...props
61
+ }),
62
+ ul: (props) => /* @__PURE__ */ jsx("ul", {
63
+ className: "my-1.5 list-disc pl-5",
64
+ ...props
65
+ }),
66
+ ol: (props) => /* @__PURE__ */ jsx("ol", {
67
+ className: "my-1.5 list-decimal pl-5",
68
+ ...props
69
+ }),
70
+ li: (props) => /* @__PURE__ */ jsx("li", {
71
+ className: "my-0.5",
72
+ ...props
73
+ }),
74
+ h1: (props) => /* @__PURE__ */ jsx("h1", {
75
+ className: "mt-3 mb-1.5 text-[17px] font-semibold",
76
+ ...props
77
+ }),
78
+ h2: (props) => /* @__PURE__ */ jsx("h2", {
79
+ className: "mt-3 mb-1.5 text-[16px] font-semibold",
80
+ ...props
81
+ }),
82
+ h3: (props) => /* @__PURE__ */ jsx("h3", {
83
+ className: "mt-3 mb-1.5 text-[15px] font-semibold",
84
+ ...props
85
+ }),
86
+ a: ({ style, ...props }) => /* @__PURE__ */ jsx("a", {
87
+ className: "underline underline-offset-2",
88
+ style: {
89
+ color: theme.primary,
90
+ ...style
91
+ },
92
+ target: "_blank",
93
+ rel: "noreferrer noopener",
94
+ ...props
95
+ }),
96
+ code: ({ className, style, children, ...rest }) => {
97
+ return /language-/.test(className ?? "") ? /* @__PURE__ */ jsx("code", {
98
+ className: "font-aai-mono text-[12.5px]",
99
+ ...rest,
100
+ children
101
+ }) : /* @__PURE__ */ jsx("code", {
102
+ className: "rounded-sm border px-1 py-0.5 font-aai-mono text-[12.5px]",
103
+ style: {
104
+ borderColor: theme.border,
105
+ background: SURFACE_TINT,
106
+ ...style
107
+ },
108
+ ...rest,
109
+ children
110
+ });
111
+ },
112
+ pre: ({ style, ...props }) => /* @__PURE__ */ jsx("pre", {
113
+ className: "my-1.5 overflow-x-auto rounded-md border p-2.5 whitespace-pre-wrap wrap-break-word",
114
+ style: {
115
+ borderColor: theme.border,
116
+ background: SURFACE_TINT,
117
+ ...style
118
+ },
119
+ ...props
120
+ }),
121
+ blockquote: ({ style, ...props }) => /* @__PURE__ */ jsx("blockquote", {
122
+ className: "my-1.5 border-l-2 pl-3",
123
+ style: {
124
+ borderColor: theme.border,
125
+ color: TEXT_MUTED,
126
+ ...style
127
+ },
128
+ ...props
129
+ }),
130
+ table: (props) => /* @__PURE__ */ jsx("table", {
131
+ className: "my-1.5 w-full border-collapse text-sm",
132
+ ...props
133
+ }),
134
+ th: ({ style, ...props }) => /* @__PURE__ */ jsx("th", {
135
+ className: "border px-2 py-1 text-left font-medium",
136
+ style: {
137
+ borderColor: theme.border,
138
+ ...style
139
+ },
140
+ ...props
141
+ }),
142
+ td: ({ style, ...props }) => /* @__PURE__ */ jsx("td", {
143
+ className: "border px-2 py-1",
144
+ style: {
145
+ borderColor: theme.border,
146
+ ...style
147
+ },
148
+ ...props
149
+ }),
150
+ hr: ({ style, ...props }) => /* @__PURE__ */ jsx("hr", {
151
+ className: "my-2.5 border-t",
152
+ style: {
153
+ borderColor: theme.border,
154
+ ...style
155
+ },
156
+ ...props
157
+ })
158
+ },
159
+ children: escapeBareListMarkers(text)
160
+ });
161
+ });
162
+ //#endregion
163
+ //#region components/message-list.tsx
164
+ /** @jsxImportSource react */
165
+ const DOT_STYLES = [
166
+ 0,
167
+ .16,
168
+ .32
169
+ ].map((delay) => ({
170
+ animation: "aai-bounce 1.4s infinite ease-in-out both",
171
+ animationDelay: `${delay}s`
172
+ }));
173
+ /** Animated three-dot "thinking" indicator. @internal */
174
+ function ThinkingDots() {
175
+ return /* @__PURE__ */ jsx("div", {
176
+ className: "flex items-center gap-2 text-sm font-medium min-h-5",
177
+ style: { color: TEXT_MUTED },
178
+ children: DOT_STYLES.map((style, i) => /* @__PURE__ */ jsx("div", {
179
+ className: "w-1.5 h-1.5 rounded-full",
180
+ style: {
181
+ ...style,
182
+ background: TEXT_MUTED
183
+ }
184
+ }, i))
185
+ });
186
+ }
187
+ /**
188
+ * Right-aligned user bubble — a primary-tinted card (the design system's
189
+ * indigo-50 fill with an indigo-100 edge, derived from the theme primary so
190
+ * custom themes stay coherent). Shared by finalized messages and the live
191
+ * transcript.
192
+ */
193
+ function UserBubble({ theme, color, children }) {
194
+ return /* @__PURE__ */ jsx("div", {
195
+ className: "flex flex-col w-full items-end",
196
+ children: /* @__PURE__ */ jsx("div", {
197
+ 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]",
198
+ style: {
199
+ background: primaryTint(theme.primary, theme.surface, 7),
200
+ borderColor: primaryTint(theme.primary, theme.surface, 16),
201
+ color
202
+ },
203
+ children
204
+ })
205
+ });
206
+ }
207
+ /**
208
+ * Renders a single chat message: labeled agent prose, or a user bubble.
209
+ *
210
+ * Memoized so appending one message re-renders one row, not the whole capped
211
+ * list: message objects and the theme are referentially stable across
212
+ * snapshots, and rows are keyed on stable ids (`ChatMessage.id`) that survive
213
+ * the sliding 200-message window.
214
+ */
215
+ const MessageBubble = memo(function MessageBubble({ message, theme }) {
216
+ if (message.role === "user") return /* @__PURE__ */ jsx(UserBubble, {
217
+ theme,
218
+ color: theme.text,
219
+ children: message.content
220
+ });
221
+ return /* @__PURE__ */ jsxs("div", {
222
+ className: "flex flex-col gap-1 max-w-[82%]",
223
+ children: [/* @__PURE__ */ jsx("span", {
224
+ className: "text-[10px] font-medium tracking-[1.2px] uppercase leading-none",
225
+ style: { color: TEXT_FAINT },
226
+ children: "Agent"
227
+ }), /* @__PURE__ */ jsx("div", {
228
+ className: "wrap-break-word text-[15px] font-normal leading-[23px]",
229
+ style: { color: theme.text },
230
+ children: /* @__PURE__ */ jsx(Markdown, { text: message.content })
231
+ })]
232
+ });
233
+ });
234
+ /**
235
+ * How close to the bottom (px) the container must be for auto-scroll to stay
236
+ * engaged. Generous enough that an in-flight smooth scroll doesn't unpin.
237
+ */
238
+ const NEAR_BOTTOM_PX = 96;
239
+ /**
240
+ * Smooth-scroll to the anchor whenever the content version advances — but
241
+ * only while the container is pinned near the bottom. A user who scrolled up
242
+ * to read history isn't yanked back down by streaming updates; scrolling back
243
+ * to the bottom re-engages the auto-scroll.
244
+ *
245
+ * The scroll runs inside `requestAnimationFrame` and is deduped per frame:
246
+ * several snapshot updates in one frame (transcript + message + tool call)
247
+ * trigger a single scroll after layout instead of one forced layout each.
248
+ *
249
+ * While a partial transcript is streaming, updates arrive faster than a
250
+ * smooth scroll finishes — each restart leaves the animation perpetually
251
+ * mid-flight — so `streaming` switches to an instant jump; committed-content
252
+ * updates keep the smooth animation.
253
+ */
254
+ function useAutoScroll(contentVersion, streaming) {
255
+ const anchorRef = useRef(null);
256
+ const pinnedRef = useRef(true);
257
+ const scheduledRef = useRef(false);
258
+ const streamingRef = useRef(streaming);
259
+ streamingRef.current = streaming;
260
+ const onScroll = useCallback((event) => {
261
+ const el = event.currentTarget;
262
+ pinnedRef.current = el.scrollTop + el.clientHeight >= el.scrollHeight - NEAR_BOTTOM_PX;
263
+ }, []);
264
+ useEffect(() => {
265
+ if (contentVersion === 0 || scheduledRef.current || !pinnedRef.current) return;
266
+ scheduledRef.current = true;
267
+ requestAnimationFrame(() => {
268
+ scheduledRef.current = false;
269
+ if (!pinnedRef.current) return;
270
+ anchorRef.current?.scrollIntoView({
271
+ behavior: streamingRef.current ? "instant" : "smooth",
272
+ block: "end"
273
+ });
274
+ });
275
+ }, [contentVersion]);
276
+ return {
277
+ anchorRef,
278
+ onScroll
279
+ };
280
+ }
281
+ /**
282
+ * Interleave messages and tool calls into render items, ordered by insertion
283
+ * time. Each tool call renders immediately after its anchor message
284
+ * (`afterMessageId`); tool calls whose anchor slid out of the retained window
285
+ * (or that were inserted before any message existed) render first.
286
+ */
287
+ function interleave(messages, toolCalls, renderMessage, renderToolCall) {
288
+ const items = [];
289
+ let tci = 0;
290
+ const pushToolCallsThrough = (maxAfterId) => {
291
+ let tc = toolCalls[tci];
292
+ while (tc && tc.afterMessageId <= maxAfterId) {
293
+ items.push(renderToolCall(tc));
294
+ tci++;
295
+ tc = toolCalls[tci];
296
+ }
297
+ };
298
+ const firstMessage = messages[0];
299
+ if (firstMessage) pushToolCallsThrough(firstMessage.id - 1);
300
+ for (const msg of messages) {
301
+ items.push(renderMessage(msg));
302
+ pushToolCallsThrough(msg.id);
303
+ }
304
+ pushToolCallsThrough(Number.POSITIVE_INFINITY);
305
+ return items;
306
+ }
307
+ /**
308
+ * Scrollable list of all chat messages, tool-call blocks, live transcript,
309
+ * streaming agent utterance, and a thinking indicator.
310
+ *
311
+ * Messages and tool calls are interleaved in the correct order. The list
312
+ * auto-scrolls to the latest content.
313
+ *
314
+ * Must be rendered inside a {@link SessionProvider}.
315
+ *
316
+ * @example
317
+ * ```tsx
318
+ * <MessageList className="flex-1" />
319
+ * ```
320
+ *
321
+ * @param className - Additional CSS class names applied to the scroll container.
322
+ *
323
+ * @public
324
+ */
325
+ const MessageList = memo(function MessageList({ className }) {
326
+ const state = useSessionSelector((s) => s.state);
327
+ const messages = useSessionSelector((s) => s.messages);
328
+ const toolCalls = useSessionSelector((s) => s.toolCalls);
329
+ const userTranscript = useSessionSelector((s) => s.userTranscript);
330
+ const agentTranscript = useSessionSelector((s) => s.agentTranscript);
331
+ const contentVersion = useSessionSelector((s) => s.contentVersion);
332
+ const theme = useTheme();
333
+ const showThinking = useMemo(() => {
334
+ if (state !== "thinking") return false;
335
+ const last = toolCalls.at(-1);
336
+ if (last?.status === "pending") return false;
337
+ const lastMsg = messages.at(-1);
338
+ return !lastMsg || lastMsg.role === "user" || Boolean(last);
339
+ }, [
340
+ state,
341
+ toolCalls,
342
+ messages
343
+ ]);
344
+ const { anchorRef, onScroll } = useAutoScroll(contentVersion, userTranscript !== null);
345
+ const items = useMemo(() => interleave(messages, toolCalls, (msg) => /* @__PURE__ */ jsx(MessageBubble, {
346
+ message: msg,
347
+ theme
348
+ }, msg.id), (tc) => /* @__PURE__ */ jsx(ToolCallBlock, { toolCall: tc }, tc.callId)), [
349
+ messages,
350
+ toolCalls,
351
+ theme
352
+ ]);
353
+ return /* @__PURE__ */ jsx("div", {
354
+ role: "log",
355
+ className: clsx("flex-1 overflow-y-auto [scrollbar-width:none]", className),
356
+ style: { background: theme.surface },
357
+ onScroll,
358
+ children: /* @__PURE__ */ jsxs("div", {
359
+ className: "flex flex-col gap-4 p-7",
360
+ children: [
361
+ items,
362
+ agentTranscript && /* @__PURE__ */ jsx(MessageBubble, {
363
+ message: {
364
+ role: "assistant",
365
+ content: agentTranscript
366
+ },
367
+ theme
368
+ }),
369
+ userTranscript !== null && /* @__PURE__ */ jsx(UserBubble, {
370
+ theme,
371
+ color: "#6F6A60",
372
+ children: userTranscript ? userTranscript : /* @__PURE__ */ jsx(ThinkingDots, {})
373
+ }),
374
+ showThinking && /* @__PURE__ */ jsx(ThinkingDots, {}),
375
+ /* @__PURE__ */ jsx("div", { ref: anchorRef })
376
+ ]
377
+ })
378
+ });
379
+ });
380
+ //#endregion
381
+ export { Markdown as n, MessageList as t };
@@ -1,6 +1,6 @@
1
1
  import { MIC_SEND_MAX_BUFFERED_BYTES } from "./types.js";
2
- import { DEFAULT_MAX_HISTORY, WS_OPEN, errorMessage, safeJsonParse } from "@alexkroman1/aai";
3
2
  import { ServerMessageSchema, lenientParse } from "@alexkroman1/aai/protocol";
3
+ import { DEFAULT_MAX_HISTORY, WS_OPEN, errorMessage, safeJsonParse } from "@alexkroman1/aai";
4
4
  import ReconnectingWebSocket from "partysocket/ws";
5
5
  //#region session-core-audio-setup.ts
6
6
  /**
@@ -23,20 +23,17 @@ import ReconnectingWebSocket from "partysocket/ws";
23
23
  * VoiceIO is closed immediately to prevent it from being assigned to a newer
24
24
  * connection.
25
25
  *
26
- * `fatal` selects the failure mode: voice sessions (`config` handshake) can't
27
- * function without the mic, so failure there sets the error state and ends
28
- * the session; a text-only session's opt-in record button must not brick an
29
- * otherwise healthy session, so failure there is a banner (`error` set,
30
- * state kept) and the session stays interactive.
26
+ * A failure is fatal: a voice session can't function without the mic, so it
27
+ * sets the error state and ends the session.
31
28
  */
32
- async function initAudioCapture(conn, msg, deps, fatal) {
29
+ async function initAudioCapture(conn, msg, deps) {
33
30
  if (conn.audioSetupInFlight) return;
34
31
  conn.audioSetupInFlight = true;
35
32
  const gen = conn.generation;
36
33
  const stale = () => conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN;
37
34
  const reportAudioFailure = (message) => {
38
35
  deps.cleanupAudio();
39
- if (fatal) deps.updateState({
36
+ deps.updateState({
40
37
  state: "error",
41
38
  error: {
42
39
  code: "audio",
@@ -45,13 +42,6 @@ async function initAudioCapture(conn, msg, deps, fatal) {
45
42
  running: false,
46
43
  recording: false
47
44
  });
48
- else deps.updateState({
49
- error: {
50
- code: "audio",
51
- message
52
- },
53
- recording: false
54
- });
55
45
  };
56
46
  try {
57
47
  const [{ createVoiceIO }, captureWorklet, playbackWorklet] = await Promise.all([
@@ -182,7 +172,28 @@ function createMessageHandlers(deps) {
182
172
  state: "thinking"
183
173
  });
184
174
  }
175
+ /**
176
+ * `agent_transcript` carries the reply's text *so far* and is cumulative
177
+ * within a reply (see the protocol schema), so it renders as the live
178
+ * assistant bubble and only becomes a message when the reply closes. Pipeline
179
+ * mode sends one per piece of speech, so appending each would break a single
180
+ * reply into a message per sentence.
181
+ */
185
182
  function handleAgentTranscriptEvent(text) {
183
+ updateState({ agentTranscript: text });
184
+ }
185
+ /**
186
+ * The reply is over (`reply_done`, or `cancelled` for a barge-in): move
187
+ * whatever was spoken into the conversation. A cancelled reply still keeps its
188
+ * text — the caller heard that much, and dropping it would leave the
189
+ * transcript claiming the agent never spoke.
190
+ */
191
+ function commitAgentTranscript() {
192
+ const text = getSnapshot().agentTranscript;
193
+ if (text === null || text.length === 0) {
194
+ updateState({ agentTranscript: null });
195
+ return;
196
+ }
186
197
  updateState({
187
198
  agentTranscript: null,
188
199
  messages: appendCapped(getSnapshot().messages, {
@@ -265,14 +276,15 @@ function createMessageHandlers(deps) {
265
276
  break;
266
277
  }
267
278
  case "reply_done":
279
+ commitAgentTranscript();
268
280
  updateState({ state: "listening" });
269
281
  break;
270
282
  case "cancelled":
271
283
  handlerGeneration++;
272
284
  conn.voiceIO?.flush();
285
+ commitAgentTranscript();
273
286
  updateState({
274
287
  userTranscript: null,
275
- agentTranscript: null,
276
288
  state: "listening"
277
289
  });
278
290
  break;
@@ -290,7 +302,6 @@ function createMessageHandlers(deps) {
290
302
  case "error":
291
303
  handleErrorEvent(e);
292
304
  break;
293
- case "idle_timeout": break;
294
305
  default: break;
295
306
  }
296
307
  }
@@ -482,8 +493,7 @@ function createSessionCore(options) {
482
493
  audioSetupInFlight: false,
483
494
  generation: 0,
484
495
  preInitAudio: [],
485
- preInitDone: false,
486
- readyConfig: null
496
+ preInitDone: false
487
497
  };
488
498
  let connectionController = null;
489
499
  let hasConnected = false;
@@ -544,11 +554,7 @@ function createSessionCore(options) {
544
554
  }
545
555
  const isReconnect = hasConnected;
546
556
  hasConnected = true;
547
- conn.readyConfig = {
548
- sampleRate: config.sampleRate,
549
- ttsSampleRate: config.ttsSampleRate
550
- };
551
- initAudioCapture(conn, config, audioDeps, true);
557
+ initAudioCapture(conn, config, audioDeps);
552
558
  if (isReconnect && currentSnapshot.messages.length > 0) sendJson({
553
559
  type: "history",
554
560
  messages: currentSnapshot.messages.map((m) => ({
@@ -24,13 +24,10 @@ export type AudioSetupDeps = {
24
24
  * VoiceIO is closed immediately to prevent it from being assigned to a newer
25
25
  * connection.
26
26
  *
27
- * `fatal` selects the failure mode: voice sessions (`config` handshake) can't
28
- * function without the mic, so failure there sets the error state and ends
29
- * the session; a text-only session's opt-in record button must not brick an
30
- * otherwise healthy session, so failure there is a banner (`error` set,
31
- * state kept) and the session stays interactive.
27
+ * A failure is fatal: a voice session can't function without the mic, so it
28
+ * sets the error state and ends the session.
32
29
  */
33
30
  export declare function initAudioCapture(conn: ConnState, msg: {
34
31
  sampleRate: number;
35
32
  ttsSampleRate: number;
36
- }, deps: AudioSetupDeps, fatal: boolean): Promise<void>;
33
+ }, deps: AudioSetupDeps): Promise<void>;
@@ -2,8 +2,6 @@
2
2
  * Type declarations for the framework-agnostic voice session core.
3
3
  *
4
4
  * Split out of `session-core.ts` to keep that module focused on behaviour.
5
- * The public types here are re-exported from `session-core.ts` for
6
- * backwards compatibility.
7
5
  */
8
6
  import type { VoiceIO } from "./audio.ts";
9
7
  import type { AgentState, ChatMessage, SessionError, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor } from "./types.ts";
@@ -111,11 +109,4 @@ export type ConnState = {
111
109
  * signal must be replayed after draining preInitAudio, or a short greeting
112
110
  * buffered during mic-permission never finishes playing. */
113
111
  preInitDone: boolean;
114
- /** The server's `config` payload for the current connection — kept so
115
- * text-only sessions can init the mic lazily (record button) and file
116
- * uploads know the STT sample rate to resample to. */
117
- readyConfig: {
118
- sampleRate: number;
119
- ttsSampleRate: number;
120
- } | null;
121
112
  };
@@ -1,5 +1,4 @@
1
1
  import type { SessionCore, SessionCoreOptions } from "./session-core-types.ts";
2
- export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core-types.ts";
3
2
  /**
4
3
  * Create a framework-agnostic voice session core that connects to an AAI
5
4
  * server via WebSocket.
@@ -1,2 +1,2 @@
1
- import { t as createSessionCore } from "./session-core-BM3WHeoY.js";
1
+ import { t as createSessionCore } from "./session-core-CkZ8LyEA.js";
2
2
  export { createSessionCore };
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { SessionErrorCode } from "@alexkroman1/aai/protocol";
2
- export { CAPTURE_STOP_ACK_TIMEOUT_MS, DEFAULT_STT_SAMPLE_RATE, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, } from "@alexkroman1/aai";
2
+ export { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, } from "@alexkroman1/aai";
3
3
  /**
4
4
  * `getUserMedia` audio constraints for every capture path in this package.
5
5
  *
package/dist/types.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CAPTURE_STOP_ACK_TIMEOUT_MS, DEFAULT_STT_SAMPLE_RATE, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS } from "@alexkroman1/aai";
1
+ import { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS } from "@alexkroman1/aai";
2
2
  //#region types.ts
3
3
  /**
4
4
  * `getUserMedia` audio constraints for every capture path in this package.
@@ -29,4 +29,4 @@ const VOICE_CAPTURE_CONSTRAINTS = {
29
29
  voiceIsolation: false
30
30
  };
31
31
  //#endregion
32
- export { CAPTURE_STOP_ACK_TIMEOUT_MS, DEFAULT_STT_SAMPLE_RATE, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, VOICE_CAPTURE_CONSTRAINTS };
32
+ export { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, VOICE_CAPTURE_CONSTRAINTS };