@assistant-ui/core 0.3.14 → 0.3.15

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 (137) hide show
  1. package/dist/adapters/voice.js +11 -0
  2. package/dist/adapters/voice.js.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/internal.d.ts +2 -1
  5. package/dist/internal.js +2 -1
  6. package/dist/model-context/frame/host.d.ts +1 -1
  7. package/dist/model-context/frame/host.d.ts.map +1 -1
  8. package/dist/model-context/frame/host.js +17 -3
  9. package/dist/model-context/frame/host.js.map +1 -1
  10. package/dist/model-context/frame/provider.d.ts +4 -0
  11. package/dist/model-context/frame/provider.d.ts.map +1 -1
  12. package/dist/model-context/frame/provider.js +50 -3
  13. package/dist/model-context/frame/provider.js.map +1 -1
  14. package/dist/model-context/frame/types.d.ts +4 -1
  15. package/dist/model-context/frame/types.d.ts.map +1 -1
  16. package/dist/model-context/frame/types.js.map +1 -1
  17. package/dist/react/client/RemoteThreadList.d.ts +1 -1
  18. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  19. package/dist/react/client/RemoteThreadList.js +95 -23
  20. package/dist/react/client/RemoteThreadList.js.map +1 -1
  21. package/dist/react/primitive-hooks/useActionBarCopy.js +7 -1
  22. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  23. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +3 -0
  24. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -1
  25. package/dist/react/primitive-hooks/useThreadIsEmpty.js +3 -0
  26. package/dist/react/primitive-hooks/useThreadIsEmpty.js.map +1 -1
  27. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +3 -0
  28. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -1
  29. package/dist/react/primitive-hooks/useThreadIsRunning.js +3 -0
  30. package/dist/react/primitive-hooks/useThreadIsRunning.js.map +1 -1
  31. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -0
  32. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  33. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +6 -0
  34. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  35. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +6 -0
  36. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  37. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +192 -50
  38. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  39. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  40. package/dist/react/runtimes/RemoteThreadResource.js +2 -0
  41. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  42. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  43. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js +1 -1
  44. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  45. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  46. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +38 -5
  47. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  48. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  49. package/dist/runtime/utils/message-repository.js +27 -18
  50. package/dist/runtime/utils/message-repository.js.map +1 -1
  51. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  52. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +2 -0
  53. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  54. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +102 -0
  55. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -0
  56. package/dist/runtimes/inert/InertThreadRuntimeCore.js +156 -0
  57. package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -0
  58. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  59. package/dist/runtimes/local/local-thread-runtime-core.js +25 -6
  60. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  61. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +12 -60
  62. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  63. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +10 -145
  64. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  65. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts +9 -0
  66. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts.map +1 -0
  67. package/dist/runtimes/remote-thread-list/adapter-changed.js +13 -0
  68. package/dist/runtimes/remote-thread-list/adapter-changed.js.map +1 -0
  69. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  70. package/dist/runtimes/remote-thread-list/empty-thread-core.js +16 -155
  71. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  72. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +2 -0
  73. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  74. package/dist/runtimes/remote-thread-list/optimistic-state.js +11 -0
  75. package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
  76. package/dist/runtimes/remote-thread-list/types.d.ts +3 -0
  77. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  78. package/dist/store/clients/external-thread.js +1 -1
  79. package/dist/store/clients/external-thread.js.map +1 -1
  80. package/dist/store/runtime-clients/handle-runtime-action.d.ts.map +1 -1
  81. package/dist/store/runtime-clients/handle-runtime-action.js +2 -0
  82. package/dist/store/runtime-clients/handle-runtime-action.js.map +1 -1
  83. package/dist/types/message.d.ts +1 -1
  84. package/dist/types/message.js.map +1 -1
  85. package/dist/utils/silent-runtime-action.d.ts +6 -0
  86. package/dist/utils/silent-runtime-action.d.ts.map +1 -0
  87. package/dist/utils/silent-runtime-action.js +7 -0
  88. package/dist/utils/silent-runtime-action.js.map +1 -0
  89. package/package.json +4 -4
  90. package/src/adapters/voice.test.ts +66 -1
  91. package/src/adapters/voice.ts +8 -0
  92. package/src/internal.ts +1 -0
  93. package/src/model-context/frame/host.test.ts +64 -5
  94. package/src/model-context/frame/host.ts +16 -3
  95. package/src/model-context/frame/provider.test.ts +347 -20
  96. package/src/model-context/frame/provider.ts +58 -2
  97. package/src/model-context/frame/types.ts +5 -0
  98. package/src/react/client/RemoteThreadList.test.ts +196 -0
  99. package/src/react/client/RemoteThreadList.ts +93 -24
  100. package/src/react/primitive-hooks/useActionBarCopy.test.ts +15 -0
  101. package/src/react/primitive-hooks/useActionBarCopy.ts +13 -3
  102. package/src/react/primitive-hooks/useThreadIsEmpty.ts +3 -0
  103. package/src/react/primitive-hooks/useThreadIsRunning.ts +3 -0
  104. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +12 -0
  105. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.republish.test.ts +87 -0
  106. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +268 -63
  107. package/src/react/runtimes/RemoteThreadResource.ts +2 -0
  108. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.test.tsx +58 -0
  109. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.ts +3 -1
  110. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +98 -3
  111. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +68 -6
  112. package/src/react/runtimes/useLocalRuntime.test.tsx +5 -5
  113. package/src/runtime/utils/message-repository.ts +26 -18
  114. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +4 -0
  115. package/src/runtimes/inert/InertThreadRuntimeCore.test.ts +253 -0
  116. package/src/runtimes/inert/InertThreadRuntimeCore.ts +232 -0
  117. package/src/runtimes/local/local-thread-runtime-core.test.ts +319 -0
  118. package/src/runtimes/local/local-thread-runtime-core.ts +38 -8
  119. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +17 -209
  120. package/src/runtimes/remote-thread-list/adapter-changed.ts +10 -0
  121. package/src/runtimes/remote-thread-list/empty-thread-core.ts +28 -221
  122. package/src/runtimes/remote-thread-list/optimistic-state.test.ts +18 -0
  123. package/src/runtimes/remote-thread-list/optimistic-state.ts +13 -0
  124. package/src/runtimes/remote-thread-list/types.ts +4 -0
  125. package/src/store/clients/external-thread.ts +1 -1
  126. package/src/store/runtime-clients/handle-runtime-action.test.ts +40 -0
  127. package/src/store/runtime-clients/handle-runtime-action.ts +3 -0
  128. package/src/tests/MessageRepository.test.ts +48 -0
  129. package/src/tests/RemoteThreadListThreadListRuntimeCore-errors.test.ts +2 -2
  130. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +8 -6
  131. package/src/tests/empty-thread-core.test.ts +6 -0
  132. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +20 -0
  133. package/src/tests/external-thread-isEmpty.test.tsx +100 -0
  134. package/src/tests/remote-thread-list-adapter-switch.test.ts +383 -0
  135. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +67 -0
  136. package/src/types/message.ts +1 -1
  137. package/src/utils/silent-runtime-action.ts +6 -0
@@ -1,16 +1,18 @@
1
- import type { Unsubscribe } from "../../types/unsubscribe";
2
1
  import type { ThreadMessage } from "../../types/message";
3
- import type { ThreadRuntimeCore } from "../../runtime/interfaces/thread-runtime-core";
4
- import { BaseSubscribable } from "../../subscribable/subscribable";
2
+ import {
3
+ InertThreadRuntimeCore,
4
+ createInertComposer,
5
+ } from "../inert/InertThreadRuntimeCore";
5
6
 
6
7
  const READONLY_THREAD_ERROR = new Error(
7
8
  "This is a readonly thread. You cannot perform mutations on readonly threads.",
8
9
  );
9
10
 
10
- export class ReadonlyThreadRuntimeCore
11
- extends BaseSubscribable
12
- implements ThreadRuntimeCore
13
- {
11
+ export class ReadonlyThreadRuntimeCore extends InertThreadRuntimeCore {
12
+ protected get error() {
13
+ return READONLY_THREAD_ERROR;
14
+ }
15
+
14
16
  private _messages: readonly ThreadMessage[] = [];
15
17
 
16
18
  get messages() {
@@ -39,202 +41,6 @@ export class ReadonlyThreadRuntimeCore
39
41
  return [messageId];
40
42
  }
41
43
 
42
- switchToBranch(): void {
43
- throw READONLY_THREAD_ERROR;
44
- }
45
-
46
- append(): void {
47
- throw READONLY_THREAD_ERROR;
48
- }
49
-
50
- deleteMessage(): void {
51
- throw READONLY_THREAD_ERROR;
52
- }
53
-
54
- startRun(): void {
55
- throw READONLY_THREAD_ERROR;
56
- }
57
-
58
- resumeRun(): void {
59
- throw READONLY_THREAD_ERROR;
60
- }
61
-
62
- cancelRun(): void {}
63
-
64
- addToolResult(): void {
65
- throw READONLY_THREAD_ERROR;
66
- }
67
-
68
- resumeToolCall(): void {
69
- throw READONLY_THREAD_ERROR;
70
- }
71
-
72
- respondToToolApproval(): void {
73
- throw READONLY_THREAD_ERROR;
74
- }
75
-
76
- speak(): void {
77
- throw READONLY_THREAD_ERROR;
78
- }
79
-
80
- stopSpeaking(): void {}
81
-
82
- connectVoice(): void {
83
- throw READONLY_THREAD_ERROR;
84
- }
85
-
86
- disconnectVoice(): void {}
87
-
88
- getVoiceVolume = () => 0;
89
- subscribeVoiceVolume = (): Unsubscribe => () => {};
90
-
91
- muteVoice(): void {
92
- throw READONLY_THREAD_ERROR;
93
- }
94
-
95
- unmuteVoice(): void {
96
- throw READONLY_THREAD_ERROR;
97
- }
98
-
99
- submitFeedback(): void {
100
- throw READONLY_THREAD_ERROR;
101
- }
102
-
103
- getModelContext() {
104
- return {};
105
- }
106
-
107
- exportExternalState() {
108
- throw READONLY_THREAD_ERROR;
109
- }
110
-
111
- importExternalState(): void {
112
- throw READONLY_THREAD_ERROR;
113
- }
114
-
115
- unstable_notifySessionReset(): void {
116
- throw READONLY_THREAD_ERROR;
117
- }
118
-
119
- composer = {
120
- attachments: [] as never[],
121
- attachmentAccept: "*",
122
-
123
- async addAttachment() {
124
- throw READONLY_THREAD_ERROR;
125
- },
126
-
127
- async removeAttachment() {
128
- throw READONLY_THREAD_ERROR;
129
- },
130
-
131
- isEditing: false as const,
132
- canCancel: false,
133
- canSend: false,
134
- isEmpty: true,
135
- text: "",
136
-
137
- setText() {
138
- throw READONLY_THREAD_ERROR;
139
- },
140
-
141
- role: "user" as const,
142
-
143
- setRole() {
144
- throw READONLY_THREAD_ERROR;
145
- },
146
-
147
- runConfig: {},
148
-
149
- setRunConfig() {
150
- throw READONLY_THREAD_ERROR;
151
- },
152
-
153
- async reset() {
154
- // noop
155
- },
156
-
157
- async clearAttachments() {
158
- // noop
159
- },
160
-
161
- send() {
162
- throw READONLY_THREAD_ERROR;
163
- },
164
-
165
- cancel() {
166
- // noop
167
- },
168
-
169
- queue: [] as never[],
170
- moveQueueItem() {},
171
- removeQueueItem() {},
172
-
173
- dictation: undefined,
174
-
175
- startDictation() {
176
- throw READONLY_THREAD_ERROR;
177
- },
178
-
179
- stopDictation() {
180
- // noop
181
- },
182
-
183
- quote: undefined,
184
-
185
- setQuote() {
186
- throw READONLY_THREAD_ERROR;
187
- },
188
-
189
- subscribe() {
190
- return () => {};
191
- },
192
-
193
- unstable_on() {
194
- return () => {};
195
- },
196
- };
197
-
198
- getEditComposer() {
199
- return undefined;
200
- }
201
-
202
- beginEdit(): void {
203
- throw READONLY_THREAD_ERROR;
204
- }
205
-
206
- speech = undefined;
207
- voice = undefined;
208
-
209
- capabilities = {
210
- switchToBranch: false,
211
- switchBranchDuringRun: false,
212
- edit: false,
213
- delete: false,
214
- reload: false,
215
- refetchThread: false,
216
- cancel: false,
217
- unstable_copy: false,
218
- speech: false,
219
- dictation: false,
220
- voice: false,
221
- attachments: false,
222
- feedback: false,
223
- queue: false,
224
- } as const;
225
-
226
- isDisabled = false;
227
- isSendDisabled = false;
228
- isLoading = false;
229
-
230
- state = null;
231
- suggestions = [] as never[];
232
- extras = undefined;
233
-
234
- import(): void {
235
- throw READONLY_THREAD_ERROR;
236
- }
237
-
238
44
  export() {
239
45
  return {
240
46
  messages: this._messages.map((message, idx) => ({
@@ -244,11 +50,13 @@ export class ReadonlyThreadRuntimeCore
244
50
  };
245
51
  }
246
52
 
247
- reset(): void {
248
- throw READONLY_THREAD_ERROR;
249
- }
53
+ composer = createInertComposer(READONLY_THREAD_ERROR, false);
250
54
 
251
- unstable_on(): Unsubscribe {
252
- return () => {};
253
- }
55
+ isLoading = false;
56
+
57
+ override cancelRun(): void {}
58
+
59
+ override stopSpeaking(): void {}
60
+
61
+ override disconnectVoice(): void {}
254
62
  }
@@ -0,0 +1,10 @@
1
+ import { SILENT_RUNTIME_ACTION } from "../../utils/silent-runtime-action";
2
+
3
+ export class ThreadListAdapterChangedError extends Error {
4
+ readonly [SILENT_RUNTIME_ACTION] = true;
5
+
6
+ constructor() {
7
+ super("Thread list adapter changed while an operation was pending.");
8
+ this.name = "ThreadListAdapterChangedError";
9
+ }
10
+ }
@@ -1,238 +1,45 @@
1
+ import type { Unsubscribe } from "../../types/unsubscribe";
1
2
  import type { ThreadRuntimeCore } from "../../runtime/interfaces/thread-runtime-core";
3
+ import {
4
+ InertThreadRuntimeCore,
5
+ createInertComposer,
6
+ } from "../inert/InertThreadRuntimeCore";
2
7
 
3
8
  const EMPTY_THREAD_ERROR = new Error(
4
9
  "This is the empty thread, a placeholder for the main thread. You cannot perform any actions on this thread instance. This error is probably because you tried to call a thread method in your render function. Call the method inside a `useEffect` hook instead.",
5
10
  );
6
- export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
7
- getMessageById() {
8
- return undefined;
9
- },
10
-
11
- getBranches() {
12
- return [];
13
- },
14
-
15
- switchToBranch() {
16
- throw EMPTY_THREAD_ERROR;
17
- },
18
-
19
- append() {
20
- throw EMPTY_THREAD_ERROR;
21
- },
22
-
23
- deleteMessage() {
24
- throw EMPTY_THREAD_ERROR;
25
- },
26
-
27
- startRun() {
28
- throw EMPTY_THREAD_ERROR;
29
- },
30
-
31
- resumeRun() {
32
- throw EMPTY_THREAD_ERROR;
33
- },
34
-
35
- cancelRun() {
36
- throw EMPTY_THREAD_ERROR;
37
- },
38
-
39
- addToolResult() {
40
- throw EMPTY_THREAD_ERROR;
41
- },
42
-
43
- resumeToolCall() {
44
- throw EMPTY_THREAD_ERROR;
45
- },
46
-
47
- respondToToolApproval() {
48
- throw EMPTY_THREAD_ERROR;
49
- },
50
-
51
- speak() {
52
- throw EMPTY_THREAD_ERROR;
53
- },
54
-
55
- stopSpeaking() {
56
- throw EMPTY_THREAD_ERROR;
57
- },
58
-
59
- connectVoice() {
60
- throw EMPTY_THREAD_ERROR;
61
- },
62
-
63
- disconnectVoice() {
64
- throw EMPTY_THREAD_ERROR;
65
- },
66
-
67
- getVoiceVolume: () => 0,
68
- subscribeVoiceVolume: () => () => {},
69
-
70
- muteVoice() {
71
- throw EMPTY_THREAD_ERROR;
72
- },
73
-
74
- unmuteVoice() {
75
- throw EMPTY_THREAD_ERROR;
76
- },
77
-
78
- submitFeedback() {
79
- throw EMPTY_THREAD_ERROR;
80
- },
81
-
82
- getModelContext() {
83
- return {};
84
- },
85
-
86
- exportExternalState() {
87
- throw EMPTY_THREAD_ERROR;
88
- },
89
-
90
- importExternalState() {
91
- throw EMPTY_THREAD_ERROR;
92
- },
93
-
94
- unstable_notifySessionReset() {
95
- throw EMPTY_THREAD_ERROR;
96
- },
97
-
98
- composer: {
99
- attachments: [],
100
- attachmentAccept: "*",
101
-
102
- async addAttachment() {
103
- throw EMPTY_THREAD_ERROR;
104
- },
105
-
106
- async removeAttachment() {
107
- throw EMPTY_THREAD_ERROR;
108
- },
109
-
110
- isEditing: true,
111
-
112
- canCancel: false,
113
- canSend: false,
114
- isEmpty: true,
115
11
 
116
- text: "",
12
+ class EmptyThreadRuntimeCore extends InertThreadRuntimeCore {
13
+ protected get error() {
14
+ return EMPTY_THREAD_ERROR;
15
+ }
117
16
 
118
- setText() {
119
- throw EMPTY_THREAD_ERROR;
120
- },
17
+ messages = [] as never[];
121
18
 
122
- role: "user",
19
+ composer = createInertComposer(EMPTY_THREAD_ERROR, true);
123
20
 
124
- setRole() {
125
- throw EMPTY_THREAD_ERROR;
126
- },
21
+ isLoading = true;
127
22
 
128
- runConfig: {},
129
-
130
- setRunConfig() {
131
- throw EMPTY_THREAD_ERROR;
132
- },
133
-
134
- async reset() {
135
- // noop
136
- },
137
-
138
- async clearAttachments() {
139
- // noop
140
- },
141
-
142
- send() {
143
- throw EMPTY_THREAD_ERROR;
144
- },
145
-
146
- cancel() {
147
- // noop
148
- },
149
-
150
- queue: [] as never[],
151
- moveQueueItem() {},
152
- removeQueueItem() {},
153
-
154
- dictation: undefined,
155
-
156
- startDictation() {
157
- throw EMPTY_THREAD_ERROR;
158
- },
159
-
160
- stopDictation() {
161
- // noop
162
- },
163
-
164
- quote: undefined,
165
-
166
- setQuote() {
167
- throw EMPTY_THREAD_ERROR;
168
- },
169
-
170
- subscribe() {
171
- return () => {};
172
- },
173
-
174
- unstable_on() {
175
- return () => {};
176
- },
177
- },
178
-
179
- getEditComposer() {
23
+ getMessageById() {
180
24
  return undefined;
181
- },
25
+ }
182
26
 
183
- beginEdit() {
184
- throw EMPTY_THREAD_ERROR;
185
- },
186
-
187
- speech: undefined,
188
- voice: undefined,
189
-
190
- capabilities: {
191
- switchToBranch: false,
192
- switchBranchDuringRun: false,
193
- edit: false,
194
- delete: false,
195
- reload: false,
196
- refetchThread: false,
197
- cancel: false,
198
- unstable_copy: false,
199
- speech: false,
200
- dictation: false,
201
- voice: false,
202
- attachments: false,
203
- feedback: false,
204
- queue: false,
205
- },
206
-
207
- isDisabled: false,
208
- isSendDisabled: false,
209
- isLoading: true,
210
-
211
- messages: [],
212
-
213
- state: null,
214
-
215
- suggestions: [],
216
-
217
- extras: undefined,
218
-
219
- subscribe() {
220
- return () => {};
221
- },
222
-
223
- import() {
224
- throw EMPTY_THREAD_ERROR;
225
- },
27
+ getBranches() {
28
+ return [];
29
+ }
226
30
 
227
31
  export() {
228
32
  return { messages: [] };
229
- },
33
+ }
230
34
 
231
- reset() {
232
- throw EMPTY_THREAD_ERROR;
233
- },
234
-
235
- unstable_on() {
35
+ override subscribe(): Unsubscribe {
236
36
  return () => {};
237
- },
238
- };
37
+ }
38
+
39
+ override waitForUpdate(): Promise<void> {
40
+ return Promise.reject(EMPTY_THREAD_ERROR);
41
+ }
42
+ }
43
+
44
+ export const EMPTY_THREAD_CORE: ThreadRuntimeCore =
45
+ new EmptyThreadRuntimeCore();
@@ -60,4 +60,22 @@ describe("OptimisticState", () => {
60
60
 
61
61
  expect(state.value.title).toBe("Project Beta");
62
62
  });
63
+
64
+ it("drops an in-flight transform after reset", async () => {
65
+ const state = new OptimisticState({ title: "Untitled", extra: 0 });
66
+ const request = deferred();
67
+ const update = state.optimisticUpdate({
68
+ execute: () => request.promise,
69
+ optimistic: (value) => ({ ...value, title: "Pending" }),
70
+ then: (value) => ({ ...value, extra: 1 }),
71
+ });
72
+
73
+ expect(state.value.title).toBe("Pending");
74
+ state.reset({ title: "Fresh", extra: 0 });
75
+ expect(state.value).toEqual({ title: "Fresh", extra: 0 });
76
+
77
+ request.resolve();
78
+ await update;
79
+ expect(state.value).toEqual({ title: "Fresh", extra: 0 });
80
+ });
63
81
  });
@@ -48,6 +48,7 @@ export class OptimisticState<TState> extends BaseSubscribable {
48
48
  [];
49
49
 
50
50
  private _nextTransformOrder = 0;
51
+ private _epoch = 0;
51
52
 
52
53
  private _baseValue: TState;
53
54
  private _cachedValue: TState;
@@ -95,9 +96,20 @@ export class OptimisticState<TState> extends BaseSubscribable {
95
96
  this._updateState();
96
97
  }
97
98
 
99
+ public reset(state: TState): void {
100
+ // In-flight optimisticUpdate calls must not write into the replacement state.
101
+ this._epoch++;
102
+ this._pendingTransforms.length = 0;
103
+ this._completedOptimistics.length = 0;
104
+ this._baseValue = state;
105
+ this._cachedValue = state;
106
+ this._notifySubscribers();
107
+ }
108
+
98
109
  public async optimisticUpdate<TResult>(
99
110
  transform: Transform<TState, TResult>,
100
111
  ): Promise<TResult> {
112
+ const epoch = this._epoch;
101
113
  const order = this._nextTransformOrder++;
102
114
  const task = transform.execute();
103
115
  const pendingTransform = {
@@ -110,6 +122,7 @@ export class OptimisticState<TState> extends BaseSubscribable {
110
122
  this._updateState();
111
123
 
112
124
  const result = await task;
125
+ if (epoch !== this._epoch) return result;
113
126
  this._baseValue = pipeTransforms(this._baseValue, result, [
114
127
  transform.optimistic,
115
128
  transform.then,
@@ -96,6 +96,10 @@ export type RemoteThreadListAdapter = {
96
96
 
97
97
  export type RemoteThreadListOptions = {
98
98
  runtimeHook: () => AssistantRuntime;
99
+
100
+ /**
101
+ * The adapter reference should remain stable across renders. Replacing it reloads the list and drops cached threads that are not in the replacement page. In-flight mutations from the previous adapter are cancelled.
102
+ */
99
103
  adapter: RemoteThreadListAdapter;
100
104
 
101
105
  /**
@@ -1085,7 +1085,7 @@ const useExternalThread = ({
1085
1085
  }));
1086
1086
 
1087
1087
  return {
1088
- isEmpty: messages.length === 0,
1088
+ isEmpty: messages.length === 0 && !isLoading,
1089
1089
  isDisabled: false,
1090
1090
  isLoading,
1091
1091
  isRunning,
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { ThreadListAdapterChangedError } from "../../runtimes/remote-thread-list/adapter-changed";
3
+ import { handleRuntimeAction } from "./handle-runtime-action";
4
+
5
+ describe("handleRuntimeAction", () => {
6
+ it("does not log a silent adapter-change cancellation", async () => {
7
+ const error = new ThreadListAdapterChangedError();
8
+ const consoleError = vi
9
+ .spyOn(console, "error")
10
+ .mockImplementation(() => {});
11
+
12
+ try {
13
+ await expect(
14
+ handleRuntimeAction("thread list archive", () => Promise.reject(error)),
15
+ ).rejects.toBe(error);
16
+ expect(consoleError).not.toHaveBeenCalled();
17
+ } finally {
18
+ consoleError.mockRestore();
19
+ }
20
+ });
21
+
22
+ it("logs an ordinary rejection", async () => {
23
+ const error = new Error("archive failed");
24
+ const consoleError = vi
25
+ .spyOn(console, "error")
26
+ .mockImplementation(() => {});
27
+
28
+ try {
29
+ await expect(
30
+ handleRuntimeAction("thread list archive", () => Promise.reject(error)),
31
+ ).rejects.toBe(error);
32
+ expect(consoleError).toHaveBeenCalledExactlyOnceWith(
33
+ "[assistant-ui] thread list archive failed:",
34
+ error,
35
+ );
36
+ } finally {
37
+ consoleError.mockRestore();
38
+ }
39
+ });
40
+ });
@@ -1,3 +1,5 @@
1
+ import { isSilentRuntimeAction } from "../../utils/silent-runtime-action";
2
+
1
3
  export const handleRuntimeAction = (
2
4
  label: string,
3
5
  execute: () => Promise<void>,
@@ -5,6 +7,7 @@ export const handleRuntimeAction = (
5
7
  const task = execute();
6
8
 
7
9
  void task.catch((error: unknown) => {
10
+ if (isSilentRuntimeAction(error)) return;
8
11
  console.error(`[assistant-ui] ${label} failed:`, error);
9
12
  });
10
13
  return task;