@alexkroman1/aai-ui 0.17.0 → 0.18.1

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
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
  /**
@@ -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-BsaBkBgo.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";
@@ -186,6 +186,23 @@ function StateDot({ color, pulseMs, className, title }) {
186
186
  title
187
187
  });
188
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;
189
206
  /**
190
207
  * The live session state as a coloured dot and a word, on its own narrow
191
208
  * subscription.
@@ -204,25 +221,14 @@ function StateDot({ color, pulseMs, className, title }) {
204
221
  *
205
222
  * Must be rendered inside the providers `mountClient()` installs.
206
223
  *
207
- * @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
208
225
  * ```tsx
209
- * import type { AgentState } from "@alexkroman1/aai-ui";
210
226
  * import { SessionStateDot } from "@alexkroman1/aai-ui";
211
227
  *
212
- * const STATE_COLORS = {
213
- * disconnected: "#6b7280",
214
- * connecting: "#6b7280",
215
- * ready: "#22c55e",
216
- * listening: "#22c55e",
217
- * thinking: "#eab308",
218
- * speaking: "#3b82f6",
219
- * error: "#6b7280",
220
- * } satisfies Record<AgentState, string>;
221
- *
222
228
  * function StatusReadout() {
223
229
  * return (
224
230
  * <SessionStateDot
225
- * colors={STATE_COLORS}
231
+ * colors={{ idle: "#6b7280", live: "#22c55e", busy: "#eab308", speaking: "#3b82f6" }}
226
232
  * labels={{ listening: "LISTENING", thinking: "PROCESSING", speaking: "TRANSMITTING" }}
227
233
  * labelClassName="text-[11px] uppercase"
228
234
  * />
@@ -230,6 +236,9 @@ function StateDot({ color, pulseMs, className, title }) {
230
236
  * }
231
237
  * ```
232
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
+ *
233
242
  * @param props - See {@link SessionStateDotProps}.
234
243
  *
235
244
  * @public
@@ -238,7 +247,8 @@ function SessionStateDot({ colors, labels, pulse = true, className, dotClassName
238
247
  const state = useSessionStatus();
239
248
  const theme = useTheme();
240
249
  const pulseMs = pulse ? PULSE_MS[state] ?? null : null;
241
- const color = colors?.[state] ?? stateColor(state, theme.primary, inkTint(theme.text, theme.bg, 65));
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));
242
252
  return /* @__PURE__ */ jsxs("span", {
243
253
  className: clsx("inline-flex items-center gap-2", className),
244
254
  "data-state": state,
@@ -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
  /**
@@ -1,4 +1,4 @@
1
- import { useTheme } from "../context.js";
1
+ import { u as useTheme } from "../context-BF0KsI9X.js";
2
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";
@@ -1,3 +1,2 @@
1
- import { t as ChatView } from "../chat-view-W6KcdjJY.js";
2
- import "../context.js";
1
+ import { t as ChatView } from "../chat-view-BwKpF0MN.js";
3
2
  export { ChatView };
@@ -1,6 +1,6 @@
1
- import { useSessionCore, useSessionSelector } from "../context.js";
1
+ import { c as useSessionSelector, o as useSessionCore } from "../context-BF0KsI9X.js";
2
2
  import { Button } from "./button.js";
3
- import { n as SessionUrlChips } from "../url-chips-oyy_Eiqh.js";
3
+ import { n as SessionUrlChips } from "../url-chips-C_SHU6fq.js";
4
4
  import { memo } from "react";
5
5
  import clsx from "clsx";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1,5 +1,5 @@
1
1
  import type { InputHTMLAttributes, JSX, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
2
- import type { FieldShell, FileReadMode } from "./form-types.ts";
2
+ import type { FieldShell, FileReadMode, FileValueShape, TextSplitMode } from "./form-types.ts";
3
3
  /**
4
4
  * Label + control + hint, in the layout every field here uses.
5
5
  *
@@ -67,11 +67,21 @@ export declare function NumberField(props: FieldShell & Omit<InputHTMLAttributes
67
67
  * Accepts every `<textarea>` attribute except `name` and `className`, plus the
68
68
  * shared {@link FieldShell} props. `rows` defaults to 4.
69
69
  *
70
- * @param props - {@link FieldShell} props plus `<textarea>` attributes.
70
+ * **`split="lines"` contributes a `string[]`** — one entry per non-blank line,
71
+ * trimmed — which is the control for a schema's `z.array(z.string())`: "one
72
+ * point per line". `document-redline-workflow` reshaped the textarea's string
73
+ * by hand on submit; the field does it now, so `<WorkflowFields>` can render
74
+ * it for a `string[]` property and a hand-written one can match. See
75
+ * {@link TextSplitMode}.
76
+ *
77
+ * @param props - {@link FieldShell} props, `split`, and `<textarea>` attributes.
71
78
  *
72
79
  * @public
73
80
  */
74
- export declare function TextAreaField({ name, label, hint, className, rows, ...rest }: FieldShell & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "className">): JSX.Element;
81
+ export declare function TextAreaField({ name, label, hint, className, rows, split, ...rest }: FieldShell & {
82
+ /** How the text is contributed — see above. Default `"none"`, the string. */
83
+ split?: TextSplitMode | undefined;
84
+ } & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "className">): JSX.Element;
75
85
  /**
76
86
  * A dropdown.
77
87
  *
@@ -126,17 +136,26 @@ export declare function CheckboxField({ name, label, hint, className, ...rest }:
126
136
  * {@link FileReadMode} for the four values; `upload` is shorthand for
127
137
  * `read="upload"`.
128
138
  *
139
+ * **`as="namedText"` contributes `{ name, text }`** ({@link NamedTextValue})
140
+ * instead of the metadata-bearing {@link FileValue}, and implies
141
+ * `read="text"`: the shape a schema declares for a draft to redline or a
142
+ * config to apply, and the one `<WorkflowFields>` renders for an object
143
+ * property of exactly that shape. `document-redline-workflow` reshaped the
144
+ * `FileValue` by hand on submit; the field does it now.
145
+ *
129
146
  * Otherwise accepts every `<input>` attribute except `name`, `className` and
130
147
  * `type`, plus the shared {@link FieldShell} props — so `accept` and
131
148
  * `multiple` are passed straight through.
132
149
  *
133
- * @param props - {@link FieldShell} props, `read`/`upload`, and `<input>`
150
+ * @param props - {@link FieldShell} props, `read`/`upload`/`as`, and `<input>`
134
151
  * attributes.
135
152
  *
136
153
  * @public
137
154
  */
138
- export declare function FileField({ name, label, hint, className, read, upload, ...rest }: FieldShell & {
155
+ export declare function FileField({ name, label, hint, className, read, upload, as, ...rest }: FieldShell & {
139
156
  read?: FileReadMode;
140
157
  /** Shorthand for `read="upload"` — see above. */
141
158
  upload?: boolean;
159
+ /** The shape of a READ file's value — see above and {@link FileValueShape}. */
160
+ as?: FileValueShape | undefined;
142
161
  } & Omit<InputHTMLAttributes<HTMLInputElement>, "name" | "className" | "type">): JSX.Element;
@@ -48,6 +48,21 @@ export type FileValue = {
48
48
  * @public
49
49
  */
50
50
  export declare function isFileValue(value: unknown): value is FileValue;
51
+ /**
52
+ * What a `<FileField read="text" as="namedText">` contributes: the file's name
53
+ * and its text, and nothing else — the shape a workflow schema declares as
54
+ * `z.object({ name: z.string(), text: z.string() })` for a draft, a config, a
55
+ * CSV of ids that belongs in the run's input rather than in an upload. A
56
+ * {@link FileValue} carries the browser's metadata too, which a schema then
57
+ * has to strip or refuse; this is the value a schema can take as it is.
58
+ *
59
+ * @sealed
60
+ * @public
61
+ */
62
+ export type NamedTextValue = {
63
+ name: string;
64
+ text: string;
65
+ };
51
66
  /**
52
67
  * How much of a chosen file a {@link FileField} reads.
53
68
  *
@@ -60,6 +75,23 @@ export declare function isFileValue(value: unknown): value is FileValue;
60
75
  * @public
61
76
  */
62
77
  export type FileReadMode = "none" | "text" | "dataUrl" | "upload";
78
+ /**
79
+ * The SHAPE a {@link FileField} contributes a read file as. `"file"` (the
80
+ * default) is a {@link FileValue}; `"namedText"` is a {@link NamedTextValue},
81
+ * which implies `read="text"`. Only `"namedText"` is a reshape today; the
82
+ * option exists so a workflow's object property has a declared control.
83
+ *
84
+ * @public
85
+ */
86
+ export type FileValueShape = "file" | "namedText";
87
+ /**
88
+ * How a {@link TextAreaField} contributes its text. `"none"` (the default) is
89
+ * the string; `"lines"` is a `string[]` — one entry per non-blank line,
90
+ * trimmed — the control for a schema's `z.array(z.string())`.
91
+ *
92
+ * @public
93
+ */
94
+ export type TextSplitMode = "none" | "lines";
63
95
  /**
64
96
  * The props every field in `form.tsx` shares.
65
97
  *
@@ -2,7 +2,7 @@ import type { ButtonHTMLAttributes, FormHTMLAttributes, JSX, ReactNode } from "r
2
2
  import { type ButtonSize, type ButtonVariant } from "./button.tsx";
3
3
  import type { FormValues } from "./form-types.ts";
4
4
  export { CheckboxField, Field, FileField, NumberField, SelectField, TextAreaField, TextField, } from "./form-fields.tsx";
5
- export type { FieldShell, FileReadMode, FileValue, FormValues } from "./form-types.ts";
5
+ export type { FieldShell, FileReadMode, FileValue, FileValueShape, FormValues, NamedTextValue, TextSplitMode, } from "./form-types.ts";
6
6
  export { isFileValue } from "./form-types.ts";
7
7
  /**
8
8
  * Props of {@link Form}.
@@ -1,3 +1,2 @@
1
- import "../context.js";
2
- import { t as MessageList } from "../message-list-BsaBkBgo.js";
1
+ import { t as MessageList } from "../message-list-BBHLK05X.js";
3
2
  export { MessageList };