@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
@@ -34,7 +34,7 @@
34
34
  * @module internal
35
35
  */
36
36
  export { buildAgentUrl, loadClientConfig } from "./client-config.ts";
37
- export { ToolConfigContext } from "./components/tool-config-context.ts";
37
+ export { ToolConfigContext, type ToolDisplayConfig } from "./components/tool-config-context.ts";
38
38
  export { ApiUrlChip, SessionUrlChips, UiUrlChip } from "./components/url-chips.tsx";
39
39
  export { SessionProvider, ThemeProvider } from "./context.ts";
40
40
  export { VOICE_CAPTURE_CONSTRAINTS } from "./types.ts";
package/dist/internal.js CHANGED
@@ -1,8 +1,8 @@
1
- import { r as loadClientConfig, t as buildAgentUrl } from "./client-config-CzvWmw4X.js";
2
- import { SessionProvider, ThemeProvider } from "./context.js";
3
- import { n as SessionUrlChips, r as UiUrlChip, t as ApiUrlChip } from "./url-chips-oyy_Eiqh.js";
4
- import { t as TRANSCRIBING_PLACEHOLDER } from "./use-user-transcript-uyHhzy4d.js";
5
- import { t as ToolConfigContext } from "./tool-config-context-Es4YUzV2.js";
6
- import { i as DEFAULT_PROGRESS_POLL_MS, n as MAX_MISSING_READS, t as DEFAULT_WORKFLOW_POLL_MS } from "./use-workflow-run-DdhWmbx-.js";
1
+ import { r as loadClientConfig, t as buildAgentUrl } from "./client-config-DcMCWmkO.js";
2
+ import { n as ThemeProvider, t as SessionProvider } from "./context-BF0KsI9X.js";
3
+ import { n as SessionUrlChips, r as UiUrlChip, t as ApiUrlChip } from "./url-chips-C_SHU6fq.js";
4
+ import { t as TRANSCRIBING_PLACEHOLDER } from "./use-user-transcript-lOu_uMnf.js";
5
+ import { n as ToolConfigContext } from "./tool-config-context-Bd-7xrfP.js";
6
+ import { i as DEFAULT_PROGRESS_POLL_MS, n as MAX_MISSING_READS, t as DEFAULT_WORKFLOW_POLL_MS } from "./use-workflow-run-BTEemod0.js";
7
7
  import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
8
8
  export { ApiUrlChip, DEFAULT_PROGRESS_POLL_MS, DEFAULT_WORKFLOW_POLL_MS, MAX_MISSING_READS, SessionProvider, SessionUrlChips, TRANSCRIBING_PLACEHOLDER, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, buildAgentUrl, loadClientConfig };
@@ -1,7 +1,7 @@
1
- import { useSessionSelector, useTheme } from "./context.js";
1
+ import { c as useSessionSelector, u as useTheme } from "./context-BF0KsI9X.js";
2
2
  import { a as inkTint, o as primaryTint } from "./_colors-CpZO-88A.js";
3
- import { n as useUserTranscript } from "./use-user-transcript-uyHhzy4d.js";
4
- import { n as ToolCallRow, t as ToolCallBlock } from "./tool-call-block-uXtMOUWN.js";
3
+ import { n as useUserTranscript } from "./use-user-transcript-lOu_uMnf.js";
4
+ import { n as ToolCallRow, t as ToolCallBlock } from "./tool-call-block-C8s6AhjS.js";
5
5
  import { memo, useCallback, useMemo } from "react";
6
6
  import clsx from "clsx";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -41,6 +41,12 @@ export type RouteFetchOptions = {
41
41
  client?: string | undefined;
42
42
  /** The agent's base URL. Default: the page's own directory. */
43
43
  baseUrl?: string | undefined;
44
+ /**
45
+ * Extra request headers — an `authorization` for a route behind
46
+ * `apiKeyRoute`, say. Names are case-insensitive, so a `content-type` (or
47
+ * `Content-Type`) here beats the JSON one a body would set.
48
+ */
49
+ headers?: Readonly<Record<string, string>> | undefined;
44
50
  /** Abort the request. */
45
51
  signal?: AbortSignal | undefined;
46
52
  };
@@ -72,7 +78,7 @@ export type RouteFetchOptions = {
72
78
  * @param method - The route's method.
73
79
  * @param path - The route's path as declared, starting with `/`; `?query` allowed.
74
80
  * @param body - Sent as JSON when given.
75
- * @param options - `client`, `baseUrl` and `signal`; see {@link RouteFetchOptions}.
81
+ * @param options - `client`, `baseUrl`, `headers` and `signal`; see {@link RouteFetchOptions}.
76
82
  * @returns The parsed JSON body.
77
83
  *
78
84
  * @public
@@ -1,2 +1,2 @@
1
- import { a as ticketCarriage, i as resolveSessionTokenSync, n as CLEARED_SESSION_STATE, o as loadAudioModules, r as resolveSessionToken, t as createBrowserSession } from "../session-3lgPYNNO.js";
1
+ import { a as ticketCarriage, i as resolveSessionTokenSync, n as CLEARED_SESSION_STATE, o as loadAudioModules, r as resolveSessionToken, t as createBrowserSession } from "../session-Rud8mmU-.js";
2
2
  export { CLEARED_SESSION_STATE, createBrowserSession, loadAudioModules, resolveSessionToken, resolveSessionTokenSync, ticketCarriage };
@@ -1,5 +1,5 @@
1
- import { n as pageBaseUrl } from "./_utils-Mi-SCFA1.js";
2
- import { r as loadClientConfig, t as buildAgentUrl } from "./client-config-CzvWmw4X.js";
1
+ import { n as pageBaseUrl } from "./_utils-BlZBJPdj.js";
2
+ import { r as loadClientConfig, t as buildAgentUrl } from "./client-config-DcMCWmkO.js";
3
3
  import { MIC_SEND_MAX_BUFFERED_BYTES } from "./types.js";
4
4
  import { errorMessage, safeJsonParse } from "@alexkroman1/aai";
5
5
  import { SESSION_AUTH_PROTOCOL_PREFIX, SESSION_PROTOCOL, SessionEventSchema, lenientParse } from "@alexkroman1/aai/protocol";
@@ -1,8 +1,8 @@
1
- import { a as tryParseJSON, i as truncate } from "./_utils-Mi-SCFA1.js";
2
- import { useTheme } from "./context.js";
1
+ import { a as tryParseJSON, i as truncate } from "./_utils-BlZBJPdj.js";
2
+ import { u as useTheme } from "./context-BF0KsI9X.js";
3
3
  import { a as inkTint } from "./_colors-CpZO-88A.js";
4
- import { t as Eyebrow } from "./eyebrow-UfmSz9yy.js";
5
- import { n as useToolConfig } from "./tool-config-context-Es4YUzV2.js";
4
+ import { t as Eyebrow } from "./eyebrow-Bp4y1rZ9.js";
5
+ import { r as useToolConfig } from "./tool-config-context-Bd-7xrfP.js";
6
6
  import { memo, useMemo, useState } from "react";
7
7
  import clsx from "clsx";
8
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -0,0 +1,28 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region src/components/tool-config-context.ts
3
+ /**
4
+ * The value the context holds before the lookup lands (and forever, when the
5
+ * lookup fails): one frozen object, so the provider's value is stable across
6
+ * renders rather than a fresh `{}` per commit.
7
+ *
8
+ * @internal
9
+ */
10
+ const EMPTY_TOOL_DISPLAY = Object.freeze({});
11
+ /**
12
+ * Context for tool display configuration. Installed by `mountClient()` from
13
+ * the agent's `GET /client-config` `tools`; the built-in components read it
14
+ * via `useToolConfig`.
15
+ *
16
+ * @internal
17
+ */
18
+ const ToolConfigContext = createContext(EMPTY_TOOL_DISPLAY);
19
+ /**
20
+ * Read tool display configuration from the nearest `ToolConfigContext.Provider`.
21
+ *
22
+ * @internal
23
+ */
24
+ function useToolConfig() {
25
+ return useContext(ToolConfigContext);
26
+ }
27
+ //#endregion
28
+ export { ToolConfigContext as n, useToolConfig as r, EMPTY_TOOL_DISPLAY as t };
package/dist/types.d.ts CHANGED
@@ -340,12 +340,13 @@ export type WebSocketConstructor = {
340
340
  readonly OPEN: number;
341
341
  };
342
342
  /**
343
- * Theme color overrides for the AAI UI components.
343
+ * The five theme tokens, each optional — the long form of {@link ClientTheme},
344
+ * for a page that names its own ground and surfaces.
344
345
  *
345
346
  * @input
346
347
  * @public
347
348
  */
348
- export type ClientTheme = {
349
+ export type ClientThemeTokens = {
349
350
  /** Background color, also painted on `html`/`body`. Default: `#FAFAFA`. */
350
351
  bg?: string;
351
352
  /** Primary accent color. Default: `#18181B`. */
@@ -357,6 +358,50 @@ export type ClientTheme = {
357
358
  /** Border color. Default: `#E4E4E7`. */
358
359
  border?: string;
359
360
  };
361
+ /**
362
+ * The short form of {@link ClientTheme}: one accent and a light or dark
363
+ * ground, from which `bg`, `surface`, `text` and `border` are DERIVED.
364
+ *
365
+ * Most pages that themed at all set `primary` and nothing else — and the few
366
+ * that wanted a dark page had to look up four neutrals that go together.
367
+ * `{ primary: "#e55", mode: "dark" }` is that lookup done once
368
+ * (`resolveClientTheme`, `_theme.ts`); `mode` defaults to `"light"`, which is
369
+ * the default palette, so `{ primary }` means the same thing in both forms.
370
+ *
371
+ * @input
372
+ * @public
373
+ */
374
+ export type ClientThemePreset = {
375
+ /** Primary accent color. */
376
+ primary: string;
377
+ /** The ground the neutrals are derived for. Default: `"light"`. */
378
+ mode?: "dark" | "light";
379
+ };
380
+ /**
381
+ * Theme color overrides for the AAI UI components: the five tokens
382
+ * ({@link ClientThemeTokens}), or an accent plus a light/dark mode
383
+ * ({@link ClientThemePreset}). Either resolves to a {@link ResolvedClientTheme}.
384
+ *
385
+ * The two forms are EXCLUSIVE: a theme naming `mode` may name no token but
386
+ * `primary`, and a theme naming a token may not name `mode`. Without the
387
+ * guards a mixed `{ primary, mode: "dark", bg }` compiled (an object literal is
388
+ * checked against the union as a whole, so a key from either member passed)
389
+ * and `bg` was silently dropped — the preset's derived neutrals won. The guard
390
+ * is `?: undefined` rather than `?: never` so an options bag that spells a hole
391
+ * as `undefined` still fits. `{ primary }` alone is both forms and resolves
392
+ * alike.
393
+ *
394
+ * @input
395
+ * @public
396
+ */
397
+ export type ClientTheme = (ClientThemeTokens & {
398
+ mode?: undefined;
399
+ }) | (ClientThemePreset & {
400
+ bg?: undefined;
401
+ text?: undefined;
402
+ surface?: undefined;
403
+ border?: undefined;
404
+ });
360
405
  /**
361
406
  * The theme a component reads back from `useTheme()`: every
362
407
  * {@link ClientTheme} field, filled with its default where the client set
@@ -1,5 +1,5 @@
1
- import { n as pageBaseUrl } from "./_utils-Mi-SCFA1.js";
2
- import { useSessionSelector, useTheme } from "./context.js";
1
+ import { n as pageBaseUrl } from "./_utils-BlZBJPdj.js";
2
+ import { c as useSessionSelector, u as useTheme } from "./context-BF0KsI9X.js";
3
3
  import { a as inkTint, i as focusRingStyle, n as FOCUS_RING } from "./_colors-CpZO-88A.js";
4
4
  import { useEffect, useRef, useState } from "react";
5
5
  import clsx from "clsx";
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `useDictation` — speak into a text box: record a clip, have it transcribed,
3
+ * hand the words to the page.
4
+ *
5
+ * Not a session. There is no agent on the other end and no socket: the clip
6
+ * goes to AssemblyAI's dictation API in ONE request, through a `transcribe`
7
+ * callback the page supplies — on the far side of which is a server holding
8
+ * the key, calling `transcribeDictation` from `@alexkroman1/aai/experimental`.
9
+ * The key never reaches the browser, which is why this hook takes a callback
10
+ * rather than a credential.
11
+ *
12
+ * The two ends that are not this hook are published too: `dictationTranscriber`
13
+ * is the `transcribe` for a server answering `handleDictationRequest`'s
14
+ * `{ text } | { error }`, and `appendDictation` adds the words to a draft.
15
+ *
16
+ * What it owns, because each is a way a hand-written mic button goes wrong:
17
+ *
18
+ * - **The recording indicator goes out.** Every way out — stop, cancel, the
19
+ * length cap, an unmount mid-recording — releases the microphone.
20
+ * - **A late answer lands nowhere.** A transcription that settles after a
21
+ * cancel, a newer recording or an unmount is dropped, never typed into the
22
+ * box the user has moved on from.
23
+ * - **The button cannot be pressed into a bad state.** `buttonProps` keeps a
24
+ * recording stoppable while `disabled`, and takes no press mid-transition.
25
+ * - **The clip fits the endpoint.** Recording stops by itself at `maxSeconds`
26
+ * and is transcribed, rather than growing into a clip the service refuses
27
+ * whole.
28
+ */
29
+ import type { DictationRecorder } from "./audio/index.ts";
30
+ /**
31
+ * Options for {@link useDictation}.
32
+ *
33
+ * @input
34
+ */
35
+ export type UseDictationOptions = {
36
+ /**
37
+ * Turn one clip — a mono 16-bit WAV — into text. Called once per recording,
38
+ * with a signal that aborts when the result would no longer be used. Usually
39
+ * a `POST` to the page's own server, which forwards the clip to the
40
+ * dictation API under its key. A rejection is shown as `error`.
41
+ */
42
+ transcribe: (audio: Blob, options: {
43
+ signal: AbortSignal;
44
+ }) => Promise<string>;
45
+ /**
46
+ * The words, once a recording is transcribed. Not called for an empty
47
+ * transcript (the user said nothing) or for a recording that was cancelled.
48
+ */
49
+ onText: (text: string) => void;
50
+ /**
51
+ * The longest recording, in seconds: it stops and is transcribed by itself
52
+ * then. Defaults to the dictation API's own ceiling, 120.
53
+ */
54
+ maxSeconds?: number;
55
+ /**
56
+ * Refuse to START a recording — the text box is not accepting input right
57
+ * now. A recording already running can still be stopped and transcribed, so
58
+ * the words said before the box went dead are not lost. Default `false`.
59
+ */
60
+ disabled?: boolean;
61
+ };
62
+ /**
63
+ * What {@link useDictation} returns.
64
+ *
65
+ * @sealed
66
+ * @public
67
+ */
68
+ export type UseDictationResult = {
69
+ /**
70
+ * Where the hook is: `"idle"`, `"starting"` (asking for the microphone),
71
+ * `"recording"` or `"transcribing"`.
72
+ */
73
+ phase: "idle" | "starting" | "recording" | "transcribing" | (string & {});
74
+ /** Why the last recording produced no text, or `null`. Cleared by `start`. */
75
+ error: string | null;
76
+ /**
77
+ * Whether this browser can record at all — a microphone API, an
78
+ * `AudioContext` and audio worklets. A button should not render without it.
79
+ */
80
+ supported: boolean;
81
+ /** Ask for the microphone and start recording. Ignored unless idle, and while `disabled`. */
82
+ start: () => void;
83
+ /** Stop recording and transcribe what was said. Ignored unless recording. */
84
+ stop: () => void;
85
+ /** Throw the recording (or the transcription in flight) away. */
86
+ cancel: () => void;
87
+ /** `start` when idle, `stop` when recording — one button. */
88
+ toggle: () => void;
89
+ /**
90
+ * Spread onto a `<button>`: `toggle`, and the two rules a hand-written one
91
+ * gets wrong — a recording can ALWAYS be stopped (even with `disabled` set),
92
+ * and nothing can be pressed while the microphone opens or the clip is
93
+ * transcribing. Label and style it however you like.
94
+ */
95
+ buttonProps: {
96
+ onClick: () => void;
97
+ disabled: boolean;
98
+ "aria-pressed": boolean;
99
+ };
100
+ };
101
+ /** How a recorder is opened; the real one loads the audio chunk on first use. */
102
+ export type OpenDictationRecorder = (opts: {
103
+ sampleRate: number;
104
+ maxSeconds: number;
105
+ }) => Promise<DictationRecorder>;
106
+ /**
107
+ * Record what the user says and hand it back as text — a microphone button
108
+ * for a text box.
109
+ *
110
+ * @example
111
+ * ```tsx
112
+ * import { appendDictation, dictationTranscriber, useDictation } from "@alexkroman1/aai-ui";
113
+ * import { useState } from "react";
114
+ *
115
+ * // `POST /dictation` on your own server is `handleDictationRequest`
116
+ * // (`@alexkroman1/aai/experimental`), which holds the API key.
117
+ * const transcribe = dictationTranscriber("/dictation");
118
+ *
119
+ * export function SpokenNote() {
120
+ * const [draft, setDraft] = useState("");
121
+ * const dictation = useDictation({
122
+ * transcribe,
123
+ * onText: (text) => setDraft((d) => appendDictation(d, text)),
124
+ * });
125
+ * return (
126
+ * <div>
127
+ * <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />
128
+ * {dictation.supported && (
129
+ * <button type="button" {...dictation.buttonProps}>
130
+ * {dictation.phase === "recording" ? "Stop" : "Speak"}
131
+ * </button>
132
+ * )}
133
+ * </div>
134
+ * );
135
+ * }
136
+ * ```
137
+ *
138
+ * @public
139
+ */
140
+ export declare function useDictation(options: UseDictationOptions): UseDictationResult;
141
+ /**
142
+ * {@link useDictation} over an injected recorder — the seam the specs drive.
143
+ * Not on the barrel.
144
+ */
145
+ export declare function useDictationWith(options: UseDictationOptions, open: OpenDictationRecorder): UseDictationResult;
@@ -7,6 +7,14 @@
7
7
  * dropped — `useWorkflowRuns`' rule), a read landing after unmount, and a
8
8
  * failure rendered as an empty answer. `data` is KEPT through a failed re-read,
9
9
  * so a poll that hiccups does not blank the panel; `error` says it is stale.
10
+ * It is NOT kept across a change of `path`: the answer is remembered with the
11
+ * path it came from, so a page switching from one record to the next never
12
+ * shows the previous one under the new id while the first read is in flight.
13
+ *
14
+ * The poll is `repeatUntil`'s loop (`_repeat-until.ts`), re-armed from the
15
+ * SETTLED read rather than on an interval — a slow route would otherwise
16
+ * stack overlapping reads — and stopped by `until`, by a change of what is
17
+ * read, and by unmount, with the in-flight request aborted on the same signal.
10
18
  *
11
19
  * @module
12
20
  */
@@ -16,15 +24,31 @@
16
24
  * @input
17
25
  * @public
18
26
  */
19
- export type UseRouteOptions = {
20
- /** Read again every this many ms, while mounted. Default: never. */
27
+ export type UseRouteOptions<T = unknown> = {
28
+ /**
29
+ * Read again every this many ms, while mounted. `0` or unset: never. A
30
+ * negative or `NaN` value throws rather than polling as fast as the route
31
+ * answers.
32
+ */
21
33
  pollMs?: number | undefined;
34
+ /**
35
+ * Stop polling once an answer satisfies this — a call that is over, a job
36
+ * that has settled. Judged on every read, the first included; `reload()`
37
+ * still works afterwards. Read per request, so an inline function is fine.
38
+ */
39
+ until?: ((value: T) => boolean) | undefined;
22
40
  /**
23
41
  * The `?client=` to send. Default: the session's client
24
42
  * (`session.identity.clientId()`, read per request) inside `mountClient()`,
25
43
  * none on a page with no session.
26
44
  */
27
45
  client?: string | undefined;
46
+ /**
47
+ * Extra request headers, as `routeFetch`'s — an `authorization` for a route
48
+ * behind `apiKeyRoute`. Read per request, so an inline object does not
49
+ * re-trigger the read.
50
+ */
51
+ headers?: Readonly<Record<string, string>> | undefined;
28
52
  };
29
53
  /**
30
54
  * What {@link useRoute} returns.
@@ -33,9 +57,12 @@ export type UseRouteOptions = {
33
57
  * @public
34
58
  */
35
59
  export type UseRouteResult<T> = {
36
- /** The last answer, kept through a failed re-read. `undefined` until the first. */
60
+ /**
61
+ * The last answer for THIS `path`, kept through a failed re-read.
62
+ * `undefined` until the first, and again whenever `path` changes.
63
+ */
37
64
  data: T | undefined;
38
- /** Why the LAST read failed — the route's `{ error }` when it said — or `undefined`. */
65
+ /** Why the LAST read of this `path` failed — the route's `{ error }` when it said — or `undefined`. */
39
66
  error: string | undefined;
40
67
  /** Whether a read is in flight. */
41
68
  loading: boolean;
@@ -44,7 +71,7 @@ export type UseRouteResult<T> = {
44
71
  };
45
72
  /**
46
73
  * `GET` one of the agent's own routes, on mount, on `reload()` and every
47
- * `pollMs` — see this module's doc.
74
+ * `pollMs` until `until` says so — see this module's doc.
48
75
  *
49
76
  * @example A list that re-reads after each delete
50
77
  * ```tsx
@@ -74,11 +101,26 @@ export type UseRouteResult<T> = {
74
101
  * }
75
102
  * ```
76
103
  *
104
+ * @example A call followed until it is over
105
+ * ```tsx
106
+ * import { useRoute } from "@alexkroman1/aai-ui";
107
+ *
108
+ * type Call = { status: string; transcript: { role: string; text: string }[] };
109
+ *
110
+ * function LiveCall({ id }: { id: string }) {
111
+ * const { data } = useRoute<Call>(`/calls/${id}`, {
112
+ * pollMs: 2000,
113
+ * until: (call) => call.status === "completed",
114
+ * });
115
+ * return <ol>{data?.transcript.map((line, i) => <li key={String(i)}>{line.text}</li>)}</ol>;
116
+ * }
117
+ * ```
118
+ *
77
119
  * @typeParam T - The shape the route answers. Not checked.
78
120
  * @param path - The route's path as declared, without `/api`. `null` reads nothing.
79
- * @param options - `pollMs` and `client`; see {@link UseRouteOptions}.
121
+ * @param options - `pollMs`, `until`, `client` and `headers`; see {@link UseRouteOptions}.
80
122
  * @returns The answer, the last error, and `reload`; see {@link UseRouteResult}.
81
123
  *
82
124
  * @public
83
125
  */
84
- export declare function useRoute<T = unknown>(path: string | null, options?: UseRouteOptions): UseRouteResult<T>;
126
+ export declare function useRoute<T = unknown>(path: string | null, options?: UseRouteOptions<T>): UseRouteResult<T>;
@@ -87,6 +87,7 @@
87
87
  * the behaviour it would have had anyway — one run per load — rather than
88
88
  * failing to render.
89
89
  */
90
+ import { type StorageKind } from "./_web-storage.ts";
90
91
  /**
91
92
  * The key `useWorkflowSubmit` uses when the page named none.
92
93
  *
@@ -107,10 +108,14 @@
107
108
  *
108
109
  * @param explicit - The caller's own key, or undefined for a page with none.
109
110
  * @returns The key to record runs under and look them up by.
111
+ * @throws When the caller's key is empty or whitespace — see
112
+ * {@link assertUsableRunKey}.
110
113
  *
111
114
  * @internal
112
115
  */
113
- export declare function useDefaultRunKey(explicit: string | undefined): string;
116
+ export declare function useDefaultRunKey(explicit: string | {
117
+ readonly storage?: StorageKind | undefined;
118
+ } | undefined): string;
114
119
  /**
115
120
  * A lookup key for `useWorkflowSubmit({ key })`, stable across reloads.
116
121
  *
@@ -1,4 +1,4 @@
1
- import { useSessionSelector } from "./context.js";
1
+ import { c as useSessionSelector } from "./context-BF0KsI9X.js";
2
2
  import { useMemo } from "react";
3
3
  //#region src/use-user-transcript.ts
4
4
  /**