@alexkroman1/aai-ui 6.11.0 → 8.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 (71) hide show
  1. package/README.md +104 -3
  2. package/dist/{_colors-CcAi2FOU.js → _colors-j8XMToi9.js} +13 -2
  3. package/dist/_run-controls.d.ts +33 -0
  4. package/dist/_upload-pause.d.ts +31 -0
  5. package/dist/{chat-view-ByQFf94G.js → chat-view-BJ6sOARu.js} +58 -12
  6. package/dist/client-config-BT_kWID5.js +134 -0
  7. package/dist/client-config.d.ts +45 -2
  8. package/dist/client-dir.d.ts +3 -1
  9. package/dist/client-dir.js +3 -1
  10. package/dist/components/auto-scroll.d.ts +24 -13
  11. package/dist/components/button.d.ts +8 -4
  12. package/dist/components/button.js +5 -5
  13. package/dist/components/chat-view.d.ts +4 -3
  14. package/dist/components/chat-view.js +1 -1
  15. package/dist/components/console-shell.d.ts +71 -10
  16. package/dist/components/controls.d.ts +15 -4
  17. package/dist/components/controls.js +48 -2
  18. package/dist/components/form-fields.d.ts +142 -0
  19. package/dist/components/form-types.d.ts +6 -0
  20. package/dist/components/form.d.ts +38 -80
  21. package/dist/components/markdown.d.ts +31 -5
  22. package/dist/components/message-list.d.ts +21 -4
  23. package/dist/components/message-list.js +1 -1
  24. package/dist/components/sidebar-layout.d.ts +11 -0
  25. package/dist/components/sidebar-layout.js +2 -0
  26. package/dist/components/start-screen.d.ts +8 -0
  27. package/dist/components/start-screen.js +3 -1
  28. package/dist/components/tool-call-block.js +1 -1
  29. package/dist/components/tool-call-row.d.ts +24 -0
  30. package/dist/components/upload-progress.d.ts +17 -7
  31. package/dist/components/workflow-fields.d.ts +10 -22
  32. package/dist/components/workflow-progress.d.ts +29 -10
  33. package/dist/context.d.ts +36 -0
  34. package/dist/context.js +105 -14
  35. package/dist/default-client/assets/index-Du8SVFin.js +299 -0
  36. package/dist/default-client/assets/index-S5fkKi6B.css +2 -0
  37. package/dist/default-client/index.html +2 -2
  38. package/dist/define-client.d.ts +67 -62
  39. package/dist/define-client.js +56 -22
  40. package/dist/hooks.d.ts +88 -3
  41. package/dist/index.d.ts +15 -14
  42. package/dist/index.js +548 -819
  43. package/dist/internal.d.ts +43 -0
  44. package/dist/internal.js +8 -0
  45. package/dist/{message-list-CpPV7dGx.js → message-list-Dtl03sGr.js} +154 -73
  46. package/dist/{session-core-CAfYmUbg.js → session-core-CMEpwRP1.js} +269 -167
  47. package/dist/session-core-audio-setup.d.ts +5 -2
  48. package/dist/session-core-messages.d.ts +4 -1
  49. package/dist/session-core-state.d.ts +146 -0
  50. package/dist/session-core-types.d.ts +77 -32
  51. package/dist/session-core.d.ts +3 -2
  52. package/dist/session-core.js +1 -1
  53. package/dist/{tool-call-block-D6pTEPrT.js → tool-call-block-B7KlhaWl.js} +28 -20
  54. package/dist/tool-config-context-DzAofqi_.js +19 -0
  55. package/dist/types.d.ts +31 -5
  56. package/dist/types.js +5 -4
  57. package/dist/{controls-CjG91QJ4.js → url-chips-DxXKgyL9.js} +4 -47
  58. package/dist/use-conversation.d.ts +122 -0
  59. package/dist/use-download-url.d.ts +83 -0
  60. package/dist/use-user-transcript-C14qWFu2.js +72 -0
  61. package/dist/use-user-transcript.d.ts +7 -2
  62. package/dist/use-workflow-form.d.ts +59 -3
  63. package/dist/use-workflow-progress-C4KgVldr.js +610 -0
  64. package/dist/use-workflow-run.d.ts +35 -0
  65. package/dist/use-workflow-stream.d.ts +36 -62
  66. package/dist/workflow-client.d.ts +36 -11
  67. package/dist/workflow-status-labels.d.ts +35 -0
  68. package/package.json +10 -5
  69. package/styles.css +14 -0
  70. package/dist/default-client/assets/index-DTLrhtTF.css +0 -2
  71. package/dist/default-client/assets/index-DXODx_9r.js +0 -293
package/dist/index.js CHANGED
@@ -1,23 +1,24 @@
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";
4
- import { r as inkTint } from "./_colors-CcAi2FOU.js";
1
+ import { n as fetchClientConfig } from "./client-config-BT_kWID5.js";
2
+ import { i as AutoScroll, n as Markdown, r as useConversation, t as MessageList } from "./message-list-Dtl03sGr.js";
3
+ import { ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
4
+ import { r as inkTint } from "./_colors-j8XMToi9.js";
5
5
  import { Button } from "./components/button.js";
6
- import { t as ChatView } from "./chat-view-ByQFf94G.js";
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";
6
+ import { n as ConsoleShell, t as ChatView } from "./chat-view-BJ6sOARu.js";
7
+ import { n as setPageTitle } from "./_utils-B6498_bm.js";
8
+ import { Controls } from "./components/controls.js";
9
+ import { n as useUserTranscript } from "./use-user-transcript-C14qWFu2.js";
10
+ import { n as ToolCallRow } from "./tool-call-block-B7KlhaWl.js";
10
11
  import { SidebarLayout } from "./components/sidebar-layout.js";
11
12
  import { StartScreen } from "./components/start-screen.js";
12
- import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
13
+ import { a as useWorkflowRun, c as isTerminal, n as useWorkflowProgress, o as useWorkflowApiRef, s as createWorkflowApi } from "./use-workflow-progress-C4KgVldr.js";
14
+ import { t as createSessionCore } from "./session-core-CMEpwRP1.js";
13
15
  import { client, mountRoot, resolveContainer } from "./define-client.js";
14
16
  import { useAgentState, useEvent, useToolCallStart, useToolResult } from "./hooks.js";
15
17
  import clsx from "clsx";
16
18
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
17
19
  import { createContext, createElement, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
18
- import { errorMessage, isTerminal, isTerminal as isTerminal$1 } from "@alexkroman1/aai";
19
- import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
20
- import { createWorkflowApiClient, readEventStream } from "@alexkroman1/aai/workflow-api";
20
+ import { errorMessage } from "@alexkroman1/aai";
21
+ import { formatBytes, isRecord, omitUndefined } from "@alexkroman1/aai/utils";
21
22
  import { createEpoch } from "@alexkroman1/aai/internal";
22
23
  //#region components/_form-readiness.ts
23
24
  /**
@@ -200,104 +201,52 @@ function dataUrl(file) {
200
201
  });
201
202
  }
202
203
  //#endregion
203
- //#region components/form.tsx
204
+ //#region components/form-fields.tsx
204
205
  /** @jsxImportSource react */
205
206
  /**
206
- * Simple forms, for the pages that are not conversations.
207
+ * The controls a form is made of — the shell, the six fields, and the styling
208
+ * they share.
207
209
  *
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.
210
+ * Split out of `form.tsx` when that file crossed the repo's source-file line
211
+ * cap; `form.tsx` re-exports every name here, so it stays the one import path
212
+ * for everything a form needs and nothing a caller writes changed.
213
213
  *
214
- * ## Values come off the DOM, not out of React state
214
+ * ## Every field takes its element's own attributes
215
215
  *
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.
216
+ * Each control is `FieldShell & Omit<…HTMLAttributes, "name" | "className">`,
217
+ * so `required`, `placeholder`, `accept`, `min`/`max`, `aria-*` and the rest
218
+ * pass straight through to the element. `name` and `className` are owned here:
219
+ * `name` is the key the field contributes to {@link FormValues} and is put on
220
+ * the control itself, and `className` styles the field's WRAPPER, the control
221
+ * taking the shared styling below.
221
222
  *
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"`.
228
- *
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.
223
+ * Note this means the per-property doc comments on the intersections do not
224
+ * reach the published reference — see "A memoized component must NAME its
225
+ * props type" in `packages/aai-ui/CLAUDE.md` — which is why each component's
226
+ * own description carries what a caller needs.
234
227
  */
235
228
  /**
236
- * A form that hands its values to `onSubmit` as one object.
229
+ * Label + control + hint, in the layout every field here uses.
237
230
  *
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.
231
+ * Exported so a caller's own control gets the same shell rather than an
232
+ * approximation of it.
240
233
  *
241
234
  * @example
242
235
  * ```tsx
243
- * import { Form, SubmitButton, TextField } from "@alexkroman1/aai-ui";
236
+ * import { Field, Form } from "@alexkroman1/aai-ui";
244
237
  *
245
- * function NameForm() {
238
+ * function ColorForm() {
246
239
  * return (
247
- * <Form onSubmit={(values) => console.log(values.topic)}>
248
- * <TextField name="topic" label="Topic" required />
249
- * <SubmitButton>Start</SubmitButton>
240
+ * <Form onSubmit={() => undefined}>
241
+ * <Field label="Accent" hint="Any CSS color." htmlFor="accent">
242
+ * <input id="accent" name="accent" type="color" />
243
+ * </Field>
250
244
  * </Form>
251
245
  * );
252
246
  * }
253
247
  * ```
254
248
  *
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.
249
+ * @param props - Field-shell props.
301
250
  *
302
251
  * @public
303
252
  */
@@ -364,6 +313,11 @@ function useFileControlProps() {
364
313
  /**
365
314
  * A single-line text input.
366
315
  *
316
+ * Accepts every `<input>` attribute except `name` and `className`, which this
317
+ * component owns, plus the shared {@link FieldShell} props.
318
+ *
319
+ * @param props - {@link FieldShell} props plus `<input>` attributes.
320
+ *
367
321
  * @public
368
322
  */
369
323
  function TextField({ name, label, hint, className, ...rest }) {
@@ -386,6 +340,12 @@ function TextField({ name, label, hint, className, ...rest }) {
386
340
  * A number input. Contributes a NUMBER to {@link FormValues}, or nothing when
387
341
  * left empty.
388
342
  *
343
+ * Accepts every `<input>` attribute except `name`, `className` and `type`,
344
+ * plus the shared {@link FieldShell} props — so `min`, `max` and `step` are
345
+ * passed straight through.
346
+ *
347
+ * @param props - {@link FieldShell} props plus `<input>` attributes.
348
+ *
389
349
  * @public
390
350
  */
391
351
  function NumberField({ name, label, hint, className, ...rest }) {
@@ -407,6 +367,11 @@ function NumberField({ name, label, hint, className, ...rest }) {
407
367
  /**
408
368
  * A multi-line text input.
409
369
  *
370
+ * Accepts every `<textarea>` attribute except `name` and `className`, plus the
371
+ * shared {@link FieldShell} props. `rows` defaults to 4.
372
+ *
373
+ * @param props - {@link FieldShell} props plus `<textarea>` attributes.
374
+ *
410
375
  * @public
411
376
  */
412
377
  function TextAreaField({ name, label, hint, className, rows = 4, ...rest }) {
@@ -426,8 +391,19 @@ function TextAreaField({ name, label, hint, className, rows = 4, ...rest }) {
426
391
  });
427
392
  }
428
393
  /**
429
- * A dropdown. Pass `options`, or `children` for full control over the
430
- * `<option>` elements.
394
+ * A dropdown.
395
+ *
396
+ * `options` is the short form — a list of strings, or of
397
+ * `{ value, label }` pairs when the two differ. Pass `children` instead for
398
+ * full control over the `<option>` elements; `children` wins when both are
399
+ * given.
400
+ *
401
+ * Otherwise accepts every `<select>` attribute except `name` and `className`,
402
+ * plus the shared {@link FieldShell} props. Note `multiple` works and
403
+ * contributes an ARRAY (`[]` when nothing is chosen).
404
+ *
405
+ * @param props - {@link FieldShell} props, `options`, and `<select>`
406
+ * attributes.
431
407
  *
432
408
  * @public
433
409
  */
@@ -456,6 +432,12 @@ function SelectField({ name, label, hint, className, options, children, ...rest
456
432
  /**
457
433
  * A checkbox. Contributes a BOOLEAN to {@link FormValues}.
458
434
  *
435
+ * Accepts every `<input>` attribute except `name`, `className` and `type`,
436
+ * plus the shared {@link FieldShell} props. The label renders beside the box
437
+ * rather than above it, so `hint` is the place for guidance.
438
+ *
439
+ * @param props - {@link FieldShell} props plus `<input>` attributes.
440
+ *
459
441
  * @public
460
442
  */
461
443
  function CheckboxField({ name, label, hint, className, ...rest }) {
@@ -493,7 +475,16 @@ function CheckboxField({ name, label, hint, className, ...rest }) {
493
475
  *
494
476
  * **Without it the field describes the file and does not read it.** `read`
495
477
  * 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.
478
+ * input — a CSV of ids, a config — and the size is the author's to check. See
479
+ * {@link FileRead} for the four values; `upload` is shorthand for
480
+ * `read="upload"`.
481
+ *
482
+ * Otherwise accepts every `<input>` attribute except `name`, `className` and
483
+ * `type`, plus the shared {@link FieldShell} props — so `accept` and
484
+ * `multiple` are passed straight through.
485
+ *
486
+ * @param props - {@link FieldShell} props, `read`/`upload`, and `<input>`
487
+ * attributes.
497
488
  *
498
489
  * @public
499
490
  */
@@ -515,20 +506,144 @@ function FileField({ name, label, hint, className, read = "none", upload = false
515
506
  })
516
507
  });
517
508
  }
509
+ //#endregion
510
+ //#region components/form.tsx
511
+ /** @jsxImportSource react */
512
+ /**
513
+ * Simple forms, for the pages that are not conversations.
514
+ *
515
+ * A workflow app's front door is a form: name a recording, upload a file, press
516
+ * submit. Nothing here knew how to render one — the components in this package
517
+ * are all about a live session (a transcript, a mic button, a tool-call row) —
518
+ * so every such page started by hand-rolling labels, inputs, a submit button and
519
+ * the value collection between them, and did it differently each time.
520
+ *
521
+ * ## Values come off the DOM, not out of React state
522
+ *
523
+ * {@link Form} reads its own `<form>` element on submit and builds one plain
524
+ * object from the named controls. That is what makes a field here nothing more
525
+ * than a styled `<input>`: no registration, no controlled-component ceremony,
526
+ * and a plain `<input name="x">` a caller writes themselves works exactly like
527
+ * the ones below.
528
+ *
529
+ * It also means the values are TYPED rather than all-strings, which a
530
+ * `new FormData(form)` cannot give: a number field yields a number, a checkbox a
531
+ * boolean, an empty optional field nothing at all. That matters because these
532
+ * values go straight into a workflow's input, where a zod schema is waiting —
533
+ * `"3"` against `z.number()` is a rejected run, and the browser is the only
534
+ * place that still knows the field was `type="number"`.
535
+ *
536
+ * ## A file field either describes a file or uploads it
537
+ *
538
+ * See {@link FileField}. A workflow input is journaled and replayed on every
539
+ * resume, so file BYTES do not belong in it — `upload` is the field that sends
540
+ * them somewhere a step can read them and contributes the handle instead.
541
+ */
542
+ /**
543
+ * A form that hands its values to `onSubmit` as one object.
544
+ *
545
+ * Native validation still applies — a `required` field blocks the submit and the
546
+ * browser says so, which is better than anything this could render.
547
+ *
548
+ * @example
549
+ * ```tsx
550
+ * import { Form, SubmitButton, TextField } from "@alexkroman1/aai-ui";
551
+ *
552
+ * function NameForm() {
553
+ * return (
554
+ * <Form onSubmit={(values) => console.log(values.topic)}>
555
+ * <TextField name="topic" label="Topic" required />
556
+ * <SubmitButton>Start</SubmitButton>
557
+ * </Form>
558
+ * );
559
+ * }
560
+ * ```
561
+ *
562
+ * @param props - See {@link FormProps}. Every `<form>` attribute except
563
+ * `onSubmit` and `className` is passed through.
564
+ *
565
+ * @public
566
+ */
567
+ function Form({ onSubmit, error, children, className, ...rest }) {
568
+ const theme = useTheme();
569
+ const [busy, setBusy] = useState(false);
570
+ const { pending: fieldsPending, declare } = useFormReadiness();
571
+ return /* @__PURE__ */ jsxs("form", {
572
+ onSubmit: useCallback((event) => {
573
+ event.preventDefault();
574
+ if (busy || fieldsPending) return;
575
+ const form = event.currentTarget;
576
+ setBusy(true);
577
+ (async () => {
578
+ try {
579
+ await onSubmit(await collectValues(form));
580
+ } finally {
581
+ setBusy(false);
582
+ }
583
+ })();
584
+ }, [
585
+ busy,
586
+ fieldsPending,
587
+ onSubmit
588
+ ]),
589
+ className: clsx("flex flex-col gap-5 font-aai", className),
590
+ ...rest,
591
+ children: [/* @__PURE__ */ jsx("fieldset", {
592
+ disabled: busy || fieldsPending,
593
+ className: "contents",
594
+ children: /* @__PURE__ */ jsx(FormReadinessProvider, {
595
+ value: declare,
596
+ children
597
+ })
598
+ }), error !== void 0 && error !== "" && /* @__PURE__ */ jsx("p", {
599
+ role: "alert",
600
+ className: "text-sm",
601
+ style: { color: theme.primary },
602
+ children: error
603
+ })]
604
+ });
605
+ }
518
606
  /**
519
607
  * The form's submit button, disabled and relabelled while a submit is in
520
608
  * flight.
521
609
  *
610
+ * Accepts all standard `<button>` HTML attributes except `type` and `disabled`,
611
+ * in addition to the props below — so `aria-label` on an icon-only submit,
612
+ * `form`, `id`, `title` and `onClick` all work here exactly as they do on
613
+ * {@link Button}. `type` and `disabled` stay owned: this component sets both
614
+ * from `pending`, and letting a caller set either is how a form gets a submit
615
+ * button that does not submit.
616
+ *
617
+ * @example
618
+ * ```tsx
619
+ * import { Form, SubmitButton, TextField } from "@alexkroman1/aai-ui";
620
+ *
621
+ * function Digest({ pending }: { pending: boolean }) {
622
+ * return (
623
+ * <Form onSubmit={() => undefined}>
624
+ * <TextField name="url" label="Link" required />
625
+ * <SubmitButton pending={pending} variant="secondary" size="lg">
626
+ * Summarize
627
+ * </SubmitButton>
628
+ * </Form>
629
+ * );
630
+ * }
631
+ * ```
632
+ *
633
+ * @param props - Button props.
634
+ *
522
635
  * @public
523
636
  */
524
- function SubmitButton({ children, pending = false, pendingLabel = "Working…", size, className }) {
637
+ function SubmitButton({ children, pending = false, pendingLabel = "Working…", size, variant, className, ...rest }) {
525
638
  return /* @__PURE__ */ jsx(Button, {
526
639
  type: "submit",
527
640
  disabled: pending,
528
641
  ...omitUndefined({
529
642
  size,
643
+ variant,
530
644
  className
531
645
  }),
646
+ ...rest,
532
647
  children: pending ? pendingLabel : children
533
648
  });
534
649
  }
@@ -543,30 +658,6 @@ function SubmitButton({ children, pending = false, pendingLabel = "Working…",
543
658
  * as a groove against the page.
544
659
  */
545
660
  const TRACK_TINT_PCT = 12;
546
- /** Bytes per KB/MB/GB step, as a file manager counts them. */
547
- const BYTE_STEP = 1024;
548
- /** Unit labels, largest last — the index is how many times the size divides. */
549
- const BYTE_UNITS = [
550
- "B",
551
- "KB",
552
- "MB",
553
- "GB"
554
- ];
555
- /**
556
- * A size a person can read, because the number that matters is the SCALE.
557
- *
558
- * `12.4 MB of 48.0 MB` says how much is left in the terms someone chose the
559
- * file in; `13002343 of 50331648` says the same thing and answers nothing.
560
- */
561
- function formatBytes(bytes) {
562
- let value = bytes;
563
- let unit = 0;
564
- while (value >= BYTE_STEP && unit < BYTE_UNITS.length - 1) {
565
- value /= BYTE_STEP;
566
- unit += 1;
567
- }
568
- return `${unit === 0 ? value : value.toFixed(1)} ${BYTE_UNITS[unit]}`;
569
- }
570
661
  /**
571
662
  * How far a form's files have got, rendered as a bar.
572
663
  *
@@ -614,13 +705,7 @@ function formatBytes(bytes) {
614
705
  * }
615
706
  * ```
616
707
  *
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.
708
+ * @param props - Progress-bar props.
624
709
  *
625
710
  * @public
626
711
  */
@@ -674,6 +759,44 @@ function UploadProgressBar({ upload, onPause, onResume, className }) {
674
759
  });
675
760
  }
676
761
  //#endregion
762
+ //#region _run-controls.ts
763
+ /**
764
+ * The two things a page does TO a run it started, bound to the run it has.
765
+ *
766
+ * `useWorkflowSubmit` and `useWorkflowStream` both hold a run id and neither
767
+ * handed it back, so a page that wanted "send it now" or "stop" had to hold an
768
+ * `api` of its own purely to write `api.wake(runId)` — which is the whole reason
769
+ * the two raw-primitive template pages keep a client at module scope. That is a
770
+ * page carrying the transport to make up for a hook withholding its own state.
771
+ *
772
+ * Both calls answer rather than fail when there is nothing to act on — `0`
773
+ * sleeps ended, `false` this call did not end it — which is the SDK's own
774
+ * contract for them (two tabs pressing Stop is ordinary), and it is what lets
775
+ * the no-run case be the same answer rather than a special one a caller has to
776
+ * branch on.
777
+ */
778
+ /**
779
+ * Bind `wake` and `cancel` to whatever run the hook is currently following.
780
+ *
781
+ * @param runId - The live run, or `undefined` before one exists.
782
+ * @param getClient - The stable getter from `useWorkflowApiRef`.
783
+ * @returns Two callbacks, stable while `runId` is.
784
+ *
785
+ * @internal
786
+ */
787
+ function useRunControls(runId, getClient) {
788
+ return {
789
+ wake: useCallback(async () => {
790
+ if (runId === void 0) return 0;
791
+ return await getClient().wake(runId);
792
+ }, [runId, getClient]),
793
+ cancel: useCallback(async () => {
794
+ if (runId === void 0) return false;
795
+ return await getClient().cancel(runId);
796
+ }, [runId, getClient])
797
+ };
798
+ }
799
+ //#endregion
677
800
  //#region _upload-recall.ts
678
801
  /**
679
802
  * Where an upload's ID survives a page RELOAD.
@@ -1099,318 +1222,43 @@ async function uploadFiles(api, input, report, parallel, session) {
1099
1222
  return out;
1100
1223
  }
1101
1224
  //#endregion
1102
- //#region workflow-client.ts
1225
+ //#region _upload-pause.ts
1103
1226
  /**
1104
- * Create a workflow API client aimed at the agent serving this page.
1105
- *
1106
- * Hoist it out of the component that uses it. `useWorkflowRun` holds the client
1107
- * in a ref precisely so a fresh object per render does not restart its watch,
1108
- * but a client built in render is still a new `fetch` closure every time and
1109
- * reads as though it were free.
1227
+ * The two buttons a page puts over a live upload, bound to whichever gate the
1228
+ * submission is currently holding.
1110
1229
  *
1111
- * @public
1230
+ * `useWorkflowSubmit` and `useWorkflowStream` both own an {@link UploadGate} and
1231
+ * both reported the pause the same way: park the gate, then fold `paused` into
1232
+ * the status the bar is already drawing. Folded rather than replaced, because
1233
+ * everything else about that status — which file, how far, of how many — is
1234
+ * still true. Two copies of that rule are two copies that can stop agreeing
1235
+ * about what a paused bar says.
1112
1236
  */
1113
- function createWorkflowApi(opts = {}) {
1114
- return createWorkflowApiClient({
1115
- baseUrl: opts.baseUrl ?? pageBaseUrl(),
1116
- ...omitUndefined({ token: opts.token })
1117
- });
1118
- }
1119
- //#endregion
1120
- //#region _workflow-api-ref.ts
1121
- /**
1122
- * The client preamble every workflow hook needs, once.
1123
- *
1124
- * Five hooks (`useWorkflowRun`, `useWorkflowProgress`, `useWorkflowRuns`,
1125
- * `useWorkflows`, `useWorkflowSubmit`) opened with the same two refs and the
1126
- * same two paragraphs explaining them, and both halves are load-bearing rather
1127
- * than stylistic — which is exactly why they should not be re-derived per hook:
1128
- *
1129
- * - **The caller's client lives in a REF, never in an effect's dependency
1130
- * array.** The natural call site is
1131
- * `useWorkflowRun(id, { api: createWorkflowApi() })`, which passes a NEW
1132
- * object every render; as a dependency that tears the effect down and
1133
- * restarts it on each one, and because a restart clears state and re-renders,
1134
- * it schedules the next. The result is an unbounded request loop against the
1135
- * agent — on the platform, against the BROKER — with `error` wiped before
1136
- * anything can read it, presenting as "the page polls forever" rather than as
1137
- * a mistake at the call site.
1138
- * - **The no-client default is built lazily and ONCE.** As a render-time
1139
- * default (`api ?? createWorkflowApi()`) it is a fresh object per render,
1140
- * which is the same hazard one layer down; built inside an effect it is a
1141
- * fresh object per watch.
1142
- *
1143
- * The returned getter is stable for the life of the component and reads the ref
1144
- * on every call, so a caller that SWAPS clients mid-watch — a token arriving
1145
- * after login — is picked up by the next request without the watch restarting.
1146
- */
1147
- /**
1148
- * Resolve the client a workflow hook should use, now.
1149
- *
1150
- * @param api - The caller's client, or undefined for one aimed at the page's
1151
- * own agent.
1152
- * @returns A stable getter. Call it per request, never once per watch.
1153
- *
1154
- * @internal
1155
- */
1156
- function useWorkflowApiRef(api) {
1157
- const apiRef = useRef(api);
1158
- apiRef.current = api;
1159
- const fallbackRef = useRef(void 0);
1160
- return useCallback(() => {
1161
- const current = apiRef.current;
1162
- if (current) return current;
1163
- fallbackRef.current ??= createWorkflowApi();
1164
- return fallbackRef.current;
1165
- }, []);
1166
- }
1167
- //#endregion
1168
- //#region _repeat-until.ts
1169
- /**
1170
- * A bounded read, re-armed from the SETTLED read — the loop both workflow
1171
- * watchers are built out of.
1172
- *
1173
- * `pollUntilTerminal` (`use-workflow-run.ts`) and `readProgressUntilComplete`
1174
- * (`use-workflow-progress.ts`) each open a request, decide from its answer
1175
- * whether to come back, and stop when told to. What they had in common was not
1176
- * the decision — one reads a snapshot, the other drains an SSE body — but the
1177
- * scaffold around it, and that scaffold is where the two rules live:
1178
- *
1179
- * - **Re-armed from the settled read, never on an interval.** A slow response
1180
- * would otherwise stack overlapping requests on an agent that is already
1181
- * struggling, and on the platform every one of them BROKERS.
1182
- * - **Cancellation is a signal, not a flag.** The teardown has to reach the
1183
- * in-flight request too — an abandoned progress read otherwise keeps pulling
1184
- * chunks out of a run for a page that has navigated away — so `step` is
1185
- * handed the signal rather than being trusted to check a boolean.
1186
- */
1187
- /**
1188
- * Call `step` until it reports it is finished, or until the returned stop
1189
- * function is called.
1190
- *
1191
- * `step` resolves `true` when there is nothing left to come back for. It is
1192
- * responsible for its own failures: a rejection would leave the loop stopped
1193
- * with nobody told, so each caller decides whether its failure is terminal or
1194
- * just another reason to try again.
1195
- *
1196
- * @internal
1197
- */
1198
- function repeatUntil(intervalMs, step) {
1199
- const controller = new AbortController();
1200
- let timer;
1201
- const tick = async () => {
1202
- if (await step(controller.signal) || controller.signal.aborted) return;
1203
- timer = setTimeout(() => void tick(), intervalMs);
1204
- };
1205
- tick();
1206
- return () => {
1207
- controller.abort();
1208
- if (timer !== void 0) clearTimeout(timer);
1209
- };
1210
- }
1211
- //#endregion
1212
- //#region workflow-events.ts
1213
1237
  /**
1214
- * Watching a run over server-sent events — the PUSH half of `useWorkflowRun`.
1238
+ * Bind pause/resume to the live submission's gate.
1215
1239
  *
1216
- * Its own module because the seam is clean: everything in `workflow-client.ts`
1217
- * is request/response shaping plus the poll, and this is one long-lived stream
1218
- * and the SSE parser it needs.
1240
+ * @param getGate - Reads the gate out of the caller's ref, so the callbacks stay
1241
+ * stable across submissions rather than being re-created per gate.
1242
+ * @param setUpload - The status setter the progress bar renders from.
1219
1243
  *
1220
1244
  * @internal
1221
1245
  */
1222
- /**
1223
- * Watch a run over SSE, falling back to the caller's poll on any failure.
1224
- *
1225
- * The poll stays the fallback rather than being replaced, and that is the whole
1226
- * shape of this: a stream is an optimisation over a mechanism that already
1227
- * works, so every way it can fail — an older agent with no `/events` route, a
1228
- * proxy that buffers, a network that drops it — has to degrade to the thing that
1229
- * does. What it buys is real, though: on the platform every polled read BROKERS,
1230
- * so N open tabs at `DEFAULT_WORKFLOW_POLL_MS` is N/2 brokered requests a
1231
- * second, each able to boot a sandbox. One stream per tab replaces all of it.
1232
- *
1233
- * `EventSource` is not used, for two reasons that both matter here: it cannot
1234
- * send an `Authorization` header (an agent with `AAI_WORKFLOW_API_TOKEN` set
1235
- * would be unreachable), and it reconnects on its own schedule, which would
1236
- * fight the caller's. A `fetch` stream gives both back.
1237
- *
1238
- * Returns a stop function. `onFallback` is called at most once, when this stream
1239
- * cannot be relied on and the poll should take over.
1240
- */
1241
- function watchRunEvents(getClient, runId, onRun, onSettled, onFallback) {
1242
- const controller = new AbortController();
1243
- let handedOver = false;
1244
- const handOver = () => {
1245
- if (handedOver || controller.signal.aborted) return;
1246
- handedOver = true;
1247
- onFallback();
1248
- };
1249
- /**
1250
- * Consume the stream. Resolves `"settled"` when the run reached a state
1251
- * nothing will change, and `"fallback"` for every other ending — including a
1252
- * clean end with no final frame, which is a dropped connection.
1253
- *
1254
- * A named function rather than an inline IIFE so `watchRunEvents` stays under
1255
- * the cognitive-complexity cap, and so the two outcomes are a return value
1256
- * instead of two callbacks invoked from six places.
1257
- */
1258
- const pump = async () => {
1259
- const res = await getClient().watch(runId, controller.signal);
1260
- if (!(res.ok && res.body)) return "fallback";
1261
- for await (const frame of readEventStream(res.body, controller.signal)) {
1262
- if (frame.event === "run" && frame.data) onRun(frame.data);
1263
- const outcome = endingFor(frame.event);
1264
- if (outcome) return outcome;
1265
- }
1266
- return "fallback";
1267
- };
1268
- pump().then((outcome) => outcome === "settled" ? onSettled() : handOver(), () => handOver());
1269
- return () => controller.abort();
1270
- }
1271
- /**
1272
- * Does this frame END the stream, and does the run need watching afterwards?
1273
- *
1274
- * `done` and `missing` are both final and neither wants a reconnect: the run is
1275
- * terminal, or the id will never exist (a 404 is a stable answer — the world's
1276
- * record is durable). `idle` is the stream handing ITSELF back after its
1277
- * duration cap, so that one falls back to the poll. Anything else is not an
1278
- * ending.
1279
- */
1280
- function endingFor(event) {
1281
- if (event === "done" || event === "missing") return "settled";
1282
- return event === "idle" ? "fallback" : void 0;
1283
- }
1284
- //#endregion
1285
- //#region use-workflow-run.ts
1286
- /**
1287
- * `useWorkflowRun` — watch one run until it settles.
1288
- *
1289
- * Split from `workflow-client.ts` on the seam that module's doc already draws:
1290
- * everything there is a REQUEST (one call, one answer, no React), and everything
1291
- * here is the loop that keeps asking. They are read for different reasons — the
1292
- * client is what a script or a `curl` equivalent needs, this is what a page needs
1293
- * — and only this half imports React.
1294
- *
1295
- * `workflow-events.ts` sits under it as the streaming fast path, and
1296
- * `use-workflow-form.ts` above it as the form-shaped caller.
1297
- */
1298
- /** How often {@link useWorkflowRun} re-reads a live run when it has to poll. */
1299
- const DEFAULT_WORKFLOW_POLL_MS = 2e3;
1300
- /**
1301
- * Consecutive "no such run" reads {@link useWorkflowRun} tolerates before giving
1302
- * up on the id.
1303
- *
1304
- * Small on purpose: a 404 is a stable answer, so the budget exists only to
1305
- * absorb a first read that races the run's creation — not to keep hoping.
1306
- * Unbounded, a stale id polls (and, on the platform, BROKERS) for as long as the
1307
- * tab is open.
1308
- */
1309
- const MAX_MISSING_READS = 3;
1310
- /**
1311
- * Poll `runId` until it is terminal, reporting each read. Returns a stop
1312
- * function.
1313
- *
1314
- * Module-level rather than inline in the hook below, so neither function carries
1315
- * the whole loop's branching — and so the loop can be read without React in the
1316
- * way.
1317
- */
1318
- function pollUntilTerminal(getClient, runId, intervalMs, onRun, onError, onStopped) {
1319
- let missing = 0;
1320
- /**
1321
- * A read that came back empty. Resolves whether the loop should STOP.
1322
- *
1323
- * A 404 is a STABLE answer — a run the agent does not know about now will not
1324
- * appear later — and retrying it unbounded is how a stale id (one restored
1325
- * from `localStorage`, or one whose agent was redeployed onto a fresh
1326
- * database) polls forever: the page stays `polling` and therefore busy, and on
1327
- * the platform every read BROKERS, so a tab's worth of dead ids keeps
1328
- * sandboxes resident. A small budget is kept anyway, because the first read
1329
- * can race a replica that has not yet seen the run.
1330
- */
1331
- const onMissing = (signal) => {
1332
- missing += 1;
1333
- if (missing < 3) return false;
1334
- if (!signal.aborted) onError(`No workflow run ${runId}`);
1335
- return true;
1336
- };
1337
- const read = async (signal) => {
1338
- try {
1339
- const next = await getClient().get(runId);
1340
- if (signal.aborted) return true;
1341
- if (!next) return onMissing(signal);
1342
- missing = 0;
1343
- onRun(next);
1344
- return isTerminal$1(next);
1345
- } catch (err) {
1346
- if (!signal.aborted) onError(errorMessage(err));
1347
- return false;
1348
- }
1349
- };
1350
- return repeatUntil(intervalMs, async (signal) => {
1351
- if (!await read(signal)) return false;
1352
- if (!signal.aborted) onStopped();
1353
- return true;
1354
- });
1355
- }
1356
- /**
1357
- * Watch one run until it reaches a terminal status.
1358
- *
1359
- * A watch rather than a subscription because a run is durable and the page is
1360
- * not: it can complete while the tab is closed, on a different sandbox, hours
1361
- * later. There is no session to reconnect — the id is the whole state.
1362
- *
1363
- * The stream (`GET /runs/:id/events`) is tried first and the poll is its
1364
- * fallback, so an agent deployed before that route existed still works. Watching
1365
- * STOPS on a terminal status, so a finished run costs nothing; passing
1366
- * `undefined` (nothing started yet) also costs nothing.
1367
- *
1368
- * @typeParam R - The workflow's output type. Supplying it is what makes
1369
- * `run.status === "completed"` narrow to a typed `run.output` instead of
1370
- * `unknown`. Derive it with `WorkflowOutputOf<typeof myWorkflow>` — a
1371
- * type-only import of `agent.ts` is erased, so it costs the bundle nothing.
1372
- *
1373
- * @public
1374
- */
1375
- function useWorkflowRun(runId, opts = {}) {
1376
- const { api, intervalMs = DEFAULT_WORKFLOW_POLL_MS } = opts;
1377
- const [run, setRun] = useState(void 0);
1378
- const [error, setError] = useState(void 0);
1379
- /**
1380
- * Has the watch stopped for a reason the snapshot does not show?
1381
- *
1382
- * Only one such reason exists — an id the agent kept reporting as unknown,
1383
- * past {@link MAX_MISSING_READS} — and it leaves `run` undefined, so `polling`
1384
- * derived from `isTerminal(run)` alone would stay true forever.
1385
- */
1386
- const [stopped, setStopped] = useState(false);
1387
- const getClient = useWorkflowApiRef(api);
1388
- useEffect(() => {
1389
- setRun(void 0);
1390
- setError(void 0);
1391
- setStopped(false);
1392
- if (!runId) return;
1393
- const onRun = (next) => {
1394
- setRun(next);
1395
- setError(void 0);
1396
- };
1397
- let stopPoll;
1398
- const stopStream = watchRunEvents(getClient, runId, onRun, () => setStopped(true), () => {
1399
- stopPoll = pollUntilTerminal(getClient, runId, intervalMs, onRun, setError, () => setStopped(true));
1400
- });
1401
- return () => {
1402
- stopStream();
1403
- stopPoll?.();
1404
- };
1405
- }, [
1406
- runId,
1407
- intervalMs,
1408
- getClient
1409
- ]);
1246
+ function useUploadPause(getGate, setUpload) {
1247
+ const setPaused = useCallback((paused) => {
1248
+ setUpload((current) => current ? {
1249
+ ...current,
1250
+ paused
1251
+ } : current);
1252
+ }, [setUpload]);
1410
1253
  return {
1411
- run,
1412
- error,
1413
- polling: runId !== void 0 && !stopped && !isTerminal$1(run)
1254
+ pauseUpload: useCallback(() => {
1255
+ getGate()?.pause();
1256
+ setPaused(true);
1257
+ }, [getGate, setPaused]),
1258
+ resumeUpload: useCallback(() => {
1259
+ getGate()?.resume();
1260
+ setPaused(false);
1261
+ }, [getGate, setPaused])
1414
1262
  };
1415
1263
  }
1416
1264
  //#endregion
@@ -1460,6 +1308,35 @@ function useWorkflowRun(runId, opts = {}) {
1460
1308
  * empty list — which renders as a form with no fields and reads as "this agent
1461
1309
  * declares no workflows" about an agent that was merely unreachable.
1462
1310
  *
1311
+ * @example
1312
+ * ```tsx
1313
+ * import { useWorkflows } from "@alexkroman1/aai-ui";
1314
+ *
1315
+ * // A page rendering its own chrome from the listing — a picker, say. A form
1316
+ * // for ONE workflow wants `<WorkflowFields workflow="name" />` instead,
1317
+ * // which does this lookup itself.
1318
+ * function WorkflowPicker({ onPick }: { onPick: (name: string) => void }) {
1319
+ * const { workflows, loading, error } = useWorkflows();
1320
+ * if (loading) return <p>Loading…</p>;
1321
+ * if (error !== undefined) return <p role="alert">{error}</p>;
1322
+ * return (
1323
+ * <ul>
1324
+ * {workflows.map((summary) => (
1325
+ * <li key={summary.name}>
1326
+ * <button type="button" onClick={() => onPick(summary.name)}>
1327
+ * {summary.description ?? summary.name}
1328
+ * </button>
1329
+ * </li>
1330
+ * ))}
1331
+ * </ul>
1332
+ * );
1333
+ * }
1334
+ * ```
1335
+ *
1336
+ * @param opts - See {@link UseWorkflowsOptions}.
1337
+ * @returns The listing, its loading flag and its failure — see
1338
+ * {@link UseWorkflowsResult}.
1339
+ *
1463
1340
  * @public
1464
1341
  */
1465
1342
  function useWorkflows(opts = {}) {
@@ -1530,59 +1407,53 @@ function useWorkflowSubmit(workflow, opts = {}) {
1530
1407
  api,
1531
1408
  intervalMs
1532
1409
  }));
1533
- return {
1534
- submit: useCallback(async (input) => {
1535
- const client = getClient();
1536
- setStarting(true);
1537
- setStartError(void 0);
1538
- setRunId(void 0);
1539
- session.current?.gate.cancel();
1540
- const current = createUploadSession(workflow);
1541
- session.current = current;
1542
- try {
1543
- const options = omitUndefined({ key });
1544
- const started = await uploadFiles(client, input, setUpload, parallel, current);
1545
- setRunId(wait === void 0 ? await client.start(workflow, started, options) : (await client.startAndWait(workflow, started, {
1546
- ...options,
1547
- wait
1548
- })).runId);
1549
- } catch (err) {
1550
- if (!current.gate.cancelled) setStartError(errorMessage(err));
1551
- } finally {
1552
- if (session.current === current) {
1553
- session.current = void 0;
1554
- setStarting(false);
1555
- setUpload(void 0);
1556
- }
1410
+ const { wake, cancel } = useRunControls(runId, getClient);
1411
+ const submit = useCallback(async (input) => {
1412
+ const client = getClient();
1413
+ setStarting(true);
1414
+ setStartError(void 0);
1415
+ setRunId(void 0);
1416
+ session.current?.gate.cancel();
1417
+ const current = createUploadSession(workflow);
1418
+ session.current = current;
1419
+ try {
1420
+ const options = omitUndefined({ key });
1421
+ const started = await uploadFiles(client, input, setUpload, parallel, current);
1422
+ setRunId(wait === void 0 ? await client.start(workflow, started, options) : (await client.startAndWait(workflow, started, {
1423
+ ...options,
1424
+ wait
1425
+ })).runId);
1426
+ } catch (err) {
1427
+ if (!current.gate.cancelled) setStartError(errorMessage(err));
1428
+ } finally {
1429
+ if (session.current === current) {
1430
+ session.current = void 0;
1431
+ setStarting(false);
1432
+ setUpload(void 0);
1557
1433
  }
1558
- }, [
1559
- workflow,
1560
- key,
1561
- wait,
1562
- parallel,
1563
- getClient
1564
- ]),
1565
- reset: useCallback(() => {
1566
- session.current?.gate.cancel();
1567
- session.current = void 0;
1568
- setRunId(void 0);
1569
- setStartError(void 0);
1570
- setUpload(void 0);
1571
- }, []),
1572
- pauseUpload: useCallback(() => {
1573
- session.current?.gate.pause();
1574
- setUpload((current) => current ? {
1575
- ...current,
1576
- paused: true
1577
- } : current);
1578
- }, []),
1579
- resumeUpload: useCallback(() => {
1580
- session.current?.gate.resume();
1581
- setUpload((current) => current ? {
1582
- ...current,
1583
- paused: false
1584
- } : current);
1585
- }, []),
1434
+ }
1435
+ }, [
1436
+ workflow,
1437
+ key,
1438
+ wait,
1439
+ parallel,
1440
+ getClient
1441
+ ]);
1442
+ const reset = useCallback(() => {
1443
+ session.current?.gate.cancel();
1444
+ session.current = void 0;
1445
+ setRunId(void 0);
1446
+ setStartError(void 0);
1447
+ setUpload(void 0);
1448
+ }, []);
1449
+ const { pauseUpload, resumeUpload } = useUploadPause(useCallback(() => session.current?.gate, []), setUpload);
1450
+ return {
1451
+ submit,
1452
+ reset,
1453
+ wake,
1454
+ cancel,
1455
+ pauseUpload,
1456
+ resumeUpload,
1586
1457
  run: tracked.run,
1587
1458
  pending: starting || tracked.polling,
1588
1459
  upload,
@@ -1591,6 +1462,26 @@ function useWorkflowSubmit(workflow, opts = {}) {
1591
1462
  }
1592
1463
  //#endregion
1593
1464
  //#region components/workflow-fields.tsx
1465
+ /** @jsxImportSource react */
1466
+ /**
1467
+ * A form built from a workflow's declared input schema.
1468
+ *
1469
+ * `GET workflows` reports each workflow's `inputSchema` as JSON Schema — the
1470
+ * zod schema an author wrote in `agent.ts`, converted at listing time precisely
1471
+ * so a browser can read it. This is what reads it: one `<WorkflowFields>` and a
1472
+ * workflow's form matches its schema by construction, so adding a field to the
1473
+ * schema adds it to the page and nothing can drift.
1474
+ *
1475
+ * ## It covers SCALARS, and says so rather than guessing
1476
+ *
1477
+ * A string, number, integer, boolean or enum has one obvious control each. A
1478
+ * nested object or an array does not — every choice (a JSON textarea, a repeater,
1479
+ * a comma-separated string) is a guess about what the author meant, and a guess
1480
+ * that produces a value the schema then rejects is worse than no field at all.
1481
+ * So those are SKIPPED, and the fields for them are written by hand: every field
1482
+ * in this package is a plain named control, so a hand-written one composes with
1483
+ * a generated one inside the same {@link Form}.
1484
+ */
1594
1485
  /**
1595
1486
  * Render one field per scalar property of a workflow's input schema.
1596
1487
  *
@@ -1622,6 +1513,8 @@ function useWorkflowSubmit(workflow, opts = {}) {
1622
1513
  * }
1623
1514
  * ```
1624
1515
  *
1516
+ * @param props - Field-set props.
1517
+ *
1625
1518
  * @public
1626
1519
  */
1627
1520
  function WorkflowFields({ workflow }) {
@@ -1707,208 +1600,6 @@ function humanize(name) {
1707
1600
  return spaced.charAt(0).toUpperCase() + spaced.slice(1);
1708
1601
  }
1709
1602
  //#endregion
1710
- //#region use-workflow-progress.ts
1711
- /**
1712
- * `useWorkflowProgress` — read what a run has WRITTEN while it runs.
1713
- *
1714
- * The sibling of `useWorkflowRun`, and the split between them is the whole
1715
- * reason this exists. That hook reports a run's STATE: the status transitions
1716
- * the world records, which every run has. This reports what the run itself
1717
- * wrote through `getWritable()`, which is the only thing a long run can say
1718
- * before it finishes — a snapshot carries a status and, once terminal, an
1719
- * output, and nothing in between. A page that shows only status shows
1720
- * "Working…" for ten minutes and then a result.
1721
- *
1722
- * ## There is no poll fallback, and that is not an omission
1723
- *
1724
- * `useWorkflowRun` degrades to polling `GET /runs/:id` because a run's STATE is
1725
- * readable that way. A run's written chunks are not: the stream is the only
1726
- * route to them, so an agent that does not serve it has no progress to give and
1727
- * `supported` says so once, rather than a poll pretending to look for something
1728
- * that is not there. A page renders its status line either way.
1729
- *
1730
- * ## Chunks are RETAINED, so this is a replay as much as a tail
1731
- *
1732
- * The run's stream keeps every chunk, so a page that mounts late — a reload, a
1733
- * second tab, a link opened tomorrow — reads the whole history from index 0 and
1734
- * arrives at the same list as one that watched throughout. That is what makes a
1735
- * durable run's progress durable too, and it is why the default `startIndex` is
1736
- * 0 rather than "from now": a tail-only default would make the same page show
1737
- * different things depending on when it opened.
1738
- *
1739
- * ## It RE-OPENS while the run is live, because a progress read is bounded
1740
- *
1741
- * The route answers with the chunks written when the request arrived and then
1742
- * ends, reporting `complete` — whether the run itself was terminal. It has to:
1743
- * a workflow stream signals its end only once CLOSED, and a progress channel
1744
- * written by one step after another is never closed, so a read that waited for
1745
- * the end would hang forever on a finished run. (It did: see the route's own
1746
- * doc.)
1747
- *
1748
- * So this hook re-opens from where it left off until a read comes back
1749
- * `complete`. That is a poll, and the honest description of progress is a durable
1750
- * log rather than a socket — but it is a poll of a CHEAP shape: each read asks
1751
- * only for chunks past the last index it saw, so a quiet run costs an empty
1752
- * answer rather than the whole log again.
1753
- */
1754
- /** How often a live run's progress is re-read once a bounded read has ended. */
1755
- const DEFAULT_PROGRESS_POLL_MS = 1e3;
1756
- /**
1757
- * Drain one bounded read's frames, reporting how it ended and everything it
1758
- * carried.
1759
- *
1760
- * The chunks are RETURNED rather than handed over one at a time, and that is
1761
- * what lets the hook commit a whole read in one React update: a per-chunk
1762
- * callback re-rendered the page once per progress line and rebuilt the list
1763
- * each time, which for a fan-out writing a line per segment is an O(n²) copy of
1764
- * the log a reader can already only see one frame at a time. A read is bounded
1765
- * by construction (see the module doc), so buffering one is bounded too.
1766
- */
1767
- async function consumeFrames(body, signal) {
1768
- const chunks = [];
1769
- let ending = "partial";
1770
- for await (const frame of readEventStream(body, signal)) if (frame.event === "chunk") chunks.push(frame.data);
1771
- else if (frame.event === "done") ending = frame.data?.complete ? "complete" : "partial";
1772
- else if (frame.event === "missing") return {
1773
- ending: "complete",
1774
- chunks
1775
- };
1776
- return {
1777
- ending,
1778
- chunks
1779
- };
1780
- }
1781
- /**
1782
- * Read one run's progress until it is complete, reporting each read's chunks.
1783
- *
1784
- * Module-level rather than inline in the hook, so the loop reads without React in
1785
- * the way — the same split `useWorkflowRun` makes with `pollUntilTerminal`.
1786
- *
1787
- * Every re-open asks from an ABSOLUTE position past the last chunk read, so a
1788
- * read only ever fetches what this reader has not seen. That is what keeps the
1789
- * poll cheap: a quiet run answers with a bare `done` rather than the whole log
1790
- * again.
1791
- *
1792
- * ## A negative `startIndex` is resolved on the FIRST read, not carried
1793
- *
1794
- * "The last N lines" names no position a later read can resume from — the tail
1795
- * it counts back from moves with every line the run writes. Carrying it meant a
1796
- * re-open asking for everything from 0 and dropping `seen` chunks off the
1797
- * FRONT, which is a different set entirely: a reader that opened at
1798
- * `startIndex: -3` on a 10-line log holds lines 7-9, and its next read handed
1799
- * over lines 3 onwards — four lines it never asked for, then the three it
1800
- * already had, in that order. The dedupe the old comment claimed would need the
1801
- * first read's absolute tail, which the reader never learned.
1802
- *
1803
- * So the first read is issued from 0 instead, and only its last N chunks are
1804
- * handed over. The window the caller asked for is unchanged, and the reader now
1805
- * knows exactly where it is — every read after it is the ordinary absolute case.
1806
- * The cost is that the first read transfers the whole log, which is what the
1807
- * DEFAULT (`startIndex: 0`, "replay everything") already does.
1808
- */
1809
- function readProgressUntilComplete(getClient, runId, options, intervalMs, onChunks, onEnded) {
1810
- const start = options.startIndex ?? 0;
1811
- const tail = start < 0 ? -start : void 0;
1812
- let next = tail === void 0 ? start : 0;
1813
- let firstRead = true;
1814
- /** One bounded read. Resolves how it ended. */
1815
- const readOnce = async (signal) => {
1816
- const res = await getClient().streamOutput(runId, {
1817
- ...omitUndefined({
1818
- namespace: options.namespace,
1819
- startIndex: next === 0 ? void 0 : next
1820
- }),
1821
- signal
1822
- });
1823
- if (!(res.ok && res.body)) return "unsupported";
1824
- const { ending, chunks } = await consumeFrames(res.body, signal);
1825
- next += chunks.length;
1826
- const fresh = firstRead && tail !== void 0 ? chunks.slice(-tail) : chunks;
1827
- firstRead = false;
1828
- if (fresh.length > 0 && !signal.aborted) onChunks(fresh);
1829
- return ending;
1830
- };
1831
- return repeatUntil(intervalMs, async (signal) => {
1832
- let ending;
1833
- try {
1834
- ending = await readOnce(signal);
1835
- } catch {
1836
- ending = "partial";
1837
- }
1838
- if (signal.aborted) return true;
1839
- if (ending === "partial") return false;
1840
- onEnded(ending);
1841
- return true;
1842
- });
1843
- }
1844
- /**
1845
- * Follow one run's progress stream.
1846
- *
1847
- * Passing `undefined` (nothing started yet) costs nothing, and reading stops for
1848
- * good once a read reports the run terminal — so a finished run costs one read.
1849
- *
1850
- * @example
1851
- * ```tsx
1852
- * import { useWorkflowProgress } from "@alexkroman1/aai-ui";
1853
- *
1854
- * function Progress({ runId }: { runId?: string }) {
1855
- * const { progress, streaming, supported } = useWorkflowProgress(runId);
1856
- * if (!supported) return null;
1857
- * return (
1858
- * <pre>
1859
- * {progress.join("\n")}
1860
- * {streaming && "\n…"}
1861
- * </pre>
1862
- * );
1863
- * }
1864
- * ```
1865
- *
1866
- * @typeParam T - What the workflow writes. Defaults to `string`, which is what
1867
- * a progress channel usually carries; a workflow writing objects names its own
1868
- * shape. Nothing in the browser can verify it — the route describes no type —
1869
- * so this is the page's assertion about its own agent, narrowed once here
1870
- * rather than at every read.
1871
- *
1872
- * @public
1873
- */
1874
- function useWorkflowProgress(runId, opts = {}) {
1875
- const { api, namespace, startIndex, intervalMs = DEFAULT_PROGRESS_POLL_MS } = opts;
1876
- const [progress, setProgress] = useState([]);
1877
- const [streaming, setStreaming] = useState(false);
1878
- const [supported, setSupported] = useState(true);
1879
- const getClient = useWorkflowApiRef(api);
1880
- useEffect(() => {
1881
- setProgress([]);
1882
- setSupported(true);
1883
- setStreaming(false);
1884
- if (!runId) return;
1885
- setStreaming(true);
1886
- const stop = readProgressUntilComplete(getClient, runId, {
1887
- namespace,
1888
- startIndex
1889
- }, intervalMs, (chunks) => setProgress((seen) => [...seen, ...chunks]), (ending) => {
1890
- setStreaming(false);
1891
- if (ending === "unsupported") setSupported(false);
1892
- });
1893
- return () => {
1894
- stop();
1895
- setStreaming(false);
1896
- };
1897
- }, [
1898
- runId,
1899
- namespace,
1900
- startIndex,
1901
- intervalMs,
1902
- getClient
1903
- ]);
1904
- return {
1905
- progress,
1906
- latest: progress.at(-1),
1907
- streaming,
1908
- supported
1909
- };
1910
- }
1911
- //#endregion
1912
1603
  //#region components/workflow-progress.tsx
1913
1604
  /** @jsxImportSource react */
1914
1605
  /**
@@ -1946,24 +1637,17 @@ function useWorkflowProgress(runId, opts = {}) {
1946
1637
  * }
1947
1638
  * ```
1948
1639
  *
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.
1640
+ * @param props - Progress-log props.
1958
1641
  *
1959
1642
  * @public
1960
1643
  */
1961
- function WorkflowProgress({ runId, api, className, placeholder }) {
1644
+ function WorkflowProgress({ runId, api, className, placeholder, lines }) {
1962
1645
  const { progress, streaming, supported } = useWorkflowProgress(runId, omitUndefined({ api }));
1963
- if (!supported || progress.length === 0) return placeholder ?? null;
1646
+ const shown = lines === void 0 ? progress : progress.slice(Math.max(progress.length - lines, 0));
1647
+ if (!supported || shown.length === 0) return placeholder ?? null;
1964
1648
  return /* @__PURE__ */ jsxs("pre", {
1965
1649
  className: clsx(className ?? "whitespace-pre-wrap border-l pl-4 text-xs opacity-70"),
1966
- children: [progress.join("\n"), streaming && "\n…"]
1650
+ children: [shown.join("\n"), streaming && "\n…"]
1967
1651
  });
1968
1652
  }
1969
1653
  //#endregion
@@ -2027,69 +1711,94 @@ function page(config) {
2027
1711
  return mountRoot(container, createElement(ThemeProvider, { value: config.theme }, createElement(config.component)));
2028
1712
  }
2029
1713
  //#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.
1714
+ //#region use-download-url.ts
1715
+ /**
1716
+ * `useDownloadUrl` — an upload id a run produced, as something `<audio>`,
1717
+ * `<img>` or `<a download>` will accept.
1718
+ *
1719
+ * `api.download(id)` resolves a `Blob`, and it has to: the byte route takes the
1720
+ * same bearer every workflow route does, and neither `<audio src>` nor
1721
+ * `<a href>` can send one. So every page that plays back what a run WROTE ends
1722
+ * up at the same four lines — `download` → `createObjectURL` → state — and the
1723
+ * two that are really the point are the two the four lines are wrapped in:
1724
+ *
1725
+ * - **`URL.revokeObjectURL` on cleanup.** An object URL pins its blob for the
1726
+ * life of the DOCUMENT. Miss it and every completed run's audio stays resident
1727
+ * until the tab closes, which on a page people run all day is a leak measured
1728
+ * in the size of the files.
1729
+ * - **A `cancelled` flag.** A second run settling while the first download is
1730
+ * still in flight otherwise sets state from the stale one, and the page plays
1731
+ * the previous run's audio under the current run's transcript — a wrong answer
1732
+ * that looks like a right one.
1733
+ *
1734
+ * Two templates had written this hook, identically, doc paragraph included, and
1735
+ * `aai-ui` exported no download helper at all. Both also faked `pending` by
1736
+ * checking `url === undefined && error === undefined`, which reads "idle" and
1737
+ * "downloading" as the same thing — so this reports it.
1738
+ */
1739
+ /** No id: nothing pending, nothing to show. A shared object so `setState` no-ops. */
1740
+ const IDLE = { pending: false };
1741
+ /** Bytes in flight. Shared for the same reason as {@link IDLE}. */
1742
+ const PENDING = { pending: true };
1743
+ /**
1744
+ * Read an upload's bytes and hand back a URL a DOM element can use.
1745
+ *
1746
+ * @param uploadId - The id a completed run reported, or `undefined` before one
1747
+ * exists — which is what a page passes straight through while it waits, and
1748
+ * reports as idle rather than pending.
1749
+ * @param opts - See {@link UseDownloadUrlOptions}.
1750
+ * @returns See {@link UseDownloadUrlResult}.
2067
1751
  *
2068
1752
  * @example
2069
1753
  * ```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>;
1754
+ * import { useDownloadUrl, useWorkflowSubmit } from "@alexkroman1/aai-ui";
1755
+ *
1756
+ * function Playback() {
1757
+ * const { run } = useWorkflowSubmit<{ audio: string }>("spokenSummary");
1758
+ * const output = run?.status === "completed" ? run.output : undefined;
1759
+ * const audio = useDownloadUrl(output?.audio);
1760
+ * if (audio.pending) return <p>Fetching audio…</p>;
1761
+ * if (audio.error !== undefined) return <p role="alert">{audio.error}</p>;
1762
+ * return audio.url === undefined ? null : (
1763
+ * <a href={audio.url} download="summary.mp3">
1764
+ * Download
1765
+ * </a>
1766
+ * );
2076
1767
  * }
2077
1768
  * ```
2078
1769
  *
2079
1770
  * @public
2080
1771
  */
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;
1772
+ function useDownloadUrl(uploadId, opts = {}) {
1773
+ const [state, setState] = useState(IDLE);
1774
+ const getClient = useWorkflowApiRef(opts.api);
1775
+ useEffect(() => {
1776
+ if (uploadId === void 0) {
1777
+ setState(IDLE);
1778
+ return;
1779
+ }
1780
+ let cancelled = false;
1781
+ let objectUrl;
1782
+ setState(PENDING);
1783
+ getClient().download(uploadId).then((blob) => {
1784
+ if (cancelled) return;
1785
+ objectUrl = URL.createObjectURL(blob);
1786
+ setState({
1787
+ url: objectUrl,
1788
+ pending: false
1789
+ });
1790
+ }).catch((err) => {
1791
+ if (!cancelled) setState({
1792
+ error: errorMessage(err),
1793
+ pending: false
1794
+ });
1795
+ });
1796
+ return () => {
1797
+ cancelled = true;
1798
+ if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
1799
+ };
1800
+ }, [uploadId, getClient]);
1801
+ return state;
2093
1802
  }
2094
1803
  //#endregion
2095
1804
  //#region use-workflow-runs.ts
@@ -2306,75 +2015,69 @@ function useWorkflowStream(workflow, opts = {}) {
2306
2015
  api,
2307
2016
  intervalMs
2308
2017
  }));
2309
- return {
2310
- submit: useCallback(async (input) => {
2311
- const client = getClient();
2312
- setStarting(true);
2313
- setStartError(void 0);
2314
- setRunId(void 0);
2315
- gateRef.current?.cancel();
2316
- const gate = createUploadGate();
2317
- gateRef.current = gate;
2318
- let started;
2319
- try {
2320
- const id = randomUploadId();
2321
- const begun = await beginRun({
2322
- client,
2323
- workflow,
2324
- input,
2325
- id,
2326
- ...omitUndefined({ key })
2327
- });
2328
- started = begun.runId;
2329
- const chosen = begun.file;
2330
- setRunId(started);
2331
- if (!chosen) return;
2332
- await streamFile({
2333
- client,
2334
- gate,
2335
- id,
2336
- file: chosen,
2337
- parallel,
2338
- report: setUpload
2339
- });
2340
- await client.wake(started).catch(() => void 0);
2341
- } catch (err) {
2342
- if (!gate.cancelled) setStartError(errorMessage(err));
2343
- if (started) await client.cancel(started).catch(() => void 0);
2344
- } finally {
2345
- if (gateRef.current === gate) {
2346
- gateRef.current = void 0;
2347
- setStarting(false);
2348
- setUpload(void 0);
2349
- }
2018
+ const { wake, cancel } = useRunControls(runId, getClient);
2019
+ const submit = useCallback(async (input) => {
2020
+ const client = getClient();
2021
+ setStarting(true);
2022
+ setStartError(void 0);
2023
+ setRunId(void 0);
2024
+ gateRef.current?.cancel();
2025
+ const gate = createUploadGate();
2026
+ gateRef.current = gate;
2027
+ let started;
2028
+ try {
2029
+ const id = randomUploadId();
2030
+ const begun = await beginRun({
2031
+ client,
2032
+ workflow,
2033
+ input,
2034
+ id,
2035
+ ...omitUndefined({ key })
2036
+ });
2037
+ started = begun.runId;
2038
+ const chosen = begun.file;
2039
+ setRunId(started);
2040
+ if (!chosen) return;
2041
+ await streamFile({
2042
+ client,
2043
+ gate,
2044
+ id,
2045
+ file: chosen,
2046
+ parallel,
2047
+ report: setUpload
2048
+ });
2049
+ await client.wake(started).catch(() => void 0);
2050
+ } catch (err) {
2051
+ if (!gate.cancelled) setStartError(errorMessage(err));
2052
+ if (started) await client.cancel(started).catch(() => void 0);
2053
+ } finally {
2054
+ if (gateRef.current === gate) {
2055
+ gateRef.current = void 0;
2056
+ setStarting(false);
2057
+ setUpload(void 0);
2350
2058
  }
2351
- }, [
2352
- workflow,
2353
- key,
2354
- parallel,
2355
- getClient
2356
- ]),
2357
- reset: useCallback(() => {
2358
- gateRef.current?.cancel();
2359
- gateRef.current = void 0;
2360
- setRunId(void 0);
2361
- setStartError(void 0);
2362
- setUpload(void 0);
2363
- }, []),
2364
- pauseUpload: useCallback(() => {
2365
- gateRef.current?.pause();
2366
- setUpload((current) => current ? {
2367
- ...current,
2368
- paused: true
2369
- } : current);
2370
- }, []),
2371
- resumeUpload: useCallback(() => {
2372
- gateRef.current?.resume();
2373
- setUpload((current) => current ? {
2374
- ...current,
2375
- paused: false
2376
- } : current);
2377
- }, []),
2059
+ }
2060
+ }, [
2061
+ workflow,
2062
+ key,
2063
+ parallel,
2064
+ getClient
2065
+ ]);
2066
+ const reset = useCallback(() => {
2067
+ gateRef.current?.cancel();
2068
+ gateRef.current = void 0;
2069
+ setRunId(void 0);
2070
+ setStartError(void 0);
2071
+ setUpload(void 0);
2072
+ }, []);
2073
+ const { pauseUpload, resumeUpload } = useUploadPause(useCallback(() => gateRef.current, []), setUpload);
2074
+ return {
2075
+ submit,
2076
+ reset,
2077
+ wake,
2078
+ cancel,
2079
+ pauseUpload,
2080
+ resumeUpload,
2378
2081
  run: tracked.run,
2379
2082
  pending: starting || tracked.polling,
2380
2083
  upload,
@@ -2474,4 +2177,30 @@ function fileAt(input, field) {
2474
2177
  return filesOf(input[field])[0];
2475
2178
  }
2476
2179
  //#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 };
2180
+ //#region workflow-status-labels.ts
2181
+ /**
2182
+ * The default status line per {@link WorkflowRunStatus}.
2183
+ *
2184
+ * Override the ones your page has a better word for and keep the rest:
2185
+ *
2186
+ * ```ts
2187
+ * import { WORKFLOW_STATUS_LABELS } from "@alexkroman1/aai-ui";
2188
+ *
2189
+ * const STATUS_LINE = { ...WORKFLOW_STATUS_LABELS, running: "Writing…" };
2190
+ * ```
2191
+ *
2192
+ * The wording is deliberately about the RUN rather than about the work — a page
2193
+ * knows what its workflow does and this does not, so `running` is the neutral
2194
+ * "Working…" and every page that cares replaces exactly that key.
2195
+ *
2196
+ * @public
2197
+ */
2198
+ const WORKFLOW_STATUS_LABELS = {
2199
+ pending: "Queued",
2200
+ running: "Working…",
2201
+ completed: "Done",
2202
+ failed: "Failed",
2203
+ cancelled: "Cancelled"
2204
+ };
2205
+ //#endregion
2206
+ export { AutoScroll, Button, ChatView, CheckboxField, ConsoleShell, Controls, Field, FileField, Form, Markdown, MessageList, NumberField, SelectField, SidebarLayout, StartScreen, SubmitButton, 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 };