@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.
- package/dist/adapters/voice.js +11 -0
- package/dist/adapters/voice.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +2 -1
- package/dist/model-context/frame/host.d.ts +1 -1
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +17 -3
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts +4 -0
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +50 -3
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/frame/types.d.ts +4 -1
- package/dist/model-context/frame/types.d.ts.map +1 -1
- package/dist/model-context/frame/types.js.map +1 -1
- package/dist/react/client/RemoteThreadList.d.ts +1 -1
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +95 -23
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js +7 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +3 -0
- package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsEmpty.js +3 -0
- package/dist/react/primitive-hooks/useThreadIsEmpty.js.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +3 -0
- package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useThreadIsRunning.js +3 -0
- package/dist/react/primitive-hooks/useThreadIsRunning.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +6 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +6 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +192 -50
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.js +2 -0
- package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
- package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js +1 -1
- package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +38 -5
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +27 -18
- package/dist/runtime/utils/message-repository.js.map +1 -1
- 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 +2 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +102 -0
- package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -0
- package/dist/runtimes/inert/InertThreadRuntimeCore.js +156 -0
- package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +25 -6
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +12 -60
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +10 -145
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/adapter-changed.d.ts +9 -0
- package/dist/runtimes/remote-thread-list/adapter-changed.d.ts.map +1 -0
- package/dist/runtimes/remote-thread-list/adapter-changed.js +13 -0
- package/dist/runtimes/remote-thread-list/adapter-changed.js.map +1 -0
- package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +16 -155
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +2 -0
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.js +11 -0
- package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +3 -0
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +1 -1
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/runtime-clients/handle-runtime-action.d.ts.map +1 -1
- package/dist/store/runtime-clients/handle-runtime-action.js +2 -0
- package/dist/store/runtime-clients/handle-runtime-action.js.map +1 -1
- package/dist/types/message.d.ts +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/silent-runtime-action.d.ts +6 -0
- package/dist/utils/silent-runtime-action.d.ts.map +1 -0
- package/dist/utils/silent-runtime-action.js +7 -0
- package/dist/utils/silent-runtime-action.js.map +1 -0
- package/package.json +4 -4
- package/src/adapters/voice.test.ts +66 -1
- package/src/adapters/voice.ts +8 -0
- package/src/internal.ts +1 -0
- package/src/model-context/frame/host.test.ts +64 -5
- package/src/model-context/frame/host.ts +16 -3
- package/src/model-context/frame/provider.test.ts +347 -20
- package/src/model-context/frame/provider.ts +58 -2
- package/src/model-context/frame/types.ts +5 -0
- package/src/react/client/RemoteThreadList.test.ts +196 -0
- package/src/react/client/RemoteThreadList.ts +93 -24
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +15 -0
- package/src/react/primitive-hooks/useActionBarCopy.ts +13 -3
- package/src/react/primitive-hooks/useThreadIsEmpty.ts +3 -0
- package/src/react/primitive-hooks/useThreadIsRunning.ts +3 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +12 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.republish.test.ts +87 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +268 -63
- package/src/react/runtimes/RemoteThreadResource.ts +2 -0
- package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.test.tsx +58 -0
- package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.ts +3 -1
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +98 -3
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +68 -6
- package/src/react/runtimes/useLocalRuntime.test.tsx +5 -5
- package/src/runtime/utils/message-repository.ts +26 -18
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +4 -0
- package/src/runtimes/inert/InertThreadRuntimeCore.test.ts +253 -0
- package/src/runtimes/inert/InertThreadRuntimeCore.ts +232 -0
- package/src/runtimes/local/local-thread-runtime-core.test.ts +319 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +38 -8
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +17 -209
- package/src/runtimes/remote-thread-list/adapter-changed.ts +10 -0
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +28 -221
- package/src/runtimes/remote-thread-list/optimistic-state.test.ts +18 -0
- package/src/runtimes/remote-thread-list/optimistic-state.ts +13 -0
- package/src/runtimes/remote-thread-list/types.ts +4 -0
- package/src/store/clients/external-thread.ts +1 -1
- package/src/store/runtime-clients/handle-runtime-action.test.ts +40 -0
- package/src/store/runtime-clients/handle-runtime-action.ts +3 -0
- package/src/tests/MessageRepository.test.ts +48 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-errors.test.ts +2 -2
- package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +8 -6
- package/src/tests/empty-thread-core.test.ts +6 -0
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +20 -0
- package/src/tests/external-thread-isEmpty.test.tsx +100 -0
- package/src/tests/remote-thread-list-adapter-switch.test.ts +383 -0
- package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +67 -0
- package/src/types/message.ts +1 -1
- package/src/utils/silent-runtime-action.ts +6 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { RemoteThreadListThreadListRuntimeCore } from "./RemoteThreadListThreadListRuntimeCore";
|
|
3
|
+
import { ThreadListRuntimeImpl } from "../../runtime/api/thread-list-runtime";
|
|
4
|
+
import { ReadonlyThreadRuntimeCore } from "../../runtimes/readonly/ReadonlyThreadRuntimeCore";
|
|
5
|
+
import {
|
|
6
|
+
contextProvider,
|
|
7
|
+
makeAdapter,
|
|
8
|
+
} from "../../tests/remote-thread-list-test-helpers";
|
|
9
|
+
import type { ThreadMessage } from "../../types/message";
|
|
10
|
+
|
|
11
|
+
const message = (id: string): ThreadMessage =>
|
|
12
|
+
({
|
|
13
|
+
id,
|
|
14
|
+
role: "user",
|
|
15
|
+
content: [{ type: "text", text: id }],
|
|
16
|
+
createdAt: new Date(0),
|
|
17
|
+
attachments: [],
|
|
18
|
+
metadata: { custom: {} },
|
|
19
|
+
}) as unknown as ThreadMessage;
|
|
20
|
+
|
|
21
|
+
const setup = () => {
|
|
22
|
+
const core = new RemoteThreadListThreadListRuntimeCore(
|
|
23
|
+
{ adapter: makeAdapter(), runtimeHook: () => ({}) as never },
|
|
24
|
+
contextProvider,
|
|
25
|
+
);
|
|
26
|
+
const hookManager = (
|
|
27
|
+
core as unknown as {
|
|
28
|
+
_hookManager: {
|
|
29
|
+
_publishThreadRuntime: (
|
|
30
|
+
id: string,
|
|
31
|
+
runtime: ReadonlyThreadRuntimeCore,
|
|
32
|
+
generation: number,
|
|
33
|
+
) => void;
|
|
34
|
+
instances: Map<string, { generation: number }>;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
)._hookManager;
|
|
38
|
+
const threadId = core.mainThreadId!;
|
|
39
|
+
const publish = (runtime: ReadonlyThreadRuntimeCore) =>
|
|
40
|
+
hookManager._publishThreadRuntime(
|
|
41
|
+
threadId,
|
|
42
|
+
runtime,
|
|
43
|
+
hookManager.instances.get(threadId)!.generation,
|
|
44
|
+
);
|
|
45
|
+
return { core, publish };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe("RemoteThreadListThreadListRuntimeCore republish", () => {
|
|
49
|
+
it("keeps the subscribed main thread state on the republished core", async () => {
|
|
50
|
+
const { core, publish } = setup();
|
|
51
|
+
const first = new ReadonlyThreadRuntimeCore();
|
|
52
|
+
first.setMessages([message("m1")]);
|
|
53
|
+
publish(first);
|
|
54
|
+
|
|
55
|
+
const thread = new ThreadListRuntimeImpl(core).main;
|
|
56
|
+
thread.subscribe(() => {});
|
|
57
|
+
expect(thread.getState().messages.map((m) => m.id)).toEqual(["m1"]);
|
|
58
|
+
|
|
59
|
+
const second = new ReadonlyThreadRuntimeCore();
|
|
60
|
+
second.setMessages([message("m2")]);
|
|
61
|
+
publish(second);
|
|
62
|
+
await Promise.resolve();
|
|
63
|
+
|
|
64
|
+
const ids = thread.getState().messages.map((m) => m.id);
|
|
65
|
+
expect(ids).toEqual(["m2"]);
|
|
66
|
+
for (const id of ids) {
|
|
67
|
+
expect(() => thread.getMessageById(id).getState()).not.toThrow();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("notifies thread subscribers when the core is republished", async () => {
|
|
72
|
+
const { core, publish } = setup();
|
|
73
|
+
publish(new ReadonlyThreadRuntimeCore());
|
|
74
|
+
|
|
75
|
+
const thread = new ThreadListRuntimeImpl(core).main;
|
|
76
|
+
let notified = 0;
|
|
77
|
+
thread.subscribe(() => notified++);
|
|
78
|
+
|
|
79
|
+
const next = new ReadonlyThreadRuntimeCore();
|
|
80
|
+
next.setMessages([message("m1")]);
|
|
81
|
+
publish(next);
|
|
82
|
+
await Promise.resolve();
|
|
83
|
+
|
|
84
|
+
expect(notified).toBeGreaterThan(0);
|
|
85
|
+
expect(thread.getState().messages.map((m) => m.id)).toEqual(["m1"]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
import type {
|
|
18
18
|
RemoteThreadListAdapter,
|
|
19
19
|
RemoteThreadListOptions,
|
|
20
|
+
RemoteThreadMetadata,
|
|
20
21
|
} from "../../runtimes/remote-thread-list/types";
|
|
22
|
+
import { ThreadListAdapterChangedError } from "../../runtimes/remote-thread-list/adapter-changed";
|
|
21
23
|
import { RemoteThreadListHookInstanceManager } from "./RemoteThreadListHookInstanceManager";
|
|
22
24
|
import { isTitleSourceMessage } from "./RemoteThreadResource";
|
|
23
25
|
import {
|
|
@@ -47,6 +49,48 @@ const threadStatusError = (
|
|
|
47
49
|
`Thread "${threadIdOrRemoteId}" has status "${status}", so it cannot ${action}.`,
|
|
48
50
|
);
|
|
49
51
|
|
|
52
|
+
const EMPTY_REMOTE_STATE: RemoteThreadState = {
|
|
53
|
+
isLoading: true,
|
|
54
|
+
isLoadingMore: false,
|
|
55
|
+
cursor: undefined,
|
|
56
|
+
newThreadId: undefined,
|
|
57
|
+
threadIds: [],
|
|
58
|
+
archivedThreadIds: [],
|
|
59
|
+
threadIdMap: {},
|
|
60
|
+
threadData: {},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const addNewThread = (state: RemoteThreadState) => {
|
|
64
|
+
let id: string;
|
|
65
|
+
do {
|
|
66
|
+
id = `__LOCALID_${generateId()}`;
|
|
67
|
+
} while (state.threadIdMap[id]);
|
|
68
|
+
|
|
69
|
+
const mappingId = createThreadMappingId(id);
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
state: {
|
|
73
|
+
...state,
|
|
74
|
+
newThreadId: id,
|
|
75
|
+
threadIdMap: {
|
|
76
|
+
...state.threadIdMap,
|
|
77
|
+
[id]: mappingId,
|
|
78
|
+
},
|
|
79
|
+
threadData: {
|
|
80
|
+
...state.threadData,
|
|
81
|
+
[mappingId]: {
|
|
82
|
+
status: "new",
|
|
83
|
+
id,
|
|
84
|
+
remoteId: undefined,
|
|
85
|
+
externalId: undefined,
|
|
86
|
+
title: undefined,
|
|
87
|
+
custom: undefined,
|
|
88
|
+
} satisfies RemoteThreadData,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
50
94
|
export class RemoteThreadListThreadListRuntimeCore
|
|
51
95
|
extends BaseSubscribable
|
|
52
96
|
implements ThreadListRuntimeCore
|
|
@@ -58,20 +102,16 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
58
102
|
private _loadThreadsPromise: Promise<void> | undefined;
|
|
59
103
|
private _loadMorePromise: Promise<void> | undefined;
|
|
60
104
|
private _loadGeneration = 0;
|
|
105
|
+
private _adapterGeneration = 0;
|
|
106
|
+
private _replaceListOnNextLoad = false;
|
|
107
|
+
private _staleThreadIdsOnReplace: ReadonlySet<string> | undefined;
|
|
61
108
|
private _switchGeneration = 0;
|
|
62
109
|
private _switchTask: Promise<void> | undefined;
|
|
63
110
|
|
|
64
111
|
private _mainThreadId!: string;
|
|
65
|
-
private readonly _state = new OptimisticState<RemoteThreadState>(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
cursor: undefined,
|
|
69
|
-
newThreadId: undefined,
|
|
70
|
-
threadIds: [],
|
|
71
|
-
archivedThreadIds: [],
|
|
72
|
-
threadIdMap: {},
|
|
73
|
-
threadData: {},
|
|
74
|
-
});
|
|
112
|
+
private readonly _state = new OptimisticState<RemoteThreadState>(
|
|
113
|
+
EMPTY_REMOTE_STATE,
|
|
114
|
+
);
|
|
75
115
|
|
|
76
116
|
private readonly _useAdaptersProvider: FC<PropsWithChildren> = ({
|
|
77
117
|
children,
|
|
@@ -117,6 +157,7 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
117
157
|
// TODO this needs to be cached in case this promise is loaded during suspense
|
|
118
158
|
if (!this._loadThreadsPromise) {
|
|
119
159
|
const generation = this._loadGeneration;
|
|
160
|
+
let replacedList = false;
|
|
120
161
|
this._loadThreadsPromise = this._state
|
|
121
162
|
.optimisticUpdate({
|
|
122
163
|
execute: () => this._options.adapter.list(),
|
|
@@ -128,27 +169,31 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
128
169
|
},
|
|
129
170
|
then: (state, l) => {
|
|
130
171
|
if (generation !== this._loadGeneration) return state;
|
|
172
|
+
const replaceList = this._replaceListOnNextLoad;
|
|
173
|
+
if (replaceList) {
|
|
174
|
+
this._replaceListOnNextLoad = false;
|
|
175
|
+
replacedList = true;
|
|
176
|
+
return this._replaceWithThreads(
|
|
177
|
+
state,
|
|
178
|
+
l.threads,
|
|
179
|
+
normalizeCursor(l.nextCursor),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
131
183
|
const fresh = classifyThreads(l.threads, {
|
|
132
184
|
threadIds: [],
|
|
133
185
|
archivedThreadIds: [],
|
|
134
186
|
threadIdMap: {},
|
|
135
187
|
threadData: {},
|
|
136
188
|
});
|
|
137
|
-
|
|
138
189
|
return {
|
|
139
190
|
...state,
|
|
140
191
|
isLoading: false,
|
|
141
192
|
cursor: normalizeCursor(l.nextCursor),
|
|
142
193
|
threadIds: fresh.threadIds,
|
|
143
194
|
archivedThreadIds: fresh.archivedThreadIds,
|
|
144
|
-
threadIdMap: {
|
|
145
|
-
|
|
146
|
-
...fresh.threadIdMap,
|
|
147
|
-
},
|
|
148
|
-
threadData: {
|
|
149
|
-
...state.threadData,
|
|
150
|
-
...fresh.threadData,
|
|
151
|
-
},
|
|
195
|
+
threadIdMap: { ...state.threadIdMap, ...fresh.threadIdMap },
|
|
196
|
+
threadData: { ...state.threadData, ...fresh.threadData },
|
|
152
197
|
};
|
|
153
198
|
},
|
|
154
199
|
})
|
|
@@ -156,12 +201,26 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
156
201
|
if (generation !== this._loadGeneration) return;
|
|
157
202
|
console.error("[assistant-ui] thread list load failed:", error);
|
|
158
203
|
this._loadThreadsPromise = undefined;
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
204
|
+
if (!this._replaceListOnNextLoad) {
|
|
205
|
+
this._state.update({
|
|
206
|
+
...this._state.baseValue,
|
|
207
|
+
isLoading: false,
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
this._replaceListOnNextLoad = false;
|
|
212
|
+
replacedList = true;
|
|
213
|
+
this._state.update(
|
|
214
|
+
this._replaceWithThreads(this._state.baseValue, [], undefined),
|
|
215
|
+
);
|
|
163
216
|
})
|
|
164
|
-
.then(() => {
|
|
217
|
+
.then(() => {
|
|
218
|
+
if (!replacedList) return;
|
|
219
|
+
const threadId = this._options.threadId;
|
|
220
|
+
if (threadId === undefined) return;
|
|
221
|
+
if (this.getItemById(threadId)?.id === this._mainThreadId) return;
|
|
222
|
+
this._switchToThreadFromProp(threadId).catch(() => {});
|
|
223
|
+
});
|
|
165
224
|
}
|
|
166
225
|
|
|
167
226
|
return this._loadThreadsPromise;
|
|
@@ -206,6 +265,7 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
206
265
|
},
|
|
207
266
|
})
|
|
208
267
|
.catch((error: unknown) => {
|
|
268
|
+
if (generation !== this._loadGeneration) return;
|
|
209
269
|
console.error("[assistant-ui] thread list loadMore failed:", error);
|
|
210
270
|
})
|
|
211
271
|
.then(() => {
|
|
@@ -237,6 +297,11 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
237
297
|
this._hookManager.__internal_subscribeRunningChanged(() =>
|
|
238
298
|
this._notifySubscribers(),
|
|
239
299
|
);
|
|
300
|
+
this._hookManager.__internal_subscribeRuntimeReplaced(() => {
|
|
301
|
+
// A republish can land during the thread resource's render, where a
|
|
302
|
+
// synchronous notify would re-enter store consumers mid-render.
|
|
303
|
+
queueMicrotask(() => this._notifySubscribers());
|
|
304
|
+
});
|
|
240
305
|
this.useProvider = create(() => ({
|
|
241
306
|
Provider: this.resolveProvider(options.adapter),
|
|
242
307
|
}));
|
|
@@ -268,8 +333,17 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
268
333
|
|
|
269
334
|
if (adapterChanged) {
|
|
270
335
|
this._loadGeneration++;
|
|
336
|
+
this._adapterGeneration++;
|
|
337
|
+
this._switchGeneration++;
|
|
338
|
+
this._switchTask = undefined;
|
|
271
339
|
this._loadThreadsPromise = undefined;
|
|
272
340
|
this._loadMorePromise = undefined;
|
|
341
|
+
this._replaceListOnNextLoad = true;
|
|
342
|
+
this._staleThreadIdsOnReplace = new Set(
|
|
343
|
+
Object.values(this._state.baseValue.threadData)
|
|
344
|
+
.filter((item) => item.status !== "new")
|
|
345
|
+
.map((item) => item.id),
|
|
346
|
+
);
|
|
273
347
|
this._state.update({
|
|
274
348
|
...this._state.baseValue,
|
|
275
349
|
cursor: undefined,
|
|
@@ -281,6 +355,115 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
281
355
|
}
|
|
282
356
|
}
|
|
283
357
|
|
|
358
|
+
private _requireAdapterGeneration(generation: number) {
|
|
359
|
+
if (generation !== this._adapterGeneration) {
|
|
360
|
+
throw new ThreadListAdapterChangedError();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private _requireAdapterSettled() {
|
|
365
|
+
if (this._replaceListOnNextLoad) {
|
|
366
|
+
throw new ThreadListAdapterChangedError();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private _replaceWithThreads(
|
|
371
|
+
state: RemoteThreadState,
|
|
372
|
+
threads: readonly RemoteThreadMetadata[],
|
|
373
|
+
cursor: string | undefined,
|
|
374
|
+
): RemoteThreadState {
|
|
375
|
+
const fresh = classifyThreads(threads, {
|
|
376
|
+
threadIds: [],
|
|
377
|
+
archivedThreadIds: [],
|
|
378
|
+
threadIdMap: {},
|
|
379
|
+
threadData: {},
|
|
380
|
+
});
|
|
381
|
+
const threadIdMap = { ...fresh.threadIdMap };
|
|
382
|
+
const threadData = { ...fresh.threadData };
|
|
383
|
+
const threadIds = [...fresh.threadIds];
|
|
384
|
+
const archivedThreadIds = [...fresh.archivedThreadIds];
|
|
385
|
+
|
|
386
|
+
if (state.newThreadId) {
|
|
387
|
+
const mappingId = state.threadIdMap[state.newThreadId];
|
|
388
|
+
const draft = mappingId ? state.threadData[mappingId] : undefined;
|
|
389
|
+
if (draft?.status === "new") {
|
|
390
|
+
threadIdMap[state.newThreadId] = mappingId!;
|
|
391
|
+
threadData[mappingId!] = draft;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const stale = this._staleThreadIdsOnReplace;
|
|
396
|
+
if (stale) {
|
|
397
|
+
for (const item of Object.values(state.threadData)) {
|
|
398
|
+
if (stale.has(item.id)) continue;
|
|
399
|
+
if (item.status !== "new" && item.remoteId === undefined) continue;
|
|
400
|
+
const mappingId = createThreadMappingId(item.id);
|
|
401
|
+
if (threadData[mappingId]) continue;
|
|
402
|
+
threadIdMap[item.id] = mappingId;
|
|
403
|
+
if (item.remoteId !== undefined) {
|
|
404
|
+
threadIdMap[item.remoteId] = mappingId;
|
|
405
|
+
}
|
|
406
|
+
threadData[mappingId] = item;
|
|
407
|
+
if (item.remoteId === undefined) continue;
|
|
408
|
+
if (item.status === "regular" && !threadIds.includes(item.remoteId)) {
|
|
409
|
+
threadIds.push(item.remoteId);
|
|
410
|
+
} else if (
|
|
411
|
+
item.status === "archived" &&
|
|
412
|
+
!archivedThreadIds.includes(item.remoteId)
|
|
413
|
+
) {
|
|
414
|
+
archivedThreadIds.push(item.remoteId);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
this._staleThreadIdsOnReplace = undefined;
|
|
419
|
+
|
|
420
|
+
let nextState: RemoteThreadState = {
|
|
421
|
+
...state,
|
|
422
|
+
isLoading: false,
|
|
423
|
+
cursor,
|
|
424
|
+
threadIds,
|
|
425
|
+
archivedThreadIds,
|
|
426
|
+
threadIdMap,
|
|
427
|
+
threadData,
|
|
428
|
+
newThreadId:
|
|
429
|
+
state.newThreadId !== undefined &&
|
|
430
|
+
threadIdMap[state.newThreadId] === undefined
|
|
431
|
+
? undefined
|
|
432
|
+
: state.newThreadId,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
if (getThreadData(nextState, this._mainThreadId) === undefined) {
|
|
436
|
+
const preservedDraft = nextState.newThreadId;
|
|
437
|
+
if (preservedDraft !== undefined) {
|
|
438
|
+
this._mainThreadId = preservedDraft;
|
|
439
|
+
} else {
|
|
440
|
+
const seeded = addNewThread(nextState);
|
|
441
|
+
this._mainThreadId = seeded.id;
|
|
442
|
+
nextState = seeded.state;
|
|
443
|
+
}
|
|
444
|
+
if (this._options.threadId === undefined) {
|
|
445
|
+
this._notifyThreadIdChange();
|
|
446
|
+
} else {
|
|
447
|
+
this._lastNotifiedThreadId = undefined;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const nextIds = new Set(
|
|
452
|
+
Object.values(nextState.threadData).map((item) => item.id),
|
|
453
|
+
);
|
|
454
|
+
for (const item of Object.values(state.threadData)) {
|
|
455
|
+
if (!nextIds.has(item.id)) {
|
|
456
|
+
this._hookManager.stopThreadRuntime(item.id);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
void this._hookManager.startThreadRuntime(this._mainThreadId).then(
|
|
460
|
+
() => this._notifySubscribers(),
|
|
461
|
+
() => undefined,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
return nextState;
|
|
465
|
+
}
|
|
466
|
+
|
|
284
467
|
public __internal_load() {
|
|
285
468
|
this.getLoadThreadsPromise(); // begin loading on initial bind
|
|
286
469
|
if (this._initialThreadLoaded) return;
|
|
@@ -440,6 +623,12 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
440
623
|
generation: number,
|
|
441
624
|
emitThreadIdChange: boolean,
|
|
442
625
|
): Promise<void> {
|
|
626
|
+
if (
|
|
627
|
+
this._replaceListOnNextLoad &&
|
|
628
|
+
threadIdOrRemoteId !== this._state.value.newThreadId
|
|
629
|
+
) {
|
|
630
|
+
throw new ThreadListAdapterChangedError();
|
|
631
|
+
}
|
|
443
632
|
let data = this.getItemById(threadIdOrRemoteId);
|
|
444
633
|
|
|
445
634
|
if (!data) {
|
|
@@ -573,48 +762,32 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
573
762
|
const state = this._state.baseValue;
|
|
574
763
|
let id: string | undefined = this._state.value.newThreadId;
|
|
575
764
|
if (id === undefined) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const mappingId = createThreadMappingId(id);
|
|
581
|
-
this._state.update({
|
|
582
|
-
...state,
|
|
583
|
-
newThreadId: id,
|
|
584
|
-
threadIdMap: {
|
|
585
|
-
...state.threadIdMap,
|
|
586
|
-
[id]: mappingId,
|
|
587
|
-
},
|
|
588
|
-
threadData: {
|
|
589
|
-
...state.threadData,
|
|
590
|
-
[mappingId]: {
|
|
591
|
-
status: "new",
|
|
592
|
-
id,
|
|
593
|
-
remoteId: undefined,
|
|
594
|
-
externalId: undefined,
|
|
595
|
-
title: undefined,
|
|
596
|
-
custom: undefined,
|
|
597
|
-
} satisfies RemoteThreadData,
|
|
598
|
-
},
|
|
599
|
-
});
|
|
765
|
+
const next = addNewThread(state);
|
|
766
|
+
id = next.id;
|
|
767
|
+
this._state.update(next.state);
|
|
600
768
|
}
|
|
601
769
|
|
|
602
770
|
return this._switchToThread(id, undefined, generation, emitThreadIdChange);
|
|
603
771
|
}
|
|
604
772
|
|
|
605
773
|
public initialize = async (threadId: string) => {
|
|
774
|
+
const adapter = this._options.adapter;
|
|
775
|
+
const adapterGeneration = this._adapterGeneration;
|
|
606
776
|
if (this._state.value.newThreadId !== threadId) {
|
|
777
|
+
this._requireAdapterSettled();
|
|
607
778
|
const data = this.getItemById(threadId);
|
|
608
779
|
if (!data) throw threadNotFoundError(threadId, "initializing it");
|
|
609
780
|
if (data.status === "new")
|
|
610
781
|
throw threadStatusError(threadId, data.status, "be initialized here");
|
|
611
782
|
const { remoteId, externalId } = await data.initializeTask;
|
|
783
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
612
784
|
return { remoteId, externalId };
|
|
613
785
|
}
|
|
614
786
|
|
|
615
787
|
const { remoteId, externalId } = await this._state.optimisticUpdate({
|
|
616
788
|
execute: () => {
|
|
617
|
-
|
|
789
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
790
|
+
return adapter.initialize(threadId);
|
|
618
791
|
},
|
|
619
792
|
optimistic: (state) => {
|
|
620
793
|
return updateStatusReducer(state, threadId, "regular");
|
|
@@ -633,6 +806,7 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
633
806
|
};
|
|
634
807
|
},
|
|
635
808
|
then: (state, { remoteId, externalId }) => {
|
|
809
|
+
if (adapterGeneration !== this._adapterGeneration) return state;
|
|
636
810
|
const data = getThreadData(state, threadId);
|
|
637
811
|
if (!data) return state;
|
|
638
812
|
|
|
@@ -655,16 +829,21 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
655
829
|
};
|
|
656
830
|
},
|
|
657
831
|
});
|
|
832
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
658
833
|
return { remoteId, externalId };
|
|
659
834
|
};
|
|
660
835
|
|
|
661
836
|
public generateTitle = async (threadId: string) => {
|
|
837
|
+
this._requireAdapterSettled();
|
|
838
|
+
const adapter = this._options.adapter;
|
|
839
|
+
const adapterGeneration = this._adapterGeneration;
|
|
662
840
|
const data = this.getItemById(threadId);
|
|
663
841
|
if (!data) throw threadNotFoundError(threadId, "generating its title");
|
|
664
842
|
if (data.status === "new")
|
|
665
843
|
throw threadStatusError(threadId, data.status, "generate a title");
|
|
666
844
|
|
|
667
845
|
const { remoteId } = await data.initializeTask;
|
|
846
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
668
847
|
|
|
669
848
|
const runtimeCore = this._hookManager.getThreadRuntimeCore(data.id);
|
|
670
849
|
if (!runtimeCore) return; // thread is no longer running
|
|
@@ -673,12 +852,10 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
673
852
|
// would make the payload race-dependent; the title reads settled
|
|
674
853
|
// messages only, matching the trigger's readiness gate.
|
|
675
854
|
const messages = runtimeCore.messages.filter(isTitleSourceMessage);
|
|
676
|
-
const stream = await
|
|
677
|
-
remoteId,
|
|
678
|
-
messages,
|
|
679
|
-
);
|
|
855
|
+
const stream = await adapter.generateTitle(remoteId, messages);
|
|
680
856
|
const messageStream = AssistantMessageStream.fromAssistantStream(stream);
|
|
681
857
|
for await (const result of messageStream) {
|
|
858
|
+
if (adapterGeneration !== this._adapterGeneration) return;
|
|
682
859
|
const newTitle = result.parts.filter((c) => c.type === "text")[0]?.text;
|
|
683
860
|
const state = this._state.baseValue;
|
|
684
861
|
const currentData = getThreadData(state, data.id);
|
|
@@ -696,7 +873,13 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
696
873
|
}
|
|
697
874
|
};
|
|
698
875
|
|
|
699
|
-
public rename(
|
|
876
|
+
public async rename(
|
|
877
|
+
threadIdOrRemoteId: string,
|
|
878
|
+
newTitle: string,
|
|
879
|
+
): Promise<void> {
|
|
880
|
+
this._requireAdapterSettled();
|
|
881
|
+
const adapter = this._options.adapter;
|
|
882
|
+
const adapterGeneration = this._adapterGeneration;
|
|
700
883
|
const data = this.getItemById(threadIdOrRemoteId);
|
|
701
884
|
if (!data) throw threadNotFoundError(threadIdOrRemoteId, "renaming it");
|
|
702
885
|
if (data.status === "new")
|
|
@@ -705,7 +888,8 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
705
888
|
return this._state.optimisticUpdate({
|
|
706
889
|
execute: async () => {
|
|
707
890
|
const { remoteId } = await data.initializeTask;
|
|
708
|
-
|
|
891
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
892
|
+
return adapter.rename(remoteId, newTitle);
|
|
709
893
|
},
|
|
710
894
|
optimistic: (state) => {
|
|
711
895
|
const data = getThreadData(state, threadIdOrRemoteId);
|
|
@@ -725,10 +909,13 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
725
909
|
});
|
|
726
910
|
}
|
|
727
911
|
|
|
728
|
-
public updateCustom(
|
|
912
|
+
public async updateCustom(
|
|
729
913
|
threadIdOrRemoteId: string,
|
|
730
914
|
custom: Record<string, unknown> | undefined,
|
|
731
915
|
): Promise<void> {
|
|
916
|
+
this._requireAdapterSettled();
|
|
917
|
+
const adapter = this._options.adapter;
|
|
918
|
+
const adapterGeneration = this._adapterGeneration;
|
|
732
919
|
const data = this.getItemById(threadIdOrRemoteId);
|
|
733
920
|
if (!data)
|
|
734
921
|
throw threadNotFoundError(
|
|
@@ -742,7 +929,7 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
742
929
|
"update custom metadata",
|
|
743
930
|
);
|
|
744
931
|
|
|
745
|
-
if (!
|
|
932
|
+
if (!adapter.updateCustom) {
|
|
746
933
|
throw new Error(
|
|
747
934
|
"Remote thread list adapter does not support updating custom metadata",
|
|
748
935
|
);
|
|
@@ -751,7 +938,7 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
751
938
|
return this._state.optimisticUpdate({
|
|
752
939
|
execute: async () => {
|
|
753
940
|
const { remoteId } = await data.initializeTask;
|
|
754
|
-
|
|
941
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
755
942
|
if (!adapter.updateCustom) {
|
|
756
943
|
throw new Error(
|
|
757
944
|
"Remote thread list adapter does not support updating custom metadata",
|
|
@@ -800,17 +987,22 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
800
987
|
}
|
|
801
988
|
|
|
802
989
|
public async archive(threadIdOrRemoteId: string) {
|
|
990
|
+
this._requireAdapterSettled();
|
|
991
|
+
const adapter = this._options.adapter;
|
|
992
|
+
const adapterGeneration = this._adapterGeneration;
|
|
803
993
|
const data = this.getItemById(threadIdOrRemoteId);
|
|
804
994
|
if (!data) throw threadNotFoundError(threadIdOrRemoteId, "archiving it");
|
|
805
995
|
if (data.status !== "regular")
|
|
806
996
|
throw threadStatusError(threadIdOrRemoteId, data.status, "be archived");
|
|
807
997
|
|
|
808
998
|
await this._ensureThreadIsNotMain(data.id);
|
|
999
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
809
1000
|
|
|
810
1001
|
return this._state.optimisticUpdate({
|
|
811
1002
|
execute: async () => {
|
|
812
1003
|
const { remoteId } = await data.initializeTask;
|
|
813
|
-
|
|
1004
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
1005
|
+
return adapter.archive(remoteId);
|
|
814
1006
|
},
|
|
815
1007
|
optimistic: (state) => {
|
|
816
1008
|
return updateStatusReducer(state, data.id, "archived");
|
|
@@ -818,7 +1010,10 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
818
1010
|
});
|
|
819
1011
|
}
|
|
820
1012
|
|
|
821
|
-
public unarchive(threadIdOrRemoteId: string): Promise<void> {
|
|
1013
|
+
public async unarchive(threadIdOrRemoteId: string): Promise<void> {
|
|
1014
|
+
this._requireAdapterSettled();
|
|
1015
|
+
const adapter = this._options.adapter;
|
|
1016
|
+
const adapterGeneration = this._adapterGeneration;
|
|
822
1017
|
const data = this.getItemById(threadIdOrRemoteId);
|
|
823
1018
|
if (!data) throw threadNotFoundError(threadIdOrRemoteId, "unarchiving it");
|
|
824
1019
|
if (data.status !== "archived")
|
|
@@ -828,9 +1023,12 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
828
1023
|
execute: async () => {
|
|
829
1024
|
try {
|
|
830
1025
|
const { remoteId } = await data.initializeTask;
|
|
831
|
-
|
|
1026
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
1027
|
+
return await adapter.unarchive(remoteId);
|
|
832
1028
|
} catch (error) {
|
|
833
|
-
|
|
1029
|
+
if (adapterGeneration === this._adapterGeneration) {
|
|
1030
|
+
await this._ensureThreadIsNotMain(data.id);
|
|
1031
|
+
}
|
|
834
1032
|
throw error;
|
|
835
1033
|
}
|
|
836
1034
|
},
|
|
@@ -841,18 +1039,23 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
841
1039
|
}
|
|
842
1040
|
|
|
843
1041
|
public async delete(threadIdOrRemoteId: string) {
|
|
1042
|
+
this._requireAdapterSettled();
|
|
1043
|
+
const adapter = this._options.adapter;
|
|
1044
|
+
const adapterGeneration = this._adapterGeneration;
|
|
844
1045
|
const data = this.getItemById(threadIdOrRemoteId);
|
|
845
1046
|
if (!data) throw threadNotFoundError(threadIdOrRemoteId, "deleting it");
|
|
846
1047
|
if (data.status !== "regular" && data.status !== "archived")
|
|
847
1048
|
throw threadStatusError(threadIdOrRemoteId, data.status, "be deleted");
|
|
848
1049
|
|
|
849
1050
|
await this._ensureThreadIsNotMain(data.id);
|
|
1051
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
850
1052
|
this._hookManager.stopThreadRuntime(data.id);
|
|
851
1053
|
|
|
852
1054
|
return this._state.optimisticUpdate({
|
|
853
1055
|
execute: async () => {
|
|
854
1056
|
const { remoteId } = await data.initializeTask;
|
|
855
|
-
|
|
1057
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
1058
|
+
return await adapter.delete(remoteId);
|
|
856
1059
|
},
|
|
857
1060
|
optimistic: (state) => {
|
|
858
1061
|
return updateStatusReducer(state, data.id, "deleted");
|
|
@@ -865,12 +1068,14 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
865
1068
|
}
|
|
866
1069
|
|
|
867
1070
|
public async detach(threadIdOrRemoteId: string): Promise<void> {
|
|
1071
|
+
const adapterGeneration = this._adapterGeneration;
|
|
868
1072
|
const data = this.getItemById(threadIdOrRemoteId);
|
|
869
1073
|
if (!data) throw threadNotFoundError(threadIdOrRemoteId, "detaching it");
|
|
870
1074
|
if (data.status !== "regular" && data.status !== "archived")
|
|
871
1075
|
throw threadStatusError(threadIdOrRemoteId, data.status, "be detached");
|
|
872
1076
|
|
|
873
1077
|
await this._ensureThreadIsNotMain(data.id);
|
|
1078
|
+
this._requireAdapterGeneration(adapterGeneration);
|
|
874
1079
|
this._hookManager.stopThreadRuntime(data.id);
|
|
875
1080
|
}
|
|
876
1081
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
useRuntimeAdaptersProvider,
|
|
22
22
|
type RuntimeAdapters,
|
|
23
23
|
} from "./RuntimeAdapterProvider";
|
|
24
|
+
import { isSilentRuntimeAction } from "../../utils/silent-runtime-action";
|
|
24
25
|
|
|
25
26
|
export type RemoteThreadListHook = () => AssistantRuntime;
|
|
26
27
|
|
|
@@ -48,6 +49,7 @@ export const subscribeToTitleGeneration = (
|
|
|
48
49
|
) => {
|
|
49
50
|
const generate = () =>
|
|
50
51
|
void itemRuntime.generateTitle().catch((error: unknown) => {
|
|
52
|
+
if (isSilentRuntimeAction(error)) return;
|
|
51
53
|
console.error("[assistant-ui] Thread title generation failed", error);
|
|
52
54
|
});
|
|
53
55
|
|