basalt-ui 1.12.0 → 1.13.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 (105) hide show
  1. package/AGENTS.md +21 -21
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +38 -35
  4. package/agent/rules/basalt-agent.md +14 -0
  5. package/configs/oxlint-plugin.js +7 -3
  6. package/configs/oxlint-plugin.test.ts +34 -5
  7. package/configs/oxlint.json +4 -4
  8. package/dist/agent/adapter.d.ts +5 -1
  9. package/dist/agent/adapter.d.ts.map +1 -1
  10. package/dist/agent/adapter.js +22 -13
  11. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  12. package/dist/agent/ai-sdk-transport.js +6 -2
  13. package/dist/agent/id.d.ts +67 -19
  14. package/dist/agent/id.d.ts.map +1 -1
  15. package/dist/agent/id.js +21 -7
  16. package/dist/agent/thread.d.ts +4 -0
  17. package/dist/agent/thread.d.ts.map +1 -1
  18. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  19. package/dist/agent/use-agent-stream.js +2 -2
  20. package/dist/agent/use-agent-thread-runs.d.ts +24 -3
  21. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  22. package/dist/agent/use-agent-thread-runs.js +55 -15
  23. package/dist/agent-chat/index.d.ts +4 -0
  24. package/dist/agent-chat/index.d.ts.map +1 -1
  25. package/dist/agent-chat/index.js +2 -0
  26. package/dist/agent-chat/message-affordances.d.ts +59 -0
  27. package/dist/agent-chat/message-affordances.d.ts.map +1 -0
  28. package/dist/agent-chat/message-affordances.js +7 -0
  29. package/dist/agent-chat/relative-time.d.ts +27 -0
  30. package/dist/agent-chat/relative-time.d.ts.map +1 -0
  31. package/dist/agent-chat/relative-time.js +27 -0
  32. package/dist/agent-chat/relative-time.test.d.ts +2 -0
  33. package/dist/agent-chat/relative-time.test.d.ts.map +1 -0
  34. package/dist/agent-chat/thread-feed-row.d.ts +65 -0
  35. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
  36. package/dist/agent-chat/thread-feed-row.js +156 -0
  37. package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
  38. package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
  39. package/dist/agent-chat/thread-feed.d.ts +34 -5
  40. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  41. package/dist/agent-chat/thread-feed.js +52 -19
  42. package/dist/agent-chat/thread-feed.test.d.ts +2 -0
  43. package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
  44. package/dist/agent-chat/thread-message.d.ts +183 -2
  45. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  46. package/dist/agent-chat/thread-message.js +315 -34
  47. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  48. package/dist/agent-chat/thread-outcome-card.js +1 -16
  49. package/dist/agent-chat/thread-workspace.d.ts +14 -2
  50. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  51. package/dist/agent-chat/thread-workspace.js +29 -3
  52. package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
  53. package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -0
  54. package/dist/agent-chat/virtualize.d.ts +123 -0
  55. package/dist/agent-chat/virtualize.d.ts.map +1 -0
  56. package/dist/agent-chat/virtualize.js +9 -0
  57. package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
  58. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +1 -0
  59. package/dist/data/index.d.ts +5 -1
  60. package/dist/data/index.d.ts.map +1 -1
  61. package/dist/data/virtual-list.d.ts.map +1 -1
  62. package/dist/data/virtual.d.ts +2 -1
  63. package/dist/data/virtual.d.ts.map +1 -1
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -0
  67. package/dist/surfaces.d.ts +2 -2
  68. package/dist/surfaces.d.ts.map +1 -1
  69. package/dist/surfaces.js +3 -2
  70. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  71. package/llms.txt +6 -6
  72. package/package.json +2 -2
  73. package/src/agent/adapter.test.ts +237 -0
  74. package/src/agent/adapter.ts +72 -14
  75. package/src/agent/ai-sdk-transport.test.ts +64 -0
  76. package/src/agent/ai-sdk-transport.ts +8 -2
  77. package/src/agent/id.test.ts +95 -1
  78. package/src/agent/id.ts +98 -28
  79. package/src/agent/thread.ts +4 -0
  80. package/src/agent/use-agent-stream.test.tsx +22 -0
  81. package/src/agent/use-agent-stream.ts +6 -3
  82. package/src/agent/use-agent-thread-runs.test.tsx +326 -0
  83. package/src/agent/use-agent-thread-runs.ts +159 -23
  84. package/src/agent/use-agent-thread-runs.wedge.test.tsx +80 -0
  85. package/src/agent-chat/index.ts +12 -0
  86. package/src/agent-chat/message-affordances.ts +61 -0
  87. package/src/agent-chat/relative-time.test.ts +95 -0
  88. package/src/agent-chat/relative-time.ts +55 -0
  89. package/src/agent-chat/thread-feed-row.test.tsx +510 -0
  90. package/src/agent-chat/thread-feed-row.tsx +274 -0
  91. package/src/agent-chat/thread-feed.test.tsx +241 -0
  92. package/src/agent-chat/thread-feed.tsx +119 -18
  93. package/src/agent-chat/thread-message.test.tsx +937 -3
  94. package/src/agent-chat/thread-message.tsx +766 -38
  95. package/src/agent-chat/thread-outcome-card.tsx +1 -25
  96. package/src/agent-chat/thread-workspace.test.tsx +169 -0
  97. package/src/agent-chat/thread-workspace.tsx +69 -5
  98. package/src/agent-chat/virtualize.ts +132 -0
  99. package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
  100. package/src/data/index.ts +5 -1
  101. package/src/data/virtual-list.tsx +2 -1
  102. package/src/data/virtual.ts +2 -1
  103. package/src/index.ts +5 -0
  104. package/src/surfaces.ts +2 -1
  105. package/src/theme/shadow-surfaces.test.ts +9 -0
@@ -113,6 +113,7 @@ function createAdapterThreadsStore(adapter, opts) {
113
113
  return seq;
114
114
  }
115
115
  const chains = /* @__PURE__ */ new Map();
116
+ const failedCreateIds = /* @__PURE__ */ new Set();
116
117
  function enqueue(threadIds, task) {
117
118
  const ids = [...new Set(threadIds)];
118
119
  const prior = ids.map((id) => chains.get(id)).filter((p) => p !== void 0);
@@ -137,9 +138,16 @@ function createAdapterThreadsStore(adapter, opts) {
137
138
  const tokenAtIssue = errorToken;
138
139
  recompute();
139
140
  enqueue(i.threadIds, async () => {
141
+ const [soleThreadId] = i.threadIds;
142
+ if (!i.isCreate && i.threadIds.length === 1 && soleThreadId !== void 0 && failedCreateIds.has(soleThreadId)) {
143
+ drop(patch);
144
+ recompute();
145
+ return;
146
+ }
140
147
  try {
141
148
  await i.commit();
142
149
  } catch (cause) {
150
+ if (i.isCreate) for (const threadId of i.threadIds) failedCreateIds.add(threadId);
143
151
  drop(patch);
144
152
  error = cause;
145
153
  errorToken += 1;
@@ -201,6 +209,7 @@ function createAdapterThreadsStore(adapter, opts) {
201
209
  mutate({
202
210
  threadIds: [id],
203
211
  apply: (threads) => threads.some((t) => t.id === id) ? [...threads] : [thread, ...threads],
212
+ isCreate: true,
204
213
  commit: () => adapter.createThread({
205
214
  id,
206
215
  ...createOpts?.meta !== void 0 ? { meta: createOpts.meta } : {}
@@ -337,7 +346,7 @@ function threadsStoreAdapterContract(makeAdapter) {
337
346
  }
338
347
  return [
339
348
  define("createThread then listThreads returns the thread with empty defaults", async (a) => {
340
- const id = crypto.randomUUID();
349
+ const id = mintThreadId();
341
350
  await a.createThread({ id });
342
351
  const threads = await a.listThreads();
343
352
  const found = threads.filter((t) => t.id === id);
@@ -352,17 +361,17 @@ function threadsStoreAdapterContract(makeAdapter) {
352
361
  assert(typeof thread.updatedAt === "number", "updatedAt must be a number");
353
362
  }),
354
363
  define("createThread persists meta", async (a) => {
355
- const id = crypto.randomUUID();
364
+ const id = mintThreadId();
356
365
  await a.createThread({ id, meta: { source: "contract" } });
357
366
  const thread = await loadOrThrow(a, id);
358
367
  assert(thread.meta?.["source"] === "contract", "meta did not round-trip");
359
368
  }),
360
369
  define("loadThread returns null for an unknown id", async (a) => {
361
- const thread = await a.loadThread(crypto.randomUUID());
370
+ const thread = await a.loadThread(mintThreadId());
362
371
  assert(thread === null, "loadThread must return null (not throw, not undefined) when absent");
363
372
  }),
364
373
  define("appendMessage is idempotent on message.id", async (a) => {
365
- const id = crypto.randomUUID();
374
+ const id = mintThreadId();
366
375
  await a.createThread({ id });
367
376
  const msg = message("contract-msg-1");
368
377
  await a.appendMessage({ threadId: id, message: msg });
@@ -375,7 +384,7 @@ function threadsStoreAdapterContract(makeAdapter) {
375
384
  assert(thread.messages[0]?.id === "contract-msg-1", "the retained message has the wrong id");
376
385
  }),
377
386
  define("appendMessage preserves order for distinct ids", async (a) => {
378
- const id = crypto.randomUUID();
387
+ const id = mintThreadId();
379
388
  await a.createThread({ id });
380
389
  await a.appendMessage({ threadId: id, message: message("contract-msg-1") });
381
390
  await a.appendMessage({ threadId: id, message: message("contract-msg-2") });
@@ -386,7 +395,7 @@ function threadsStoreAdapterContract(makeAdapter) {
386
395
  );
387
396
  }),
388
397
  define("setStatus round-trips", async (a) => {
389
- const id = crypto.randomUUID();
398
+ const id = mintThreadId();
390
399
  await a.createThread({ id });
391
400
  await a.setStatus({ threadId: id, status: "streaming" });
392
401
  assert((await loadOrThrow(a, id)).status === "streaming", "status did not become 'streaming'");
@@ -394,7 +403,7 @@ function threadsStoreAdapterContract(makeAdapter) {
394
403
  assert((await loadOrThrow(a, id)).status === "error", "status did not become 'error'");
395
404
  }),
396
405
  define("setOutcome round-trips", async (a) => {
397
- const id = crypto.randomUUID();
406
+ const id = mintThreadId();
398
407
  await a.createThread({ id });
399
408
  const outcome = { title: "Title", summary: "Summary", status: "done" };
400
409
  await a.setOutcome({ threadId: id, outcome });
@@ -404,7 +413,7 @@ function threadsStoreAdapterContract(makeAdapter) {
404
413
  assert(thread.outcome?.status === "done", "outcome.status did not round-trip");
405
414
  }),
406
415
  define("setResumeToken round-trips and clears with undefined", async (a) => {
407
- const id = crypto.randomUUID();
416
+ const id = mintThreadId();
408
417
  await a.createThread({ id });
409
418
  await a.setResumeToken({ threadId: id, token: "tok-1" });
410
419
  assert((await loadOrThrow(a, id)).resumeToken === "tok-1", "resumeToken did not round-trip");
@@ -415,14 +424,14 @@ function threadsStoreAdapterContract(makeAdapter) {
415
424
  );
416
425
  }),
417
426
  define("markRead marks the thread read", async (a) => {
418
- const id = crypto.randomUUID();
427
+ const id = mintThreadId();
419
428
  await a.createThread({ id });
420
429
  assert((await loadOrThrow(a, id)).read === false, "a new thread must start unread");
421
430
  await a.markRead(id);
422
431
  assert((await loadOrThrow(a, id)).read === true, "markRead did not set read");
423
432
  }),
424
433
  define("listThreads reflects a write that has already resolved", async (a) => {
425
- const id = crypto.randomUUID();
434
+ const id = mintThreadId();
426
435
  await a.createThread({ id });
427
436
  await a.appendMessage({ threadId: id, message: message("contract-msg-raw") });
428
437
  await a.setStatus({ threadId: id, status: "streaming" });
@@ -440,7 +449,7 @@ function threadsStoreAdapterContract(makeAdapter) {
440
449
  define(
441
450
  "dependent writes issued straight after createThread resolves are honoured",
442
451
  async (a) => {
443
- const id = crypto.randomUUID();
452
+ const id = mintThreadId();
444
453
  await a.createThread({ id });
445
454
  await a.markRead(id);
446
455
  await a.appendMessage({ threadId: id, message: message("contract-msg-dep") });
@@ -455,7 +464,7 @@ function threadsStoreAdapterContract(makeAdapter) {
455
464
  }
456
465
  ),
457
466
  define("removeThread removes the thread", async (a) => {
458
- const id = crypto.randomUUID();
467
+ const id = mintThreadId();
459
468
  await a.createThread({ id });
460
469
  await a.removeThread(id);
461
470
  assert(await a.loadThread(id).then((t) => t === null), "loadThread must be null after remove");
@@ -463,7 +472,7 @@ function threadsStoreAdapterContract(makeAdapter) {
463
472
  assert(!threads.some((t) => t.id === id), "listThreads still contains the removed thread");
464
473
  }),
465
474
  define("removeThread of an unknown id does not throw", async (a) => {
466
- await a.removeThread(crypto.randomUUID());
475
+ await a.removeThread(mintThreadId());
467
476
  })
468
477
  ];
469
478
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-transport.d.ts","sourceRoot":"","sources":["../../src/agent/ai-sdk-transport.ts"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,SAAS,CAAA;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAmB1D,MAAM,MAAM,qBAAqB,GAAG;IAClC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAA;IACnD,oFAAoF;IACpF,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IACzC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CACzC,CAAA;AAID,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG;IACvF;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CAC/E,CAAA;AAwRD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAG,SAAS,EAC9C,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,KAAK,CAAC,CA6CvB"}
1
+ {"version":3,"file":"ai-sdk-transport.d.ts","sourceRoot":"","sources":["../../src/agent/ai-sdk-transport.ts"],"names":[],"mappings":"AAoDA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,SAAS,CAAA;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAmB1D,MAAM,MAAM,qBAAqB,GAAG;IAClC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAA;IACnD,oFAAoF;IACpF,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IACzC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CACzC,CAAA;AAID,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG;IACvF;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CAC/E,CAAA;AAwRD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAG,SAAS,EAC9C,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,KAAK,CAAC,CAkDvB"}
@@ -1,4 +1,5 @@
1
1
  import { assertNever } from "../register.js";
2
+ import { mintMessageId, mintThreadId } from "./id.js";
2
3
  import { TERMINAL_TOOL_STATES } from "./parts.js";
3
4
  function createAiSdkResolver(options) {
4
5
  let resolved;
@@ -161,7 +162,10 @@ function aiSdkTransport(options) {
161
162
  yield { id: `${chatId}#start`, type: "start", runId: chatId, resumeToken: chatId };
162
163
  const { httpTransport, readUIMessageStream: readStream } = await resolveAiSdk();
163
164
  const userMessage = {
164
- id: crypto.randomUUID(),
165
+ // This id is what AI SDK's own backend persistence/dedup keys turns hang off — the same
166
+ // idempotency-key cost as basalt's own ChatMessage.id (see mintMessageId's doc), so
167
+ // mintMessageId, not mintThreadId, is the right helper here.
168
+ id: mintMessageId(),
165
169
  role: "user",
166
170
  parts: [{ type: "text", text: input }]
167
171
  };
@@ -182,7 +186,7 @@ function aiSdkTransport(options) {
182
186
  }
183
187
  };
184
188
  }
185
- const fixedChatId = crypto.randomUUID();
189
+ const fixedChatId = mintThreadId();
186
190
  return {
187
191
  ...makeTransport(fixedChatId),
188
192
  forThread: (chatId) => makeTransport(chatId)
@@ -1,33 +1,81 @@
1
1
  /**
2
- * Id-minting helpers shared by the thread stores: `mintThreadId` (a client-side thread id, used by
3
- * both `./thread`'s `createThreadsStore` and `./adapter`'s `createAdapterThreadsStore`) and
4
- * `withPartIds` (stamps sequence ids onto streamed parts). Related in theme — both mint an id a
5
- * store needs — unrelated in mechanism; grouped here because they're the two id-minting leaves the
6
- * two thread stores both sit above.
2
+ * Id-minting helpers shared across the agent layer: `mintThreadId` (a low-stakes client-side id —
3
+ * threads, run/chat namespaces — used by `./thread`'s `createThreadsStore`, `./adapter`'s
4
+ * `createAdapterThreadsStore`/`threadsStoreAdapterContract`, `./use-agent-stream`'s per-send run
5
+ * id, and `aiSdkTransport`'s fixed chat id), `mintMessageId` (the higher-stakes id `appendMessage`
6
+ * treats as its only idempotency key), and `withPartIds` (stamps sequence ids onto streamed
7
+ * parts). Grouped here because they're the id-minting leaves every store/hook/transport in this
8
+ * layer sits above — not because they share a mechanism (`mintMessageId` deliberately diverges
9
+ * from `mintThreadId` on rung 3; see its own doc).
7
10
  */
8
11
  /**
9
- * Mints a client-side thread id. `crypto.randomUUID()` exists only in SECURE CONTEXTS (HTTPS or
10
- * localhost) — a consumer served over plain HTTP on a LAN hostname (a homelab dashboard on a bare
11
- * IP, a staging box) would otherwise have `create()`, about the most basic user action a thread
12
- * store has, throw a plain TypeError. A store used from a browser must not throw on a user gesture
13
- * — the same principle already applied twice this release (`spliceText` clamps rather than throws,
14
- * `coalesceParts` degrades rather than throws).
12
+ * Mints a client-side id for a LOW-COLLISION-COST use: a thread id, a per-send run-id namespace, a
13
+ * fixed chat id — anything minted client-side that is NOT the idempotency key some write depends
14
+ * on. `crypto.randomUUID()` exists only in SECURE CONTEXTS (HTTPS or localhost) — a consumer served
15
+ * over plain HTTP on a LAN hostname (a homelab dashboard on a bare IP, a staging box) would
16
+ * otherwise have `create()`, about the most basic user action a thread store has, throw a plain
17
+ * TypeError. A store used from a browser must not throw on a user gesture — the same principle
18
+ * already applied twice this release (`spliceText` clamps rather than throws, `coalesceParts`
19
+ * degrades rather than throws).
15
20
  *
16
21
  * Fallback chain, each rung reached only when the one above it is unavailable:
17
22
  * 1. `crypto.randomUUID()` — cryptographically random, RFC 4122 UUID. The normal path.
18
23
  * 2. `crypto.getRandomValues()` — still cryptographically random; hand-assembled into a UUIDv4
19
- * (the version/variant bits are set per RFC 4122, since this rung hands back raw bytes, not a
20
- * formatted UUID).
24
+ * via `assembleUuidV4`.
21
25
  * 3. No usable `crypto` at all (missing entirely, or missing both methods above — an old WebView,
22
26
  * an SSR shim). NOT cryptographically random and NOT collision-resistant in general. This rung
23
- * exists solely so `create()` never throws on such a host. It is acceptable ONLY because this
24
- * id is minted a handful of times per client session and is not the idempotency key any write
25
- * here depends on — `ThreadsStoreAdapter.appendMessage`'s idempotency key is the MESSAGE id,
26
- * not this thread id, so a collision on this rung costs a locally merged/duplicated thread, not
27
- * silent data loss on the append path. Do not reuse this rung for anything with a higher
28
- * collision cost than that.
27
+ * exists solely so a caller never throws on such a host. It is acceptable ONLY because an id
28
+ * minted here is minted a handful of times per client session and is not the idempotency key
29
+ * any write depends on — `ThreadsStoreAdapter.appendMessage`'s idempotency key is the MESSAGE
30
+ * id (see `mintMessageId`), not this one, so a collision on this rung costs a locally
31
+ * merged/duplicated thread (or two runs sharing a part-id namespace), not silent data loss on
32
+ * the append path. Do not reuse this rung for anything with a higher collision cost than that
33
+ * — see `mintMessageId` for the id that needs one.
29
34
  */
30
35
  export declare function mintThreadId(): string;
36
+ /**
37
+ * Mints a message id — the value `ThreadsStoreAdapter.appendMessage` treats as its ONLY
38
+ * idempotency key (see that method's contract doc in `./adapter`). Shares `mintThreadId`'s rungs
39
+ * 1-2 (`crypto.randomUUID()`, then `crypto.getRandomValues()` assembled into a UUIDv4) — both are
40
+ * cryptographically random and collision-resistant, and rung 2 is the fix that actually matters in
41
+ * practice (a plain-HTTP/LAN host still has `getRandomValues`; only `randomUUID` is secure-context-
42
+ * gated).
43
+ *
44
+ * Rung 3 deliberately DIVERGES from `mintThreadId`: it THROWS instead of degrading to a
45
+ * non-random fallback. A thread id's rung-3 collision costs a locally merged/duplicated thread —
46
+ * annoying, recoverable, visible. A message id's collision cost is different IN KIND, not degree:
47
+ * `appendMessage` is idempotent on this id, so two messages that collide are not two rows, they
48
+ * are ONE — the second write silently no-ops and its content is gone, with nothing downstream able
49
+ * to tell. Degrading here would trade a loud, debuggable failure for silent, permanent data loss.
50
+ *
51
+ * This deliberately diverges from this layer's other standing rule — render-path code degrades,
52
+ * never throws (`spliceText` clamps, `coalesceParts` degrades, a consumer fence renderer must not
53
+ * take the message down) — because minting a message id is NOT render-path code. It runs once, in
54
+ * the write path that constructs a message before handing it to a store (`start()`,
55
+ * `consumeAndFinalize`'s finalize step, `finalizeStop`, `aiSdkTransport`'s per-turn user message),
56
+ * never inside the per-chunk accumulation/render loop those functions guard. A host with no usable
57
+ * `crypto` at all (missing both `randomUUID` and `getRandomValues`) is vanishingly rare — no
58
+ * runtime with a DOM/fetch surface worth targeting ships without a Crypto object — and an uncaught
59
+ * throw here is a loud, debuggable failure rather than a message silently vanishing with no trace,
60
+ * but WHERE it surfaces differs by call site (audited, not assumed — this paragraph previously
61
+ * claimed a single shared catch handled all of them, which was false):
62
+ * - `consumeAndFinalize`'s own assistantMessage mint runs inside that function's try/catch, so a
63
+ * throw there is correctly turned into `onFailureStatus` ('error' from `start()`, 'interrupted'
64
+ * from the mount-time resume) — a visibly stuck-then-settled run, as originally claimed.
65
+ * - `useAgentThreadRuns.start()`'s userMessage mint runs BEFORE any state is touched
66
+ * (`appendMessage`/`setStatus`/the controller registration all come after it), so a throw there
67
+ * is a true no-op that propagates synchronously out of `start()` itself — not a stuck run at
68
+ * all, since nothing ever started. See `UseAgentThreadRunsReturn.start`'s `@throws` doc.
69
+ * - `finalizeStop`'s stoppedMessage mint is wrapped in its own try/catch precisely so a throw
70
+ * here cannot wedge the thread at 'streaming' forever (stop() has already torn down this run's
71
+ * bookkeeping by the time finalizeStop runs, so a second stop() is always a no-op) — see
72
+ * `finalizeStop`'s doc for the guarded behavior and the resulting 'error' status.
73
+ * A stuck spinner is debuggable; silently discarded chat history is not — that half of the
74
+ * original claim stands regardless of which of the three paths above catches it.
75
+ *
76
+ * @throws {Error} when neither `crypto.randomUUID` nor `crypto.getRandomValues` is available.
77
+ */
78
+ export declare function mintMessageId(): string;
31
79
  /**
32
80
  * withPartIds — stamps a stable id onto every draft part arriving without one.
33
81
  *
@@ -1 +1 @@
1
- {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/agent/id.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAgBrC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAuB,WAAW,CAAC,KAAK,SAAS;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAC9D,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,GAC5B,cAAc,CAAC,KAAK,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAUxC"}
1
+ {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/agent/id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAmBH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAQrC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAYtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAuB,WAAW,CAAC,KAAK,SAAS;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAC9D,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,GAC5B,cAAc,CAAC,KAAK,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAUxC"}
package/dist/agent/id.js CHANGED
@@ -1,18 +1,31 @@
1
+ function assembleUuidV4(bytes) {
2
+ const hex = Array.from({ length: 16 }, (_, i) => {
3
+ const raw = bytes.at(i) ?? 0;
4
+ const byte = i === 6 ? raw & 15 | 64 : i === 8 ? raw & 63 | 128 : raw;
5
+ return byte.toString(16).padStart(2, "0");
6
+ }).join("");
7
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
8
+ }
1
9
  function mintThreadId() {
2
10
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
3
11
  return crypto.randomUUID();
4
12
  }
5
13
  if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
6
- const bytes = crypto.getRandomValues(new Uint8Array(16));
7
- const hex = Array.from({ length: 16 }, (_, i) => {
8
- const raw = bytes.at(i) ?? 0;
9
- const byte = i === 6 ? raw & 15 | 64 : i === 8 ? raw & 63 | 128 : raw;
10
- return byte.toString(16).padStart(2, "0");
11
- }).join("");
12
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
14
+ return assembleUuidV4(crypto.getRandomValues(new Uint8Array(16)));
13
15
  }
14
16
  return `id-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
15
17
  }
18
+ function mintMessageId() {
19
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
20
+ return crypto.randomUUID();
21
+ }
22
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
23
+ return assembleUuidV4(crypto.getRandomValues(new Uint8Array(16)));
24
+ }
25
+ throw new Error(
26
+ "mintMessageId: no usable crypto (both randomUUID and getRandomValues are unavailable) \u2014 refusing to mint a non-random message id, since ThreadsStoreAdapter.appendMessage treats this id as its only idempotency key and a collision would silently drop a message."
27
+ );
28
+ }
16
29
  async function* withPartIds(runId, source) {
17
30
  let n = 0;
18
31
  for await (const part of source) {
@@ -25,6 +38,7 @@ async function* withPartIds(runId, source) {
25
38
  }
26
39
  }
27
40
  export {
41
+ mintMessageId,
28
42
  mintThreadId,
29
43
  withPartIds
30
44
  };
@@ -109,6 +109,10 @@ export type ThreadsStore<TPart = AgentPart> = {
109
109
  * store built with `createAdapterThreadsStore` (./adapter), where it stays `false` until the
110
110
  * first `listThreads` SUCCEEDS. Pair it with `error` — `!hydrated && error !== undefined` is a
111
111
  * failed load, `!hydrated && error === undefined` is still loading.
112
+ *
113
+ * Gating an empty state on this is the CALLER's job when hand-assembling `ThreadFeed` /
114
+ * `ThreadDetailPanel`; the shipped `ThreadWorkspace` already does it internally, so an
115
+ * async store never flashes "no threads yet" through it before it actually knows.
112
116
  */
113
117
  readonly hydrated: boolean;
114
118
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/agent/thread.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/agent/thread.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-agent-stream.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-stream.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,OAAO,CAAA;AAIlE,MAAM,MAAM,oBAAoB,CAAC,KAAK,EAAE,MAAM,IAAI;IAChD,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;IACvB,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAA;IACzB,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACzC,CAAA;AAID;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,SAAS,QAAQ,GAAG,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,EAClF,SAAS,GACV,EAAE;IACD,SAAS,EAAE,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CACzC,GAAG,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,CA0EtC"}
1
+ {"version":3,"file":"use-agent-stream.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-stream.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,OAAO,CAAA;AAIlE,MAAM,MAAM,oBAAoB,CAAC,KAAK,EAAE,MAAM,IAAI;IAChD,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;IACvB,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAA;IACzB,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACzC,CAAA;AAID;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,SAAS,QAAQ,GAAG,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,EAClF,SAAS,GACV,EAAE;IACD,SAAS,EAAE,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;CACzC,GAAG,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,CA6EtC"}
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { withPartIds } from "./id.js";
2
+ import { mintThreadId, withPartIds } from "./id.js";
3
3
  import { isStartPart } from "./parts.js";
4
4
  import { mergePart } from "./merge.js";
5
5
  function useAgentStream({
@@ -25,7 +25,7 @@ function useAgentStream({
25
25
  setParts([]);
26
26
  setError(void 0);
27
27
  setStatus("streaming");
28
- const runId = crypto.randomUUID();
28
+ const runId = mintThreadId();
29
29
  try {
30
30
  for await (const part of withPartIds(runId, transport.stream(input, controller.signal))) {
31
31
  if (controllerRef.current !== controller) return;
@@ -46,18 +46,39 @@ export type UseAgentThreadRunsArgs<TPart = AgentPart> = {
46
46
  export type UseAgentThreadRunsReturn<TPart = AgentPart> = {
47
47
  /** Live stream state per thread id, for threads with a run in progress or just completed. */
48
48
  readonly runs: ReadonlyMap<string, ThreadRunState<TPart>>;
49
- /** Start a new turn on `threadId`. No-op if that thread already has a stream in flight. */
49
+ /**
50
+ * Start a new turn on `threadId`. No-op if that thread already has a stream in flight.
51
+ *
52
+ * @throws {Error} synchronously, before any state is touched, on a host with no usable
53
+ * `crypto` at all (see `mintMessageId` in `./id.ts` — the id it mints here is the new user
54
+ * ChatMessage's idempotency key, so it degrades to a throw rather than a silent collision).
55
+ * This throw is a true no-op: it happens before `appendMessage`/`setStatus`/the run's
56
+ * controller are registered, so nothing is left half-started — the thread is exactly as it was
57
+ * before the call. A caller invoking `start` from an event handler on a target that must
58
+ * tolerate this (very old WebViews, non-DOM SSR shims) should wrap the call in its own
59
+ * try/catch; this rung is vanishingly rare in practice (see `mintMessageId`'s own doc).
60
+ */
50
61
  readonly start: (threadId: string, input: string) => void;
51
62
  /**
52
63
  * Replay the last user input sent on `threadId` (same code path as `start`). No-op if
53
64
  * `threadId` has never had a turn started, or already has one in flight.
65
+ *
66
+ * @throws {Error} same condition and same true-no-op guarantee as `start` — see its doc.
54
67
  */
55
68
  readonly retry: (threadId: string) => void;
56
69
  /**
57
- * Abort the in-flight stream for `threadId` (no-op if idle). Preserves whatever content had
58
- * already arrived: if the run had accumulated any parts, they're persisted as an assistant
70
+ * Abort the in-flight stream for `threadId` (true no-op only if BOTH `controllersRef` and
71
+ * `runs` have nothing for it — i.e. the thread genuinely isn't live). Preserves whatever content
72
+ * had already arrived: if the run had accumulated any parts, they're persisted as an assistant
59
73
  * ChatMessage with `finish: 'stopped'` (unless that message already landed — see finalizeStop),
60
74
  * then distilled into an outcome and the thread is settled to 'done'.
75
+ *
76
+ * Defense-in-depth: if `runs` reports `threadId` as `'streaming'` but no controller is
77
+ * registered for it (a phantom entry — not reachable via any path in this hook today, since the
78
+ * unmount-cleanup effect and `finalizeStop` both tear down `controllersRef` and `runs` together,
79
+ * but guarded against here so nothing in this file can EVER wedge a thread the UI shows as live
80
+ * with no way to reach a terminal state), `stop()` still settles it instead of silently
81
+ * no-opping forever.
61
82
  */
62
83
  readonly stop: (threadId: string) => void;
63
84
  /** Abort every in-flight stream across all threads, settling each the same way stop() would. */
@@ -1 +1 @@
1
- {"version":3,"file":"use-agent-thread-runs.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-thread-runs.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,WAAW,CAAA;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,UAAU,CAAA;AAEvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI;IAC9C,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;CACxB,CAAA;AAID,MAAM,MAAM,sBAAsB,CAAC,KAAK,GAAG,SAAS,IAAI;IACtD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EACd,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAC7B,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IACzD,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC,KAAK,CAAC,CAAA;IAC/C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,EAAE,CAAA;CAClD,CAAA;AAID,MAAM,MAAM,wBAAwB,CAAC,KAAK,GAAG,SAAS,IAAI;IACxD,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IACzD,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,gGAAgG;IAChG,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,CAAA;AAkND;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,SAAS,QAAQ,GAAG,SAAS,EAAE,EACrE,SAAS,EACT,KAAK,EACL,cAAc,EACd,WAAW,GACZ,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAkPjE"}
1
+ {"version":3,"file":"use-agent-thread-runs.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-thread-runs.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,WAAW,CAAA;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,UAAU,CAAA;AAEvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI;IAC9C,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;CACxB,CAAA;AAID,MAAM,MAAM,sBAAsB,CAAC,KAAK,GAAG,SAAS,IAAI;IACtD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EACd,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAC7B,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IACzD,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC,KAAK,CAAC,CAAA;IAC/C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,EAAE,CAAA;CAClD,CAAA;AAID,MAAM,MAAM,wBAAwB,CAAC,KAAK,GAAG,SAAS,IAAI;IACxD,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IACzD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzD;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,gGAAgG;IAChG,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,CAAA;AAuRD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,SAAS,QAAQ,GAAG,SAAS,EAAE,EACrE,SAAS,EACT,KAAK,EACL,cAAc,EACd,WAAW,GACZ,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAgSjE"}
@@ -1,10 +1,10 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { withPartIds } from "./id.js";
2
+ import { mintMessageId, mintThreadId, withPartIds } from "./id.js";
3
3
  import { mergePart } from "./merge.js";
4
4
  import { isStartPart } from "./parts.js";
5
5
  import { isResumable } from "./transport.js";
6
6
  function defaultToUserParts(input) {
7
- return [{ id: crypto.randomUUID(), type: "text", text: input }];
7
+ return [{ id: mintThreadId(), type: "text", text: input }];
8
8
  }
9
9
  function buildDoneSnapshot(priorThread, threadId, userMessage, assistantMessage) {
10
10
  const updatedAt = Date.now();
@@ -59,7 +59,9 @@ async function consumeAndFinalize(args) {
59
59
  return;
60
60
  }
61
61
  const assistantMessage = {
62
- id: crypto.randomUUID(),
62
+ // appendMessage's idempotency key — mintMessageId, never mintThreadId (see that helper's
63
+ // doc): a collided id here would silently drop this message, not merge a duplicate thread.
64
+ id: mintMessageId(),
63
65
  role: "assistant",
64
66
  parts,
65
67
  createdAt: Date.now(),
@@ -101,24 +103,43 @@ async function consumeAndFinalize(args) {
101
103
  }
102
104
  async function finalizeStop(args) {
103
105
  const { threadId, parts, alreadyAppended, storeRef, resolveOutcome, setRuns } = args;
106
+ let appendFailed = false;
104
107
  if (!alreadyAppended && parts.length > 0) {
105
- const stoppedMessage = {
106
- id: crypto.randomUUID(),
107
- role: "assistant",
108
- parts,
109
- createdAt: Date.now(),
110
- finish: "stopped"
111
- };
112
- storeRef.current.appendMessage(threadId, stoppedMessage);
108
+ try {
109
+ const stoppedMessage = {
110
+ // Same idempotency-key reasoning as consumeAndFinalize's assistantMessage — mintMessageId.
111
+ // This can throw on rung 3 (no usable crypto — see ./id.ts) just like appendMessage
112
+ // (consumer code) can throw for its own reasons; both are caught below so neither can
113
+ // wedge the thread — see this function's doc.
114
+ id: mintMessageId(),
115
+ role: "assistant",
116
+ parts,
117
+ createdAt: Date.now(),
118
+ finish: "stopped"
119
+ };
120
+ storeRef.current.appendMessage(threadId, stoppedMessage);
121
+ } catch {
122
+ appendFailed = true;
123
+ }
124
+ }
125
+ let settleFailed = appendFailed;
126
+ try {
127
+ storeRef.current.setStatus(threadId, appendFailed ? "error" : "done");
128
+ } catch {
129
+ settleFailed = true;
130
+ }
131
+ try {
132
+ storeRef.current.setResumeToken(threadId, void 0);
133
+ } catch {
134
+ settleFailed = true;
113
135
  }
114
- storeRef.current.setStatus(threadId, "done");
115
- storeRef.current.setResumeToken(threadId, void 0);
116
136
  setRuns((prev) => {
117
137
  if (!prev.has(threadId)) return prev;
118
138
  const next = new Map(prev);
119
139
  next.delete(threadId);
120
140
  return next;
121
141
  });
142
+ if (settleFailed) return;
122
143
  const snapshot = storeRef.current.threads.find((thread) => thread.id === threadId);
123
144
  if (snapshot === void 0) return;
124
145
  const outcome = await resolveOutcome(snapshot);
@@ -155,9 +176,20 @@ function useAgentThreadRuns({
155
176
  };
156
177
  useEffect(
157
178
  () => () => {
179
+ const abortedThreadIds = Array.from(controllersRef.current.keys());
158
180
  controllersRef.current.forEach((controller) => controller.abort());
159
181
  controllersRef.current.clear();
160
182
  appendedRef.current.clear();
183
+ if (abortedThreadIds.length === 0) return;
184
+ setRuns((prev) => {
185
+ let next;
186
+ for (const threadId of abortedThreadIds) {
187
+ if (!prev.has(threadId)) continue;
188
+ next ??= new Map(prev);
189
+ next.delete(threadId);
190
+ }
191
+ return next ?? prev;
192
+ });
161
193
  },
162
194
  []
163
195
  );
@@ -206,7 +238,8 @@ function useAgentThreadRuns({
206
238
  if (controllersRef.current.has(threadId)) return;
207
239
  lastInputRef.current.set(threadId, input);
208
240
  const userMessage = {
209
- id: crypto.randomUUID(),
241
+ // Same idempotency-key reasoning as consumeAndFinalize's assistantMessage — mintMessageId.
242
+ id: mintMessageId(),
210
243
  role: "user",
211
244
  parts: toUserPartsRef.current(input),
212
245
  createdAt: Date.now()
@@ -248,7 +281,14 @@ function useAgentThreadRuns({
248
281
  const stop = useCallback(
249
282
  (threadId) => {
250
283
  const controller = controllersRef.current.get(threadId);
251
- if (controller === void 0) return;
284
+ if (controller === void 0) {
285
+ if (!runsRef.current.has(threadId)) return;
286
+ const parts2 = runsRef.current.get(threadId)?.parts ?? [];
287
+ const alreadyAppended2 = appendedRef.current.has(threadId);
288
+ appendedRef.current.delete(threadId);
289
+ void finalizeStop({ threadId, parts: parts2, alreadyAppended: alreadyAppended2, storeRef, resolveOutcome, setRuns });
290
+ return;
291
+ }
252
292
  controllersRef.current.delete(threadId);
253
293
  const parts = runsRef.current.get(threadId)?.parts ?? [];
254
294
  controller.abort();
@@ -14,6 +14,10 @@ export { ThreadWorkspace } from './thread-workspace.js';
14
14
  export type { ThreadWorkspaceProps } from './thread-workspace.js';
15
15
  export { ThreadFeed } from './thread-feed.js';
16
16
  export type { ThreadFeedProps } from './thread-feed.js';
17
+ export { ThreadFeedRow } from './thread-feed-row.js';
18
+ export type { ThreadFeedRowProps } from './thread-feed-row.js';
19
+ export type { MessageAffordances } from './message-affordances.js';
20
+ export type { VirtualizeOptions, VirtualizeProps } from './virtualize.js';
17
21
  export { ThreadOutcomeCard } from './thread-outcome-card.js';
18
22
  export type { ThreadOutcomeCardProps } from './thread-outcome-card.js';
19
23
  export { ThreadDetailPanel } from './thread-detail-panel.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG9D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAGpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGnG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxE,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAG7D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG9D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAGpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAO3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAGtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGnG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxE,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAG7D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA"}
@@ -1,5 +1,6 @@
1
1
  import { ThreadWorkspace } from "./thread-workspace.js";
2
2
  import { ThreadFeed } from "./thread-feed.js";
3
+ import { ThreadFeedRow } from "./thread-feed-row.js";
3
4
  import { ThreadOutcomeCard } from "./thread-outcome-card.js";
4
5
  import { ThreadDetailPanel } from "./thread-detail-panel.js";
5
6
  import { Composer } from "./composer.js";
@@ -9,6 +10,7 @@ export {
9
10
  Composer,
10
11
  ThreadDetailPanel,
11
12
  ThreadFeed,
13
+ ThreadFeedRow,
12
14
  ThreadOutcomeCard,
13
15
  ThreadTranscript,
14
16
  ThreadWorkspace,