@assistant-ui/core 0.1.7 → 0.1.8
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/index.d.ts +10 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mention.d.ts +24 -0
- package/dist/adapters/mention.d.ts.map +1 -0
- package/dist/adapters/mention.js +42 -0
- package/dist/adapters/mention.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/react/RuntimeAdapter.js +5 -6
- package/dist/react/RuntimeAdapter.js.map +1 -1
- package/dist/react/client/Tools.js +5 -6
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerSend.d.ts +2 -1
- package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useComposerSend.js +5 -3
- package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
- package/dist/react/primitives/composer/ComposerQueue.d.ts +31 -0
- package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -0
- package/dist/react/primitives/composer/ComposerQueue.js +30 -0
- package/dist/react/primitives/composer/ComposerQueue.js.map +1 -0
- package/dist/react/providers/QueueItemByIndexProvider.d.ts +6 -0
- package/dist/react/providers/QueueItemByIndexProvider.d.ts.map +1 -0
- package/dist/react/providers/QueueItemByIndexProvider.js +13 -0
- package/dist/react/providers/QueueItemByIndexProvider.js.map +1 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +1 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.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/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.d.ts.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/runtimes/remote-thread-list/optimistic-state.d.ts +9 -0
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.js +20 -0
- package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
- package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
- package/dist/store/clients/no-op-composer-client.js +4 -0
- package/dist/store/clients/no-op-composer-client.js.map +1 -1
- package/dist/store/clients/runtime-adapter.d.ts +1 -1
- package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
- package/dist/store/clients/runtime-adapter.js +19 -26
- package/dist/store/clients/runtime-adapter.js.map +1 -1
- package/dist/store/index.d.ts +2 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +4 -0
- package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
- package/dist/store/scope-registration.d.ts +2 -0
- package/dist/store/scope-registration.d.ts.map +1 -1
- package/dist/store/scopes/composer.d.ts +20 -1
- package/dist/store/scopes/composer.d.ts.map +1 -1
- package/dist/store/scopes/queue-item.d.ts +20 -0
- package/dist/store/scopes/queue-item.d.ts.map +1 -0
- package/dist/store/scopes/queue-item.js +2 -0
- package/dist/store/scopes/queue-item.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mention.d.ts +32 -0
- package/dist/types/mention.d.ts.map +1 -0
- package/dist/types/mention.js +2 -0
- package/dist/types/mention.js.map +1 -0
- package/package.json +8 -8
- package/src/adapters/index.ts +34 -0
- package/src/adapters/mention.ts +77 -0
- package/src/index.ts +11 -0
- package/src/react/RuntimeAdapter.ts +5 -7
- package/src/react/client/Tools.ts +5 -6
- package/src/react/index.ts +5 -0
- package/src/react/primitive-hooks/useComposerSend.ts +11 -4
- package/src/react/primitives/composer/ComposerQueue.tsx +58 -0
- package/src/react/providers/QueueItemByIndexProvider.tsx +21 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +1 -0
- package/src/runtime/interfaces/thread-runtime-core.ts +1 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +2 -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/runtimes/remote-thread-list/optimistic-state.ts +27 -0
- package/src/store/clients/no-op-composer-client.ts +4 -0
- package/src/store/clients/runtime-adapter.ts +20 -31
- package/src/store/index.ts +7 -0
- package/src/store/runtime-clients/composer-runtime-client.ts +4 -0
- package/src/store/scope-registration.ts +2 -0
- package/src/store/scopes/composer.ts +21 -1
- package/src/store/scopes/queue-item.ts +20 -0
- package/src/tests/OptimisticState-list-race.test.ts +256 -0
- package/src/tests/mention-formatter.test.ts +112 -0
- package/src/types/index.ts +47 -0
- package/src/types/mention.ts +50 -0
|
@@ -5,6 +5,15 @@ import type { RunConfig } from "../../types/message";
|
|
|
5
5
|
import type { ComposerRuntime } from "../../runtime/api/composer-runtime";
|
|
6
6
|
import type { DictationState } from "../../runtime/interfaces/composer-runtime-core";
|
|
7
7
|
import type { AttachmentMethods } from "./attachment";
|
|
8
|
+
import type { QueueItemState, QueueItemMethods } from "./queue-item";
|
|
9
|
+
|
|
10
|
+
export type ComposerSendOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* Whether to steer (interrupt the current run and process this message immediately).
|
|
13
|
+
* When false (default), the message is queued and processed in order.
|
|
14
|
+
*/
|
|
15
|
+
steer?: boolean;
|
|
16
|
+
};
|
|
8
17
|
|
|
9
18
|
export type ComposerState = {
|
|
10
19
|
readonly text: string;
|
|
@@ -28,6 +37,12 @@ export type ComposerState = {
|
|
|
28
37
|
* Undefined when no quote is set.
|
|
29
38
|
*/
|
|
30
39
|
readonly quote: QuoteInfo | undefined;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The queue of messages waiting to be processed.
|
|
43
|
+
* Empty when no messages are queued.
|
|
44
|
+
*/
|
|
45
|
+
readonly queue: readonly QueueItemState[];
|
|
31
46
|
};
|
|
32
47
|
|
|
33
48
|
export type ComposerMethods = {
|
|
@@ -39,7 +54,7 @@ export type ComposerMethods = {
|
|
|
39
54
|
clearAttachments(): Promise<void>;
|
|
40
55
|
attachment(selector: { index: number } | { id: string }): AttachmentMethods;
|
|
41
56
|
reset(): Promise<void>;
|
|
42
|
-
send(): void;
|
|
57
|
+
send(opts?: ComposerSendOptions): void;
|
|
43
58
|
cancel(): void;
|
|
44
59
|
beginEdit(): void;
|
|
45
60
|
|
|
@@ -59,6 +74,11 @@ export type ComposerMethods = {
|
|
|
59
74
|
*/
|
|
60
75
|
setQuote(quote: QuoteInfo | undefined): void;
|
|
61
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Access a queue item by index.
|
|
79
|
+
*/
|
|
80
|
+
queueItem(selector: { index: number }): QueueItemMethods;
|
|
81
|
+
|
|
62
82
|
__internal_getRuntime?(): ComposerRuntime;
|
|
63
83
|
};
|
|
64
84
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type QueueItemState = {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly prompt: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type QueueItemMethods = {
|
|
7
|
+
getState(): QueueItemState;
|
|
8
|
+
steer(): void;
|
|
9
|
+
remove(): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type QueueItemMeta = {
|
|
13
|
+
source: "composer";
|
|
14
|
+
query: { index: number };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type QueueItemClientSchema = {
|
|
18
|
+
methods: QueueItemMethods;
|
|
19
|
+
meta: QueueItemMeta;
|
|
20
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { OptimisticState } from "../runtimes/remote-thread-list/optimistic-state";
|
|
3
|
+
import {
|
|
4
|
+
type RemoteThreadState,
|
|
5
|
+
type RemoteThreadData,
|
|
6
|
+
type THREAD_MAPPING_ID,
|
|
7
|
+
createThreadMappingId,
|
|
8
|
+
updateStatusReducer,
|
|
9
|
+
} from "../runtimes/remote-thread-list/remote-thread-state";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reproduces the race condition where a stale list() response
|
|
13
|
+
* re-introduces a thread that was deleted/archived while the list
|
|
14
|
+
* was in flight.
|
|
15
|
+
*
|
|
16
|
+
* Root cause (before fix): OptimisticState's `then` callback could
|
|
17
|
+
* overwrite `_baseValue`, erasing effects from transforms that had
|
|
18
|
+
* already completed. The fix re-applies completed `optimistic`
|
|
19
|
+
* callbacks after every `then`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const EMPTY_STATE: RemoteThreadState = {
|
|
23
|
+
isLoading: false,
|
|
24
|
+
newThreadId: undefined,
|
|
25
|
+
threadIds: [],
|
|
26
|
+
archivedThreadIds: [],
|
|
27
|
+
threadIdMap: {},
|
|
28
|
+
threadData: {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type ListResult = {
|
|
32
|
+
threads: {
|
|
33
|
+
remoteId: string;
|
|
34
|
+
status: "regular" | "archived";
|
|
35
|
+
title?: string;
|
|
36
|
+
externalId?: string | undefined;
|
|
37
|
+
}[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Simulates getLoadThreadsPromise's then callback (plain version, no workarounds). */
|
|
41
|
+
const applyListResult = (
|
|
42
|
+
state: RemoteThreadState,
|
|
43
|
+
l: ListResult,
|
|
44
|
+
): RemoteThreadState => {
|
|
45
|
+
const newThreadIds: string[] = [];
|
|
46
|
+
const newArchivedThreadIds: string[] = [];
|
|
47
|
+
const newThreadIdMap = {} as Record<string, THREAD_MAPPING_ID>;
|
|
48
|
+
const newThreadData = {} as Record<THREAD_MAPPING_ID, RemoteThreadData>;
|
|
49
|
+
|
|
50
|
+
for (const thread of l.threads) {
|
|
51
|
+
if (thread.status === "regular") newThreadIds.push(thread.remoteId);
|
|
52
|
+
else newArchivedThreadIds.push(thread.remoteId);
|
|
53
|
+
|
|
54
|
+
const mappingId = createThreadMappingId(thread.remoteId);
|
|
55
|
+
newThreadIdMap[thread.remoteId] = mappingId;
|
|
56
|
+
newThreadData[mappingId] = {
|
|
57
|
+
id: thread.remoteId,
|
|
58
|
+
remoteId: thread.remoteId,
|
|
59
|
+
externalId: thread.externalId,
|
|
60
|
+
status: thread.status,
|
|
61
|
+
title: thread.title,
|
|
62
|
+
initializeTask: Promise.resolve({
|
|
63
|
+
remoteId: thread.remoteId,
|
|
64
|
+
externalId: thread.externalId,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...state,
|
|
71
|
+
threadIds: newThreadIds,
|
|
72
|
+
archivedThreadIds: newArchivedThreadIds,
|
|
73
|
+
threadIdMap: { ...state.threadIdMap, ...newThreadIdMap },
|
|
74
|
+
threadData: { ...state.threadData, ...newThreadData },
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Creates a deferred promise for controlling resolution order. */
|
|
79
|
+
function deferred<T>() {
|
|
80
|
+
let resolve!: (v: T) => void;
|
|
81
|
+
const promise = new Promise<T>((r) => {
|
|
82
|
+
resolve = r;
|
|
83
|
+
});
|
|
84
|
+
return { promise, resolve };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("list + delete race condition", () => {
|
|
88
|
+
it("stale list() does not re-add a thread deleted while list was in flight", async () => {
|
|
89
|
+
const state = new OptimisticState<RemoteThreadState>(EMPTY_STATE);
|
|
90
|
+
|
|
91
|
+
const listDeferred = deferred<ListResult>();
|
|
92
|
+
const deleteDeferred = deferred<void>();
|
|
93
|
+
|
|
94
|
+
// 1. list() starts
|
|
95
|
+
const listPromise = state.optimisticUpdate({
|
|
96
|
+
execute: () => listDeferred.promise,
|
|
97
|
+
loading: (s) => ({ ...s, isLoading: true }),
|
|
98
|
+
then: applyListResult,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 2. delete starts (while list is in flight)
|
|
102
|
+
const deletePromise = state.optimisticUpdate({
|
|
103
|
+
execute: () => deleteDeferred.promise,
|
|
104
|
+
optimistic: (s) => updateStatusReducer(s, "thread-A", "deleted"),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 3. DELETE resolves first
|
|
108
|
+
deleteDeferred.resolve();
|
|
109
|
+
await deletePromise;
|
|
110
|
+
|
|
111
|
+
// 4. list resolves with stale data that includes deleted thread
|
|
112
|
+
listDeferred.resolve({
|
|
113
|
+
threads: [{ remoteId: "thread-A", status: "regular", title: "A" }],
|
|
114
|
+
});
|
|
115
|
+
await listPromise;
|
|
116
|
+
|
|
117
|
+
// Thread A must NOT reappear
|
|
118
|
+
expect(state.value.threadIds).not.toContain("thread-A");
|
|
119
|
+
expect(
|
|
120
|
+
state.value.threadData[createThreadMappingId("thread-A")],
|
|
121
|
+
).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("stale list() does not revert archive back to regular", async () => {
|
|
125
|
+
// Start with thread A already in state
|
|
126
|
+
const mappingId = createThreadMappingId("thread-A");
|
|
127
|
+
const initial: RemoteThreadState = {
|
|
128
|
+
...EMPTY_STATE,
|
|
129
|
+
threadIds: ["thread-A"],
|
|
130
|
+
threadIdMap: { "thread-A": mappingId },
|
|
131
|
+
threadData: {
|
|
132
|
+
[mappingId]: {
|
|
133
|
+
id: "thread-A",
|
|
134
|
+
remoteId: "thread-A",
|
|
135
|
+
externalId: undefined,
|
|
136
|
+
status: "regular",
|
|
137
|
+
title: "A",
|
|
138
|
+
initializeTask: Promise.resolve({
|
|
139
|
+
remoteId: "thread-A",
|
|
140
|
+
externalId: undefined,
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const state = new OptimisticState<RemoteThreadState>(initial);
|
|
147
|
+
|
|
148
|
+
const listDeferred = deferred<ListResult>();
|
|
149
|
+
const archiveDeferred = deferred<void>();
|
|
150
|
+
|
|
151
|
+
// 1. list() starts
|
|
152
|
+
const listPromise = state.optimisticUpdate({
|
|
153
|
+
execute: () => listDeferred.promise,
|
|
154
|
+
loading: (s) => ({ ...s, isLoading: true }),
|
|
155
|
+
then: applyListResult,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 2. archive starts
|
|
159
|
+
const archivePromise = state.optimisticUpdate({
|
|
160
|
+
execute: () => archiveDeferred.promise,
|
|
161
|
+
optimistic: (s) => updateStatusReducer(s, "thread-A", "archived"),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// 3. archive resolves first
|
|
165
|
+
archiveDeferred.resolve();
|
|
166
|
+
await archivePromise;
|
|
167
|
+
|
|
168
|
+
// 4. list resolves with stale data (thread-A as regular)
|
|
169
|
+
listDeferred.resolve({
|
|
170
|
+
threads: [{ remoteId: "thread-A", status: "regular", title: "A" }],
|
|
171
|
+
});
|
|
172
|
+
await listPromise;
|
|
173
|
+
|
|
174
|
+
// Thread A should remain archived, NOT be reverted to regular
|
|
175
|
+
expect(state.value.threadIds).not.toContain("thread-A");
|
|
176
|
+
expect(state.value.archivedThreadIds).toContain("thread-A");
|
|
177
|
+
expect(
|
|
178
|
+
state.value.threadData[createThreadMappingId("thread-A")]?.status,
|
|
179
|
+
).toBe("archived");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("threads NOT modified during list() are loaded normally", async () => {
|
|
183
|
+
const state = new OptimisticState<RemoteThreadState>(EMPTY_STATE);
|
|
184
|
+
|
|
185
|
+
const listDeferred = deferred<ListResult>();
|
|
186
|
+
const deleteDeferred = deferred<void>();
|
|
187
|
+
|
|
188
|
+
// 1. list() starts
|
|
189
|
+
const listPromise = state.optimisticUpdate({
|
|
190
|
+
execute: () => listDeferred.promise,
|
|
191
|
+
loading: (s) => ({ ...s, isLoading: true }),
|
|
192
|
+
then: applyListResult,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// 2. delete thread A (while list is in flight)
|
|
196
|
+
const deletePromise = state.optimisticUpdate({
|
|
197
|
+
execute: () => deleteDeferred.promise,
|
|
198
|
+
optimistic: (s) => updateStatusReducer(s, "thread-A", "deleted"),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
deleteDeferred.resolve();
|
|
202
|
+
await deletePromise;
|
|
203
|
+
|
|
204
|
+
// 3. list resolves with [A, B] — A should be filtered, B should load
|
|
205
|
+
listDeferred.resolve({
|
|
206
|
+
threads: [
|
|
207
|
+
{ remoteId: "thread-A", status: "regular", title: "A" },
|
|
208
|
+
{ remoteId: "thread-B", status: "regular", title: "B" },
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
await listPromise;
|
|
212
|
+
|
|
213
|
+
expect(state.value.threadIds).toEqual(["thread-B"]);
|
|
214
|
+
expect(state.value.threadIds).not.toContain("thread-A");
|
|
215
|
+
expect(
|
|
216
|
+
state.value.threadData[createThreadMappingId("thread-B")],
|
|
217
|
+
).toBeDefined();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("list resolves before delete — no race, both work correctly", async () => {
|
|
221
|
+
const state = new OptimisticState<RemoteThreadState>(EMPTY_STATE);
|
|
222
|
+
|
|
223
|
+
const listDeferred = deferred<ListResult>();
|
|
224
|
+
const deleteDeferred = deferred<void>();
|
|
225
|
+
|
|
226
|
+
const listPromise = state.optimisticUpdate({
|
|
227
|
+
execute: () => listDeferred.promise,
|
|
228
|
+
loading: (s) => ({ ...s, isLoading: true }),
|
|
229
|
+
then: applyListResult,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const deletePromise = state.optimisticUpdate({
|
|
233
|
+
execute: () => deleteDeferred.promise,
|
|
234
|
+
optimistic: (s) => updateStatusReducer(s, "thread-A", "deleted"),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// list resolves FIRST this time
|
|
238
|
+
listDeferred.resolve({
|
|
239
|
+
threads: [{ remoteId: "thread-A", status: "regular", title: "A" }],
|
|
240
|
+
});
|
|
241
|
+
await listPromise;
|
|
242
|
+
|
|
243
|
+
// thread A is in base state now, but delete's optimistic still hides it
|
|
244
|
+
expect(state.value.threadIds).not.toContain("thread-A");
|
|
245
|
+
|
|
246
|
+
// delete resolves
|
|
247
|
+
deleteDeferred.resolve();
|
|
248
|
+
await deletePromise;
|
|
249
|
+
|
|
250
|
+
// thread A is gone from both base and cached
|
|
251
|
+
expect(state.value.threadIds).not.toContain("thread-A");
|
|
252
|
+
expect(
|
|
253
|
+
state.value.threadData[createThreadMappingId("thread-A")],
|
|
254
|
+
).toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { unstable_defaultDirectiveFormatter } from "../adapters/mention";
|
|
3
|
+
|
|
4
|
+
describe("unstable_defaultDirectiveFormatter", () => {
|
|
5
|
+
describe("serialize", () => {
|
|
6
|
+
it("serializes with id === label (no name attr)", () => {
|
|
7
|
+
expect(
|
|
8
|
+
unstable_defaultDirectiveFormatter.serialize({
|
|
9
|
+
id: "weather",
|
|
10
|
+
type: "tool",
|
|
11
|
+
label: "weather",
|
|
12
|
+
}),
|
|
13
|
+
).toBe(":tool[weather]");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("serializes with id !== label (includes name attr)", () => {
|
|
17
|
+
expect(
|
|
18
|
+
unstable_defaultDirectiveFormatter.serialize({
|
|
19
|
+
id: "get_weather",
|
|
20
|
+
type: "tool",
|
|
21
|
+
label: "Weather",
|
|
22
|
+
}),
|
|
23
|
+
).toBe(":tool[Weather]{name=get_weather}");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("parse", () => {
|
|
28
|
+
it("parses plain text", () => {
|
|
29
|
+
expect(unstable_defaultDirectiveFormatter.parse("hello world")).toEqual([
|
|
30
|
+
{ kind: "text", text: "hello world" },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("parses a single directive without name attr", () => {
|
|
35
|
+
expect(
|
|
36
|
+
unstable_defaultDirectiveFormatter.parse("use :tool[weather] please"),
|
|
37
|
+
).toEqual([
|
|
38
|
+
{ kind: "text", text: "use " },
|
|
39
|
+
{ kind: "mention", type: "tool", label: "weather", id: "weather" },
|
|
40
|
+
{ kind: "text", text: " please" },
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("parses a directive with name attr", () => {
|
|
45
|
+
expect(
|
|
46
|
+
unstable_defaultDirectiveFormatter.parse(
|
|
47
|
+
":tool[Weather]{name=get_weather}",
|
|
48
|
+
),
|
|
49
|
+
).toEqual([
|
|
50
|
+
{
|
|
51
|
+
kind: "mention",
|
|
52
|
+
type: "tool",
|
|
53
|
+
label: "Weather",
|
|
54
|
+
id: "get_weather",
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("parses multiple directives in text", () => {
|
|
60
|
+
const result = unstable_defaultDirectiveFormatter.parse(
|
|
61
|
+
":tool[a] and :tool[b]{name=bb}",
|
|
62
|
+
);
|
|
63
|
+
expect(result).toHaveLength(3);
|
|
64
|
+
expect(result[0]).toEqual({
|
|
65
|
+
kind: "mention",
|
|
66
|
+
type: "tool",
|
|
67
|
+
label: "a",
|
|
68
|
+
id: "a",
|
|
69
|
+
});
|
|
70
|
+
expect(result[1]).toEqual({ kind: "text", text: " and " });
|
|
71
|
+
expect(result[2]).toEqual({
|
|
72
|
+
kind: "mention",
|
|
73
|
+
type: "tool",
|
|
74
|
+
label: "b",
|
|
75
|
+
id: "bb",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("parses hyphenated types", () => {
|
|
80
|
+
expect(
|
|
81
|
+
unstable_defaultDirectiveFormatter.parse(
|
|
82
|
+
":data-source[My DB]{name=db_1}",
|
|
83
|
+
),
|
|
84
|
+
).toEqual([
|
|
85
|
+
{
|
|
86
|
+
kind: "mention",
|
|
87
|
+
type: "data-source",
|
|
88
|
+
label: "My DB",
|
|
89
|
+
id: "db_1",
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("roundtrips serialize → parse", () => {
|
|
95
|
+
const item = {
|
|
96
|
+
id: "get_weather",
|
|
97
|
+
type: "tool",
|
|
98
|
+
label: "Weather",
|
|
99
|
+
};
|
|
100
|
+
const serialized = unstable_defaultDirectiveFormatter.serialize(item);
|
|
101
|
+
const parsed = unstable_defaultDirectiveFormatter.parse(serialized);
|
|
102
|
+
expect(parsed).toEqual([
|
|
103
|
+
{
|
|
104
|
+
kind: "mention",
|
|
105
|
+
type: "tool",
|
|
106
|
+
label: "Weather",
|
|
107
|
+
id: "get_weather",
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
// Message parts
|
|
3
|
+
TextMessagePart,
|
|
4
|
+
ReasoningMessagePart,
|
|
5
|
+
SourceMessagePart,
|
|
6
|
+
ImageMessagePart,
|
|
7
|
+
FileMessagePart,
|
|
8
|
+
DataMessagePart,
|
|
9
|
+
Unstable_AudioMessagePart,
|
|
10
|
+
ToolCallMessagePart,
|
|
11
|
+
ThreadUserMessagePart,
|
|
12
|
+
ThreadAssistantMessagePart,
|
|
13
|
+
// Message status
|
|
14
|
+
MessagePartStatus,
|
|
15
|
+
ToolCallMessagePartStatus,
|
|
16
|
+
MessageStatus,
|
|
17
|
+
// Thread messages
|
|
18
|
+
MessageTiming,
|
|
19
|
+
ThreadStep,
|
|
20
|
+
ThreadSystemMessage,
|
|
21
|
+
ThreadUserMessage,
|
|
22
|
+
ThreadAssistantMessage,
|
|
23
|
+
ThreadMessage,
|
|
24
|
+
MessageRole,
|
|
25
|
+
// Config
|
|
26
|
+
RunConfig,
|
|
27
|
+
AppendMessage,
|
|
28
|
+
} from "./message";
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
Attachment,
|
|
32
|
+
PendingAttachment,
|
|
33
|
+
CompleteAttachment,
|
|
34
|
+
AttachmentStatus,
|
|
35
|
+
CreateAttachment,
|
|
36
|
+
} from "./attachment";
|
|
37
|
+
|
|
38
|
+
export type { Unsubscribe } from "./unsubscribe";
|
|
39
|
+
|
|
40
|
+
export type { QuoteInfo } from "./quote";
|
|
41
|
+
|
|
42
|
+
export type {
|
|
43
|
+
Unstable_MentionItem,
|
|
44
|
+
Unstable_MentionCategory,
|
|
45
|
+
Unstable_DirectiveSegment,
|
|
46
|
+
Unstable_DirectiveFormatter,
|
|
47
|
+
} from "./mention";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Mention Item (user-facing definition for items that can be @-mentioned)
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
export type Unstable_MentionItem = {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly icon?: string | undefined;
|
|
12
|
+
readonly description?: string | undefined;
|
|
13
|
+
readonly metadata?: ReadonlyJSONObject | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Mention Category (for hierarchical navigation)
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
export type Unstable_MentionCategory = {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly icon?: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Directive Segment (parsed representation of mention directives in text)
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
/** Parsed segment from directive text */
|
|
31
|
+
export type Unstable_DirectiveSegment =
|
|
32
|
+
| { readonly kind: "text"; readonly text: string }
|
|
33
|
+
| {
|
|
34
|
+
readonly kind: "mention";
|
|
35
|
+
readonly type: string;
|
|
36
|
+
readonly label: string;
|
|
37
|
+
readonly id: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Directive Formatter (configurable serialization/parsing for mentions)
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
/** Configurable formatter for mention directive serialization/parsing */
|
|
45
|
+
export type Unstable_DirectiveFormatter = {
|
|
46
|
+
/** Serialize a mention item to directive text */
|
|
47
|
+
serialize(item: Unstable_MentionItem): string;
|
|
48
|
+
/** Parse text into alternating text and mention segments */
|
|
49
|
+
parse(text: string): readonly Unstable_DirectiveSegment[];
|
|
50
|
+
};
|