@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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import { cleanup, renderHook } from "@testing-library/react";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => {
|
|
6
|
+
const append = vi.fn();
|
|
7
|
+
const setText = vi.fn();
|
|
8
|
+
|
|
9
|
+
const state = {
|
|
10
|
+
thread: {
|
|
11
|
+
isDisabled: false,
|
|
12
|
+
isRunning: false,
|
|
13
|
+
capabilities: { queue: false },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
const composerState = {
|
|
17
|
+
text: "",
|
|
18
|
+
runConfig: { custom: { model: "gpt-test" } },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
append,
|
|
23
|
+
setText,
|
|
24
|
+
state,
|
|
25
|
+
composerState,
|
|
26
|
+
aui: {
|
|
27
|
+
thread: {
|
|
28
|
+
getState: () => state.thread,
|
|
29
|
+
append,
|
|
30
|
+
},
|
|
31
|
+
composer: {
|
|
32
|
+
getState: () => composerState,
|
|
33
|
+
setText,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
40
|
+
...(await importOriginal<typeof import("@assistant-ui/store")>()),
|
|
41
|
+
useAui: () => mocks.aui,
|
|
42
|
+
useAuiState: ((selector: (state: typeof mocks.state) => unknown) =>
|
|
43
|
+
selector(mocks.state)) as typeof import("@assistant-ui/store").useAuiState,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
import { useSuggestionTrigger } from "./useSuggestionTrigger";
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
cleanup();
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
mocks.state.thread.isDisabled = false;
|
|
52
|
+
mocks.state.thread.isRunning = false;
|
|
53
|
+
mocks.state.thread.capabilities = { queue: false };
|
|
54
|
+
mocks.composerState.text = "";
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("useSuggestionTrigger", () => {
|
|
58
|
+
it("appends the prompt and clears the composer when sending while idle", () => {
|
|
59
|
+
const { result } = renderHook(() =>
|
|
60
|
+
useSuggestionTrigger({ prompt: "Hello", send: true }),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
result.current.trigger();
|
|
64
|
+
|
|
65
|
+
expect(result.current.disabled).toBe(false);
|
|
66
|
+
expect(mocks.append).toHaveBeenCalledWith({
|
|
67
|
+
content: [{ type: "text", text: "Hello" }],
|
|
68
|
+
runConfig: { custom: { model: "gpt-test" } },
|
|
69
|
+
});
|
|
70
|
+
expect(mocks.setText).toHaveBeenCalledWith("");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("disables and no-ops when sending while running without queue support", () => {
|
|
74
|
+
mocks.state.thread.isRunning = true;
|
|
75
|
+
mocks.composerState.text = "my draft";
|
|
76
|
+
const { result } = renderHook(() =>
|
|
77
|
+
useSuggestionTrigger({ prompt: "Hello", send: true }),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
result.current.trigger();
|
|
81
|
+
|
|
82
|
+
expect(result.current.disabled).toBe(true);
|
|
83
|
+
expect(mocks.append).not.toHaveBeenCalled();
|
|
84
|
+
expect(mocks.setText).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("queues without touching the composer while running when the thread supports queueing", () => {
|
|
88
|
+
mocks.state.thread.isRunning = true;
|
|
89
|
+
mocks.state.thread.capabilities = { queue: true };
|
|
90
|
+
mocks.composerState.text = "my draft";
|
|
91
|
+
const { result } = renderHook(() =>
|
|
92
|
+
useSuggestionTrigger({ prompt: "Hello", send: true }),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
result.current.trigger();
|
|
96
|
+
|
|
97
|
+
expect(result.current.disabled).toBe(false);
|
|
98
|
+
expect(mocks.append).toHaveBeenCalledWith({
|
|
99
|
+
content: [{ type: "text", text: "Hello" }],
|
|
100
|
+
runConfig: { custom: { model: "gpt-test" } },
|
|
101
|
+
});
|
|
102
|
+
expect(mocks.setText).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("replaces the composer text when send is false, even while running", () => {
|
|
106
|
+
mocks.state.thread.isRunning = true;
|
|
107
|
+
mocks.composerState.text = "my draft";
|
|
108
|
+
const { result } = renderHook(() =>
|
|
109
|
+
useSuggestionTrigger({ prompt: "Hello" }),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
result.current.trigger();
|
|
113
|
+
|
|
114
|
+
expect(result.current.disabled).toBe(false);
|
|
115
|
+
expect(mocks.append).not.toHaveBeenCalled();
|
|
116
|
+
expect(mocks.setText).toHaveBeenCalledWith("Hello");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("appends to the composer text when clearComposer is false", () => {
|
|
120
|
+
mocks.composerState.text = "my draft";
|
|
121
|
+
const { result } = renderHook(() =>
|
|
122
|
+
useSuggestionTrigger({ prompt: "Hello", clearComposer: false }),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
result.current.trigger();
|
|
126
|
+
|
|
127
|
+
expect(mocks.setText).toHaveBeenCalledWith("my draft Hello");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("inserts the prompt alone when the composer is empty and clearComposer is false", () => {
|
|
131
|
+
const { result } = renderHook(() =>
|
|
132
|
+
useSuggestionTrigger({ prompt: "Hello", clearComposer: false }),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
result.current.trigger();
|
|
136
|
+
|
|
137
|
+
expect(mocks.setText).toHaveBeenCalledWith("Hello");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("disables when the thread is disabled", () => {
|
|
141
|
+
mocks.state.thread.isDisabled = true;
|
|
142
|
+
const { result } = renderHook(() =>
|
|
143
|
+
useSuggestionTrigger({ prompt: "Hello" }),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(result.current.disabled).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -13,18 +13,24 @@ export const useSuggestionTrigger = ({
|
|
|
13
13
|
clearComposer = true,
|
|
14
14
|
}: UseSuggestionTriggerOptions) => {
|
|
15
15
|
const aui = useAui();
|
|
16
|
-
const disabled = useAuiState((s) => s.thread.isDisabled);
|
|
17
16
|
const resolvedSend = send ?? false;
|
|
17
|
+
const disabled = useAuiState(
|
|
18
|
+
(s) =>
|
|
19
|
+
s.thread.isDisabled ||
|
|
20
|
+
(resolvedSend && s.thread.isRunning && !s.thread.capabilities.queue),
|
|
21
|
+
);
|
|
18
22
|
|
|
19
23
|
const trigger = useCallback(() => {
|
|
20
|
-
|
|
24
|
+
if (resolvedSend) {
|
|
25
|
+
const { isRunning, capabilities } = aui.thread.getState();
|
|
26
|
+
if (isRunning && !capabilities.queue) return;
|
|
21
27
|
|
|
22
|
-
if (resolvedSend && !isRunning) {
|
|
23
28
|
aui.thread.append({
|
|
24
29
|
content: [{ type: "text", text: prompt }],
|
|
25
30
|
runConfig: aui.composer.getState().runConfig,
|
|
26
31
|
});
|
|
27
|
-
|
|
32
|
+
// A queued send must not clear the draft the user is still composing.
|
|
33
|
+
if (clearComposer && !isRunning) {
|
|
28
34
|
aui.composer.setText("");
|
|
29
35
|
}
|
|
30
36
|
} else {
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
MessagePartStatus,
|
|
9
9
|
ToolCallMessagePartStatus,
|
|
10
10
|
} from "../../../types/message";
|
|
11
|
+
import { getGroupStatus } from "../../../utils/getGroupStatus";
|
|
11
12
|
import {
|
|
12
13
|
buildGroupTree,
|
|
13
14
|
GROUPBY_MEMO_KEY,
|
|
@@ -21,7 +22,8 @@ export namespace MessagePrimitiveGroupedParts {
|
|
|
21
22
|
* A coalesced group of adjacent parts. Surfaced through the same
|
|
22
23
|
* `{ part }` channel as a leaf {@link EnrichedPartState} so consumers
|
|
23
24
|
* dispatch on a single `switch (part.type)`. `type` is the group key
|
|
24
|
-
* (always `"group-…"`); `status`
|
|
25
|
+
* (always `"group-…"`); `status` is running when any contained part runs,
|
|
26
|
+
* otherwise it mirrors the last contained part.
|
|
25
27
|
*/
|
|
26
28
|
export type GroupPart<TKey extends `group-${string}` = `group-${string}`> = {
|
|
27
29
|
readonly type: TKey;
|
|
@@ -133,8 +135,6 @@ export namespace MessagePrimitiveGroupedParts {
|
|
|
133
135
|
};
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({ type: "complete" });
|
|
137
|
-
|
|
138
138
|
const shouldShowIndicator = (
|
|
139
139
|
mode: MessagePrimitiveGroupedParts.IndicatorMode,
|
|
140
140
|
parts: readonly PartState[],
|
|
@@ -194,7 +194,7 @@ const renderNode = <TKey extends `group-${string}`>(
|
|
|
194
194
|
);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
const status = parts
|
|
197
|
+
const status = getGroupStatus(parts, node.indices);
|
|
198
198
|
const groupPart: MessagePrimitiveGroupedParts.GroupPart<TKey> = {
|
|
199
199
|
type: node.key as TKey,
|
|
200
200
|
status,
|
|
@@ -214,7 +214,12 @@ export namespace MessagePrimitiveParts {
|
|
|
214
214
|
Image?: ImageMessagePartComponent | undefined;
|
|
215
215
|
/** Component for rendering file content */
|
|
216
216
|
File?: FileMessagePartComponent | undefined;
|
|
217
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* Component for rendering audio content.
|
|
219
|
+
*
|
|
220
|
+
* @deprecated Render audio through the `File` slot instead, branching on an
|
|
221
|
+
* `audio/*` mime type.
|
|
222
|
+
*/
|
|
218
223
|
Unstable_Audio?: Unstable_AudioMessagePartComponent | undefined;
|
|
219
224
|
/** Configuration for data part rendering */
|
|
220
225
|
data?: DataConfig | undefined;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { ThreadListRuntimeCore } from "../../runtime/interfaces/thread-list-runtime-core";
|
|
3
|
+
import type { ThreadRuntimeCore } from "../../runtime/interfaces/thread-runtime-core";
|
|
4
|
+
import type { ThreadMessage } from "../../types/message";
|
|
5
|
+
import { RemoteThreadListHookInstanceManager } from "./RemoteThreadListHookInstanceManager";
|
|
6
|
+
|
|
7
|
+
const makeRuntime = (
|
|
8
|
+
initial: Partial<Pick<ThreadRuntimeCore, "isRunning" | "messages">> = {},
|
|
9
|
+
) => {
|
|
10
|
+
const subscribers = new Set<() => void>();
|
|
11
|
+
const runtime = {
|
|
12
|
+
isRunning: initial.isRunning,
|
|
13
|
+
messages: initial.messages ?? [],
|
|
14
|
+
subscribe: (callback: () => void) => {
|
|
15
|
+
subscribers.add(callback);
|
|
16
|
+
return () => subscribers.delete(callback);
|
|
17
|
+
},
|
|
18
|
+
} as unknown as ThreadRuntimeCore & {
|
|
19
|
+
isRunning: boolean | undefined;
|
|
20
|
+
messages: readonly ThreadMessage[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
runtime,
|
|
25
|
+
subscriberCount: () => subscribers.size,
|
|
26
|
+
setRunning: (isRunning: boolean | undefined) => {
|
|
27
|
+
runtime.isRunning = isRunning;
|
|
28
|
+
for (const callback of subscribers) callback();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const makeManager = () =>
|
|
34
|
+
new RemoteThreadListHookInstanceManager(
|
|
35
|
+
() => ({}) as never,
|
|
36
|
+
{} as ThreadListRuntimeCore,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// mirrors what the React binder does on every publication
|
|
40
|
+
const publish = (
|
|
41
|
+
manager: RemoteThreadListHookInstanceManager,
|
|
42
|
+
threadId: string,
|
|
43
|
+
runtime: ThreadRuntimeCore,
|
|
44
|
+
) => {
|
|
45
|
+
const internals = manager as unknown as {
|
|
46
|
+
instances: Map<string, { generation: number }>;
|
|
47
|
+
_publishThreadRuntime: (
|
|
48
|
+
threadId: string,
|
|
49
|
+
runtime: ThreadRuntimeCore,
|
|
50
|
+
generation: number,
|
|
51
|
+
) => void;
|
|
52
|
+
};
|
|
53
|
+
const generation = internals.instances.get(threadId)!.generation;
|
|
54
|
+
internals._publishThreadRuntime(threadId, runtime, generation);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const start = (manager: RemoteThreadListHookInstanceManager, id: string) => {
|
|
58
|
+
manager.startThreadRuntime(id).catch(() => {});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe("RemoteThreadListHookInstanceManager run tracking", () => {
|
|
62
|
+
it("reports a thread with no attached runtime as not running", () => {
|
|
63
|
+
const manager = makeManager();
|
|
64
|
+
start(manager, "thread-1");
|
|
65
|
+
|
|
66
|
+
expect(manager.__internal_isThreadRunning("thread-1")).toBe(false);
|
|
67
|
+
expect(manager.__internal_isThreadRunning("never-started")).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("adopts the run state of the published runtime", () => {
|
|
71
|
+
const manager = makeManager();
|
|
72
|
+
start(manager, "thread-1");
|
|
73
|
+
const { runtime } = makeRuntime({ isRunning: true });
|
|
74
|
+
|
|
75
|
+
publish(manager, "thread-1", runtime);
|
|
76
|
+
|
|
77
|
+
expect(manager.__internal_isThreadRunning("thread-1")).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("falls back to the trailing assistant message when the runtime does not track runs", () => {
|
|
81
|
+
const manager = makeManager();
|
|
82
|
+
start(manager, "thread-1");
|
|
83
|
+
const { runtime } = makeRuntime({
|
|
84
|
+
messages: [
|
|
85
|
+
{ role: "assistant", status: { type: "running" } },
|
|
86
|
+
] as unknown as readonly ThreadMessage[],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
publish(manager, "thread-1", runtime);
|
|
90
|
+
|
|
91
|
+
expect(manager.__internal_isThreadRunning("thread-1")).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("tracks a thread the user has switched away from", () => {
|
|
95
|
+
const manager = makeManager();
|
|
96
|
+
start(manager, "background");
|
|
97
|
+
start(manager, "main");
|
|
98
|
+
const background = makeRuntime({ isRunning: false });
|
|
99
|
+
publish(manager, "background", background.runtime);
|
|
100
|
+
publish(manager, "main", makeRuntime({ isRunning: false }).runtime);
|
|
101
|
+
|
|
102
|
+
background.setRunning(true);
|
|
103
|
+
|
|
104
|
+
expect(manager.__internal_isThreadRunning("background")).toBe(true);
|
|
105
|
+
expect(manager.__internal_isThreadRunning("main")).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("notifies the thread list only when the thread crosses the running boundary", () => {
|
|
109
|
+
const manager = makeManager();
|
|
110
|
+
start(manager, "thread-1");
|
|
111
|
+
const thread = makeRuntime({ isRunning: false });
|
|
112
|
+
publish(manager, "thread-1", thread.runtime);
|
|
113
|
+
|
|
114
|
+
const onChange = vi.fn();
|
|
115
|
+
manager.__internal_subscribeRunningChanged(onChange);
|
|
116
|
+
|
|
117
|
+
thread.setRunning(false);
|
|
118
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
119
|
+
|
|
120
|
+
thread.setRunning(true);
|
|
121
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
122
|
+
|
|
123
|
+
thread.setRunning(true);
|
|
124
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
125
|
+
|
|
126
|
+
thread.setRunning(false);
|
|
127
|
+
expect(onChange).toHaveBeenCalledTimes(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("moves tracking to the runtime a restart publishes", () => {
|
|
131
|
+
const manager = makeManager();
|
|
132
|
+
start(manager, "thread-1");
|
|
133
|
+
const before = makeRuntime({ isRunning: true });
|
|
134
|
+
publish(manager, "thread-1", before.runtime);
|
|
135
|
+
|
|
136
|
+
const after = makeRuntime({ isRunning: false });
|
|
137
|
+
publish(manager, "thread-1", after.runtime);
|
|
138
|
+
|
|
139
|
+
expect(manager.__internal_isThreadRunning("thread-1")).toBe(false);
|
|
140
|
+
expect(before.subscriberCount()).toBe(0);
|
|
141
|
+
|
|
142
|
+
after.setRunning(true);
|
|
143
|
+
expect(manager.__internal_isThreadRunning("thread-1")).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("releases the run subscription when the thread runtime stops", () => {
|
|
147
|
+
const manager = makeManager();
|
|
148
|
+
start(manager, "thread-1");
|
|
149
|
+
const thread = makeRuntime({ isRunning: true });
|
|
150
|
+
publish(manager, "thread-1", thread.runtime);
|
|
151
|
+
|
|
152
|
+
manager.stopThreadRuntime("thread-1");
|
|
153
|
+
|
|
154
|
+
expect(thread.subscriberCount()).toBe(0);
|
|
155
|
+
expect(manager.__internal_isThreadRunning("thread-1")).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -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
|
+
});
|