@alexkroman1/aai-ui 0.16.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 (123) 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-labels.d.ts +8 -3
  9. package/dist/agent-state.d.ts +4 -2
  10. package/dist/append-dictation.d.ts +23 -0
  11. package/dist/audio/dictation.d.ts +36 -0
  12. package/dist/audio/index.d.ts +4 -2
  13. package/dist/audio/index.js +44 -1
  14. package/dist/{chat-view-DIwPoAEG.js → chat-view-BwKpF0MN.js} +65 -45
  15. package/dist/{client-config-C7fQrioM.js → client-config-DcMCWmkO.js} +8 -13
  16. package/dist/client-config.d.ts +29 -10
  17. package/dist/components/_form-readiness.d.ts +2 -1
  18. package/dist/components/audio-result.d.ts +2 -0
  19. package/dist/components/bullet-list.d.ts +1 -0
  20. package/dist/components/button.d.ts +2 -2
  21. package/dist/components/button.js +1 -1
  22. package/dist/components/chat-view.js +1 -2
  23. package/dist/components/console-shell.d.ts +8 -5
  24. package/dist/components/controls.d.ts +1 -0
  25. package/dist/components/controls.js +2 -2
  26. package/dist/components/conversation-view.d.ts +1 -0
  27. package/dist/components/facts.d.ts +1 -0
  28. package/dist/components/form-fields.d.ts +31 -12
  29. package/dist/components/form-types.d.ts +48 -2
  30. package/dist/components/form.d.ts +10 -5
  31. package/dist/components/markdown.d.ts +1 -0
  32. package/dist/components/message-list.d.ts +1 -0
  33. package/dist/components/message-list.js +1 -2
  34. package/dist/components/session-controls.d.ts +7 -2
  35. package/dist/components/session-error-banner.d.ts +1 -0
  36. package/dist/components/session-state-dot.d.ts +45 -20
  37. package/dist/components/sidebar-layout.d.ts +2 -2
  38. package/dist/components/sidebar-layout.js +1 -1
  39. package/dist/components/start-screen.js +2 -2
  40. package/dist/components/tool-call-block.d.ts +3 -3
  41. package/dist/components/tool-call-block.js +1 -2
  42. package/dist/components/tool-call-row.d.ts +1 -0
  43. package/dist/components/tool-config-context.d.ts +23 -5
  44. package/dist/components/url-chips.d.ts +4 -3
  45. package/dist/components/workflow-fields.d.ts +36 -11
  46. package/dist/components/workflow-pending-note.d.ts +31 -12
  47. package/dist/components/workflow-run-error.d.ts +1 -0
  48. package/dist/components/workflow-run-panel.d.ts +1 -0
  49. package/dist/context-BF0KsI9X.js +473 -0
  50. package/dist/context.d.ts +8 -7
  51. package/dist/context.js +1 -381
  52. package/dist/default-client/assets/audio-CtBvSS3L.js +1 -0
  53. package/dist/default-client/assets/{capture-processor-CXCWxCO8.js → capture-processor-BZXisGjZ.js} +1 -1
  54. package/dist/default-client/assets/index-BkjCv4MX.css +2 -0
  55. package/dist/default-client/assets/index-DUQoaxdq.js +127 -0
  56. package/dist/default-client/assets/{playback-processor-DXHfyQD9.js → playback-processor-BHk-iXEU.js} +1 -1
  57. package/dist/default-client/index.html +2 -2
  58. package/dist/default-client.d.ts +1 -3
  59. package/dist/define-client.d.ts +32 -28
  60. package/dist/define-client.js +85 -44
  61. package/dist/dictation-transcriber.d.ts +49 -0
  62. package/dist/{eyebrow-UfmSz9yy.js → eyebrow-Bp4y1rZ9.js} +1 -1
  63. package/dist/{hooks-DN4gHYsj.js → hooks-1CoQY_Fc.js} +2 -2
  64. package/dist/hooks.js +1 -2
  65. package/dist/inbox-protocol.d.ts +14 -4
  66. package/dist/inbox.d.ts +1 -0
  67. package/dist/index.d.ts +12 -9
  68. package/dist/index.js +905 -277
  69. package/dist/internal.d.ts +1 -1
  70. package/dist/internal.js +6 -6
  71. package/dist/linked-client.d.ts +2 -0
  72. package/dist/{message-list-DHZcLhRA.js → message-list-BBHLK05X.js} +24 -13
  73. package/dist/page.d.ts +2 -0
  74. package/dist/phone.d.ts +1 -0
  75. package/dist/route-fetch.d.ts +8 -1
  76. package/dist/session/audio-setup.d.ts +1 -1
  77. package/dist/session/audio-state.d.ts +6 -6
  78. package/dist/session/index.js +1 -1
  79. package/dist/session/messages.d.ts +1 -9
  80. package/dist/session/preconnect.d.ts +1 -1
  81. package/dist/session/types.d.ts +4 -0
  82. package/dist/{session-CYdjRvbl.js → session-Rud8mmU-.js} +26 -15
  83. package/dist/stored-value.d.ts +2 -0
  84. package/dist/{tool-call-block-uXtMOUWN.js → tool-call-block-C8s6AhjS.js} +4 -4
  85. package/dist/tool-config-context-Bd-7xrfP.js +28 -0
  86. package/dist/types.d.ts +95 -6
  87. package/dist/{url-chips-oyy_Eiqh.js → url-chips-C_SHU6fq.js} +2 -2
  88. package/dist/use-client-runs.d.ts +2 -0
  89. package/dist/use-conversation-log.d.ts +2 -0
  90. package/dist/use-conversation.d.ts +24 -9
  91. package/dist/use-copy.d.ts +1 -0
  92. package/dist/use-dictation.d.ts +145 -0
  93. package/dist/use-download-url.d.ts +6 -1
  94. package/dist/use-flash.d.ts +1 -0
  95. package/dist/use-inbox.d.ts +2 -0
  96. package/dist/use-push-to-talk.d.ts +6 -1
  97. package/dist/use-route-mutation.d.ts +3 -0
  98. package/dist/use-route.d.ts +51 -7
  99. package/dist/use-run-key.d.ts +6 -1
  100. package/dist/use-session-controls.d.ts +1 -0
  101. package/dist/use-tap-to-talk.d.ts +5 -2
  102. package/dist/{use-user-transcript-uyHhzy4d.js → use-user-transcript-lOu_uMnf.js} +1 -1
  103. package/dist/use-user-transcript.d.ts +5 -1
  104. package/dist/use-workflow-form.d.ts +76 -167
  105. package/dist/use-workflow-progress.d.ts +5 -0
  106. package/dist/{use-workflow-run-DdhWmbx-.js → use-workflow-run-BTEemod0.js} +2 -2
  107. package/dist/use-workflow-run.d.ts +5 -0
  108. package/dist/use-workflow-runs.d.ts +18 -4
  109. package/dist/use-workflow-stream.d.ts +33 -7
  110. package/dist/use-workflows.d.ts +10 -2
  111. package/dist/workflow-client.d.ts +5 -0
  112. package/dist/workflow-def-types.d.ts +18 -2
  113. package/dist/workflow-field-kind.d.ts +66 -10
  114. package/dist/workflow-submission.d.ts +265 -0
  115. package/dist/worklets/capture-processor.d.ts +3 -3
  116. package/dist/worklets/capture-processor.js +2 -2
  117. package/dist/worklets/playback-processor.d.ts +3 -3
  118. package/dist/worklets/playback-processor.js +2 -2
  119. package/package.json +2 -2
  120. package/dist/default-client/assets/audio-DzwGxdv4.js +0 -1
  121. package/dist/default-client/assets/index-BOTLwsNO.js +0 -126
  122. package/dist/default-client/assets/index-_-x5JLLa.css +0 -2
  123. package/dist/tool-config-context-Es4YUzV2.js +0 -19
package/README.md CHANGED
@@ -28,21 +28,20 @@ still React, still the same theme tokens.
28
28
 
29
29
  ### Agent `agent.ts`
30
30
 
31
- The session's state lives in a slot, and `syncState` is what the browser sees —
32
- pushed after every tool call, so the client never rebuilds it from events:
31
+ The session's state lives in a slot, and a slot declared `sync: true` is what
32
+ the browser sees — pushed after every tool call, so the client never rebuilds it
33
+ from events:
33
34
 
34
35
  ```ts
35
36
  import { agent, sessionSlot } from "@alexkroman1/aai";
36
37
 
37
38
  const desk = sessionSlot("desk", () => ({ symbols: [] as string[] }), {
38
39
  view: (state) => ({ symbols: state.symbols }),
40
+ sync: true,
39
41
  });
40
42
 
41
- export default agent({
42
- name: "Market Desk",
43
- systemPrompt: "You look up quotes and keep the caller's watchlist.",
44
- syncState: desk.projected,
45
- });
43
+ // The `sync: true` slot is collected; the prompt is the system-prompt.md beside this file.
44
+ export default agent({ name: "Market Desk" });
46
45
  ```
47
46
 
48
47
  ### Tool `tools/get_quote.ts`
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The press-to-settle stopwatch both submit hooks report as
3
+ * `WorkflowSubmission.timing`: the mark `submit()` was called at, and the mark
4
+ * the submission settled at, read off the one flag that spans both waits.
5
+ *
6
+ * It is the half of `transcription-workflow`'s `useTotalLatency` that is a
7
+ * fact about the SUBMISSION rather than about the page: that template kept
8
+ * `startedAt`/`frozenMs` in its own state, armed by its own submit handler
9
+ * and cleared by its own Clear button — shadow state for two instants only
10
+ * the hook can place exactly. The rendering half (the interval that re-draws
11
+ * a running clock, the "before the run / inside it" split) stays on the page,
12
+ * because it is the page that decides how to print a duration.
13
+ *
14
+ * Two details it would be easy to get wrong, both kept from the template:
15
+ *
16
+ * - **The settle is measured in the effect that sees `pending` fall**, so the
17
+ * mark is the moment the run settled rather than whenever the page next
18
+ * drew. And only after `pending` has been seen TRUE since the press — a
19
+ * press that lands one render before the submission reports itself in
20
+ * flight would otherwise freeze the clock at zero.
21
+ * - **`performance.now()`, not `Date.now()`.** Monotonic; see
22
+ * `WorkflowSubmissionTiming`.
23
+ */
24
+ import type { WorkflowSubmissionTiming } from "./workflow-submission.ts";
25
+ /** What the stopwatch hands a submit hook. */
26
+ export type SubmissionTiming = {
27
+ /** The marks, for `WorkflowSubmission.timing`. A new object only when a mark changes. */
28
+ timing: WorkflowSubmissionTiming;
29
+ /** Call from `submit()`: sets `submittedAt`, clears `settledAt`. */
30
+ markSubmitted: () => void;
31
+ /** Call from `reset()`: drops both marks. */
32
+ clear: () => void;
33
+ };
34
+ /**
35
+ * The stopwatch over `pending` — see the module doc.
36
+ *
37
+ * @param pending - The submission's own `pending`: true from the press until
38
+ * the run settles, which is the whole of what the clock covers.
39
+ */
40
+ export declare function useSubmissionTiming(pending: boolean): SubmissionTiming;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Everything a submit hook is, bar its BODY.
3
+ *
4
+ * `useWorkflowSubmit` (`use-workflow-form.ts`) and `useWorkflowStream`
5
+ * (`use-workflow-stream.ts`) differ in one thing — when the run is created
6
+ * relative to the bytes — and that one thing is the submission body each hands
7
+ * to the form's statechart. Everything after the body was written out twice,
8
+ * identically: the watch over the run id (`useWorkflowRun`), the two controls
9
+ * bound to it (`_run-controls.ts`), `pending` as "the machine is busy or the
10
+ * watch is still polling", the press-to-settle stopwatch
11
+ * (`_submission-timing.ts`), a `reset` that clears both, and the fourteen-field
12
+ * `WorkflowSubmission` assembled from all of it. Two copies of a return object
13
+ * that have to agree field for field are two copies that can stop agreeing —
14
+ * the stream hook's type is an ALIAS of the form hook's for the same reason —
15
+ * so the tail is one function, and each hook keeps only what makes it itself.
16
+ */
17
+ import type { RecoverRunOptions } from "./_recover-run.ts";
18
+ import type { SubmissionWork } from "./_workflow-form-state.ts";
19
+ import type { UploadGate } from "./upload/index.ts";
20
+ import type { WorkflowApi } from "./workflow-client.ts";
21
+ import type { WorkflowSubmission, WorkflowSubmissionScope } from "./workflow-submission.ts";
22
+ /**
23
+ * What one `submit(input)` does, from the hook that owns it: the gate the
24
+ * bytes pause through, and the work that runs as THE submission (see
25
+ * `SubmissionActions.submit`). Called per press with the client read at that
26
+ * moment, after the stopwatch has marked it.
27
+ *
28
+ * It is a `useCallback` dependency of `submit`, so a hook hands over a stable
29
+ * one — rebuilt only when what it closes over changes — or `submit`, and with
30
+ * it the `onSubmit` a page gives `<Form>`, is rebuilt every render.
31
+ */
32
+ export type BeginSubmission = (input: unknown, client: WorkflowApi) => {
33
+ gate: UploadGate;
34
+ work: SubmissionWork;
35
+ };
36
+ /** What {@link useSubmission} needs from the hook. */
37
+ export type SubmissionOptions = {
38
+ /** The stable getter from `useWorkflowApiRef` — the hook's, since the convention keys on the hook file. */
39
+ getClient: () => WorkflowApi;
40
+ /** The caller's client, for the watch; `undefined` is the page's own agent. */
41
+ api: WorkflowApi | undefined;
42
+ /** How often the fallback poll re-reads a live run; `undefined` is the default. */
43
+ intervalMs: number | undefined;
44
+ /** The mount-time lookup, or nothing for a hook that refuses it (`useWorkflowStream`). */
45
+ recover?: RecoverRunOptions;
46
+ /** Who finds the run again — reported back as `WorkflowSubmission.scope`. */
47
+ scope: WorkflowSubmissionScope;
48
+ /** The hook's own half — see {@link BeginSubmission}. */
49
+ begin: BeginSubmission;
50
+ };
51
+ /**
52
+ * The shared tail of a submit hook: the statechart, the watch, the controls,
53
+ * the stopwatch, and the `WorkflowSubmission` built from them. See the module
54
+ * doc.
55
+ */
56
+ export declare function useSubmission(options: SubmissionOptions): WorkflowSubmission<unknown, never>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * One `ClientTheme` — either form — to the five resolved tokens.
3
+ *
4
+ * Its own module rather than a branch in `context.ts` (which is at the cap)
5
+ * because the derivation is a decision worth a test of its own: the dark
6
+ * neutrals are chosen to keep the components' derived tints (`_colors.ts`'s
7
+ * `inkTint`, which walks from `text` toward the ground) above WCAG AA on a
8
+ * dark surface, exactly as the light defaults do on a light one.
9
+ */
10
+ import type { ClientTheme, ResolvedClientTheme } from "./types.ts";
11
+ export declare const DEFAULT_THEME: ResolvedClientTheme;
12
+ /**
13
+ * Every token filled in: the preset's neutrals under its accent, or the given
14
+ * tokens over the defaults. `undefined` is the default theme itself. A token
15
+ * given as `""` is ignored with a warning ({@link withoutBlankTokens}).
16
+ */
17
+ export declare function resolveClientTheme(theme: ClientTheme | undefined): ResolvedClientTheme;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The two forms of `ClientTheme` are EXCLUSIVE at the type level.
3
+ *
4
+ * A mixed `{ primary, mode: "dark", bg }` used to compile: an object literal is
5
+ * checked against the union as a whole, so a key from either member passed
6
+ * excess-property checking, and at runtime `isPreset` won and `bg` was dropped
7
+ * without a word. The `?: undefined` guards on each member are what make the
8
+ * mixed literal a compile error, and this file is what keeps them there.
9
+ *
10
+ * Probed with a conditional rather than a suppression directive: an assignability
11
+ * question answered as `true`/`false` names the shape that moved, where a
12
+ * suppressed error passes on ANY error.
13
+ */
14
+ export {};
@@ -21,11 +21,10 @@ function pageBaseUrl() {
21
21
  /**
22
22
  * Set the document title, when there is one to set and a document to set it on.
23
23
  *
24
- * Both mounts do this and neither may clobber a title the page's own HTML
25
- * declared — `mountClient()`'s custom-component tier because there is no shell header
26
- * to show the name in, `mountPage()` because there is no shell at all. One copy, for
27
- * the reason `resolveContainer` and `mountRoot` are shared: the rule is the
28
- * same, so the two mounts must not be able to disagree about it.
24
+ * `mountPage()` does this for its `name`, and may not clobber a title the
25
+ * page's own HTML declared — there is no shell to show the name in otherwise.
26
+ * `mountClient()` sets none: the agent's name reaches its default shell from
27
+ * the agent's own `client-config`, and a custom component owns the page.
29
28
  */
30
29
  function setPageTitle(name) {
31
30
  if (name && typeof document !== "undefined") document.title = name;
package/dist/_utils.d.ts CHANGED
@@ -11,11 +11,10 @@ export declare function pageBaseUrl(): string;
11
11
  /**
12
12
  * Set the document title, when there is one to set and a document to set it on.
13
13
  *
14
- * Both mounts do this and neither may clobber a title the page's own HTML
15
- * declared — `mountClient()`'s custom-component tier because there is no shell header
16
- * to show the name in, `mountPage()` because there is no shell at all. One copy, for
17
- * the reason `resolveContainer` and `mountRoot` are shared: the rule is the
18
- * same, so the two mounts must not be able to disagree about it.
14
+ * `mountPage()` does this for its `name`, and may not clobber a title the
15
+ * page's own HTML declared — there is no shell to show the name in otherwise.
16
+ * `mountClient()` sets none: the agent's name reaches its default shell from
17
+ * the agent's own `client-config`, and a custom component owns the page.
19
18
  */
20
19
  export declare function setPageTitle(name: string | undefined): void;
21
20
  /**
@@ -23,7 +23,7 @@
23
23
  * chrome exists to look like — a CRT's glow, a dispatch board's alert colours —
24
24
  * and a component would take the one part that is genuinely shared (the words)
25
25
  * hostage to the part that is not. It takes the palette as a PROP, so it does
26
- * not: what it shares is the exhaustive colour lookup, this record as the label
26
+ * not: what it shares is the colour lookup, this record as the label
27
27
  * fallback, and the pulse rule, and the three chromes that had each written
28
28
  * those fourteen lines around their own `satisfies Record<AgentState, string>`
29
29
  * keep the record and lose the lines.
@@ -32,6 +32,8 @@ import type { AgentState } from "./types.ts";
32
32
  /**
33
33
  * The default label per {@link AgentState}.
34
34
  *
35
+ * {@link AgentState} is open, so a lookup by a live state is `string |
36
+ * undefined`: fall back to the raw state for one this version has no word for.
35
37
  * Override the ones your page has a better word for and keep the rest:
36
38
  *
37
39
  * ```ts
@@ -39,8 +41,11 @@ import type { AgentState } from "./types.ts";
39
41
  * import { AGENT_STATE_LABELS } from "@alexkroman1/aai-ui";
40
42
  *
41
43
  * // A dispatch board that shouts, and renames one state.
42
- * const STATE_LABEL = { ...AGENT_STATE_LABELS, thinking: "Processing" };
43
- * const shout = (s: AgentState) => STATE_LABEL[s].toUpperCase();
44
+ * const STATE_LABEL: Readonly<Record<AgentState, string>> = {
45
+ * ...AGENT_STATE_LABELS,
46
+ * thinking: "Processing",
47
+ * };
48
+ * const shout = (s: AgentState) => (STATE_LABEL[s] ?? s).toUpperCase();
44
49
  * ```
45
50
  *
46
51
  * **Sentence case, deliberately.** A template that wants caps applies its own
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Reading what the agent projects — `agent({ syncState })` — in the browser.
2
+ * Reading what the agent projects — the slots declared `sync: true`, collected
3
+ * into its `syncState` — in the browser.
3
4
  *
4
5
  * The frame is KEYED by slot name (`{ [slot]: view }`), so every reader here
5
6
  * selects ONE slot by the name the agent keyed it under. That is what lets a
@@ -52,9 +53,10 @@ export declare function useAgentState(): AgentStateFrame | null;
52
53
  * import { useAgentState } from "@alexkroman1/aai-ui";
53
54
  *
54
55
  * // In a real project the slot is declared beside the agent and imported here;
55
- * // the agent declares `syncState: cartSlot.projected`.
56
+ * // `sync: true` is what puts it in the agent's `syncState`.
56
57
  * const cartSlot = sessionSlot("cart", () => ({ items: [] as string[] }), {
57
58
  * view: (cart) => ({ count: cart.items.length }),
59
+ * sync: true,
58
60
  * });
59
61
  *
60
62
  * function CartBadge() {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `appendDictation(draft, text)` — where dictated words land in a text box
3
+ * that may already hold some.
4
+ */
5
+ /**
6
+ * `draft` with `text` added after it, joined by exactly one space: a draft
7
+ * ending in whitespace or a newline does not grow a double space, and an empty
8
+ * or blank draft is replaced rather than given a leading one.
9
+ *
10
+ * Never REPLACES what was typed — the words someone said are an addition to
11
+ * the box, which is what makes speaking into a half-written prompt safe.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { appendDictation } from "@alexkroman1/aai-ui";
16
+ *
17
+ * appendDictation("Build me an agent", "that books tables."); // "Build me an agent that books tables."
18
+ * appendDictation("", "Hello"); // "Hello"
19
+ * ```
20
+ *
21
+ * @public
22
+ */
23
+ export declare function appendDictation(draft: string, text: string): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The dictation recorder: the microphone captured into one clip, handed back
3
+ * as a WAV file for a transcription endpoint to read whole.
4
+ *
5
+ * A clip, not a stream — the dictation API takes the whole utterance in one
6
+ * request — so this is the pre-connect capture (`audio/preconnect.ts`) used
7
+ * for what it already does: buffer the mic at a known rate. Stopping is its
8
+ * `detach` (the worklet's tail flush lands in the buffer, so the last word is
9
+ * kept) and then `close`, which turns the browser's recording indicator off.
10
+ *
11
+ * @module
12
+ */
13
+ /** One recording in progress. Every method is safe to call after another. */
14
+ export type DictationRecorder = {
15
+ /** The rate the clip is recorded at. */
16
+ readonly sampleRate: number;
17
+ /**
18
+ * Stop, release the microphone, and answer the clip as a mono 16-bit WAV.
19
+ * An empty clip (stopped before the first batch) is a 44-byte header.
20
+ */
21
+ stop(): Promise<Uint8Array<ArrayBuffer>>;
22
+ /** Stop and release the microphone, keeping nothing. Never rejects. */
23
+ cancel(): Promise<void>;
24
+ };
25
+ /**
26
+ * Ask for the microphone and start recording.
27
+ *
28
+ * Rejects like the session's own capture does — a denied prompt, a rate the
29
+ * browser refused — with the microphone released.
30
+ */
31
+ export declare function openDictationRecorder(opts: {
32
+ sampleRate: number;
33
+ /** The capture buffer's bound; the caller stops the clip by then. */
34
+ maxSeconds: number;
35
+ captureWorkletSrc: string;
36
+ }): Promise<DictationRecorder>;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The browser audio layer's import surface for the rest of the package: the
3
- * {@link VoiceIO} a live call plays and captures through, and the pre-connect
4
- * capture that buffers the mic before the server names its rates.
3
+ * {@link VoiceIO} a live call plays and captures through, the pre-connect
4
+ * capture that buffers the mic before the server names its rates, and the
5
+ * dictation recorder `useDictation` turns into one clip.
5
6
  *
6
7
  * `capture.ts` (the grant checks and the capture node both halves share) is
7
8
  * private to this directory — guard-invariants rule 37 fails an import from
@@ -14,5 +15,6 @@
14
15
  *
15
16
  * @module
16
17
  */
18
+ export { type DictationRecorder, openDictationRecorder } from "./dictation.ts";
17
19
  export { openPreConnectCapture, type PreConnectCapture } from "./preconnect.ts";
18
20
  export { createVoiceIO, type VoiceIO, type VoiceIOOptions } from "./voice-io.ts";
@@ -1,4 +1,5 @@
1
1
  import { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, VOICE_CAPTURE_CONSTRAINTS } from "../types.js";
2
+ import { encodeWav } from "@alexkroman1/aai/step";
2
3
  //#region src/audio/capture.ts
3
4
  /**
4
5
  * The capture half's primitives, shared by the audio path (`audio/voice-io.ts`) and
@@ -250,6 +251,48 @@ async function startCapture(ctx, stream, opts) {
250
251
  };
251
252
  }
252
253
  //#endregion
254
+ //#region src/audio/dictation.ts
255
+ /**
256
+ * The dictation recorder: the microphone captured into one clip, handed back
257
+ * as a WAV file for a transcription endpoint to read whole.
258
+ *
259
+ * A clip, not a stream — the dictation API takes the whole utterance in one
260
+ * request — so this is the pre-connect capture (`audio/preconnect.ts`) used
261
+ * for what it already does: buffer the mic at a known rate. Stopping is its
262
+ * `detach` (the worklet's tail flush lands in the buffer, so the last word is
263
+ * kept) and then `close`, which turns the browser's recording indicator off.
264
+ *
265
+ * @module
266
+ */
267
+ /**
268
+ * Ask for the microphone and start recording.
269
+ *
270
+ * Rejects like the session's own capture does — a denied prompt, a rate the
271
+ * browser refused — with the microphone released.
272
+ */
273
+ async function openDictationRecorder(opts) {
274
+ const capture = await openPreConnectCapture(opts);
275
+ let done = false;
276
+ return {
277
+ sampleRate: capture.sampleRate,
278
+ async stop() {
279
+ if (done) return encodeWav([], { sampleRate: capture.sampleRate });
280
+ done = true;
281
+ try {
282
+ const { chunks, sampleRate } = await capture.detach();
283
+ return encodeWav(chunks.map((c) => new Uint8Array(c)), { sampleRate });
284
+ } finally {
285
+ await capture.close();
286
+ }
287
+ },
288
+ async cancel() {
289
+ if (done) return;
290
+ done = true;
291
+ await capture.close().catch(() => {});
292
+ }
293
+ };
294
+ }
295
+ //#endregion
253
296
  //#region src/audio/voice-io.ts
254
297
  /**
255
298
  * Create a {@link VoiceIO} instance that captures microphone audio and
@@ -428,4 +471,4 @@ async function createVoiceIO(opts) {
428
471
  return io;
429
472
  }
430
473
  //#endregion
431
- export { createVoiceIO, openPreConnectCapture };
474
+ export { createVoiceIO, openDictationRecorder, openPreConnectCapture };
@@ -1,7 +1,7 @@
1
- import { useSessionError, useSessionStatus, useTheme } from "./context.js";
2
- import { t as MessageList } from "./message-list-DHZcLhRA.js";
1
+ import { l as useSessionStatus, s as useSessionError, u as useTheme } from "./context-BF0KsI9X.js";
2
+ import { t as MessageList } from "./message-list-BBHLK05X.js";
3
3
  import { a as inkTint, r as THINKING_COLOR, t as ERROR_COLOR } from "./_colors-CpZO-88A.js";
4
- import { t as Eyebrow } from "./eyebrow-UfmSz9yy.js";
4
+ import { t as Eyebrow } from "./eyebrow-Bp4y1rZ9.js";
5
5
  import { Controls } from "./components/controls.js";
6
6
  import clsx from "clsx";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -9,6 +9,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
9
9
  /**
10
10
  * The default label per {@link AgentState}.
11
11
  *
12
+ * {@link AgentState} is open, so a lookup by a live state is `string |
13
+ * undefined`: fall back to the raw state for one this version has no word for.
12
14
  * Override the ones your page has a better word for and keep the rest:
13
15
  *
14
16
  * ```ts
@@ -16,8 +18,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
16
18
  * import { AGENT_STATE_LABELS } from "@alexkroman1/aai-ui";
17
19
  *
18
20
  * // A dispatch board that shouts, and renames one state.
19
- * const STATE_LABEL = { ...AGENT_STATE_LABELS, thinking: "Processing" };
20
- * const shout = (s: AgentState) => STATE_LABEL[s].toUpperCase();
21
+ * const STATE_LABEL: Readonly<Record<AgentState, string>> = {
22
+ * ...AGENT_STATE_LABELS,
23
+ * thinking: "Processing",
24
+ * };
25
+ * const shout = (s: AgentState) => (STATE_LABEL[s] ?? s).toUpperCase();
21
26
  * ```
22
27
  *
23
28
  * **Sentence case, deliberately.** A template that wants caps applies its own
@@ -126,6 +131,26 @@ function SessionErrorBanner({ className }) {
126
131
  //#region src/components/session-state-dot.tsx
127
132
  /** @jsxImportSource react */
128
133
  /**
134
+ * Indicator dot colour per state, from the theme — `ConsoleShell`'s dot, and
135
+ * {@link SessionStateDot}'s for every state its `colors` does not name.
136
+ *
137
+ * `idle` is the theme's own faint ink step rather than a fixed warm grey, so
138
+ * the dot stays visible on a dark ground; the thinking amber and error red
139
+ * are semantic and stay fixed. A state this version does not know is `idle`.
140
+ *
141
+ * @internal
142
+ */
143
+ function stateColor(state, primary, idle) {
144
+ switch (state) {
145
+ case "listening":
146
+ case "speaking":
147
+ case "ready": return primary;
148
+ case "thinking": return THINKING_COLOR;
149
+ case "error": return ERROR_COLOR;
150
+ default: return idle;
151
+ }
152
+ }
153
+ /**
129
154
  * One `aai-pulse` cycle per pulsing state, in milliseconds. The two states
130
155
  * that pulse are the two in which the agent is DOING something the reader
131
156
  * cannot otherwise see — the microphone is open, or a reply is being worked
@@ -161,6 +186,23 @@ function StateDot({ color, pulseMs, className, title }) {
161
186
  title
162
187
  });
163
188
  }
189
+ /** Which coarse group a state belongs to — `error` deliberately none. */
190
+ function groupOf(state) {
191
+ switch (state) {
192
+ case "ready":
193
+ case "listening":
194
+ case "speaking": return "live";
195
+ case "thinking": return "busy";
196
+ case "error": return;
197
+ default: return "idle";
198
+ }
199
+ }
200
+ /**
201
+ * The idle dot's ink step: the theme's text at half strength over its ground.
202
+ * Lighter than `INK_FAINT_PCT` (the text steps) on purpose — a dot is a
203
+ * 8px disc, not a word, and at 65% it read as "on" beside a live one.
204
+ */
205
+ const INK_IDLE_PCT = 50;
164
206
  /**
165
207
  * The live session state as a coloured dot and a word, on its own narrow
166
208
  * subscription.
@@ -173,31 +215,20 @@ function StateDot({ color, pulseMs, className, title }) {
173
215
  * against a dot component on the grounds that it would take the shared part
174
216
  * (the words) hostage to the part that is not (the palette); this takes the
175
217
  * palette as a prop precisely so it does not. What is shared is the structure
176
- * — the exhaustive colour lookup, the label fallback, the pulse rule, the
218
+ * — the colour lookup and its theme fallback, the label fallback, the pulse rule, the
177
219
  * `useSessionStatus()` subscription that keeps the rest of the header from
178
220
  * re-rendering at STT-partial rate.
179
221
  *
180
222
  * Must be rendered inside the providers `mountClient()` installs.
181
223
  *
182
- * @example A dispatch board's readout: its own colours, three of its own words
224
+ * @example A dispatch board's readout: coarse colours, three of its own words
183
225
  * ```tsx
184
- * import type { AgentState } from "@alexkroman1/aai-ui";
185
226
  * import { SessionStateDot } from "@alexkroman1/aai-ui";
186
227
  *
187
- * const STATE_COLORS = {
188
- * disconnected: "#6b7280",
189
- * connecting: "#6b7280",
190
- * ready: "#22c55e",
191
- * listening: "#22c55e",
192
- * thinking: "#eab308",
193
- * speaking: "#3b82f6",
194
- * error: "#6b7280",
195
- * } satisfies Record<AgentState, string>;
196
- *
197
228
  * function StatusReadout() {
198
229
  * return (
199
230
  * <SessionStateDot
200
- * colors={STATE_COLORS}
231
+ * colors={{ idle: "#6b7280", live: "#22c55e", busy: "#eab308", speaking: "#3b82f6" }}
201
232
  * labels={{ listening: "LISTENING", thinking: "PROCESSING", speaking: "TRANSMITTING" }}
202
233
  * labelClassName="text-[11px] uppercase"
203
234
  * />
@@ -205,24 +236,30 @@ function StateDot({ color, pulseMs, className, title }) {
205
236
  * }
206
237
  * ```
207
238
  *
239
+ * With no `colors` at all the dot follows the mounted theme — a chrome that
240
+ * sets `mountClient({ theme })` gets a matching dot for free.
241
+ *
208
242
  * @param props - See {@link SessionStateDotProps}.
209
243
  *
210
244
  * @public
211
245
  */
212
246
  function SessionStateDot({ colors, labels, pulse = true, className, dotClassName, labelClassName }) {
213
247
  const state = useSessionStatus();
248
+ const theme = useTheme();
214
249
  const pulseMs = pulse ? PULSE_MS[state] ?? null : null;
250
+ const group = groupOf(state);
251
+ const color = colors?.[state] ?? (group === void 0 ? void 0 : colors?.[group]) ?? stateColor(state, theme.primary, inkTint(theme.text, theme.bg, INK_IDLE_PCT));
215
252
  return /* @__PURE__ */ jsxs("span", {
216
253
  className: clsx("inline-flex items-center gap-2", className),
217
254
  "data-state": state,
218
255
  children: [/* @__PURE__ */ jsx(StateDot, {
219
- color: colors[state],
256
+ color,
220
257
  pulseMs,
221
258
  className: dotClassName ?? "w-2 h-2",
222
259
  title: state
223
260
  }), /* @__PURE__ */ jsx("span", {
224
261
  className: labelClassName,
225
- children: labels?.[state] ?? AGENT_STATE_LABELS[state]
262
+ children: labels?.[state] ?? AGENT_STATE_LABELS[state] ?? state
226
263
  })]
227
264
  });
228
265
  }
@@ -230,25 +267,6 @@ function SessionStateDot({ colors, labels, pulse = true, className, dotClassName
230
267
  //#region src/components/console-shell.tsx
231
268
  /** @jsxImportSource react */
232
269
  /**
233
- * Indicator dot color per state.
234
- *
235
- * `idle` is the theme's own faint ink step rather than a fixed warm grey, so
236
- * the dot stays visible on a dark ground; the thinking amber and error red
237
- * are semantic and stay fixed.
238
- *
239
- * @internal
240
- */
241
- function stateColor(state, primary, idle) {
242
- switch (state) {
243
- case "listening":
244
- case "speaking":
245
- case "ready": return primary;
246
- case "thinking": return THINKING_COLOR;
247
- case "error": return ERROR_COLOR;
248
- default: return idle;
249
- }
250
- }
251
- /**
252
270
  * The design-system "console" chrome: a 760px column on the themed page with a
253
271
  * header (icon + live-status eyebrow), an announced error banner, the main
254
272
  * content on a raised card, and a footer row beneath it.
@@ -294,11 +312,13 @@ function stateColor(state, primary, idle) {
294
312
  * footer={<Controls />}
295
313
  * >
296
314
  * <ul>
297
- * {items.map((item) => (
298
- * <li key={item.kind === "message" ? item.message.id : item.toolCall.callId}>
299
- * {item.kind === "message" ? item.message.content : item.toolCall.name}
300
- * </li>
301
- * ))}
315
+ * {items.map((item) =>
316
+ * item.kind === "message" ? (
317
+ * <li key={item.message.id}>{item.message.content}</li>
318
+ * ) : item.kind === "tool" ? (
319
+ * <li key={item.toolCall.callId}>{item.toolCall.name}</li>
320
+ * ) : null,
321
+ * )}
302
322
  * </ul>
303
323
  * </ConsoleShell>
304
324
  * );
@@ -1,4 +1,4 @@
1
- import { n as pageBaseUrl } from "./_utils-Mi-SCFA1.js";
1
+ import { n as pageBaseUrl } from "./_utils-BlZBJPdj.js";
2
2
  import { CLIENT_CONFIG_PATH, ClientConfigResponseSchema, SESSION_TICKET_HEADER } from "@alexkroman1/aai/protocol";
3
3
  //#region src/client-config.ts
4
4
  /**
@@ -69,11 +69,12 @@ const CLIENT_CONFIG_ATTEMPT_TIMEOUT_MS = 1e4;
69
69
  *
70
70
  * @internal
71
71
  */
72
- async function loadClientConfig(platformUrl, fetchFn, presentedTicket) {
72
+ async function loadClientConfig(platformUrl, fetchFn, presentedTicket, signal) {
73
73
  const doFetch = fetchFn ?? ((input, init) => globalThis.fetch(input, init));
74
+ const deadline = AbortSignal.timeout(CLIENT_CONFIG_ATTEMPT_TIMEOUT_MS);
74
75
  try {
75
76
  const resp = await doFetch(buildAgentUrl(platformUrl, CLIENT_CONFIG_PATH).href, {
76
- signal: AbortSignal.timeout(CLIENT_CONFIG_ATTEMPT_TIMEOUT_MS),
77
+ signal: signal === void 0 ? deadline : AbortSignal.any([deadline, signal]),
77
78
  ...presentedTicket === void 0 ? {} : { headers: { [SESSION_TICKET_HEADER]: presentedTicket } }
78
79
  });
79
80
  if (!resp.ok) return null;
@@ -102,14 +103,7 @@ async function loadClientConfig(platformUrl, fetchFn, presentedTicket) {
102
103
  * treat every field as optional, because an agent that declared none is a
103
104
  * normal agent.
104
105
  *
105
- * @param platformUrl - The agent's base URL. **Defaults to the page's own
106
- * origin and path** (`pageBaseUrl()`), which is the agent that served the page
107
- * and the only case a browser has — the argument was required while the default
108
- * sat one module away in this same package, so every caller wrote
109
- * `location.origin + location.pathname` for itself. Pass one for a page reading
110
- * a DIFFERENT agent. The endpoint is resolved relative to it.
111
- * @param fetchFn - Fetch implementation, for tests and for a caller that
112
- * supplies its own credentials. Defaults to the global `fetch`.
106
+ * @param options - Where to look and how; see {@link FetchClientConfigOptions}.
113
107
  * @returns The agent's config, or `{}` when the lookup produced no answer.
114
108
  *
115
109
  * @example
@@ -132,8 +126,9 @@ async function loadClientConfig(platformUrl, fetchFn, presentedTicket) {
132
126
  *
133
127
  * @public
134
128
  */
135
- async function fetchClientConfig(platformUrl = pageBaseUrl(), fetchFn) {
136
- return await loadClientConfig(platformUrl, fetchFn) ?? AGENT_DEFAULT;
129
+ async function fetchClientConfig(options = {}) {
130
+ const { platformUrl = pageBaseUrl(), fetch: fetchFn, signal } = options;
131
+ return await loadClientConfig(platformUrl, fetchFn, void 0, signal) ?? AGENT_DEFAULT;
137
132
  }
138
133
  //#endregion
139
134
  export { fetchClientConfig as n, loadClientConfig as r, buildAgentUrl as t };