@assistant-ui/core 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +9 -7
- package/dist/internal.js +8 -6
- package/dist/react/primitive-hooks/useActionBarCopy.js +51 -24
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useSuggestionTrigger.js +29 -25
- package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +2 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts +6 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.d.ts.map +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.js +1 -0
- package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +67 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +76 -15
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +4 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +21 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +26 -2
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +29 -8
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/types/MessagePartComponentTypes.d.ts +8 -0
- package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
- package/dist/runtime/api/bindings.d.ts +5 -0
- package/dist/runtime/api/bindings.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +5 -1
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +14 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +6 -1
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +7 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +11 -3
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +3 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +54 -12
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +10 -0
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/internal.d.ts +4 -4
- package/dist/runtime/internal.js +4 -4
- package/dist/runtimes/external-store/external-store-adapter.d.ts +9 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +4 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +6 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -0
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
- package/dist/store/clients/chain-of-thought-client.js +29 -23
- package/dist/store/clients/chain-of-thought-client.js.map +1 -1
- package/dist/store/clients/thread-message-client.d.ts.map +1 -1
- package/dist/store/clients/thread-message-client.js +101 -89
- package/dist/store/clients/thread-message-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +66 -45
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +91 -75
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/scopes/thread-list-item.d.ts +7 -0
- package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
- package/dist/store/scopes/threads.d.ts +1 -0
- package/dist/store/scopes/threads.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/message.d.ts +22 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/data-url.d.ts +14 -1
- package/dist/utils/data-url.d.ts.map +1 -1
- package/dist/utils/data-url.js +23 -1
- package/dist/utils/data-url.js.map +1 -1
- package/dist/utils/getGroupStatus.d.ts +9 -0
- package/dist/utils/getGroupStatus.d.ts.map +1 -0
- package/dist/utils/getGroupStatus.js +15 -0
- package/dist/utils/getGroupStatus.js.map +1 -0
- package/dist/utils/image-media-type.d.ts +13 -0
- package/dist/utils/image-media-type.d.ts.map +1 -0
- package/dist/utils/image-media-type.js +141 -0
- package/dist/utils/image-media-type.js.map +1 -0
- package/dist/utils/normalizePartStatus.d.ts +8 -0
- package/dist/utils/normalizePartStatus.d.ts.map +1 -0
- package/dist/utils/normalizePartStatus.js +39 -0
- package/dist/utils/normalizePartStatus.js.map +1 -0
- package/dist/utils/wire-media.d.ts +35 -0
- package/dist/utils/wire-media.d.ts.map +1 -0
- package/dist/utils/wire-media.js +55 -0
- package/dist/utils/wire-media.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/internal.ts +13 -1
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +109 -14
- package/src/react/primitive-hooks/useActionBarCopy.ts +27 -2
- package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +148 -0
- package/src/react/primitive-hooks/useSuggestionTrigger.ts +10 -4
- package/src/react/primitives/message/MessageGroupedParts.tsx +4 -4
- package/src/react/primitives/message/MessageParts.tsx +6 -1
- package/src/react/providers/ReadonlyThreadProvider.tsx +1 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +157 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +160 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +135 -32
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +37 -0
- package/src/react/runtimes/cloud/auiV0.ts +59 -14
- package/src/react/types/MessagePartComponentTypes.ts +8 -0
- package/src/runtime/api/bindings.ts +5 -0
- package/src/runtime/api/message-runtime.test.ts +140 -0
- package/src/runtime/api/message-runtime.ts +10 -5
- package/src/runtime/api/thread-list-runtime.ts +22 -1
- package/src/runtime/api/thread-runtime.ts +15 -6
- package/src/runtime/base/base-composer-runtime-core.ts +72 -19
- package/src/runtime/interfaces/thread-list-runtime-core.ts +11 -0
- package/src/runtime/interfaces/thread-runtime-core.ts +15 -0
- package/src/runtimes/external-store/external-store-adapter.ts +9 -0
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +9 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +8 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -0
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -0
- package/src/store/clients/chain-of-thought-client.ts +2 -7
- package/src/store/clients/thread-message-client.test.ts +84 -0
- package/src/store/clients/thread-message-client.ts +14 -4
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -2
- package/src/store/runtime-clients/thread-list-runtime-client.ts +13 -1
- package/src/store/scopes/thread-list-item.ts +7 -0
- package/src/store/scopes/threads.ts +1 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-reloadMainThread.test.ts +257 -0
- package/src/tests/auiV0Encode.test.ts +218 -1
- package/src/tests/base-composer-runtime-core-addAttachment.test.ts +296 -0
- package/src/tests/event-subscription-listener-errors.test.ts +1 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +39 -1
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +30 -0
- package/src/utils/data-url.ts +24 -0
- package/src/utils/getGroupStatus.test.ts +36 -0
- package/src/utils/getGroupStatus.ts +31 -0
- package/src/utils/image-media-type.test.ts +96 -0
- package/src/utils/image-media-type.ts +118 -0
- package/src/utils/normalizePartStatus.test.ts +59 -0
- package/src/utils/normalizePartStatus.ts +55 -0
- package/src/utils/wire-media.test.ts +96 -0
- package/src/utils/wire-media.ts +70 -0
|
@@ -31,7 +31,10 @@ export type ThreadListState = {
|
|
|
31
31
|
readonly isLoadingMore: boolean;
|
|
32
32
|
readonly hasMore: boolean;
|
|
33
33
|
readonly threadItems: Readonly<
|
|
34
|
-
Record<
|
|
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,
|
|
@@ -34,6 +34,11 @@ import { notifyEventListeners } from "../../utils/notify-event-listeners";
|
|
|
34
34
|
const isAttachmentComplete = (a: Attachment): a is CompleteAttachment =>
|
|
35
35
|
a.status.type === "complete";
|
|
36
36
|
|
|
37
|
+
type AttachmentAddOperation = {
|
|
38
|
+
cancelled: boolean;
|
|
39
|
+
attachmentIds: Set<string>;
|
|
40
|
+
};
|
|
41
|
+
|
|
37
42
|
export abstract class BaseComposerRuntimeCore
|
|
38
43
|
extends BaseSubscribable
|
|
39
44
|
implements ComposerRuntimeCore
|
|
@@ -138,6 +143,22 @@ export abstract class BaseComposerRuntimeCore
|
|
|
138
143
|
protected _isSending = false;
|
|
139
144
|
private _removedDuringSend = new Set<string>();
|
|
140
145
|
private _sendGeneration = 0;
|
|
146
|
+
private _attachmentAddOperations = new Set<AttachmentAddOperation>();
|
|
147
|
+
|
|
148
|
+
private _cancelAttachmentAdd(attachmentId: string) {
|
|
149
|
+
for (const operation of [...this._attachmentAddOperations]) {
|
|
150
|
+
if (!operation.attachmentIds.has(attachmentId)) continue;
|
|
151
|
+
operation.cancelled = true;
|
|
152
|
+
this._attachmentAddOperations.delete(operation);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private _cancelAllAttachmentAdds() {
|
|
157
|
+
for (const operation of this._attachmentAddOperations) {
|
|
158
|
+
operation.cancelled = true;
|
|
159
|
+
}
|
|
160
|
+
this._attachmentAddOperations.clear();
|
|
161
|
+
}
|
|
141
162
|
|
|
142
163
|
private _emptyTextAndAttachments() {
|
|
143
164
|
this._attachments = [];
|
|
@@ -154,6 +175,8 @@ export abstract class BaseComposerRuntimeCore
|
|
|
154
175
|
}
|
|
155
176
|
|
|
156
177
|
public async reset() {
|
|
178
|
+
this._cancelAllAttachmentAdds();
|
|
179
|
+
|
|
157
180
|
// A send whose adapter never settles must not brick the composer; reset is
|
|
158
181
|
// the escape hatch that releases the in-flight lock. Bumping the generation
|
|
159
182
|
// invalidates that send entirely so a late-settling upload can neither
|
|
@@ -182,6 +205,7 @@ export abstract class BaseComposerRuntimeCore
|
|
|
182
205
|
}
|
|
183
206
|
|
|
184
207
|
public async clearAttachments() {
|
|
208
|
+
this._cancelAllAttachmentAdds();
|
|
185
209
|
const task = this._onClearAttachments();
|
|
186
210
|
this.setAttachments([]);
|
|
187
211
|
|
|
@@ -324,23 +348,6 @@ export abstract class BaseComposerRuntimeCore
|
|
|
324
348
|
return;
|
|
325
349
|
}
|
|
326
350
|
|
|
327
|
-
const upsertAttachment = (a: PendingAttachment) => {
|
|
328
|
-
const idx = this._attachments.findIndex(
|
|
329
|
-
(attachment) => attachment.id === a.id,
|
|
330
|
-
);
|
|
331
|
-
if (idx !== -1)
|
|
332
|
-
this._attachments = [
|
|
333
|
-
...this._attachments.slice(0, idx),
|
|
334
|
-
a,
|
|
335
|
-
...this._attachments.slice(idx + 1),
|
|
336
|
-
];
|
|
337
|
-
else {
|
|
338
|
-
this._attachments = [...this._attachments, a];
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
this._notifySubscribers();
|
|
342
|
-
};
|
|
343
|
-
|
|
344
351
|
const adapter = this.getAttachmentAdapter();
|
|
345
352
|
if (!adapter) {
|
|
346
353
|
const message = "Attachments are not supported";
|
|
@@ -361,19 +368,46 @@ export abstract class BaseComposerRuntimeCore
|
|
|
361
368
|
throw err;
|
|
362
369
|
}
|
|
363
370
|
|
|
371
|
+
const operation: AttachmentAddOperation = {
|
|
372
|
+
cancelled: false,
|
|
373
|
+
attachmentIds: new Set(),
|
|
374
|
+
};
|
|
375
|
+
const operations = this._attachmentAddOperations;
|
|
376
|
+
operations.add(operation);
|
|
377
|
+
const upsertAttachment = (a: PendingAttachment) => {
|
|
378
|
+
if (operation.cancelled) return false;
|
|
379
|
+
|
|
380
|
+
operation.attachmentIds.add(a.id);
|
|
381
|
+
const idx = this._attachments.findIndex(
|
|
382
|
+
(attachment) => attachment.id === a.id,
|
|
383
|
+
);
|
|
384
|
+
if (idx !== -1)
|
|
385
|
+
this._attachments = [
|
|
386
|
+
...this._attachments.slice(0, idx),
|
|
387
|
+
a,
|
|
388
|
+
...this._attachments.slice(idx + 1),
|
|
389
|
+
];
|
|
390
|
+
else {
|
|
391
|
+
this._attachments = [...this._attachments, a];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this._notifySubscribers();
|
|
395
|
+
return true;
|
|
396
|
+
};
|
|
364
397
|
let lastAttachment: PendingAttachment | undefined;
|
|
365
398
|
try {
|
|
366
399
|
const promiseOrGenerator = adapter.add({ file: fileOrAttachment });
|
|
367
400
|
if (Symbol.asyncIterator in promiseOrGenerator) {
|
|
368
401
|
for await (const r of promiseOrGenerator) {
|
|
369
402
|
lastAttachment = r;
|
|
370
|
-
upsertAttachment(r);
|
|
403
|
+
if (!upsertAttachment(r)) break;
|
|
371
404
|
}
|
|
372
405
|
} else {
|
|
373
406
|
lastAttachment = await promiseOrGenerator;
|
|
374
407
|
upsertAttachment(lastAttachment);
|
|
375
408
|
}
|
|
376
409
|
} catch (e) {
|
|
410
|
+
if (operation.cancelled) return;
|
|
377
411
|
if (lastAttachment) {
|
|
378
412
|
upsertAttachment({
|
|
379
413
|
...lastAttachment,
|
|
@@ -391,8 +425,11 @@ export abstract class BaseComposerRuntimeCore
|
|
|
391
425
|
e instanceof Error ? e : undefined,
|
|
392
426
|
);
|
|
393
427
|
throw e;
|
|
428
|
+
} finally {
|
|
429
|
+
operations.delete(operation);
|
|
394
430
|
}
|
|
395
431
|
|
|
432
|
+
if (operation.cancelled) return;
|
|
396
433
|
if (
|
|
397
434
|
lastAttachment?.status.type === "incomplete" &&
|
|
398
435
|
lastAttachment.status.reason === "error"
|
|
@@ -434,6 +471,8 @@ export abstract class BaseComposerRuntimeCore
|
|
|
434
471
|
if (index === -1) throw new Error("Attachment not found");
|
|
435
472
|
const attachment = this._attachments[index]!;
|
|
436
473
|
|
|
474
|
+
this._cancelAttachmentAdd(attachmentId);
|
|
475
|
+
|
|
437
476
|
// A send in flight may already be uploading this attachment; the upload
|
|
438
477
|
// can't be cancelled, so mark it to be dropped from the outgoing message
|
|
439
478
|
// before any await gives the upload a chance to settle first.
|
|
@@ -442,7 +481,21 @@ export abstract class BaseComposerRuntimeCore
|
|
|
442
481
|
if (!isAttachmentComplete(attachment)) {
|
|
443
482
|
const adapter = this.getAttachmentAdapter();
|
|
444
483
|
if (!adapter) throw new Error("Attachments are not supported");
|
|
445
|
-
|
|
484
|
+
try {
|
|
485
|
+
await adapter.remove(attachment);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
+
this._attachments = this._attachments.map((candidate) =>
|
|
489
|
+
candidate.id === attachmentId && !isAttachmentComplete(candidate)
|
|
490
|
+
? {
|
|
491
|
+
...candidate,
|
|
492
|
+
status: { type: "incomplete", reason: "error", message },
|
|
493
|
+
}
|
|
494
|
+
: candidate,
|
|
495
|
+
);
|
|
496
|
+
this._notifySubscribers();
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
446
499
|
}
|
|
447
500
|
this._attachments = this._attachments.filter((a) => a.id !== attachmentId);
|
|
448
501
|
this._notifySubscribers();
|
|
@@ -31,6 +31,16 @@ export type ThreadListRuntimeCore = {
|
|
|
31
31
|
getMainThreadRuntimeCore(): ThreadRuntimeCore;
|
|
32
32
|
getThreadRuntimeCore(threadId: string): ThreadRuntimeCore;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether the thread currently has a run in progress, including a run on a
|
|
36
|
+
* thread that is not the main one. Implemented by thread lists that keep
|
|
37
|
+
* runtimes alive for non-main threads, and they notify their subscribers
|
|
38
|
+
* whenever the answer changes. A thread list that mounts only the main thread
|
|
39
|
+
* leaves this undefined: its other threads have no runtime and so cannot be
|
|
40
|
+
* running, and the main thread's run state is read from its runtime directly.
|
|
41
|
+
*/
|
|
42
|
+
unstable_isThreadRunning?(threadId: string): boolean;
|
|
43
|
+
|
|
34
44
|
getItemById(threadId: string): ThreadListItemCoreState | undefined;
|
|
35
45
|
|
|
36
46
|
switchToThread(
|
|
@@ -41,6 +51,7 @@ export type ThreadListRuntimeCore = {
|
|
|
41
51
|
|
|
42
52
|
getLoadThreadsPromise(): Promise<void>;
|
|
43
53
|
reload?(): Promise<void>;
|
|
54
|
+
reloadMainThread?(): Promise<void>;
|
|
44
55
|
loadMore?(): Promise<void>;
|
|
45
56
|
|
|
46
57
|
detach(threadId: string): Promise<void>;
|
|
@@ -23,6 +23,8 @@ export type RuntimeCapabilities = {
|
|
|
23
23
|
readonly switchBranchDuringRun: boolean;
|
|
24
24
|
readonly edit: boolean;
|
|
25
25
|
readonly reload: boolean;
|
|
26
|
+
/** Whether the runtime can refetch this thread's remote state in place. */
|
|
27
|
+
readonly refetchThread: boolean;
|
|
26
28
|
readonly delete: boolean;
|
|
27
29
|
readonly cancel: boolean;
|
|
28
30
|
readonly unstable_copy: boolean;
|
|
@@ -216,6 +218,19 @@ export type ThreadRuntimeCore = Readonly<{
|
|
|
216
218
|
|
|
217
219
|
reset(initialMessages?: readonly ThreadMessageLike[]): void;
|
|
218
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Re-fetches this thread's state from its backing store, in place: no
|
|
223
|
+
* runtime-hook remount, so runtime identity and composer drafts survive.
|
|
224
|
+
* Presence signals the capability to `threads.reloadMainThread()`, which
|
|
225
|
+
* calls this and propagates its rejection. It does not touch a run in
|
|
226
|
+
* progress first, because stopping one is `cancelRun`, whose contract is
|
|
227
|
+
* that the user abandoned a send: it returns the trailing user message to
|
|
228
|
+
* the composer. An implementation is therefore responsible for whatever
|
|
229
|
+
* coordination a concurrent run needs. Runtimes without remote state leave
|
|
230
|
+
* it undefined.
|
|
231
|
+
*/
|
|
232
|
+
unstable_refetchThread?: (() => Promise<void>) | undefined;
|
|
233
|
+
|
|
219
234
|
/**
|
|
220
235
|
* @deprecated This API is still under active development and might change without notice.
|
|
221
236
|
* For state-derivable transitions, prefer `subscribe` + `getState`. This channel is the
|
|
@@ -143,6 +143,15 @@ type ExternalStoreAdapterBase<T> = {
|
|
|
143
143
|
| undefined;
|
|
144
144
|
onResume?: ((config: ResumeRunConfig) => Promise<void>) | undefined;
|
|
145
145
|
onCancel?: (() => Promise<void>) | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* Re-fetches the thread's state from the backing store, in place; a
|
|
148
|
+
* rejection reaches the `threads.reloadMainThread()` caller. Unrelated to
|
|
149
|
+
* `onReload`, which re-generates an assistant message.
|
|
150
|
+
*
|
|
151
|
+
* The caller does not stop a run in progress first, so an adapter that can
|
|
152
|
+
* stream owns whatever coordination one needs.
|
|
153
|
+
*/
|
|
154
|
+
onRefetchThread?: (() => Promise<void>) | undefined;
|
|
146
155
|
onAddToolResult?:
|
|
147
156
|
| ((options: AddToolResultOptions) => Promise<void> | void)
|
|
148
157
|
| undefined;
|
|
@@ -169,6 +169,15 @@ export class ExternalStoreThreadListRuntimeCore implements ThreadListRuntimeCore
|
|
|
169
169
|
this._notifySubscribers();
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
public async reloadMainThread(): Promise<void> {
|
|
173
|
+
// There is no runtime hook to remount here, so the capability is the only
|
|
174
|
+
// path and an adapter without it has nothing to refetch with.
|
|
175
|
+
if (!this._mainThread.unstable_refetchThread) return;
|
|
176
|
+
// No unsent-thread guard: every entry here is regular or archived, so the
|
|
177
|
+
// "new" status the remote thread list has to exclude cannot occur.
|
|
178
|
+
await this._mainThread.unstable_refetchThread();
|
|
179
|
+
}
|
|
180
|
+
|
|
172
181
|
public async switchToThread(
|
|
173
182
|
threadId: string,
|
|
174
183
|
_options?: { unarchive?: boolean },
|
|
@@ -65,6 +65,7 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
65
65
|
edit: false,
|
|
66
66
|
delete: false,
|
|
67
67
|
reload: false,
|
|
68
|
+
refetchThread: false,
|
|
68
69
|
cancel: false,
|
|
69
70
|
unstable_copy: false,
|
|
70
71
|
speech: false,
|
|
@@ -102,6 +103,12 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
102
103
|
return this._store.adapters;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
// A getter, not a method, so its presence tracks the adapter.
|
|
107
|
+
public get unstable_refetchThread(): (() => Promise<void>) | undefined {
|
|
108
|
+
if (!this._store.onRefetchThread) return undefined;
|
|
109
|
+
return () => this._store.onRefetchThread!();
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
public suggestions: readonly ThreadSuggestion[] = [];
|
|
106
113
|
public extras: unknown = undefined;
|
|
107
114
|
|
|
@@ -156,6 +163,7 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
156
163
|
this._store.onDelete !== undefined ||
|
|
157
164
|
this._store.setMessages !== undefined,
|
|
158
165
|
reload: this._store.onReload !== undefined,
|
|
166
|
+
refetchThread: this._store.onRefetchThread !== undefined,
|
|
159
167
|
cancel: this._store.onCancel !== undefined,
|
|
160
168
|
speech: this._store.adapters?.speech !== undefined,
|
|
161
169
|
dictation: this._store.adapters?.dictation !== undefined,
|
|
@@ -5,12 +5,8 @@ import type {
|
|
|
5
5
|
ChainOfThoughtState,
|
|
6
6
|
ChainOfThoughtPart,
|
|
7
7
|
} from "../scopes/chain-of-thought";
|
|
8
|
-
import type { MessagePartStatus } from "../../types/message";
|
|
9
8
|
import type { PartMethods } from "../scopes/part";
|
|
10
|
-
|
|
11
|
-
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
|
|
12
|
-
type: "complete",
|
|
13
|
-
});
|
|
9
|
+
import { getGroupStatus } from "../../utils/getGroupStatus";
|
|
14
10
|
|
|
15
11
|
const useChainOfThoughtClient = ({
|
|
16
12
|
parts,
|
|
@@ -22,8 +18,7 @@ const useChainOfThoughtClient = ({
|
|
|
22
18
|
const [collapsed, setCollapsed] = useState(true);
|
|
23
19
|
|
|
24
20
|
const status = useMemo(() => {
|
|
25
|
-
|
|
26
|
-
return lastPart?.status ?? COMPLETE_STATUS;
|
|
21
|
+
return getGroupStatus(parts);
|
|
27
22
|
}, [parts]);
|
|
28
23
|
|
|
29
24
|
const state = useMemo<ChainOfThoughtState>(
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createTapRoot, useResource } from "@assistant-ui/tap";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import type { ThreadAssistantMessage } from "../../types/message";
|
|
4
|
+
import { ThreadMessageClient } from "./thread-message-client";
|
|
5
|
+
|
|
6
|
+
describe("ThreadMessageClient", () => {
|
|
7
|
+
const getPartStatus = (
|
|
8
|
+
part: ThreadAssistantMessage["content"][number],
|
|
9
|
+
status: ThreadAssistantMessage["status"],
|
|
10
|
+
) => {
|
|
11
|
+
const message: ThreadAssistantMessage = {
|
|
12
|
+
id: "message-1",
|
|
13
|
+
role: "assistant",
|
|
14
|
+
createdAt: new Date(0),
|
|
15
|
+
content: [part],
|
|
16
|
+
status,
|
|
17
|
+
metadata: {
|
|
18
|
+
unstable_state: null,
|
|
19
|
+
unstable_annotations: [],
|
|
20
|
+
unstable_data: [],
|
|
21
|
+
steps: [],
|
|
22
|
+
custom: {},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const root = createTapRoot(function ThreadMessageRoot() {
|
|
26
|
+
return useResource(ThreadMessageClient({ message, index: 0 }));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return root.getValue().getState().parts[0]?.status;
|
|
31
|
+
} finally {
|
|
32
|
+
root.unmount();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
it("preserves a running part status on a running detached message", () => {
|
|
37
|
+
const part = {
|
|
38
|
+
type: "text",
|
|
39
|
+
text: "done",
|
|
40
|
+
status: { type: "running" },
|
|
41
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
42
|
+
|
|
43
|
+
expect(getPartStatus(part, { type: "running" })).toEqual({
|
|
44
|
+
type: "running",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("normalizes an unknown incomplete reason on a running detached message", () => {
|
|
49
|
+
const part = {
|
|
50
|
+
type: "text",
|
|
51
|
+
text: "done",
|
|
52
|
+
status: { type: "incomplete", reason: "unknown" },
|
|
53
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
54
|
+
|
|
55
|
+
expect(getPartStatus(part, { type: "running" })).toEqual({
|
|
56
|
+
type: "incomplete",
|
|
57
|
+
reason: "other",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("normalizes an upstream complete reason on a running detached message", () => {
|
|
62
|
+
const part = {
|
|
63
|
+
type: "text",
|
|
64
|
+
text: "done",
|
|
65
|
+
status: { type: "complete", reason: "unknown" },
|
|
66
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
67
|
+
|
|
68
|
+
expect(getPartStatus(part, { type: "running" })).toEqual({
|
|
69
|
+
type: "complete",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("marks parts complete on a non-running detached message", () => {
|
|
74
|
+
const part = {
|
|
75
|
+
type: "text",
|
|
76
|
+
text: "done",
|
|
77
|
+
status: { type: "running" },
|
|
78
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
79
|
+
|
|
80
|
+
expect(getPartStatus(part, { type: "complete", reason: "stop" })).toEqual({
|
|
81
|
+
type: "complete",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -11,19 +11,27 @@ import { useClientLookup } from "@assistant-ui/store";
|
|
|
11
11
|
import type { MessageState } from "../scopes/message";
|
|
12
12
|
import type { PartState } from "../scopes/part";
|
|
13
13
|
import { NoOpComposerClient } from "./no-op-composer-client";
|
|
14
|
+
import {
|
|
15
|
+
COMPLETE_STATUS,
|
|
16
|
+
normalizePartStatus,
|
|
17
|
+
} from "../../utils/normalizePartStatus";
|
|
14
18
|
import { getThreadMessageText } from "../../utils/text";
|
|
15
19
|
|
|
16
20
|
const useThreadMessagePartClient = ({
|
|
17
21
|
part,
|
|
22
|
+
isMessageRunning,
|
|
18
23
|
}: {
|
|
19
24
|
part: ThreadAssistantMessagePart | ThreadUserMessagePart;
|
|
25
|
+
isMessageRunning: boolean;
|
|
20
26
|
}): ClientOutput<"part"> => {
|
|
21
27
|
const state = useMemo<PartState>(() => {
|
|
22
28
|
return {
|
|
23
29
|
...part,
|
|
24
|
-
status:
|
|
30
|
+
status: isMessageRunning
|
|
31
|
+
? (normalizePartStatus(part) ?? COMPLETE_STATUS)
|
|
32
|
+
: COMPLETE_STATUS,
|
|
25
33
|
};
|
|
26
|
-
}, [part]);
|
|
34
|
+
}, [part, isMessageRunning]);
|
|
27
35
|
|
|
28
36
|
return {
|
|
29
37
|
getState: () => state,
|
|
@@ -74,6 +82,8 @@ const useThreadMessageClient = ({
|
|
|
74
82
|
}: ThreadMessageClientProps): ClientOutput<"message"> => {
|
|
75
83
|
const [isCopiedState, setIsCopied] = useState(false);
|
|
76
84
|
const [isHoveringState, setIsHovering] = useState(false);
|
|
85
|
+
const isMessageRunning =
|
|
86
|
+
message.role === "assistant" && message.status.type === "running";
|
|
77
87
|
|
|
78
88
|
const parts = useClientLookup(
|
|
79
89
|
message.content.map((part, idx) =>
|
|
@@ -81,8 +91,8 @@ const useThreadMessageClient = ({
|
|
|
81
91
|
"toolCallId" in part && part.toolCallId != null
|
|
82
92
|
? `toolCallId-${part.toolCallId}`
|
|
83
93
|
: `index-${idx}`,
|
|
84
|
-
ThreadMessagePartClient({ part }),
|
|
85
|
-
[part],
|
|
94
|
+
ThreadMessagePartClient({ part, isMessageRunning }),
|
|
95
|
+
[part, isMessageRunning],
|
|
86
96
|
),
|
|
87
97
|
),
|
|
88
98
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Unsubscribe } from "../../types/unsubscribe";
|
|
2
|
-
import { useEffect } from "react";
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
3
|
import { resource } from "@assistant-ui/tap";
|
|
4
4
|
import { type ClientOutput, useAssistantEmit } from "@assistant-ui/store";
|
|
5
5
|
import type {
|
|
@@ -10,10 +10,21 @@ import { useSubscribable } from "./useSubscribable";
|
|
|
10
10
|
|
|
11
11
|
const useThreadListItemClient = ({
|
|
12
12
|
runtime,
|
|
13
|
+
mainThreadIsRunning = false,
|
|
13
14
|
}: {
|
|
14
15
|
runtime: ThreadListItemRuntime;
|
|
16
|
+
// A thread list that cannot report per-thread run state still leaves the open
|
|
17
|
+
// thread observable, and the thread client tracks that reactively. Omitted
|
|
18
|
+
// where the runtime state is already authoritative for every thread.
|
|
19
|
+
mainThreadIsRunning?: boolean | undefined;
|
|
15
20
|
}): ClientOutput<"threadListItem"> => {
|
|
16
|
-
const
|
|
21
|
+
const runtimeState = useSubscribable(runtime);
|
|
22
|
+
const state = useMemo(() => {
|
|
23
|
+
const isRunning =
|
|
24
|
+
runtimeState.isRunning || (runtimeState.isMain && mainThreadIsRunning);
|
|
25
|
+
if (isRunning === runtimeState.isRunning) return runtimeState;
|
|
26
|
+
return { ...runtimeState, isRunning };
|
|
27
|
+
}, [runtimeState, mainThreadIsRunning]);
|
|
17
28
|
const emit = useAssistantEmit();
|
|
18
29
|
|
|
19
30
|
// Bind thread list item events to event manager
|
|
@@ -15,9 +15,11 @@ import type { ThreadsState } from "../scopes/threads";
|
|
|
15
15
|
const useThreadListItemClientById = ({
|
|
16
16
|
runtime,
|
|
17
17
|
id,
|
|
18
|
+
mainThreadIsRunning,
|
|
18
19
|
}: {
|
|
19
20
|
runtime: ThreadListRuntime;
|
|
20
21
|
id: string;
|
|
22
|
+
mainThreadIsRunning: boolean;
|
|
21
23
|
}) => {
|
|
22
24
|
const threadListItemRuntime = useMemo(
|
|
23
25
|
() => runtime.getItemById(id),
|
|
@@ -26,6 +28,7 @@ const useThreadListItemClientById = ({
|
|
|
26
28
|
return useResource(
|
|
27
29
|
ThreadListItemClient({
|
|
28
30
|
runtime: threadListItemRuntime,
|
|
31
|
+
mainThreadIsRunning,
|
|
29
32
|
}),
|
|
30
33
|
);
|
|
31
34
|
};
|
|
@@ -48,7 +51,15 @@ const useThreadListClient = ({
|
|
|
48
51
|
);
|
|
49
52
|
const threadItems = useClientLookup(
|
|
50
53
|
Object.keys(runtimeState.threadItems).map((id) =>
|
|
51
|
-
withKey(
|
|
54
|
+
withKey(
|
|
55
|
+
id,
|
|
56
|
+
ThreadListItemClientById({
|
|
57
|
+
runtime,
|
|
58
|
+
id,
|
|
59
|
+
mainThreadIsRunning: main.state.isRunning,
|
|
60
|
+
}),
|
|
61
|
+
[runtime, id, main.state.isRunning],
|
|
62
|
+
),
|
|
52
63
|
),
|
|
53
64
|
);
|
|
54
65
|
|
|
@@ -93,6 +104,7 @@ const useThreadListClient = ({
|
|
|
93
104
|
},
|
|
94
105
|
getLoadThreadsPromise: () => runtime.getLoadThreadsPromise(),
|
|
95
106
|
reload: () => runtime.reload(),
|
|
107
|
+
reloadMainThread: () => runtime.reloadMainThread(),
|
|
96
108
|
loadMore: () => runtime.loadMore(),
|
|
97
109
|
__internal_getAssistantRuntime: () => __internal_assistantRuntime,
|
|
98
110
|
};
|
|
@@ -9,6 +9,13 @@ export type ThreadListItemState = {
|
|
|
9
9
|
readonly lastMessageAt?: Date | undefined;
|
|
10
10
|
readonly status: ThreadListItemStatus;
|
|
11
11
|
readonly custom?: Record<string, unknown> | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Whether this thread has a run in progress, including a run that continues
|
|
14
|
+
* after the user switches to another thread. A thread list that mounts only
|
|
15
|
+
* the open thread has no runtime to run the others, so they read as not
|
|
16
|
+
* running rather than as unknown.
|
|
17
|
+
*/
|
|
18
|
+
readonly isRunning: boolean;
|
|
12
19
|
};
|
|
13
20
|
|
|
14
21
|
export type ThreadListItemMethods = {
|
|
@@ -30,6 +30,7 @@ export type ThreadsMethods = {
|
|
|
30
30
|
thread(selector: "main"): ThreadMethods;
|
|
31
31
|
getLoadThreadsPromise(): Promise<void>;
|
|
32
32
|
reload(): Promise<void>;
|
|
33
|
+
reloadMainThread(): Promise<void>;
|
|
33
34
|
loadMore(): Promise<void>;
|
|
34
35
|
__internal_getAssistantRuntime?(): AssistantRuntime;
|
|
35
36
|
};
|