@assistant-ui/core 0.3.0 → 0.3.2
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.
- package/dist/internal.d.ts +1 -2
- package/dist/internal.js +1 -2
- package/dist/react/AssistantProvider.d.ts +1 -1
- package/dist/react/AssistantProvider.d.ts.map +1 -1
- package/dist/react/AssistantProvider.js +35 -9
- package/dist/react/AssistantProvider.js.map +1 -1
- package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
- package/dist/react/AssistantRuntimeProvider.js +1 -1
- package/dist/react/RuntimeAdapter.js +2 -2
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +17 -33
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/DataRenderers.js +1 -1
- package/dist/react/client/Interactables.js +1 -1
- package/dist/react/client/Tools.js +2 -2
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +2 -1
- package/dist/react/interactables-legacy/Interactables.js +1 -1
- package/dist/react/interactables-legacy/useAssistantInteractable.js +1 -1
- package/dist/react/interactables-legacy/useInteractableState.js +2 -2
- package/dist/react/model-context/useAssistantContext.js +1 -1
- package/dist/react/model-context/useAssistantDataUI.js +2 -2
- package/dist/react/model-context/useAssistantInstructions.js +2 -2
- package/dist/react/model-context/useAssistantTool.js +2 -2
- package/dist/react/model-context/useAssistantToolUI.js +2 -2
- package/dist/react/model-context/useAuiToolOverrides.js +1 -1
- package/dist/react/model-context/useInlineRender.js +1 -1
- package/dist/react/model-context/useInteractable.d.ts.map +1 -1
- package/dist/react/model-context/useInteractable.js +3 -5
- package/dist/react/model-context/useInteractable.js.map +1 -1
- package/dist/react/model-context/useInteractableState.js +2 -2
- package/dist/react/model-context/useInteractableVersions.js +2 -2
- package/dist/react/model-context/useToolArgsStatus.js +2 -2
- package/dist/react/primitive-hooks/useActionBarCopy.js +2 -2
- package/dist/react/primitive-hooks/useActionBarEdit.js +2 -2
- package/dist/react/primitive-hooks/useActionBarFeedback.js +2 -2
- package/dist/react/primitive-hooks/useActionBarReload.js +2 -2
- package/dist/react/primitive-hooks/useActionBarSpeak.js +2 -2
- package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +2 -2
- package/dist/react/primitive-hooks/useBranchPickerNext.js +2 -2
- package/dist/react/primitive-hooks/useBranchPickerPrevious.js +2 -2
- package/dist/react/primitive-hooks/useComposerAddAttachment.js +2 -2
- package/dist/react/primitive-hooks/useComposerCancel.js +2 -2
- package/dist/react/primitive-hooks/useComposerDictate.js +2 -2
- package/dist/react/primitive-hooks/useComposerSend.js +2 -2
- package/dist/react/primitive-hooks/useEditComposerCancel.js +2 -2
- package/dist/react/primitive-hooks/useEditComposerSend.js +2 -2
- package/dist/react/primitive-hooks/useMessageBranching.js +2 -2
- package/dist/react/primitive-hooks/useMessageReload.js +2 -2
- package/dist/react/primitive-hooks/useSuggestionTrigger.js +2 -2
- package/dist/react/primitive-hooks/useThreadListItemArchive.js +2 -2
- package/dist/react/primitive-hooks/useThreadListItemDelete.js +2 -2
- package/dist/react/primitive-hooks/useThreadListItemTrigger.js +2 -2
- package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +2 -2
- package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -2
- package/dist/react/primitive-hooks/useThreadListNew.js +2 -2
- package/dist/react/primitive-hooks/useThreadMessageIds.js +1 -1
- package/dist/react/primitive-hooks/useVoice.js +2 -2
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +2 -2
- package/dist/react/primitives/composer/ComposerAttachments.js +2 -2
- package/dist/react/primitives/composer/ComposerIf.js +1 -1
- package/dist/react/primitives/composer/ComposerQueue.js +1 -1
- package/dist/react/primitives/generativeUI/GenerativeUI.js +2 -2
- package/dist/react/primitives/message/MessageAttachments.js +2 -2
- package/dist/react/primitives/message/MessageGroupedParts.js +1 -1
- package/dist/react/primitives/message/MessageParts.js +2 -2
- package/dist/react/primitives/message/MessageQuote.js +2 -2
- package/dist/react/primitives/part/PartMessages.js +2 -2
- package/dist/react/primitives/thread/ThreadMessages.js +2 -2
- package/dist/react/primitives/thread/ThreadSuggestions.js +2 -2
- package/dist/react/primitives/threadList/ThreadListItems.js +2 -2
- package/dist/react/primitives/threadListItem/ThreadListItemTitle.js +1 -1
- package/dist/react/providers/AttachmentByIndexProvider.js +1 -1
- package/dist/react/providers/ChainOfThoughtByIndicesProvider.js +1 -1
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +1 -1
- package/dist/react/providers/MessageByIdProvider.js +1 -1
- package/dist/react/providers/MessageByIndexProvider.js +1 -1
- package/dist/react/providers/PartByIndexProvider.js +1 -1
- package/dist/react/providers/QueueItemByIndexProvider.js +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.js +2 -2
- package/dist/react/providers/SuggestionByIndexProvider.js +1 -1
- package/dist/react/providers/TextMessagePartProvider.js +2 -2
- package/dist/react/providers/ThreadListItemByIndexProvider.js +1 -1
- package/dist/react/providers/ThreadListItemRuntimeProvider.js +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +14 -13
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +22 -12
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RuntimeAdapterProvider.js +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +9 -23
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -1
- package/dist/react/runtimes/useExternalStoreRuntime.js +1 -1
- package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -1
- package/dist/react/runtimes/useLocalRuntime.js +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +7 -13
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/utils/useJSONEqual.js +1 -1
- package/dist/runtime/utils/thread-message-like.js +2 -2
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +4 -3
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +5 -4
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/chain-of-thought-client.js +1 -1
- package/dist/store/clients/model-context-client.js +1 -1
- package/dist/store/clients/no-op-composer-client.js +1 -1
- package/dist/store/clients/suggestions.js +2 -2
- package/dist/store/clients/thread-message-client.js +2 -2
- package/dist/store/runtime-clients/composer-runtime-client.js +2 -2
- package/dist/store/runtime-clients/message-runtime-client.js +2 -2
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +2 -2
- package/dist/store/runtime-clients/thread-list-runtime-client.js +2 -2
- package/dist/store/runtime-clients/thread-runtime-client.js +2 -2
- package/dist/store/scopes/thread.d.ts +1 -1
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/utils/data-url.js +2 -2
- package/dist/utils/data-url.js.map +1 -1
- package/package.json +4 -4
- package/src/internal.ts +0 -7
- package/src/react/AssistantProvider.tsx +28 -15
- package/src/react/AssistantRuntimeProvider.test.tsx +101 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +28 -7
- package/src/react/index.ts +4 -0
- package/src/react/model-context/useInteractable.ts +2 -6
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +16 -13
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +53 -10
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +7 -5
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +9 -15
- package/src/runtime/utils/thread-message-like.ts +2 -2
- package/src/runtimes/local/local-thread-runtime-core.test.ts +38 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +6 -3
- package/src/runtimes/remote-thread-list/types.ts +5 -4
- package/src/store/scopes/thread.ts +1 -1
- package/src/tests/remote-thread-list-reactive-threadId.test.ts +0 -92
- package/src/tests/thread-message-like.test.ts +15 -0
- package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +293 -0
- package/src/utils/data-url.test.ts +26 -2
- package/src/utils/data-url.ts +2 -2
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { type AssistantStream, createAssistantStream } from "assistant-stream";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type FC,
|
|
4
|
+
type PropsWithChildren,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
3
10
|
import { useAui } from "@assistant-ui/store";
|
|
4
11
|
import type {
|
|
5
12
|
RemoteThreadInitializeResponse,
|
|
@@ -276,22 +283,26 @@ export const parseStoredMessageRepository = (
|
|
|
276
283
|
|
|
277
284
|
class AsyncStorageHistoryAdapter implements ThreadHistoryAdapter {
|
|
278
285
|
private storage: AsyncStorageLike;
|
|
279
|
-
private
|
|
286
|
+
private getAui: () => ReturnType<typeof useAui>;
|
|
280
287
|
private prefix: string;
|
|
281
288
|
private mutationQueue: KeyedMutationQueue;
|
|
282
289
|
|
|
283
290
|
constructor(
|
|
284
291
|
storage: AsyncStorageLike,
|
|
285
|
-
|
|
292
|
+
getAui: () => ReturnType<typeof useAui>,
|
|
286
293
|
prefix: string,
|
|
287
294
|
mutationQueue: KeyedMutationQueue,
|
|
288
295
|
) {
|
|
289
296
|
this.storage = storage;
|
|
290
|
-
this.
|
|
297
|
+
this.getAui = getAui;
|
|
291
298
|
this.prefix = prefix;
|
|
292
299
|
this.mutationQueue = mutationQueue;
|
|
293
300
|
}
|
|
294
301
|
|
|
302
|
+
private get aui(): ReturnType<typeof useAui> {
|
|
303
|
+
return this.getAui();
|
|
304
|
+
}
|
|
305
|
+
|
|
295
306
|
private _messagesKey(remoteId: string) {
|
|
296
307
|
return `${this.prefix}messages:${remoteId}`;
|
|
297
308
|
}
|
|
@@ -334,9 +345,19 @@ const createHistoryProvider = (
|
|
|
334
345
|
): FC<PropsWithChildren> => {
|
|
335
346
|
const Provider: FC<PropsWithChildren> = ({ children }) => {
|
|
336
347
|
const aui = useAui();
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
348
|
+
// Not useEffectEvent: history adapter methods run during render (SSR load).
|
|
349
|
+
const auiRef = useRef(aui);
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
auiRef.current = aui;
|
|
352
|
+
});
|
|
353
|
+
const [history] = useState(
|
|
354
|
+
() =>
|
|
355
|
+
new AsyncStorageHistoryAdapter(
|
|
356
|
+
storage,
|
|
357
|
+
() => auiRef.current,
|
|
358
|
+
prefix,
|
|
359
|
+
mutationQueue,
|
|
360
|
+
),
|
|
340
361
|
);
|
|
341
362
|
const adapters = useMemo(() => ({ history }), [history]);
|
|
342
363
|
|
package/src/react/index.ts
CHANGED
|
@@ -226,6 +226,10 @@ export {
|
|
|
226
226
|
type StreamingTimingOptions,
|
|
227
227
|
type StreamingTimingState,
|
|
228
228
|
} from "./runtimes/useStreamingTiming";
|
|
229
|
+
export {
|
|
230
|
+
createRuntimeExtras,
|
|
231
|
+
type RuntimeExtras,
|
|
232
|
+
} from "./runtimes/createRuntimeExtras";
|
|
229
233
|
export { RemoteThreadListHookInstanceManager } from "./runtimes/RemoteThreadListHookInstanceManager";
|
|
230
234
|
export { RemoteThreadListThreadListRuntimeCore } from "./runtimes/RemoteThreadListThreadListRuntimeCore";
|
|
231
235
|
export { useRemoteThreadListRuntime } from "./runtimes/useRemoteThreadListRuntime";
|
|
@@ -90,14 +90,10 @@ const useInteractable = <TSchema extends Unstable_InteractableStateSchema>(
|
|
|
90
90
|
|
|
91
91
|
const autoId = useId().replace(/[^a-zA-Z0-9]/g, "");
|
|
92
92
|
|
|
93
|
-
// Whether this component renders inside a message part is fixed for its
|
|
94
|
-
// lifetime, so conditioning the selectors on it is safe.
|
|
95
|
-
const inPart = aui.part.source != null;
|
|
96
93
|
const updateToolName = interactableToolName(name);
|
|
97
94
|
const part = useAuiState((s) => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return p.type === "tool-call" ? p : undefined;
|
|
95
|
+
const p = s.optional.part;
|
|
96
|
+
return p?.type === "tool-call" ? p : undefined;
|
|
101
97
|
});
|
|
102
98
|
|
|
103
99
|
// Inside an update_{name} part, the instance id comes from the call itself:
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useCallback,
|
|
5
5
|
useRef,
|
|
6
6
|
useEffect,
|
|
7
|
+
useEffectEvent,
|
|
7
8
|
useLayoutEffect,
|
|
8
9
|
memo,
|
|
9
10
|
type PropsWithChildren,
|
|
@@ -142,23 +143,25 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
142
143
|
}
|
|
143
144
|
}, [threadBinding]);
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
hasInitializedRef.current
|
|
147
|
-
return runtime.threads.main.unstable_on("initialize", () => {
|
|
148
|
-
if (hasInitializedRef.current) return;
|
|
146
|
+
const handleInitialize = useEffectEvent(() => {
|
|
147
|
+
if (hasInitializedRef.current) return;
|
|
149
148
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
const state = aui.threadListItem.getState();
|
|
150
|
+
if (state.status !== "new") return;
|
|
151
|
+
hasInitializedRef.current = true;
|
|
153
152
|
|
|
154
|
-
|
|
153
|
+
initPromiseRef.current = aui.threadListItem.initialize();
|
|
155
154
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
});
|
|
155
|
+
const dispose = runtime.thread.unstable_on("runEnd", () => {
|
|
156
|
+
dispose();
|
|
157
|
+
aui.threadListItem.generateTitle();
|
|
160
158
|
});
|
|
161
|
-
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
hasInitializedRef.current = false;
|
|
163
|
+
return runtime.threads.main.unstable_on("initialize", handleInitialize);
|
|
164
|
+
}, [runtime]);
|
|
162
165
|
|
|
163
166
|
return <>{children}</>;
|
|
164
167
|
};
|
|
@@ -200,6 +200,10 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
200
200
|
|
|
201
201
|
const adapterChanged =
|
|
202
202
|
this._options !== undefined && this._options.adapter !== options.adapter;
|
|
203
|
+
const controlledThreadIdChanged =
|
|
204
|
+
this._initialThreadLoaded &&
|
|
205
|
+
this._options !== undefined &&
|
|
206
|
+
this._options.threadId !== options.threadId;
|
|
203
207
|
|
|
204
208
|
this._options = options;
|
|
205
209
|
|
|
@@ -219,15 +223,25 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
219
223
|
cursor: undefined,
|
|
220
224
|
});
|
|
221
225
|
}
|
|
226
|
+
|
|
227
|
+
if (controlledThreadIdChanged) {
|
|
228
|
+
this._switchToThreadFromProp(options.threadId).catch(() => {});
|
|
229
|
+
}
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
public __internal_load() {
|
|
225
233
|
this.getLoadThreadsPromise(); // begin loading on initial bind
|
|
234
|
+
if (this._initialThreadLoaded) return;
|
|
235
|
+
this._initialThreadLoaded = true;
|
|
236
|
+
|
|
226
237
|
const startThreadId =
|
|
227
238
|
this._options.threadId ?? this._options.initialThreadId;
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
if (startThreadId !== undefined) {
|
|
240
|
+
const switchTask =
|
|
241
|
+
this._options.threadId !== undefined
|
|
242
|
+
? this._switchToThreadFromProp(startThreadId)
|
|
243
|
+
: this.switchToThread(startThreadId);
|
|
244
|
+
switchTask.catch(() => {});
|
|
231
245
|
}
|
|
232
246
|
}
|
|
233
247
|
|
|
@@ -279,11 +293,13 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
279
293
|
|
|
280
294
|
private _lastNotifiedThreadId: string | undefined = undefined;
|
|
281
295
|
|
|
282
|
-
private _notifyThreadIdChange() {
|
|
296
|
+
private _notifyThreadIdChange(emit = true) {
|
|
283
297
|
const threadId = this._mainThreadRemoteId;
|
|
284
298
|
if (this._lastNotifiedThreadId === threadId) return;
|
|
285
299
|
this._lastNotifiedThreadId = threadId;
|
|
286
|
-
|
|
300
|
+
if (emit) {
|
|
301
|
+
this._options.onThreadIdChange?.(threadId);
|
|
302
|
+
}
|
|
287
303
|
}
|
|
288
304
|
|
|
289
305
|
public getMainThreadRuntimeCore() {
|
|
@@ -312,9 +328,22 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
312
328
|
public switchToThread(
|
|
313
329
|
threadIdOrRemoteId: string,
|
|
314
330
|
options?: { unarchive?: boolean },
|
|
331
|
+
): Promise<void> {
|
|
332
|
+
return this._startSwitchToThread(threadIdOrRemoteId, options, true);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private _startSwitchToThread(
|
|
336
|
+
threadIdOrRemoteId: string,
|
|
337
|
+
options: { unarchive?: boolean } | undefined,
|
|
338
|
+
emitThreadIdChange: boolean,
|
|
315
339
|
): Promise<void> {
|
|
316
340
|
const generation = ++this._switchGeneration;
|
|
317
|
-
const task = this._switchToThread(
|
|
341
|
+
const task = this._switchToThread(
|
|
342
|
+
threadIdOrRemoteId,
|
|
343
|
+
options,
|
|
344
|
+
generation,
|
|
345
|
+
emitThreadIdChange,
|
|
346
|
+
);
|
|
318
347
|
this._switchTask = task;
|
|
319
348
|
return task;
|
|
320
349
|
}
|
|
@@ -323,6 +352,7 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
323
352
|
threadIdOrRemoteId: string,
|
|
324
353
|
options: { unarchive?: boolean } | undefined,
|
|
325
354
|
generation: number,
|
|
355
|
+
emitThreadIdChange: boolean,
|
|
326
356
|
): Promise<void> {
|
|
327
357
|
let data = this.getItemById(threadIdOrRemoteId);
|
|
328
358
|
|
|
@@ -418,17 +448,30 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
418
448
|
this._mainThreadId = data.id;
|
|
419
449
|
|
|
420
450
|
this._notifySubscribers();
|
|
421
|
-
this._notifyThreadIdChange();
|
|
451
|
+
this._notifyThreadIdChange(emitThreadIdChange);
|
|
422
452
|
}
|
|
423
453
|
|
|
424
454
|
public switchToNewThread(): Promise<void> {
|
|
455
|
+
return this._startSwitchToNewThread(true);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private _switchToThreadFromProp(threadId: string | undefined): Promise<void> {
|
|
459
|
+
return threadId !== undefined
|
|
460
|
+
? this._startSwitchToThread(threadId, undefined, false)
|
|
461
|
+
: this._startSwitchToNewThread(false);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private _startSwitchToNewThread(emitThreadIdChange: boolean): Promise<void> {
|
|
425
465
|
const generation = ++this._switchGeneration;
|
|
426
|
-
const task = this._switchToNewThread(generation);
|
|
466
|
+
const task = this._switchToNewThread(generation, emitThreadIdChange);
|
|
427
467
|
this._switchTask = task;
|
|
428
468
|
return task;
|
|
429
469
|
}
|
|
430
470
|
|
|
431
|
-
private async _switchToNewThread(
|
|
471
|
+
private async _switchToNewThread(
|
|
472
|
+
generation: number,
|
|
473
|
+
emitThreadIdChange: boolean,
|
|
474
|
+
): Promise<void> {
|
|
432
475
|
// an initialization transaction is in progress, wait for it to settle
|
|
433
476
|
while (
|
|
434
477
|
this._state.baseValue.newThreadId !== undefined &&
|
|
@@ -467,7 +510,7 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
467
510
|
});
|
|
468
511
|
}
|
|
469
512
|
|
|
470
|
-
return this._switchToThread(id, undefined, generation);
|
|
513
|
+
return this._switchToThread(id, undefined, generation, emitThreadIdChange);
|
|
471
514
|
}
|
|
472
515
|
|
|
473
516
|
public initialize = async (threadId: string) => {
|
|
@@ -22,18 +22,18 @@ const globalPersistence = new WeakMap<
|
|
|
22
22
|
|
|
23
23
|
class AssistantCloudThreadHistoryAdapter implements ThreadHistoryAdapter {
|
|
24
24
|
private cloudRef: RefObject<AssistantCloud>;
|
|
25
|
-
private
|
|
25
|
+
private getAui: () => AssistantClient;
|
|
26
26
|
|
|
27
27
|
constructor(
|
|
28
28
|
cloudRef: RefObject<AssistantCloud>,
|
|
29
|
-
|
|
29
|
+
getAui: () => AssistantClient,
|
|
30
30
|
) {
|
|
31
31
|
this.cloudRef = cloudRef;
|
|
32
|
-
this.
|
|
32
|
+
this.getAui = getAui;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
private get aui(): AssistantClient {
|
|
36
|
-
return this.
|
|
36
|
+
return this.getAui();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
private get _persistence(): CloudMessagePersistence {
|
|
@@ -816,12 +816,14 @@ export function useAssistantCloudThreadHistoryAdapter(
|
|
|
816
816
|
cloudRef: RefObject<AssistantCloud>,
|
|
817
817
|
): ThreadHistoryAdapter {
|
|
818
818
|
const aui = useAui();
|
|
819
|
+
// Not useEffectEvent: history adapter methods run during render (SSR load).
|
|
819
820
|
const auiRef = useRef(aui);
|
|
820
821
|
useEffect(() => {
|
|
821
822
|
auiRef.current = aui;
|
|
822
823
|
});
|
|
823
824
|
const [adapter] = useState(
|
|
824
|
-
() =>
|
|
825
|
+
() =>
|
|
826
|
+
new AssistantCloudThreadHistoryAdapter(cloudRef, () => auiRef.current),
|
|
825
827
|
);
|
|
826
828
|
return adapter;
|
|
827
829
|
}
|
|
@@ -51,8 +51,7 @@ export const useRemoteThreadListRuntime = (
|
|
|
51
51
|
const runtimeHookRef = useRef(options.runtimeHook);
|
|
52
52
|
runtimeHookRef.current = options.runtimeHook;
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
const startThreadIdRef = useRef(options.threadId ?? options.initialThreadId);
|
|
54
|
+
const initialThreadIdRef = useRef(options.initialThreadId);
|
|
56
55
|
|
|
57
56
|
const stableRuntimeHook = useCallback(() => {
|
|
58
57
|
return runtimeHookRef.current();
|
|
@@ -66,11 +65,17 @@ export const useRemoteThreadListRuntime = (
|
|
|
66
65
|
() => ({
|
|
67
66
|
adapter: options.adapter,
|
|
68
67
|
allowNesting: options.allowNesting,
|
|
69
|
-
|
|
68
|
+
threadId: options.threadId,
|
|
69
|
+
initialThreadId: initialThreadIdRef.current,
|
|
70
70
|
runtimeHook: stableRuntimeHook,
|
|
71
71
|
onThreadIdChange,
|
|
72
72
|
}),
|
|
73
|
-
[
|
|
73
|
+
[
|
|
74
|
+
options.adapter,
|
|
75
|
+
options.allowNesting,
|
|
76
|
+
options.threadId,
|
|
77
|
+
stableRuntimeHook,
|
|
78
|
+
],
|
|
74
79
|
);
|
|
75
80
|
|
|
76
81
|
const aui = useAui();
|
|
@@ -91,16 +96,5 @@ export const useRemoteThreadListRuntime = (
|
|
|
91
96
|
|
|
92
97
|
const runtime = useRemoteThreadListRuntimeImpl(stableOptions);
|
|
93
98
|
|
|
94
|
-
const prevThreadIdRef = useRef(options.threadId);
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
if (options.threadId === prevThreadIdRef.current) return;
|
|
97
|
-
prevThreadIdRef.current = options.threadId;
|
|
98
|
-
if (options.threadId) {
|
|
99
|
-
runtime.threads.switchToThread(options.threadId).catch(() => {});
|
|
100
|
-
} else {
|
|
101
|
-
runtime.threads.switchToNewThread().catch(() => {});
|
|
102
|
-
}
|
|
103
|
-
}, [runtime, options.threadId]);
|
|
104
|
-
|
|
105
99
|
return runtime;
|
|
106
100
|
};
|
|
@@ -131,12 +131,12 @@ export const fromThreadMessageLike = (
|
|
|
131
131
|
}: ImageMessagePart): ImageMessagePart | null => {
|
|
132
132
|
if (typeof image !== "string") return null;
|
|
133
133
|
const dataUri = image.match(
|
|
134
|
-
/^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.*)
|
|
134
|
+
/^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.*)$/i,
|
|
135
135
|
);
|
|
136
136
|
if (dataUri) {
|
|
137
137
|
return { ...rest, image };
|
|
138
138
|
}
|
|
139
|
-
if (/^(https:\/\/|blob:)
|
|
139
|
+
if (/^(https:\/\/|blob:)/i.test(image)) {
|
|
140
140
|
return { ...rest, image };
|
|
141
141
|
}
|
|
142
142
|
console.warn(`Invalid image data format detected`);
|
|
@@ -497,6 +497,44 @@ describe("LocalThreadRuntimeCore tool approvals", () => {
|
|
|
497
497
|
});
|
|
498
498
|
|
|
499
499
|
describe("LocalThreadRuntimeCore cancellation", () => {
|
|
500
|
+
it("keeps a replacement run cancellable after the previous run settles", async () => {
|
|
501
|
+
let releaseFirst!: () => void;
|
|
502
|
+
let releaseSecond!: () => void;
|
|
503
|
+
const firstGate = new Promise<void>((resolve) => {
|
|
504
|
+
releaseFirst = resolve;
|
|
505
|
+
});
|
|
506
|
+
const secondGate = new Promise<void>((resolve) => {
|
|
507
|
+
releaseSecond = resolve;
|
|
508
|
+
});
|
|
509
|
+
const signals: AbortSignal[] = [];
|
|
510
|
+
const thread = createThread({
|
|
511
|
+
async *run({ abortSignal }) {
|
|
512
|
+
signals.push(abortSignal);
|
|
513
|
+
await (signals.length === 1 ? firstGate : secondGate);
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const firstAppend = thread.append(userMessage("first"));
|
|
518
|
+
await flush();
|
|
519
|
+
const secondAppend = thread.append(userMessage("second"));
|
|
520
|
+
await flush();
|
|
521
|
+
|
|
522
|
+
expect(signals).toHaveLength(2);
|
|
523
|
+
expect(signals[0]?.aborted).toBe(true);
|
|
524
|
+
expect(signals[1]?.aborted).toBe(false);
|
|
525
|
+
|
|
526
|
+
releaseFirst();
|
|
527
|
+
await firstAppend;
|
|
528
|
+
|
|
529
|
+
thread.cancelRun();
|
|
530
|
+
const replacementWasAborted = signals[1]?.aborted;
|
|
531
|
+
|
|
532
|
+
releaseSecond();
|
|
533
|
+
await secondAppend;
|
|
534
|
+
|
|
535
|
+
expect(replacementWasAborted).toBe(true);
|
|
536
|
+
});
|
|
537
|
+
|
|
500
538
|
it("marks the message cancelled when a streaming adapter returns after abort", async () => {
|
|
501
539
|
let released!: () => void;
|
|
502
540
|
const streaming = new Promise<void>((resolve) => {
|
|
@@ -459,7 +459,8 @@ export class LocalThreadRuntimeCore
|
|
|
459
459
|
|
|
460
460
|
// abort existing run
|
|
461
461
|
this.abortController?.abort();
|
|
462
|
-
|
|
462
|
+
const abortController = new AbortController();
|
|
463
|
+
this.abortController = abortController;
|
|
463
464
|
|
|
464
465
|
const initialContent = message.content;
|
|
465
466
|
const initialAnnotations = message.metadata?.unstable_annotations;
|
|
@@ -550,7 +551,7 @@ export class LocalThreadRuntimeCore
|
|
|
550
551
|
runCallback ??
|
|
551
552
|
this.adapters.chatModel.run.bind(this.adapters.chatModel);
|
|
552
553
|
|
|
553
|
-
const abortSignal =
|
|
554
|
+
const abortSignal = abortController.signal;
|
|
554
555
|
const threadId = this._getThreadId?.();
|
|
555
556
|
const promiseOrGenerator = runCallback({
|
|
556
557
|
messages,
|
|
@@ -609,7 +610,9 @@ export class LocalThreadRuntimeCore
|
|
|
609
610
|
throw e;
|
|
610
611
|
}
|
|
611
612
|
} finally {
|
|
612
|
-
this.abortController
|
|
613
|
+
if (this.abortController === abortController) {
|
|
614
|
+
this.abortController = null;
|
|
615
|
+
}
|
|
613
616
|
|
|
614
617
|
const history = this._options.adapters.history;
|
|
615
618
|
const item = {
|
|
@@ -78,10 +78,11 @@ export type RemoteThreadListOptions = {
|
|
|
78
78
|
threadId?: string | undefined;
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* Called whenever the active thread's canonical (remote)
|
|
82
|
-
* value can be treated as a managed/controlled variable (e.g.
|
|
83
|
-
* URL query param).
|
|
84
|
-
*
|
|
81
|
+
* Called whenever the runtime changes the active thread's canonical (remote)
|
|
82
|
+
* ID, so the value can be treated as a managed/controlled variable (e.g.
|
|
83
|
+
* synced to a URL query param). Changes initiated by the controlled
|
|
84
|
+
* `threadId` option are not echoed back. Together these options form the
|
|
85
|
+
* controlled pattern: `threadId` in, `onThreadIdChange` out.
|
|
85
86
|
*
|
|
86
87
|
* Only the settled remote ID is emitted: while a freshly created thread is
|
|
87
88
|
* still optimistic (no remote ID yet) the value is `undefined`, and the real
|
|
@@ -108,7 +108,7 @@ export type ThreadMethods = {
|
|
|
108
108
|
* @param initialMessages - Optional array of initial messages to populate the thread
|
|
109
109
|
*/
|
|
110
110
|
reset(initialMessages?: readonly ThreadMessageLike[]): void;
|
|
111
|
-
importExternalState
|
|
111
|
+
importExternalState(state: unknown): void;
|
|
112
112
|
message(selector: { id: string } | { index: number }): MessageMethods;
|
|
113
113
|
/** @deprecated This API is still under active development and might change without notice. */
|
|
114
114
|
stopSpeaking(): void;
|
|
@@ -2,98 +2,6 @@ import { describe, it, expect, vi } from "vitest";
|
|
|
2
2
|
import type { RemoteThreadListAdapter } from "../runtimes/remote-thread-list/types";
|
|
3
3
|
import { createCore, makeAdapter } from "./remote-thread-list-test-helpers";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Tests for the reactive threadId useEffect logic in useRemoteThreadListRuntime.
|
|
7
|
-
*
|
|
8
|
-
* The useEffect compares options.threadId against a prevThreadIdRef and calls
|
|
9
|
-
* switchToThread or switchToNewThread when it changes. We test this logic
|
|
10
|
-
* by simulating the ref + comparison pattern.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// Mirrors the useEffect in useRemoteThreadListRuntimeImpl.
|
|
14
|
-
// Keep in sync if that implementation changes.
|
|
15
|
-
function simulateThreadIdEffect(
|
|
16
|
-
prevRef: { current: string | undefined },
|
|
17
|
-
threadId: string | undefined,
|
|
18
|
-
switchToThread: (id: string) => void,
|
|
19
|
-
switchToNewThread: () => void,
|
|
20
|
-
) {
|
|
21
|
-
if (threadId === prevRef.current) return;
|
|
22
|
-
prevRef.current = threadId;
|
|
23
|
-
if (threadId) {
|
|
24
|
-
switchToThread(threadId);
|
|
25
|
-
} else {
|
|
26
|
-
switchToNewThread();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("threadId reactive effect", () => {
|
|
31
|
-
it("does nothing when threadId stays the same", () => {
|
|
32
|
-
const ref = { current: "thread-1" };
|
|
33
|
-
const switchToThread = vi.fn();
|
|
34
|
-
const switchToNewThread = vi.fn();
|
|
35
|
-
|
|
36
|
-
simulateThreadIdEffect(ref, "thread-1", switchToThread, switchToNewThread);
|
|
37
|
-
|
|
38
|
-
expect(switchToThread).not.toHaveBeenCalled();
|
|
39
|
-
expect(switchToNewThread).not.toHaveBeenCalled();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("calls switchToThread when threadId changes", () => {
|
|
43
|
-
const ref = { current: "thread-1" };
|
|
44
|
-
const switchToThread = vi.fn();
|
|
45
|
-
const switchToNewThread = vi.fn();
|
|
46
|
-
|
|
47
|
-
simulateThreadIdEffect(ref, "thread-2", switchToThread, switchToNewThread);
|
|
48
|
-
|
|
49
|
-
expect(switchToThread).toHaveBeenCalledWith("thread-2");
|
|
50
|
-
expect(ref.current).toBe("thread-2");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("calls switchToNewThread when threadId becomes undefined", () => {
|
|
54
|
-
const ref = { current: "thread-1" };
|
|
55
|
-
const switchToThread = vi.fn();
|
|
56
|
-
const switchToNewThread = vi.fn();
|
|
57
|
-
|
|
58
|
-
simulateThreadIdEffect(ref, undefined, switchToThread, switchToNewThread);
|
|
59
|
-
|
|
60
|
-
expect(switchToNewThread).toHaveBeenCalledOnce();
|
|
61
|
-
expect(ref.current).toBeUndefined();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("skips switchToNewThread on first render when threadId is already undefined", () => {
|
|
65
|
-
const ref = { current: undefined as string | undefined };
|
|
66
|
-
const switchToThread = vi.fn();
|
|
67
|
-
const switchToNewThread = vi.fn();
|
|
68
|
-
|
|
69
|
-
// First render: undefined === undefined → skipped
|
|
70
|
-
simulateThreadIdEffect(ref, undefined, switchToThread, switchToNewThread);
|
|
71
|
-
expect(switchToNewThread).not.toHaveBeenCalled();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("handles full navigation cycle", () => {
|
|
75
|
-
const ref = { current: undefined as string | undefined };
|
|
76
|
-
const switchToThread = vi.fn();
|
|
77
|
-
const switchToNewThread = vi.fn();
|
|
78
|
-
|
|
79
|
-
// Mount with thread-1
|
|
80
|
-
simulateThreadIdEffect(ref, "thread-1", switchToThread, switchToNewThread);
|
|
81
|
-
expect(switchToThread).toHaveBeenCalledWith("thread-1");
|
|
82
|
-
|
|
83
|
-
// Navigate to thread-2
|
|
84
|
-
simulateThreadIdEffect(ref, "thread-2", switchToThread, switchToNewThread);
|
|
85
|
-
expect(switchToThread).toHaveBeenCalledWith("thread-2");
|
|
86
|
-
|
|
87
|
-
// Navigate to new thread
|
|
88
|
-
simulateThreadIdEffect(ref, undefined, switchToThread, switchToNewThread);
|
|
89
|
-
expect(switchToNewThread).toHaveBeenCalledOnce();
|
|
90
|
-
|
|
91
|
-
// Navigate back to thread-1
|
|
92
|
-
simulateThreadIdEffect(ref, "thread-1", switchToThread, switchToNewThread);
|
|
93
|
-
expect(switchToThread).toHaveBeenCalledTimes(3);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
5
|
/**
|
|
98
6
|
* Tests for onThreadIdChange, driving the real
|
|
99
7
|
* RemoteThreadListThreadListRuntimeCore via a mock adapter (no React). These
|
|
@@ -159,6 +159,21 @@ describe("fromThreadMessageLike", () => {
|
|
|
159
159
|
|
|
160
160
|
expect(result.content).toEqual([]);
|
|
161
161
|
});
|
|
162
|
+
|
|
163
|
+
it("keeps an image part with an uppercase-scheme data URL", () => {
|
|
164
|
+
const result = fromThreadMessageLike(
|
|
165
|
+
{
|
|
166
|
+
role: "assistant",
|
|
167
|
+
content: [{ type: "image", image: "DATA:IMAGE/PNG;base64,AAAA" }],
|
|
168
|
+
},
|
|
169
|
+
fallbackId,
|
|
170
|
+
fallbackStatus,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(result.content).toEqual([
|
|
174
|
+
{ type: "image", image: "DATA:IMAGE/PNG;base64,AAAA" },
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
162
177
|
});
|
|
163
178
|
|
|
164
179
|
describe("providerMetadata passthrough", () => {
|