@assistant-ui/core 0.3.1 → 0.3.3

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 (183) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.js.map +1 -1
  3. package/dist/internal.d.ts +6 -7
  4. package/dist/internal.js +5 -6
  5. package/dist/react/AssistantProvider.d.ts +1 -1
  6. package/dist/react/AssistantProvider.d.ts.map +1 -1
  7. package/dist/react/AssistantProvider.js +35 -9
  8. package/dist/react/AssistantProvider.js.map +1 -1
  9. package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
  10. package/dist/react/AssistantRuntimeProvider.js +1 -1
  11. package/dist/react/RuntimeAdapter.js +2 -2
  12. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  13. package/dist/react/adapters/LocalStorageThreadListAdapter.js +17 -33
  14. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  15. package/dist/react/client/DataRenderers.js +1 -1
  16. package/dist/react/client/Interactables.js +1 -1
  17. package/dist/react/client/Tools.js +2 -2
  18. package/dist/react/index.d.ts +2 -1
  19. package/dist/react/index.js +2 -1
  20. package/dist/react/interactables-legacy/Interactables.js +1 -1
  21. package/dist/react/interactables-legacy/useAssistantInteractable.js +1 -1
  22. package/dist/react/interactables-legacy/useInteractableState.js +2 -2
  23. package/dist/react/model-context/useAssistantContext.js +1 -1
  24. package/dist/react/model-context/useAssistantDataUI.js +2 -2
  25. package/dist/react/model-context/useAssistantInstructions.js +2 -2
  26. package/dist/react/model-context/useAssistantTool.js +2 -2
  27. package/dist/react/model-context/useAssistantToolUI.js +2 -2
  28. package/dist/react/model-context/useAuiToolOverrides.js +1 -1
  29. package/dist/react/model-context/useInlineRender.js +1 -1
  30. package/dist/react/model-context/useInteractable.js +1 -1
  31. package/dist/react/model-context/useInteractableState.js +2 -2
  32. package/dist/react/model-context/useInteractableVersions.js +2 -2
  33. package/dist/react/model-context/useToolArgsStatus.js +2 -2
  34. package/dist/react/primitive-hooks/useActionBarCopy.js +52 -25
  35. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  36. package/dist/react/primitive-hooks/useActionBarEdit.js +2 -2
  37. package/dist/react/primitive-hooks/useActionBarFeedback.js +2 -2
  38. package/dist/react/primitive-hooks/useActionBarReload.js +2 -2
  39. package/dist/react/primitive-hooks/useActionBarSpeak.js +2 -2
  40. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +2 -2
  41. package/dist/react/primitive-hooks/useBranchPickerNext.js +2 -2
  42. package/dist/react/primitive-hooks/useBranchPickerPrevious.js +2 -2
  43. package/dist/react/primitive-hooks/useComposerAddAttachment.js +2 -2
  44. package/dist/react/primitive-hooks/useComposerCancel.js +2 -2
  45. package/dist/react/primitive-hooks/useComposerDictate.js +2 -2
  46. package/dist/react/primitive-hooks/useComposerSend.js +2 -2
  47. package/dist/react/primitive-hooks/useEditComposerCancel.js +2 -2
  48. package/dist/react/primitive-hooks/useEditComposerSend.js +2 -2
  49. package/dist/react/primitive-hooks/useMessageBranching.js +2 -2
  50. package/dist/react/primitive-hooks/useMessageReload.js +2 -2
  51. package/dist/react/primitive-hooks/useSuggestionTrigger.js +2 -2
  52. package/dist/react/primitive-hooks/useThreadListItemArchive.js +2 -2
  53. package/dist/react/primitive-hooks/useThreadListItemDelete.js +2 -2
  54. package/dist/react/primitive-hooks/useThreadListItemTrigger.js +2 -2
  55. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +2 -2
  56. package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -2
  57. package/dist/react/primitive-hooks/useThreadListNew.js +2 -2
  58. package/dist/react/primitive-hooks/useThreadMessageIds.js +1 -1
  59. package/dist/react/primitive-hooks/useVoice.js +2 -2
  60. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +2 -2
  61. package/dist/react/primitives/composer/ComposerAttachments.js +2 -2
  62. package/dist/react/primitives/composer/ComposerIf.js +1 -1
  63. package/dist/react/primitives/composer/ComposerQueue.js +1 -1
  64. package/dist/react/primitives/generativeUI/GenerativeUI.js +2 -2
  65. package/dist/react/primitives/message/MessageAttachments.js +2 -2
  66. package/dist/react/primitives/message/MessageGroupedParts.d.ts +2 -1
  67. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  68. package/dist/react/primitives/message/MessageGroupedParts.js +3 -3
  69. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  70. package/dist/react/primitives/message/MessageParts.js +2 -2
  71. package/dist/react/primitives/message/MessageQuote.js +2 -2
  72. package/dist/react/primitives/part/PartMessages.js +2 -2
  73. package/dist/react/primitives/thread/ThreadMessages.js +2 -2
  74. package/dist/react/primitives/thread/ThreadSuggestions.js +2 -2
  75. package/dist/react/primitives/threadList/ThreadListItems.js +2 -2
  76. package/dist/react/primitives/threadListItem/ThreadListItemTitle.js +1 -1
  77. package/dist/react/providers/AttachmentByIndexProvider.js +1 -1
  78. package/dist/react/providers/ChainOfThoughtByIndicesProvider.js +1 -1
  79. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +1 -1
  80. package/dist/react/providers/MessageByIdProvider.js +1 -1
  81. package/dist/react/providers/MessageByIndexProvider.js +1 -1
  82. package/dist/react/providers/PartByIndexProvider.js +1 -1
  83. package/dist/react/providers/QueueItemByIndexProvider.js +1 -1
  84. package/dist/react/providers/ReadonlyThreadProvider.js +2 -2
  85. package/dist/react/providers/SuggestionByIndexProvider.js +1 -1
  86. package/dist/react/providers/TextMessagePartProvider.js +2 -2
  87. package/dist/react/providers/ThreadListItemByIndexProvider.js +1 -1
  88. package/dist/react/providers/ThreadListItemRuntimeProvider.js +1 -1
  89. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  90. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +14 -13
  91. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  92. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -0
  93. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  94. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +22 -12
  95. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  96. package/dist/react/runtimes/RuntimeAdapterProvider.js +1 -1
  97. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +9 -23
  98. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  99. package/dist/react/runtimes/cloud/auiV0.d.ts +24 -2
  100. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  101. package/dist/react/runtimes/cloud/auiV0.js +24 -5
  102. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  103. package/dist/react/runtimes/external-message-converter.js +1 -1
  104. package/dist/react/runtimes/useExternalStoreRuntime.js +1 -1
  105. package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -1
  106. package/dist/react/runtimes/useLocalRuntime.js +1 -1
  107. package/dist/react/runtimes/useRemoteThreadListRuntime.js +7 -13
  108. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  109. package/dist/react/utils/useJSONEqual.js +1 -1
  110. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  111. package/dist/runtime/api/message-runtime.js +5 -1
  112. package/dist/runtime/api/message-runtime.js.map +1 -1
  113. package/dist/runtime/internal.d.ts +4 -4
  114. package/dist/runtime/internal.js +4 -4
  115. package/dist/runtime/utils/thread-message-like.js +2 -2
  116. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  117. package/dist/runtimes/remote-thread-list/types.d.ts +5 -4
  118. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  119. package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
  120. package/dist/store/clients/chain-of-thought-client.js +30 -24
  121. package/dist/store/clients/chain-of-thought-client.js.map +1 -1
  122. package/dist/store/clients/model-context-client.js +1 -1
  123. package/dist/store/clients/no-op-composer-client.js +1 -1
  124. package/dist/store/clients/suggestions.js +2 -2
  125. package/dist/store/clients/thread-message-client.d.ts.map +1 -1
  126. package/dist/store/clients/thread-message-client.js +103 -91
  127. package/dist/store/clients/thread-message-client.js.map +1 -1
  128. package/dist/store/runtime-clients/composer-runtime-client.js +2 -2
  129. package/dist/store/runtime-clients/message-runtime-client.js +2 -2
  130. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +2 -2
  131. package/dist/store/runtime-clients/thread-list-runtime-client.js +2 -2
  132. package/dist/store/runtime-clients/thread-runtime-client.js +2 -2
  133. package/dist/store/scopes/thread.d.ts +1 -1
  134. package/dist/store/scopes/thread.d.ts.map +1 -1
  135. package/dist/types/index.d.ts +2 -2
  136. package/dist/types/message.d.ts +11 -1
  137. package/dist/types/message.d.ts.map +1 -1
  138. package/dist/types/message.js.map +1 -1
  139. package/dist/utils/data-url.js +2 -2
  140. package/dist/utils/data-url.js.map +1 -1
  141. package/dist/utils/getGroupStatus.d.ts +9 -0
  142. package/dist/utils/getGroupStatus.d.ts.map +1 -0
  143. package/dist/utils/getGroupStatus.js +15 -0
  144. package/dist/utils/getGroupStatus.js.map +1 -0
  145. package/dist/utils/normalizePartStatus.d.ts +8 -0
  146. package/dist/utils/normalizePartStatus.d.ts.map +1 -0
  147. package/dist/utils/normalizePartStatus.js +39 -0
  148. package/dist/utils/normalizePartStatus.js.map +1 -0
  149. package/package.json +4 -4
  150. package/src/index.ts +1 -0
  151. package/src/internal.ts +1 -7
  152. package/src/react/AssistantProvider.tsx +28 -15
  153. package/src/react/AssistantRuntimeProvider.test.tsx +101 -0
  154. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +28 -7
  155. package/src/react/index.ts +4 -0
  156. package/src/react/primitive-hooks/useActionBarCopy.test.ts +109 -14
  157. package/src/react/primitive-hooks/useActionBarCopy.ts +27 -2
  158. package/src/react/primitives/message/MessageGroupedParts.tsx +4 -4
  159. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +16 -13
  160. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +53 -10
  161. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +7 -5
  162. package/src/react/runtimes/cloud/auiV0.ts +51 -12
  163. package/src/react/runtimes/useRemoteThreadListRuntime.ts +9 -15
  164. package/src/runtime/api/message-runtime.test.ts +140 -0
  165. package/src/runtime/api/message-runtime.ts +10 -5
  166. package/src/runtime/utils/thread-message-like.ts +2 -2
  167. package/src/runtimes/remote-thread-list/types.ts +5 -4
  168. package/src/store/clients/chain-of-thought-client.ts +2 -7
  169. package/src/store/clients/thread-message-client.test.ts +84 -0
  170. package/src/store/clients/thread-message-client.ts +14 -4
  171. package/src/store/scopes/thread.ts +1 -1
  172. package/src/tests/auiV0Encode.test.ts +93 -0
  173. package/src/tests/remote-thread-list-reactive-threadId.test.ts +0 -92
  174. package/src/tests/thread-message-like.test.ts +15 -0
  175. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +293 -0
  176. package/src/types/index.ts +1 -0
  177. package/src/types/message.ts +19 -0
  178. package/src/utils/data-url.test.ts +26 -2
  179. package/src/utils/data-url.ts +2 -2
  180. package/src/utils/getGroupStatus.test.ts +36 -0
  181. package/src/utils/getGroupStatus.ts +31 -0
  182. package/src/utils/normalizePartStatus.test.ts +59 -0
  183. package/src/utils/normalizePartStatus.ts +55 -0
@@ -4,6 +4,7 @@ import {
4
4
  useCallback,
5
5
  useRef,
6
6
  useEffect,
7
+ useEffectEvent,
7
8
  useLayoutEffect,
8
9
  memo,
9
10
  type PropsWithChildren,
@@ -142,23 +143,25 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
142
143
  }
143
144
  }, [threadBinding]);
144
145
 
145
- useEffect(() => {
146
- hasInitializedRef.current = false;
147
- return runtime.threads.main.unstable_on("initialize", () => {
148
- if (hasInitializedRef.current) return;
146
+ const handleInitialize = useEffectEvent(() => {
147
+ if (hasInitializedRef.current) return;
149
148
 
150
- const state = aui.threadListItem.getState();
151
- if (state.status !== "new") return;
152
- hasInitializedRef.current = true;
149
+ const state = aui.threadListItem.getState();
150
+ if (state.status !== "new") return;
151
+ hasInitializedRef.current = true;
153
152
 
154
- initPromiseRef.current = aui.threadListItem.initialize();
153
+ initPromiseRef.current = aui.threadListItem.initialize();
155
154
 
156
- const dispose = runtime.thread.unstable_on("runEnd", () => {
157
- dispose();
158
- aui.threadListItem.generateTitle();
159
- });
155
+ const dispose = runtime.thread.unstable_on("runEnd", () => {
156
+ dispose();
157
+ aui.threadListItem.generateTitle();
160
158
  });
161
- }, [runtime, aui]);
159
+ });
160
+
161
+ useEffect(() => {
162
+ hasInitializedRef.current = false;
163
+ return runtime.threads.main.unstable_on("initialize", handleInitialize);
164
+ }, [runtime]);
162
165
 
163
166
  return <>{children}</>;
164
167
  };
@@ -200,6 +200,10 @@ export class RemoteThreadListThreadListRuntimeCore
200
200
 
201
201
  const adapterChanged =
202
202
  this._options !== undefined && this._options.adapter !== options.adapter;
203
+ const controlledThreadIdChanged =
204
+ this._initialThreadLoaded &&
205
+ this._options !== undefined &&
206
+ this._options.threadId !== options.threadId;
203
207
 
204
208
  this._options = options;
205
209
 
@@ -219,15 +223,25 @@ export class RemoteThreadListThreadListRuntimeCore
219
223
  cursor: undefined,
220
224
  });
221
225
  }
226
+
227
+ if (controlledThreadIdChanged) {
228
+ this._switchToThreadFromProp(options.threadId).catch(() => {});
229
+ }
222
230
  }
223
231
 
224
232
  public __internal_load() {
225
233
  this.getLoadThreadsPromise(); // begin loading on initial bind
234
+ if (this._initialThreadLoaded) return;
235
+ this._initialThreadLoaded = true;
236
+
226
237
  const startThreadId =
227
238
  this._options.threadId ?? this._options.initialThreadId;
228
- if (!this._initialThreadLoaded && startThreadId) {
229
- this._initialThreadLoaded = true;
230
- this.switchToThread(startThreadId).catch(() => {});
239
+ if (startThreadId !== undefined) {
240
+ const switchTask =
241
+ this._options.threadId !== undefined
242
+ ? this._switchToThreadFromProp(startThreadId)
243
+ : this.switchToThread(startThreadId);
244
+ switchTask.catch(() => {});
231
245
  }
232
246
  }
233
247
 
@@ -279,11 +293,13 @@ export class RemoteThreadListThreadListRuntimeCore
279
293
 
280
294
  private _lastNotifiedThreadId: string | undefined = undefined;
281
295
 
282
- private _notifyThreadIdChange() {
296
+ private _notifyThreadIdChange(emit = true) {
283
297
  const threadId = this._mainThreadRemoteId;
284
298
  if (this._lastNotifiedThreadId === threadId) return;
285
299
  this._lastNotifiedThreadId = threadId;
286
- this._options.onThreadIdChange?.(threadId);
300
+ if (emit) {
301
+ this._options.onThreadIdChange?.(threadId);
302
+ }
287
303
  }
288
304
 
289
305
  public getMainThreadRuntimeCore() {
@@ -312,9 +328,22 @@ export class RemoteThreadListThreadListRuntimeCore
312
328
  public switchToThread(
313
329
  threadIdOrRemoteId: string,
314
330
  options?: { unarchive?: boolean },
331
+ ): Promise<void> {
332
+ return this._startSwitchToThread(threadIdOrRemoteId, options, true);
333
+ }
334
+
335
+ private _startSwitchToThread(
336
+ threadIdOrRemoteId: string,
337
+ options: { unarchive?: boolean } | undefined,
338
+ emitThreadIdChange: boolean,
315
339
  ): Promise<void> {
316
340
  const generation = ++this._switchGeneration;
317
- const task = this._switchToThread(threadIdOrRemoteId, options, generation);
341
+ const task = this._switchToThread(
342
+ threadIdOrRemoteId,
343
+ options,
344
+ generation,
345
+ emitThreadIdChange,
346
+ );
318
347
  this._switchTask = task;
319
348
  return task;
320
349
  }
@@ -323,6 +352,7 @@ export class RemoteThreadListThreadListRuntimeCore
323
352
  threadIdOrRemoteId: string,
324
353
  options: { unarchive?: boolean } | undefined,
325
354
  generation: number,
355
+ emitThreadIdChange: boolean,
326
356
  ): Promise<void> {
327
357
  let data = this.getItemById(threadIdOrRemoteId);
328
358
 
@@ -418,17 +448,30 @@ export class RemoteThreadListThreadListRuntimeCore
418
448
  this._mainThreadId = data.id;
419
449
 
420
450
  this._notifySubscribers();
421
- this._notifyThreadIdChange();
451
+ this._notifyThreadIdChange(emitThreadIdChange);
422
452
  }
423
453
 
424
454
  public switchToNewThread(): Promise<void> {
455
+ return this._startSwitchToNewThread(true);
456
+ }
457
+
458
+ private _switchToThreadFromProp(threadId: string | undefined): Promise<void> {
459
+ return threadId !== undefined
460
+ ? this._startSwitchToThread(threadId, undefined, false)
461
+ : this._startSwitchToNewThread(false);
462
+ }
463
+
464
+ private _startSwitchToNewThread(emitThreadIdChange: boolean): Promise<void> {
425
465
  const generation = ++this._switchGeneration;
426
- const task = this._switchToNewThread(generation);
466
+ const task = this._switchToNewThread(generation, emitThreadIdChange);
427
467
  this._switchTask = task;
428
468
  return task;
429
469
  }
430
470
 
431
- private async _switchToNewThread(generation: number): Promise<void> {
471
+ private async _switchToNewThread(
472
+ generation: number,
473
+ emitThreadIdChange: boolean,
474
+ ): Promise<void> {
432
475
  // an initialization transaction is in progress, wait for it to settle
433
476
  while (
434
477
  this._state.baseValue.newThreadId !== undefined &&
@@ -467,7 +510,7 @@ export class RemoteThreadListThreadListRuntimeCore
467
510
  });
468
511
  }
469
512
 
470
- return this._switchToThread(id, undefined, generation);
513
+ return this._switchToThread(id, undefined, generation, emitThreadIdChange);
471
514
  }
472
515
 
473
516
  public initialize = async (threadId: string) => {
@@ -22,18 +22,18 @@ const globalPersistence = new WeakMap<
22
22
 
23
23
  class AssistantCloudThreadHistoryAdapter implements ThreadHistoryAdapter {
24
24
  private cloudRef: RefObject<AssistantCloud>;
25
- private auiRef: RefObject<AssistantClient>;
25
+ private getAui: () => AssistantClient;
26
26
 
27
27
  constructor(
28
28
  cloudRef: RefObject<AssistantCloud>,
29
- auiRef: RefObject<AssistantClient>,
29
+ getAui: () => AssistantClient,
30
30
  ) {
31
31
  this.cloudRef = cloudRef;
32
- this.auiRef = auiRef;
32
+ this.getAui = getAui;
33
33
  }
34
34
 
35
35
  private get aui(): AssistantClient {
36
- return this.auiRef.current;
36
+ return this.getAui();
37
37
  }
38
38
 
39
39
  private get _persistence(): CloudMessagePersistence {
@@ -816,12 +816,14 @@ export function useAssistantCloudThreadHistoryAdapter(
816
816
  cloudRef: RefObject<AssistantCloud>,
817
817
  ): ThreadHistoryAdapter {
818
818
  const aui = useAui();
819
+ // Not useEffectEvent: history adapter methods run during render (SSR load).
819
820
  const auiRef = useRef(aui);
820
821
  useEffect(() => {
821
822
  auiRef.current = aui;
822
823
  });
823
824
  const [adapter] = useState(
824
- () => new AssistantCloudThreadHistoryAdapter(cloudRef, auiRef),
825
+ () =>
826
+ new AssistantCloudThreadHistoryAdapter(cloudRef, () => auiRef.current),
825
827
  );
826
828
  return adapter;
827
829
  }
@@ -1,13 +1,8 @@
1
1
  import type {
2
- DataMessagePart,
3
- FileMessagePart,
4
- ImageMessagePart,
5
2
  MessageStatus,
6
3
  SourceProviderMetadata,
7
4
  ThreadMessage,
8
- TextMessagePart,
9
5
  ToolApprovalOption,
10
- Unstable_AudioMessagePart,
11
6
  } from "../../../types/message";
12
7
  import type { CompleteAttachment } from "../../../types/attachment";
13
8
  import { fromThreadMessageLike } from "../../../runtime/utils/thread-message-like";
@@ -85,11 +80,33 @@ type AuiV0MessagePart =
85
80
  };
86
81
 
87
82
  type AuiV0AttachmentPart =
88
- | TextMessagePart
89
- | ImageMessagePart
90
- | FileMessagePart
91
- | Unstable_AudioMessagePart
92
- | DataMessagePart<ReadonlyJSONValue>;
83
+ | {
84
+ readonly type: "text";
85
+ readonly text: string;
86
+ }
87
+ | {
88
+ readonly type: "image";
89
+ readonly image: string;
90
+ readonly filename?: string;
91
+ }
92
+ | {
93
+ readonly type: "file";
94
+ readonly data: string;
95
+ readonly mimeType: string;
96
+ readonly filename?: string;
97
+ }
98
+ | {
99
+ readonly type: "audio";
100
+ readonly audio: {
101
+ readonly data: string;
102
+ readonly format: "mp3" | "wav";
103
+ };
104
+ }
105
+ | {
106
+ readonly type: "data";
107
+ readonly name: string;
108
+ readonly data: ReadonlyJSONValue;
109
+ };
93
110
 
94
111
  type AuiV0Attachment = {
95
112
  readonly id: string;
@@ -125,16 +142,38 @@ const encodeAttachmentPart = (
125
142
  const type = part.type;
126
143
  switch (type) {
127
144
  case "text":
145
+ return { type: "text", text: part.text };
146
+
128
147
  case "image":
148
+ return {
149
+ type: "image",
150
+ image: part.image,
151
+ ...(part.filename != null ? { filename: part.filename } : undefined),
152
+ };
153
+
129
154
  case "file":
155
+ return {
156
+ type: "file",
157
+ data: part.data,
158
+ mimeType: part.mimeType,
159
+ ...(part.filename != null ? { filename: part.filename } : undefined),
160
+ };
161
+
130
162
  case "audio":
131
- return part;
163
+ return {
164
+ type: "audio",
165
+ audio: { data: part.audio.data, format: part.audio.format },
166
+ };
132
167
 
133
168
  case "data": {
134
169
  if (!isJSONValue(part.data)) {
135
170
  console.warn(`attachment data is not JSON! ${JSON.stringify(part)}`);
136
171
  }
137
- return { ...part, data: part.data as ReadonlyJSONValue };
172
+ return {
173
+ type: "data",
174
+ name: part.name,
175
+ data: part.data as ReadonlyJSONValue,
176
+ };
138
177
  }
139
178
 
140
179
  default: {
@@ -51,8 +51,7 @@ export const useRemoteThreadListRuntime = (
51
51
  const runtimeHookRef = useRef(options.runtimeHook);
52
52
  runtimeHookRef.current = options.runtimeHook;
53
53
 
54
- // threadId/initialThreadId only affect the constructor; capture once via ref
55
- const startThreadIdRef = useRef(options.threadId ?? options.initialThreadId);
54
+ const initialThreadIdRef = useRef(options.initialThreadId);
56
55
 
57
56
  const stableRuntimeHook = useCallback(() => {
58
57
  return runtimeHookRef.current();
@@ -66,11 +65,17 @@ export const useRemoteThreadListRuntime = (
66
65
  () => ({
67
66
  adapter: options.adapter,
68
67
  allowNesting: options.allowNesting,
69
- initialThreadId: startThreadIdRef.current,
68
+ threadId: options.threadId,
69
+ initialThreadId: initialThreadIdRef.current,
70
70
  runtimeHook: stableRuntimeHook,
71
71
  onThreadIdChange,
72
72
  }),
73
- [options.adapter, options.allowNesting, stableRuntimeHook],
73
+ [
74
+ options.adapter,
75
+ options.allowNesting,
76
+ options.threadId,
77
+ stableRuntimeHook,
78
+ ],
74
79
  );
75
80
 
76
81
  const aui = useAui();
@@ -91,16 +96,5 @@ export const useRemoteThreadListRuntime = (
91
96
 
92
97
  const runtime = useRemoteThreadListRuntimeImpl(stableOptions);
93
98
 
94
- const prevThreadIdRef = useRef(options.threadId);
95
- useEffect(() => {
96
- if (options.threadId === prevThreadIdRef.current) return;
97
- prevThreadIdRef.current = options.threadId;
98
- if (options.threadId) {
99
- runtime.threads.switchToThread(options.threadId).catch(() => {});
100
- } else {
101
- runtime.threads.switchToNewThread().catch(() => {});
102
- }
103
- }, [runtime, options.threadId]);
104
-
105
99
  return runtime;
106
100
  };
@@ -1,8 +1,10 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import type { CompleteAttachment } from "../../types/attachment";
3
+ import type { ThreadAssistantMessage } from "../../types/message";
3
4
  import type { ThreadRuntimeCoreBinding } from "./thread-runtime";
4
5
  import {
5
6
  MessageRuntimeImpl,
7
+ toMessagePartStatus,
6
8
  type MessageState,
7
9
  type MessageStateBinding,
8
10
  } from "./message-runtime";
@@ -63,6 +65,144 @@ const threadBinding = {
63
65
  subscribe: () => () => {},
64
66
  } as unknown as ThreadRuntimeCoreBinding;
65
67
 
68
+ const createAssistantMessage = (
69
+ content: ThreadAssistantMessage["content"],
70
+ status: ThreadAssistantMessage["status"],
71
+ ): ThreadAssistantMessage => ({
72
+ id: "message-1",
73
+ role: "assistant",
74
+ createdAt: new Date(0),
75
+ content,
76
+ status,
77
+ metadata: {
78
+ unstable_state: null,
79
+ unstable_annotations: [],
80
+ unstable_data: [],
81
+ steps: [],
82
+ custom: {},
83
+ },
84
+ });
85
+
86
+ describe("toMessagePartStatus", () => {
87
+ it("honours a supplied running status on a non-last part", () => {
88
+ const message = createAssistantMessage(
89
+ [
90
+ { type: "text", text: "first", status: { type: "running" } },
91
+ { type: "text", text: "last" },
92
+ ],
93
+ { type: "running" },
94
+ );
95
+
96
+ expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
97
+ type: "running",
98
+ });
99
+ });
100
+
101
+ it("honours a supplied complete status on the last part", () => {
102
+ const message = createAssistantMessage(
103
+ [{ type: "reasoning", text: "done", status: { type: "complete" } }],
104
+ { type: "running" },
105
+ );
106
+
107
+ expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
108
+ type: "complete",
109
+ });
110
+ });
111
+
112
+ it("ignores supplied statuses after the message completes", () => {
113
+ const message = createAssistantMessage(
114
+ [{ type: "text", text: "truncated", status: { type: "running" } }],
115
+ { type: "complete", reason: "stop" },
116
+ );
117
+
118
+ expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
119
+ type: "complete",
120
+ reason: "stop",
121
+ });
122
+ });
123
+
124
+ it("falls back to positional statuses for statusless running parts", () => {
125
+ const message = createAssistantMessage(
126
+ [
127
+ { type: "text", text: "first" },
128
+ { type: "reasoning", text: "last" },
129
+ ],
130
+ { type: "running" },
131
+ );
132
+
133
+ expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
134
+ type: "complete",
135
+ });
136
+ expect(toMessagePartStatus(message, 1, message.content[1]!)).toEqual({
137
+ type: "running",
138
+ });
139
+ });
140
+
141
+ it("preserves tool-call status derivation", () => {
142
+ const unresolved = createAssistantMessage(
143
+ [
144
+ {
145
+ type: "tool-call",
146
+ toolCallId: "call-1",
147
+ toolName: "weather",
148
+ args: {},
149
+ argsText: "{}",
150
+ },
151
+ ],
152
+ { type: "running" },
153
+ );
154
+ const resolved = createAssistantMessage(
155
+ [
156
+ {
157
+ type: "tool-call",
158
+ toolCallId: "call-1",
159
+ toolName: "weather",
160
+ args: {},
161
+ argsText: "{}",
162
+ result: "sunny",
163
+ },
164
+ ],
165
+ { type: "running" },
166
+ );
167
+
168
+ expect(toMessagePartStatus(unresolved, 0, unresolved.content[0]!)).toEqual({
169
+ type: "running",
170
+ });
171
+ expect(toMessagePartStatus(resolved, 0, resolved.content[0]!)).toEqual({
172
+ type: "complete",
173
+ });
174
+ });
175
+
176
+ it("normalizes supplied upstream statuses", () => {
177
+ const upstreamComplete = {
178
+ type: "text",
179
+ text: "done",
180
+ status: { type: "complete", reason: "unknown" },
181
+ } as unknown as ThreadAssistantMessage["content"][number];
182
+ const upstreamIncomplete = {
183
+ type: "reasoning",
184
+ text: "interrupted",
185
+ status: {
186
+ type: "incomplete",
187
+ reason: "unknown",
188
+ error: "upstream error",
189
+ },
190
+ } as unknown as ThreadAssistantMessage["content"][number];
191
+ const message = createAssistantMessage(
192
+ [upstreamComplete, upstreamIncomplete],
193
+ { type: "running" },
194
+ );
195
+
196
+ expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
197
+ type: "complete",
198
+ });
199
+ expect(toMessagePartStatus(message, 1, message.content[1]!)).toEqual({
200
+ type: "incomplete",
201
+ reason: "other",
202
+ });
203
+ });
204
+ });
205
+
66
206
  describe("MessageRuntimeImpl paths", () => {
67
207
  it("appends nested selectors to the message path", () => {
68
208
  const runtime = new MessageRuntimeImpl(messageBinding, threadBinding);
@@ -8,6 +8,10 @@ import type {
8
8
  } from "../../types/message";
9
9
  import type { Unsubscribe } from "../../types/unsubscribe";
10
10
  import type { MessagePartStatus, RunConfig } from "../../types/message";
11
+ import {
12
+ COMPLETE_STATUS,
13
+ normalizePartStatus,
14
+ } from "../../utils/normalizePartStatus";
11
15
  import { getThreadMessageText } from "../../utils/text";
12
16
  import { NestedSubscriptionSubject } from "../../subscribable/subscribable";
13
17
  import {
@@ -32,10 +36,6 @@ import type { MessageRuntimePath } from "./paths";
32
36
  import type { ThreadRuntimeCoreBinding } from "./thread-runtime";
33
37
  import type { MessageStateBinding } from "./bindings";
34
38
 
35
- const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
36
- type: "complete",
37
- });
38
-
39
39
  export const toMessagePartStatus = (
40
40
  message: ThreadMessage,
41
41
  partIndex: number,
@@ -51,6 +51,11 @@ export const toMessagePartStatus = (
51
51
  }
52
52
  }
53
53
 
54
+ if (message.status.type === "running") {
55
+ const status = normalizePartStatus(part);
56
+ if (status) return status;
57
+ }
58
+
54
59
  const isLastPart = partIndex === Math.max(0, message.content.length - 1);
55
60
  if (message.status.type === "requires-action") return COMPLETE_STATUS;
56
61
  return isLastPart ? (message.status as MessagePartStatus) : COMPLETE_STATUS;
@@ -70,7 +75,7 @@ const getMessagePartState = (
70
75
  return Object.freeze({
71
76
  ...part,
72
77
  ...{ [symbolInnerMessage]: (part as any)[symbolInnerMessage] },
73
- status,
78
+ status: status as MessagePartStatus,
74
79
  });
75
80
  };
76
81
 
@@ -131,12 +131,12 @@ export const fromThreadMessageLike = (
131
131
  }: ImageMessagePart): ImageMessagePart | null => {
132
132
  if (typeof image !== "string") return null;
133
133
  const dataUri = image.match(
134
- /^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.*)$/,
134
+ /^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.*)$/i,
135
135
  );
136
136
  if (dataUri) {
137
137
  return { ...rest, image };
138
138
  }
139
- if (/^(https:\/\/|blob:)/.test(image)) {
139
+ if (/^(https:\/\/|blob:)/i.test(image)) {
140
140
  return { ...rest, image };
141
141
  }
142
142
  console.warn(`Invalid image data format detected`);
@@ -78,10 +78,11 @@ export type RemoteThreadListOptions = {
78
78
  threadId?: string | undefined;
79
79
 
80
80
  /**
81
- * Called whenever the active thread's canonical (remote) ID changes, so the
82
- * value can be treated as a managed/controlled variable (e.g. synced to a
83
- * URL query param). Together with `threadId` this forms the controlled
84
- * pattern: `threadId` in, `onThreadIdChange` out.
81
+ * Called whenever the runtime changes the active thread's canonical (remote)
82
+ * ID, so the value can be treated as a managed/controlled variable (e.g.
83
+ * synced to a URL query param). Changes initiated by the controlled
84
+ * `threadId` option are not echoed back. Together these options form the
85
+ * controlled pattern: `threadId` in, `onThreadIdChange` out.
85
86
  *
86
87
  * Only the settled remote ID is emitted: while a freshly created thread is
87
88
  * still optimistic (no remote ID yet) the value is `undefined`, and the real
@@ -5,12 +5,8 @@ import type {
5
5
  ChainOfThoughtState,
6
6
  ChainOfThoughtPart,
7
7
  } from "../scopes/chain-of-thought";
8
- import type { MessagePartStatus } from "../../types/message";
9
8
  import type { PartMethods } from "../scopes/part";
10
-
11
- const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
12
- type: "complete",
13
- });
9
+ import { getGroupStatus } from "../../utils/getGroupStatus";
14
10
 
15
11
  const useChainOfThoughtClient = ({
16
12
  parts,
@@ -22,8 +18,7 @@ const useChainOfThoughtClient = ({
22
18
  const [collapsed, setCollapsed] = useState(true);
23
19
 
24
20
  const status = useMemo(() => {
25
- const lastPart = parts[parts.length - 1];
26
- return lastPart?.status ?? COMPLETE_STATUS;
21
+ return getGroupStatus(parts);
27
22
  }, [parts]);
28
23
 
29
24
  const state = useMemo<ChainOfThoughtState>(