@alexkroman1/aai-ui 6.11.0 → 7.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 (66) hide show
  1. package/README.md +104 -3
  2. package/dist/_run-controls.d.ts +33 -0
  3. package/dist/{chat-view-ByQFf94G.js → chat-view-BKsFfFZJ.js} +57 -11
  4. package/dist/client-config-B4nznRvH.js +134 -0
  5. package/dist/client-config.d.ts +45 -2
  6. package/dist/client-dir.d.ts +3 -1
  7. package/dist/client-dir.js +3 -1
  8. package/dist/components/auto-scroll.d.ts +24 -13
  9. package/dist/components/button.d.ts +8 -4
  10. package/dist/components/button.js +4 -4
  11. package/dist/components/chat-view.d.ts +4 -3
  12. package/dist/components/chat-view.js +1 -1
  13. package/dist/components/console-shell.d.ts +71 -10
  14. package/dist/components/controls.d.ts +15 -4
  15. package/dist/components/controls.js +48 -2
  16. package/dist/components/form-fields.d.ts +142 -0
  17. package/dist/components/form-types.d.ts +6 -0
  18. package/dist/components/form.d.ts +38 -80
  19. package/dist/components/markdown.d.ts +31 -5
  20. package/dist/components/message-list.d.ts +21 -4
  21. package/dist/components/message-list.js +1 -1
  22. package/dist/components/sidebar-layout.d.ts +11 -0
  23. package/dist/components/sidebar-layout.js +2 -0
  24. package/dist/components/start-screen.d.ts +8 -0
  25. package/dist/components/start-screen.js +2 -0
  26. package/dist/components/tool-call-block.js +1 -1
  27. package/dist/components/tool-call-row.d.ts +24 -0
  28. package/dist/components/upload-progress.d.ts +17 -7
  29. package/dist/components/workflow-fields.d.ts +10 -22
  30. package/dist/components/workflow-progress.d.ts +29 -10
  31. package/dist/context.d.ts +36 -0
  32. package/dist/context.js +105 -14
  33. package/dist/default-client/assets/index-S5fkKi6B.css +2 -0
  34. package/dist/default-client/assets/index-fEkrcZgo.js +293 -0
  35. package/dist/default-client/index.html +2 -2
  36. package/dist/define-client.d.ts +67 -62
  37. package/dist/define-client.js +56 -22
  38. package/dist/hooks.d.ts +88 -3
  39. package/dist/index.d.ts +12 -11
  40. package/dist/index.js +501 -167
  41. package/dist/internal.d.ts +40 -0
  42. package/dist/internal.js +6 -0
  43. package/dist/{message-list-CpPV7dGx.js → message-list-DHddO4QC.js} +217 -72
  44. package/dist/{session-core-CAfYmUbg.js → session-core-C2JtLArh.js} +267 -165
  45. package/dist/session-core-audio-setup.d.ts +3 -0
  46. package/dist/session-core-messages.d.ts +3 -0
  47. package/dist/session-core-state.d.ts +146 -0
  48. package/dist/session-core-types.d.ts +77 -32
  49. package/dist/session-core.d.ts +3 -2
  50. package/dist/session-core.js +1 -1
  51. package/dist/{tool-call-block-D6pTEPrT.js → tool-call-block-DoF-cSIZ.js} +27 -19
  52. package/dist/tool-config-context-DzAofqi_.js +19 -0
  53. package/dist/types.d.ts +31 -5
  54. package/dist/types.js +5 -4
  55. package/dist/{controls-CjG91QJ4.js → url-chips-DpM7Oocj.js} +3 -46
  56. package/dist/use-conversation.d.ts +122 -0
  57. package/dist/use-download-url.d.ts +83 -0
  58. package/dist/use-workflow-form.d.ts +59 -3
  59. package/dist/use-workflow-run.d.ts +35 -0
  60. package/dist/use-workflow-stream.d.ts +36 -62
  61. package/dist/workflow-client.d.ts +36 -11
  62. package/dist/workflow-status-labels.d.ts +35 -0
  63. package/package.json +10 -5
  64. package/styles.css +14 -0
  65. package/dist/default-client/assets/index-DTLrhtTF.css +0 -2
  66. package/dist/default-client/assets/index-DXODx_9r.js +0 -293
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The browser session's {@link AgentState}, and the error beside it, as a
3
+ * statechart.
4
+ *
5
+ * These two were written independently from thirteen sites across
6
+ * `session-core.ts`, `session-core-messages.ts` and
7
+ * `session-core-audio-setup.ts`, each one deciding for itself whether its write
8
+ * was legal by reading the snapshot back first. Three shipped bugs came out of
9
+ * that, and all three are the same shape — a transition nothing forbade:
10
+ *
11
+ * - **A straggler audio chunk flipped an errored session to `"speaking"`.**
12
+ * Guarded by hand at the call site
13
+ * (`if (snap.state === "error" || (snap.state === "disconnected" && …))`).
14
+ * Here `error` simply does not handle `SPEAK`.
15
+ * - **The frame that announced a session's death also wiped the banner
16
+ * reporting it.** Every fatal path in the host tears the transport down, and
17
+ * tearing down EMITS — so `reply.cancelled` arrived right behind the error and
18
+ * `toListening()` painted a live-mic state over it. A missing provider key is
19
+ * the case that made it visible: the one message that says exactly what to fix
20
+ * was on screen for a few hundred milliseconds and left a session that looked
21
+ * live and was deaf.
22
+ * - **A later frame RECOVERED the state.** `clearRecoveredError` reads a
23
+ * non-error frame as proof the session works, which is right for a
24
+ * turn-level failure and wrong for a server error that ended the call.
25
+ *
26
+ * The second and third were fixed with a `conn.fatalError` boolean that every
27
+ * writer had to remember to consult. It is the `fatal` region here, so
28
+ * forgetting is not available: `LISTEN` and `ACTIVITY` are declined in one
29
+ * place rather than at each of their five call sites.
30
+ *
31
+ * ## Two regions, because the fatal latch OUTLIVES the error state
32
+ *
33
+ * `fatal` is not a substate of `error`, tempting as that looks. It is cleared by
34
+ * exactly one thing — the next `config` frame, i.e. a completed handshake — and
35
+ * that is per CONNECTION rather than per session, so a reconnect that really
36
+ * works is not pinned to a dead session's banner. Between the error and that
37
+ * frame the phase runs `error → connecting → ready` while the latch stays set,
38
+ * which a substate cannot express.
39
+ *
40
+ * ## The published surface is unchanged
41
+ *
42
+ * {@link AgentState} is a public type in the versioned `aai-ui:session`
43
+ * capability, so {@link AgentStateSnapshot.state} is one of its seven names and
44
+ * nothing here widens it. The internal distinctions live in the second region
45
+ * and in context, not in the projection.
46
+ */
47
+ import type { AgentState, SessionError } from "./types.ts";
48
+ /** What the session core folds into its snapshot after each transition. */
49
+ export type AgentStateSnapshot = {
50
+ state: AgentState;
51
+ error: SessionError | null;
52
+ };
53
+ /**
54
+ * Everything that moves the session's state.
55
+ *
56
+ * Named for what HAPPENED rather than for the state wanted, which is the point:
57
+ * `reply.cancelled` sends `LISTEN` and gets `"error"` back when the session is
58
+ * fatally over, and the caller neither knows nor needs to.
59
+ */
60
+ export type AgentStateEvent =
61
+ /** A connect attempt started — including a reconnect or a handshake retry. */
62
+ {
63
+ type: "CONNECT";
64
+ }
65
+ /** The socket opened; the handshake has not completed yet. */
66
+ | {
67
+ type: "SOCKET_OPEN";
68
+ }
69
+ /** A completed handshake: a live session, whatever ended the last one. */
70
+ | {
71
+ type: "HANDSHAKE_COMPLETE";
72
+ }
73
+ /** A turn boundary, or the audio path coming up: back to the mic. */
74
+ | {
75
+ type: "LISTEN";
76
+ }
77
+ /** A reset: back to the mic AND the conversation cleared. */
78
+ | {
79
+ type: "RESET";
80
+ }
81
+ /** The agent is working. */
82
+ | {
83
+ type: "THINK";
84
+ }
85
+ /** Audio for this turn reached the client. */
86
+ | {
87
+ type: "SPEAK";
88
+ }
89
+ /** A non-error frame arrived, so the session demonstrably works. */
90
+ | {
91
+ type: "ACTIVITY";
92
+ }
93
+ /** A turn-level failure: a banner over a session that keeps running. */
94
+ | {
95
+ type: "TURN_ERROR";
96
+ error: SessionError;
97
+ }
98
+ /** The session is over. */
99
+ | {
100
+ type: "FATAL";
101
+ error: SessionError;
102
+ }
103
+ /**
104
+ * A failure that stops the session but might not be the end of it: the
105
+ * handshake exhausting its retries, a socket error close, or the audio path
106
+ * dying. The `fatal` latch stays clear, so a later non-error frame recovers
107
+ * — which is right for a client-side failure and is exactly what a FATAL
108
+ * server error must not get.
109
+ */
110
+ | {
111
+ type: "FAILED";
112
+ error: SessionError;
113
+ }
114
+ /** The socket closed for good. */
115
+ | {
116
+ type: "CLOSED";
117
+ }
118
+ /** The caller hung up. */
119
+ | {
120
+ type: "DISCONNECT";
121
+ }
122
+ /** The caller ended the session and forgot it. */
123
+ | {
124
+ type: "END";
125
+ };
126
+ /** The session core's handle on its own state. */
127
+ export type SessionStateMachine = {
128
+ /** The published state and the error beside it. */
129
+ snapshot(): AgentStateSnapshot;
130
+ /**
131
+ * Apply `event` and return the resulting projection.
132
+ *
133
+ * Returns rather than writes so the caller folds it into ONE `updateState`
134
+ * call: a snapshot is published on every write, and splitting one transition
135
+ * into two would let a subscriber render half of it.
136
+ */
137
+ apply(event: AgentStateEvent): AgentStateSnapshot;
138
+ /**
139
+ * Is the session fatally over? Asked by the one caller that has to clear the
140
+ * CONVERSATION and not merely the state — a `session.reset` on a dead session
141
+ * keeps its whole banner.
142
+ */
143
+ fatal(): boolean;
144
+ };
145
+ /** Create the state machine for one browser session. */
146
+ export declare function createSessionStateMachine(): SessionStateMachine;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { Epoch } from "@alexkroman1/aai/internal";
7
7
  import type { VoiceIO } from "./audio.ts";
8
- import type { AgentState, ChatMessage, SessionError, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor } from "./types.ts";
8
+ import type { AgentState, ChatMessage, SessionError, ToolCallInfo, WebSocketConstructor } from "./types.ts";
9
9
  /**
10
10
  * A custom event emitted by the agent via `ctx.send(event, data)` — the
11
11
  * payload the session records in `SessionSnapshot.customEvents` (`id` is a
@@ -27,11 +27,30 @@ export type AgentCustomEvent = {
27
27
  * Consumers (e.g. React hooks via `useSyncExternalStore`) read this to render.
28
28
  * A new object reference is created on every state change.
29
29
  *
30
+ * @remarks
31
+ * **Four fields describe liveness and they answer different questions.** They
32
+ * are routinely collapsed into one truthy check, which is how a chrome ends up
33
+ * showing a live indicator over a call that has ended:
34
+ *
35
+ * | Field | The question it answers |
36
+ * | --- | --- |
37
+ * | `started` | Has the caller pressed Start? `end()` puts it back to `false`; `disconnect()` does not. |
38
+ * | `running` | Is the socket MEANT to be up? `toggle()` is what flips it. |
39
+ * | `recording` | Is the microphone actually live right now? |
40
+ * | `state` | What is the agent doing — see {@link AgentState}. |
41
+ *
30
42
  * @public
31
43
  */
32
44
  export type SessionSnapshot = {
45
+ /** What the agent is doing. See {@link AgentState} for the seven members. */
33
46
  readonly state: AgentState;
34
- /** True while the microphone is live and streaming to the server. */
47
+ /**
48
+ * True while the microphone is live and streaming to the server.
49
+ *
50
+ * This is the mic, not the session: a session can be `running` with the mic
51
+ * still opening, and a failure to acquire it leaves this `false` with the
52
+ * socket up.
53
+ */
35
54
  readonly recording: boolean;
36
55
  /**
37
56
  * The WebSocket URL a program can connect to directly — the long-living
@@ -50,8 +69,24 @@ export type SessionSnapshot = {
50
69
  * never collides when the capped arrays slide.
51
70
  */
52
71
  readonly contentVersion: number;
72
+ /**
73
+ * The conversation so far, oldest first — user and assistant turns only.
74
+ * Tool activity is NOT in here; it is in `toolCalls`. Capped, so the oldest
75
+ * entries slide off a long call.
76
+ */
53
77
  readonly messages: ChatMessage[];
78
+ /**
79
+ * Every tool call the agent has made this session, in order, each carrying
80
+ * its own pending/settled state. Capped like `messages`. `useToolResult` and
81
+ * `useToolCallStart` are the narrow readers; this is the whole log.
82
+ */
54
83
  readonly toolCalls: ToolCallInfo[];
84
+ /**
85
+ * Custom events the agent pushed with `ctx.send(event, data)`, in order.
86
+ * A LOG rather than a value — `useEvent(name, cb)` is the reader that
87
+ * delivers each one exactly once; reading this array directly means owning
88
+ * the cursor yourself.
89
+ */
55
90
  readonly customEvents: AgentCustomEvent[];
56
91
  /**
57
92
  * Latest state the agent projected via `syncState`, or `null` before the
@@ -59,10 +94,50 @@ export type SessionSnapshot = {
59
94
  * reads current state rather than replaying events it missed.
60
95
  */
61
96
  readonly agentState: unknown;
97
+ /**
98
+ * The caller's in-progress turn, as STT reports it.
99
+ *
100
+ * **`null` and `""` are different turns, and collapsing them is the mistake
101
+ * this field invites.** `null` is silence; `""` is speech DETECTED with no
102
+ * words back yet — where a live session sits for a few hundred milliseconds
103
+ * at the start of every turn. Read as one falsy check, the live-transcript
104
+ * row appears a beat late, on the first word rather than on the first sound,
105
+ * which is the moment it exists for.
106
+ *
107
+ * Prefer {@link useUserTranscript}, which returns the distinction as two
108
+ * named things (`speaking` to render on, `text` with a placeholder) rather
109
+ * than leaving each chrome to re-derive the ternary.
110
+ *
111
+ * Cleared when the turn is committed to `messages`.
112
+ */
62
113
  readonly userTranscript: string | null;
114
+ /**
115
+ * The agent's reply as it streams, or `null` when it is not speaking.
116
+ * Cleared when the reply is committed to `messages`, so a chrome renders
117
+ * this row and the finished message, never both.
118
+ */
63
119
  readonly agentTranscript: string | null;
120
+ /**
121
+ * The session's current failure, or `null`. Carries a `code`
122
+ * ({@link SessionErrorCode}), a message, and whether it was FATAL.
123
+ *
124
+ * A fatal error LATCHES: nothing clears it but the next completed handshake,
125
+ * because the frame announcing a session's death is also the frame that used
126
+ * to wipe the message explaining it. A non-fatal one is retired by later
127
+ * activity, which is what the recovery was written for.
128
+ */
64
129
  readonly error: SessionError | null;
130
+ /**
131
+ * Whether the caller has pressed Start. `false` until the first `start()`,
132
+ * and back to `false` after `end()` — which is what makes a start-screen
133
+ * chrome show its Start control again. `disconnect()` leaves it `true`.
134
+ */
65
135
  readonly started: boolean;
136
+ /**
137
+ * Whether the session is MEANT to be connected — the pause/resume state
138
+ * `toggle()` flips, not a report of the socket. A reconnecting session is
139
+ * still `running`.
140
+ */
66
141
  readonly running: boolean;
67
142
  };
68
143
  /**
@@ -123,14 +198,6 @@ export type SessionCore = {
123
198
  /** Alias for `disconnect` for use with `using`. */
124
199
  [Symbol.dispose](): void;
125
200
  };
126
- /**
127
- * Options accepted by `createSessionCore` — an alias of
128
- * {@link VoiceSessionOptions}, which documents every field. Two names, one
129
- * type: `client()` and `createSessionCore` share the same session options.
130
- *
131
- * @public
132
- */
133
- export type SessionCoreOptions = VoiceSessionOptions;
134
201
  /**
135
202
  * Shared mutable connection state for audio initialization.
136
203
  *
@@ -151,28 +218,6 @@ export type ConnState = {
151
218
  * reconnect on demand).
152
219
  */
153
220
  retiredByServer: boolean;
154
- /**
155
- * A FATAL error has been reported for this connection — the session is
156
- * over, and no later frame may take its banner off the screen.
157
- *
158
- * The recovery rule below it (`clearRecoveredError`) reads a non-error
159
- * frame as proof the session works, which is right for a client-side or
160
- * turn-level failure and wrong for a server error that ended the call:
161
- * every fatal path in the host tears the transport down, and tearing down
162
- * EMITS — `terminate()` calls `onCancelled()` immediately after
163
- * `emitError`. So the very next frame recovered the state to "listening"
164
- * and wiped the message. A missing provider key is the case that made this
165
- * visible: the one error that says exactly what to go and fix
166
- * ("Cartesia TTS: missing API key. Set CARTESIA_API_KEY in the agent env.")
167
- * was on screen for a few hundred milliseconds and left a session that
168
- * looked live and was deaf.
169
- *
170
- * Cleared by exactly one thing — the next `config` frame, i.e. a completed
171
- * handshake. That is per CONNECTION rather than per session, so a
172
- * reconnect that really works is not pinned to a dead session's banner,
173
- * and it is the only frame a dying session cannot produce.
174
- */
175
- fatalError: boolean;
176
221
  voiceIO: VoiceIO | null;
177
222
  audioSetupInFlight: boolean;
178
223
  /** Connection epoch, bumped on each connect()/retry (see `createEpoch`).
@@ -1,4 +1,5 @@
1
- import type { SessionCore, SessionCoreOptions } from "./session-core-types.ts";
1
+ import type { SessionCore } from "./session-core-types.ts";
2
+ import { type VoiceSessionOptions } from "./types.ts";
2
3
  /**
3
4
  * Create a framework-agnostic voice session core that connects to an AAI
4
5
  * server via WebSocket.
@@ -26,4 +27,4 @@ import type { SessionCore, SessionCoreOptions } from "./session-core-types.ts";
26
27
  *
27
28
  * @public
28
29
  */
29
- export declare function createSessionCore(options: SessionCoreOptions): SessionCore;
30
+ export declare function createSessionCore(options: VoiceSessionOptions): SessionCore;
@@ -1,2 +1,2 @@
1
- import { t as createSessionCore } from "./session-core-CAfYmUbg.js";
1
+ import { t as createSessionCore } from "./session-core-C2JtLArh.js";
2
2
  export { createSessionCore };
@@ -2,9 +2,10 @@ import { useTheme } from "./context.js";
2
2
  import { r as inkTint } from "./_colors-CcAi2FOU.js";
3
3
  import { t as Eyebrow } from "./eyebrow-C6ZFuiz6.js";
4
4
  import { i as tryParseJSON, r as truncate } from "./_utils-B6498_bm.js";
5
+ import { n as useToolConfig } from "./tool-config-context-DzAofqi_.js";
5
6
  import clsx from "clsx";
6
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
- import { createContext, memo, useContext, useMemo, useState } from "react";
8
+ import { memo, useMemo, useState } from "react";
8
9
  //#region components/tool-call-row.tsx
9
10
  /** @jsxImportSource react */
10
11
  const VARIANT_CLASSES = {
@@ -33,6 +34,30 @@ const VARIANT_CLASSES = {
33
34
  * Colors come from the nearest theme context (see {@link useTheme}); without
34
35
  * a provider the default AssemblyAI theme applies.
35
36
  *
37
+ * @example
38
+ * ```tsx
39
+ * import { ToolCallRow, useSession } from "@alexkroman1/aai-ui";
40
+ *
41
+ * // A custom chrome's tool log, from the snapshot's own `toolCalls`.
42
+ * function ToolLog() {
43
+ * const { toolCalls } = useSession();
44
+ * return (
45
+ * <div>
46
+ * {toolCalls.map((call) => (
47
+ * <ToolCallRow
48
+ * key={call.callId}
49
+ * title={call.name}
50
+ * detail={call.result}
51
+ * pending={call.status === "pending"}
52
+ * />
53
+ * ))}
54
+ * </div>
55
+ * );
56
+ * }
57
+ * ```
58
+ *
59
+ * @param props - See {@link ToolCallRowProps}.
60
+ *
36
61
  * @public
37
62
  */
38
63
  function ToolCallRow({ title, detail, pending = false, icon, variant = "default", className, children }) {
@@ -91,23 +116,6 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
91
116
  });
92
117
  }
93
118
  //#endregion
94
- //#region components/tool-config-context.ts
95
- /**
96
- * Context for tool display configuration. Installed by `client()` from
97
- * `ClientConfig.tools`; the built-in components read it via `useToolConfig`.
98
- *
99
- * @internal
100
- */
101
- const ToolConfigContext = createContext({});
102
- /**
103
- * Read tool display configuration from the nearest `ToolConfigContext.Provider`.
104
- *
105
- * @internal
106
- */
107
- function useToolConfig() {
108
- return useContext(ToolConfigContext);
109
- }
110
- //#endregion
111
119
  //#region components/tool-call-block.tsx
112
120
  /** @jsxImportSource react */
113
121
  function formatResult(result) {
@@ -174,4 +182,4 @@ const ToolCallBlock = memo(function ToolCallBlock({ toolCall, className }) {
174
182
  });
175
183
  });
176
184
  //#endregion
177
- export { ToolConfigContext as n, ToolCallRow as r, ToolCallBlock as t };
185
+ export { ToolCallRow as n, ToolCallBlock as t };
@@ -0,0 +1,19 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region components/tool-config-context.ts
3
+ /**
4
+ * Context for tool display configuration. Installed by `client()` from
5
+ * `ClientConfig.tools`; the built-in components read it via `useToolConfig`.
6
+ *
7
+ * @internal
8
+ */
9
+ const ToolConfigContext = createContext({});
10
+ /**
11
+ * Read tool display configuration from the nearest `ToolConfigContext.Provider`.
12
+ *
13
+ * @internal
14
+ */
15
+ function useToolConfig() {
16
+ return useContext(ToolConfigContext);
17
+ }
18
+ //#endregion
19
+ export { useToolConfig as n, ToolConfigContext as t };
package/dist/types.d.ts CHANGED
@@ -21,14 +21,40 @@ export { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
21
21
  *
22
22
  * Cast because `voiceIsolation` is newer than TypeScript's DOM lib.
23
23
  *
24
- * Public so a custom client that opens its own microphone gets the same
25
- * signal the built-in capture paths do.
26
- *
27
- * @public
24
+ * On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
25
+ * reason as the audio budgets above: it is a framework decision with no
26
+ * `client()` field to set, and the root is the authoring surface. A custom
27
+ * chrome that bypasses `client()` and opens its own microphone reaches it there
28
+ * alongside the providers it also needs.
28
29
  */
29
30
  export declare const VOICE_CAPTURE_CONSTRAINTS: MediaTrackConstraints;
30
31
  /**
31
- * Current state of the voice agent session.
32
+ * Current state of the voice agent session — the `state` field of
33
+ * {@link SessionSnapshot}, and what a chrome paints its status indicator from.
34
+ *
35
+ * @remarks
36
+ * The seven members, in the order a call passes through them:
37
+ *
38
+ * - `"disconnected"` — no socket. The state before the first `start()` and
39
+ * after `disconnect()` / `end()`.
40
+ * - `"connecting"` — dialling. Covers the broker lookup and every automatic
41
+ * reconnect attempt, so a session flickers back through it mid-call.
42
+ * - `"ready"` — the socket is open and the handshake is done, but no turn has
43
+ * happened yet. **The default chrome paints this with the same live
44
+ * indicator as `"listening"`**, which is deliberate — to a caller they are
45
+ * the same "the agent is there" — but they are not the same thing, and a
46
+ * session can wedge here (see `session-core-handshake.ts`).
47
+ * - `"listening"` — the microphone is open and the agent is waiting for the
48
+ * caller. Check {@link SessionSnapshot.recording} for whether the mic is
49
+ * actually live.
50
+ * - `"thinking"` — the caller's turn is committed and the agent is working:
51
+ * the LLM step, and any tool calls under it.
52
+ * - `"speaking"` — the agent's reply is playing. A caller may still barge in;
53
+ * the mic stays open throughout.
54
+ * - `"error"` — the session reported a failure. See
55
+ * {@link SessionSnapshot.error} for what it was. A FATAL error latches here
56
+ * until the next completed handshake, so a later frame cannot quietly paint
57
+ * over the banner explaining a dead call.
32
58
  *
33
59
  * @public
34
60
  */
package/dist/types.js CHANGED
@@ -20,10 +20,11 @@ import { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
20
20
  *
21
21
  * Cast because `voiceIsolation` is newer than TypeScript's DOM lib.
22
22
  *
23
- * Public so a custom client that opens its own microphone gets the same
24
- * signal the built-in capture paths do.
25
- *
26
- * @public
23
+ * On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
24
+ * reason as the audio budgets above: it is a framework decision with no
25
+ * `client()` field to set, and the root is the authoring surface. A custom
26
+ * chrome that bypasses `client()` and opens its own microphone reaches it there
27
+ * alongside the providers it also needs.
27
28
  */
28
29
  const VOICE_CAPTURE_CONSTRAINTS = {
29
30
  echoCancellation: true,
@@ -1,10 +1,9 @@
1
- import { useSessionCore, useSessionSelector, useTheme } from "./context.js";
1
+ import { useSessionSelector, useTheme } from "./context.js";
2
2
  import { r as inkTint } from "./_colors-CcAi2FOU.js";
3
- import { Button } from "./components/button.js";
4
3
  import { t as pageBaseUrl } from "./_utils-B6498_bm.js";
5
4
  import clsx from "clsx";
6
5
  import { jsx, jsxs } from "react/jsx-runtime";
7
- import { memo, useEffect, useRef, useState } from "react";
6
+ import { useEffect, useRef, useState } from "react";
8
7
  //#region components/url-chips.tsx
9
8
  /** @jsxImportSource react */
10
9
  /** How long the "Copied" confirmation replaces the label after a click. */
@@ -97,46 +96,4 @@ function SessionUrlChips({ className }) {
97
96
  });
98
97
  }
99
98
  //#endregion
100
- //#region components/controls.tsx
101
- /** @jsxImportSource react */
102
- /**
103
- * Session control buttons: **Stop / Resume** and **New Conversation**.
104
- *
105
- * Reads session state from {@link useSession}. Must be rendered inside a
106
- * `SessionProvider`.
107
- *
108
- * @example
109
- * ```tsx
110
- * import { Controls } from "@alexkroman1/aai-ui";
111
- *
112
- * function Footer() {
113
- * return <Controls className="justify-end" />;
114
- * }
115
- * ```
116
- *
117
- * @param className - Additional CSS class names applied to the container.
118
- *
119
- * @public
120
- */
121
- const Controls = memo(function Controls({ className }) {
122
- const running = useSessionSelector((s) => s.running);
123
- const { toggle, reset } = useSessionCore();
124
- return /* @__PURE__ */ jsxs("div", {
125
- className: clsx("flex flex-wrap items-center gap-3 shrink-0", className),
126
- children: [
127
- /* @__PURE__ */ jsx(Button, {
128
- variant: "secondary",
129
- onClick: toggle,
130
- children: running ? "Stop" : "Resume"
131
- }),
132
- /* @__PURE__ */ jsx(Button, {
133
- variant: "ghost",
134
- onClick: reset,
135
- children: "New Conversation"
136
- }),
137
- /* @__PURE__ */ jsx(SessionUrlChips, { className: "basis-full sm:basis-auto sm:ml-auto sm:max-w-[60%]" })
138
- ]
139
- });
140
- });
141
- //#endregion
142
- export { UiUrlChip as i, ApiUrlChip as n, SessionUrlChips as r, Controls as t };
99
+ export { SessionUrlChips as n, UiUrlChip as r, ApiUrlChip as t };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `useConversation` — the exchange, already assembled, with nothing rendered.
3
+ *
4
+ * `<MessageList>` owns four decisions that are not obvious and are not
5
+ * derivable from the snapshot by eye: the message/tool-call INTERLEAVE, the
6
+ * streaming agent utterance, the live user transcript with its `null`-vs-`""`
7
+ * protocol distinction, and the thinking indicator's suppression rule. Its only
8
+ * prop is `className`, so the moment a client wants its own bubble markup it
9
+ * drops all four at once — and the three hand-rolled chromes in the template
10
+ * tree each shipped a strictly worse conversation for exactly that reason. One
11
+ * of them runs fifteen tools and its operator sees none of them, because the
12
+ * tool calls live in a second array nothing in that page ever reads.
13
+ *
14
+ * This is the headless half. `<MessageList>` is now a thin consumer of it —
15
+ * which is the only way to know the hook is complete, since a hook that the
16
+ * package's own list cannot be built from is a hook a custom chrome will find a
17
+ * hole in.
18
+ *
19
+ * ## It subscribes NARROWLY, and that is half the value
20
+ *
21
+ * The three custom chromes also call whole-page `useSession()`, which re-renders
22
+ * on *every* snapshot change — so a dispatch board re-renders at STT-partial
23
+ * rate. `<ChatView>` deliberately avoids that with per-field
24
+ * {@link useSessionSelector} calls, and so does this: a component that reads the
25
+ * conversation gets the conversation's own update rate, not the session's.
26
+ */
27
+ import type { ChatMessage, ToolCallInfo } from "./types.ts";
28
+ import { type UseUserTranscriptResult } from "./use-user-transcript.ts";
29
+ /**
30
+ * One row of the conversation: a finalized message, or a tool invocation.
31
+ *
32
+ * A discriminated union rather than two arrays, because the ORDER between them
33
+ * is the thing this hook computes — handing back two lists would hand back the
34
+ * problem. `kind` is what a `switch` in a custom renderer narrows on.
35
+ *
36
+ * @public
37
+ */
38
+ export type ConversationItem = {
39
+ kind: "message";
40
+ message: ChatMessage;
41
+ } | {
42
+ kind: "tool";
43
+ toolCall: ToolCallInfo;
44
+ };
45
+ /**
46
+ * What {@link useConversation} returns.
47
+ *
48
+ * @public
49
+ */
50
+ export type UseConversationResult = {
51
+ /**
52
+ * Messages and tool calls in one list, in the order they happened.
53
+ *
54
+ * Referentially stable while neither array changes, so a consumer may map it
55
+ * inside a `useMemo` keyed on it, or hand rows to `memo()`ed components,
56
+ * without rebuilding the list on unrelated snapshot updates.
57
+ */
58
+ items: readonly ConversationItem[];
59
+ /**
60
+ * The agent's utterance as it arrives, or `null` between turns.
61
+ *
62
+ * Not yet a member of `items`: it has no id and it is replaced wholesale on
63
+ * every delta, so it is rendered as its own trailing row and disappears when
64
+ * the finalized message takes its place.
65
+ */
66
+ streaming: string | null;
67
+ /**
68
+ * The caller's in-progress turn — {@link useUserTranscript}'s result,
69
+ * forwarded rather than re-derived, so the `null`-vs-`""` distinction is made
70
+ * in exactly one place.
71
+ */
72
+ transcript: UseUserTranscriptResult;
73
+ /**
74
+ * Whether to show a thinking indicator.
75
+ *
76
+ * The suppression rule, and it is why this is a field rather than
77
+ * `state === "thinking"`: the agent is `thinking` for a stretch during which
78
+ * something ELSE is already saying so. A pending tool call draws its own
79
+ * spinner, and a trailing agent message means the reply has begun landing —
80
+ * in both cases a second indicator underneath reads as a second thing
81
+ * happening. So it is on only while `thinking` with no pending tool call, and
82
+ * either no messages yet, a trailing USER message, or a settled tool call
83
+ * after it.
84
+ */
85
+ thinking: boolean;
86
+ };
87
+ /**
88
+ * Subscribe to the conversation: the interleaved exchange, the streaming
89
+ * utterance, the live transcript and the thinking rule — with no markup.
90
+ *
91
+ * Must be used inside the provider `client()` installs.
92
+ *
93
+ * @example A custom bubble, keeping every rule `<MessageList>` knows
94
+ * ```tsx
95
+ * import { useConversation } from "@alexkroman1/aai-ui";
96
+ *
97
+ * function Transcript() {
98
+ * const { items, streaming, transcript, thinking } = useConversation();
99
+ * return (
100
+ * <div>
101
+ * {items.map((item) =>
102
+ * item.kind === "message" ? (
103
+ * <p key={item.message.id} data-role={item.message.role}>
104
+ * {item.message.content}
105
+ * </p>
106
+ * ) : (
107
+ * <code key={item.toolCall.callId}>{item.toolCall.name}</code>
108
+ * ),
109
+ * )}
110
+ * {streaming !== null && <p data-role="assistant">{streaming}</p>}
111
+ * {transcript.speaking && <p data-role="user">{transcript.text}</p>}
112
+ * {thinking && <p>…</p>}
113
+ * </div>
114
+ * );
115
+ * }
116
+ * ```
117
+ *
118
+ * @returns See {@link UseConversationResult}.
119
+ *
120
+ * @public
121
+ */
122
+ export declare function useConversation(): UseConversationResult;