@alexkroman1/aai-ui 0.17.0 → 0.18.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 (71) hide show
  1. package/README.md +6 -7
  2. package/dist/_submission-timing.d.ts +40 -0
  3. package/dist/_submission.d.ts +56 -0
  4. package/dist/_theme.d.ts +17 -0
  5. package/dist/_theme.test-d.d.ts +14 -0
  6. package/dist/{_utils-Mi-SCFA1.js → _utils-BlZBJPdj.js} +4 -5
  7. package/dist/_utils.d.ts +4 -5
  8. package/dist/agent-state.d.ts +4 -2
  9. package/dist/append-dictation.d.ts +23 -0
  10. package/dist/audio/dictation.d.ts +36 -0
  11. package/dist/audio/index.d.ts +4 -2
  12. package/dist/audio/index.js +44 -1
  13. package/dist/{chat-view-W6KcdjJY.js → chat-view-BwKpF0MN.js} +27 -17
  14. package/dist/{client-config-CzvWmw4X.js → client-config-DcMCWmkO.js} +1 -1
  15. package/dist/components/button.js +1 -1
  16. package/dist/components/chat-view.js +1 -2
  17. package/dist/components/controls.js +2 -2
  18. package/dist/components/form-fields.d.ts +24 -5
  19. package/dist/components/form-types.d.ts +32 -0
  20. package/dist/components/form.d.ts +1 -1
  21. package/dist/components/message-list.js +1 -2
  22. package/dist/components/session-state-dot.d.ts +30 -18
  23. package/dist/components/sidebar-layout.js +1 -1
  24. package/dist/components/start-screen.js +2 -2
  25. package/dist/components/tool-call-block.js +1 -2
  26. package/dist/components/tool-config-context.d.ts +21 -4
  27. package/dist/components/workflow-fields.d.ts +36 -12
  28. package/dist/components/workflow-pending-note.d.ts +30 -12
  29. package/dist/context-BF0KsI9X.js +473 -0
  30. package/dist/context.js +1 -381
  31. package/dist/default-client/assets/audio-CtBvSS3L.js +1 -0
  32. package/dist/default-client/assets/{capture-processor-Cx1AdMHL.js → capture-processor-BZXisGjZ.js} +1 -1
  33. package/dist/default-client/assets/index-BkjCv4MX.css +2 -0
  34. package/dist/default-client/assets/index-DUQoaxdq.js +127 -0
  35. package/dist/default-client/assets/{playback-processor-CCwTknSu.js → playback-processor-BHk-iXEU.js} +1 -1
  36. package/dist/default-client/index.html +2 -2
  37. package/dist/default-client.d.ts +1 -3
  38. package/dist/define-client.d.ts +30 -28
  39. package/dist/define-client.js +84 -43
  40. package/dist/dictation-transcriber.d.ts +49 -0
  41. package/dist/{eyebrow-UfmSz9yy.js → eyebrow-Bp4y1rZ9.js} +1 -1
  42. package/dist/{hooks-DN4gHYsj.js → hooks-1CoQY_Fc.js} +2 -2
  43. package/dist/hooks.js +1 -2
  44. package/dist/index.d.ts +11 -8
  45. package/dist/index.js +849 -263
  46. package/dist/internal.d.ts +1 -1
  47. package/dist/internal.js +6 -6
  48. package/dist/{message-list-BsaBkBgo.js → message-list-BBHLK05X.js} +3 -3
  49. package/dist/route-fetch.d.ts +7 -1
  50. package/dist/session/index.js +1 -1
  51. package/dist/{session-3lgPYNNO.js → session-Rud8mmU-.js} +2 -2
  52. package/dist/{tool-call-block-uXtMOUWN.js → tool-call-block-C8s6AhjS.js} +4 -4
  53. package/dist/tool-config-context-Bd-7xrfP.js +28 -0
  54. package/dist/types.d.ts +47 -2
  55. package/dist/{url-chips-oyy_Eiqh.js → url-chips-C_SHU6fq.js} +2 -2
  56. package/dist/use-dictation.d.ts +145 -0
  57. package/dist/use-route.d.ts +49 -7
  58. package/dist/use-run-key.d.ts +6 -1
  59. package/dist/{use-user-transcript-uyHhzy4d.js → use-user-transcript-lOu_uMnf.js} +1 -1
  60. package/dist/use-workflow-form.d.ts +74 -172
  61. package/dist/{use-workflow-run-DdhWmbx-.js → use-workflow-run-BTEemod0.js} +2 -2
  62. package/dist/use-workflow-runs.d.ts +8 -2
  63. package/dist/use-workflow-stream.d.ts +9 -5
  64. package/dist/workflow-def-types.d.ts +18 -2
  65. package/dist/workflow-field-kind.d.ts +59 -7
  66. package/dist/workflow-submission.d.ts +265 -0
  67. package/package.json +2 -2
  68. package/dist/default-client/assets/audio-Ct7JpSNT.js +0 -1
  69. package/dist/default-client/assets/index-BlUYCTB5.js +0 -126
  70. package/dist/default-client/assets/index-_-x5JLLa.css +0 -2
  71. package/dist/tool-config-context-Es4YUzV2.js +0 -19
@@ -0,0 +1,473 @@
1
+ import { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
2
+ import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
3
+ import { omitUndefined } from "@alexkroman1/aai/utils";
4
+ //#region src/_theme.ts
5
+ /**
6
+ * One `ClientTheme` — either form — to the five resolved tokens.
7
+ *
8
+ * Its own module rather than a branch in `context.ts` (which is at the cap)
9
+ * because the derivation is a decision worth a test of its own: the dark
10
+ * neutrals are chosen to keep the components' derived tints (`_colors.ts`'s
11
+ * `inkTint`, which walks from `text` toward the ground) above WCAG AA on a
12
+ * dark surface, exactly as the light defaults do on a light one.
13
+ */
14
+ const DEFAULT_THEME = Object.freeze({
15
+ bg: "#FAFAFA",
16
+ primary: "#18181B",
17
+ text: "#18181B",
18
+ surface: "#FFFFFF",
19
+ border: "#E4E4E7"
20
+ });
21
+ /**
22
+ * The dark ground the preset form derives: the light defaults' zinc scale
23
+ * read from the other end (zinc-950 ground, zinc-900 cards, zinc-800 borders,
24
+ * near-white ink), so `inkTint` lands on the same steps mirrored.
25
+ */
26
+ const DARK_NEUTRALS = Object.freeze({
27
+ bg: "#09090B",
28
+ surface: "#18181B",
29
+ text: "#FAFAFA",
30
+ border: "#27272A"
31
+ });
32
+ /** Is this the preset form? Only it carries `mode`; `{ primary }` alone is both and resolves alike. */
33
+ function isPreset(theme) {
34
+ return "mode" in theme && theme.mode !== void 0;
35
+ }
36
+ /**
37
+ * The theme without its BLANK tokens, each one warned about.
38
+ *
39
+ * `primary: ""` used to be written verbatim as `--aai-primary: ;`, an empty
40
+ * custom property, so every primary-coloured control went transparent with
41
+ * nothing anywhere saying why. An empty string is not a colour and not "unset"
42
+ * either, so it is dropped — the default fills the hole as for an `undefined`
43
+ * token — and said once per resolve. Colour SYNTAX is not validated: the
44
+ * browser's own parsing is the authority on what a colour is.
45
+ */
46
+ function withoutBlankTokens(theme) {
47
+ const kept = { ...theme };
48
+ for (const [token, value] of Object.entries(theme)) {
49
+ if (value !== "") continue;
50
+ console.warn(`[aai-ui] theme.${token} is "" and was ignored; the default is used. Pass a colour, or leave the token out.`);
51
+ delete kept[token];
52
+ }
53
+ return kept;
54
+ }
55
+ /**
56
+ * Every token filled in: the preset's neutrals under its accent, or the given
57
+ * tokens over the defaults. `undefined` is the default theme itself. A token
58
+ * given as `""` is ignored with a warning ({@link withoutBlankTokens}).
59
+ */
60
+ function resolveClientTheme(theme) {
61
+ if (theme === void 0) return DEFAULT_THEME;
62
+ if (isPreset(theme)) {
63
+ const neutrals = theme.mode === "dark" ? DARK_NEUTRALS : DEFAULT_THEME;
64
+ const { primary } = withoutBlankTokens({ primary: theme.primary });
65
+ return {
66
+ ...neutrals,
67
+ primary: primary ?? neutrals.text
68
+ };
69
+ }
70
+ return {
71
+ ...DEFAULT_THEME,
72
+ ...omitUndefined(withoutBlankTokens(theme))
73
+ };
74
+ }
75
+ //#endregion
76
+ //#region src/context.ts
77
+ const SessionCtx = createContext(null);
78
+ /**
79
+ * Provides the {@link BrowserSession} the session hooks read. `mountClient()`
80
+ * installs it automatically; a custom tree only needs it when bypassing
81
+ * `mountClient()` and mounting React itself.
82
+ *
83
+ * @internal
84
+ */
85
+ function SessionProvider({ value, children }) {
86
+ return createElement(SessionCtx.Provider, { value }, children);
87
+ }
88
+ /**
89
+ * Return the raw {@link BrowserSession} from context without subscribing to
90
+ * snapshot changes. Useful for accessing stable methods (`start`, `toggle`,
91
+ * `reset`, …) from components that select narrow state via
92
+ * {@link useSessionSelector}.
93
+ *
94
+ * Not part of the package's public export surface — internal to aai-ui
95
+ * components.
96
+ */
97
+ function useSessionCore() {
98
+ const core = useContext(SessionCtx);
99
+ if (!core) throw new Error("Session hooks must be used within <SessionProvider>");
100
+ return core;
101
+ }
102
+ /** The session from context, or `null` outside the provider (`useRoute` on a `mountPage()` page). */
103
+ function useOptionalSessionCore() {
104
+ return useContext(SessionCtx);
105
+ }
106
+ /**
107
+ * The session's control methods — `start`, `cancel`, `sendText`,
108
+ * `setMicMuted`, `resetState`, `reset`, `restart`, `resume`, `disconnect`,
109
+ * `toggle`, `end` — with **no snapshot subscription**.
110
+ * Push-to-talk is not among them: that is `usePushToTalk`.
111
+ *
112
+ * This is the narrow half of {@link useSession}, and it is the half a custom
113
+ * chrome could not reach. `<Controls>` and `<StartScreen>` in this package pair
114
+ * a one-field `useSessionSelector` with this package's own `useSessionCore`
115
+ * (`context.ts`, unpublished); a `client.tsx`
116
+ * could not, because that hook is not published — so a footer needing `start`
117
+ * and `toggle` held a WHOLE-SNAPSHOT `useSession()`, and `session/browser-session.ts`
118
+ * rebuilds the snapshot object on every change. Measured consequence: four
119
+ * components across three templates re-rendered on every STT partial and every
120
+ * streaming delta, in files whose every other component is narrowly subscribed
121
+ * on purpose. One of them (`text-adventure-agent`'s `TitleScreen`) reads nothing
122
+ * from the snapshot at all and subscribes to all of it for `session.start`.
123
+ *
124
+ * **Why publishing this does not reopen what `/internal` closed.**
125
+ * `useSessionCore` hands back the STORE — `subscribe`, `getSnapshot`,
126
+ * `connect`, `Symbol.dispose` — which is the framework's own plumbing, the same
127
+ * category as the providers and `buildAgentUrl` that live on
128
+ * `@alexkroman1/aai-ui/internal`. A client that holds it can subscribe out of
129
+ * band of React, dial a socket the mount did not, and dispose the session under
130
+ * the tree that is rendering it. What comes back from here is the SAME
131
+ * methods `useSession()` already publishes on its result, built into a fresh
132
+ * object rather than passed through, so the store is not reachable from it.
133
+ * There is no new capability here — only the existing one without the
134
+ * subscription tax.
135
+ *
136
+ * Identity-stable per core, so it is safe in a dependency array and in a
137
+ * `memo()` child's props: the methods are closures created once by
138
+ * `createBrowserSession`, and the object wrapping them is memoized on the core.
139
+ *
140
+ * Throws outside the provider `mountClient()` installs, like every session hook.
141
+ *
142
+ * @example A footer that acts on the session without re-rendering with it
143
+ * ```tsx
144
+ * import { useSessionActions, useSessionSelector } from "@alexkroman1/aai-ui";
145
+ *
146
+ * function Footer() {
147
+ * // Two narrow subscriptions and no snapshot read: this row re-renders when
148
+ * // `running` flips, and not on every transcript delta.
149
+ * const running = useSessionSelector((s) => s.running);
150
+ * const { toggle, end } = useSessionActions();
151
+ * return (
152
+ * <>
153
+ * <button onClick={toggle}>{running ? "Pause" : "Resume"}</button>
154
+ * <button onClick={end}>Hang up</button>
155
+ * </>
156
+ * );
157
+ * }
158
+ * ```
159
+ *
160
+ * @returns The control methods — see {@link SessionActions}.
161
+ *
162
+ * @public
163
+ */
164
+ function useSessionActions() {
165
+ const core = useSessionCore();
166
+ return useMemo(() => ({
167
+ start: core.start,
168
+ cancel: core.cancel,
169
+ sendText: core.sendText,
170
+ setMicMuted: core.setMicMuted,
171
+ resetState: core.resetState,
172
+ reset: core.reset,
173
+ restart: core.restart,
174
+ resume: core.resume,
175
+ disconnect: core.disconnect,
176
+ toggle: core.toggle,
177
+ end: core.end
178
+ }), [core]);
179
+ }
180
+ /**
181
+ * Return the live {@link Session}: the current snapshot fields plus the
182
+ * control methods (`start`, `toggle`, `reset`, `resetState`, `disconnect`,
183
+ * `cancel`, `end`).
184
+ *
185
+ * Throws if used outside the provider `mountClient()` installs (the error names
186
+ * `<SessionProvider>` — you only mount that yourself when bypassing
187
+ * `mountClient()`). Re-renders the component on *every* snapshot change; for a
188
+ * component that reads one field, prefer {@link useSessionSelector} for a
189
+ * targeted subscription.
190
+ *
191
+ * @example
192
+ * ```tsx
193
+ * import { useSession } from "@alexkroman1/aai-ui";
194
+ *
195
+ * function Controls() {
196
+ * const session = useSession();
197
+ * if (!session.started) return <button onClick={session.start}>Start</button>;
198
+ * return <button onClick={session.toggle}>{session.running ? "Pause" : "Resume"}</button>;
199
+ * }
200
+ * ```
201
+ *
202
+ * @public
203
+ */
204
+ function useSession() {
205
+ const core = useSessionCore();
206
+ const snapshot = useSyncExternalStore(core.subscribe, core.getSnapshot);
207
+ const actions = useSessionActions();
208
+ return useMemo(() => ({
209
+ ...snapshot,
210
+ ...actions
211
+ }), [snapshot, actions]);
212
+ }
213
+ /**
214
+ * Subscribe to a narrow slice of the session snapshot.
215
+ *
216
+ * Unlike {@link useSession} — which re-renders the component on *every*
217
+ * snapshot change — this only triggers a re-render when the selected value
218
+ * changes (per `isEqual`, default `Object.is`). Use it for components that
219
+ * read a single field, e.g. `useSessionSelector((s) => s.running)`.
220
+ *
221
+ * The selector must be pure. It may run on every snapshot change, so keep it
222
+ * cheap; when it returns a derived object, pass a custom `isEqual` to avoid
223
+ * re-renders on referentially-new-but-equal results.
224
+ *
225
+ * @example
226
+ * ```tsx
227
+ * import { useSessionSelector } from "@alexkroman1/aai-ui";
228
+ *
229
+ * // Re-renders when `running` flips, and on nothing else — not on every
230
+ * // transcript delta the way `useSession()` would.
231
+ * function MicDot() {
232
+ * const running = useSessionSelector((snapshot) => snapshot.running);
233
+ * return <span>{running ? "●" : "○"}</span>;
234
+ * }
235
+ * ```
236
+ *
237
+ * @param selector - Reads the slice out of the snapshot. Must be pure.
238
+ * @param isEqual - Compares two selected values. Defaults to `Object.is`.
239
+ * @returns The selected slice.
240
+ *
241
+ * @public
242
+ */
243
+ function useSessionSelector(selector, isEqual = Object.is) {
244
+ const core = useSessionCore();
245
+ return useSyncExternalStoreWithSelector(core.subscribe, core.getSnapshot, core.getSnapshot, selector, isEqual);
246
+ }
247
+ /**
248
+ * The two selectors below are MODULE-SCOPE functions, and that is load-bearing
249
+ * rather than tidy.
250
+ *
251
+ * `useSyncExternalStoreWithSelector` caches its selection keyed on the selector
252
+ * it was handed: a fresh arrow per render invalidates that memo every render,
253
+ * so the selector re-runs and the `isEqual` short-circuit protects only the
254
+ * re-RENDER, never the work. Hoisting them means the two fields more than one
255
+ * chrome ever reads are selected by one stable function for the life of the
256
+ * program — which is also the thing a caller writing the arrow inline cannot
257
+ * do for themselves, and the reason these two are hooks at all rather than a
258
+ * documented one-liner.
259
+ */
260
+ const selectState = (s) => s.state;
261
+ const selectError = (s) => s.error;
262
+ /**
263
+ * The agent's live {@link AgentState} — `disconnected`, `connecting`, `ready`,
264
+ * `listening`, `thinking`, `speaking`, `error` — on its own narrow
265
+ * subscription.
266
+ *
267
+ * `useSessionSelector((s) => s.state)` spelled once. It is one of exactly two
268
+ * snapshot fields that more than one custom chrome ever selects (the other is
269
+ * {@link useSessionError}), and it had been written inline at eight sites —
270
+ * including inside this package and, worse, in `ConsoleShell`'s own `@example`,
271
+ * which taught the inline form to everyone who read it.
272
+ *
273
+ * **Named `useSessionStatus`, not `useSessionState`.** `useAgentState` is the
274
+ * SLOT hook — the agent's own synced application state, whatever a
275
+ * `sessionSlot()` projects — and `AgentState` here is the phase of the CALL.
276
+ * Two different concepts one letter apart, so the shorter-sounding name is the
277
+ * one deliberately not taken.
278
+ *
279
+ * Pair it with {@link AGENT_STATE_LABELS} for a rendered word; the raw member
280
+ * is a wire value, not a label.
281
+ *
282
+ * @example
283
+ * ```tsx
284
+ * import { AGENT_STATE_LABELS, useSessionStatus } from "@alexkroman1/aai-ui";
285
+ *
286
+ * function StatusDot() {
287
+ * const status = useSessionStatus();
288
+ * return <span data-state={status}>{AGENT_STATE_LABELS[status] ?? status}</span>;
289
+ * }
290
+ * ```
291
+ *
292
+ * @returns The current agent state.
293
+ *
294
+ * @public
295
+ */
296
+ function useSessionStatus() {
297
+ return useSessionSelector(selectState);
298
+ }
299
+ /**
300
+ * The session's current {@link SessionError}, or `null` when there is none, on
301
+ * its own narrow subscription.
302
+ *
303
+ * The other half of {@link useSessionStatus} — the second of the two fields a
304
+ * custom chrome reads over and over, and the one whose absence is invisible:
305
+ * per the `fatalError` latch in `session/browser-session.ts` the error is the ONLY
306
+ * remaining signal that a session died, since the state beside it goes back to
307
+ * reading like a live one.
308
+ *
309
+ * A chrome rendering it owes `role="alert"` — which is what
310
+ * {@link SessionErrorBanner} is for, and why reaching for that beats reaching
311
+ * for this.
312
+ *
313
+ * @returns The current error, or `null`.
314
+ *
315
+ * @public
316
+ */
317
+ function useSessionError() {
318
+ return useSessionSelector(selectError);
319
+ }
320
+ const ThemeCtx = createContext(DEFAULT_THEME);
321
+ /**
322
+ * Provides the theme the components read via `useTheme`. `mountClient()` installs
323
+ * it automatically (from `ClientConfig.theme`); a custom tree only needs it
324
+ * when bypassing `mountClient()` and mounting React itself.
325
+ *
326
+ * @internal
327
+ */
328
+ function ThemeProvider({ value, children }) {
329
+ const merged = useMemo(() => resolveClientTheme(value), [value]);
330
+ useThemeStyles(merged);
331
+ return createElement(ThemeCtx.Provider, { value: merged }, children);
332
+ }
333
+ /**
334
+ * The custom property each {@link ClientTheme} field is published as.
335
+ *
336
+ * Named after the field, so `theme.surface` and `--aai-surface` are obviously
337
+ * the same thing, and prefixed for the reason `--aai-btn-bg` and
338
+ * `--aai-sidebar-w` already are: these land on `:root`, where an app's own
339
+ * variables live too.
340
+ *
341
+ * @internal
342
+ */
343
+ const THEME_VARS = {
344
+ bg: "--aai-bg",
345
+ surface: "--aai-surface",
346
+ text: "--aai-text",
347
+ border: "--aai-border",
348
+ primary: "--aai-primary"
349
+ };
350
+ /**
351
+ * Every LIVE provider's theme, oldest first, and the page they all wrote over.
352
+ *
353
+ * Page-wide rather than per provider, because the page is: two providers that
354
+ * each snapshot `:root` at mount and restore it at unmount only undo each other
355
+ * in reverse order. Unmounted in MOUNT order (two `mountClient`s disposed as
356
+ * they were created, or Testing Library's `cleanup`, which unmounts in render
357
+ * order), the first restored the page and the second then wrote the first's
358
+ * tokens back — a stale theme left on a page with no client on it. Now the
359
+ * newest live theme paints, and the page is restored only when the last
360
+ * provider goes, whichever order they go in.
361
+ */
362
+ const liveThemes = [];
363
+ let pageBefore = null;
364
+ function readPageStyles() {
365
+ const { body, documentElement: html } = document;
366
+ return {
367
+ vars: Object.values(THEME_VARS).map((prop) => [prop, html.style.getPropertyValue(prop)]),
368
+ htmlBg: html.style.background,
369
+ bodyBg: body.style.background
370
+ };
371
+ }
372
+ function writePageStyles(page) {
373
+ const { body, documentElement: html } = document;
374
+ for (const [prop, value] of page.vars) if (value === "") html.style.removeProperty(prop);
375
+ else html.style.setProperty(prop, value);
376
+ html.style.background = page.htmlBg;
377
+ body.style.background = page.bodyBg;
378
+ }
379
+ function paintTheme(theme) {
380
+ const { body, documentElement: html } = document;
381
+ for (const [field, prop] of Object.entries(THEME_VARS)) html.style.setProperty(prop, theme[field]);
382
+ body.style.background = theme.bg;
383
+ html.style.background = theme.bg;
384
+ }
385
+ /**
386
+ * Publish the theme to CSS, and paint the page with it.
387
+ *
388
+ * Two jobs on one element deliberately — both are about the parts of the page
389
+ * React does not own.
390
+ *
391
+ * **The variables.** {@link useTheme} hands a component a JavaScript object, so
392
+ * everything it styles carries an inline `style={{ }}`; measured across the
393
+ * template tree the ratio of `theme.` reads to inline style objects is
394
+ * essentially one to one. A Tailwind class cannot see a JavaScript object — so
395
+ * the five values are written to `:root` as custom properties, `styles.css`
396
+ * maps them into the `@theme` block's `--color-*` namespace, and
397
+ * `className="bg-aai-surface text-aai-text border-aai-border"` works. This is
398
+ * ADDITIVE: `useTheme()` stays, because the derived tints in
399
+ * `components/_colors.ts` `color-mix` on the resolved values and a page is
400
+ * entitled to read them for a `satisfies`-pinned palette.
401
+ *
402
+ * **The background.** Components paint `theme.bg` on their own containers, but
403
+ * the page behind them keeps whatever the static `<style>` in index.html set.
404
+ * Any viewport wider (or taller) than the app column then shows that color as a
405
+ * border around the UI — which is how a cream theme ended up letterboxed in
406
+ * black. It stays an explicit `background` on `html` AND `body` rather than
407
+ * riding on the new variable: a variable only paints where some rule consumes
408
+ * it, and the two elements this is about are the two nothing in this package
409
+ * renders.
410
+ */
411
+ function useThemeStyles(theme) {
412
+ const { bg, surface, text, border, primary } = theme;
413
+ useEffect(() => {
414
+ if (typeof document === "undefined") return;
415
+ const entry = {
416
+ bg,
417
+ surface,
418
+ text,
419
+ border,
420
+ primary
421
+ };
422
+ if (liveThemes.length === 0) pageBefore = readPageStyles();
423
+ liveThemes.push(entry);
424
+ paintTheme(entry);
425
+ return () => {
426
+ liveThemes.splice(liveThemes.indexOf(entry), 1);
427
+ const newest = liveThemes.at(-1);
428
+ if (newest) paintTheme(newest);
429
+ else if (pageBefore) {
430
+ writePageStyles(pageBefore);
431
+ pageBefore = null;
432
+ }
433
+ };
434
+ }, [
435
+ bg,
436
+ surface,
437
+ text,
438
+ border,
439
+ primary
440
+ ]);
441
+ }
442
+ /**
443
+ * Read the resolved theme (every {@link ClientTheme} field filled with its
444
+ * default) from the nearest theme context. Returns the default theme when no
445
+ * provider is present, so components can call it unconditionally.
446
+ *
447
+ * This is how a custom component stays on the agent's palette: a
448
+ * `mountClient({ theme })` override reaches it here, where a hardcoded colour or a
449
+ * Tailwind class cannot see it.
450
+ *
451
+ * @example
452
+ * ```tsx
453
+ * import { useTheme } from "@alexkroman1/aai-ui";
454
+ *
455
+ * function Total({ amount }: { amount: string }) {
456
+ * const theme = useTheme();
457
+ * return (
458
+ * <strong style={{ color: theme.primary, background: theme.surface }}>
459
+ * {amount}
460
+ * </strong>
461
+ * );
462
+ * }
463
+ * ```
464
+ *
465
+ * @returns Every {@link ClientTheme} field, filled in — a {@link ResolvedClientTheme}.
466
+ *
467
+ * @public
468
+ */
469
+ function useTheme() {
470
+ return useContext(ThemeCtx);
471
+ }
472
+ //#endregion
473
+ export { useSessionActions as a, useSessionSelector as c, useSession as i, useSessionStatus as l, ThemeProvider as n, useSessionCore as o, useOptionalSessionCore as r, useSessionError as s, SessionProvider as t, useTheme as u };