@assistant-ui/core 0.1.16 → 0.1.17
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.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +104 -0
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -0
- package/dist/react/primitives/message/MessageGroupedParts.js +74 -0
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -0
- package/dist/react/primitives/message/MessageParts.d.ts +8 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +45 -42
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -2
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +2 -2
- package/dist/react/utils/groupParts.d.ts +49 -0
- package/dist/react/utils/groupParts.d.ts.map +1 -0
- package/dist/react/utils/groupParts.js +97 -0
- package/dist/react/utils/groupParts.js.map +1 -0
- package/dist/runtime/api/composer-runtime.d.ts +3 -3
- package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
- package/dist/runtime/api/composer-runtime.js +1 -1
- package/dist/runtime/api/composer-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts +18 -3
- package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.js +1 -1
- package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +5 -5
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +1 -1
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -3
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +47 -33
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +3 -3
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +11 -11
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/composer-runtime-core.d.ts +28 -2
- package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +37 -2
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +2 -2
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +2 -2
- package/dist/runtimes/local/local-thread-runtime-core.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 +5 -6
- package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
- package/dist/store/scopes/composer.d.ts +11 -1
- package/dist/store/scopes/composer.d.ts.map +1 -1
- package/dist/store/scopes/thread-list-item.d.ts +10 -0
- package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
- package/dist/store/scopes/thread.d.ts +17 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +4 -4
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +4 -4
- package/dist/subscribable/subscribable.js.map +1 -1
- package/package.json +17 -5
- package/src/index.ts +10 -0
- package/src/react/index.ts +2 -0
- package/src/react/primitives/message/MessageGroupedParts.tsx +186 -0
- package/src/react/primitives/message/MessageParts.tsx +80 -55
- package/src/react/utils/groupParts.ts +152 -0
- package/src/runtime/api/composer-runtime.ts +10 -9
- package/src/runtime/api/thread-list-item-runtime.ts +28 -6
- package/src/runtime/api/thread-runtime.ts +11 -7
- package/src/runtime/base/base-composer-runtime-core.ts +85 -42
- package/src/runtime/base/base-thread-runtime-core.ts +21 -12
- package/src/runtime/interfaces/composer-runtime-core.ts +39 -7
- package/src/runtime/interfaces/thread-runtime-core.ts +44 -6
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +2 -2
- package/src/runtimes/local/local-thread-runtime-core.ts +2 -2
- package/src/store/runtime-clients/composer-runtime-client.ts +5 -9
- package/src/store/scopes/composer.ts +11 -0
- package/src/store/scopes/thread-list-item.ts +10 -0
- package/src/store/scopes/thread.ts +17 -0
- package/src/subscribable/subscribable.ts +10 -7
- package/src/tests/base-composer-runtime-core-addAttachment.test.ts +63 -0
- package/src/tests/groupParts.test.ts +114 -0
|
@@ -208,7 +208,7 @@ export namespace MessagePrimitiveParts {
|
|
|
208
208
|
* @param endIndex - Index of the last tool call in the group
|
|
209
209
|
* @param children - Rendered tool call components to display within the group
|
|
210
210
|
*
|
|
211
|
-
* @deprecated
|
|
211
|
+
* @deprecated Use `<MessagePrimitive.GroupedParts>` with a custom `groupBy` instead.
|
|
212
212
|
*/
|
|
213
213
|
ToolGroup?: ComponentType<
|
|
214
214
|
PropsWithChildren<{ startIndex: number; endIndex: number }>
|
|
@@ -220,6 +220,8 @@ export namespace MessagePrimitiveParts {
|
|
|
220
220
|
* @param startIndex - Index of the first reasoning part in the group
|
|
221
221
|
* @param endIndex - Index of the last reasoning part in the group
|
|
222
222
|
* @param children - Rendered reasoning part components
|
|
223
|
+
*
|
|
224
|
+
* @deprecated Use `<MessagePrimitive.GroupedParts>` with a custom `groupBy` instead.
|
|
223
225
|
*/
|
|
224
226
|
ReasoningGroup?: ReasoningGroupComponent;
|
|
225
227
|
|
|
@@ -234,6 +236,11 @@ export namespace MessagePrimitiveParts {
|
|
|
234
236
|
* `ToolGroup` components cannot be used alongside it.
|
|
235
237
|
*/
|
|
236
238
|
type ChainOfThoughtComponents = BaseComponents & {
|
|
239
|
+
/**
|
|
240
|
+
* @deprecated Use `<MessagePrimitive.GroupedParts>` with a `groupBy`
|
|
241
|
+
* that returns `["group-thought", ...]` for reasoning and tool-call
|
|
242
|
+
* parts. See `@assistant-ui/ui` for a worked example.
|
|
243
|
+
*/
|
|
237
244
|
ChainOfThought: ComponentType;
|
|
238
245
|
|
|
239
246
|
Reasoning?: never;
|
|
@@ -604,70 +611,88 @@ const EMPTY_RUNNING_TEXT_PART: Extract<EnrichedPartState, { type: "text" }> =
|
|
|
604
611
|
status: RUNNING_STATUS,
|
|
605
612
|
});
|
|
606
613
|
|
|
614
|
+
/**
|
|
615
|
+
* @internal
|
|
616
|
+
* Renders a single part by index, calling `children` with the
|
|
617
|
+
* {@link EnrichedPartState} (tool/data UI enrichments + addResult/resume
|
|
618
|
+
* for tool calls). Shared between `<MessagePrimitive.Parts>` and
|
|
619
|
+
* `<MessagePrimitive.GroupedParts>`. Returns whatever `children`
|
|
620
|
+
* returns — callers decide how to handle a `null` return.
|
|
621
|
+
*/
|
|
622
|
+
export const MessagePartChildren: FC<{
|
|
623
|
+
index: number;
|
|
624
|
+
children: (value: { part: EnrichedPartState }) => ReactNode;
|
|
625
|
+
}> = ({ index, children }) => {
|
|
626
|
+
const aui = useAui();
|
|
627
|
+
// Subscribed (not snapshotted like `tools`) so fallbacks registered
|
|
628
|
+
// after the first render trigger a re-render and `hasUI` re-evaluates.
|
|
629
|
+
const dataRenderers = useAuiState((s) => s.dataRenderers);
|
|
630
|
+
|
|
631
|
+
return (
|
|
632
|
+
<PartByIndexProvider index={index}>
|
|
633
|
+
<RenderChildrenWithAccessor
|
|
634
|
+
getItemState={(aui) => aui.message().part({ index }).getState()}
|
|
635
|
+
>
|
|
636
|
+
{(getItem) =>
|
|
637
|
+
children({
|
|
638
|
+
get part() {
|
|
639
|
+
const state = getItem();
|
|
640
|
+
if (state.type === "tool-call") {
|
|
641
|
+
const entry = aui.tools().getState().tools[state.toolName];
|
|
642
|
+
const hasUI = Array.isArray(entry) ? !!entry[0] : !!entry;
|
|
643
|
+
const partMethods = aui.message().part({ index });
|
|
644
|
+
return {
|
|
645
|
+
...state,
|
|
646
|
+
toolUI: hasUI ? <RegisteredToolUI /> : null,
|
|
647
|
+
addResult: partMethods.addToolResult,
|
|
648
|
+
resume: partMethods.resumeToolCall,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (state.type === "data") {
|
|
652
|
+
const hasUI =
|
|
653
|
+
getDataRenderer(dataRenderers, state.name, undefined) !==
|
|
654
|
+
undefined;
|
|
655
|
+
return {
|
|
656
|
+
...state,
|
|
657
|
+
dataRendererUI: hasUI ? <RegisteredDataRendererUI /> : null,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return state;
|
|
661
|
+
},
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
</RenderChildrenWithAccessor>
|
|
665
|
+
</PartByIndexProvider>
|
|
666
|
+
);
|
|
667
|
+
};
|
|
668
|
+
|
|
607
669
|
const MessagePrimitivePartsInner: FC<{
|
|
608
670
|
children: (value: { part: EnrichedPartState }) => ReactNode;
|
|
609
671
|
}> = ({ children }) => {
|
|
610
|
-
const aui = useAui();
|
|
611
672
|
const contentLength = useAuiState((s) => s.message.parts.length);
|
|
612
673
|
const isRunning = useAuiState(
|
|
613
674
|
(s) => (s.message.status?.type ?? "complete") === "running",
|
|
614
675
|
);
|
|
615
676
|
const isEmptyRunning = contentLength === 0 && isRunning;
|
|
616
|
-
// Subscribed (not snapshotted like `tools`) so fallbacks registered after
|
|
617
|
-
// the first render trigger a re-render and `hasUI` re-evaluates.
|
|
618
|
-
const dataRenderers = useAuiState((s) => s.dataRenderers);
|
|
619
|
-
|
|
620
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: aui accessors are stable refs
|
|
621
|
-
return useMemo(() => {
|
|
622
|
-
if (contentLength === 0) {
|
|
623
|
-
if (!isEmptyRunning) return null;
|
|
624
677
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
678
|
+
if (contentLength === 0) {
|
|
679
|
+
if (!isEmptyRunning) return null;
|
|
680
|
+
return (
|
|
681
|
+
<TextMessagePartProvider text="" isRunning>
|
|
682
|
+
{children({ part: EMPTY_RUNNING_TEXT_PART })}
|
|
683
|
+
</TextMessagePartProvider>
|
|
684
|
+
);
|
|
685
|
+
}
|
|
631
686
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (state.type === "tool-call") {
|
|
642
|
-
const entry = aui.tools().getState().tools[state.toolName];
|
|
643
|
-
const hasUI = Array.isArray(entry) ? !!entry[0] : !!entry;
|
|
644
|
-
const partMethods = aui.message().part({ index });
|
|
645
|
-
return {
|
|
646
|
-
...state,
|
|
647
|
-
toolUI: hasUI ? <RegisteredToolUI /> : null,
|
|
648
|
-
addResult: partMethods.addToolResult,
|
|
649
|
-
resume: partMethods.resumeToolCall,
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
if (state.type === "data") {
|
|
653
|
-
const hasUI =
|
|
654
|
-
getDataRenderer(dataRenderers, state.name, undefined) !==
|
|
655
|
-
undefined;
|
|
656
|
-
return {
|
|
657
|
-
...state,
|
|
658
|
-
dataRendererUI: hasUI ? <RegisteredDataRendererUI /> : null,
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
return state;
|
|
662
|
-
},
|
|
663
|
-
});
|
|
664
|
-
if (result !== null) return result;
|
|
665
|
-
return <DefaultPartFallback />;
|
|
666
|
-
}}
|
|
667
|
-
</RenderChildrenWithAccessor>
|
|
668
|
-
</PartByIndexProvider>
|
|
669
|
-
));
|
|
670
|
-
}, [contentLength, children, isEmptyRunning, dataRenderers]);
|
|
687
|
+
return (
|
|
688
|
+
<>
|
|
689
|
+
{Array.from({ length: contentLength }, (_, index) => (
|
|
690
|
+
<MessagePartChildren key={index} index={index}>
|
|
691
|
+
{(value) => children(value) ?? <DefaultPartFallback />}
|
|
692
|
+
</MessagePartChildren>
|
|
693
|
+
))}
|
|
694
|
+
</>
|
|
695
|
+
);
|
|
671
696
|
};
|
|
672
697
|
|
|
673
698
|
/**
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hierarchical adjacent-coalescing grouping for message parts.
|
|
3
|
+
*
|
|
4
|
+
* Given a group path per part (from `groupBy`), builds a tree of group
|
|
5
|
+
* nodes wrapping individual parts. Adjacent parts sharing a path prefix
|
|
6
|
+
* coalesce into the same group; ungrouped parts are direct children of
|
|
7
|
+
* the root.
|
|
8
|
+
*
|
|
9
|
+
* Each node gets a structural `nodeKey` built from sibling indices
|
|
10
|
+
* (`"0.1.0"`), stable under append-only streaming.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Public group key type. Group keys must be prefixed with `group-` so
|
|
15
|
+
* that a unified `switch (part.type)` in the renderer can distinguish
|
|
16
|
+
* a group key (e.g. `"group-thought"`) from a real part type
|
|
17
|
+
* (`"text"`, `"tool-call"`).
|
|
18
|
+
*/
|
|
19
|
+
export type GroupKey<TKey extends `group-${string}` = `group-${string}`> =
|
|
20
|
+
| TKey
|
|
21
|
+
| readonly TKey[]
|
|
22
|
+
| null
|
|
23
|
+
| undefined;
|
|
24
|
+
|
|
25
|
+
export type GroupNode = GroupNodeGroup | GroupNodePart;
|
|
26
|
+
|
|
27
|
+
export interface GroupNodeGroup {
|
|
28
|
+
readonly type: "group";
|
|
29
|
+
/** Current-level group key (last segment of the path). */
|
|
30
|
+
readonly key: string;
|
|
31
|
+
/** Structural React key: sibling-index path, e.g. `"0.1.0"`. */
|
|
32
|
+
readonly nodeKey: string;
|
|
33
|
+
/** Indices of parts in this subtree, in order. */
|
|
34
|
+
readonly indices: readonly number[];
|
|
35
|
+
readonly children: readonly GroupNode[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GroupNodePart {
|
|
39
|
+
readonly type: "part";
|
|
40
|
+
/** Index of the part in the message. */
|
|
41
|
+
readonly index: number;
|
|
42
|
+
/** Structural React key: sibling-index path within parent. */
|
|
43
|
+
readonly nodeKey: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const EMPTY_PATH: readonly string[] = Object.freeze([]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalize a `groupBy` return value to a path array.
|
|
50
|
+
* `null`/`undefined`/`[]` → `[]` (ungrouped).
|
|
51
|
+
* `"foo"` → `["foo"]`. Arrays pass through.
|
|
52
|
+
*/
|
|
53
|
+
export const normalizeGroupKey = (key: GroupKey): readonly string[] => {
|
|
54
|
+
if (key == null) return EMPTY_PATH;
|
|
55
|
+
if (typeof key === "string") return [key];
|
|
56
|
+
return key;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
interface BuildFrame {
|
|
60
|
+
key: string;
|
|
61
|
+
nodeKey: string;
|
|
62
|
+
indices: number[];
|
|
63
|
+
children: GroupNode[];
|
|
64
|
+
nextChildIdx: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const makeChildNodeKey = (parent: BuildFrame): string => {
|
|
68
|
+
const idx = parent.nextChildIdx++;
|
|
69
|
+
return parent.nodeKey === "" ? String(idx) : `${parent.nodeKey}.${idx}`;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the group tree from an array of normalized group paths.
|
|
74
|
+
* `paths[i]` is the path for part `i`. The output tree contains one
|
|
75
|
+
* `part` node per part and one `group` node per coalesced run.
|
|
76
|
+
*/
|
|
77
|
+
export const buildGroupTree = (
|
|
78
|
+
paths: readonly (readonly string[])[],
|
|
79
|
+
): readonly GroupNode[] => {
|
|
80
|
+
const root: BuildFrame = {
|
|
81
|
+
key: "",
|
|
82
|
+
nodeKey: "",
|
|
83
|
+
indices: [],
|
|
84
|
+
children: [],
|
|
85
|
+
nextChildIdx: 0,
|
|
86
|
+
};
|
|
87
|
+
const stack: BuildFrame[] = [root];
|
|
88
|
+
|
|
89
|
+
const closeTop = (): void => {
|
|
90
|
+
const closing = stack.pop()!;
|
|
91
|
+
const parent = stack[stack.length - 1]!;
|
|
92
|
+
parent.children.push({
|
|
93
|
+
type: "group",
|
|
94
|
+
key: closing.key,
|
|
95
|
+
nodeKey: closing.nodeKey,
|
|
96
|
+
indices: closing.indices,
|
|
97
|
+
children: closing.children,
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < paths.length; i++) {
|
|
102
|
+
const path = paths[i]!;
|
|
103
|
+
|
|
104
|
+
// Find the longest prefix shared between currently-open groups
|
|
105
|
+
// (excluding root) and this part's path.
|
|
106
|
+
let common = 0;
|
|
107
|
+
while (
|
|
108
|
+
common < stack.length - 1 &&
|
|
109
|
+
common < path.length &&
|
|
110
|
+
stack[common + 1]!.key === path[common]
|
|
111
|
+
) {
|
|
112
|
+
common++;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Close groups not on this path.
|
|
116
|
+
while (stack.length - 1 > common) {
|
|
117
|
+
closeTop();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Open new groups down to the part's depth.
|
|
121
|
+
while (stack.length - 1 < path.length) {
|
|
122
|
+
const parent = stack[stack.length - 1]!;
|
|
123
|
+
stack.push({
|
|
124
|
+
key: path[stack.length - 1]!,
|
|
125
|
+
nodeKey: makeChildNodeKey(parent),
|
|
126
|
+
indices: [],
|
|
127
|
+
children: [],
|
|
128
|
+
nextChildIdx: 0,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Push this part as a leaf in the deepest open group (or root).
|
|
133
|
+
const top = stack[stack.length - 1]!;
|
|
134
|
+
top.children.push({
|
|
135
|
+
type: "part",
|
|
136
|
+
index: i,
|
|
137
|
+
nodeKey: makeChildNodeKey(top),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Record the part index in every open ancestor group.
|
|
141
|
+
for (let s = 1; s < stack.length; s++) {
|
|
142
|
+
stack[s]!.indices.push(i);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Close any still-open groups.
|
|
147
|
+
while (stack.length > 1) {
|
|
148
|
+
closeTop();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return root.children;
|
|
152
|
+
};
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
SKIP_UPDATE,
|
|
13
13
|
} from "../../subscribable/subscribable";
|
|
14
14
|
import type {
|
|
15
|
+
ComposerRuntimeEventCallback,
|
|
15
16
|
ComposerRuntimeEventType,
|
|
16
17
|
DictationState,
|
|
17
18
|
EditComposerRuntimeCore,
|
|
@@ -222,9 +223,9 @@ export type ComposerRuntime = {
|
|
|
222
223
|
/**
|
|
223
224
|
* @deprecated This API is still under active development and might change without notice.
|
|
224
225
|
*/
|
|
225
|
-
unstable_on(
|
|
226
|
-
event:
|
|
227
|
-
callback:
|
|
226
|
+
unstable_on<E extends ComposerRuntimeEventType>(
|
|
227
|
+
event: E,
|
|
228
|
+
callback: ComposerRuntimeEventCallback<E>,
|
|
228
229
|
): Unsubscribe;
|
|
229
230
|
};
|
|
230
231
|
|
|
@@ -332,19 +333,19 @@ export abstract class ComposerRuntimeImpl implements ComposerRuntime {
|
|
|
332
333
|
EventSubscriptionSubject<ComposerRuntimeEventType>
|
|
333
334
|
>();
|
|
334
335
|
|
|
335
|
-
public unstable_on(
|
|
336
|
-
event:
|
|
337
|
-
callback:
|
|
336
|
+
public unstable_on<E extends ComposerRuntimeEventType>(
|
|
337
|
+
event: E,
|
|
338
|
+
callback: ComposerRuntimeEventCallback<E>,
|
|
338
339
|
): Unsubscribe {
|
|
339
340
|
let subject = this._eventSubscriptionSubjects.get(event);
|
|
340
341
|
if (!subject) {
|
|
341
|
-
subject = new EventSubscriptionSubject({
|
|
342
|
-
event
|
|
342
|
+
subject = new EventSubscriptionSubject<ComposerRuntimeEventType>({
|
|
343
|
+
event,
|
|
343
344
|
binding: this._core,
|
|
344
345
|
});
|
|
345
346
|
this._eventSubscriptionSubjects.set(event, subject);
|
|
346
347
|
}
|
|
347
|
-
return subject.subscribe(callback);
|
|
348
|
+
return subject.subscribe(callback as (payload?: unknown) => void);
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
public abstract getAttachmentByIndex(idx: number): AttachmentRuntime;
|
|
@@ -3,7 +3,26 @@ import type { SubscribableWithState } from "../../subscribable/subscribable";
|
|
|
3
3
|
import type { ThreadListItemRuntimePath } from "./paths";
|
|
4
4
|
import type { ThreadListRuntimeCoreBinding } from "./thread-list-runtime";
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type ThreadListItemEventPayload = {
|
|
7
|
+
/**
|
|
8
|
+
* @deprecated State-derivable. Compare `s.threads.mainThreadId` against the
|
|
9
|
+
* item's `s.threadListItem.id` via `useAuiState` instead. Kept for backward
|
|
10
|
+
* compatibility.
|
|
11
|
+
*/
|
|
12
|
+
switchedTo: Record<string, never>;
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated State-derivable. Compare `s.threads.mainThreadId` against the
|
|
15
|
+
* item's `s.threadListItem.id` via `useAuiState` instead. Kept for backward
|
|
16
|
+
* compatibility.
|
|
17
|
+
*/
|
|
18
|
+
switchedAway: Record<string, never>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ThreadListItemEventType = keyof ThreadListItemEventPayload;
|
|
22
|
+
|
|
23
|
+
export type ThreadListItemEventCallback<E extends ThreadListItemEventType> = (
|
|
24
|
+
payload: ThreadListItemEventPayload[E],
|
|
25
|
+
) => void;
|
|
7
26
|
|
|
8
27
|
import type { ThreadListItemState } from "./bindings";
|
|
9
28
|
import type { ThreadListItemStatus } from "../interfaces/thread-list-runtime-core";
|
|
@@ -27,9 +46,9 @@ export type ThreadListItemRuntime = {
|
|
|
27
46
|
|
|
28
47
|
subscribe(callback: () => void): Unsubscribe;
|
|
29
48
|
|
|
30
|
-
unstable_on(
|
|
31
|
-
event:
|
|
32
|
-
callback:
|
|
49
|
+
unstable_on<E extends ThreadListItemEventType>(
|
|
50
|
+
event: E,
|
|
51
|
+
callback: ThreadListItemEventCallback<E>,
|
|
33
52
|
): Unsubscribe;
|
|
34
53
|
|
|
35
54
|
__internal_getRuntime(): ThreadListItemRuntime;
|
|
@@ -112,7 +131,10 @@ export class ThreadListItemRuntimeImpl implements ThreadListItemRuntime {
|
|
|
112
131
|
return this._threadListBinding.generateTitle(state.id);
|
|
113
132
|
}
|
|
114
133
|
|
|
115
|
-
public unstable_on
|
|
134
|
+
public unstable_on<E extends ThreadListItemEventType>(
|
|
135
|
+
event: E,
|
|
136
|
+
callback: ThreadListItemEventCallback<E>,
|
|
137
|
+
) {
|
|
116
138
|
let prevIsMain = this._core.getState().isMain;
|
|
117
139
|
let prevThreadId = this._core.getState().id;
|
|
118
140
|
return this.subscribe(() => {
|
|
@@ -125,7 +147,7 @@ export class ThreadListItemRuntimeImpl implements ThreadListItemRuntime {
|
|
|
125
147
|
|
|
126
148
|
if (event === "switchedTo" && !newIsMain) return;
|
|
127
149
|
if (event === "switchedAway" && newIsMain) return;
|
|
128
|
-
callback();
|
|
150
|
+
(callback as (payload?: unknown) => void)({});
|
|
129
151
|
});
|
|
130
152
|
}
|
|
131
153
|
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ThreadRuntimeCore,
|
|
5
5
|
SpeechState,
|
|
6
6
|
VoiceSessionState,
|
|
7
|
+
ThreadRuntimeEventCallback,
|
|
7
8
|
ThreadRuntimeEventType,
|
|
8
9
|
StartRunConfig,
|
|
9
10
|
ResumeRunConfig,
|
|
@@ -329,7 +330,10 @@ export type ThreadRuntime = {
|
|
|
329
330
|
muteVoice(): void;
|
|
330
331
|
unmuteVoice(): void;
|
|
331
332
|
|
|
332
|
-
unstable_on
|
|
333
|
+
unstable_on<E extends ThreadRuntimeEventType>(
|
|
334
|
+
event: E,
|
|
335
|
+
callback: ThreadRuntimeEventCallback<E>,
|
|
336
|
+
): Unsubscribe;
|
|
333
337
|
};
|
|
334
338
|
|
|
335
339
|
export class ThreadRuntimeImpl implements ThreadRuntime {
|
|
@@ -601,18 +605,18 @@ export class ThreadRuntimeImpl implements ThreadRuntime {
|
|
|
601
605
|
EventSubscriptionSubject<ThreadRuntimeEventType>
|
|
602
606
|
>();
|
|
603
607
|
|
|
604
|
-
public unstable_on(
|
|
605
|
-
event:
|
|
606
|
-
callback:
|
|
608
|
+
public unstable_on<E extends ThreadRuntimeEventType>(
|
|
609
|
+
event: E,
|
|
610
|
+
callback: ThreadRuntimeEventCallback<E>,
|
|
607
611
|
): Unsubscribe {
|
|
608
612
|
let subject = this._eventSubscriptionSubjects.get(event);
|
|
609
613
|
if (!subject) {
|
|
610
|
-
subject = new EventSubscriptionSubject({
|
|
611
|
-
event
|
|
614
|
+
subject = new EventSubscriptionSubject<ThreadRuntimeEventType>({
|
|
615
|
+
event,
|
|
612
616
|
binding: this._threadBinding,
|
|
613
617
|
});
|
|
614
618
|
this._eventSubscriptionSubjects.set(event, subject);
|
|
615
619
|
}
|
|
616
|
-
return subject.subscribe(callback);
|
|
620
|
+
return subject.subscribe(callback as (payload?: unknown) => void);
|
|
617
621
|
}
|
|
618
622
|
}
|
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
fileMatchesAccept,
|
|
15
15
|
} from "../../adapters/attachment";
|
|
16
16
|
import type {
|
|
17
|
+
AttachmentAddErrorReason,
|
|
17
18
|
ComposerRuntimeCore,
|
|
19
|
+
ComposerRuntimeEventCallback,
|
|
20
|
+
ComposerRuntimeEventPayload,
|
|
18
21
|
ComposerRuntimeEventType,
|
|
19
22
|
DictationState,
|
|
20
23
|
SendOptions,
|
|
@@ -189,7 +192,7 @@ export abstract class BaseComposerRuntimeCore
|
|
|
189
192
|
};
|
|
190
193
|
|
|
191
194
|
this.handleSend(message, options);
|
|
192
|
-
this._notifyEventSubscribers("send");
|
|
195
|
+
this._notifyEventSubscribers("send", {});
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
public cancel() {
|
|
@@ -215,14 +218,15 @@ export abstract class BaseComposerRuntimeCore
|
|
|
215
218
|
adapter.accept,
|
|
216
219
|
)
|
|
217
220
|
) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
const message = `File type ${fileOrAttachment.contentType || "unknown"} is not accepted. Accepted types: ${adapter.accept}`;
|
|
222
|
+
const err = new Error(message);
|
|
223
|
+
this._safeEmitAttachmentAddError(
|
|
224
|
+
"not-accepted",
|
|
225
|
+
message,
|
|
226
|
+
undefined,
|
|
227
|
+
err,
|
|
225
228
|
);
|
|
229
|
+
throw err;
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
const a: CompleteAttachment = {
|
|
@@ -235,7 +239,7 @@ export abstract class BaseComposerRuntimeCore
|
|
|
235
239
|
};
|
|
236
240
|
this._attachments = [...this._attachments, a];
|
|
237
241
|
this._notifySubscribers();
|
|
238
|
-
this._notifyEventSubscribers("attachmentAdd");
|
|
242
|
+
this._notifyEventSubscribers("attachmentAdd", {});
|
|
239
243
|
return;
|
|
240
244
|
}
|
|
241
245
|
|
|
@@ -256,22 +260,28 @@ export abstract class BaseComposerRuntimeCore
|
|
|
256
260
|
this._notifySubscribers();
|
|
257
261
|
};
|
|
258
262
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
+
const adapter = this.getAttachmentAdapter();
|
|
264
|
+
if (!adapter) {
|
|
265
|
+
const message = "Attachments are not supported";
|
|
266
|
+
const err = new Error(message);
|
|
267
|
+
this._safeEmitAttachmentAddError("no-adapter", message, undefined, err);
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
263
270
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
if (
|
|
272
|
+
!fileMatchesAccept(
|
|
273
|
+
{ name: fileOrAttachment.name, type: fileOrAttachment.type },
|
|
274
|
+
adapter.accept,
|
|
275
|
+
)
|
|
276
|
+
) {
|
|
277
|
+
const message = `File type ${fileOrAttachment.type || "unknown"} is not accepted. Accepted types: ${adapter.accept}`;
|
|
278
|
+
const err = new Error(message);
|
|
279
|
+
this._safeEmitAttachmentAddError("not-accepted", message, undefined, err);
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
274
282
|
|
|
283
|
+
let lastAttachment: PendingAttachment | undefined;
|
|
284
|
+
try {
|
|
275
285
|
const promiseOrGenerator = adapter.add({ file: fileOrAttachment });
|
|
276
286
|
if (Symbol.asyncIterator in promiseOrGenerator) {
|
|
277
287
|
for await (const r of promiseOrGenerator) {
|
|
@@ -289,20 +299,48 @@ export abstract class BaseComposerRuntimeCore
|
|
|
289
299
|
status: { type: "incomplete", reason: "error" },
|
|
290
300
|
});
|
|
291
301
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
302
|
+
this._safeEmitAttachmentAddError(
|
|
303
|
+
"adapter-error",
|
|
304
|
+
e instanceof Error ? e.message : String(e),
|
|
305
|
+
lastAttachment?.id,
|
|
306
|
+
e instanceof Error ? e : undefined,
|
|
307
|
+
);
|
|
297
308
|
throw e;
|
|
298
309
|
}
|
|
299
310
|
|
|
300
311
|
const hasError =
|
|
301
312
|
lastAttachment?.status.type === "incomplete" &&
|
|
302
313
|
lastAttachment.status.reason === "error";
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
314
|
+
if (hasError) {
|
|
315
|
+
this._safeEmitAttachmentAddError(
|
|
316
|
+
"adapter-error",
|
|
317
|
+
"Attachment upload did not complete successfully.",
|
|
318
|
+
lastAttachment?.id,
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
this._notifyEventSubscribers("attachmentAdd", {});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private _safeEmitAttachmentAddError(
|
|
326
|
+
reason: AttachmentAddErrorReason,
|
|
327
|
+
message: string,
|
|
328
|
+
attachmentId?: string,
|
|
329
|
+
error?: Error,
|
|
330
|
+
) {
|
|
331
|
+
try {
|
|
332
|
+
this._notifyEventSubscribers("attachmentAddError", {
|
|
333
|
+
reason,
|
|
334
|
+
message,
|
|
335
|
+
...(attachmentId !== undefined && { attachmentId }),
|
|
336
|
+
...(error !== undefined && { error }),
|
|
337
|
+
});
|
|
338
|
+
} catch (subscriberError) {
|
|
339
|
+
console.error(
|
|
340
|
+
"[assistant-ui] attachmentAddError subscriber threw:",
|
|
341
|
+
subscriberError,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
306
344
|
}
|
|
307
345
|
|
|
308
346
|
async removeAttachment(attachmentId: string) {
|
|
@@ -471,28 +509,33 @@ export abstract class BaseComposerRuntimeCore
|
|
|
471
509
|
|
|
472
510
|
private _eventSubscribers = new Map<
|
|
473
511
|
ComposerRuntimeEventType,
|
|
474
|
-
Set<() => void>
|
|
512
|
+
Set<(payload?: unknown) => void>
|
|
475
513
|
>();
|
|
476
514
|
|
|
477
|
-
protected _notifyEventSubscribers
|
|
515
|
+
protected _notifyEventSubscribers<E extends ComposerRuntimeEventType>(
|
|
516
|
+
event: E,
|
|
517
|
+
payload: ComposerRuntimeEventPayload[E],
|
|
518
|
+
) {
|
|
478
519
|
const subscribers = this._eventSubscribers.get(event);
|
|
479
520
|
if (!subscribers) return;
|
|
480
521
|
|
|
481
|
-
for (const callback of subscribers) callback();
|
|
522
|
+
for (const callback of subscribers) callback(payload);
|
|
482
523
|
}
|
|
483
524
|
|
|
484
|
-
public unstable_on
|
|
485
|
-
|
|
525
|
+
public unstable_on<E extends ComposerRuntimeEventType>(
|
|
526
|
+
event: E,
|
|
527
|
+
callback: ComposerRuntimeEventCallback<E>,
|
|
528
|
+
) {
|
|
529
|
+
const wrapped = callback as (payload?: unknown) => void;
|
|
530
|
+
let subscribers = this._eventSubscribers.get(event);
|
|
486
531
|
if (!subscribers) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
subscribers.add(callback);
|
|
532
|
+
subscribers = new Set();
|
|
533
|
+
this._eventSubscribers.set(event, subscribers);
|
|
490
534
|
}
|
|
535
|
+
subscribers.add(wrapped);
|
|
491
536
|
|
|
492
537
|
return () => {
|
|
493
|
-
|
|
494
|
-
if (!subscribers) return;
|
|
495
|
-
subscribers.delete(callback);
|
|
538
|
+
this._eventSubscribers.get(event)?.delete(wrapped);
|
|
496
539
|
};
|
|
497
540
|
}
|
|
498
541
|
}
|