@assistant-ui/core 0.3.3 → 0.3.5

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 (128) hide show
  1. package/dist/internal.d.ts +4 -2
  2. package/dist/internal.js +4 -2
  3. package/dist/react/primitive-hooks/useSuggestionTrigger.js +29 -25
  4. package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
  5. package/dist/react/primitives/message/MessageParts.d.ts +6 -1
  6. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  7. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  8. package/dist/react/providers/ReadonlyThreadProvider.d.ts.map +1 -1
  9. package/dist/react/providers/ReadonlyThreadProvider.js +1 -0
  10. package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
  11. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +67 -0
  12. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  13. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +76 -15
  14. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  15. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +4 -0
  16. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  17. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +22 -1
  18. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  19. package/dist/react/runtimes/cloud/auiV0.d.ts +2 -0
  20. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  21. package/dist/react/runtimes/cloud/auiV0.js +6 -4
  22. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  23. package/dist/react/types/MessagePartComponentTypes.d.ts +8 -0
  24. package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
  25. package/dist/runtime/api/bindings.d.ts +5 -0
  26. package/dist/runtime/api/bindings.d.ts.map +1 -1
  27. package/dist/runtime/api/thread-list-runtime.d.ts +14 -1
  28. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  29. package/dist/runtime/api/thread-list-runtime.js +6 -1
  30. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  31. package/dist/runtime/api/thread-runtime.d.ts +7 -1
  32. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  33. package/dist/runtime/api/thread-runtime.js +11 -3
  34. package/dist/runtime/api/thread-runtime.js.map +1 -1
  35. package/dist/runtime/base/base-composer-runtime-core.d.ts +3 -0
  36. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  37. package/dist/runtime/base/base-composer-runtime-core.js +54 -12
  38. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  39. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +10 -0
  40. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  41. package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
  42. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  43. package/dist/runtimes/external-store/external-store-adapter.d.ts +9 -0
  44. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  45. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -0
  46. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  47. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +4 -0
  48. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  49. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
  50. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  51. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +6 -0
  52. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  53. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
  54. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  55. package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
  56. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  57. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -0
  58. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  59. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -0
  60. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  61. package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -0
  62. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  63. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +1 -0
  64. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  65. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +66 -45
  66. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  67. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  68. package/dist/store/runtime-clients/thread-list-runtime-client.js +91 -75
  69. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  70. package/dist/store/scopes/thread-list-item.d.ts +7 -0
  71. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  72. package/dist/store/scopes/threads.d.ts +1 -0
  73. package/dist/store/scopes/threads.d.ts.map +1 -1
  74. package/dist/types/message.d.ts +11 -0
  75. package/dist/types/message.d.ts.map +1 -1
  76. package/dist/types/message.js.map +1 -1
  77. package/dist/utils/data-url.d.ts +14 -1
  78. package/dist/utils/data-url.d.ts.map +1 -1
  79. package/dist/utils/data-url.js +23 -1
  80. package/dist/utils/data-url.js.map +1 -1
  81. package/dist/utils/image-media-type.d.ts +13 -0
  82. package/dist/utils/image-media-type.d.ts.map +1 -0
  83. package/dist/utils/image-media-type.js +141 -0
  84. package/dist/utils/image-media-type.js.map +1 -0
  85. package/dist/utils/wire-media.d.ts +35 -0
  86. package/dist/utils/wire-media.d.ts.map +1 -0
  87. package/dist/utils/wire-media.js +55 -0
  88. package/dist/utils/wire-media.js.map +1 -0
  89. package/package.json +4 -4
  90. package/src/internal.ts +12 -1
  91. package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +148 -0
  92. package/src/react/primitive-hooks/useSuggestionTrigger.ts +10 -4
  93. package/src/react/primitives/message/MessageParts.tsx +6 -1
  94. package/src/react/providers/ReadonlyThreadProvider.tsx +1 -0
  95. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +157 -0
  96. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +160 -0
  97. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +135 -32
  98. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +41 -1
  99. package/src/react/runtimes/cloud/auiV0.ts +8 -2
  100. package/src/react/types/MessagePartComponentTypes.ts +8 -0
  101. package/src/runtime/api/bindings.ts +5 -0
  102. package/src/runtime/api/thread-list-runtime.ts +22 -1
  103. package/src/runtime/api/thread-runtime.ts +15 -6
  104. package/src/runtime/base/base-composer-runtime-core.ts +72 -19
  105. package/src/runtime/interfaces/thread-list-runtime-core.ts +11 -0
  106. package/src/runtime/interfaces/thread-runtime-core.ts +15 -0
  107. package/src/runtimes/external-store/external-store-adapter.ts +9 -0
  108. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +9 -0
  109. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +8 -0
  110. package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
  111. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -0
  112. package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -0
  113. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -2
  114. package/src/store/runtime-clients/thread-list-runtime-client.ts +13 -1
  115. package/src/store/scopes/thread-list-item.ts +7 -0
  116. package/src/store/scopes/threads.ts +1 -0
  117. package/src/tests/RemoteThreadListThreadListRuntimeCore-reloadMainThread.test.ts +257 -0
  118. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-order.test.ts +31 -0
  119. package/src/tests/auiV0Encode.test.ts +125 -1
  120. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +296 -0
  121. package/src/tests/event-subscription-listener-errors.test.ts +1 -0
  122. package/src/tests/external-store-thread-list-runtime-core.test.ts +39 -1
  123. package/src/types/message.ts +11 -0
  124. package/src/utils/data-url.ts +24 -0
  125. package/src/utils/image-media-type.test.ts +96 -0
  126. package/src/utils/image-media-type.ts +118 -0
  127. package/src/utils/wire-media.test.ts +96 -0
  128. package/src/utils/wire-media.ts +70 -0
@@ -16,3 +16,163 @@ describe("RemoteThreadListHookInstanceManager", () => {
16
16
  );
17
17
  });
18
18
  });
19
+
20
+ describe("RemoteThreadListHookInstanceManager.__internal_restartThreadRuntime", () => {
21
+ const makeManager = () =>
22
+ new RemoteThreadListHookInstanceManager(
23
+ () => ({}) as never,
24
+ {} as ThreadListRuntimeCore,
25
+ );
26
+
27
+ // no React binder attaches a runtime in these tests, so the returned promises
28
+ // stay pending or reject on stop; neither is what is under test here
29
+ const start = (manager: RemoteThreadListHookInstanceManager, id: string) => {
30
+ manager.startThreadRuntime(id).catch(() => {});
31
+ };
32
+ const restart = (
33
+ manager: RemoteThreadListHookInstanceManager,
34
+ id: string,
35
+ ) => {
36
+ manager.__internal_restartThreadRuntime(id).catch(() => {});
37
+ };
38
+
39
+ type InstanceInternals = {
40
+ instances: Map<
41
+ string,
42
+ {
43
+ runtime?: unknown;
44
+ publishedGeneration?: number;
45
+ generation: number;
46
+ }
47
+ >;
48
+ _notifySubscribers: () => void;
49
+ };
50
+ const internalsOf = (manager: RemoteThreadListHookInstanceManager) =>
51
+ manager as unknown as InstanceInternals;
52
+
53
+ const renderedKeys = (manager: RemoteThreadListHookInstanceManager) =>
54
+ Array.from(internalsOf(manager).instances.entries()).map(
55
+ ([id, { generation }]) => `${id}:${generation}`,
56
+ );
57
+
58
+ it("changes the binder key so React remounts the runtime hook", () => {
59
+ const manager = makeManager();
60
+ start(manager, "thread-1");
61
+ const before = renderedKeys(manager);
62
+
63
+ restart(manager, "thread-1");
64
+
65
+ expect(renderedKeys(manager)).not.toEqual(before);
66
+ expect(renderedKeys(manager)).toEqual(["thread-1:1"]);
67
+ });
68
+
69
+ it("keeps the thread rendered across the restart, unlike stop", () => {
70
+ const restarted = makeManager();
71
+ start(restarted, "thread-1");
72
+ restart(restarted, "thread-1");
73
+
74
+ const stopped = makeManager();
75
+ start(stopped, "thread-1");
76
+ stopped.stopThreadRuntime("thread-1");
77
+
78
+ expect(renderedKeys(restarted)).toHaveLength(1);
79
+ expect(renderedKeys(stopped)).toHaveLength(0);
80
+ });
81
+
82
+ it("starts the runtime when the thread is not alive yet", () => {
83
+ const manager = makeManager();
84
+
85
+ restart(manager, "thread-1");
86
+
87
+ expect(renderedKeys(manager)).toEqual(["thread-1:0"]);
88
+ });
89
+
90
+ it("stop then start in one tick yields a fresh key, so the old still-mounted binder cannot satisfy the new start", () => {
91
+ const manager = makeManager();
92
+ start(manager, "thread-1");
93
+ const before = renderedKeys(manager);
94
+
95
+ manager.stopThreadRuntime("thread-1");
96
+ start(manager, "thread-1");
97
+
98
+ expect(renderedKeys(manager)).not.toEqual(before);
99
+ expect(renderedKeys(manager)).toEqual(["thread-1:1"]);
100
+ });
101
+
102
+ // simulates the binder's updateRuntime: publish a runtime for the
103
+ // instance's current generation (or an explicit stale one)
104
+ const publish = (
105
+ manager: RemoteThreadListHookInstanceManager,
106
+ id: string,
107
+ runtime: unknown,
108
+ options?: { generation?: number },
109
+ ) => {
110
+ const instance = internalsOf(manager).instances.get(id)!;
111
+ instance.runtime = runtime;
112
+ instance.publishedGeneration = options?.generation ?? instance.generation;
113
+ internalsOf(manager)._notifySubscribers();
114
+ };
115
+
116
+ it("does not settle with the pre-restart runtime; only the incoming binder's publication resolves it", async () => {
117
+ const manager = makeManager();
118
+ start(manager, "thread-1");
119
+ publish(manager, "thread-1", { tag: "pre-reload-runtime" });
120
+
121
+ let settledWith = "NOT_SETTLED";
122
+ manager.__internal_restartThreadRuntime("thread-1").then((r) => {
123
+ settledWith = (r as { tag: string }).tag;
124
+ });
125
+
126
+ // the outgoing runtime rides across the restart (stays readable)…
127
+ expect(manager.getThreadRuntimeCore("thread-1")).toEqual({
128
+ tag: "pre-reload-runtime",
129
+ });
130
+ // …but must not count as attached
131
+ await Promise.resolve();
132
+ await Promise.resolve();
133
+ await Promise.resolve();
134
+ expect(settledWith).toBe("NOT_SETTLED");
135
+
136
+ publish(manager, "thread-1", { tag: "post-reload-runtime" });
137
+ await Promise.resolve();
138
+ expect(settledWith).toBe("post-reload-runtime");
139
+ });
140
+
141
+ it("keeps the outgoing runtime readable while the restart promise is pending", () => {
142
+ const manager = makeManager();
143
+ start(manager, "thread-1");
144
+ publish(manager, "thread-1", { tag: "pre-reload-runtime" });
145
+
146
+ restart(manager, "thread-1");
147
+
148
+ expect(manager.getThreadRuntimeCore("thread-1")).toEqual({
149
+ tag: "pre-reload-runtime",
150
+ });
151
+ });
152
+
153
+ it("a stale-generation publication does not resolve the restart promise", async () => {
154
+ const manager = makeManager();
155
+ start(manager, "thread-1");
156
+ publish(manager, "thread-1", { tag: "pre-reload-runtime" });
157
+
158
+ let settled = false;
159
+ manager.__internal_restartThreadRuntime("thread-1").then(() => {
160
+ settled = true;
161
+ });
162
+
163
+ // an outgoing binder re-publishing for its old generation (e.g. a late
164
+ // outerSubscribe callback) must not count as the new attachment
165
+ const staleGeneration =
166
+ internalsOf(manager).instances.get("thread-1")!.generation - 1;
167
+ publish(
168
+ manager,
169
+ "thread-1",
170
+ { tag: "stale-publication" },
171
+ { generation: staleGeneration },
172
+ );
173
+
174
+ await Promise.resolve();
175
+ await Promise.resolve();
176
+ expect(settled).toBe(false);
177
+ });
178
+ });
@@ -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
  }
@@ -436,7 +473,10 @@ export class RemoteThreadListThreadListRuntimeCore
436
473
  if (this.mainThreadId !== undefined) {
437
474
  await task;
438
475
  } else {
439
- task.then(() => this._notifySubscribers());
476
+ void task.then(
477
+ () => this._notifySubscribers(),
478
+ () => undefined,
479
+ );
440
480
  }
441
481
 
442
482
  if (generation !== this._switchGeneration) return;
@@ -77,6 +77,7 @@ type AuiV0MessagePart =
77
77
  readonly data: string;
78
78
  readonly mimeType: string;
79
79
  readonly filename?: string;
80
+ readonly sourceType?: "url" | "id";
80
81
  };
81
82
 
82
83
  type AuiV0AttachmentPart =
@@ -94,6 +95,7 @@ type AuiV0AttachmentPart =
94
95
  readonly data: string;
95
96
  readonly mimeType: string;
96
97
  readonly filename?: string;
98
+ readonly sourceType?: "url" | "id";
97
99
  }
98
100
  | {
99
101
  readonly type: "audio";
@@ -157,6 +159,9 @@ const encodeAttachmentPart = (
157
159
  data: part.data,
158
160
  mimeType: part.mimeType,
159
161
  ...(part.filename != null ? { filename: part.filename } : undefined),
162
+ ...(part.sourceType != null
163
+ ? { sourceType: part.sourceType }
164
+ : undefined),
160
165
  };
161
166
 
162
167
  case "audio":
@@ -252,7 +257,7 @@ export function auiV0Encode(message: ThreadMessage): AuiV0Message {
252
257
  };
253
258
 
254
259
  case "tool-call": {
255
- if (!isJSONValue(part.result)) {
260
+ if (part.result !== undefined && !isJSONValue(part.result)) {
256
261
  console.warn(
257
262
  `tool-call result is not JSON! ${JSON.stringify(part)}`,
258
263
  );
@@ -264,7 +269,7 @@ export function auiV0Encode(message: ThreadMessage): AuiV0Message {
264
269
  ...(JSON.stringify(part.args) === part.argsText
265
270
  ? { args: part.args }
266
271
  : { argsText: part.argsText }),
267
- ...(part.result
272
+ ...(part.result !== undefined
268
273
  ? { result: part.result as ReadonlyJSONValue }
269
274
  : undefined),
270
275
  ...(part.isError ? { isError: true } : undefined),
@@ -281,6 +286,7 @@ export function auiV0Encode(message: ThreadMessage): AuiV0Message {
281
286
  data: part.data,
282
287
  mimeType: part.mimeType,
283
288
  ...(part.filename ? { filename: part.filename } : undefined),
289
+ ...(part.sourceType ? { sourceType: part.sourceType } : undefined),
284
290
  };
285
291
 
286
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;
@@ -31,7 +31,10 @@ export type ThreadListState = {
31
31
  readonly isLoadingMore: boolean;
32
32
  readonly hasMore: boolean;
33
33
  readonly threadItems: Readonly<
34
- Record<string, Omit<ThreadListItemState, "isMain" | "threadId">>
34
+ Record<
35
+ string,
36
+ Omit<ThreadListItemState, "isMain" | "threadId" | "isRunning">
37
+ >
35
38
  >;
36
39
  };
37
40
 
@@ -56,6 +59,18 @@ export type ThreadListRuntime = {
56
59
 
57
60
  getLoadThreadsPromise(): Promise<void>;
58
61
  reload(): Promise<void>;
62
+ /**
63
+ * Refetches the open thread's remote state, for state that changed out of
64
+ * band and so never reached the stream. When the runtime declares the
65
+ * in-place capability (`unstable_refetchThread`), composer drafts survive,
66
+ * existing messages stay rendered during the refetch, and the promise
67
+ * settles with the refetch, rejecting if it fails; that runtime also owns
68
+ * what happens to a run in progress, since this does not stop one. Runtimes
69
+ * without the capability have their hook remounted instead, which discards
70
+ * unsent composer input and ends any run, and the promise resolves once the
71
+ * new runtime attaches. A thread that has not been sent yet is left alone.
72
+ */
73
+ reloadMainThread(): Promise<void>;
59
74
  loadMore(): Promise<void>;
60
75
  };
61
76
 
@@ -91,6 +106,7 @@ const getThreadListItemState = (
91
106
  lastMessageAt: threadData.lastMessageAt,
92
107
  custom: threadData.custom,
93
108
  isMain: threadData.id === threadList.mainThreadId,
109
+ isRunning: threadList.unstable_isThreadRunning?.(threadData.id) ?? false,
94
110
  };
95
111
  };
96
112
 
@@ -155,6 +171,7 @@ export class ThreadListRuntimeImpl implements ThreadListRuntime {
155
171
  this.switchToNewThread = this.switchToNewThread.bind(this);
156
172
  this.getLoadThreadsPromise = this.getLoadThreadsPromise.bind(this);
157
173
  this.reload = this.reload.bind(this);
174
+ this.reloadMainThread = this.reloadMainThread.bind(this);
158
175
  this.loadMore = this.loadMore.bind(this);
159
176
  this.getState = this.getState.bind(this);
160
177
  this.subscribe = this.subscribe.bind(this);
@@ -183,6 +200,10 @@ export class ThreadListRuntimeImpl implements ThreadListRuntime {
183
200
  return this._core.reload?.() ?? RESOLVED_PROMISE;
184
201
  }
185
202
 
203
+ public reloadMainThread(): Promise<void> {
204
+ return this._core.reloadMainThread?.() ?? RESOLVED_PROMISE;
205
+ }
206
+
186
207
  public loadMore(): Promise<void> {
187
208
  return this._core.loadMore?.() ?? RESOLVED_PROMISE;
188
209
  }
@@ -193,22 +193,31 @@ export type ThreadState = {
193
193
  readonly voice: VoiceSessionState | undefined;
194
194
  };
195
195
 
196
+ /**
197
+ * The canonical `isRunning` derivation. A runtime that tracks run state itself
198
+ * reports it directly; the rest fall back to the trailing assistant message.
199
+ */
200
+ export const getThreadRuntimeCoreIsRunning = (
201
+ runtime: ThreadRuntimeCore,
202
+ ): boolean => {
203
+ if (runtime.isRunning !== undefined) return runtime.isRunning;
204
+ const lastMessage = runtime.messages.at(-1);
205
+ return (
206
+ lastMessage?.role === "assistant" && lastMessage.status.type === "running"
207
+ );
208
+ };
209
+
196
210
  export const getThreadState = (
197
211
  runtime: ThreadRuntimeCore,
198
212
  threadListItemState: ThreadListItemState,
199
213
  ): ThreadState => {
200
- const lastMessage = runtime.messages.at(-1);
201
214
  return Object.freeze({
202
215
  threadId: threadListItemState.id,
203
216
  metadata: threadListItemState,
204
217
  capabilities: runtime.capabilities,
205
218
  isDisabled: runtime.isDisabled,
206
219
  isLoading: runtime.isLoading,
207
- isRunning:
208
- runtime.isRunning ??
209
- (lastMessage?.role !== "assistant"
210
- ? false
211
- : lastMessage.status.type === "running"),
220
+ isRunning: getThreadRuntimeCoreIsRunning(runtime),
212
221
  messages: runtime.messages,
213
222
  state: runtime.state,
214
223
  suggestions: runtime.suggestions,