@alexkroman1/aai-ui 6.5.0 → 6.6.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.
@@ -6,7 +6,7 @@
6
6
  <title>aai</title>
7
7
  <link rel="icon" href="./favicon.ico" />
8
8
  <style>html, body { background: #FBF8F2; margin: 0; }</style>
9
- <script type="module" crossorigin src="./assets/index-aXDcAQ6M.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-CepvxbNU.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="./assets/client-audio-constants-Ck0IJO4c.js">
11
11
  <link rel="stylesheet" crossorigin href="./assets/index-BZlePk3y.css">
12
12
  </head>
@@ -1,8 +1,8 @@
1
- import { r as fetchClientConfig, t as createSessionCore } from "./session-core-ClKdVgRU.js";
1
+ import { r as fetchClientConfig, t as createSessionCore } from "./session-core-CAfYmUbg.js";
2
2
  import { SessionProvider, ThemeProvider } from "./context.js";
3
- import { t as ChatView } from "./chat-view-CK61bWWx.js";
4
- import { t as pageBaseUrl } from "./_utils-CsqbIVGI.js";
5
- import { n as ToolConfigContext } from "./tool-call-block-CrLN7xlI.js";
3
+ import { t as ChatView } from "./chat-view-ByQFf94G.js";
4
+ import { n as setPageTitle, t as pageBaseUrl } from "./_utils-B6498_bm.js";
5
+ import { n as ToolConfigContext } from "./tool-call-block-D6pTEPrT.js";
6
6
  import { SidebarLayout } from "./components/sidebar-layout.js";
7
7
  import { StartScreen } from "./components/start-screen.js";
8
8
  import { jsx } from "react/jsx-runtime";
@@ -165,7 +165,7 @@ function client(config) {
165
165
  resumeSessionId: config.resumeSessionId,
166
166
  WebSocket: config.WebSocket
167
167
  });
168
- if (config.name && typeof document !== "undefined") document.title = config.name;
168
+ setPageTitle(config.name);
169
169
  const rootNode = config.component ? createElement(config.component) : createElement(DefaultRoot, {
170
170
  platformUrl,
171
171
  name: config.name,
package/dist/hooks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useSessionSelector } from "./context.js";
2
- import { r as tryParseJSON } from "./_utils-CsqbIVGI.js";
2
+ import { i as tryParseJSON } from "./_utils-B6498_bm.js";
3
3
  import { useEffect, useRef } from "react";
4
4
  //#region hooks.ts
5
5
  /** Index of the first item whose sequence number is above the watermark (tail scan from the end). */
@@ -44,8 +44,9 @@ function processToolCallTail(toolCalls, cursor, isSettled, onNew) {
44
44
  * and invokes `fire` once per tool call reaching `status`.
45
45
  */
46
46
  function useToolCallEffect(status, args, fire) {
47
- const filterName = typeof args[0] === "string" ? args[0] : null;
48
- const callback = typeof args[0] === "string" ? args[1] : args[0];
47
+ const first = args[0];
48
+ const filterName = typeof first === "string" ? first : null;
49
+ const callback = filterName === null ? first : args[1];
49
50
  const toolCalls = useSessionSelector((s) => s.toolCalls);
50
51
  const cursorRef = useRef({
51
52
  seq: 0,
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import { i as loadClientConfig, n as buildAgentUrl, r as fetchClientConfig, t as createSessionCore } from "./session-core-ClKdVgRU.js";
2
- import { n as Markdown, r as AutoScroll, t as MessageList } from "./message-list-BwA3rdPi.js";
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
3
  import { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
4
4
  import { r as inkTint } from "./_colors-CcAi2FOU.js";
5
5
  import { Button } from "./components/button.js";
6
- import { t as ChatView } from "./chat-view-CK61bWWx.js";
7
- import { t as pageBaseUrl } from "./_utils-CsqbIVGI.js";
8
- import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-Cy_YVfsa.js";
9
- import { n as ToolConfigContext, r as ToolCallRow } from "./tool-call-block-CrLN7xlI.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";
10
10
  import { SidebarLayout } from "./components/sidebar-layout.js";
11
11
  import { StartScreen } from "./components/start-screen.js";
12
12
  import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
@@ -526,6 +526,7 @@ function UploadProgressBar({ upload, className }) {
526
526
  if (!upload) return null;
527
527
  const { name, index, count, loaded, total, fraction } = upload;
528
528
  const percent = fraction === void 0 ? void 0 : Math.round(fraction * 100);
529
+ const faint = inkTint(theme.text, theme.surface, 65);
529
530
  return /* @__PURE__ */ jsxs("div", {
530
531
  className: clsx(className ?? "flex flex-col gap-1.5"),
531
532
  children: [/* @__PURE__ */ jsxs("div", {
@@ -533,11 +534,11 @@ function UploadProgressBar({ upload, className }) {
533
534
  children: [/* @__PURE__ */ jsx("span", {
534
535
  id: labelId,
535
536
  className: "truncate",
536
- style: { color: inkTint(theme.text, theme.surface, 65) },
537
+ style: { color: faint },
537
538
  children: count > 1 ? `Uploading ${name} (${index} of ${count})` : `Uploading ${name}`
538
539
  }), /* @__PURE__ */ jsx("span", {
539
540
  className: "shrink-0 tabular-nums",
540
- style: { color: inkTint(theme.text, theme.surface, 65) },
541
+ style: { color: faint },
541
542
  children: total === void 0 ? formatBytes(loaded) : `${formatBytes(loaded)} of ${formatBytes(total)}`
542
543
  })]
543
544
  }), /* @__PURE__ */ jsx("div", {
@@ -649,6 +650,25 @@ function filesOf(value) {
649
650
  const files = value.filter((one) => one instanceof File);
650
651
  return files.length > 0 && files.length === value.length ? files : [];
651
652
  }
653
+ /**
654
+ * The input properties still carrying a `File` — i.e. the ones that CANNOT survive
655
+ * being sent.
656
+ *
657
+ * A run input is JSON, and `JSON.stringify(new File(…))` is `{}` — no `toJSON`, no
658
+ * own enumerable properties. So a File left in a payload does not fail to send: it
659
+ * arrives as an empty object, and the workflow rejects it against whatever its own
660
+ * schema says the property should be. Measured in production as
661
+ * `Invalid input for workflow "transcribe": recording: Invalid input` — a message
662
+ * about a type, on a form where the user had picked a perfectly good file.
663
+ *
664
+ * Exported beside {@link filesOf} because it is the same question asked at the
665
+ * other end: that one decides which fields to UPLOAD, this one checks that none
666
+ * were missed. Both hooks are the callers.
667
+ */
668
+ function fileFields(input) {
669
+ if (!isRecord(input)) return [];
670
+ return Object.entries(input).filter(([, value]) => filesOf(value).length > 0).map(([key]) => key);
671
+ }
652
672
  //#endregion
653
673
  //#region _repeat-until.ts
654
674
  /**
@@ -1137,10 +1157,10 @@ function useWorkflowSubmit(workflow, opts = {}) {
1137
1157
  const [startError, setStartError] = useState(void 0);
1138
1158
  const [upload, setUpload] = useState(void 0);
1139
1159
  const getClient = useWorkflowApiRef(api);
1140
- const tracked = useWorkflowRun(runId, {
1141
- ...api && { api },
1142
- ...omitUndefined({ intervalMs })
1143
- });
1160
+ const tracked = useWorkflowRun(runId, omitUndefined({
1161
+ api,
1162
+ intervalMs
1163
+ }));
1144
1164
  return {
1145
1165
  submit: useCallback(async (input) => {
1146
1166
  const client = getClient();
@@ -1247,7 +1267,8 @@ function SchemaField({ name, property, required, upload = false }) {
1247
1267
  ...hint,
1248
1268
  ...defaults
1249
1269
  });
1250
- switch (typeOf(property)) {
1270
+ const type = typeOf(property);
1271
+ switch (type) {
1251
1272
  case "boolean": return /* @__PURE__ */ jsx(CheckboxField, {
1252
1273
  name,
1253
1274
  label,
@@ -1259,7 +1280,7 @@ function SchemaField({ name, property, required, upload = false }) {
1259
1280
  name,
1260
1281
  label,
1261
1282
  required,
1262
- step: typeOf(property) === "integer" ? 1 : "any",
1283
+ step: type === "integer" ? 1 : "any",
1263
1284
  ...hint,
1264
1285
  ...defaults
1265
1286
  });
@@ -1546,7 +1567,7 @@ function useWorkflowProgress(runId, opts = {}) {
1546
1567
  * @public
1547
1568
  */
1548
1569
  function WorkflowProgress({ runId, api, className, placeholder }) {
1549
- const { progress, streaming, supported } = useWorkflowProgress(runId, api ? { api } : {});
1570
+ const { progress, streaming, supported } = useWorkflowProgress(runId, omitUndefined({ api }));
1550
1571
  if (!supported || progress.length === 0) return placeholder ?? null;
1551
1572
  return /* @__PURE__ */ jsxs("pre", {
1552
1573
  className: clsx(className ?? "whitespace-pre-wrap border-l pl-4 text-xs opacity-70"),
@@ -1610,7 +1631,7 @@ function WorkflowProgress({ runId, api, className, placeholder }) {
1610
1631
  */
1611
1632
  function page(config) {
1612
1633
  const container = resolveContainer(config.target);
1613
- if (config.name && typeof document !== "undefined") document.title = config.name;
1634
+ setPageTitle(config.name);
1614
1635
  return mountRoot(container, createElement(ThemeProvider, { value: config.theme }, createElement(config.component)));
1615
1636
  }
1616
1637
  //#endregion
@@ -1828,14 +1849,18 @@ function useWorkflowRuns(workflow, opts = {}) {
1828
1849
  * - **Reporting the bytes.** The same `UploadStatus` `useWorkflowSubmit` reports,
1829
1850
  * so `<UploadProgressBar>` renders either without knowing which hook it came from.
1830
1851
  *
1831
- * ## A failed upload CANCELS the run
1852
+ * ## A failed upload is RESUMED once, and only then cancels the run
1832
1853
  *
1833
1854
  * An upload that dies stays in the store, incomplete, and `complete` never becomes
1834
1855
  * true — so a run left behind polls until its own abandonment bound and then fails,
1835
- * minutes after the page has already reported the error. Cancelling is the honest
1836
- * end to a submission that did not happen. The cost is that work already done is
1837
- * thrown away with the run; a caller who wants to resume instead drives
1838
- * `api.uploadStream` and `api.uploadInfo` directly.
1856
+ * minutes after the page has already reported the error.
1857
+ *
1858
+ * That used to be the whole story, and it threw away a run and a file together for
1859
+ * what is usually one dropped connection near the end. So a failure gets one more
1860
+ * attempt with `resume: true`, which sends only the windows the store does not
1861
+ * already have (`UploadInfo.ranges`) — the run is still waiting on the same id, so
1862
+ * a resume that succeeds is invisible to it. Cancelling is what happens when THAT
1863
+ * fails too, and it is the honest end to a submission that did not happen.
1839
1864
  */
1840
1865
  /**
1841
1866
  * Start a workflow run and stream a file into it while it works.
@@ -1869,10 +1894,10 @@ function useWorkflowStream(workflow, opts = {}) {
1869
1894
  const [startError, setStartError] = useState(void 0);
1870
1895
  const [upload, setUpload] = useState(void 0);
1871
1896
  const getClient = useWorkflowApiRef(api);
1872
- const tracked = useWorkflowRun(runId, {
1873
- ...api && { api },
1874
- ...omitUndefined({ intervalMs })
1875
- });
1897
+ const tracked = useWorkflowRun(runId, omitUndefined({
1898
+ api,
1899
+ intervalMs
1900
+ }));
1876
1901
  return {
1877
1902
  submit: useCallback(async (input) => {
1878
1903
  const client = getClient();
@@ -1888,19 +1913,26 @@ function useWorkflowStream(workflow, opts = {}) {
1888
1913
  ...input,
1889
1914
  [field]: id
1890
1915
  } : input;
1916
+ assertSendable(workflow, payload, field);
1891
1917
  started = await client.start(workflow, payload, omitUndefined({ key }));
1892
1918
  setRunId(started);
1893
1919
  if (!chosen) return;
1894
- await client.uploadStream(id, chosen, {
1895
- name: chosen.name,
1896
- onProgress: (progress) => setUpload({
1897
- ...progress,
1920
+ const send = async (resume) => {
1921
+ await client.uploadStream(id, chosen, {
1898
1922
  name: chosen.name,
1899
- index: 1,
1900
- count: 1
1901
- }),
1902
- ...omitUndefined({ parallel })
1903
- });
1923
+ onProgress: (progress) => setUpload({
1924
+ ...progress,
1925
+ name: chosen.name,
1926
+ index: 1,
1927
+ count: 1
1928
+ }),
1929
+ ...omitUndefined({
1930
+ parallel,
1931
+ resume: resume ? true : void 0
1932
+ })
1933
+ });
1934
+ };
1935
+ await send(false).catch(async () => await send(true));
1904
1936
  await client.wake(started).catch(() => void 0);
1905
1937
  } catch (err) {
1906
1938
  setStartError(errorMessage(err));
@@ -1926,6 +1958,33 @@ function useWorkflowStream(workflow, opts = {}) {
1926
1958
  error: startError ?? tracked.error
1927
1959
  };
1928
1960
  }
1961
+ /**
1962
+ * Refuse a payload carrying a `File`, before a run is started over it.
1963
+ *
1964
+ * A File cannot be SENT: a run input is JSON and `JSON.stringify(new File(…))` is
1965
+ * `{}` (see `fileFields`). So without this the run starts, rejects its own input,
1966
+ * and reports a type error about a property the user filled in correctly — which is
1967
+ * what production did, five times: `Invalid input for workflow "transcribe":
1968
+ * recording: Invalid input`, from a page whose file picker was working.
1969
+ *
1970
+ * The reachable cause is a workflow whose listing declares no `uploads` for the
1971
+ * property the form put a file in — one built before the declaration existed, or a
1972
+ * page pointing at the wrong workflow — so the message names BOTH halves: the
1973
+ * property that carries a file, and the one (if any) the workflow says is its
1974
+ * upload. A message naming only one of them leaves the reader guessing which end
1975
+ * to change.
1976
+ *
1977
+ * A throw rather than a silent repair: this hook cannot know whether the right fix
1978
+ * is to declare the property or to stop sending the file, and guessing either way
1979
+ * would start a run the caller did not mean.
1980
+ */
1981
+ function assertSendable(workflow, payload, field) {
1982
+ const unsendable = fileFields(payload);
1983
+ if (unsendable.length === 0) return;
1984
+ const carries = unsendable.length === 1 ? "carries a file" : "carry files";
1985
+ const declares = field === void 0 ? "" : ` (it declares "${field}")`;
1986
+ throw new Error(`Cannot start "${workflow}": ${unsendable.join(", ")} ${carries} the workflow does not declare as an upload${declares}. Add the property to \`workflow({ uploads: [...] })\`, or submit an upload id.`);
1987
+ }
1929
1988
  /** A fresh upload id: a capability, so it is random rather than derived. */
1930
1989
  function randomUploadId() {
1931
1990
  return crypto.randomUUID().replaceAll("-", "");
@@ -1,6 +1,6 @@
1
1
  import { useSessionSelector, useTheme } from "./context.js";
2
2
  import { i as primaryTint, r as inkTint } from "./_colors-CcAi2FOU.js";
3
- import { t as ToolCallBlock } from "./tool-call-block-CrLN7xlI.js";
3
+ import { t as ToolCallBlock } from "./tool-call-block-D6pTEPrT.js";
4
4
  import clsx from "clsx";
5
5
  import { StickToBottom } from "use-stick-to-bottom";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -232,12 +232,6 @@ function openReconnectingSocket(urlProvider) {
232
232
  return new ReconnectingWebSocket(urlProvider, void 0, RECONNECT_OPTIONS);
233
233
  }
234
234
  /**
235
- * True while `socket` is a reconnecting socket that will retry after the
236
- * close event currently being handled. partysocket schedules the retry
237
- * *before* dispatching `close`, so `retryCount` already names the attempt
238
- * just scheduled — at `maxRetries` it has given up.
239
- */
240
- /**
241
235
  * Force a fresh connection attempt on a reconnecting socket, reporting
242
236
  * whether it was one. Used for a failure partysocket cannot see: a socket it
243
237
  * considers open and healthy, whose peer never completed OUR handshake.
@@ -249,6 +243,12 @@ function forceReconnect(socket) {
249
243
  socket.reconnect();
250
244
  return true;
251
245
  }
246
+ /**
247
+ * True while `socket` is a reconnecting socket that will retry after the
248
+ * close event currently being handled. partysocket schedules the retry
249
+ * *before* dispatching `close`, so `retryCount` already names the attempt
250
+ * just scheduled — at `maxRetries` it has given up.
251
+ */
252
252
  function reconnectPending(socket) {
253
253
  return socket instanceof ReconnectingWebSocket && socket.shouldReconnect && socket.retryCount < RECONNECT_OPTIONS.maxRetries;
254
254
  }
@@ -706,16 +706,15 @@ function createMessageHandlers(deps) {
706
706
  case "tool.completed": {
707
707
  const tcs = getSnapshot().toolCalls;
708
708
  const idx = tcs.findIndex((tc) => tc.callId === e.toolCallId);
709
- if (idx !== -1) {
710
- const updated = [...tcs];
711
- const existing = updated[idx];
712
- if (existing) updated[idx] = {
713
- ...existing,
714
- status: "done",
715
- result: e.result
716
- };
717
- updateState({ toolCalls: updated });
718
- }
709
+ const existing = tcs[idx];
710
+ if (!existing) break;
711
+ const updated = [...tcs];
712
+ updated[idx] = {
713
+ ...existing,
714
+ status: "done",
715
+ result: e.result
716
+ };
717
+ updateState({ toolCalls: updated });
719
718
  break;
720
719
  }
721
720
  case "reply.completed":
@@ -765,7 +764,7 @@ function createMessageHandlers(deps) {
765
764
  handleErrorEvent(e);
766
765
  break;
767
766
  case "session.timed-out":
768
- deps.conn.retiredByServer = true;
767
+ conn.retiredByServer = true;
769
768
  break;
770
769
  default: break;
771
770
  }
@@ -949,13 +948,24 @@ function createSessionCore(options) {
949
948
  function resetState() {
950
949
  updateState(CLEARED_SESSION_STATE);
951
950
  }
951
+ /**
952
+ * The socket, when there is one and it can carry a frame.
953
+ *
954
+ * Returning the socket rather than a boolean because every caller needs it
955
+ * next, and a predicate does not narrow `conn.ws` across the call. One
956
+ * spelling of the readiness test, which four call sites used to repeat.
957
+ */
958
+ function openSocket() {
959
+ return conn.ws?.readyState === WS_OPEN ? conn.ws : null;
960
+ }
952
961
  function sendJson(msg) {
953
- if (conn.ws && conn.ws.readyState === WS_OPEN) conn.ws.send(JSON.stringify(msg));
962
+ openSocket()?.send(JSON.stringify(msg));
954
963
  }
955
964
  function sendAudio(bytes) {
956
- if (!conn.ws || conn.ws.readyState !== WS_OPEN) return;
957
- if (conn.ws.bufferedAmount > MIC_SEND_MAX_BUFFERED_BYTES) return;
958
- conn.ws.send(bytes);
965
+ const ws = openSocket();
966
+ if (!ws) return;
967
+ if (ws.bufferedAmount > MIC_SEND_MAX_BUFFERED_BYTES) return;
968
+ ws.send(bytes);
959
969
  }
960
970
  const { handleMessage, settleWhenAudioDrained } = createMessageHandlers({
961
971
  getSnapshot,
@@ -1090,7 +1100,7 @@ function createSessionCore(options) {
1090
1100
  }, { signal: sig });
1091
1101
  }
1092
1102
  function cancel() {
1093
- if (!conn.ws || conn.ws.readyState !== WS_OPEN) return;
1103
+ if (!openSocket()) return;
1094
1104
  conn.turn.bump();
1095
1105
  conn.voiceIO?.flush();
1096
1106
  updateState({ state: "listening" });
@@ -1099,7 +1109,7 @@ function createSessionCore(options) {
1099
1109
  function reset() {
1100
1110
  conn.turn.bump();
1101
1111
  conn.voiceIO?.flush();
1102
- if (conn.ws && conn.ws.readyState === WS_OPEN) {
1112
+ if (openSocket()) {
1103
1113
  sendJson({ type: "reset" });
1104
1114
  return;
1105
1115
  }
@@ -6,12 +6,6 @@ import type { WebSocketConstructor } from "./types.ts";
6
6
  * session started with.
7
7
  */
8
8
  export declare function openReconnectingSocket(urlProvider: () => Promise<string>): InstanceType<WebSocketConstructor>;
9
- /**
10
- * True while `socket` is a reconnecting socket that will retry after the
11
- * close event currently being handled. partysocket schedules the retry
12
- * *before* dispatching `close`, so `retryCount` already names the attempt
13
- * just scheduled — at `maxRetries` it has given up.
14
- */
15
9
  /**
16
10
  * Force a fresh connection attempt on a reconnecting socket, reporting
17
11
  * whether it was one. Used for a failure partysocket cannot see: a socket it
@@ -20,4 +14,10 @@ export declare function openReconnectingSocket(urlProvider: () => Promise<string
20
14
  * reconnect machinery — the caller then treats the failure as terminal.
21
15
  */
22
16
  export declare function forceReconnect(socket: unknown): boolean;
17
+ /**
18
+ * True while `socket` is a reconnecting socket that will retry after the
19
+ * close event currently being handled. partysocket schedules the retry
20
+ * *before* dispatching `close`, so `retryCount` already names the attempt
21
+ * just scheduled — at `maxRetries` it has given up.
22
+ */
23
23
  export declare function reconnectPending(socket: unknown): boolean;
@@ -1,2 +1,2 @@
1
- import { t as createSessionCore } from "./session-core-ClKdVgRU.js";
1
+ import { t as createSessionCore } from "./session-core-CAfYmUbg.js";
2
2
  export { createSessionCore };
@@ -1,7 +1,7 @@
1
1
  import { useTheme } from "./context.js";
2
2
  import { r as inkTint } from "./_colors-CcAi2FOU.js";
3
3
  import { t as Eyebrow } from "./eyebrow-C6ZFuiz6.js";
4
- import { n as truncate, r as tryParseJSON } from "./_utils-CsqbIVGI.js";
4
+ import { i as tryParseJSON, r as truncate } from "./_utils-B6498_bm.js";
5
5
  import clsx from "clsx";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { createContext, memo, useContext, useMemo, useState } from "react";
@@ -40,6 +40,7 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
40
40
  const theme = useTheme();
41
41
  const sizes = VARIANT_CLASSES[variant];
42
42
  const canExpand = children != null;
43
+ const faint = inkTint(theme.text, theme.bg, 65);
43
44
  return /* @__PURE__ */ jsxs("div", {
44
45
  className: clsx("flex flex-col rounded-md border overflow-hidden", className),
45
46
  style: {
@@ -60,7 +61,7 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
60
61
  children: icon
61
62
  }) : /* @__PURE__ */ jsx(Eyebrow, {
62
63
  className: "shrink-0",
63
- style: { color: inkTint(theme.text, theme.bg, 65) },
64
+ style: { color: faint },
64
65
  children: "Tool"
65
66
  }),
66
67
  /* @__PURE__ */ jsx("span", {
@@ -70,12 +71,12 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
70
71
  }),
71
72
  /* @__PURE__ */ jsx("span", {
72
73
  className: clsx("font-aai-mono truncate flex-1 min-w-0", sizes.title),
73
- style: { color: inkTint(theme.text, theme.bg, 65) },
74
+ style: { color: faint },
74
75
  children: detail
75
76
  }),
76
77
  canExpand && /* @__PURE__ */ jsx("span", {
77
78
  className: clsx("shrink-0 transition-transform duration-150", sizes.chevron, isOpen && "rotate-90"),
78
- style: { color: inkTint(theme.text, theme.bg, 65) },
79
+ style: { color: faint },
79
80
  children: "▶"
80
81
  })
81
82
  ]
@@ -141,13 +141,14 @@ export type UseWorkflowSubmitOptions = {
141
141
  /**
142
142
  * Send each chosen file as concurrent parts instead of in one request.
143
143
  *
144
- * `true` for the defaults, or `{ partBytes, concurrency }` to tune them. This is
145
- * the wait a form with a recording in it actually spends: the run does not exist
146
- * until its input is stored, so until the last byte lands there is no run to
147
- * watch and nothing for `<WorkflowProgress>` to say. Splitting the file across
148
- * connections is what makes that stretch shorter, and it degrades to the single
149
- * request wherever it would not help — a small file, an older agent — so turning
150
- * it on is safe for every form. See `UploadOptions.parallel`.
144
+ * **On by default.** `false` opts out, `{ partBytes, concurrency }` tunes it.
145
+ * This is the wait a form with a recording in it actually spends: the run does
146
+ * not exist until its input is stored, so until the last byte lands there is no
147
+ * run to watch and nothing for `<WorkflowProgress>` to say. Splitting the file
148
+ * across connections is what makes that stretch shorter, and it degrades to the
149
+ * single request wherever it would not help — a small file, an older agent — so
150
+ * the default costs nothing where it would not have paid. See
151
+ * `UploadOptions.parallel`.
151
152
  */
152
153
  parallel?: UploadParallel;
153
154
  };
@@ -48,14 +48,18 @@
48
48
  * - **Reporting the bytes.** The same `UploadStatus` `useWorkflowSubmit` reports,
49
49
  * so `<UploadProgressBar>` renders either without knowing which hook it came from.
50
50
  *
51
- * ## A failed upload CANCELS the run
51
+ * ## A failed upload is RESUMED once, and only then cancels the run
52
52
  *
53
53
  * An upload that dies stays in the store, incomplete, and `complete` never becomes
54
54
  * true — so a run left behind polls until its own abandonment bound and then fails,
55
- * minutes after the page has already reported the error. Cancelling is the honest
56
- * end to a submission that did not happen. The cost is that work already done is
57
- * thrown away with the run; a caller who wants to resume instead drives
58
- * `api.uploadStream` and `api.uploadInfo` directly.
55
+ * minutes after the page has already reported the error.
56
+ *
57
+ * That used to be the whole story, and it threw away a run and a file together for
58
+ * what is usually one dropped connection near the end. So a failure gets one more
59
+ * attempt with `resume: true`, which sends only the windows the store does not
60
+ * already have (`UploadInfo.ranges`) — the run is still waiting on the same id, so
61
+ * a resume that succeeds is invisible to it. Cancelling is what happens when THAT
62
+ * fails too, and it is the honest end to a submission that did not happen.
59
63
  */
60
64
  import type { UploadParallel } from "@alexkroman1/aai/workflow-api";
61
65
  import type { UploadStatus } from "./use-workflow-form.ts";
@@ -77,8 +81,8 @@ export type UseWorkflowStreamOptions = {
77
81
  * the uplink sees the same growing file whether one connection or four are
78
82
  * filling it. What changes is only how fast it grows.
79
83
  *
80
- * `true` for the defaults, or `{ partBytes, concurrency }` to tune them. See
81
- * `UploadOptions.parallel`.
84
+ * **On by default.** `false` opts out, `{ partBytes, concurrency }` tunes it.
85
+ * See `UploadOptions.parallel`.
82
86
  */
83
87
  parallel?: UploadParallel;
84
88
  };
@@ -1,7 +1,8 @@
1
1
  import { MIC_BUFFER_SECONDS, MIC_SILENCE_PROBE_MS } from "../types.js";
2
2
  import { t as workletModuleUrl } from "../_module-url-C_4gRVL0.js";
3
3
  //#region worklets/capture-processor.ts
4
- const CaptureProcessorWorklet = `
4
+ /** Raw worklet source — exported so tests can evaluate the processor directly. */
5
+ const captureProcessorSource = `
5
6
  class CaptureProcessor extends AudioWorkletProcessor {
6
7
  constructor(options) {
7
8
  super();
@@ -91,8 +92,6 @@ class CaptureProcessor extends AudioWorkletProcessor {
91
92
 
92
93
  registerProcessor('capture-processor', CaptureProcessor);
93
94
  `;
94
- /** Raw worklet source — exported so tests can evaluate the processor directly. */
95
- const captureProcessorSource = CaptureProcessorWorklet;
96
- var capture_processor_default = workletModuleUrl(CaptureProcessorWorklet);
95
+ var capture_processor_default = workletModuleUrl(captureProcessorSource);
97
96
  //#endregion
98
97
  export { captureProcessorSource, capture_processor_default as default };
@@ -1,7 +1,8 @@
1
1
  import { PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_FILL_MS, PLAYBACK_PROGRESS_INTERVAL_MS } from "../types.js";
2
2
  import { t as workletModuleUrl } from "../_module-url-C_4gRVL0.js";
3
3
  //#region worklets/playback-processor.ts
4
- const PlaybackProcessorWorklet = `
4
+ /** Raw worklet source — exported so tests can evaluate the processor directly. */
5
+ const playbackProcessorSource = `
5
6
  class PlaybackProcessor extends AudioWorkletProcessor {
6
7
  constructor(options) {
7
8
  super();
@@ -315,8 +316,6 @@ class PlaybackProcessor extends AudioWorkletProcessor {
315
316
 
316
317
  registerProcessor('playback-processor', PlaybackProcessor);
317
318
  `;
318
- /** Raw worklet source — exported so tests can evaluate the processor directly. */
319
- const playbackProcessorSource = PlaybackProcessorWorklet;
320
- var playback_processor_default = workletModuleUrl(PlaybackProcessorWorklet);
319
+ var playback_processor_default = workletModuleUrl(playbackProcessorSource);
321
320
  //#endregion
322
321
  export { playback_processor_default as default, playbackProcessorSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-ui",
3
- "version": "6.5.0",
3
+ "version": "6.6.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -29,7 +29,7 @@
29
29
  "remark-gfm": "^4.0.1",
30
30
  "use-stick-to-bottom": "^1.1.6",
31
31
  "use-sync-external-store": "^1.6.0",
32
- "@alexkroman1/aai": "6.5.0"
32
+ "@alexkroman1/aai": "6.6.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^19.0.0",
@@ -1 +0,0 @@
1
- import{o as e,s as t}from"./client-audio-constants-Ck0IJO4c.js";var n={echoCancellation:!0,noiseSuppression:!1,autoGainControl:!1,voiceIsolation:!1};function r(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}function i(e){e.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{})}function a(e,t,n){let r=new AudioWorkletNode(e,`capture-processor`,{channelCount:1,channelCountMode:`explicit`}),i=null;return r.port.onmessage=e=>{let r=e.data;r.event===`chunk`&&r.buffer?t(r.buffer):r.event===`silent`?n?.():r.event===`stopped`&&(i?.(),i=null)},{node:r,start(){r.port.postMessage({event:`start`})},stop(){let{promise:e,resolve:t}=Promise.withResolvers(),n=setTimeout(t,250);return i=()=>{clearTimeout(n),t()},r.port.postMessage({event:`stop`}),e}}}async function o(o){let{sttSampleRate:s,ttsSampleRate:c,captureWorkletSrc:l,playbackWorkletSrc:u,onMicData:d,onError:f,onPlaybackStats:p,onPlaybackProgress:m,onMicSilent:h}=o,g=new AudioContext({sampleRate:c,latencyHint:`playback`}),_=s===c,v=_?g:new AudioContext({sampleRate:s,latencyHint:`interactive`});async function y(){let e=_?[g]:[g,v];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let b=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...n}}),x;try{[x]=await Promise.all([b,g.resume(),v.resume(),v.audioWorklet.addModule(l),g.audioWorklet.addModule(u)]),r(v.sampleRate,s,`capture`),r(g.sampleRate,c,`playback`)}catch(e){throw i(b),await y(),e}let S=v.createMediaStreamSource(x),C=a(v,d,h);S.connect(C.node),C.node.onprocessorerror=()=>{let e=Error(`Audio capture worklet crashed`);console.error(`[aai-ui]`,e.message),f?.(e)},C.start();let w=null,T=null,E=0,D=null,O=new AbortController;function k(){T?.(),T=null,D=null}function A(e){e.stats&&e.stats.concealedSamples>0&&p?.(e.stats),e.reason!==`interrupt`&&(D===null||e.turn===D)&&k()}function j(){if(w)return w;let e=new AudioWorkletNode(g,`playback-processor`);return e.connect(g.destination),e.port.onmessage=e=>{e.data.event===`stop`?A(e.data):e.data.event===`progress`&&m?.(e.data.bufferedMs)},e.onprocessorerror=()=>{let e=Error(`Audio playback worklet crashed`);console.error(`[aai-ui]`,e.message),k(),f?.(e)},w=e,e}let M={enqueue(e){O.signal.aborted||e.byteLength!==0&&j().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){if(!w)return Promise.resolve();let n=++E;if(w.port.postMessage({event:`done`,turn:n}),g.state!==`running`)return D=null,Promise.resolve();let{promise:r,resolve:i}=Promise.withResolvers();T?.();let a=()=>{clearInterval(o),clearTimeout(s),T===a&&(T=null,D=null),i()},o=setInterval(()=>{g.state!==`running`&&a()},t),s=setTimeout(a,e);return T=a,D=n,r},flush(){w&&(k(),w.port.postMessage({event:`interrupt`}))},async close(){if(!O.signal.aborted){O.abort(),await C.stop(),S.disconnect(),C.node.disconnect(),w&&w.disconnect();for(let e of x.getTracks())e.stop();await y()}},async[Symbol.asyncDispose](){await M.close()}};return M}export{o as createVoiceIO};