@assistant-ui/core 0.3.3 → 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/internal.d.ts +4 -2
- package/dist/internal.js +4 -2
- package/dist/react/primitive-hooks/useSuggestionTrigger.js +29 -25
- package/dist/react/primitive-hooks/useSuggestionTrigger.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 +2 -0
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +6 -4
- 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/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/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/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/message.d.ts +11 -0
- 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/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/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 +3 -3
- package/src/internal.ts +12 -1
- package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +148 -0
- package/src/react/primitive-hooks/useSuggestionTrigger.ts +10 -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 +8 -2
- package/src/react/types/MessagePartComponentTypes.ts +8 -0
- package/src/runtime/api/bindings.ts +5 -0
- 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/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 +125 -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/message.ts +11 -0
- package/src/utils/data-url.ts +24 -0
- package/src/utils/image-media-type.test.ts +96 -0
- package/src/utils/image-media-type.ts +118 -0
- package/src/utils/wire-media.test.ts +96 -0
- package/src/utils/wire-media.ts +70 -0
|
@@ -16,3 +16,163 @@ describe("RemoteThreadListHookInstanceManager", () => {
|
|
|
16
16
|
);
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
|
+
|
|
20
|
+
describe("RemoteThreadListHookInstanceManager.__internal_restartThreadRuntime", () => {
|
|
21
|
+
const makeManager = () =>
|
|
22
|
+
new RemoteThreadListHookInstanceManager(
|
|
23
|
+
() => ({}) as never,
|
|
24
|
+
{} as ThreadListRuntimeCore,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// no React binder attaches a runtime in these tests, so the returned promises
|
|
28
|
+
// stay pending or reject on stop; neither is what is under test here
|
|
29
|
+
const start = (manager: RemoteThreadListHookInstanceManager, id: string) => {
|
|
30
|
+
manager.startThreadRuntime(id).catch(() => {});
|
|
31
|
+
};
|
|
32
|
+
const restart = (
|
|
33
|
+
manager: RemoteThreadListHookInstanceManager,
|
|
34
|
+
id: string,
|
|
35
|
+
) => {
|
|
36
|
+
manager.__internal_restartThreadRuntime(id).catch(() => {});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type InstanceInternals = {
|
|
40
|
+
instances: Map<
|
|
41
|
+
string,
|
|
42
|
+
{
|
|
43
|
+
runtime?: unknown;
|
|
44
|
+
publishedGeneration?: number;
|
|
45
|
+
generation: number;
|
|
46
|
+
}
|
|
47
|
+
>;
|
|
48
|
+
_notifySubscribers: () => void;
|
|
49
|
+
};
|
|
50
|
+
const internalsOf = (manager: RemoteThreadListHookInstanceManager) =>
|
|
51
|
+
manager as unknown as InstanceInternals;
|
|
52
|
+
|
|
53
|
+
const renderedKeys = (manager: RemoteThreadListHookInstanceManager) =>
|
|
54
|
+
Array.from(internalsOf(manager).instances.entries()).map(
|
|
55
|
+
([id, { generation }]) => `${id}:${generation}`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
it("changes the binder key so React remounts the runtime hook", () => {
|
|
59
|
+
const manager = makeManager();
|
|
60
|
+
start(manager, "thread-1");
|
|
61
|
+
const before = renderedKeys(manager);
|
|
62
|
+
|
|
63
|
+
restart(manager, "thread-1");
|
|
64
|
+
|
|
65
|
+
expect(renderedKeys(manager)).not.toEqual(before);
|
|
66
|
+
expect(renderedKeys(manager)).toEqual(["thread-1:1"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("keeps the thread rendered across the restart, unlike stop", () => {
|
|
70
|
+
const restarted = makeManager();
|
|
71
|
+
start(restarted, "thread-1");
|
|
72
|
+
restart(restarted, "thread-1");
|
|
73
|
+
|
|
74
|
+
const stopped = makeManager();
|
|
75
|
+
start(stopped, "thread-1");
|
|
76
|
+
stopped.stopThreadRuntime("thread-1");
|
|
77
|
+
|
|
78
|
+
expect(renderedKeys(restarted)).toHaveLength(1);
|
|
79
|
+
expect(renderedKeys(stopped)).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("starts the runtime when the thread is not alive yet", () => {
|
|
83
|
+
const manager = makeManager();
|
|
84
|
+
|
|
85
|
+
restart(manager, "thread-1");
|
|
86
|
+
|
|
87
|
+
expect(renderedKeys(manager)).toEqual(["thread-1:0"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("stop then start in one tick yields a fresh key, so the old still-mounted binder cannot satisfy the new start", () => {
|
|
91
|
+
const manager = makeManager();
|
|
92
|
+
start(manager, "thread-1");
|
|
93
|
+
const before = renderedKeys(manager);
|
|
94
|
+
|
|
95
|
+
manager.stopThreadRuntime("thread-1");
|
|
96
|
+
start(manager, "thread-1");
|
|
97
|
+
|
|
98
|
+
expect(renderedKeys(manager)).not.toEqual(before);
|
|
99
|
+
expect(renderedKeys(manager)).toEqual(["thread-1:1"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// simulates the binder's updateRuntime: publish a runtime for the
|
|
103
|
+
// instance's current generation (or an explicit stale one)
|
|
104
|
+
const publish = (
|
|
105
|
+
manager: RemoteThreadListHookInstanceManager,
|
|
106
|
+
id: string,
|
|
107
|
+
runtime: unknown,
|
|
108
|
+
options?: { generation?: number },
|
|
109
|
+
) => {
|
|
110
|
+
const instance = internalsOf(manager).instances.get(id)!;
|
|
111
|
+
instance.runtime = runtime;
|
|
112
|
+
instance.publishedGeneration = options?.generation ?? instance.generation;
|
|
113
|
+
internalsOf(manager)._notifySubscribers();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
it("does not settle with the pre-restart runtime; only the incoming binder's publication resolves it", async () => {
|
|
117
|
+
const manager = makeManager();
|
|
118
|
+
start(manager, "thread-1");
|
|
119
|
+
publish(manager, "thread-1", { tag: "pre-reload-runtime" });
|
|
120
|
+
|
|
121
|
+
let settledWith = "NOT_SETTLED";
|
|
122
|
+
manager.__internal_restartThreadRuntime("thread-1").then((r) => {
|
|
123
|
+
settledWith = (r as { tag: string }).tag;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// the outgoing runtime rides across the restart (stays readable)…
|
|
127
|
+
expect(manager.getThreadRuntimeCore("thread-1")).toEqual({
|
|
128
|
+
tag: "pre-reload-runtime",
|
|
129
|
+
});
|
|
130
|
+
// …but must not count as attached
|
|
131
|
+
await Promise.resolve();
|
|
132
|
+
await Promise.resolve();
|
|
133
|
+
await Promise.resolve();
|
|
134
|
+
expect(settledWith).toBe("NOT_SETTLED");
|
|
135
|
+
|
|
136
|
+
publish(manager, "thread-1", { tag: "post-reload-runtime" });
|
|
137
|
+
await Promise.resolve();
|
|
138
|
+
expect(settledWith).toBe("post-reload-runtime");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("keeps the outgoing runtime readable while the restart promise is pending", () => {
|
|
142
|
+
const manager = makeManager();
|
|
143
|
+
start(manager, "thread-1");
|
|
144
|
+
publish(manager, "thread-1", { tag: "pre-reload-runtime" });
|
|
145
|
+
|
|
146
|
+
restart(manager, "thread-1");
|
|
147
|
+
|
|
148
|
+
expect(manager.getThreadRuntimeCore("thread-1")).toEqual({
|
|
149
|
+
tag: "pre-reload-runtime",
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("a stale-generation publication does not resolve the restart promise", async () => {
|
|
154
|
+
const manager = makeManager();
|
|
155
|
+
start(manager, "thread-1");
|
|
156
|
+
publish(manager, "thread-1", { tag: "pre-reload-runtime" });
|
|
157
|
+
|
|
158
|
+
let settled = false;
|
|
159
|
+
manager.__internal_restartThreadRuntime("thread-1").then(() => {
|
|
160
|
+
settled = true;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// an outgoing binder re-publishing for its old generation (e.g. a late
|
|
164
|
+
// outerSubscribe callback) must not count as the new attachment
|
|
165
|
+
const staleGeneration =
|
|
166
|
+
internalsOf(manager).instances.get("thread-1")!.generation - 1;
|
|
167
|
+
publish(
|
|
168
|
+
manager,
|
|
169
|
+
"thread-1",
|
|
170
|
+
{ tag: "stale-publication" },
|
|
171
|
+
{ generation: staleGeneration },
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
await Promise.resolve();
|
|
175
|
+
await Promise.resolve();
|
|
176
|
+
expect(settled).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -18,14 +18,25 @@ import { ThreadListItemRuntimeProvider } from "../providers/ThreadListItemRuntim
|
|
|
18
18
|
import type { ThreadRuntimeCore } from "../../runtime/interfaces/thread-runtime-core";
|
|
19
19
|
import type { ThreadListRuntimeCore } from "../../runtime/interfaces/thread-list-runtime-core";
|
|
20
20
|
import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
|
|
21
|
+
import type { Unsubscribe } from "../../types/unsubscribe";
|
|
21
22
|
import { BaseSubscribable } from "../../subscribable/subscribable";
|
|
22
|
-
import
|
|
23
|
+
import {
|
|
24
|
+
getThreadRuntimeCoreIsRunning,
|
|
25
|
+
type ThreadRuntimeImpl,
|
|
26
|
+
} from "../../runtime/api/thread-runtime";
|
|
23
27
|
import { ThreadListRuntimeImpl } from "../../runtime/api/thread-list-runtime";
|
|
24
28
|
|
|
25
29
|
type RemoteThreadListHook = () => AssistantRuntime;
|
|
26
30
|
|
|
27
31
|
type RemoteThreadListHookInstance = {
|
|
28
|
-
runtime?: ThreadRuntimeCore;
|
|
32
|
+
runtime?: ThreadRuntimeCore | undefined;
|
|
33
|
+
// A runtime riding across a restart stays readable, but only counts as
|
|
34
|
+
// attached once a binder of the current generation re-publishes it.
|
|
35
|
+
publishedGeneration?: number | undefined;
|
|
36
|
+
// Part of the binder's React key, so only a bump remounts the hook.
|
|
37
|
+
generation: number;
|
|
38
|
+
isRunning: boolean;
|
|
39
|
+
unsubscribeRunning?: Unsubscribe | undefined;
|
|
29
40
|
};
|
|
30
41
|
|
|
31
42
|
const ProviderRenderDetector: FC<{
|
|
@@ -41,6 +52,9 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
41
52
|
StoreApi<{ useRuntime: RemoteThreadListHook }>
|
|
42
53
|
>;
|
|
43
54
|
private instances = new Map<string, RemoteThreadListHookInstance>();
|
|
55
|
+
// Manager-wide so it survives instance deletion: a stop and start within one
|
|
56
|
+
// React commit must not reuse a binder key.
|
|
57
|
+
private nextGeneration = 0;
|
|
44
58
|
private useAliveThreadsKeysChanged = create(() => ({}));
|
|
45
59
|
private parent: ThreadListRuntimeCore;
|
|
46
60
|
|
|
@@ -53,20 +67,18 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
53
67
|
this.useRuntimeHook = create(() => ({ useRuntime: runtimeHook }));
|
|
54
68
|
}
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
if (!this.instances.has(threadId)) {
|
|
58
|
-
this.instances.set(threadId, {});
|
|
59
|
-
this.useAliveThreadsKeysChanged.setState({}, true);
|
|
60
|
-
}
|
|
61
|
-
|
|
70
|
+
private _whenRuntimeAttached(threadId: string) {
|
|
62
71
|
return new Promise<ThreadRuntimeCore>((resolve, reject) => {
|
|
63
72
|
const callback = () => {
|
|
64
73
|
const instance = this.instances.get(threadId);
|
|
65
74
|
if (!instance) {
|
|
66
75
|
dispose();
|
|
67
76
|
reject(new Error("Thread was deleted before runtime was started"));
|
|
68
|
-
} else if (
|
|
69
|
-
|
|
77
|
+
} else if (
|
|
78
|
+
!instance.runtime ||
|
|
79
|
+
instance.publishedGeneration !== instance.generation
|
|
80
|
+
) {
|
|
81
|
+
return; // not yet published by the current generation's binder
|
|
70
82
|
} else {
|
|
71
83
|
dispose();
|
|
72
84
|
resolve(instance.runtime);
|
|
@@ -77,13 +89,104 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
77
89
|
});
|
|
78
90
|
}
|
|
79
91
|
|
|
92
|
+
public startThreadRuntime(threadId: string) {
|
|
93
|
+
if (!this.instances.has(threadId)) {
|
|
94
|
+
this.instances.set(threadId, {
|
|
95
|
+
generation: this.nextGeneration++,
|
|
96
|
+
isRunning: false,
|
|
97
|
+
});
|
|
98
|
+
this.useAliveThreadsKeysChanged.setState({}, true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this._whenRuntimeAttached(threadId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public __internal_restartThreadRuntime(threadId: string) {
|
|
105
|
+
const instance = this.instances.get(threadId);
|
|
106
|
+
if (!instance) return this.startThreadRuntime(threadId);
|
|
107
|
+
|
|
108
|
+
instance.generation = this.nextGeneration++;
|
|
109
|
+
this.useAliveThreadsKeysChanged.setState({}, true);
|
|
110
|
+
this._notifySubscribers();
|
|
111
|
+
|
|
112
|
+
return this._whenRuntimeAttached(threadId);
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
public getThreadRuntimeCore(threadId: string) {
|
|
81
116
|
const instance = this.instances.get(threadId);
|
|
82
117
|
if (!instance) return undefined;
|
|
83
118
|
return instance.runtime;
|
|
84
119
|
}
|
|
85
120
|
|
|
121
|
+
public __internal_isThreadRunning(threadId: string) {
|
|
122
|
+
return this.instances.get(threadId)?.isRunning ?? false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private runningSubscribers = new Set<() => void>();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fires when any thread crosses the running boundary. Separate from the
|
|
129
|
+
* general subscription so a run does not push the thread list through the
|
|
130
|
+
* channel that resolves pending runtime attachments.
|
|
131
|
+
*/
|
|
132
|
+
public __internal_subscribeRunningChanged(callback: () => void): Unsubscribe {
|
|
133
|
+
this.runningSubscribers.add(callback);
|
|
134
|
+
return () => this.runningSubscribers.delete(callback);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private _publishThreadRuntime(
|
|
138
|
+
threadId: string,
|
|
139
|
+
runtime: ThreadRuntimeCore,
|
|
140
|
+
generation: number,
|
|
141
|
+
) {
|
|
142
|
+
const instance = this.instances.get(threadId);
|
|
143
|
+
if (!instance)
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Thread "${threadId}" runtime binding not found. This is a bug in assistant-ui.`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// An outgoing binder outlives its generation until React commits the key
|
|
149
|
+
// change, and must not publish over the incoming one.
|
|
150
|
+
if (instance.generation !== generation) return;
|
|
151
|
+
|
|
152
|
+
const previousRuntime = instance.runtime;
|
|
153
|
+
instance.runtime = runtime;
|
|
154
|
+
instance.publishedGeneration = generation;
|
|
155
|
+
if (previousRuntime !== runtime) {
|
|
156
|
+
this._trackRunning(instance);
|
|
157
|
+
}
|
|
158
|
+
this._notifySubscribers();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Run state changes far more often than the thread list does, so the list is
|
|
162
|
+
// only notified when a thread crosses the running boundary.
|
|
163
|
+
private _trackRunning(instance: RemoteThreadListHookInstance) {
|
|
164
|
+
instance.unsubscribeRunning?.();
|
|
165
|
+
|
|
166
|
+
const runtime = instance.runtime;
|
|
167
|
+
if (!runtime) {
|
|
168
|
+
instance.unsubscribeRunning = undefined;
|
|
169
|
+
this._setRunning(instance, false);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this._setRunning(instance, getThreadRuntimeCoreIsRunning(runtime));
|
|
174
|
+
instance.unsubscribeRunning = runtime.subscribe(() => {
|
|
175
|
+
this._setRunning(instance, getThreadRuntimeCoreIsRunning(runtime));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private _setRunning(
|
|
180
|
+
instance: RemoteThreadListHookInstance,
|
|
181
|
+
isRunning: boolean,
|
|
182
|
+
) {
|
|
183
|
+
if (instance.isRunning === isRunning) return;
|
|
184
|
+
instance.isRunning = isRunning;
|
|
185
|
+
for (const callback of this.runningSubscribers) callback();
|
|
186
|
+
}
|
|
187
|
+
|
|
86
188
|
public stopThreadRuntime(threadId: string) {
|
|
189
|
+
this.instances.get(threadId)?.unsubscribeRunning?.();
|
|
87
190
|
this.instances.delete(threadId);
|
|
88
191
|
this.useAliveThreadsKeysChanged.setState({}, true);
|
|
89
192
|
this._notifySubscribers();
|
|
@@ -98,10 +201,9 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
98
201
|
|
|
99
202
|
// Rendered as a child of the user's Provider so the runtime hook can
|
|
100
203
|
// read context the Provider injects (e.g. RuntimeAdapterProvider).
|
|
101
|
-
private _RuntimeBinder: FC<
|
|
102
|
-
threadId
|
|
103
|
-
|
|
104
|
-
}) => {
|
|
204
|
+
private _RuntimeBinder: FC<
|
|
205
|
+
PropsWithChildren<{ threadId: string; generation: number }>
|
|
206
|
+
> = ({ threadId, generation, children }) => {
|
|
105
207
|
const { useRuntime } = this.useRuntimeHook();
|
|
106
208
|
const runtime = useRuntime();
|
|
107
209
|
|
|
@@ -109,15 +211,12 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
109
211
|
.__internal_threadBinding;
|
|
110
212
|
|
|
111
213
|
const updateRuntime = useCallback(() => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
aliveThread.runtime = threadBinding.getState();
|
|
119
|
-
this._notifySubscribers();
|
|
120
|
-
}, [threadId, threadBinding]);
|
|
214
|
+
this._publishThreadRuntime(
|
|
215
|
+
threadId,
|
|
216
|
+
threadBinding.getState(),
|
|
217
|
+
generation,
|
|
218
|
+
);
|
|
219
|
+
}, [threadId, generation, threadBinding]);
|
|
121
220
|
|
|
122
221
|
const isMounted = useRef(false);
|
|
123
222
|
if (!isMounted.current) {
|
|
@@ -168,8 +267,9 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
168
267
|
|
|
169
268
|
private _OuterActiveThreadProvider: FC<{
|
|
170
269
|
threadId: string;
|
|
270
|
+
generation: number;
|
|
171
271
|
provider: ComponentType<PropsWithChildren>;
|
|
172
|
-
}> = memo(({ threadId, provider: Provider }) => {
|
|
272
|
+
}> = memo(({ threadId, generation, provider: Provider }) => {
|
|
173
273
|
const runtime = useMemo(
|
|
174
274
|
() => new ThreadListRuntimeImpl(this.parent).getItemById(threadId),
|
|
175
275
|
[threadId],
|
|
@@ -195,7 +295,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
195
295
|
return (
|
|
196
296
|
<ThreadListItemRuntimeProvider runtime={runtime}>
|
|
197
297
|
<Provider>
|
|
198
|
-
<this._RuntimeBinder threadId={threadId}>
|
|
298
|
+
<this._RuntimeBinder threadId={threadId} generation={generation}>
|
|
199
299
|
<ProviderRenderDetector detectorRef={detectorRef} />
|
|
200
300
|
</this._RuntimeBinder>
|
|
201
301
|
</Provider>
|
|
@@ -208,12 +308,15 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
|
|
|
208
308
|
}> = ({ provider }) => {
|
|
209
309
|
this.useAliveThreadsKeysChanged(); // trigger re-render on alive threads change
|
|
210
310
|
|
|
211
|
-
return Array.from(this.instances.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
311
|
+
return Array.from(this.instances.entries()).map(
|
|
312
|
+
([threadId, { generation }]) => (
|
|
313
|
+
<this._OuterActiveThreadProvider
|
|
314
|
+
key={`${threadId}:${generation}`}
|
|
315
|
+
threadId={threadId}
|
|
316
|
+
generation={generation}
|
|
317
|
+
provider={provider}
|
|
318
|
+
/>
|
|
319
|
+
),
|
|
320
|
+
);
|
|
218
321
|
};
|
|
219
322
|
}
|
|
@@ -185,6 +185,9 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
185
185
|
options.runtimeHook,
|
|
186
186
|
this,
|
|
187
187
|
);
|
|
188
|
+
this._hookManager.__internal_subscribeRunningChanged(() =>
|
|
189
|
+
this._notifySubscribers(),
|
|
190
|
+
);
|
|
188
191
|
this.useProvider = create(() => ({
|
|
189
192
|
Provider: options.adapter.unstable_Provider ?? Fragment,
|
|
190
193
|
}));
|
|
@@ -245,6 +248,34 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
public async reloadMainThread(): Promise<void> {
|
|
252
|
+
const threadId = this._mainThreadId;
|
|
253
|
+
if (threadId === undefined) return;
|
|
254
|
+
|
|
255
|
+
// An unsent thread holds no remote state, so a refetch would only discard
|
|
256
|
+
// what the user has typed.
|
|
257
|
+
if (this.getItemById(threadId)?.status === "new") return;
|
|
258
|
+
|
|
259
|
+
const runtimeCore = this._hookManager.getThreadRuntimeCore(threadId);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
if (runtimeCore?.unstable_refetchThread) {
|
|
263
|
+
// Called on the core so class-method implementations keep `this`.
|
|
264
|
+
await runtimeCore.unstable_refetchThread();
|
|
265
|
+
} else {
|
|
266
|
+
await this._hookManager.__internal_restartThreadRuntime(threadId);
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// delete and detach switch the main thread away before stopping the
|
|
270
|
+
// runtime, so a rejection once that has happened belongs to them.
|
|
271
|
+
if (threadId !== this._mainThreadId) return;
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (threadId !== this._mainThreadId) return;
|
|
276
|
+
this._notifySubscribers();
|
|
277
|
+
}
|
|
278
|
+
|
|
248
279
|
public reload() {
|
|
249
280
|
this._loadGeneration++;
|
|
250
281
|
this._loadThreadsPromise = undefined;
|
|
@@ -321,6 +352,12 @@ export class RemoteThreadListThreadListRuntimeCore
|
|
|
321
352
|
return result;
|
|
322
353
|
}
|
|
323
354
|
|
|
355
|
+
public unstable_isThreadRunning(threadIdOrRemoteId: string) {
|
|
356
|
+
const data = this.getItemById(threadIdOrRemoteId);
|
|
357
|
+
if (!data) return false;
|
|
358
|
+
return this._hookManager.__internal_isThreadRunning(data.id);
|
|
359
|
+
}
|
|
360
|
+
|
|
324
361
|
public getItemById(threadIdOrRemoteId: string) {
|
|
325
362
|
return getThreadData(this._state.value, threadIdOrRemoteId);
|
|
326
363
|
}
|
|
@@ -77,6 +77,7 @@ type AuiV0MessagePart =
|
|
|
77
77
|
readonly data: string;
|
|
78
78
|
readonly mimeType: string;
|
|
79
79
|
readonly filename?: string;
|
|
80
|
+
readonly sourceType?: "url" | "id";
|
|
80
81
|
};
|
|
81
82
|
|
|
82
83
|
type AuiV0AttachmentPart =
|
|
@@ -94,6 +95,7 @@ type AuiV0AttachmentPart =
|
|
|
94
95
|
readonly data: string;
|
|
95
96
|
readonly mimeType: string;
|
|
96
97
|
readonly filename?: string;
|
|
98
|
+
readonly sourceType?: "url" | "id";
|
|
97
99
|
}
|
|
98
100
|
| {
|
|
99
101
|
readonly type: "audio";
|
|
@@ -157,6 +159,9 @@ const encodeAttachmentPart = (
|
|
|
157
159
|
data: part.data,
|
|
158
160
|
mimeType: part.mimeType,
|
|
159
161
|
...(part.filename != null ? { filename: part.filename } : undefined),
|
|
162
|
+
...(part.sourceType != null
|
|
163
|
+
? { sourceType: part.sourceType }
|
|
164
|
+
: undefined),
|
|
160
165
|
};
|
|
161
166
|
|
|
162
167
|
case "audio":
|
|
@@ -252,7 +257,7 @@ export function auiV0Encode(message: ThreadMessage): AuiV0Message {
|
|
|
252
257
|
};
|
|
253
258
|
|
|
254
259
|
case "tool-call": {
|
|
255
|
-
if (!isJSONValue(part.result)) {
|
|
260
|
+
if (part.result !== undefined && !isJSONValue(part.result)) {
|
|
256
261
|
console.warn(
|
|
257
262
|
`tool-call result is not JSON! ${JSON.stringify(part)}`,
|
|
258
263
|
);
|
|
@@ -264,7 +269,7 @@ export function auiV0Encode(message: ThreadMessage): AuiV0Message {
|
|
|
264
269
|
...(JSON.stringify(part.args) === part.argsText
|
|
265
270
|
? { args: part.args }
|
|
266
271
|
: { argsText: part.argsText }),
|
|
267
|
-
...(part.result
|
|
272
|
+
...(part.result !== undefined
|
|
268
273
|
? { result: part.result as ReadonlyJSONValue }
|
|
269
274
|
: undefined),
|
|
270
275
|
...(part.isError ? { isError: true } : undefined),
|
|
@@ -281,6 +286,7 @@ export function auiV0Encode(message: ThreadMessage): AuiV0Message {
|
|
|
281
286
|
data: part.data,
|
|
282
287
|
mimeType: part.mimeType,
|
|
283
288
|
...(part.filename ? { filename: part.filename } : undefined),
|
|
289
|
+
...(part.sourceType ? { sourceType: part.sourceType } : undefined),
|
|
284
290
|
};
|
|
285
291
|
|
|
286
292
|
default: {
|
|
@@ -44,8 +44,16 @@ export type ImageMessagePartComponent = ComponentType<ImageMessagePartProps>;
|
|
|
44
44
|
export type FileMessagePartProps = MessagePartState & FileMessagePart;
|
|
45
45
|
export type FileMessagePartComponent = ComponentType<FileMessagePartProps>;
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @deprecated Use {@link FileMessagePartProps} and render `audio/*` mime types
|
|
49
|
+
* from the `File` slot.
|
|
50
|
+
*/
|
|
47
51
|
export type Unstable_AudioMessagePartProps = MessagePartState &
|
|
48
52
|
Unstable_AudioMessagePart;
|
|
53
|
+
/**
|
|
54
|
+
* @deprecated Use {@link FileMessagePartComponent} and render `audio/*` mime
|
|
55
|
+
* types from the `File` slot.
|
|
56
|
+
*/
|
|
49
57
|
export type Unstable_AudioMessagePartComponent =
|
|
50
58
|
ComponentType<Unstable_AudioMessagePartProps>;
|
|
51
59
|
|
|
@@ -37,6 +37,11 @@ export type MessageStateBinding = SubscribableWithState<
|
|
|
37
37
|
|
|
38
38
|
export type ThreadListItemState = {
|
|
39
39
|
readonly isMain: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether this thread has a run in progress, including a run that continues
|
|
42
|
+
* after the user switches to another thread.
|
|
43
|
+
*/
|
|
44
|
+
readonly isRunning: boolean;
|
|
40
45
|
readonly id: string;
|
|
41
46
|
readonly remoteId: string | undefined;
|
|
42
47
|
readonly externalId: string | undefined;
|
|
@@ -31,7 +31,10 @@ export type ThreadListState = {
|
|
|
31
31
|
readonly isLoadingMore: boolean;
|
|
32
32
|
readonly hasMore: boolean;
|
|
33
33
|
readonly threadItems: Readonly<
|
|
34
|
-
Record<
|
|
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,
|