@alexkroman1/aai-ui 13.2.0 → 14.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 (83) hide show
  1. package/README.md +159 -69
  2. package/dist/{_colors-j8XMToi9.js → _colors-CpZO-88A.js} +25 -2
  3. package/dist/{_module-url-C_4gRVL0.js → _module-url-C13kAJ87.js} +1 -1
  4. package/dist/_recover-run.d.ts +13 -7
  5. package/dist/_submission-state.d.ts +92 -0
  6. package/dist/_upload-files.d.ts +2 -2
  7. package/dist/_upload-report.d.ts +26 -0
  8. package/dist/{_utils-B6498_bm.js → _utils-DnQDM9Uy.js} +4 -4
  9. package/dist/_utils.d.ts +3 -3
  10. package/dist/_web-storage.d.ts +43 -0
  11. package/dist/_workflow-files.d.ts +1 -1
  12. package/dist/{aai-logo-CXZGPSIY.js → aai-logo-CFlomZlS.js} +1 -1
  13. package/dist/agent-state-labels.d.ts +60 -0
  14. package/dist/audio.js +21 -15
  15. package/dist/{chat-view-DDTtrh7N.js → chat-view-C_3T7Ln8.js} +100 -33
  16. package/dist/{client-config-BT_kWID5.js → client-config-DJQHnYjm.js} +5 -5
  17. package/dist/client-config.d.ts +4 -4
  18. package/dist/client-dir.d.ts +1 -1
  19. package/dist/client-dir.js +2 -2
  20. package/dist/components/_colors.d.ts +23 -0
  21. package/dist/components/_form-readiness.d.ts +1 -1
  22. package/dist/components/bullet-list.d.ts +74 -0
  23. package/dist/components/button.js +4 -4
  24. package/dist/components/chat-view.js +1 -1
  25. package/dist/components/console-shell.d.ts +16 -20
  26. package/dist/components/controls.js +4 -4
  27. package/dist/components/facts.d.ts +81 -0
  28. package/dist/components/form-fields.d.ts +6 -6
  29. package/dist/components/form-types.d.ts +1 -1
  30. package/dist/components/form.d.ts +1 -1
  31. package/dist/components/message-list.js +1 -1
  32. package/dist/components/session-error-banner.d.ts +69 -0
  33. package/dist/components/sidebar-layout.js +1 -1
  34. package/dist/components/start-screen.js +4 -4
  35. package/dist/components/tool-call-block.js +1 -1
  36. package/dist/components/tool-config-context.d.ts +1 -1
  37. package/dist/components/workflow-progress.d.ts +11 -4
  38. package/dist/context.d.ts +142 -19
  39. package/dist/context.js +156 -18
  40. package/dist/default-client/assets/{audio-BuDICbPf.js → audio-9zQsNc1w.js} +1 -1
  41. package/dist/default-client/assets/index-BTv30Z4F.css +2 -0
  42. package/dist/default-client/assets/index-RAZ-29Sz.js +284 -0
  43. package/dist/default-client/index.html +2 -2
  44. package/dist/default-client.d.ts +1 -1
  45. package/dist/define-client.d.ts +19 -19
  46. package/dist/define-client.js +20 -20
  47. package/dist/{eyebrow-C6ZFuiz6.js → eyebrow-UfmSz9yy.js} +1 -1
  48. package/dist/hooks.d.ts +44 -8
  49. package/dist/hooks.js +20 -14
  50. package/dist/index.d.ts +13 -8
  51. package/dist/index.js +1447 -1160
  52. package/dist/internal.d.ts +2 -2
  53. package/dist/internal.js +5 -5
  54. package/dist/{message-list-BJYyuIcR.js → message-list-CdOnSh5m.js} +23 -16
  55. package/dist/page.d.ts +11 -11
  56. package/dist/session-core-audio-setup.d.ts +1 -1
  57. package/dist/session-core-dial.d.ts +0 -2
  58. package/dist/{session-core-DxBYsfHA.js → session-core-gwePM95B.js} +135 -62
  59. package/dist/session-core-messages.d.ts +2 -2
  60. package/dist/session-core-types.d.ts +58 -1
  61. package/dist/session-core.d.ts +6 -6
  62. package/dist/session-core.js +2 -2
  63. package/dist/session-resume-store.d.ts +3 -3
  64. package/dist/{tool-call-block-tcPQAkcP.js → tool-call-block-C2t_5fpp.js} +30 -12
  65. package/dist/{tool-config-context-DzAofqi_.js → tool-config-context-Es4YUzV2.js} +2 -2
  66. package/dist/types.d.ts +19 -4
  67. package/dist/types.js +3 -3
  68. package/dist/{url-chips-YqhCjWfQ.js → url-chips-BxhzZgk2.js} +5 -5
  69. package/dist/use-conversation.d.ts +1 -1
  70. package/dist/use-run-key.d.ts +44 -10
  71. package/dist/{use-user-transcript-C14qWFu2.js → use-user-transcript-uyHhzy4d.js} +4 -3
  72. package/dist/use-workflow-form.d.ts +64 -91
  73. package/dist/{use-workflow-progress-Cu0SxMyg.js → use-workflow-run-CP2ekKPV.js} +254 -257
  74. package/dist/use-workflow-stream.d.ts +5 -2
  75. package/dist/use-workflows.d.ts +77 -0
  76. package/dist/workflow-client.d.ts +1 -1
  77. package/dist/worklets/capture-processor.js +2 -2
  78. package/dist/worklets/playback-processor.js +2 -2
  79. package/package.json +6 -6
  80. package/styles.css +78 -0
  81. package/dist/default-client/assets/index-B1_ROnTJ.js +0 -284
  82. package/dist/default-client/assets/index-S5fkKi6B.css +0 -2
  83. package/dist/tsdown.config.d.ts +0 -2
@@ -149,7 +149,7 @@ export type SessionSnapshot = {
149
149
  *
150
150
  * @public
151
151
  */
152
- export type SessionCore = {
152
+ export type BrowserSession = {
153
153
  /** Return the current immutable state snapshot. */
154
154
  getSnapshot(): SessionSnapshot;
155
155
  /** Subscribe to state changes. Returns an unsubscribe function. */
@@ -195,6 +195,30 @@ export type SessionCore = {
195
195
  * per-session tool state, greeting included.
196
196
  */
197
197
  end(): void;
198
+ /**
199
+ * End the current call and immediately begin a new one — `end()` then
200
+ * `start()`, which is what "New Conversation" means for an agent that keeps
201
+ * SESSION-SCOPED STATE.
202
+ *
203
+ * `reset()` is the one whose name suggests this and it is not the same
204
+ * thing: it clears the transcript and reconnects, but the reconnect carries
205
+ * the same `?sessionId=`, so every `sessionSlot` on the server survives —
206
+ * the game world, the incident board, the cart. A caller who asked to start
207
+ * over gets a blank transcript in front of the old state. This drops the
208
+ * session id, so the next connect mints a fresh one and the greeting plays
209
+ * again.
210
+ *
211
+ * Three templates had each written `session.end(); session.start();` with
212
+ * the same paragraph explaining why `reset()` was wrong; the six on the
213
+ * stock shell could not, because {@link Controls} called `reset()` for them.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * declare const session: import("@alexkroman1/aai-ui").Session;
218
+ * session.restart();
219
+ * ```
220
+ */
221
+ restart(): void;
198
222
  /** Alias for `disconnect` for use with `using`. */
199
223
  [Symbol.dispose](): void;
200
224
  };
@@ -247,3 +271,36 @@ export type ConnState = {
247
271
  * buffered during mic-permission never finishes playing. */
248
272
  preInitDone: boolean;
249
273
  };
274
+ /**
275
+ * The two liveness fields at rest.
276
+ *
277
+ * `running` and `recording` ride with almost every state transition and were
278
+ * spread as a pair of literals at seven sites across three modules — the same
279
+ * shape `session-core-state.ts` folded `state` and `error` out of, one field
280
+ * short. Naming it relates them: a transition that ends the call says so once,
281
+ * and a reader looking for "what stops the mic" finds one symbol rather than a
282
+ * grep.
283
+ *
284
+ * It is deliberately NOT the whole snapshot patch — a transition still supplies
285
+ * its own `agentState.apply(...)` projection beside this.
286
+ */
287
+ export declare const STOPPED: {
288
+ readonly running: false;
289
+ readonly recording: false;
290
+ };
291
+ /**
292
+ * A turn boundary: end the current turn and settle whatever it was playing.
293
+ *
294
+ * The two calls are one fact and were written out at four sites — `cancel()`
295
+ * and `reset()` here, `reply.cancelled` and `session.reset` on the server side
296
+ * — where the pair is load-bearing in both halves. The bump stops a stale drain
297
+ * continuation from stamping `"listening"` over a state the session has since
298
+ * moved to; the flush settles the interrupted turn's `done()` so it cannot
299
+ * strand.
300
+ *
301
+ * Two further sites bump WITHOUT flushing (`cleanupAudio`, a committed user
302
+ * transcript) and stay spelled out, which is the point of naming this one: a
303
+ * bump on its own now reads as a deliberate choice rather than a forgotten
304
+ * flush.
305
+ */
306
+ export declare function bargeIn(conn: ConnState): void;
@@ -1,4 +1,4 @@
1
- import type { SessionCore } from "./session-core-types.ts";
1
+ import { type BrowserSession } from "./session-core-types.ts";
2
2
  import { type VoiceSessionOptions } from "./types.ts";
3
3
  /**
4
4
  * Create a framework-agnostic voice session core that connects to an AAI
@@ -7,24 +7,24 @@ import { type VoiceSessionOptions } from "./types.ts";
7
7
  * Uses a subscribe/getSnapshot pattern for state management, compatible with
8
8
  * React's `useSyncExternalStore` and other external store integrations.
9
9
  *
10
- * Most clients never call this: `client()` creates a core and installs it in
10
+ * Most clients never call this: `mountClient()` creates a core and installs it in
11
11
  * React context for the hooks. Reach for it directly when building a
12
12
  * non-React UI (or wiring the session into another framework's store).
13
13
  *
14
14
  * @example
15
15
  * ```ts
16
- * import { createSessionCore, type SessionSnapshot } from "@alexkroman1/aai-ui";
16
+ * import { createBrowserSession, type SessionSnapshot } from "@alexkroman1/aai-ui";
17
17
  *
18
18
  * declare function render(snapshot: SessionSnapshot): void;
19
19
  *
20
- * const session = createSessionCore({ platformUrl: "https://host/my-agent/" });
20
+ * const session = createBrowserSession({ platformUrl: "https://host/my-agent/" });
21
21
  * session.subscribe(() => render(session.getSnapshot()));
22
22
  * session.start();
23
23
  * ```
24
24
  *
25
25
  * @param options - Session configuration including the platform server URL.
26
- * @returns A {@link SessionCore} handle for controlling the session.
26
+ * @returns A {@link BrowserSession} handle for controlling the session.
27
27
  *
28
28
  * @public
29
29
  */
30
- export declare function createSessionCore(options: VoiceSessionOptions): SessionCore;
30
+ export declare function createBrowserSession(options: VoiceSessionOptions): BrowserSession;
@@ -1,2 +1,2 @@
1
- import { t as createSessionCore } from "./session-core-DxBYsfHA.js";
2
- export { createSessionCore };
1
+ import { t as createBrowserSession } from "./session-core-gwePM95B.js";
2
+ export { createBrowserSession };
@@ -23,9 +23,9 @@
23
23
  * Keyed by the agent's own URL, so two agents served from one origin — which is
24
24
  * every deployed agent, at `/:slug/` — cannot inherit each other's session.
25
25
  *
26
- * Every access is guarded: storage throws outright in some contexts (Safari
27
- * private mode, storage blocked by policy), and a session that cannot be
28
- * remembered must degrade to today's behaviour rather than failing to start.
26
+ * Every access is guarded, and the guard lives in `_web-storage.ts`: a session
27
+ * that cannot be remembered must degrade to today's behaviour rather than
28
+ * failing to start.
29
29
  */
30
30
  /** The stored session id for this agent, or undefined. @internal */
31
31
  export declare function readStoredSessionId(platformUrl: string): string | undefined;
@@ -1,12 +1,12 @@
1
1
  import { useTheme } from "./context.js";
2
- import { r as inkTint } from "./_colors-j8XMToi9.js";
3
- import { t as Eyebrow } from "./eyebrow-C6ZFuiz6.js";
4
- import { i as tryParseJSON, r as truncate } from "./_utils-B6498_bm.js";
5
- import { n as useToolConfig } from "./tool-config-context-DzAofqi_.js";
2
+ import { a as inkTint } from "./_colors-CpZO-88A.js";
3
+ import { t as Eyebrow } from "./eyebrow-UfmSz9yy.js";
4
+ import { i as tryParseJSON, r as truncate } from "./_utils-DnQDM9Uy.js";
5
+ import { n as useToolConfig } from "./tool-config-context-Es4YUzV2.js";
6
6
  import clsx from "clsx";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  import { memo, useMemo, useState } from "react";
9
- //#region components/tool-call-row.tsx
9
+ //#region src/components/tool-call-row.tsx
10
10
  /** @jsxImportSource react */
11
11
  const VARIANT_CLASSES = {
12
12
  default: {
@@ -116,13 +116,36 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
116
116
  });
117
117
  }
118
118
  //#endregion
119
- //#region components/tool-call-block.tsx
119
+ //#region src/components/tool-call-block.tsx
120
120
  /** @jsxImportSource react */
121
121
  function formatResult(result) {
122
122
  const parsed = tryParseJSON(result);
123
123
  return parsed === result ? result : JSON.stringify(parsed, null, 2);
124
124
  }
125
125
  /**
126
+ * The expanded result pane.
127
+ *
128
+ * Its own component so the parse-and-pretty-print happens only when the row is
129
+ * actually OPEN. `ToolCallRow` renders `children` under `isOpen && canExpand`,
130
+ * and a row starts closed — so computing this in the parent meant every row
131
+ * paid `JSON.parse` + `JSON.stringify(_, null, 2)` over its whole result, and
132
+ * then RETAINED the pretty string, for a panel nobody clicked. The burst case
133
+ * is `history.restored`, which mounts up to `DEFAULT_MAX_HISTORY` rows in one
134
+ * commit.
135
+ *
136
+ * Creating this element is free; React only runs the body once it is mounted.
137
+ */
138
+ function ToolCallResult({ result }) {
139
+ const theme = useTheme();
140
+ const formatted = useMemo(() => formatResult(result), [result]);
141
+ if (!formatted) return null;
142
+ return /* @__PURE__ */ jsx("pre", {
143
+ className: "font-aai-mono text-xs px-3.5 py-3 m-0 whitespace-pre-wrap wrap-break-word",
144
+ style: { color: inkTint(theme.text, theme.surface, 75) },
145
+ children: formatted
146
+ });
147
+ }
148
+ /**
126
149
  * Renders a tool invocation as the design system's console row (see
127
150
  * `ToolCallRow`): a small outlined "TOOL" chip (or the tool's configured
128
151
  * icon), the tool name in mono, a truncated args preview, and a rotating
@@ -151,7 +174,6 @@ const ToolCallBlock = memo(function ToolCallBlock({ toolCall, className }) {
151
174
  const title = config?.label || toolCall.name;
152
175
  const icon = config?.icon;
153
176
  const canExpand = !isPending && Boolean(toolCall.result);
154
- const formatted = useMemo(() => toolCall.result ? formatResult(toolCall.result) : "", [toolCall.result]);
155
177
  const subtitle = useMemo(() => {
156
178
  const args = toolCall.args;
157
179
  if (toolCall.name === "run_code" && args.code) return truncate(String(args.code).split("\n")[0] ?? "");
@@ -175,11 +197,7 @@ const ToolCallBlock = memo(function ToolCallBlock({ toolCall, className }) {
175
197
  borderColor: theme.border
176
198
  },
177
199
  children: String(toolCall.args.code)
178
- }), formatted && /* @__PURE__ */ jsx("pre", {
179
- className: "font-aai-mono text-xs px-3.5 py-3 m-0 whitespace-pre-wrap wrap-break-word",
180
- style: { color: inkTint(theme.text, theme.surface, 75) },
181
- children: formatted
182
- })] }) : void 0
200
+ }), toolCall.result && /* @__PURE__ */ jsx(ToolCallResult, { result: toolCall.result })] }) : void 0
183
201
  });
184
202
  });
185
203
  //#endregion
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext } from "react";
2
- //#region components/tool-config-context.ts
2
+ //#region src/components/tool-config-context.ts
3
3
  /**
4
- * Context for tool display configuration. Installed by `client()` from
4
+ * Context for tool display configuration. Installed by `mountClient()` from
5
5
  * `ClientConfig.tools`; the built-in components read it via `useToolConfig`.
6
6
  *
7
7
  * @internal
package/dist/types.d.ts CHANGED
@@ -23,8 +23,8 @@ export { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
23
23
  *
24
24
  * On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
25
25
  * reason as the audio budgets above: it is a framework decision with no
26
- * `client()` field to set, and the root is the authoring surface. A custom
27
- * chrome that bypasses `client()` and opens its own microphone reaches it there
26
+ * `mountClient()` field to set, and the root is the authoring surface. A custom
27
+ * chrome that bypasses `mountClient()` and opens its own microphone reaches it there
28
28
  * alongside the providers it also needs.
29
29
  */
30
30
  export declare const VOICE_CAPTURE_CONSTRAINTS: MediaTrackConstraints;
@@ -135,11 +135,26 @@ export type SessionError = {
135
135
  readonly code: SessionErrorCode;
136
136
  /** A human-readable description of the error. */
137
137
  readonly message: string;
138
+ /**
139
+ * Whether the session is OVER.
140
+ *
141
+ * `false` means surface the message and keep the session interactive — a
142
+ * turn-level failure over a server that kept running. `true` means the call
143
+ * is dead and the microphone has been released.
144
+ *
145
+ * Required rather than optional, because the wire always carries it
146
+ * (`error.reported` declares `fatal: z.boolean()`) and a client that cannot
147
+ * tell the two apart has to guess which banner to render. It was dropped one
148
+ * line before reaching here for long enough that this type's own doc, and
149
+ * the reference page generated from it, described a field that did not
150
+ * exist.
151
+ */
152
+ readonly fatal: boolean;
138
153
  };
139
154
  /**
140
155
  * Options for creating a voice session — the shared field set accepted by
141
- * both `client()` and `createSessionCore`. The one difference: `client()`
142
- * defaults `platformUrl` from `location.href`, while `createSessionCore`
156
+ * both `mountClient()` and `createBrowserSession`. The one difference: `mountClient()`
157
+ * defaults `platformUrl` from `location.href`, while `createBrowserSession`
143
158
  * requires it.
144
159
  *
145
160
  * @public
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PACER_BURST_MS, PIPELINE_PLAYBACK_GRACE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_FILL_MS, PLAYBACK_PROGRESS_INTERVAL_MS } from "@alexkroman1/aai/internal";
2
- //#region types.ts
2
+ //#region src/types.ts
3
3
  /**
4
4
  * `getUserMedia` audio constraints for every capture path in this package.
5
5
  *
@@ -22,8 +22,8 @@ import { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
22
22
  *
23
23
  * On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
24
24
  * reason as the audio budgets above: it is a framework decision with no
25
- * `client()` field to set, and the root is the authoring surface. A custom
26
- * chrome that bypasses `client()` and opens its own microphone reaches it there
25
+ * `mountClient()` field to set, and the root is the authoring surface. A custom
26
+ * chrome that bypasses `mountClient()` and opens its own microphone reaches it there
27
27
  * alongside the providers it also needs.
28
28
  */
29
29
  const VOICE_CAPTURE_CONSTRAINTS = {
@@ -1,10 +1,10 @@
1
1
  import { useSessionSelector, useTheme } from "./context.js";
2
- import { r as inkTint } from "./_colors-j8XMToi9.js";
3
- import { t as pageBaseUrl } from "./_utils-B6498_bm.js";
2
+ import { a as inkTint, i as focusRingStyle, n as FOCUS_RING } from "./_colors-CpZO-88A.js";
3
+ import { t as pageBaseUrl } from "./_utils-DnQDM9Uy.js";
4
4
  import clsx from "clsx";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { useEffect, useRef, useState } from "react";
7
- //#region components/url-chips.tsx
7
+ //#region src/components/url-chips.tsx
8
8
  /** @jsxImportSource react */
9
9
  /** How long the "Copied" confirmation replaces the label after a click. */
10
10
  const COPIED_FEEDBACK_MS = 1500;
@@ -32,12 +32,12 @@ function UrlChip({ label, url, hint, testId, className }) {
32
32
  type: "button",
33
33
  onClick: copy,
34
34
  title: `${hint} (click to copy)\n${url}`,
35
- className: clsx("flex items-center gap-1.5 min-w-0 appearance-none m-0 px-2 py-1 rounded-aai border cursor-pointer text-[11px] leading-none font-aai-mono", "outline-none focus-visible:[outline:2px_solid] focus-visible:[outline-offset:2px]", className),
35
+ className: clsx("flex items-center gap-1.5 min-w-0 appearance-none m-0 px-2 py-1 rounded-aai border cursor-pointer text-[11px] leading-none font-aai-mono", FOCUS_RING, className),
36
36
  style: {
37
37
  background: inkTint(theme.text, theme.surface, 3),
38
38
  borderColor: theme.border,
39
39
  color: inkTint(theme.text, theme.surface, 65),
40
- outlineColor: theme.primary
40
+ ...focusRingStyle(theme.primary)
41
41
  },
42
42
  "data-testid": testId,
43
43
  children: [/* @__PURE__ */ jsx("span", {
@@ -88,7 +88,7 @@ export type UseConversationResult = {
88
88
  * Subscribe to the conversation: the interleaved exchange, the streaming
89
89
  * utterance, the live transcript and the thinking rule — with no markup.
90
90
  *
91
- * Must be used inside the provider `client()` installs.
91
+ * Must be used inside the provider `mountClient()` installs.
92
92
  *
93
93
  * @example A custom bubble, keeping every rule `<MessageList>` knows
94
94
  * ```tsx
@@ -1,13 +1,22 @@
1
1
  /**
2
2
  * The handle a page keeps on the runs it started, across a reload.
3
3
  *
4
- * `useWorkflowSubmit({ key, recover: true })` is what makes a run survivable —
5
- * the run id is that hook's own state, so a refresh loses it while the run
6
- * carries on — and the `key` is deliberately the caller's to choose, because it
7
- * is a lookup CAPABILITY: there is no per-user filtering behind `find`, so the
8
- * key IS the scoping mechanism. Choosing one is easy to get wrong in three
9
- * separate ways, and six shipped templates had each written the same twenty
10
- * lines to get it right. This is those lines.
4
+ * `useWorkflowSubmit` is what makes a run survivable — the run id is that
5
+ * hook's own state, so a refresh loses it while the run carries on — and the
6
+ * `key` is what it looks the run up BY, because it is a lookup CAPABILITY:
7
+ * there is no per-user filtering behind `find`, so the key IS the scoping
8
+ * mechanism. Choosing one is easy to get wrong in three separate ways, and six
9
+ * shipped templates had each written the same twenty lines to get it right.
10
+ * This is those lines.
11
+ *
12
+ * **`useWorkflowSubmit` now mints one for itself** ({@link useDefaultRunKey}),
13
+ * so a page resumes its own run across a reload with nothing written at the
14
+ * call site — six of six page templates passed `useRunKey()` and
15
+ * `recover: true`, which is a default in the wrong place. The hook stays
16
+ * PUBLIC for the page that wants to choose: an app with accounts passes the
17
+ * ACCOUNT's own id instead, and a run then follows the person to a new device,
18
+ * which is a promise only a login can keep; a page whose run outlives the tab
19
+ * passes `useRunKey({ storage: "local" })`.
11
20
  *
12
21
  * ## Three properties, and the rejected alternatives are why each one matters
13
22
  *
@@ -42,7 +51,9 @@
42
51
  * coming back on Friday to press Stop is the ordinary case rather than an edge
43
52
  * one, and a tab-scoped key would answer that with an empty form beside a run
44
53
  * still posting somewhere. It is as far as a key can go without a login, and no
45
- * further. `podcast-digest` is that template; the other five ship the default.
54
+ * further. `podcast-digest` is that template, and the reason this hook is still
55
+ * called by name anywhere; the other five take the tab-scoped default the
56
+ * submit hook mints for them.
46
57
  *
47
58
  * ## Anything ELSE a page stores back must be VALIDATED on read
48
59
  *
@@ -77,8 +88,31 @@
77
88
  * failing to render.
78
89
  */
79
90
  /**
80
- * A lookup key for `useWorkflowSubmit({ key, recover: true })`, stable across
81
- * reloads.
91
+ * The key `useWorkflowSubmit` uses when the page named none.
92
+ *
93
+ * Two things it does that a plain `useRunKey()` at the call site cannot, and
94
+ * both are about a page that DID name one:
95
+ *
96
+ * - **It mints nothing when the caller has a key.** Minting writes to storage,
97
+ * so an unconditional `useRunKey()` inside the hook would leave a slot behind
98
+ * on every page that passes an account id and never reads it back.
99
+ * - **It stays reactive to the caller's key.** A key that arrives late — an
100
+ * account id resolved after a login — must reach the lookup, which re-asks on
101
+ * a changed key by design; freezing it into `useState` would pin the page to
102
+ * whatever it held on its first render.
103
+ *
104
+ * The minted half is still frozen for the component's life, which is what
105
+ * `useRunKey` freezes it for: a fresh key per render would record every run
106
+ * under a name the next load cannot produce.
107
+ *
108
+ * @param explicit - The caller's own key, or undefined for a page with none.
109
+ * @returns The key to record runs under and look them up by.
110
+ *
111
+ * @internal
112
+ */
113
+ export declare function useDefaultRunKey(explicit: string | undefined): string;
114
+ /**
115
+ * A lookup key for `useWorkflowSubmit({ key })`, stable across reloads.
82
116
  *
83
117
  * @param options - See the module doc for the whole argument. The storage kind
84
118
  * is read once, when the key is minted: a value that changed afterwards would
@@ -1,5 +1,6 @@
1
1
  import { useSessionSelector } from "./context.js";
2
- //#region use-user-transcript.ts
2
+ import { useMemo } from "react";
3
+ //#region src/use-user-transcript.ts
3
4
  /**
4
5
  * `useUserTranscript` — what the caller is saying RIGHT NOW, read correctly.
5
6
  *
@@ -57,11 +58,11 @@ const TRANSCRIBING_PLACEHOLDER = "…";
57
58
  */
58
59
  function useUserTranscript() {
59
60
  const partial = useSessionSelector((snapshot) => snapshot.userTranscript);
60
- return {
61
+ return useMemo(() => ({
61
62
  speaking: partial !== null,
62
63
  text: displayText(partial),
63
64
  partial
64
- };
65
+ }), [partial]);
65
66
  }
66
67
  /** The three cases, spelled out: silent, detected-but-wordless, and words. */
67
68
  function displayText(partial) {
@@ -1,11 +1,10 @@
1
1
  /**
2
- * The two hooks a FORM needs, as against the one a status view does.
2
+ * The hook a FORM needs, as against the one a status view does.
3
3
  *
4
4
  * `useWorkflowRun` (`workflow-client.ts`) watches a run you already have.
5
- * These two are what comes before it: `useWorkflows` reads the declared
6
- * workflows so `<WorkflowFields>` can render a form from a schema, and
7
- * `useWorkflowSubmit` starts a run and hands the id straight to
8
- * `useWorkflowRun`.
5
+ * This is what comes before it: `useWorkflowSubmit` starts a run and hands the
6
+ * id straight to `useWorkflowRun`. Its sibling `useWorkflows` — the listing
7
+ * `<WorkflowFields>` renders a form from — is `use-workflows.ts`.
9
8
  *
10
9
  * ## `useWorkflowSubmit` — a form's two halves in one hook
11
10
  *
@@ -32,76 +31,10 @@
32
31
  * `wait` here when the page really does want one request, and the run is
33
32
  * followed from the same id either way.
34
33
  */
35
- import type { AnyWorkflowDef, UploadParallel, UploadProgress, WorkflowOutputOf, WorkflowSummary } from "@alexkroman1/aai/workflow-api";
34
+ import type { AnyWorkflowDef, UploadParallelOption, UploadProgress, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
36
35
  import type { FormValues } from "./components/form-types.ts";
37
36
  import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
38
37
  import type { SubmitInputOf } from "./workflow-def-types.ts";
39
- /** Options for {@link useWorkflows}. */
40
- export type UseWorkflowsOptions = {
41
- /** The client to read the listing with. Defaults to one for the page's own agent. */
42
- api?: WorkflowApi;
43
- /**
44
- * Skip the lookup entirely, reporting an empty listing that is not loading.
45
- *
46
- * For a caller that may or may not need the listing and cannot decide with a
47
- * conditional hook — `<WorkflowFields>` handed a summary rather than a name is
48
- * the one in this package. It reports `loading: false`, because a skipped
49
- * lookup is finished rather than pending.
50
- */
51
- skip?: boolean;
52
- };
53
- /** What {@link useWorkflows} reports. */
54
- export type UseWorkflowsResult = {
55
- /** The agent's declared workflows, each with the JSON Schema of its input. */
56
- workflows: WorkflowSummary[];
57
- /** True until the listing lands, so a form can hold its fields back. */
58
- loading: boolean;
59
- /** The lookup's failure. Set alongside an EMPTY list, which is why it exists. */
60
- error: string | undefined;
61
- };
62
- /**
63
- * Read the agent's declared workflows.
64
- *
65
- * What `<WorkflowFields>` renders a form FROM: each summary carries the JSON
66
- * Schema of that workflow's input, converted server-side precisely so a browser
67
- * can read it.
68
- *
69
- * The failure is reported rather than swallowed, because the alternative is an
70
- * empty list — which renders as a form with no fields and reads as "this agent
71
- * declares no workflows" about an agent that was merely unreachable.
72
- *
73
- * @example
74
- * ```tsx
75
- * import { useWorkflows } from "@alexkroman1/aai-ui";
76
- *
77
- * // A page rendering its own chrome from the listing — a picker, say. A form
78
- * // for ONE workflow wants `<WorkflowFields workflow="name" />` instead,
79
- * // which does this lookup itself.
80
- * function WorkflowPicker({ onPick }: { onPick: (name: string) => void }) {
81
- * const { workflows, loading, error } = useWorkflows();
82
- * if (loading) return <p>Loading…</p>;
83
- * if (error !== undefined) return <p role="alert">{error}</p>;
84
- * return (
85
- * <ul>
86
- * {workflows.map((summary) => (
87
- * <li key={summary.name}>
88
- * <button type="button" onClick={() => onPick(summary.name)}>
89
- * {summary.description ?? summary.name}
90
- * </button>
91
- * </li>
92
- * ))}
93
- * </ul>
94
- * );
95
- * }
96
- * ```
97
- *
98
- * @param opts - See {@link UseWorkflowsOptions}.
99
- * @returns The listing, its loading flag and its failure — see
100
- * {@link UseWorkflowsResult}.
101
- *
102
- * @public
103
- */
104
- export declare function useWorkflows(opts?: UseWorkflowsOptions): UseWorkflowsResult;
105
38
  /**
106
39
  * What {@link WorkflowSubmission.upload} reports while the bytes are going.
107
40
  *
@@ -186,6 +119,38 @@ export type WorkflowSubmission<R = unknown, I = unknown> = {
186
119
  cancel: () => Promise<boolean>;
187
120
  /** The run, once started, followed to completion. */
188
121
  run: WorkflowRun<R> | undefined;
122
+ /**
123
+ * True from `submit()` on this mount until `reset()` — did THIS page start
124
+ * the run it is showing?
125
+ *
126
+ * A page needs it to say the right sentence and cannot derive it: a run
127
+ * ADOPTED by the mount-time lookup after a reload looks exactly like one this
128
+ * page started. Six templates kept a `useState(false)` next to this hook, set
129
+ * it in their `onSubmit` and mirrored it in their `onClear` — shadow state
130
+ * for a fact only this hook can know, since it is the thing that decides
131
+ * between `submit()` and the recovery lookup. One of the six grew a fourth
132
+ * branch and had to move the whole note into its own module with its own
133
+ * spec, which is what a seam missing one layer down looks like.
134
+ *
135
+ * The RAW fact rather than a derived "recovered", deliberately: with `run`
136
+ * these are three states, not two, and the third is the one a page most needs
137
+ * to explain. `startedHere` is "you pressed the button"; `!startedHere &&
138
+ * !run` is the mount-time lookup still going; `!startedHere && run` is a run
139
+ * this browser started earlier, now in front of somebody who did not press
140
+ * anything. A boolean meaning only the last of those cannot express the
141
+ * middle one.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * declare const submission: import("@alexkroman1/aai-ui").WorkflowSubmission;
146
+ * const note = submission.startedHere
147
+ * ? "You can close this tab or reload it — this page will find the run again."
148
+ * : submission.run === undefined
149
+ * ? "Looking for a run this tab started earlier…"
150
+ * : "Still working on the run this tab started earlier. Reloading is safe.";
151
+ * ```
152
+ */
153
+ startedHere: boolean;
189
154
  /**
190
155
  * True from `submit()` until the run reaches a terminal status.
191
156
  *
@@ -229,29 +194,37 @@ export type WorkflowSubmission<R = unknown, I = unknown> = {
229
194
  export type UseWorkflowSubmitOptions = {
230
195
  /** The client to start runs with. Defaults to one for the page's own agent. */
231
196
  api?: WorkflowApi;
232
- /** Correlation key recorded with the run, for finding it again without the id. */
197
+ /**
198
+ * Correlation key recorded with the run, for finding it again without the id.
199
+ *
200
+ * **Defaulted**, to an opaque per-page key in `sessionStorage` that the next
201
+ * load produces again — `useRunKey()`'s, minted by the hook. Pass one to
202
+ * scope runs to something the page knows better: an ACCOUNT's own id, which
203
+ * is what makes a run follow the person to a new device, or
204
+ * `useRunKey({ storage: "local" })` for a run that outlives the tab by
205
+ * design. The key is a lookup CAPABILITY (there is no per-user filtering
206
+ * behind `find`), it must fit the route's 256-character bound, and anything
207
+ * derived from a person's own input both collides and carries what they
208
+ * typed — `use-run-key.ts` argues every alternative.
209
+ */
233
210
  key?: string;
234
211
  /**
235
212
  * On mount, adopt the newest run this `key` already has.
236
213
  *
237
- * **This is what makes a reload survivable.** The run id is this hook's own
238
- * state, so a refresh loses it while the run carries on — and a page that
239
- * cannot name a run cannot show it, cancel it or wake it. With a `key` and
240
- * this flag the hook asks `find(workflow, key)` once as it mounts and follows
241
- * whatever comes back, so the answer, the progress and the controls are all
242
- * there again.
243
- *
244
- * Inert without a `key`, because the key IS the lookup. Opt-in because a
245
- * `key` on its own means only "record this with the run", which is what a
246
- * page passing an account id may well want; adopting a run is a decision
247
- * about the page.
214
+ * **This is what makes a reload survivable, and it is ON.** The run id is
215
+ * this hook's own state, so a refresh loses it while the run carries on — and
216
+ * a page that cannot name a run cannot show it, cancel it or wake it. The
217
+ * hook asks `find(workflow, key)` once as it mounts and follows whatever
218
+ * comes back, so the answer, the progress and the controls are all there
219
+ * again.
248
220
  *
249
- * The key has to be one the next load can produce, and choosing it is the
250
- * caller's: it is a lookup CAPABILITY (there is no per-user filtering behind
251
- * `find`), it must fit the route's 256-character bound, and anything derived
252
- * from a person's own input both collides and carries what they typed.
253
- * `useRunKey()` is that key, and its module argues every alternative; a page
254
- * with accounts passes the account's own id instead.
221
+ * It used to be opt-in, on the argument that a `key` alone means only "record
222
+ * this with the run" — true of `ctx.workflows.start({ key })`, where there is
223
+ * no page to put a run back on, and not of a form: six of six page templates
224
+ * passed `useRunKey()` and `recover: true` together, which is a default in
225
+ * the wrong place. `false` is the opt-out, and what it buys is a form that
226
+ * always opens empty — no lookup on mount, and a live run reachable only by
227
+ * an id the page has already lost.
255
228
  */
256
229
  recover?: boolean;
257
230
  /**
@@ -273,7 +246,7 @@ export type UseWorkflowSubmitOptions = {
273
246
  * the default costs nothing where it would not have paid. See
274
247
  * `UploadOptions.parallel`.
275
248
  */
276
- parallel?: UploadParallel;
249
+ parallel?: UploadParallelOption;
277
250
  };
278
251
  /**
279
252
  * Start a workflow from a form, and follow the run it creates.