@assistant-ui/core 0.1.7 → 0.1.9
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/attachment.d.ts +4 -0
- package/dist/adapters/attachment.d.ts.map +1 -1
- package/dist/adapters/attachment.js +1 -1
- package/dist/adapters/attachment.js.map +1 -1
- 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/Interactables.d.ts +3 -0
- package/dist/react/client/Interactables.d.ts.map +1 -0
- package/dist/react/client/Interactables.js +173 -0
- package/dist/react/client/Interactables.js.map +1 -0
- package/dist/react/client/Tools.js +5 -6
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/model-context/makeInteractable.d.ts +10 -0
- package/dist/react/model-context/makeInteractable.d.ts.map +1 -0
- package/dist/react/model-context/makeInteractable.js +10 -0
- package/dist/react/model-context/makeInteractable.js.map +1 -0
- package/dist/react/model-context/useInteractable.d.ts +16 -0
- package/dist/react/model-context/useInteractable.d.ts.map +1 -0
- package/dist/react/model-context/useInteractable.js +36 -0
- package/dist/react/model-context/useInteractable.js.map +1 -0
- 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/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +2 -0
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- 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/react/types/scopes/interactables.d.ts +39 -0
- package/dist/react/types/scopes/interactables.d.ts.map +1 -0
- package/dist/react/types/scopes/interactables.js +2 -0
- package/dist/react/types/scopes/interactables.js.map +1 -0
- package/dist/react/types/store-augmentation.d.ts +2 -0
- package/dist/react/types/store-augmentation.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +33 -8
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
- package/dist/runtime/interfaces/composer-runtime-core.d.ts.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 +16 -5
- 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 +25 -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 +11 -11
- package/src/adapters/attachment.ts +1 -1
- 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/Interactables.ts +233 -0
- package/src/react/client/Tools.ts +5 -6
- package/src/react/index.ts +24 -0
- package/src/react/model-context/makeInteractable.ts +21 -0
- package/src/react/model-context/useInteractable.ts +73 -0
- package/src/react/primitive-hooks/useComposerSend.ts +11 -4
- package/src/react/primitives/composer/ComposerQueue.tsx +58 -0
- package/src/react/primitives/message/MessageParts.tsx +2 -0
- package/src/react/providers/QueueItemByIndexProvider.tsx +21 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +1 -0
- package/src/react/types/scopes/interactables.ts +44 -0
- package/src/react/types/store-augmentation.ts +2 -0
- package/src/runtime/base/base-composer-runtime-core.ts +45 -9
- package/src/runtime/interfaces/composer-runtime-core.ts +4 -1
- 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 +22 -7
- package/src/store/scope-registration.ts +2 -0
- package/src/store/scopes/composer.ts +26 -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
|
@@ -65,6 +65,7 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
65
65
|
dictation: false,
|
|
66
66
|
attachments: false,
|
|
67
67
|
feedback: false,
|
|
68
|
+
queue: false,
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
public get capabilities() {
|
|
@@ -139,6 +140,7 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
139
140
|
unstable_copy: this._store.unstable_capabilities?.copy !== false,
|
|
140
141
|
attachments: !!this._store.adapters?.attachments,
|
|
141
142
|
feedback: !!this._store.adapters?.feedback,
|
|
143
|
+
queue: false,
|
|
142
144
|
};
|
|
143
145
|
if (!shallowEqual(this._capabilities, newCapabilities)) {
|
|
144
146
|
this._capabilities = newCapabilities;
|
|
@@ -30,6 +30,17 @@ const pipeTransforms = <TState, TExtra>(
|
|
|
30
30
|
export class OptimisticState<TState> extends BaseSubscribable {
|
|
31
31
|
private readonly _pendingTransforms: Array<PendingTransform<TState, any>> =
|
|
32
32
|
[];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `optimistic` callbacks from transforms that have already resolved.
|
|
36
|
+
* Re-applied after every `then` callback so that a wholesale state
|
|
37
|
+
* replacement (e.g. list()) cannot erase earlier completed effects
|
|
38
|
+
* (e.g. delete). Cleared when no pending transforms remain.
|
|
39
|
+
*
|
|
40
|
+
* Correctness requirement: `optimistic` callbacks must be idempotent.
|
|
41
|
+
*/
|
|
42
|
+
private readonly _completedOptimistics: Array<(state: TState) => TState> = [];
|
|
43
|
+
|
|
33
44
|
private _baseValue: TState;
|
|
34
45
|
private _cachedValue: TState;
|
|
35
46
|
|
|
@@ -77,12 +88,28 @@ export class OptimisticState<TState> extends BaseSubscribable {
|
|
|
77
88
|
transform.optimistic,
|
|
78
89
|
transform.then,
|
|
79
90
|
]);
|
|
91
|
+
|
|
92
|
+
// Re-apply previously completed optimistic callbacks so that a
|
|
93
|
+
// then() that does wholesale replacement cannot erase their effects.
|
|
94
|
+
for (const fn of this._completedOptimistics) {
|
|
95
|
+
this._baseValue = fn(this._baseValue);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (transform.optimistic) {
|
|
99
|
+
this._completedOptimistics.push(transform.optimistic);
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
return result;
|
|
81
103
|
} finally {
|
|
82
104
|
const index = this._pendingTransforms.indexOf(pendingTransform);
|
|
83
105
|
if (index > -1) {
|
|
84
106
|
this._pendingTransforms.splice(index, 1);
|
|
85
107
|
}
|
|
108
|
+
|
|
109
|
+
if (this._pendingTransforms.length === 0) {
|
|
110
|
+
this._completedOptimistics.length = 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
this._updateState();
|
|
87
114
|
}
|
|
88
115
|
}
|
|
@@ -17,6 +17,7 @@ export const NoOpComposerClient = resource(
|
|
|
17
17
|
type: type,
|
|
18
18
|
dictation: undefined,
|
|
19
19
|
quote: undefined,
|
|
20
|
+
queue: [],
|
|
20
21
|
};
|
|
21
22
|
}, [type]);
|
|
22
23
|
|
|
@@ -61,6 +62,9 @@ export const NoOpComposerClient = resource(
|
|
|
61
62
|
setQuote: () => {
|
|
62
63
|
throw new Error("Not supported");
|
|
63
64
|
},
|
|
65
|
+
queueItem: () => {
|
|
66
|
+
throw new Error("Not supported");
|
|
67
|
+
},
|
|
64
68
|
};
|
|
65
69
|
},
|
|
66
70
|
);
|
|
@@ -30,38 +30,27 @@ export const RuntimeAdapterResource = resource((runtime: AssistantRuntime) => {
|
|
|
30
30
|
export const baseRuntimeAdapterTransformScopes = (
|
|
31
31
|
scopes: ScopesConfig,
|
|
32
32
|
parent: AssistantClient,
|
|
33
|
-
):
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}),
|
|
50
|
-
composer:
|
|
51
|
-
scopes.composer ??
|
|
52
|
-
Derived({
|
|
53
|
-
source: "thread",
|
|
54
|
-
query: {},
|
|
55
|
-
get: (aui) => aui.threads().thread("main").composer(),
|
|
56
|
-
}),
|
|
57
|
-
};
|
|
33
|
+
): void => {
|
|
34
|
+
scopes.thread ??= Derived({
|
|
35
|
+
source: "threads",
|
|
36
|
+
query: { type: "main" },
|
|
37
|
+
get: (aui) => aui.threads().thread("main"),
|
|
38
|
+
});
|
|
39
|
+
scopes.threadListItem ??= Derived({
|
|
40
|
+
source: "threads",
|
|
41
|
+
query: { type: "main" },
|
|
42
|
+
get: (aui) => aui.threads().item("main"),
|
|
43
|
+
});
|
|
44
|
+
scopes.composer ??= Derived({
|
|
45
|
+
source: "thread",
|
|
46
|
+
query: {},
|
|
47
|
+
get: (aui) => aui.threads().thread("main").composer(),
|
|
48
|
+
});
|
|
58
49
|
|
|
59
|
-
if (!
|
|
60
|
-
|
|
50
|
+
if (!scopes.modelContext && parent.modelContext.source === null) {
|
|
51
|
+
scopes.modelContext = ModelContext();
|
|
61
52
|
}
|
|
62
|
-
if (!
|
|
63
|
-
|
|
53
|
+
if (!scopes.suggestions && parent.suggestions.source === null) {
|
|
54
|
+
scopes.suggestions = Suggestions();
|
|
64
55
|
}
|
|
65
|
-
|
|
66
|
-
return result;
|
|
67
56
|
};
|
package/src/store/index.ts
CHANGED
|
@@ -35,10 +35,17 @@ export type {
|
|
|
35
35
|
export type {
|
|
36
36
|
ComposerState,
|
|
37
37
|
ComposerMethods,
|
|
38
|
+
ComposerSendOptions,
|
|
38
39
|
ComposerMeta,
|
|
39
40
|
ComposerEvents,
|
|
40
41
|
ComposerClientSchema,
|
|
41
42
|
} from "./scopes/composer";
|
|
43
|
+
export type {
|
|
44
|
+
QueueItemState,
|
|
45
|
+
QueueItemMethods,
|
|
46
|
+
QueueItemMeta,
|
|
47
|
+
QueueItemClientSchema,
|
|
48
|
+
} from "./scopes/queue-item";
|
|
42
49
|
export type {
|
|
43
50
|
AttachmentState,
|
|
44
51
|
AttachmentMethods,
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
ComposerRuntime,
|
|
17
17
|
EditComposerRuntime,
|
|
18
18
|
} from "../../runtime/api/composer-runtime";
|
|
19
|
-
import { ComposerRuntimeEventType } from "../../runtime/interfaces/composer-runtime-core";
|
|
20
19
|
import { ComposerState } from "../scopes/composer";
|
|
21
20
|
import { AttachmentRuntimeClient } from "./attachment-runtime-client";
|
|
22
21
|
import { tapSubscribable } from "./tap-subscribable";
|
|
@@ -54,12 +53,7 @@ export const ComposerClient = resource(
|
|
|
54
53
|
const unsubscribers: Unsubscribe[] = [];
|
|
55
54
|
|
|
56
55
|
// Subscribe to composer events
|
|
57
|
-
const
|
|
58
|
-
"send",
|
|
59
|
-
"attachmentAdd",
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
for (const event of composerEvents) {
|
|
56
|
+
for (const event of ["send", "attachmentAdd"] as const) {
|
|
63
57
|
const unsubscribe = runtime.unstable_on(event, () => {
|
|
64
58
|
emit(`composer.${event}`, {
|
|
65
59
|
threadId: threadIdRef.current,
|
|
@@ -69,6 +63,23 @@ export const ComposerClient = resource(
|
|
|
69
63
|
unsubscribers.push(unsubscribe);
|
|
70
64
|
}
|
|
71
65
|
|
|
66
|
+
// attachmentAddError carries the failed attachment ID
|
|
67
|
+
unsubscribers.push(
|
|
68
|
+
runtime.unstable_on("attachmentAddError", () => {
|
|
69
|
+
const errorAttachment = runtime
|
|
70
|
+
.getState()
|
|
71
|
+
.attachments.findLast(
|
|
72
|
+
(a) =>
|
|
73
|
+
a.status.type === "incomplete" && a.status.reason === "error",
|
|
74
|
+
);
|
|
75
|
+
emit("composer.attachmentAddError", {
|
|
76
|
+
threadId: threadIdRef.current,
|
|
77
|
+
...(messageIdRef && { messageId: messageIdRef.current }),
|
|
78
|
+
...(errorAttachment && { attachmentId: errorAttachment.id }),
|
|
79
|
+
});
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
72
83
|
return () => {
|
|
73
84
|
for (const unsub of unsubscribers) unsub();
|
|
74
85
|
};
|
|
@@ -101,6 +112,7 @@ export const ComposerClient = resource(
|
|
|
101
112
|
type: runtimeState.type ?? "thread",
|
|
102
113
|
dictation: runtimeState.dictation,
|
|
103
114
|
quote: runtimeState.quote,
|
|
115
|
+
queue: [],
|
|
104
116
|
};
|
|
105
117
|
}, [runtimeState, attachments.state]);
|
|
106
118
|
|
|
@@ -129,6 +141,9 @@ export const ComposerClient = resource(
|
|
|
129
141
|
return attachments.get(selector);
|
|
130
142
|
}
|
|
131
143
|
},
|
|
144
|
+
queueItem: () => {
|
|
145
|
+
throw new Error("Queue is not supported in this runtime");
|
|
146
|
+
},
|
|
132
147
|
__internal_getRuntime: () => runtime,
|
|
133
148
|
};
|
|
134
149
|
},
|
|
@@ -9,6 +9,7 @@ import type { ModelContextClientSchema } from "./scopes/model-context";
|
|
|
9
9
|
import type { SuggestionsClientSchema } from "./scopes/suggestions";
|
|
10
10
|
import type { SuggestionClientSchema } from "./scopes/suggestion";
|
|
11
11
|
import type { ChainOfThoughtClientSchema } from "./scopes/chain-of-thought";
|
|
12
|
+
import type { QueueItemClientSchema } from "./scopes/queue-item";
|
|
12
13
|
|
|
13
14
|
declare module "@assistant-ui/store" {
|
|
14
15
|
interface ScopeRegistry {
|
|
@@ -23,5 +24,6 @@ declare module "@assistant-ui/store" {
|
|
|
23
24
|
suggestions: SuggestionsClientSchema;
|
|
24
25
|
suggestion: SuggestionClientSchema;
|
|
25
26
|
chainOfThought: ChainOfThoughtClientSchema;
|
|
27
|
+
queueItem: QueueItemClientSchema;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -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
|
|
|
@@ -70,6 +90,11 @@ export type ComposerMeta = {
|
|
|
70
90
|
export type ComposerEvents = {
|
|
71
91
|
"composer.send": { threadId: string; messageId?: string };
|
|
72
92
|
"composer.attachmentAdd": { threadId: string; messageId?: string };
|
|
93
|
+
"composer.attachmentAddError": {
|
|
94
|
+
threadId: string;
|
|
95
|
+
messageId?: string;
|
|
96
|
+
attachmentId?: string;
|
|
97
|
+
};
|
|
73
98
|
};
|
|
74
99
|
|
|
75
100
|
export type ComposerClientSchema = {
|
|
@@ -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
|
+
});
|