@assistant-ui/core 0.1.16 → 0.2.0

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.
Files changed (173) hide show
  1. package/dist/index.d.ts +7 -7
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/react/index.d.ts +3 -1
  6. package/dist/react/index.d.ts.map +1 -1
  7. package/dist/react/index.js +2 -0
  8. package/dist/react/index.js.map +1 -1
  9. package/dist/react/primitive-hooks/useThreadListLoadMore.d.ts +5 -0
  10. package/dist/react/primitive-hooks/useThreadListLoadMore.d.ts.map +1 -0
  11. package/dist/react/primitive-hooks/useThreadListLoadMore.js +11 -0
  12. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -0
  13. package/dist/react/primitives/message/MessageGroupedParts.d.ts +104 -0
  14. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -0
  15. package/dist/react/primitives/message/MessageGroupedParts.js +74 -0
  16. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -0
  17. package/dist/react/primitives/message/MessageParts.d.ts +8 -1
  18. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  19. package/dist/react/primitives/message/MessageParts.js +45 -42
  20. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  21. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -4
  22. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  23. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +4 -3
  24. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  25. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +8 -6
  26. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  27. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +86 -38
  28. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  29. package/dist/react/runtimes/useLocalRuntime.d.ts +1 -1
  30. package/dist/react/utils/groupParts.d.ts +49 -0
  31. package/dist/react/utils/groupParts.d.ts.map +1 -0
  32. package/dist/react/utils/groupParts.js +97 -0
  33. package/dist/react/utils/groupParts.js.map +1 -0
  34. package/dist/runtime/api/assistant-runtime.d.ts +0 -33
  35. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  36. package/dist/runtime/api/assistant-runtime.js +0 -23
  37. package/dist/runtime/api/assistant-runtime.js.map +1 -1
  38. package/dist/runtime/api/bindings.d.ts +1 -3
  39. package/dist/runtime/api/bindings.d.ts.map +1 -1
  40. package/dist/runtime/api/composer-runtime.d.ts +3 -3
  41. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  42. package/dist/runtime/api/composer-runtime.js +1 -1
  43. package/dist/runtime/api/composer-runtime.js.map +1 -1
  44. package/dist/runtime/api/message-runtime.d.ts +1 -6
  45. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  46. package/dist/runtime/api/message-runtime.js.map +1 -1
  47. package/dist/runtime/api/thread-list-item-runtime.d.ts +18 -3
  48. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  49. package/dist/runtime/api/thread-list-item-runtime.js +1 -1
  50. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  51. package/dist/runtime/api/thread-list-runtime.d.ts +4 -0
  52. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  53. package/dist/runtime/api/thread-list-runtime.js +6 -0
  54. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  55. package/dist/runtime/api/thread-runtime.d.ts +6 -29
  56. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  57. package/dist/runtime/api/thread-runtime.js +2 -21
  58. package/dist/runtime/api/thread-runtime.js.map +1 -1
  59. package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -3
  60. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  61. package/dist/runtime/base/base-composer-runtime-core.js +47 -33
  62. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  63. package/dist/runtime/base/base-thread-runtime-core.d.ts +3 -4
  64. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  65. package/dist/runtime/base/base-thread-runtime-core.js +11 -11
  66. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  67. package/dist/runtime/interfaces/composer-runtime-core.d.ts +28 -2
  68. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  69. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +3 -0
  70. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  71. package/dist/runtime/interfaces/thread-runtime-core.d.ts +35 -4
  72. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  73. package/dist/runtime/utils/chat-model-adapter.d.ts +0 -4
  74. package/dist/runtime/utils/chat-model-adapter.d.ts.map +1 -1
  75. package/dist/runtime/utils/external-store-message.d.ts +0 -4
  76. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  77. package/dist/runtime/utils/external-store-message.js +0 -7
  78. package/dist/runtime/utils/external-store-message.js.map +1 -1
  79. package/dist/runtimes/assistant-transport/utils.d.ts +0 -9
  80. package/dist/runtimes/assistant-transport/utils.d.ts.map +1 -1
  81. package/dist/runtimes/assistant-transport/utils.js +0 -13
  82. package/dist/runtimes/assistant-transport/utils.js.map +1 -1
  83. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +0 -1
  84. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  85. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +2 -5
  86. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  87. package/dist/runtimes/local/local-thread-runtime-core.d.ts +0 -1
  88. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  89. package/dist/runtimes/local/local-thread-runtime-core.js +2 -6
  90. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  91. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +0 -1
  92. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  93. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +0 -3
  94. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  95. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  96. package/dist/runtimes/remote-thread-list/empty-thread-core.js +0 -3
  97. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  98. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  99. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  100. package/dist/runtimes/remote-thread-list/remote-thread-state.js +34 -0
  101. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  102. package/dist/runtimes/remote-thread-list/types.d.ts +5 -1
  103. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  104. package/dist/store/clients/thread-message-client.d.ts.map +1 -1
  105. package/dist/store/clients/thread-message-client.js +0 -1
  106. package/dist/store/clients/thread-message-client.js.map +1 -1
  107. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  108. package/dist/store/runtime-clients/composer-runtime-client.js +5 -6
  109. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  110. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  111. package/dist/store/runtime-clients/thread-list-runtime-client.js +3 -0
  112. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  113. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  114. package/dist/store/runtime-clients/thread-runtime-client.js +0 -1
  115. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  116. package/dist/store/scopes/composer.d.ts +11 -1
  117. package/dist/store/scopes/composer.d.ts.map +1 -1
  118. package/dist/store/scopes/message.d.ts +1 -3
  119. package/dist/store/scopes/message.d.ts.map +1 -1
  120. package/dist/store/scopes/thread-list-item.d.ts +10 -0
  121. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  122. package/dist/store/scopes/thread.d.ts +17 -4
  123. package/dist/store/scopes/thread.d.ts.map +1 -1
  124. package/dist/store/scopes/threads.d.ts +3 -0
  125. package/dist/store/scopes/threads.d.ts.map +1 -1
  126. package/dist/subscribable/subscribable.d.ts +4 -4
  127. package/dist/subscribable/subscribable.d.ts.map +1 -1
  128. package/dist/subscribable/subscribable.js +4 -4
  129. package/dist/subscribable/subscribable.js.map +1 -1
  130. package/package.json +25 -13
  131. package/src/index.ts +12 -6
  132. package/src/react/index.ts +3 -0
  133. package/src/react/primitive-hooks/useThreadListLoadMore.ts +15 -0
  134. package/src/react/primitives/message/MessageGroupedParts.tsx +186 -0
  135. package/src/react/primitives/message/MessageParts.tsx +80 -55
  136. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +7 -6
  137. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +96 -43
  138. package/src/react/utils/groupParts.ts +152 -0
  139. package/src/runtime/api/assistant-runtime.ts +0 -62
  140. package/src/runtime/api/bindings.ts +1 -6
  141. package/src/runtime/api/composer-runtime.ts +10 -9
  142. package/src/runtime/api/message-runtime.ts +1 -8
  143. package/src/runtime/api/thread-list-item-runtime.ts +28 -6
  144. package/src/runtime/api/thread-list-runtime.ts +10 -0
  145. package/src/runtime/api/thread-runtime.ts +12 -53
  146. package/src/runtime/base/base-composer-runtime-core.ts +85 -42
  147. package/src/runtime/base/base-thread-runtime-core.ts +21 -13
  148. package/src/runtime/interfaces/composer-runtime-core.ts +39 -7
  149. package/src/runtime/interfaces/thread-list-runtime-core.ts +3 -0
  150. package/src/runtime/interfaces/thread-runtime-core.ts +42 -9
  151. package/src/runtime/utils/chat-model-adapter.ts +0 -5
  152. package/src/runtime/utils/external-store-message.ts +0 -8
  153. package/src/runtimes/assistant-transport/utils.ts +0 -28
  154. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +2 -6
  155. package/src/runtimes/local/local-thread-runtime-core.ts +2 -7
  156. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +0 -4
  157. package/src/runtimes/remote-thread-list/empty-thread-core.ts +0 -4
  158. package/src/runtimes/remote-thread-list/remote-thread-state.ts +54 -1
  159. package/src/runtimes/remote-thread-list/types.ts +6 -1
  160. package/src/store/clients/thread-message-client.ts +0 -1
  161. package/src/store/runtime-clients/composer-runtime-client.ts +5 -9
  162. package/src/store/runtime-clients/thread-list-runtime-client.ts +3 -0
  163. package/src/store/runtime-clients/thread-runtime-client.ts +0 -1
  164. package/src/store/scopes/composer.ts +11 -0
  165. package/src/store/scopes/message.ts +1 -6
  166. package/src/store/scopes/thread-list-item.ts +10 -0
  167. package/src/store/scopes/thread.ts +17 -5
  168. package/src/store/scopes/threads.ts +3 -0
  169. package/src/subscribable/subscribable.ts +10 -7
  170. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +448 -0
  171. package/src/tests/RemoteThreadListThreadListRuntimeCore-reload.test.ts +6 -1
  172. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +63 -0
  173. package/src/tests/groupParts.test.ts +114 -0
package/src/index.ts CHANGED
@@ -32,7 +32,9 @@ export type {
32
32
  export type {
33
33
  Attachment,
34
34
  PendingAttachment,
35
+ PendingAttachmentStatus,
35
36
  CompleteAttachment,
37
+ CompleteAttachmentStatus,
36
38
  AttachmentStatus,
37
39
  CreateAttachment,
38
40
  } from "./types/attachment";
@@ -143,7 +145,11 @@ export type {
143
145
 
144
146
  // Runtime Core Interface Types
145
147
  export type {
148
+ AttachmentAddErrorEvent,
149
+ AttachmentAddErrorReason,
146
150
  ComposerRuntimeCore,
151
+ ComposerRuntimeEventCallback,
152
+ ComposerRuntimeEventPayload,
147
153
  ComposerRuntimeEventType,
148
154
  DictationState,
149
155
  EditComposerRuntimeCore,
@@ -160,6 +166,8 @@ export type {
160
166
  SpeechState,
161
167
  VoiceSessionState,
162
168
  SubmittedFeedback,
169
+ ThreadRuntimeEventCallback,
170
+ ThreadRuntimeEventPayload,
163
171
  ThreadRuntimeEventType,
164
172
  StartRunConfig,
165
173
  ResumeRunConfig,
@@ -191,6 +199,8 @@ export type {
191
199
  } from "./runtime/api/thread-list-runtime";
192
200
 
193
201
  export type {
202
+ ThreadListItemEventCallback,
203
+ ThreadListItemEventPayload,
194
204
  ThreadListItemEventType,
195
205
  ThreadListItemRuntime,
196
206
  } from "./runtime/api/thread-list-item-runtime";
@@ -234,7 +244,6 @@ export type { ThreadMessageLike } from "./runtime/utils/thread-message-like";
234
244
 
235
245
  // External Store Message Utilities
236
246
  export {
237
- getExternalStoreMessage,
238
247
  getExternalStoreMessages,
239
248
  bindExternalStoreMessage,
240
249
  } from "./runtime/utils/external-store-message";
@@ -261,13 +270,10 @@ export type {
261
270
  RemoteThreadInitializeResponse,
262
271
  RemoteThreadMetadata,
263
272
  RemoteThreadListResponse,
273
+ RemoteThreadListPageOptions,
264
274
  } from "./runtimes/remote-thread-list/types";
265
275
 
266
276
  export { InMemoryThreadListAdapter } from "./runtimes/remote-thread-list/adapter/in-memory";
267
277
 
268
278
  // Assistant Transport Utilities
269
- export {
270
- toAISDKTools,
271
- getEnabledTools,
272
- createRequestHeaders,
273
- } from "./runtimes/assistant-transport/utils";
279
+ export { createRequestHeaders } from "./runtimes/assistant-transport/utils";
@@ -163,7 +163,9 @@ export {
163
163
  MessagePrimitivePartByIndex,
164
164
  defaultComponents as messagePartsDefaultComponents,
165
165
  type EnrichedPartState,
166
+ type PartState,
166
167
  } from "./primitives/message/MessageParts";
168
+ export { MessagePrimitiveGroupedParts } from "./primitives/message/MessageGroupedParts";
167
169
  export { MessagePrimitiveQuote } from "./primitives/message/MessageQuote";
168
170
  export {
169
171
  MessagePrimitiveAttachments,
@@ -233,6 +235,7 @@ export { useThreadListItemDelete } from "./primitive-hooks/useThreadListItemDele
233
235
  export { useThreadListItemUnarchive } from "./primitive-hooks/useThreadListItemUnarchive";
234
236
  export { useThreadListItemTrigger } from "./primitive-hooks/useThreadListItemTrigger";
235
237
  export { useThreadListNew } from "./primitive-hooks/useThreadListNew";
238
+ export { useThreadListLoadMore } from "./primitive-hooks/useThreadListLoadMore";
236
239
  export { useEditComposerCancel } from "./primitive-hooks/useEditComposerCancel";
237
240
  export { useEditComposerSend } from "./primitive-hooks/useEditComposerSend";
238
241
  export { useMessageError } from "./primitive-hooks/useMessageError";
@@ -0,0 +1,15 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export const useThreadListLoadMore = () => {
5
+ const aui = useAui();
6
+ const disabled = useAuiState(
7
+ (s) => !s.threads.hasMore || s.threads.isLoading || s.threads.isLoadingMore,
8
+ );
9
+
10
+ const loadMore = useCallback(() => {
11
+ aui.threads().loadMore();
12
+ }, [aui]);
13
+
14
+ return { loadMore, disabled };
15
+ };
@@ -0,0 +1,186 @@
1
+ "use client";
2
+
3
+ import { Fragment, type FC, type ReactNode, useMemo } from "react";
4
+ import { useAuiState } from "@assistant-ui/store";
5
+ import { useShallow } from "zustand/shallow";
6
+ import type { PartState } from "../../../store/scopes/part";
7
+ import type {
8
+ MessagePartStatus,
9
+ ToolCallMessagePartStatus,
10
+ } from "../../../types/message";
11
+ import {
12
+ buildGroupTree,
13
+ type GroupKey,
14
+ type GroupNode,
15
+ normalizeGroupKey,
16
+ } from "../../utils/groupParts";
17
+ import { MessagePartChildren, type EnrichedPartState } from "./MessageParts";
18
+
19
+ export namespace MessagePrimitiveGroupedParts {
20
+ /**
21
+ * A coalesced group of adjacent parts. Surfaced through the same
22
+ * `{ part }` channel as a leaf {@link EnrichedPartState} so consumers
23
+ * dispatch on a single `switch (part.type)`. `type` is the group key
24
+ * (always `"group-…"`); `status` mirrors the last contained part.
25
+ */
26
+ export type GroupPart<TKey extends `group-${string}` = `group-${string}`> = {
27
+ readonly type: TKey;
28
+ readonly status: MessagePartStatus | ToolCallMessagePartStatus;
29
+ readonly indices: readonly number[];
30
+ };
31
+
32
+ export type RenderInfo<TKey extends `group-${string}` = `group-${string}`> = {
33
+ /**
34
+ * Either a coalesced group ({@link GroupPart}, identified by a
35
+ * `group-…` `type`) or a single enriched part. Use one switch over
36
+ * `part.type` to handle both.
37
+ */
38
+ readonly part: GroupPart<TKey> | EnrichedPartState;
39
+ /**
40
+ * For group nodes: the recursively-rendered subtree (subgroups +
41
+ * leaf parts). For leaf parts: a sentinel that throws when rendered
42
+ * — accidental fall-through (`default: return children;`) errors
43
+ * loudly instead of silently rendering nothing.
44
+ */
45
+ readonly children: ReactNode;
46
+ };
47
+
48
+ export type Props<TKey extends `group-${string}` = `group-${string}`> = {
49
+ /**
50
+ * Maps each part to its group key path. Adjacent parts that share a
51
+ * prefix coalesce up to that prefix. Return `null`, `undefined`, or
52
+ * `[]` to leave a part ungrouped — it will be rendered as a leaf
53
+ * through `children` with `part` set to its {@link EnrichedPartState}.
54
+ *
55
+ * Keys must start with `"group-"` so the renderer's
56
+ * `switch (part.type)` can distinguish groups from real part types.
57
+ *
58
+ * For best performance, pass a stable reference (module-level
59
+ * constant or `useCallback`).
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const groupBy = (part) =>
64
+ * part.type === "reasoning" ? ["group-thought", "group-reasoning"] :
65
+ * part.type === "tool-call" ? ["group-thought", "group-tool"] :
66
+ * null;
67
+ * ```
68
+ */
69
+ readonly groupBy: (
70
+ part: PartState,
71
+ index: number,
72
+ parts: readonly PartState[],
73
+ ) => GroupKey<TKey>;
74
+
75
+ /**
76
+ * Render function called once per group node and once per leaf part.
77
+ * Switch on `part.type`: `"group-…"` cases wrap `children`; real
78
+ * part types (`"text"`, `"tool-call"`, …) render the part directly.
79
+ *
80
+ * Leaf parts receive the same {@link EnrichedPartState} that
81
+ * `<MessagePrimitive.Parts>` would produce (`toolUI`, `addResult`,
82
+ * `resume`, `dataRendererUI`).
83
+ */
84
+ readonly children: (info: RenderInfo<TKey>) => ReactNode;
85
+ };
86
+ }
87
+
88
+ const COMPLETE_STATUS: MessagePartStatus = Object.freeze({ type: "complete" });
89
+
90
+ /**
91
+ * `children` placeholder passed for leaf-part renders. Leaf parts have no
92
+ * inner subtree; rendering this sentinel signals the consumer wrote
93
+ * `default: return children;` and accidentally fell through for a part —
94
+ * surface the bug loudly instead of silently rendering nothing.
95
+ */
96
+ const PartChildrenSentinel: FC = () => {
97
+ throw new Error(
98
+ "MessagePrimitive.GroupedParts: rendered `children` under a leaf " +
99
+ "part. `children` is only meaningful for `group-…` cases — add a " +
100
+ "matching case for the part type or return `null` to skip it.",
101
+ );
102
+ };
103
+
104
+ const renderNode = <TKey extends `group-${string}`>(
105
+ node: GroupNode,
106
+ parts: readonly PartState[],
107
+ render: (info: MessagePrimitiveGroupedParts.RenderInfo<TKey>) => ReactNode,
108
+ ): ReactNode => {
109
+ if (node.type === "part") {
110
+ return (
111
+ <MessagePartChildren key={node.nodeKey} index={node.index}>
112
+ {({ part }) => render({ part, children: <PartChildrenSentinel /> })}
113
+ </MessagePartChildren>
114
+ );
115
+ }
116
+
117
+ const status = parts[node.indices.at(-1)!]?.status ?? COMPLETE_STATUS;
118
+ const groupPart: MessagePrimitiveGroupedParts.GroupPart<TKey> = {
119
+ type: node.key as TKey,
120
+ status,
121
+ indices: node.indices,
122
+ };
123
+
124
+ return (
125
+ <Fragment key={node.nodeKey}>
126
+ {render({
127
+ part: groupPart,
128
+ children: (
129
+ <>{node.children.map((child) => renderNode(child, parts, render))}</>
130
+ ),
131
+ })}
132
+ </Fragment>
133
+ );
134
+ };
135
+
136
+ /**
137
+ * Groups adjacent message parts into a tree of coalesced runs and
138
+ * renders each node — group or part — through a single `children`
139
+ * function.
140
+ *
141
+ * The render function receives `{ part, children }` where `part.type`
142
+ * is either a `"group-…"` literal (for a group, `children` is the
143
+ * recursively-rendered subtree) or a real part type (`"text"`,
144
+ * `"tool-call"`, …) for a leaf (`children` is a sentinel that throws
145
+ * if rendered — use `part.type` to distinguish).
146
+ *
147
+ * @example
148
+ * ```tsx
149
+ * <MessagePrimitive.GroupedParts
150
+ * groupBy={(part) =>
151
+ * part.type === "reasoning" ? ["group-thought", "group-reasoning"] :
152
+ * part.type === "tool-call" ? ["group-thought", "group-tool"] :
153
+ * null
154
+ * }
155
+ * >
156
+ * {({ part, children }) => {
157
+ * switch (part.type) {
158
+ * case "group-thought": return <Thought>{children}</Thought>;
159
+ * case "group-reasoning": return <Reasoning>{children}</Reasoning>;
160
+ * case "group-tool": return <ToolStack>{children}</ToolStack>;
161
+ * case "text": return <MarkdownText />;
162
+ * case "tool-call": return part.toolUI ?? <ToolFallback {...part} />;
163
+ * default: return null;
164
+ * }
165
+ * }}
166
+ * </MessagePrimitive.GroupedParts>
167
+ * ```
168
+ */
169
+ export const MessagePrimitiveGroupedParts = <TKey extends `group-${string}`>({
170
+ groupBy,
171
+ children,
172
+ }: MessagePrimitiveGroupedParts.Props<TKey>): ReactNode => {
173
+ const parts = useAuiState(useShallow((s) => s.message.parts));
174
+
175
+ const tree = useMemo(
176
+ () =>
177
+ buildGroupTree(
178
+ parts.map((part, i) => normalizeGroupKey(groupBy(part, i, parts))),
179
+ ),
180
+ [parts, groupBy],
181
+ );
182
+
183
+ return <>{tree.map((node) => renderNode(node, parts, children))}</>;
184
+ };
185
+
186
+ MessagePrimitiveGroupedParts.displayName = "MessagePrimitive.GroupedParts";
@@ -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 This feature is still experimental and subject to change.
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
- return (
626
- <TextMessagePartProvider text="" isRunning>
627
- {children({ part: EMPTY_RUNNING_TEXT_PART })}
628
- </TextMessagePartProvider>
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
- return Array.from({ length: contentLength }, (_, index) => (
633
- <PartByIndexProvider key={index} index={index}>
634
- <RenderChildrenWithAccessor
635
- getItemState={(aui) => aui.message().part({ index }).getState()}
636
- >
637
- {(getItem) => {
638
- const result = children({
639
- get part() {
640
- const state = getItem();
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
  /**
@@ -94,7 +94,8 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
94
94
  }
95
95
  }
96
96
 
97
- // Rendered outside the user's Provider so deferred `children` cannot strand the binding.
97
+ // Rendered as a child of the user's Provider so the runtime hook can
98
+ // read context the Provider injects (e.g. RuntimeAdapterProvider).
98
99
  private _RuntimeBinder: FC<PropsWithChildren<{ threadId: string }>> = ({
99
100
  threadId,
100
101
  children,
@@ -172,7 +173,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
172
173
  console.warn(
173
174
  "RemoteThreadListAdapter.unstable_Provider did not render its `children` synchronously. " +
174
175
  "Render `children` on first commit; deferring them behind a loading state, Suspense boundary, " +
175
- "or `useEffect` gate leaves thread context unavailable to downstream consumers.",
176
+ "or `useEffect` gate strands the runtime binder and leaves the thread without context.",
176
177
  );
177
178
  }
178
179
  }, 100);
@@ -183,11 +184,11 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
183
184
 
184
185
  return (
185
186
  <ThreadListItemRuntimeProvider runtime={runtime}>
186
- <this._RuntimeBinder threadId={threadId}>
187
- <Provider>
187
+ <Provider>
188
+ <this._RuntimeBinder threadId={threadId}>
188
189
  <ProviderRenderDetector detectorRef={detectorRef} />
189
- </Provider>
190
- </this._RuntimeBinder>
190
+ </this._RuntimeBinder>
191
+ </Provider>
191
192
  </ThreadListItemRuntimeProvider>
192
193
  );
193
194
  });