@alexkroman1/aai-ui 13.2.0 → 14.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 (83) hide show
  1. package/README.md +159 -69
  2. package/dist/{_colors-j8XMToi9.js → _colors-CpZO-88A.js} +25 -2
  3. package/dist/{_module-url-C_4gRVL0.js → _module-url-C13kAJ87.js} +1 -1
  4. package/dist/_recover-run.d.ts +13 -7
  5. package/dist/_submission-state.d.ts +92 -0
  6. package/dist/_upload-files.d.ts +2 -2
  7. package/dist/_upload-report.d.ts +26 -0
  8. package/dist/{_utils-B6498_bm.js → _utils-DnQDM9Uy.js} +4 -4
  9. package/dist/_utils.d.ts +3 -3
  10. package/dist/_web-storage.d.ts +43 -0
  11. package/dist/_workflow-files.d.ts +1 -1
  12. package/dist/{aai-logo-CXZGPSIY.js → aai-logo-CFlomZlS.js} +1 -1
  13. package/dist/agent-state-labels.d.ts +60 -0
  14. package/dist/audio.js +21 -15
  15. package/dist/{chat-view-DDTtrh7N.js → chat-view-C_3T7Ln8.js} +100 -33
  16. package/dist/{client-config-BT_kWID5.js → client-config-DJQHnYjm.js} +5 -5
  17. package/dist/client-config.d.ts +4 -4
  18. package/dist/client-dir.d.ts +1 -1
  19. package/dist/client-dir.js +2 -2
  20. package/dist/components/_colors.d.ts +23 -0
  21. package/dist/components/_form-readiness.d.ts +1 -1
  22. package/dist/components/bullet-list.d.ts +74 -0
  23. package/dist/components/button.js +4 -4
  24. package/dist/components/chat-view.js +1 -1
  25. package/dist/components/console-shell.d.ts +16 -20
  26. package/dist/components/controls.js +4 -4
  27. package/dist/components/facts.d.ts +81 -0
  28. package/dist/components/form-fields.d.ts +6 -6
  29. package/dist/components/form-types.d.ts +1 -1
  30. package/dist/components/form.d.ts +1 -1
  31. package/dist/components/message-list.js +1 -1
  32. package/dist/components/session-error-banner.d.ts +69 -0
  33. package/dist/components/sidebar-layout.js +1 -1
  34. package/dist/components/start-screen.js +4 -4
  35. package/dist/components/tool-call-block.js +1 -1
  36. package/dist/components/tool-config-context.d.ts +1 -1
  37. package/dist/components/workflow-progress.d.ts +11 -4
  38. package/dist/context.d.ts +142 -19
  39. package/dist/context.js +156 -18
  40. package/dist/default-client/assets/{audio-BuDICbPf.js → audio-9zQsNc1w.js} +1 -1
  41. package/dist/default-client/assets/index-BTv30Z4F.css +2 -0
  42. package/dist/default-client/assets/index-RAZ-29Sz.js +284 -0
  43. package/dist/default-client/index.html +2 -2
  44. package/dist/default-client.d.ts +1 -1
  45. package/dist/define-client.d.ts +19 -19
  46. package/dist/define-client.js +20 -20
  47. package/dist/{eyebrow-C6ZFuiz6.js → eyebrow-UfmSz9yy.js} +1 -1
  48. package/dist/hooks.d.ts +44 -8
  49. package/dist/hooks.js +20 -14
  50. package/dist/index.d.ts +13 -8
  51. package/dist/index.js +1447 -1160
  52. package/dist/internal.d.ts +2 -2
  53. package/dist/internal.js +5 -5
  54. package/dist/{message-list-BJYyuIcR.js → message-list-CdOnSh5m.js} +23 -16
  55. package/dist/page.d.ts +11 -11
  56. package/dist/session-core-audio-setup.d.ts +1 -1
  57. package/dist/session-core-dial.d.ts +0 -2
  58. package/dist/{session-core-DxBYsfHA.js → session-core-gwePM95B.js} +135 -62
  59. package/dist/session-core-messages.d.ts +2 -2
  60. package/dist/session-core-types.d.ts +58 -1
  61. package/dist/session-core.d.ts +6 -6
  62. package/dist/session-core.js +2 -2
  63. package/dist/session-resume-store.d.ts +3 -3
  64. package/dist/{tool-call-block-tcPQAkcP.js → tool-call-block-C2t_5fpp.js} +30 -12
  65. package/dist/{tool-config-context-DzAofqi_.js → tool-config-context-Es4YUzV2.js} +2 -2
  66. package/dist/types.d.ts +19 -4
  67. package/dist/types.js +3 -3
  68. package/dist/{url-chips-YqhCjWfQ.js → url-chips-BxhzZgk2.js} +5 -5
  69. package/dist/use-conversation.d.ts +1 -1
  70. package/dist/use-run-key.d.ts +44 -10
  71. package/dist/{use-user-transcript-C14qWFu2.js → use-user-transcript-uyHhzy4d.js} +4 -3
  72. package/dist/use-workflow-form.d.ts +64 -91
  73. package/dist/{use-workflow-progress-Cu0SxMyg.js → use-workflow-run-CP2ekKPV.js} +254 -257
  74. package/dist/use-workflow-stream.d.ts +5 -2
  75. package/dist/use-workflows.d.ts +77 -0
  76. package/dist/workflow-client.d.ts +1 -1
  77. package/dist/worklets/capture-processor.js +2 -2
  78. package/dist/worklets/playback-processor.js +2 -2
  79. package/package.json +6 -6
  80. package/styles.css +78 -0
  81. package/dist/default-client/assets/index-B1_ROnTJ.js +0 -284
  82. package/dist/default-client/assets/index-S5fkKi6B.css +0 -2
  83. package/dist/tsdown.config.d.ts +0 -2
@@ -0,0 +1,60 @@
1
+ /**
2
+ * One word for where the CALL has got to, for each {@link AgentState} there is.
3
+ *
4
+ * The sibling of `WORKFLOW_STATUS_LABELS` next door, for the same reason and
5
+ * with the same shape. Three custom chromes carried three renderings of this
6
+ * one seven-member union, all written as a chain of ternaries over the raw
7
+ * member: one mapped `speaking` to "Narrating" and fell through to "Idle" for
8
+ * everything it had not listed, one shouted (`thinking` → "PROCESSING"), and
9
+ * the third rendered the enum member itself — so a caller of that agent read a
10
+ * lowercase `disconnected` in the header of a page nobody had decided to say
11
+ * that on.
12
+ *
13
+ * The exhaustiveness argument is the one that makes this worth exporting rather
14
+ * than documenting. A `Record` over {@link AgentState} means a state ADDED to
15
+ * the union is a compile error HERE, in one place every page inherits, instead
16
+ * of falling through each page's own `: "Idle"` tail into whichever word that
17
+ * page happened to end on — which is a silent wrong label, not a missing one. A
18
+ * page overriding one member keeps that: spreading a complete record cannot
19
+ * drop a key.
20
+ *
21
+ * There is deliberately no `StatusDot` component beside it. The dot markup and
22
+ * its palette are what each chrome exists to look like — a CRT's glow, a
23
+ * dispatch board's alert colours — and a component would take the one part
24
+ * that is genuinely shared (the words) hostage to the part that is not.
25
+ */
26
+ import type { AgentState } from "./types.ts";
27
+ /**
28
+ * The default label per {@link AgentState}.
29
+ *
30
+ * Override the ones your page has a better word for and keep the rest:
31
+ *
32
+ * ```ts
33
+ * import type { AgentState } from "@alexkroman1/aai-ui";
34
+ * import { AGENT_STATE_LABELS } from "@alexkroman1/aai-ui";
35
+ *
36
+ * // A dispatch board that shouts, and renames one state.
37
+ * const STATE_LABEL = { ...AGENT_STATE_LABELS, thinking: "Processing" };
38
+ * const shout = (s: AgentState) => STATE_LABEL[s].toUpperCase();
39
+ * ```
40
+ *
41
+ * **Sentence case, deliberately.** A template that wants caps applies its own
42
+ * `.toUpperCase()`, and a template that wants Title Case is already there;
43
+ * shipping the shouted form instead would leave the two chromes that do not
44
+ * shout with a string they have to un-shout, which no case transform does
45
+ * correctly.
46
+ *
47
+ * Two wordings are decisions rather than transliterations of the member name:
48
+ *
49
+ * - `disconnected` is **"Idle"**. It is the state a session is in BEFORE it has
50
+ * ever started as well as after it ends, so it is the first word most callers
51
+ * see; "Disconnected" reads as a fault on a page where nothing has gone
52
+ * wrong yet. Both chromes that mapped this state by hand chose "Idle" too.
53
+ * - `connecting` and `thinking` carry an ellipsis, `listening` and `speaking`
54
+ * do not. The first two are waits with nothing for the caller to do; the
55
+ * other two describe someone actually talking. Same distinction
56
+ * `WORKFLOW_STATUS_LABELS` draws with its one "Working…".
57
+ *
58
+ * @public
59
+ */
60
+ export declare const AGENT_STATE_LABELS: Readonly<Record<AgentState, string>>;
package/dist/audio.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CAPTURE_STOP_ACK_TIMEOUT_MS, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
2
- //#region audio.ts
2
+ //#region src/audio.ts
3
3
  /**
4
4
  * Throw unless the browser honored a requested context sample rate. The
5
5
  * requested rates are never advisory: captured audio is tagged with the
@@ -132,7 +132,6 @@ async function createVoiceIO(opts) {
132
132
  capture.node.onprocessorerror = () => onError?.(workletCrash("capture"));
133
133
  capture.start();
134
134
  let playNode = null;
135
- let onPlaybackStop = null;
136
135
  /**
137
136
  * Turn ids for the drain handshake. Every `done()` posts a fresh id and the
138
137
  * worklet echoes it on the matching 'stop', so a stop the worklet posted for
@@ -143,18 +142,26 @@ async function createVoiceIO(opts) {
143
142
  * reply finished while it is still speaking.
144
143
  */
145
144
  let turnSeq = 0;
146
- let pendingStopTurn = null;
145
+ /**
146
+ * The drain wait in flight, if any.
147
+ *
148
+ * ONE field, because the resolver and the turn it belongs to are one fact:
149
+ * as two independently-nulled variables they could disagree, and did — the
150
+ * suspended-context branch of `done()` cleared the turn and left the resolver
151
+ * set. A single nullable makes that unrepresentable.
152
+ */
153
+ let pendingStop = null;
147
154
  const lifecycle = new AbortController();
148
155
  /** Settle whatever drain wait is pending and forget the turn it belonged to. */
149
156
  function settlePendingStop() {
150
- onPlaybackStop?.();
151
- onPlaybackStop = null;
152
- pendingStopTurn = null;
157
+ const pending = pendingStop;
158
+ pendingStop = null;
159
+ pending?.settle();
153
160
  }
154
161
  function onWorkletStop(msg) {
155
162
  if (msg.stats && msg.stats.concealedSamples > 0) onPlaybackStats?.(msg.stats);
156
163
  if (msg.reason === "interrupt") return;
157
- if (pendingStopTurn !== null && msg.turn !== pendingStopTurn) return;
164
+ if (pendingStop !== null && msg.turn !== pendingStop.turn) return;
158
165
  settlePendingStop();
159
166
  }
160
167
  function ensurePlayNode() {
@@ -190,26 +197,25 @@ async function createVoiceIO(opts) {
190
197
  turn
191
198
  });
192
199
  if (ctx.state !== "running") {
193
- pendingStopTurn = null;
200
+ settlePendingStop();
194
201
  return Promise.resolve();
195
202
  }
196
203
  const { promise, resolve } = Promise.withResolvers();
197
- onPlaybackStop?.();
204
+ settlePendingStop();
198
205
  const settle = () => {
199
206
  clearInterval(poll);
200
207
  clearTimeout(cap);
201
- if (onPlaybackStop === settle) {
202
- onPlaybackStop = null;
203
- pendingStopTurn = null;
204
- }
208
+ if (pendingStop?.settle === settle) pendingStop = null;
205
209
  resolve();
206
210
  };
207
211
  const poll = setInterval(() => {
208
212
  if (ctx.state !== "running") settle();
209
213
  }, PLAYBACK_DONE_POLL_MS);
210
214
  const cap = setTimeout(settle, PLAYBACK_DONE_MAX_WAIT_MS);
211
- onPlaybackStop = settle;
212
- pendingStopTurn = turn;
215
+ pendingStop = {
216
+ turn,
217
+ settle
218
+ };
213
219
  return promise;
214
220
  },
215
221
  flush() {
@@ -1,12 +1,86 @@
1
- import { t as MessageList } from "./message-list-BJYyuIcR.js";
2
- import { useSessionSelector, useTheme } from "./context.js";
3
- import { n as THINKING_COLOR, r as inkTint, t as ERROR_COLOR } from "./_colors-j8XMToi9.js";
4
- import { t as AaiLogo } from "./aai-logo-CXZGPSIY.js";
5
- import { t as Eyebrow } from "./eyebrow-C6ZFuiz6.js";
1
+ import { t as MessageList } from "./message-list-CdOnSh5m.js";
2
+ import { useSessionError, useSessionStatus, useTheme } from "./context.js";
3
+ import { a as inkTint, r as THINKING_COLOR, t as ERROR_COLOR } from "./_colors-CpZO-88A.js";
4
+ import { t as AaiLogo } from "./aai-logo-CFlomZlS.js";
5
+ import { t as Eyebrow } from "./eyebrow-UfmSz9yy.js";
6
6
  import { Controls } from "./components/controls.js";
7
7
  import clsx from "clsx";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
- //#region components/console-shell.tsx
9
+ //#region src/components/session-error-banner.tsx
10
+ /** @jsxImportSource react */
11
+ /**
12
+ * The announced banner for a failed session: the error's message and code, or
13
+ * nothing at all when the session is fine.
14
+ *
15
+ * **This used to be four lines inside `ConsoleShell`, and that is why it is its
16
+ * own component.** The banner was the reason `ConsoleShell` was published —
17
+ * `role="alert"` is the one part of that component a reviewer cannot see is
18
+ * missing, since per the `fatalError` latch in `session-core.ts` the banner is
19
+ * the ONLY remaining signal a session died (the state eyebrow beside it goes
20
+ * back to reading like a live session), and a screen reader is never told an
21
+ * unannounced one appeared. But `ConsoleShell` is a whole FRAME: a centred
22
+ * `max-w-190` column with its own header and footer. Every full-bleed chrome —
23
+ * a two-pane board, a CRT — therefore could not adopt it, rebuilt the banner
24
+ * instead, and the three that did had ALREADY drifted: one rendered
25
+ * `ERROR: {message}` and dropped the code entirely, one `ERROR: {message}
26
+ * ({code})`, one `{message} ({code})`. Splitting the banner out is what lets a
27
+ * chrome take the announced-error decision without taking the layout, and
28
+ * `ConsoleShell` composes this rather than keeping a second copy, so the two
29
+ * cannot drift again.
30
+ *
31
+ * **It reads the session itself.** There is no `error` prop: a banner that
32
+ * takes its text from the caller is a banner a caller can forget to wire, which
33
+ * is exactly the failure above with an extra step. It subscribes narrowly via
34
+ * {@link useSessionError}, so a page that renders it does not re-render with
35
+ * the transcript.
36
+ *
37
+ * **The code is shown, always.** `SessionError.code` is the eight-member wire
38
+ * union — it is what a user pastes into a bug report and the only part of the
39
+ * error that is stable across wordings — and the chrome that dropped it left
40
+ * its readers with a sentence and no way to say which failure it was.
41
+ *
42
+ * Must be rendered inside the providers `client()` installs.
43
+ *
44
+ * @example A full-bleed chrome that wants the banner and not the frame
45
+ * ```tsx
46
+ * import { SessionErrorBanner } from "@alexkroman1/aai-ui";
47
+ *
48
+ * function Board() {
49
+ * return (
50
+ * <div className="grid grid-cols-[1fr_320px] h-screen">
51
+ * <main>…</main>
52
+ * <aside>…</aside>
53
+ * <SessionErrorBanner className="col-span-2" />
54
+ * </div>
55
+ * );
56
+ * }
57
+ * ```
58
+ *
59
+ * @param props - See {@link SessionErrorBannerProps}.
60
+ *
61
+ * @public
62
+ */
63
+ function SessionErrorBanner({ className }) {
64
+ const error = useSessionError();
65
+ if (!error) return null;
66
+ return /* @__PURE__ */ jsxs("div", {
67
+ role: "alert",
68
+ className: clsx("px-3.5 py-2.5 rounded-aai border text-[13px] leading-[130%] shrink-0", className),
69
+ style: {
70
+ borderColor: "rgba(179,38,30,0.35)",
71
+ background: "rgba(179,38,30,0.06)",
72
+ color: ERROR_COLOR
73
+ },
74
+ children: [
75
+ error.message,
76
+ " (",
77
+ error.code,
78
+ ")"
79
+ ]
80
+ });
81
+ }
82
+ //#endregion
83
+ //#region src/components/console-shell.tsx
10
84
  /** @jsxImportSource react */
11
85
  /**
12
86
  * Indicator dot color per state.
@@ -36,16 +110,22 @@ function stateColor(state, primary, idle) {
36
110
  * `<Controls>` under it, and until now that was the only way to get it — the
37
111
  * shell itself was internal, so a client wanting its own conversation markup
38
112
  * had to rebuild the chrome as well. Each one that did re-derived the error
39
- * banner WITHOUT `role="alert"`, which is the one part of this component a
40
- * reviewer cannot see is missing: per the `fatalError` latch in
41
- * `session-core.ts`, the banner is the only remaining signal once the state
42
- * eyebrow goes back to reading like a live session, and a screen reader is
43
- * never told an unannounced one appeared.
113
+ * banner WITHOUT `role="alert"`.
114
+ *
115
+ * **The banner is {@link SessionErrorBanner} now, composed here rather than
116
+ * spelled out.** It was four lines of this file, and this file is a whole
117
+ * FRAME — a centred `max-w-190` column — so the full-bleed chromes that needed
118
+ * the announced banner could not take it without taking a layout that would
119
+ * replace the design they exist to demonstrate. Composing means there is one
120
+ * banner, and it means this component no longer takes an `error` prop: the
121
+ * banner reads the session itself, which is one fewer thing a caller can wire
122
+ * up wrong.
44
123
  *
45
124
  * Reach for it when the conversation is yours and the frame is not. Reach for
46
- * `<ChatView>` when both are ours.
125
+ * `<ChatView>` when both are ours. Reach for `<SessionErrorBanner>` alone when
126
+ * neither is.
47
127
  *
48
- * Must be rendered inside the providers `client()` installs.
128
+ * Must be rendered inside the providers `mountClient()` installs.
49
129
  *
50
130
  * @example A custom conversation in the stock chrome
51
131
  * ```tsx
@@ -53,19 +133,17 @@ function stateColor(state, primary, idle) {
53
133
  * ConsoleShell,
54
134
  * Controls,
55
135
  * useConversation,
56
- * useSessionSelector,
136
+ * useSessionStatus,
57
137
  * } from "@alexkroman1/aai-ui";
58
138
  *
59
139
  * function Console() {
60
- * const state = useSessionSelector((s) => s.state);
61
- * const error = useSessionSelector((s) => s.error);
140
+ * const state = useSessionStatus();
62
141
  * const { items } = useConversation();
63
142
  * return (
64
143
  * <ConsoleShell
65
144
  * title="Dispatch"
66
145
  * state={state}
67
146
  * pulsing={state === "listening"}
68
- * error={error?.message}
69
147
  * footer={<Controls />}
70
148
  * >
71
149
  * <ul>
@@ -84,7 +162,7 @@ function stateColor(state, primary, idle) {
84
162
  *
85
163
  * @public
86
164
  */
87
- function ConsoleShell({ icon, title, state, pulsing, error, children, footer, className }) {
165
+ function ConsoleShell({ icon, title, state, pulsing, children, footer, className }) {
88
166
  const theme = useTheme();
89
167
  return /* @__PURE__ */ jsxs("div", {
90
168
  className: clsx("flex flex-col h-screen w-full max-w-190 mx-auto box-border px-6 py-8 gap-5 font-aai text-sm", className),
@@ -114,16 +192,7 @@ function ConsoleShell({ icon, title, state, pulsing, error, children, footer, cl
114
192
  }), state]
115
193
  })]
116
194
  }),
117
- error && /* @__PURE__ */ jsx("div", {
118
- role: "alert",
119
- className: "px-3.5 py-2.5 rounded-aai border text-[13px] leading-[130%] shrink-0",
120
- style: {
121
- borderColor: "rgba(179,38,30,0.35)",
122
- background: "rgba(179,38,30,0.06)",
123
- color: "#B3261E"
124
- },
125
- children: error
126
- }),
195
+ /* @__PURE__ */ jsx(SessionErrorBanner, {}),
127
196
  /* @__PURE__ */ jsx("div", {
128
197
  className: "flex flex-col flex-1 min-h-0 border rounded-lg overflow-hidden",
129
198
  style: {
@@ -138,7 +207,7 @@ function ConsoleShell({ icon, title, state, pulsing, error, children, footer, cl
138
207
  });
139
208
  }
140
209
  //#endregion
141
- //#region components/chat-view.tsx
210
+ //#region src/components/chat-view.tsx
142
211
  const PULSING_STATES = /* @__PURE__ */ new Set(["listening", "speaking"]);
143
212
  /**
144
213
  * The main chat interface for a voice agent session — the design-system
@@ -166,18 +235,16 @@ const PULSING_STATES = /* @__PURE__ */ new Set(["listening", "speaking"]);
166
235
  * @public
167
236
  */
168
237
  function ChatView({ icon, title, className }) {
169
- const state = useSessionSelector((s) => s.state);
170
- const error = useSessionSelector((s) => s.error);
238
+ const state = useSessionStatus();
171
239
  return /* @__PURE__ */ jsx(ConsoleShell, {
172
240
  icon,
173
241
  title,
174
242
  state,
175
243
  pulsing: PULSING_STATES.has(state),
176
- error: error?.message,
177
244
  className,
178
245
  footer: /* @__PURE__ */ jsx(Controls, {}),
179
246
  children: /* @__PURE__ */ jsx(MessageList, {})
180
247
  });
181
248
  }
182
249
  //#endregion
183
- export { ConsoleShell as n, ChatView as t };
250
+ export { ConsoleShell as n, SessionErrorBanner as r, ChatView as t };
@@ -1,5 +1,5 @@
1
1
  import { CLIENT_CONFIG_PATH, ClientConfigResponseSchema } from "@alexkroman1/aai/protocol";
2
- //#region client-config.ts
2
+ //#region src/client-config.ts
3
3
  /**
4
4
  * Pre-connection client-config lookup.
5
5
  *
@@ -84,8 +84,8 @@ async function loadClientConfig(platformUrl, fetchFn) {
84
84
  * yields the agent default (`{}`).
85
85
  *
86
86
  * **This is what a workflow app calls instead of receiving the config.**
87
- * `client()` fetches `GET client-config` for itself before it renders the
88
- * default chat shell, so a voice client never has to. `page()` mounts no
87
+ * `mountClient()` fetches `GET client-config` for itself before it renders the
88
+ * default chat shell, so a voice client never has to. `mountPage()` mounts no
89
89
  * session and makes no such request — deliberately, since a page has no shell
90
90
  * to put a name in — so a page that wants the agent's own `name` or `greeting`
91
91
  * asks for them here.
@@ -107,7 +107,7 @@ async function loadClientConfig(platformUrl, fetchFn) {
107
107
  *
108
108
  * @example
109
109
  * ```tsx
110
- * import { fetchClientConfig, page } from "@alexkroman1/aai-ui";
110
+ * import { fetchClientConfig, mountPage } from "@alexkroman1/aai-ui";
111
111
  *
112
112
  * const { name, greeting } = await fetchClientConfig(
113
113
  * location.origin + location.pathname,
@@ -122,7 +122,7 @@ async function loadClientConfig(platformUrl, fetchFn) {
122
122
  * );
123
123
  * }
124
124
  *
125
- * page({ name: name ?? "Workflows", component: App });
125
+ * mountPage({ name: name ?? "Workflows", component: App });
126
126
  * ```
127
127
  *
128
128
  * @public
@@ -72,8 +72,8 @@ export declare function loadClientConfig(platformUrl: string, fetchFn?: typeof g
72
72
  * yields the agent default (`{}`).
73
73
  *
74
74
  * **This is what a workflow app calls instead of receiving the config.**
75
- * `client()` fetches `GET client-config` for itself before it renders the
76
- * default chat shell, so a voice client never has to. `page()` mounts no
75
+ * `mountClient()` fetches `GET client-config` for itself before it renders the
76
+ * default chat shell, so a voice client never has to. `mountPage()` mounts no
77
77
  * session and makes no such request — deliberately, since a page has no shell
78
78
  * to put a name in — so a page that wants the agent's own `name` or `greeting`
79
79
  * asks for them here.
@@ -95,7 +95,7 @@ export declare function loadClientConfig(platformUrl: string, fetchFn?: typeof g
95
95
  *
96
96
  * @example
97
97
  * ```tsx
98
- * import { fetchClientConfig, page } from "@alexkroman1/aai-ui";
98
+ * import { fetchClientConfig, mountPage } from "@alexkroman1/aai-ui";
99
99
  *
100
100
  * const { name, greeting } = await fetchClientConfig(
101
101
  * location.origin + location.pathname,
@@ -110,7 +110,7 @@ export declare function loadClientConfig(platformUrl: string, fetchFn?: typeof g
110
110
  * );
111
111
  * }
112
112
  *
113
- * page({ name: name ?? "Workflows", component: App });
113
+ * mountPage({ name: name ?? "Workflows", component: App });
114
114
  * ```
115
115
  *
116
116
  * @public
@@ -15,7 +15,7 @@
15
15
  */
16
16
  /**
17
17
  * Absolute path to the prebuilt browser client's static files — pass it to
18
- * `createServer`/`createAgentServer` as `clientDir`.
18
+ * `createRuntimeServer`/`createAgentServer` as `clientDir`.
19
19
  *
20
20
  * A function, not a constant: resolution touches the module graph and throws
21
21
  * when the package is missing, and a module-level constant would move that
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import path from "node:path";
3
- //#region client-dir.ts
3
+ //#region src/client-dir.ts
4
4
  /**
5
5
  * Filesystem location of the prebuilt default client — **Node only**.
6
6
  *
@@ -19,7 +19,7 @@ import path from "node:path";
19
19
  const require = createRequire(import.meta.url);
20
20
  /**
21
21
  * Absolute path to the prebuilt browser client's static files — pass it to
22
- * `createServer`/`createAgentServer` as `clientDir`.
22
+ * `createRuntimeServer`/`createAgentServer` as `clientDir`.
23
23
  *
24
24
  * A function, not a constant: resolution touches the module graph and throws
25
25
  * when the package is missing, and a module-level constant would move that
@@ -48,3 +48,26 @@ export declare const THINKING_COLOR = "#B98900";
48
48
  * coherent tinted bubble instead of a hardcoded indigo.
49
49
  */
50
50
  export declare function primaryTint(primary: string, surface: string, pct: number): string;
51
+ /**
52
+ * The focus ring, as the class half of a pair.
53
+ *
54
+ * The ring is the only thing a keyboard user gets, and it has regressed to
55
+ * invisible twice — `outline-none` shipped here with no replacement, on the
56
+ * button and then on the URL chips, which is WCAG 2.4.7 on every control in the
57
+ * default client. Written as a SHORTHAND so it cannot be undone by Tailwind's
58
+ * shared `--tw-outline-style` variable.
59
+ *
60
+ * It is inert on its own: an outline with no colour draws nothing, so a control
61
+ * using this must also spread {@link focusRingStyle}. Naming the two together
62
+ * is what stops the next control shipping one without the other.
63
+ */
64
+ export declare const FOCUS_RING = "outline-none focus-visible:[outline:2px_solid] focus-visible:[outline-offset:2px]";
65
+ /**
66
+ * The style half of {@link FOCUS_RING} — the colour the outline draws in.
67
+ *
68
+ * A real CSS property rather than a variant class, so the `focus-visible` rule
69
+ * only has to turn the outline on.
70
+ */
71
+ export declare function focusRingStyle(primary: string): {
72
+ outlineColor: string;
73
+ };
@@ -24,7 +24,7 @@
24
24
  * one describes an element that EXISTS. So this is a context rather than an
25
25
  * attribute, and it carries the one fact a DOM read cannot recover.
26
26
  *
27
- * A form with no such children is ready by definition — `useFormFieldsPending`
27
+ * A form with no such children is ready by definition — `useFormReadiness`
28
28
  * outside a provider reports nothing pending, so every hand-written form is
29
29
  * unaffected and `Form` keeps working outside this package.
30
30
  */
@@ -0,0 +1,74 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Props for {@link BulletList}.
4
+ *
5
+ * @public
6
+ */
7
+ export type BulletListProps = {
8
+ /**
9
+ * The bullets, in the order they should read.
10
+ *
11
+ * TEXT rather than `ReactNode`, deliberately: every list this replaced was a
12
+ * string array straight off a run's output, and taking strings is what lets
13
+ * this component key them (see the component doc). A page that needs a link
14
+ * inside a bullet wants its own `<ul>`, not a prop here.
15
+ */
16
+ items: readonly string[];
17
+ /**
18
+ * Rendered as a heading above the list, inside a wrapping `<section>`.
19
+ *
20
+ * Omitted (or `null`/`false`, so `title={cond && "Risks"}` means what it
21
+ * looks like) renders the bare `<ul>` with no wrapper — which is what four of
22
+ * the five lists this replaced were.
23
+ */
24
+ title?: ReactNode | undefined;
25
+ /**
26
+ * `"sm"` adds `text-sm`, which two of the five copies carried and three did
27
+ * not. `"base"` is the default and adds nothing.
28
+ */
29
+ size?: "sm" | "base" | undefined;
30
+ /**
31
+ * ADDED to the list's own classes rather than replacing them. There is no
32
+ * `tailwind-merge` in this package, so a class that CONFLICTS with a base one
33
+ * is not reliably the winner — use this for additions, not overrides.
34
+ */
35
+ className?: string | undefined;
36
+ };
37
+ /**
38
+ * A disc-bulleted list of short strings — a run's key points, findings, risks.
39
+ *
40
+ * Five pages had written this, byte-identical apart from a `text-sm` suffix on
41
+ * two of them, and all five had the same two defects. Both are the reason this
42
+ * is a component rather than four lines a page repeats:
43
+ *
44
+ * - **All five keyed by the string itself, and these lists are MODEL OUTPUT.**
45
+ * Two identical bullets are entirely plausible — a summariser that repeats
46
+ * itself is a bad summary, not a bad program — and a repeated string is then
47
+ * a duplicate `key`: React warns, and the two `<li>`s contend for one slot in
48
+ * the reconciliation. What is keyed here instead is the content PLUS how many
49
+ * times that content has already appeared in this list, which is unique by
50
+ * construction and unchanged by a re-render that did not change the text.
51
+ * (Position alone would also be sound — these lists are replaced wholesale by
52
+ * each new output and never reordered — but it is what `noArrayIndexKey`
53
+ * exists to talk you out of, and a unique key is one `Map` away, so there is
54
+ * no reason to spend a lint suppression on it.)
55
+ * - **Three of the five rendered an empty `<ul>` under a heading.** Two had
56
+ * hand-rolled `if (items.length === 0) return null` and three had not, so the
57
+ * same absent field was "nothing" on two pages and a stray heading with a
58
+ * void under it on three. Emptiness renders NOTHING here, `title` included:
59
+ * a heading over no bullets is a claim the run did not make.
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * import { BulletList } from "@alexkroman1/aai-ui";
64
+ *
65
+ * function Findings({ risks }: { risks: string[] }) {
66
+ * return <BulletList title="Risks" items={risks} size="sm" />;
67
+ * }
68
+ * ```
69
+ *
70
+ * @param props - Bullet-list props.
71
+ *
72
+ * @public
73
+ */
74
+ export declare function BulletList({ items, title, size, className }: BulletListProps): ReactNode;
@@ -1,8 +1,8 @@
1
1
  import { useTheme } from "../context.js";
2
- import { i as primaryTint, r as inkTint } from "../_colors-j8XMToi9.js";
2
+ import { a as inkTint, i as focusRingStyle, n as FOCUS_RING, o as primaryTint } from "../_colors-CpZO-88A.js";
3
3
  import clsx from "clsx";
4
4
  import { jsx } from "react/jsx-runtime";
5
- //#region components/button.tsx
5
+ //#region src/components/button.tsx
6
6
  /** @jsxImportSource react */
7
7
  /** One variant's rest and hover colors, derived from the theme. */
8
8
  function variantColors(variant, theme) {
@@ -70,13 +70,13 @@ function Button({ variant = "default", size = "default", className, children, st
70
70
  "--aai-btn-bg-hover": colors.hoverBg,
71
71
  "--aai-btn-fg-hover": colors.hoverFg,
72
72
  "--aai-btn-bd-hover": colors.hoverBorder,
73
- outlineColor: theme.primary,
73
+ ...focusRingStyle(theme.primary),
74
74
  ...style
75
75
  };
76
76
  return /* @__PURE__ */ jsx("button", {
77
77
  type: "button",
78
78
  style: vars,
79
- className: clsx("inline-flex items-center justify-center appearance-none m-0 w-fit whitespace-nowrap", size === "lg" ? "h-11 px-7 text-xs" : "h-9 px-5 text-[11px]", "rounded-aai font-aai font-medium tracking-[1.4px] uppercase leading-none", "cursor-pointer border transition-colors duration-150", "bg-(--aai-btn-bg) text-(--aai-btn-fg) border-(--aai-btn-bd)", "enabled:hover:bg-(--aai-btn-bg-hover) enabled:hover:text-(--aai-btn-fg-hover)", "enabled:hover:border-(--aai-btn-bd-hover)", "outline-none focus-visible:[outline:2px_solid] focus-visible:[outline-offset:2px]", "disabled:cursor-not-allowed disabled:opacity-50", className),
79
+ className: clsx("inline-flex items-center justify-center appearance-none m-0 w-fit whitespace-nowrap", size === "lg" ? "h-11 px-7 text-xs" : "h-9 px-5 text-[11px]", "rounded-aai font-aai font-medium tracking-[1.4px] uppercase leading-none", "cursor-pointer border transition-colors duration-150", "bg-(--aai-btn-bg) text-(--aai-btn-fg) border-(--aai-btn-bd)", "enabled:hover:bg-(--aai-btn-bg-hover) enabled:hover:text-(--aai-btn-fg-hover)", "enabled:hover:border-(--aai-btn-bd-hover)", FOCUS_RING, "disabled:cursor-not-allowed disabled:opacity-50", className),
80
80
  ...rest,
81
81
  children
82
82
  });
@@ -1,3 +1,3 @@
1
1
  import "../context.js";
2
- import { t as ChatView } from "../chat-view-DDTtrh7N.js";
2
+ import { t as ChatView } from "../chat-view-C_3T7Ln8.js";
3
3
  export { ChatView };
@@ -14,14 +14,6 @@ export type ConsoleShellProps = {
14
14
  state: AgentState;
15
15
  /** Whether the status dot pulses. */
16
16
  pulsing: boolean;
17
- /**
18
- * Error banner text; `null`/`undefined` hides the banner.
19
- *
20
- * Pass `session.error?.message` — the banner is announced, which a
21
- * hand-rolled `<div>` in a custom chrome is not. See the `role="alert"`
22
- * comment below for why that matters more here than it looks.
23
- */
24
- error?: string | null | undefined;
25
17
  /** Card content — normally a {@link MessageList}. */
26
18
  children: ReactNode;
27
19
  /** Row rendered beneath the card (controls). */
@@ -38,16 +30,22 @@ export type ConsoleShellProps = {
38
30
  * `<Controls>` under it, and until now that was the only way to get it — the
39
31
  * shell itself was internal, so a client wanting its own conversation markup
40
32
  * had to rebuild the chrome as well. Each one that did re-derived the error
41
- * banner WITHOUT `role="alert"`, which is the one part of this component a
42
- * reviewer cannot see is missing: per the `fatalError` latch in
43
- * `session-core.ts`, the banner is the only remaining signal once the state
44
- * eyebrow goes back to reading like a live session, and a screen reader is
45
- * never told an unannounced one appeared.
33
+ * banner WITHOUT `role="alert"`.
34
+ *
35
+ * **The banner is {@link SessionErrorBanner} now, composed here rather than
36
+ * spelled out.** It was four lines of this file, and this file is a whole
37
+ * FRAME — a centred `max-w-190` column — so the full-bleed chromes that needed
38
+ * the announced banner could not take it without taking a layout that would
39
+ * replace the design they exist to demonstrate. Composing means there is one
40
+ * banner, and it means this component no longer takes an `error` prop: the
41
+ * banner reads the session itself, which is one fewer thing a caller can wire
42
+ * up wrong.
46
43
  *
47
44
  * Reach for it when the conversation is yours and the frame is not. Reach for
48
- * `<ChatView>` when both are ours.
45
+ * `<ChatView>` when both are ours. Reach for `<SessionErrorBanner>` alone when
46
+ * neither is.
49
47
  *
50
- * Must be rendered inside the providers `client()` installs.
48
+ * Must be rendered inside the providers `mountClient()` installs.
51
49
  *
52
50
  * @example A custom conversation in the stock chrome
53
51
  * ```tsx
@@ -55,19 +53,17 @@ export type ConsoleShellProps = {
55
53
  * ConsoleShell,
56
54
  * Controls,
57
55
  * useConversation,
58
- * useSessionSelector,
56
+ * useSessionStatus,
59
57
  * } from "@alexkroman1/aai-ui";
60
58
  *
61
59
  * function Console() {
62
- * const state = useSessionSelector((s) => s.state);
63
- * const error = useSessionSelector((s) => s.error);
60
+ * const state = useSessionStatus();
64
61
  * const { items } = useConversation();
65
62
  * return (
66
63
  * <ConsoleShell
67
64
  * title="Dispatch"
68
65
  * state={state}
69
66
  * pulsing={state === "listening"}
70
- * error={error?.message}
71
67
  * footer={<Controls />}
72
68
  * >
73
69
  * <ul>
@@ -86,4 +82,4 @@ export type ConsoleShellProps = {
86
82
  *
87
83
  * @public
88
84
  */
89
- export declare function ConsoleShell({ icon, title, state, pulsing, error, children, footer, className, }: ConsoleShellProps): ReactNode;
85
+ export declare function ConsoleShell({ icon, title, state, pulsing, children, footer, className, }: ConsoleShellProps): ReactNode;