@assistant-ui/core 0.3.2 → 0.3.4

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 (169) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.js.map +1 -1
  3. package/dist/internal.d.ts +9 -7
  4. package/dist/internal.js +8 -6
  5. package/dist/react/primitive-hooks/useActionBarCopy.js +51 -24
  6. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  7. package/dist/react/primitive-hooks/useSuggestionTrigger.js +29 -25
  8. package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
  9. package/dist/react/primitives/message/MessageGroupedParts.d.ts +2 -1
  10. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  11. package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
  12. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  13. package/dist/react/primitives/message/MessageParts.d.ts +6 -1
  14. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  15. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  16. package/dist/react/providers/ReadonlyThreadProvider.d.ts.map +1 -1
  17. package/dist/react/providers/ReadonlyThreadProvider.js +1 -0
  18. package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
  19. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +67 -0
  20. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  21. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +76 -15
  22. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  23. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +4 -0
  24. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  25. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +21 -0
  26. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  27. package/dist/react/runtimes/cloud/auiV0.d.ts +26 -2
  28. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  29. package/dist/react/runtimes/cloud/auiV0.js +29 -8
  30. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  31. package/dist/react/types/MessagePartComponentTypes.d.ts +8 -0
  32. package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
  33. package/dist/runtime/api/bindings.d.ts +5 -0
  34. package/dist/runtime/api/bindings.d.ts.map +1 -1
  35. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  36. package/dist/runtime/api/message-runtime.js +5 -1
  37. package/dist/runtime/api/message-runtime.js.map +1 -1
  38. package/dist/runtime/api/thread-list-runtime.d.ts +14 -1
  39. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  40. package/dist/runtime/api/thread-list-runtime.js +6 -1
  41. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  42. package/dist/runtime/api/thread-runtime.d.ts +7 -1
  43. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  44. package/dist/runtime/api/thread-runtime.js +11 -3
  45. package/dist/runtime/api/thread-runtime.js.map +1 -1
  46. package/dist/runtime/base/base-composer-runtime-core.d.ts +3 -0
  47. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  48. package/dist/runtime/base/base-composer-runtime-core.js +54 -12
  49. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  50. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +10 -0
  51. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  52. package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
  53. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  54. package/dist/runtime/internal.d.ts +4 -4
  55. package/dist/runtime/internal.js +4 -4
  56. package/dist/runtimes/external-store/external-store-adapter.d.ts +9 -0
  57. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  58. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -0
  59. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  60. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +4 -0
  61. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  62. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
  63. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  64. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +6 -0
  65. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  66. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
  67. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  68. package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
  69. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  70. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -0
  71. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  72. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -0
  73. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  74. package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -0
  75. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  76. package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
  77. package/dist/store/clients/chain-of-thought-client.js +29 -23
  78. package/dist/store/clients/chain-of-thought-client.js.map +1 -1
  79. package/dist/store/clients/thread-message-client.d.ts.map +1 -1
  80. package/dist/store/clients/thread-message-client.js +101 -89
  81. package/dist/store/clients/thread-message-client.js.map +1 -1
  82. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +1 -0
  83. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  84. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +66 -45
  85. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  86. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  87. package/dist/store/runtime-clients/thread-list-runtime-client.js +91 -75
  88. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  89. package/dist/store/scopes/thread-list-item.d.ts +7 -0
  90. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  91. package/dist/store/scopes/threads.d.ts +1 -0
  92. package/dist/store/scopes/threads.d.ts.map +1 -1
  93. package/dist/types/index.d.ts +2 -2
  94. package/dist/types/message.d.ts +22 -1
  95. package/dist/types/message.d.ts.map +1 -1
  96. package/dist/types/message.js.map +1 -1
  97. package/dist/utils/data-url.d.ts +14 -1
  98. package/dist/utils/data-url.d.ts.map +1 -1
  99. package/dist/utils/data-url.js +23 -1
  100. package/dist/utils/data-url.js.map +1 -1
  101. package/dist/utils/getGroupStatus.d.ts +9 -0
  102. package/dist/utils/getGroupStatus.d.ts.map +1 -0
  103. package/dist/utils/getGroupStatus.js +15 -0
  104. package/dist/utils/getGroupStatus.js.map +1 -0
  105. package/dist/utils/image-media-type.d.ts +13 -0
  106. package/dist/utils/image-media-type.d.ts.map +1 -0
  107. package/dist/utils/image-media-type.js +141 -0
  108. package/dist/utils/image-media-type.js.map +1 -0
  109. package/dist/utils/normalizePartStatus.d.ts +8 -0
  110. package/dist/utils/normalizePartStatus.d.ts.map +1 -0
  111. package/dist/utils/normalizePartStatus.js +39 -0
  112. package/dist/utils/normalizePartStatus.js.map +1 -0
  113. package/dist/utils/wire-media.d.ts +35 -0
  114. package/dist/utils/wire-media.d.ts.map +1 -0
  115. package/dist/utils/wire-media.js +55 -0
  116. package/dist/utils/wire-media.js.map +1 -0
  117. package/package.json +4 -4
  118. package/src/index.ts +1 -0
  119. package/src/internal.ts +13 -1
  120. package/src/react/primitive-hooks/useActionBarCopy.test.ts +109 -14
  121. package/src/react/primitive-hooks/useActionBarCopy.ts +27 -2
  122. package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +148 -0
  123. package/src/react/primitive-hooks/useSuggestionTrigger.ts +10 -4
  124. package/src/react/primitives/message/MessageGroupedParts.tsx +4 -4
  125. package/src/react/primitives/message/MessageParts.tsx +6 -1
  126. package/src/react/providers/ReadonlyThreadProvider.tsx +1 -0
  127. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +157 -0
  128. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +160 -0
  129. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +135 -32
  130. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +37 -0
  131. package/src/react/runtimes/cloud/auiV0.ts +59 -14
  132. package/src/react/types/MessagePartComponentTypes.ts +8 -0
  133. package/src/runtime/api/bindings.ts +5 -0
  134. package/src/runtime/api/message-runtime.test.ts +140 -0
  135. package/src/runtime/api/message-runtime.ts +10 -5
  136. package/src/runtime/api/thread-list-runtime.ts +22 -1
  137. package/src/runtime/api/thread-runtime.ts +15 -6
  138. package/src/runtime/base/base-composer-runtime-core.ts +72 -19
  139. package/src/runtime/interfaces/thread-list-runtime-core.ts +11 -0
  140. package/src/runtime/interfaces/thread-runtime-core.ts +15 -0
  141. package/src/runtimes/external-store/external-store-adapter.ts +9 -0
  142. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +9 -0
  143. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +8 -0
  144. package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
  145. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -0
  146. package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -0
  147. package/src/store/clients/chain-of-thought-client.ts +2 -7
  148. package/src/store/clients/thread-message-client.test.ts +84 -0
  149. package/src/store/clients/thread-message-client.ts +14 -4
  150. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -2
  151. package/src/store/runtime-clients/thread-list-runtime-client.ts +13 -1
  152. package/src/store/scopes/thread-list-item.ts +7 -0
  153. package/src/store/scopes/threads.ts +1 -0
  154. package/src/tests/RemoteThreadListThreadListRuntimeCore-reloadMainThread.test.ts +257 -0
  155. package/src/tests/auiV0Encode.test.ts +218 -1
  156. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +296 -0
  157. package/src/tests/event-subscription-listener-errors.test.ts +1 -0
  158. package/src/tests/external-store-thread-list-runtime-core.test.ts +39 -1
  159. package/src/types/index.ts +1 -0
  160. package/src/types/message.ts +30 -0
  161. package/src/utils/data-url.ts +24 -0
  162. package/src/utils/getGroupStatus.test.ts +36 -0
  163. package/src/utils/getGroupStatus.ts +31 -0
  164. package/src/utils/image-media-type.test.ts +96 -0
  165. package/src/utils/image-media-type.ts +118 -0
  166. package/src/utils/normalizePartStatus.test.ts +59 -0
  167. package/src/utils/normalizePartStatus.ts +55 -0
  168. package/src/utils/wire-media.test.ts +96 -0
  169. package/src/utils/wire-media.ts +70 -0
@@ -18,14 +18,25 @@ import { ThreadListItemRuntimeProvider } from "../providers/ThreadListItemRuntim
18
18
  import type { ThreadRuntimeCore } from "../../runtime/interfaces/thread-runtime-core";
19
19
  import type { ThreadListRuntimeCore } from "../../runtime/interfaces/thread-list-runtime-core";
20
20
  import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
21
+ import type { Unsubscribe } from "../../types/unsubscribe";
21
22
  import { BaseSubscribable } from "../../subscribable/subscribable";
22
- import type { ThreadRuntimeImpl } from "../../runtime/api/thread-runtime";
23
+ import {
24
+ getThreadRuntimeCoreIsRunning,
25
+ type ThreadRuntimeImpl,
26
+ } from "../../runtime/api/thread-runtime";
23
27
  import { ThreadListRuntimeImpl } from "../../runtime/api/thread-list-runtime";
24
28
 
25
29
  type RemoteThreadListHook = () => AssistantRuntime;
26
30
 
27
31
  type RemoteThreadListHookInstance = {
28
- runtime?: ThreadRuntimeCore;
32
+ runtime?: ThreadRuntimeCore | undefined;
33
+ // A runtime riding across a restart stays readable, but only counts as
34
+ // attached once a binder of the current generation re-publishes it.
35
+ publishedGeneration?: number | undefined;
36
+ // Part of the binder's React key, so only a bump remounts the hook.
37
+ generation: number;
38
+ isRunning: boolean;
39
+ unsubscribeRunning?: Unsubscribe | undefined;
29
40
  };
30
41
 
31
42
  const ProviderRenderDetector: FC<{
@@ -41,6 +52,9 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
41
52
  StoreApi<{ useRuntime: RemoteThreadListHook }>
42
53
  >;
43
54
  private instances = new Map<string, RemoteThreadListHookInstance>();
55
+ // Manager-wide so it survives instance deletion: a stop and start within one
56
+ // React commit must not reuse a binder key.
57
+ private nextGeneration = 0;
44
58
  private useAliveThreadsKeysChanged = create(() => ({}));
45
59
  private parent: ThreadListRuntimeCore;
46
60
 
@@ -53,20 +67,18 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
53
67
  this.useRuntimeHook = create(() => ({ useRuntime: runtimeHook }));
54
68
  }
55
69
 
56
- public startThreadRuntime(threadId: string) {
57
- if (!this.instances.has(threadId)) {
58
- this.instances.set(threadId, {});
59
- this.useAliveThreadsKeysChanged.setState({}, true);
60
- }
61
-
70
+ private _whenRuntimeAttached(threadId: string) {
62
71
  return new Promise<ThreadRuntimeCore>((resolve, reject) => {
63
72
  const callback = () => {
64
73
  const instance = this.instances.get(threadId);
65
74
  if (!instance) {
66
75
  dispose();
67
76
  reject(new Error("Thread was deleted before runtime was started"));
68
- } else if (!instance.runtime) {
69
- return; // misc update
77
+ } else if (
78
+ !instance.runtime ||
79
+ instance.publishedGeneration !== instance.generation
80
+ ) {
81
+ return; // not yet published by the current generation's binder
70
82
  } else {
71
83
  dispose();
72
84
  resolve(instance.runtime);
@@ -77,13 +89,104 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
77
89
  });
78
90
  }
79
91
 
92
+ public startThreadRuntime(threadId: string) {
93
+ if (!this.instances.has(threadId)) {
94
+ this.instances.set(threadId, {
95
+ generation: this.nextGeneration++,
96
+ isRunning: false,
97
+ });
98
+ this.useAliveThreadsKeysChanged.setState({}, true);
99
+ }
100
+
101
+ return this._whenRuntimeAttached(threadId);
102
+ }
103
+
104
+ public __internal_restartThreadRuntime(threadId: string) {
105
+ const instance = this.instances.get(threadId);
106
+ if (!instance) return this.startThreadRuntime(threadId);
107
+
108
+ instance.generation = this.nextGeneration++;
109
+ this.useAliveThreadsKeysChanged.setState({}, true);
110
+ this._notifySubscribers();
111
+
112
+ return this._whenRuntimeAttached(threadId);
113
+ }
114
+
80
115
  public getThreadRuntimeCore(threadId: string) {
81
116
  const instance = this.instances.get(threadId);
82
117
  if (!instance) return undefined;
83
118
  return instance.runtime;
84
119
  }
85
120
 
121
+ public __internal_isThreadRunning(threadId: string) {
122
+ return this.instances.get(threadId)?.isRunning ?? false;
123
+ }
124
+
125
+ private runningSubscribers = new Set<() => void>();
126
+
127
+ /**
128
+ * Fires when any thread crosses the running boundary. Separate from the
129
+ * general subscription so a run does not push the thread list through the
130
+ * channel that resolves pending runtime attachments.
131
+ */
132
+ public __internal_subscribeRunningChanged(callback: () => void): Unsubscribe {
133
+ this.runningSubscribers.add(callback);
134
+ return () => this.runningSubscribers.delete(callback);
135
+ }
136
+
137
+ private _publishThreadRuntime(
138
+ threadId: string,
139
+ runtime: ThreadRuntimeCore,
140
+ generation: number,
141
+ ) {
142
+ const instance = this.instances.get(threadId);
143
+ if (!instance)
144
+ throw new Error(
145
+ `Thread "${threadId}" runtime binding not found. This is a bug in assistant-ui.`,
146
+ );
147
+
148
+ // An outgoing binder outlives its generation until React commits the key
149
+ // change, and must not publish over the incoming one.
150
+ if (instance.generation !== generation) return;
151
+
152
+ const previousRuntime = instance.runtime;
153
+ instance.runtime = runtime;
154
+ instance.publishedGeneration = generation;
155
+ if (previousRuntime !== runtime) {
156
+ this._trackRunning(instance);
157
+ }
158
+ this._notifySubscribers();
159
+ }
160
+
161
+ // Run state changes far more often than the thread list does, so the list is
162
+ // only notified when a thread crosses the running boundary.
163
+ private _trackRunning(instance: RemoteThreadListHookInstance) {
164
+ instance.unsubscribeRunning?.();
165
+
166
+ const runtime = instance.runtime;
167
+ if (!runtime) {
168
+ instance.unsubscribeRunning = undefined;
169
+ this._setRunning(instance, false);
170
+ return;
171
+ }
172
+
173
+ this._setRunning(instance, getThreadRuntimeCoreIsRunning(runtime));
174
+ instance.unsubscribeRunning = runtime.subscribe(() => {
175
+ this._setRunning(instance, getThreadRuntimeCoreIsRunning(runtime));
176
+ });
177
+ }
178
+
179
+ private _setRunning(
180
+ instance: RemoteThreadListHookInstance,
181
+ isRunning: boolean,
182
+ ) {
183
+ if (instance.isRunning === isRunning) return;
184
+ instance.isRunning = isRunning;
185
+ for (const callback of this.runningSubscribers) callback();
186
+ }
187
+
86
188
  public stopThreadRuntime(threadId: string) {
189
+ this.instances.get(threadId)?.unsubscribeRunning?.();
87
190
  this.instances.delete(threadId);
88
191
  this.useAliveThreadsKeysChanged.setState({}, true);
89
192
  this._notifySubscribers();
@@ -98,10 +201,9 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
98
201
 
99
202
  // Rendered as a child of the user's Provider so the runtime hook can
100
203
  // read context the Provider injects (e.g. RuntimeAdapterProvider).
101
- private _RuntimeBinder: FC<PropsWithChildren<{ threadId: string }>> = ({
102
- threadId,
103
- children,
104
- }) => {
204
+ private _RuntimeBinder: FC<
205
+ PropsWithChildren<{ threadId: string; generation: number }>
206
+ > = ({ threadId, generation, children }) => {
105
207
  const { useRuntime } = this.useRuntimeHook();
106
208
  const runtime = useRuntime();
107
209
 
@@ -109,15 +211,12 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
109
211
  .__internal_threadBinding;
110
212
 
111
213
  const updateRuntime = useCallback(() => {
112
- const aliveThread = this.instances.get(threadId);
113
- if (!aliveThread)
114
- throw new Error(
115
- `Thread "${threadId}" runtime binding not found. This is a bug in assistant-ui.`,
116
- );
117
-
118
- aliveThread.runtime = threadBinding.getState();
119
- this._notifySubscribers();
120
- }, [threadId, threadBinding]);
214
+ this._publishThreadRuntime(
215
+ threadId,
216
+ threadBinding.getState(),
217
+ generation,
218
+ );
219
+ }, [threadId, generation, threadBinding]);
121
220
 
122
221
  const isMounted = useRef(false);
123
222
  if (!isMounted.current) {
@@ -168,8 +267,9 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
168
267
 
169
268
  private _OuterActiveThreadProvider: FC<{
170
269
  threadId: string;
270
+ generation: number;
171
271
  provider: ComponentType<PropsWithChildren>;
172
- }> = memo(({ threadId, provider: Provider }) => {
272
+ }> = memo(({ threadId, generation, provider: Provider }) => {
173
273
  const runtime = useMemo(
174
274
  () => new ThreadListRuntimeImpl(this.parent).getItemById(threadId),
175
275
  [threadId],
@@ -195,7 +295,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
195
295
  return (
196
296
  <ThreadListItemRuntimeProvider runtime={runtime}>
197
297
  <Provider>
198
- <this._RuntimeBinder threadId={threadId}>
298
+ <this._RuntimeBinder threadId={threadId} generation={generation}>
199
299
  <ProviderRenderDetector detectorRef={detectorRef} />
200
300
  </this._RuntimeBinder>
201
301
  </Provider>
@@ -208,12 +308,15 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
208
308
  }> = ({ provider }) => {
209
309
  this.useAliveThreadsKeysChanged(); // trigger re-render on alive threads change
210
310
 
211
- return Array.from(this.instances.keys()).map((threadId) => (
212
- <this._OuterActiveThreadProvider
213
- key={threadId}
214
- threadId={threadId}
215
- provider={provider}
216
- />
217
- ));
311
+ return Array.from(this.instances.entries()).map(
312
+ ([threadId, { generation }]) => (
313
+ <this._OuterActiveThreadProvider
314
+ key={`${threadId}:${generation}`}
315
+ threadId={threadId}
316
+ generation={generation}
317
+ provider={provider}
318
+ />
319
+ ),
320
+ );
218
321
  };
219
322
  }
@@ -185,6 +185,9 @@ export class RemoteThreadListThreadListRuntimeCore
185
185
  options.runtimeHook,
186
186
  this,
187
187
  );
188
+ this._hookManager.__internal_subscribeRunningChanged(() =>
189
+ this._notifySubscribers(),
190
+ );
188
191
  this.useProvider = create(() => ({
189
192
  Provider: options.adapter.unstable_Provider ?? Fragment,
190
193
  }));
@@ -245,6 +248,34 @@ export class RemoteThreadListThreadListRuntimeCore
245
248
  }
246
249
  }
247
250
 
251
+ public async reloadMainThread(): Promise<void> {
252
+ const threadId = this._mainThreadId;
253
+ if (threadId === undefined) return;
254
+
255
+ // An unsent thread holds no remote state, so a refetch would only discard
256
+ // what the user has typed.
257
+ if (this.getItemById(threadId)?.status === "new") return;
258
+
259
+ const runtimeCore = this._hookManager.getThreadRuntimeCore(threadId);
260
+
261
+ try {
262
+ if (runtimeCore?.unstable_refetchThread) {
263
+ // Called on the core so class-method implementations keep `this`.
264
+ await runtimeCore.unstable_refetchThread();
265
+ } else {
266
+ await this._hookManager.__internal_restartThreadRuntime(threadId);
267
+ }
268
+ } catch (error) {
269
+ // delete and detach switch the main thread away before stopping the
270
+ // runtime, so a rejection once that has happened belongs to them.
271
+ if (threadId !== this._mainThreadId) return;
272
+ throw error;
273
+ }
274
+
275
+ if (threadId !== this._mainThreadId) return;
276
+ this._notifySubscribers();
277
+ }
278
+
248
279
  public reload() {
249
280
  this._loadGeneration++;
250
281
  this._loadThreadsPromise = undefined;
@@ -321,6 +352,12 @@ export class RemoteThreadListThreadListRuntimeCore
321
352
  return result;
322
353
  }
323
354
 
355
+ public unstable_isThreadRunning(threadIdOrRemoteId: string) {
356
+ const data = this.getItemById(threadIdOrRemoteId);
357
+ if (!data) return false;
358
+ return this._hookManager.__internal_isThreadRunning(data.id);
359
+ }
360
+
324
361
  public getItemById(threadIdOrRemoteId: string) {
325
362
  return getThreadData(this._state.value, threadIdOrRemoteId);
326
363
  }
@@ -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";
@@ -82,14 +77,38 @@ type AuiV0MessagePart =
82
77
  readonly data: string;
83
78
  readonly mimeType: string;
84
79
  readonly filename?: string;
80
+ readonly sourceType?: "url" | "id";
85
81
  };
86
82
 
87
83
  type AuiV0AttachmentPart =
88
- | TextMessagePart
89
- | ImageMessagePart
90
- | FileMessagePart
91
- | Unstable_AudioMessagePart
92
- | DataMessagePart<ReadonlyJSONValue>;
84
+ | {
85
+ readonly type: "text";
86
+ readonly text: string;
87
+ }
88
+ | {
89
+ readonly type: "image";
90
+ readonly image: string;
91
+ readonly filename?: string;
92
+ }
93
+ | {
94
+ readonly type: "file";
95
+ readonly data: string;
96
+ readonly mimeType: string;
97
+ readonly filename?: string;
98
+ readonly sourceType?: "url" | "id";
99
+ }
100
+ | {
101
+ readonly type: "audio";
102
+ readonly audio: {
103
+ readonly data: string;
104
+ readonly format: "mp3" | "wav";
105
+ };
106
+ }
107
+ | {
108
+ readonly type: "data";
109
+ readonly name: string;
110
+ readonly data: ReadonlyJSONValue;
111
+ };
93
112
 
94
113
  type AuiV0Attachment = {
95
114
  readonly id: string;
@@ -125,16 +144,41 @@ const encodeAttachmentPart = (
125
144
  const type = part.type;
126
145
  switch (type) {
127
146
  case "text":
147
+ return { type: "text", text: part.text };
148
+
128
149
  case "image":
150
+ return {
151
+ type: "image",
152
+ image: part.image,
153
+ ...(part.filename != null ? { filename: part.filename } : undefined),
154
+ };
155
+
129
156
  case "file":
157
+ return {
158
+ type: "file",
159
+ data: part.data,
160
+ mimeType: part.mimeType,
161
+ ...(part.filename != null ? { filename: part.filename } : undefined),
162
+ ...(part.sourceType != null
163
+ ? { sourceType: part.sourceType }
164
+ : undefined),
165
+ };
166
+
130
167
  case "audio":
131
- return part;
168
+ return {
169
+ type: "audio",
170
+ audio: { data: part.audio.data, format: part.audio.format },
171
+ };
132
172
 
133
173
  case "data": {
134
174
  if (!isJSONValue(part.data)) {
135
175
  console.warn(`attachment data is not JSON! ${JSON.stringify(part)}`);
136
176
  }
137
- return { ...part, data: part.data as ReadonlyJSONValue };
177
+ return {
178
+ type: "data",
179
+ name: part.name,
180
+ data: part.data as ReadonlyJSONValue,
181
+ };
138
182
  }
139
183
 
140
184
  default: {
@@ -213,7 +257,7 @@ export function auiV0Encode(message: ThreadMessage): AuiV0Message {
213
257
  };
214
258
 
215
259
  case "tool-call": {
216
- if (!isJSONValue(part.result)) {
260
+ if (part.result !== undefined && !isJSONValue(part.result)) {
217
261
  console.warn(
218
262
  `tool-call result is not JSON! ${JSON.stringify(part)}`,
219
263
  );
@@ -225,7 +269,7 @@ export function auiV0Encode(message: ThreadMessage): AuiV0Message {
225
269
  ...(JSON.stringify(part.args) === part.argsText
226
270
  ? { args: part.args }
227
271
  : { argsText: part.argsText }),
228
- ...(part.result
272
+ ...(part.result !== undefined
229
273
  ? { result: part.result as ReadonlyJSONValue }
230
274
  : undefined),
231
275
  ...(part.isError ? { isError: true } : undefined),
@@ -242,6 +286,7 @@ export function auiV0Encode(message: ThreadMessage): AuiV0Message {
242
286
  data: part.data,
243
287
  mimeType: part.mimeType,
244
288
  ...(part.filename ? { filename: part.filename } : undefined),
289
+ ...(part.sourceType ? { sourceType: part.sourceType } : undefined),
245
290
  };
246
291
 
247
292
  default: {
@@ -44,8 +44,16 @@ export type ImageMessagePartComponent = ComponentType<ImageMessagePartProps>;
44
44
  export type FileMessagePartProps = MessagePartState & FileMessagePart;
45
45
  export type FileMessagePartComponent = ComponentType<FileMessagePartProps>;
46
46
 
47
+ /**
48
+ * @deprecated Use {@link FileMessagePartProps} and render `audio/*` mime types
49
+ * from the `File` slot.
50
+ */
47
51
  export type Unstable_AudioMessagePartProps = MessagePartState &
48
52
  Unstable_AudioMessagePart;
53
+ /**
54
+ * @deprecated Use {@link FileMessagePartComponent} and render `audio/*` mime
55
+ * types from the `File` slot.
56
+ */
49
57
  export type Unstable_AudioMessagePartComponent =
50
58
  ComponentType<Unstable_AudioMessagePartProps>;
51
59
 
@@ -37,6 +37,11 @@ export type MessageStateBinding = SubscribableWithState<
37
37
 
38
38
  export type ThreadListItemState = {
39
39
  readonly isMain: boolean;
40
+ /**
41
+ * Whether this thread has a run in progress, including a run that continues
42
+ * after the user switches to another thread.
43
+ */
44
+ readonly isRunning: boolean;
40
45
  readonly id: string;
41
46
  readonly remoteId: string | undefined;
42
47
  readonly externalId: string | undefined;
@@ -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