@alexkroman1/aai-ui 6.11.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +104 -3
  2. package/dist/_run-controls.d.ts +33 -0
  3. package/dist/{chat-view-ByQFf94G.js → chat-view-BKsFfFZJ.js} +57 -11
  4. package/dist/client-config-B4nznRvH.js +134 -0
  5. package/dist/client-config.d.ts +45 -2
  6. package/dist/client-dir.d.ts +3 -1
  7. package/dist/client-dir.js +3 -1
  8. package/dist/components/auto-scroll.d.ts +24 -13
  9. package/dist/components/button.d.ts +8 -4
  10. package/dist/components/button.js +4 -4
  11. package/dist/components/chat-view.d.ts +4 -3
  12. package/dist/components/chat-view.js +1 -1
  13. package/dist/components/console-shell.d.ts +71 -10
  14. package/dist/components/controls.d.ts +15 -4
  15. package/dist/components/controls.js +48 -2
  16. package/dist/components/form-fields.d.ts +142 -0
  17. package/dist/components/form-types.d.ts +6 -0
  18. package/dist/components/form.d.ts +38 -80
  19. package/dist/components/markdown.d.ts +31 -5
  20. package/dist/components/message-list.d.ts +21 -4
  21. package/dist/components/message-list.js +1 -1
  22. package/dist/components/sidebar-layout.d.ts +11 -0
  23. package/dist/components/sidebar-layout.js +2 -0
  24. package/dist/components/start-screen.d.ts +8 -0
  25. package/dist/components/start-screen.js +2 -0
  26. package/dist/components/tool-call-block.js +1 -1
  27. package/dist/components/tool-call-row.d.ts +24 -0
  28. package/dist/components/upload-progress.d.ts +17 -7
  29. package/dist/components/workflow-fields.d.ts +10 -22
  30. package/dist/components/workflow-progress.d.ts +29 -10
  31. package/dist/context.d.ts +36 -0
  32. package/dist/context.js +105 -14
  33. package/dist/default-client/assets/index-S5fkKi6B.css +2 -0
  34. package/dist/default-client/assets/index-fEkrcZgo.js +293 -0
  35. package/dist/default-client/index.html +2 -2
  36. package/dist/define-client.d.ts +67 -62
  37. package/dist/define-client.js +56 -22
  38. package/dist/hooks.d.ts +88 -3
  39. package/dist/index.d.ts +12 -11
  40. package/dist/index.js +501 -167
  41. package/dist/internal.d.ts +40 -0
  42. package/dist/internal.js +6 -0
  43. package/dist/{message-list-CpPV7dGx.js → message-list-DHddO4QC.js} +217 -72
  44. package/dist/{session-core-CAfYmUbg.js → session-core-C2JtLArh.js} +267 -165
  45. package/dist/session-core-audio-setup.d.ts +3 -0
  46. package/dist/session-core-messages.d.ts +3 -0
  47. package/dist/session-core-state.d.ts +146 -0
  48. package/dist/session-core-types.d.ts +77 -32
  49. package/dist/session-core.d.ts +3 -2
  50. package/dist/session-core.js +1 -1
  51. package/dist/{tool-call-block-D6pTEPrT.js → tool-call-block-DoF-cSIZ.js} +27 -19
  52. package/dist/tool-config-context-DzAofqi_.js +19 -0
  53. package/dist/types.d.ts +31 -5
  54. package/dist/types.js +5 -4
  55. package/dist/{controls-CjG91QJ4.js → url-chips-DpM7Oocj.js} +3 -46
  56. package/dist/use-conversation.d.ts +122 -0
  57. package/dist/use-download-url.d.ts +83 -0
  58. package/dist/use-workflow-form.d.ts +59 -3
  59. package/dist/use-workflow-run.d.ts +35 -0
  60. package/dist/use-workflow-stream.d.ts +36 -62
  61. package/dist/workflow-client.d.ts +36 -11
  62. package/dist/workflow-status-labels.d.ts +35 -0
  63. package/package.json +10 -5
  64. package/styles.css +14 -0
  65. package/dist/default-client/assets/index-DTLrhtTF.css +0 -2
  66. package/dist/default-client/assets/index-DXODx_9r.js +0 -293
package/dist/index.js CHANGED
@@ -1,23 +1,23 @@
1
- import { i as loadClientConfig, n as buildAgentUrl, r as fetchClientConfig, t as createSessionCore } from "./session-core-CAfYmUbg.js";
2
- import { n as Markdown, r as AutoScroll, t as MessageList } from "./message-list-CpPV7dGx.js";
3
- import { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
1
+ import { n as fetchClientConfig } from "./client-config-B4nznRvH.js";
2
+ import { a as useUserTranscript, i as TRANSCRIBING_PLACEHOLDER, n as Markdown, o as AutoScroll, r as useConversation, t as MessageList } from "./message-list-DHddO4QC.js";
3
+ import { ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
4
4
  import { r as inkTint } from "./_colors-CcAi2FOU.js";
5
5
  import { Button } from "./components/button.js";
6
- import { t as ChatView } from "./chat-view-ByQFf94G.js";
6
+ import { n as ConsoleShell, t as ChatView } from "./chat-view-BKsFfFZJ.js";
7
7
  import { n as setPageTitle, t as pageBaseUrl } from "./_utils-B6498_bm.js";
8
- import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-CjG91QJ4.js";
9
- import { n as ToolConfigContext, r as ToolCallRow } from "./tool-call-block-D6pTEPrT.js";
8
+ import { Controls } from "./components/controls.js";
9
+ import { n as ToolCallRow } from "./tool-call-block-DoF-cSIZ.js";
10
10
  import { SidebarLayout } from "./components/sidebar-layout.js";
11
11
  import { StartScreen } from "./components/start-screen.js";
12
- import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
12
+ import { t as createSessionCore } from "./session-core-C2JtLArh.js";
13
13
  import { client, mountRoot, resolveContainer } from "./define-client.js";
14
14
  import { useAgentState, useEvent, useToolCallStart, useToolResult } from "./hooks.js";
15
15
  import clsx from "clsx";
16
16
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
17
17
  import { createContext, createElement, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
18
- import { errorMessage, isTerminal, isTerminal as isTerminal$1 } from "@alexkroman1/aai";
18
+ import { errorMessage } from "@alexkroman1/aai";
19
19
  import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
20
- import { createWorkflowApiClient, readEventStream } from "@alexkroman1/aai/workflow-api";
20
+ import { createWorkflowApiClient, isTerminal, isTerminal as isTerminal$1, readEventStream } from "@alexkroman1/aai/workflow-api";
21
21
  import { createEpoch } from "@alexkroman1/aai/internal";
22
22
  //#region components/_form-readiness.ts
23
23
  /**
@@ -200,104 +200,52 @@ function dataUrl(file) {
200
200
  });
201
201
  }
202
202
  //#endregion
203
- //#region components/form.tsx
203
+ //#region components/form-fields.tsx
204
204
  /** @jsxImportSource react */
205
205
  /**
206
- * Simple forms, for the pages that are not conversations.
207
- *
208
- * A workflow app's front door is a form: name a recording, upload a file, press
209
- * submit. Nothing here knew how to render one — the components in this package
210
- * are all about a live session (a transcript, a mic button, a tool-call row) —
211
- * so every such page started by hand-rolling labels, inputs, a submit button and
212
- * the value collection between them, and did it differently each time.
206
+ * The controls a form is made of — the shell, the six fields, and the styling
207
+ * they share.
213
208
  *
214
- * ## Values come off the DOM, not out of React state
209
+ * Split out of `form.tsx` when that file crossed the repo's source-file line
210
+ * cap; `form.tsx` re-exports every name here, so it stays the one import path
211
+ * for everything a form needs and nothing a caller writes changed.
215
212
  *
216
- * {@link Form} reads its own `<form>` element on submit and builds one plain
217
- * object from the named controls. That is what makes a field here nothing more
218
- * than a styled `<input>`: no registration, no controlled-component ceremony,
219
- * and a plain `<input name="x">` a caller writes themselves works exactly like
220
- * the ones below.
213
+ * ## Every field takes its element's own attributes
221
214
  *
222
- * It also means the values are TYPED rather than all-strings, which a
223
- * `new FormData(form)` cannot give: a number field yields a number, a checkbox a
224
- * boolean, an empty optional field nothing at all. That matters because these
225
- * values go straight into a workflow's input, where a zod schema is waiting —
226
- * `"3"` against `z.number()` is a rejected run, and the browser is the only
227
- * place that still knows the field was `type="number"`.
215
+ * Each control is `FieldShell & Omit<…HTMLAttributes, "name" | "className">`,
216
+ * so `required`, `placeholder`, `accept`, `min`/`max`, `aria-*` and the rest
217
+ * pass straight through to the element. `name` and `className` are owned here:
218
+ * `name` is the key the field contributes to {@link FormValues} and is put on
219
+ * the control itself, and `className` styles the field's WRAPPER, the control
220
+ * taking the shared styling below.
228
221
  *
229
- * ## A file field either describes a file or uploads it
230
- *
231
- * See {@link FileField}. A workflow input is journaled and replayed on every
232
- * resume, so file BYTES do not belong in it — `upload` is the field that sends
233
- * them somewhere a step can read them and contributes the handle instead.
222
+ * Note this means the per-property doc comments on the intersections do not
223
+ * reach the published reference — see "A memoized component must NAME its
224
+ * props type" in `packages/aai-ui/CLAUDE.md` — which is why each component's
225
+ * own description carries what a caller needs.
234
226
  */
235
227
  /**
236
- * A form that hands its values to `onSubmit` as one object.
228
+ * Label + control + hint, in the layout every field here uses.
237
229
  *
238
- * Native validation still applies — a `required` field blocks the submit and the
239
- * browser says so, which is better than anything this could render.
230
+ * Exported so a caller's own control gets the same shell rather than an
231
+ * approximation of it.
240
232
  *
241
233
  * @example
242
234
  * ```tsx
243
- * import { Form, SubmitButton, TextField } from "@alexkroman1/aai-ui";
235
+ * import { Field, Form } from "@alexkroman1/aai-ui";
244
236
  *
245
- * function NameForm() {
237
+ * function ColorForm() {
246
238
  * return (
247
- * <Form onSubmit={(values) => console.log(values.topic)}>
248
- * <TextField name="topic" label="Topic" required />
249
- * <SubmitButton>Start</SubmitButton>
239
+ * <Form onSubmit={() => undefined}>
240
+ * <Field label="Accent" hint="Any CSS color." htmlFor="accent">
241
+ * <input id="accent" name="accent" type="color" />
242
+ * </Field>
250
243
  * </Form>
251
244
  * );
252
245
  * }
253
246
  * ```
254
247
  *
255
- * @public
256
- */
257
- function Form({ onSubmit, error, children, className, ...rest }) {
258
- const theme = useTheme();
259
- const [busy, setBusy] = useState(false);
260
- const { pending: fieldsPending, declare } = useFormReadiness();
261
- return /* @__PURE__ */ jsxs("form", {
262
- onSubmit: useCallback((event) => {
263
- event.preventDefault();
264
- if (busy || fieldsPending) return;
265
- const form = event.currentTarget;
266
- setBusy(true);
267
- (async () => {
268
- try {
269
- await onSubmit(await collectValues(form));
270
- } finally {
271
- setBusy(false);
272
- }
273
- })();
274
- }, [
275
- busy,
276
- fieldsPending,
277
- onSubmit
278
- ]),
279
- className: clsx("flex flex-col gap-5 font-aai", className),
280
- ...rest,
281
- children: [/* @__PURE__ */ jsx("fieldset", {
282
- disabled: busy || fieldsPending,
283
- className: "contents",
284
- children: /* @__PURE__ */ jsx(FormReadinessProvider, {
285
- value: declare,
286
- children
287
- })
288
- }), error !== void 0 && error !== "" && /* @__PURE__ */ jsx("p", {
289
- role: "alert",
290
- className: "text-sm",
291
- style: { color: theme.primary },
292
- children: error
293
- })]
294
- });
295
- }
296
- /**
297
- * Label + control + hint, in the layout every field here uses.
298
- *
299
- * Exported so a caller's own control gets the same shell rather than an
300
- * approximation of it.
248
+ * @param props - Field-shell props.
301
249
  *
302
250
  * @public
303
251
  */
@@ -364,6 +312,11 @@ function useFileControlProps() {
364
312
  /**
365
313
  * A single-line text input.
366
314
  *
315
+ * Accepts every `<input>` attribute except `name` and `className`, which this
316
+ * component owns, plus the shared {@link FieldShell} props.
317
+ *
318
+ * @param props - {@link FieldShell} props plus `<input>` attributes.
319
+ *
367
320
  * @public
368
321
  */
369
322
  function TextField({ name, label, hint, className, ...rest }) {
@@ -386,6 +339,12 @@ function TextField({ name, label, hint, className, ...rest }) {
386
339
  * A number input. Contributes a NUMBER to {@link FormValues}, or nothing when
387
340
  * left empty.
388
341
  *
342
+ * Accepts every `<input>` attribute except `name`, `className` and `type`,
343
+ * plus the shared {@link FieldShell} props — so `min`, `max` and `step` are
344
+ * passed straight through.
345
+ *
346
+ * @param props - {@link FieldShell} props plus `<input>` attributes.
347
+ *
389
348
  * @public
390
349
  */
391
350
  function NumberField({ name, label, hint, className, ...rest }) {
@@ -407,6 +366,11 @@ function NumberField({ name, label, hint, className, ...rest }) {
407
366
  /**
408
367
  * A multi-line text input.
409
368
  *
369
+ * Accepts every `<textarea>` attribute except `name` and `className`, plus the
370
+ * shared {@link FieldShell} props. `rows` defaults to 4.
371
+ *
372
+ * @param props - {@link FieldShell} props plus `<textarea>` attributes.
373
+ *
410
374
  * @public
411
375
  */
412
376
  function TextAreaField({ name, label, hint, className, rows = 4, ...rest }) {
@@ -426,8 +390,19 @@ function TextAreaField({ name, label, hint, className, rows = 4, ...rest }) {
426
390
  });
427
391
  }
428
392
  /**
429
- * A dropdown. Pass `options`, or `children` for full control over the
430
- * `<option>` elements.
393
+ * A dropdown.
394
+ *
395
+ * `options` is the short form — a list of strings, or of
396
+ * `{ value, label }` pairs when the two differ. Pass `children` instead for
397
+ * full control over the `<option>` elements; `children` wins when both are
398
+ * given.
399
+ *
400
+ * Otherwise accepts every `<select>` attribute except `name` and `className`,
401
+ * plus the shared {@link FieldShell} props. Note `multiple` works and
402
+ * contributes an ARRAY (`[]` when nothing is chosen).
403
+ *
404
+ * @param props - {@link FieldShell} props, `options`, and `<select>`
405
+ * attributes.
431
406
  *
432
407
  * @public
433
408
  */
@@ -456,6 +431,12 @@ function SelectField({ name, label, hint, className, options, children, ...rest
456
431
  /**
457
432
  * A checkbox. Contributes a BOOLEAN to {@link FormValues}.
458
433
  *
434
+ * Accepts every `<input>` attribute except `name`, `className` and `type`,
435
+ * plus the shared {@link FieldShell} props. The label renders beside the box
436
+ * rather than above it, so `hint` is the place for guidance.
437
+ *
438
+ * @param props - {@link FieldShell} props plus `<input>` attributes.
439
+ *
459
440
  * @public
460
441
  */
461
442
  function CheckboxField({ name, label, hint, className, ...rest }) {
@@ -493,7 +474,16 @@ function CheckboxField({ name, label, hint, className, ...rest }) {
493
474
  *
494
475
  * **Without it the field describes the file and does not read it.** `read`
495
476
  * exists for the cases where the bytes really are small and really are the
496
- * input — a CSV of ids, a config — and the size is the author's to check.
477
+ * input — a CSV of ids, a config — and the size is the author's to check. See
478
+ * {@link FileRead} for the four values; `upload` is shorthand for
479
+ * `read="upload"`.
480
+ *
481
+ * Otherwise accepts every `<input>` attribute except `name`, `className` and
482
+ * `type`, plus the shared {@link FieldShell} props — so `accept` and
483
+ * `multiple` are passed straight through.
484
+ *
485
+ * @param props - {@link FieldShell} props, `read`/`upload`, and `<input>`
486
+ * attributes.
497
487
  *
498
488
  * @public
499
489
  */
@@ -515,20 +505,144 @@ function FileField({ name, label, hint, className, read = "none", upload = false
515
505
  })
516
506
  });
517
507
  }
508
+ //#endregion
509
+ //#region components/form.tsx
510
+ /** @jsxImportSource react */
511
+ /**
512
+ * Simple forms, for the pages that are not conversations.
513
+ *
514
+ * A workflow app's front door is a form: name a recording, upload a file, press
515
+ * submit. Nothing here knew how to render one — the components in this package
516
+ * are all about a live session (a transcript, a mic button, a tool-call row) —
517
+ * so every such page started by hand-rolling labels, inputs, a submit button and
518
+ * the value collection between them, and did it differently each time.
519
+ *
520
+ * ## Values come off the DOM, not out of React state
521
+ *
522
+ * {@link Form} reads its own `<form>` element on submit and builds one plain
523
+ * object from the named controls. That is what makes a field here nothing more
524
+ * than a styled `<input>`: no registration, no controlled-component ceremony,
525
+ * and a plain `<input name="x">` a caller writes themselves works exactly like
526
+ * the ones below.
527
+ *
528
+ * It also means the values are TYPED rather than all-strings, which a
529
+ * `new FormData(form)` cannot give: a number field yields a number, a checkbox a
530
+ * boolean, an empty optional field nothing at all. That matters because these
531
+ * values go straight into a workflow's input, where a zod schema is waiting —
532
+ * `"3"` against `z.number()` is a rejected run, and the browser is the only
533
+ * place that still knows the field was `type="number"`.
534
+ *
535
+ * ## A file field either describes a file or uploads it
536
+ *
537
+ * See {@link FileField}. A workflow input is journaled and replayed on every
538
+ * resume, so file BYTES do not belong in it — `upload` is the field that sends
539
+ * them somewhere a step can read them and contributes the handle instead.
540
+ */
541
+ /**
542
+ * A form that hands its values to `onSubmit` as one object.
543
+ *
544
+ * Native validation still applies — a `required` field blocks the submit and the
545
+ * browser says so, which is better than anything this could render.
546
+ *
547
+ * @example
548
+ * ```tsx
549
+ * import { Form, SubmitButton, TextField } from "@alexkroman1/aai-ui";
550
+ *
551
+ * function NameForm() {
552
+ * return (
553
+ * <Form onSubmit={(values) => console.log(values.topic)}>
554
+ * <TextField name="topic" label="Topic" required />
555
+ * <SubmitButton>Start</SubmitButton>
556
+ * </Form>
557
+ * );
558
+ * }
559
+ * ```
560
+ *
561
+ * @param props - See {@link FormProps}. Every `<form>` attribute except
562
+ * `onSubmit` and `className` is passed through.
563
+ *
564
+ * @public
565
+ */
566
+ function Form({ onSubmit, error, children, className, ...rest }) {
567
+ const theme = useTheme();
568
+ const [busy, setBusy] = useState(false);
569
+ const { pending: fieldsPending, declare } = useFormReadiness();
570
+ return /* @__PURE__ */ jsxs("form", {
571
+ onSubmit: useCallback((event) => {
572
+ event.preventDefault();
573
+ if (busy || fieldsPending) return;
574
+ const form = event.currentTarget;
575
+ setBusy(true);
576
+ (async () => {
577
+ try {
578
+ await onSubmit(await collectValues(form));
579
+ } finally {
580
+ setBusy(false);
581
+ }
582
+ })();
583
+ }, [
584
+ busy,
585
+ fieldsPending,
586
+ onSubmit
587
+ ]),
588
+ className: clsx("flex flex-col gap-5 font-aai", className),
589
+ ...rest,
590
+ children: [/* @__PURE__ */ jsx("fieldset", {
591
+ disabled: busy || fieldsPending,
592
+ className: "contents",
593
+ children: /* @__PURE__ */ jsx(FormReadinessProvider, {
594
+ value: declare,
595
+ children
596
+ })
597
+ }), error !== void 0 && error !== "" && /* @__PURE__ */ jsx("p", {
598
+ role: "alert",
599
+ className: "text-sm",
600
+ style: { color: theme.primary },
601
+ children: error
602
+ })]
603
+ });
604
+ }
518
605
  /**
519
606
  * The form's submit button, disabled and relabelled while a submit is in
520
607
  * flight.
521
608
  *
609
+ * Accepts all standard `<button>` HTML attributes except `type` and `disabled`,
610
+ * in addition to the props below — so `aria-label` on an icon-only submit,
611
+ * `form`, `id`, `title` and `onClick` all work here exactly as they do on
612
+ * {@link Button}. `type` and `disabled` stay owned: this component sets both
613
+ * from `pending`, and letting a caller set either is how a form gets a submit
614
+ * button that does not submit.
615
+ *
616
+ * @example
617
+ * ```tsx
618
+ * import { Form, SubmitButton, TextField } from "@alexkroman1/aai-ui";
619
+ *
620
+ * function Digest({ pending }: { pending: boolean }) {
621
+ * return (
622
+ * <Form onSubmit={() => undefined}>
623
+ * <TextField name="url" label="Link" required />
624
+ * <SubmitButton pending={pending} variant="secondary" size="lg">
625
+ * Summarize
626
+ * </SubmitButton>
627
+ * </Form>
628
+ * );
629
+ * }
630
+ * ```
631
+ *
632
+ * @param props - Button props.
633
+ *
522
634
  * @public
523
635
  */
524
- function SubmitButton({ children, pending = false, pendingLabel = "Working…", size, className }) {
636
+ function SubmitButton({ children, pending = false, pendingLabel = "Working…", size, variant, className, ...rest }) {
525
637
  return /* @__PURE__ */ jsx(Button, {
526
638
  type: "submit",
527
639
  disabled: pending,
528
640
  ...omitUndefined({
529
641
  size,
642
+ variant,
530
643
  className
531
644
  }),
645
+ ...rest,
532
646
  children: pending ? pendingLabel : children
533
647
  });
534
648
  }
@@ -614,13 +728,7 @@ function formatBytes(bytes) {
614
728
  * }
615
729
  * ```
616
730
  *
617
- * @param upload - What `useWorkflowSubmit` reports. `undefined` renders nothing,
618
- * so a page may pass its state straight through.
619
- * @param onPause - The hook's `pauseUpload`. Pass it together with `onResume` to
620
- * get the control; pass neither for a bar that only reports.
621
- * @param onResume - The hook's `resumeUpload`.
622
- * @param className - Replaces the default classes rather than extending them,
623
- * so a custom chrome is not fighting a default it did not ask for.
731
+ * @param props - Progress-bar props.
624
732
  *
625
733
  * @public
626
734
  */
@@ -674,6 +782,44 @@ function UploadProgressBar({ upload, onPause, onResume, className }) {
674
782
  });
675
783
  }
676
784
  //#endregion
785
+ //#region _run-controls.ts
786
+ /**
787
+ * The two things a page does TO a run it started, bound to the run it has.
788
+ *
789
+ * `useWorkflowSubmit` and `useWorkflowStream` both hold a run id and neither
790
+ * handed it back, so a page that wanted "send it now" or "stop" had to hold an
791
+ * `api` of its own purely to write `api.wake(runId)` — which is the whole reason
792
+ * the two raw-primitive template pages keep a client at module scope. That is a
793
+ * page carrying the transport to make up for a hook withholding its own state.
794
+ *
795
+ * Both calls answer rather than fail when there is nothing to act on — `0`
796
+ * sleeps ended, `false` this call did not end it — which is the SDK's own
797
+ * contract for them (two tabs pressing Stop is ordinary), and it is what lets
798
+ * the no-run case be the same answer rather than a special one a caller has to
799
+ * branch on.
800
+ */
801
+ /**
802
+ * Bind `wake` and `cancel` to whatever run the hook is currently following.
803
+ *
804
+ * @param runId - The live run, or `undefined` before one exists.
805
+ * @param getClient - The stable getter from `useWorkflowApiRef`.
806
+ * @returns Two callbacks, stable while `runId` is.
807
+ *
808
+ * @internal
809
+ */
810
+ function useRunControls(runId, getClient) {
811
+ return {
812
+ wake: useCallback(async () => {
813
+ if (runId === void 0) return 0;
814
+ return await getClient().wake(runId);
815
+ }, [runId, getClient]),
816
+ cancel: useCallback(async () => {
817
+ if (runId === void 0) return false;
818
+ return await getClient().cancel(runId);
819
+ }, [runId, getClient])
820
+ };
821
+ }
822
+ //#endregion
677
823
  //#region _upload-recall.ts
678
824
  /**
679
825
  * Where an upload's ID survives a page RELOAD.
@@ -1101,6 +1247,32 @@ async function uploadFiles(api, input, report, parallel, session) {
1101
1247
  //#endregion
1102
1248
  //#region workflow-client.ts
1103
1249
  /**
1250
+ * Browser client for the workflow HTTP API (`aai/host/workflow-api.ts`).
1251
+ *
1252
+ * This is the whole client half of a WORKFLOW APP: an agent whose front door is
1253
+ * a form rather than a microphone (`workflowApp()`) starts runs
1254
+ * here and watches them for the answer. It deliberately does NOT go through
1255
+ * `SessionCore` — there is no socket, no audio graph, and no session to resume.
1256
+ *
1257
+ * **The requests themselves are the SDK's now**
1258
+ * (`createWorkflowApiClient`, `@alexkroman1/aai/workflow-api`), and what is left
1259
+ * here is the one thing that is genuinely a BROWSER's: the default base URL.
1260
+ * Every route, every query, the 404-is-an-answer rule and the `wait` clamp were
1261
+ * written three times over — here, in the studio's Workflows card, and in
1262
+ * `aai workflow` — and the parts the copies disagreed on were exactly the ones a
1263
+ * reader cannot check by eye. The SDK module's doc carries that argument; this
1264
+ * file must not grow a second implementation of any of it.
1265
+ *
1266
+ * The one thing worth knowing before using it: **a run outlives the page.**
1267
+ * Starting one resolves as soon as the run is created, so `runId` is the only
1268
+ * handle that matters and it stays valid across a reload, a different device, or
1269
+ * `curl` — which is what makes `useWorkflowRun` a watch rather than a
1270
+ * subscription to something the page owns.
1271
+ *
1272
+ * The loop that keeps asking lives in `use-workflow-run.ts`, and the streaming
1273
+ * fast path under it in `workflow-events.ts`.
1274
+ */
1275
+ /**
1104
1276
  * Create a workflow API client aimed at the agent serving this page.
1105
1277
  *
1106
1278
  * Hoist it out of the component that uses it. `useWorkflowRun` holds the client
@@ -1108,6 +1280,32 @@ async function uploadFiles(api, input, report, parallel, session) {
1108
1280
  * but a client built in render is still a new `fetch` closure every time and
1109
1281
  * reads as though it were free.
1110
1282
  *
1283
+ * @example
1284
+ * ```tsx
1285
+ * import { createWorkflowApi, useWorkflowRun } from "@alexkroman1/aai-ui";
1286
+ * import { useState } from "react";
1287
+ *
1288
+ * // Module scope, not render scope — see above.
1289
+ * const api = createWorkflowApi();
1290
+ *
1291
+ * function StartDigest() {
1292
+ * const [runId, setRunId] = useState<string>();
1293
+ * const { run } = useWorkflowRun(runId, { api });
1294
+ * return (
1295
+ * <button
1296
+ * type="button"
1297
+ * onClick={() => void api.start("digest", { url: "…" }).then(setRunId)}
1298
+ * >
1299
+ * {run ? run.status : "Start"}
1300
+ * </button>
1301
+ * );
1302
+ * }
1303
+ * ```
1304
+ *
1305
+ * @param opts - See {@link WorkflowApiOptions}. Both fields are optional; the
1306
+ * default base URL is the page's own origin and path.
1307
+ * @returns The call set — see {@link WorkflowApi}.
1308
+ *
1111
1309
  * @public
1112
1310
  */
1113
1311
  function createWorkflowApi(opts = {}) {
@@ -1365,10 +1563,45 @@ function pollUntilTerminal(getClient, runId, intervalMs, onRun, onError, onStopp
1365
1563
  * STOPS on a terminal status, so a finished run costs nothing; passing
1366
1564
  * `undefined` (nothing started yet) also costs nothing.
1367
1565
  *
1566
+ * @example
1567
+ * ```tsx
1568
+ * import type { ToolInputSchema, WorkflowDef } from "@alexkroman1/aai";
1569
+ * import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
1570
+ * import { useWorkflowRun } from "@alexkroman1/aai-ui";
1571
+ *
1572
+ * // A real page writes `import type { digest } from "./agent.ts"`. Stood in
1573
+ * // for here so the example compiles on its own.
1574
+ * declare const digest: WorkflowDef<ToolInputSchema, Promise<{ points: string[] }>>;
1575
+ *
1576
+ * type Digest = WorkflowOutputOf<typeof digest>;
1577
+ *
1578
+ * function RunPanel({ runId }: { runId: string | undefined }) {
1579
+ * // The type argument is what makes `run.output` a `Digest` below.
1580
+ * const { run, error, polling } = useWorkflowRun<Digest>(runId);
1581
+ * if (error !== undefined) return <p role="alert">{error}</p>;
1582
+ * if (run?.status === "completed") {
1583
+ * return (
1584
+ * <ul>
1585
+ * {run.output.points.map((point) => (
1586
+ * <li key={point}>{point}</li>
1587
+ * ))}
1588
+ * </ul>
1589
+ * );
1590
+ * }
1591
+ * return <p>{polling ? "Working…" : "Nothing running."}</p>;
1592
+ * }
1593
+ * ```
1594
+ *
1368
1595
  * @typeParam R - The workflow's output type. Supplying it is what makes
1369
1596
  * `run.status === "completed"` narrow to a typed `run.output` instead of
1370
1597
  * `unknown`. Derive it with `WorkflowOutputOf<typeof myWorkflow>` — a
1371
1598
  * type-only import of `agent.ts` is erased, so it costs the bundle nothing.
1599
+ * @param runId - The run to watch. `undefined` costs nothing, so a page may
1600
+ * pass its state straight through before a run exists.
1601
+ * @param opts - `api` when the page holds its own client; `intervalMs` to
1602
+ * change the poll interval the stream falls back to.
1603
+ * @returns The latest snapshot, the last read's error, and whether the watch
1604
+ * is still going — see {@link UseWorkflowRunResult}.
1372
1605
  *
1373
1606
  * @public
1374
1607
  */
@@ -1460,6 +1693,35 @@ function useWorkflowRun(runId, opts = {}) {
1460
1693
  * empty list — which renders as a form with no fields and reads as "this agent
1461
1694
  * declares no workflows" about an agent that was merely unreachable.
1462
1695
  *
1696
+ * @example
1697
+ * ```tsx
1698
+ * import { useWorkflows } from "@alexkroman1/aai-ui";
1699
+ *
1700
+ * // A page rendering its own chrome from the listing — a picker, say. A form
1701
+ * // for ONE workflow wants `<WorkflowFields workflow="name" />` instead,
1702
+ * // which does this lookup itself.
1703
+ * function WorkflowPicker({ onPick }: { onPick: (name: string) => void }) {
1704
+ * const { workflows, loading, error } = useWorkflows();
1705
+ * if (loading) return <p>Loading…</p>;
1706
+ * if (error !== undefined) return <p role="alert">{error}</p>;
1707
+ * return (
1708
+ * <ul>
1709
+ * {workflows.map((summary) => (
1710
+ * <li key={summary.name}>
1711
+ * <button type="button" onClick={() => onPick(summary.name)}>
1712
+ * {summary.description ?? summary.name}
1713
+ * </button>
1714
+ * </li>
1715
+ * ))}
1716
+ * </ul>
1717
+ * );
1718
+ * }
1719
+ * ```
1720
+ *
1721
+ * @param opts - See {@link UseWorkflowsOptions}.
1722
+ * @returns The listing, its loading flag and its failure — see
1723
+ * {@link UseWorkflowsResult}.
1724
+ *
1463
1725
  * @public
1464
1726
  */
1465
1727
  function useWorkflows(opts = {}) {
@@ -1530,6 +1792,7 @@ function useWorkflowSubmit(workflow, opts = {}) {
1530
1792
  api,
1531
1793
  intervalMs
1532
1794
  }));
1795
+ const { wake, cancel } = useRunControls(runId, getClient);
1533
1796
  return {
1534
1797
  submit: useCallback(async (input) => {
1535
1798
  const client = getClient();
@@ -1569,6 +1832,8 @@ function useWorkflowSubmit(workflow, opts = {}) {
1569
1832
  setStartError(void 0);
1570
1833
  setUpload(void 0);
1571
1834
  }, []),
1835
+ wake,
1836
+ cancel,
1572
1837
  pauseUpload: useCallback(() => {
1573
1838
  session.current?.gate.pause();
1574
1839
  setUpload((current) => current ? {
@@ -1591,6 +1856,26 @@ function useWorkflowSubmit(workflow, opts = {}) {
1591
1856
  }
1592
1857
  //#endregion
1593
1858
  //#region components/workflow-fields.tsx
1859
+ /** @jsxImportSource react */
1860
+ /**
1861
+ * A form built from a workflow's declared input schema.
1862
+ *
1863
+ * `GET workflows` reports each workflow's `inputSchema` as JSON Schema — the
1864
+ * zod schema an author wrote in `agent.ts`, converted at listing time precisely
1865
+ * so a browser can read it. This is what reads it: one `<WorkflowFields>` and a
1866
+ * workflow's form matches its schema by construction, so adding a field to the
1867
+ * schema adds it to the page and nothing can drift.
1868
+ *
1869
+ * ## It covers SCALARS, and says so rather than guessing
1870
+ *
1871
+ * A string, number, integer, boolean or enum has one obvious control each. A
1872
+ * nested object or an array does not — every choice (a JSON textarea, a repeater,
1873
+ * a comma-separated string) is a guess about what the author meant, and a guess
1874
+ * that produces a value the schema then rejects is worse than no field at all.
1875
+ * So those are SKIPPED, and the fields for them are written by hand: every field
1876
+ * in this package is a plain named control, so a hand-written one composes with
1877
+ * a generated one inside the same {@link Form}.
1878
+ */
1594
1879
  /**
1595
1880
  * Render one field per scalar property of a workflow's input schema.
1596
1881
  *
@@ -1622,6 +1907,8 @@ function useWorkflowSubmit(workflow, opts = {}) {
1622
1907
  * }
1623
1908
  * ```
1624
1909
  *
1910
+ * @param props - Field-set props.
1911
+ *
1625
1912
  * @public
1626
1913
  */
1627
1914
  function WorkflowFields({ workflow }) {
@@ -1946,24 +2233,17 @@ function useWorkflowProgress(runId, opts = {}) {
1946
2233
  * }
1947
2234
  * ```
1948
2235
  *
1949
- * @param runId - The run to read. `undefined` renders nothing, so a page may
1950
- * pass its state straight through before a run exists.
1951
- * @param api - The workflow API client, when the page holds its own. Defaults
1952
- * to the one `page()` installs.
1953
- * @param className - Replaces the default classes rather than extending them,
1954
- * so a custom chrome is not fighting a default it did not ask for.
1955
- * @param placeholder - Rendered instead of nothing while the run has said
1956
- * nothing yet — for a page that would otherwise reflow when the first line
1957
- * lands.
2236
+ * @param props - Progress-log props.
1958
2237
  *
1959
2238
  * @public
1960
2239
  */
1961
- function WorkflowProgress({ runId, api, className, placeholder }) {
2240
+ function WorkflowProgress({ runId, api, className, placeholder, lines }) {
1962
2241
  const { progress, streaming, supported } = useWorkflowProgress(runId, omitUndefined({ api }));
1963
- if (!supported || progress.length === 0) return placeholder ?? null;
2242
+ const shown = lines === void 0 ? progress : progress.slice(Math.max(progress.length - lines, 0));
2243
+ if (!supported || shown.length === 0) return placeholder ?? null;
1964
2244
  return /* @__PURE__ */ jsxs("pre", {
1965
2245
  className: clsx(className ?? "whitespace-pre-wrap border-l pl-4 text-xs opacity-70"),
1966
- children: [progress.join("\n"), streaming && "\n…"]
2246
+ children: [shown.join("\n"), streaming && "\n…"]
1967
2247
  });
1968
2248
  }
1969
2249
  //#endregion
@@ -2027,69 +2307,94 @@ function page(config) {
2027
2307
  return mountRoot(container, createElement(ThemeProvider, { value: config.theme }, createElement(config.component)));
2028
2308
  }
2029
2309
  //#endregion
2030
- //#region use-user-transcript.ts
2031
- /**
2032
- * `useUserTranscript` — what the caller is saying RIGHT NOW, read correctly.
2033
- *
2034
- * `SessionSnapshot.userTranscript` is `string | null`, and the two falsy values
2035
- * mean different things:
2036
- *
2037
- * - `null` — nobody is speaking. There is no partial turn.
2038
- * - `""` — speech HAS been detected and no words have come back yet. A live
2039
- * session sits here for a few hundred milliseconds at the start of every turn.
2040
- *
2041
- * Read as one falsy check, those collapse and the indicator never appears at the
2042
- * start of a turn — which is the moment it is for. So every custom chrome writes
2043
- * `transcript !== null && (transcript === "" ? "…" : transcript)`, three
2044
- * templates did exactly that, and each one re-derived a protocol distinction
2045
- * from the type rather than from anything that told them.
2046
- *
2047
- * This is the same distinction as two named booleans, so a component can say
2048
- * what it means: render on `speaking`, show `text`, and use the SDK's own
2049
- * placeholder when there is nothing to show yet.
2050
- */
2051
- /**
2052
- * Placeholder for "listening, no words yet" — the `""` case above.
2053
- *
2054
- * A one-character ellipsis rather than three dots, because it is read by a
2055
- * screen reader as an ellipsis and it does not reflow the row when the first
2056
- * real word replaces it.
2057
- *
2058
- * @public
2059
- */
2060
- const TRANSCRIBING_PLACEHOLDER = "…";
2061
- /**
2062
- * Subscribe to the caller's in-progress turn.
2063
- *
2064
- * Narrowly subscribed — a component using this re-renders at STT-partial rate,
2065
- * which is exactly what it is for and exactly what a whole-page `useSession()`
2066
- * should not do.
2310
+ //#region use-download-url.ts
2311
+ /**
2312
+ * `useDownloadUrl` — an upload id a run produced, as something `<audio>`,
2313
+ * `<img>` or `<a download>` will accept.
2314
+ *
2315
+ * `api.download(id)` resolves a `Blob`, and it has to: the byte route takes the
2316
+ * same bearer every workflow route does, and neither `<audio src>` nor
2317
+ * `<a href>` can send one. So every page that plays back what a run WROTE ends
2318
+ * up at the same four lines — `download` → `createObjectURL` → state — and the
2319
+ * two that are really the point are the two the four lines are wrapped in:
2320
+ *
2321
+ * - **`URL.revokeObjectURL` on cleanup.** An object URL pins its blob for the
2322
+ * life of the DOCUMENT. Miss it and every completed run's audio stays resident
2323
+ * until the tab closes, which on a page people run all day is a leak measured
2324
+ * in the size of the files.
2325
+ * - **A `cancelled` flag.** A second run settling while the first download is
2326
+ * still in flight otherwise sets state from the stale one, and the page plays
2327
+ * the previous run's audio under the current run's transcript — a wrong answer
2328
+ * that looks like a right one.
2329
+ *
2330
+ * Two templates had written this hook, identically, doc paragraph included, and
2331
+ * `aai-ui` exported no download helper at all. Both also faked `pending` by
2332
+ * checking `url === undefined && error === undefined`, which reads "idle" and
2333
+ * "downloading" as the same thing — so this reports it.
2334
+ */
2335
+ /** No id: nothing pending, nothing to show. A shared object so `setState` no-ops. */
2336
+ const IDLE = { pending: false };
2337
+ /** Bytes in flight. Shared for the same reason as {@link IDLE}. */
2338
+ const PENDING = { pending: true };
2339
+ /**
2340
+ * Read an upload's bytes and hand back a URL a DOM element can use.
2341
+ *
2342
+ * @param uploadId - The id a completed run reported, or `undefined` before one
2343
+ * exists — which is what a page passes straight through while it waits, and
2344
+ * reports as idle rather than pending.
2345
+ * @param opts - See {@link UseDownloadUrlOptions}.
2346
+ * @returns See {@link UseDownloadUrlResult}.
2067
2347
  *
2068
2348
  * @example
2069
2349
  * ```tsx
2070
- * import { useUserTranscript } from "@alexkroman1/aai-ui";
2071
- *
2072
- * function LiveTranscript() {
2073
- * const { speaking, text } = useUserTranscript();
2074
- * if (!speaking) return null;
2075
- * return <div className="italic opacity-60">{text}</div>;
2350
+ * import { useDownloadUrl, useWorkflowSubmit } from "@alexkroman1/aai-ui";
2351
+ *
2352
+ * function Playback() {
2353
+ * const { run } = useWorkflowSubmit<{ audio: string }>("spokenSummary");
2354
+ * const output = run?.status === "completed" ? run.output : undefined;
2355
+ * const audio = useDownloadUrl(output?.audio);
2356
+ * if (audio.pending) return <p>Fetching audio…</p>;
2357
+ * if (audio.error !== undefined) return <p role="alert">{audio.error}</p>;
2358
+ * return audio.url === undefined ? null : (
2359
+ * <a href={audio.url} download="summary.mp3">
2360
+ * Download
2361
+ * </a>
2362
+ * );
2076
2363
  * }
2077
2364
  * ```
2078
2365
  *
2079
2366
  * @public
2080
2367
  */
2081
- function useUserTranscript() {
2082
- const partial = useSessionSelector((snapshot) => snapshot.userTranscript);
2083
- return {
2084
- speaking: partial !== null,
2085
- text: displayText(partial),
2086
- partial
2087
- };
2088
- }
2089
- /** The three cases, spelled out: silent, detected-but-wordless, and words. */
2090
- function displayText(partial) {
2091
- if (partial === null) return "";
2092
- return partial === "" ? "…" : partial;
2368
+ function useDownloadUrl(uploadId, opts = {}) {
2369
+ const [state, setState] = useState(IDLE);
2370
+ const getClient = useWorkflowApiRef(opts.api);
2371
+ useEffect(() => {
2372
+ if (uploadId === void 0) {
2373
+ setState(IDLE);
2374
+ return;
2375
+ }
2376
+ let cancelled = false;
2377
+ let objectUrl;
2378
+ setState(PENDING);
2379
+ getClient().download(uploadId).then((blob) => {
2380
+ if (cancelled) return;
2381
+ objectUrl = URL.createObjectURL(blob);
2382
+ setState({
2383
+ url: objectUrl,
2384
+ pending: false
2385
+ });
2386
+ }).catch((err) => {
2387
+ if (!cancelled) setState({
2388
+ error: errorMessage(err),
2389
+ pending: false
2390
+ });
2391
+ });
2392
+ return () => {
2393
+ cancelled = true;
2394
+ if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
2395
+ };
2396
+ }, [uploadId, getClient]);
2397
+ return state;
2093
2398
  }
2094
2399
  //#endregion
2095
2400
  //#region use-workflow-runs.ts
@@ -2306,6 +2611,7 @@ function useWorkflowStream(workflow, opts = {}) {
2306
2611
  api,
2307
2612
  intervalMs
2308
2613
  }));
2614
+ const { wake, cancel } = useRunControls(runId, getClient);
2309
2615
  return {
2310
2616
  submit: useCallback(async (input) => {
2311
2617
  const client = getClient();
@@ -2361,6 +2667,8 @@ function useWorkflowStream(workflow, opts = {}) {
2361
2667
  setStartError(void 0);
2362
2668
  setUpload(void 0);
2363
2669
  }, []),
2670
+ wake,
2671
+ cancel,
2364
2672
  pauseUpload: useCallback(() => {
2365
2673
  gateRef.current?.pause();
2366
2674
  setUpload((current) => current ? {
@@ -2474,4 +2782,30 @@ function fileAt(input, field) {
2474
2782
  return filesOf(input[field])[0];
2475
2783
  }
2476
2784
  //#endregion
2477
- export { ApiUrlChip, AutoScroll, Button, ChatView, CheckboxField, Controls, DEFAULT_PROGRESS_POLL_MS, DEFAULT_WORKFLOW_POLL_MS, Field, FileField, Form, MAX_MISSING_READS, Markdown, MessageList, NumberField, SelectField, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, SubmitButton, TRANSCRIBING_PLACEHOLDER, TextAreaField, TextField, ThemeProvider, ToolCallRow, ToolConfigContext, UiUrlChip, UploadProgressBar, VOICE_CAPTURE_CONSTRAINTS, WorkflowFields, WorkflowProgress, buildAgentUrl, client, createSessionCore, createWorkflowApi, fetchClientConfig, isTerminal, loadClientConfig, page, useAgentState, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult, useUserTranscript, useWorkflowProgress, useWorkflowRun, useWorkflowRuns, useWorkflowStream, useWorkflowSubmit, useWorkflows };
2785
+ //#region workflow-status-labels.ts
2786
+ /**
2787
+ * The default status line per {@link WorkflowRunStatus}.
2788
+ *
2789
+ * Override the ones your page has a better word for and keep the rest:
2790
+ *
2791
+ * ```ts
2792
+ * import { WORKFLOW_STATUS_LABELS } from "@alexkroman1/aai-ui";
2793
+ *
2794
+ * const STATUS_LINE = { ...WORKFLOW_STATUS_LABELS, running: "Writing…" };
2795
+ * ```
2796
+ *
2797
+ * The wording is deliberately about the RUN rather than about the work — a page
2798
+ * knows what its workflow does and this does not, so `running` is the neutral
2799
+ * "Working…" and every page that cares replaces exactly that key.
2800
+ *
2801
+ * @public
2802
+ */
2803
+ const WORKFLOW_STATUS_LABELS = {
2804
+ pending: "Queued",
2805
+ running: "Working…",
2806
+ completed: "Done",
2807
+ failed: "Failed",
2808
+ cancelled: "Cancelled"
2809
+ };
2810
+ //#endregion
2811
+ export { AutoScroll, Button, ChatView, CheckboxField, ConsoleShell, Controls, DEFAULT_PROGRESS_POLL_MS, DEFAULT_WORKFLOW_POLL_MS, Field, FileField, Form, MAX_MISSING_READS, Markdown, MessageList, NumberField, SelectField, SidebarLayout, StartScreen, SubmitButton, TRANSCRIBING_PLACEHOLDER, TextAreaField, TextField, ToolCallRow, UploadProgressBar, WORKFLOW_STATUS_LABELS, WorkflowFields, WorkflowProgress, client, createSessionCore, createWorkflowApi, fetchClientConfig, isTerminal, page, useAgentState, useConversation, useDownloadUrl, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult, useUserTranscript, useWorkflowProgress, useWorkflowRun, useWorkflowRuns, useWorkflowStream, useWorkflowSubmit, useWorkflows };