@assistant-ui/react 0.14.28 → 0.15.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 (190) hide show
  1. package/dist/client/ExternalThread.d.ts +11 -1
  2. package/dist/client/ExternalThread.d.ts.map +1 -1
  3. package/dist/client/ExternalThread.js +762 -516
  4. package/dist/client/ExternalThread.js.map +1 -1
  5. package/dist/client/InMemoryThreadList.js +3 -3
  6. package/dist/client/InMemoryThreadList.js.map +1 -1
  7. package/dist/context/index.d.ts +1 -8
  8. package/dist/context/index.js +1 -8
  9. package/dist/index.d.ts +1 -8
  10. package/dist/index.js +1 -8
  11. package/dist/legacy-runtime/cloud/auiV0.d.ts +12 -1
  12. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
  13. package/dist/legacy-runtime/cloud/auiV0.js +2 -1
  14. package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
  15. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +1 -1
  16. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +7 -6
  17. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +1 -1
  18. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +1 -1
  19. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +30 -15
  20. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +1 -1
  21. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  22. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +11 -7
  23. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  24. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +1 -0
  25. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +1 -1
  26. package/dist/mcp-apps/McpAppRenderer.js +1 -1
  27. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  28. package/dist/model-context/makeAssistantVisible.js +26 -25
  29. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +3 -3
  31. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  32. package/dist/primitives/actionBar/ActionBarRoot.js +3 -3
  33. package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
  34. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts +7 -6
  35. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts.map +1 -1
  36. package/dist/primitives/actionBar/useActionBarFloatStatus.js +10 -11
  37. package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
  38. package/dist/primitives/attachment/AttachmentRemove.js +4 -3
  39. package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
  40. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +4 -3
  41. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
  42. package/dist/primitives/composer/ComposerAddAttachment.d.ts.map +1 -1
  43. package/dist/primitives/composer/ComposerAddAttachment.js +11 -5
  44. package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
  45. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  46. package/dist/primitives/composer/ComposerAttachmentDropzone.js +2 -4
  47. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  48. package/dist/primitives/composer/ComposerInput.js +18 -16
  49. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  50. package/dist/primitives/composer/ComposerQuote.js +1 -1
  51. package/dist/primitives/composer/ComposerQuote.js.map +1 -1
  52. package/dist/primitives/composer/ComposerSend.js +1 -0
  53. package/dist/primitives/composer/ComposerSend.js.map +1 -1
  54. package/dist/primitives/composer/ComposerStopDictation.js +4 -3
  55. package/dist/primitives/composer/ComposerStopDictation.js.map +1 -1
  56. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  57. package/dist/primitives/composer/trigger/triggerSelectionResource.js +3 -3
  58. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  59. package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
  60. package/dist/primitives/message/MessagePartsGrouped.js +67 -89
  61. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  62. package/dist/primitives/message/MessageRoot.js +3 -3
  63. package/dist/primitives/message/MessageRoot.js.map +1 -1
  64. package/dist/primitives/queueItem/QueueItemRemove.js +4 -3
  65. package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -1
  66. package/dist/primitives/queueItem/QueueItemSteer.js +4 -3
  67. package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -1
  68. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
  69. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
  70. package/dist/primitives/suggestion/SuggestionTrigger.js +17 -15
  71. package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
  72. package/dist/primitives/thread/ThreadScrollToBottom.js +1 -0
  73. package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
  74. package/dist/unstable/useComposerInput.js +2 -2
  75. package/dist/unstable/useComposerInput.js.map +1 -1
  76. package/dist/unstable/useComposerInputHistory.js +6 -6
  77. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  78. package/dist/unstable/useMentionAdapter.js +1 -1
  79. package/dist/unstable/useMentionAdapter.js.map +1 -1
  80. package/dist/utils/hooks/useOnResizeContent.js +1 -0
  81. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  82. package/dist/utils/hooks/useSizeHandle.js +1 -0
  83. package/dist/utils/hooks/useSizeHandle.js.map +1 -1
  84. package/dist/utils/smooth/SmoothContext.js +3 -3
  85. package/dist/utils/smooth/SmoothContext.js.map +1 -1
  86. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  87. package/dist/utils/smooth/useSmooth.js +3 -3
  88. package/dist/utils/smooth/useSmooth.js.map +1 -1
  89. package/package.json +9 -9
  90. package/src/client/ExternalThread.ts +345 -56
  91. package/src/client/InMemoryThreadList.ts +3 -3
  92. package/src/context/index.ts +0 -44
  93. package/src/legacy-runtime/cloud/auiV0.ts +14 -0
  94. package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +4 -1
  95. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +37 -10
  96. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +79 -0
  97. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +97 -0
  98. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +113 -2
  99. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +10 -2
  100. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +185 -0
  101. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +21 -5
  102. package/src/mcp-apps/McpAppRenderer.tsx +1 -1
  103. package/src/model-context/makeAssistantVisible.test.tsx +1 -1
  104. package/src/model-context/makeAssistantVisible.tsx +1 -1
  105. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +1 -1
  106. package/src/primitives/actionBar/useActionBarFloatStatus.ts +7 -5
  107. package/src/primitives/attachment/AttachmentRemove.ts +1 -1
  108. package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.ts +1 -1
  109. package/src/primitives/composer/ComposerAddAttachment.test.tsx +108 -0
  110. package/src/primitives/composer/ComposerAddAttachment.ts +11 -5
  111. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +5 -4
  112. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +4 -3
  113. package/src/primitives/composer/ComposerInput.test.tsx +104 -7
  114. package/src/primitives/composer/ComposerInput.tsx +22 -20
  115. package/src/primitives/composer/ComposerQuote.tsx +1 -1
  116. package/src/primitives/composer/ComposerStopDictation.ts +1 -1
  117. package/src/primitives/composer/trigger/triggerSelectionResource.ts +7 -9
  118. package/src/primitives/message/MessagePartsGrouped.tsx +6 -8
  119. package/src/primitives/message/MessageRoot.tsx +1 -1
  120. package/src/primitives/queueItem/QueueItemRemove.ts +1 -1
  121. package/src/primitives/queueItem/QueueItemSteer.ts +1 -1
  122. package/src/primitives/selectionToolbar/SelectionToolbarQuote.tsx +1 -1
  123. package/src/primitives/suggestion/SuggestionTrigger.ts +9 -9
  124. package/src/tests/DataRenderers.test.tsx +212 -0
  125. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +1 -11
  126. package/src/tests/RemoteThreadListRuntime.deferredProvider.test.tsx +3 -3
  127. package/src/tests/external-thread-attachments.test.tsx +300 -11
  128. package/src/tests/external-thread-branches.test.tsx +10 -10
  129. package/src/tests/external-thread-parity.test.tsx +230 -0
  130. package/src/tests/generative-ui.test.tsx +1 -46
  131. package/src/tests/local-runtime-queue.test.tsx +29 -34
  132. package/src/tests/messagePartSwitchRace.test.tsx +46 -15
  133. package/src/tests/messagePartTypeMismatch.test.tsx +2 -2
  134. package/src/tests/threadMessageById.test.tsx +0 -7
  135. package/src/unstable/useComposerInput.test.tsx +2 -2
  136. package/src/unstable/useComposerInput.ts +2 -2
  137. package/src/unstable/useComposerInputHistory.test.tsx +3 -3
  138. package/src/unstable/useComposerInputHistory.ts +6 -6
  139. package/src/unstable/useMentionAdapter.ts +1 -1
  140. package/src/utils/smooth/SmoothContext.tsx +1 -1
  141. package/src/utils/smooth/useSmooth.test.tsx +1 -1
  142. package/src/utils/smooth/useSmooth.ts +8 -5
  143. package/dist/context/react/utils/createStateHookForRuntime.d.ts +0 -34
  144. package/dist/context/react/utils/createStateHookForRuntime.d.ts.map +0 -1
  145. package/dist/context/react/utils/createStateHookForRuntime.js +0 -21
  146. package/dist/context/react/utils/createStateHookForRuntime.js.map +0 -1
  147. package/dist/legacy-runtime/hooks/AssistantContext.d.ts +0 -74
  148. package/dist/legacy-runtime/hooks/AssistantContext.d.ts.map +0 -1
  149. package/dist/legacy-runtime/hooks/AssistantContext.js +0 -29
  150. package/dist/legacy-runtime/hooks/AssistantContext.js.map +0 -1
  151. package/dist/legacy-runtime/hooks/AttachmentContext.d.ts +0 -1007
  152. package/dist/legacy-runtime/hooks/AttachmentContext.d.ts.map +0 -1
  153. package/dist/legacy-runtime/hooks/AttachmentContext.js +0 -47
  154. package/dist/legacy-runtime/hooks/AttachmentContext.js.map +0 -1
  155. package/dist/legacy-runtime/hooks/ComposerContext.d.ts +0 -141
  156. package/dist/legacy-runtime/hooks/ComposerContext.d.ts.map +0 -1
  157. package/dist/legacy-runtime/hooks/ComposerContext.js +0 -65
  158. package/dist/legacy-runtime/hooks/ComposerContext.js.map +0 -1
  159. package/dist/legacy-runtime/hooks/MessageContext.d.ts +0 -155
  160. package/dist/legacy-runtime/hooks/MessageContext.d.ts.map +0 -1
  161. package/dist/legacy-runtime/hooks/MessageContext.js +0 -68
  162. package/dist/legacy-runtime/hooks/MessageContext.js.map +0 -1
  163. package/dist/legacy-runtime/hooks/MessagePartContext.d.ts +0 -44
  164. package/dist/legacy-runtime/hooks/MessagePartContext.d.ts.map +0 -1
  165. package/dist/legacy-runtime/hooks/MessagePartContext.js +0 -26
  166. package/dist/legacy-runtime/hooks/MessagePartContext.js.map +0 -1
  167. package/dist/legacy-runtime/hooks/ThreadContext.d.ts +0 -139
  168. package/dist/legacy-runtime/hooks/ThreadContext.d.ts.map +0 -1
  169. package/dist/legacy-runtime/hooks/ThreadContext.js +0 -84
  170. package/dist/legacy-runtime/hooks/ThreadContext.js.map +0 -1
  171. package/dist/legacy-runtime/hooks/ThreadListItemContext.d.ts +0 -44
  172. package/dist/legacy-runtime/hooks/ThreadListItemContext.d.ts.map +0 -1
  173. package/dist/legacy-runtime/hooks/ThreadListItemContext.js +0 -26
  174. package/dist/legacy-runtime/hooks/ThreadListItemContext.js.map +0 -1
  175. package/src/context/react/utils/createStateHookForRuntime.ts +0 -82
  176. package/src/legacy-runtime/hooks/AssistantContext.ts +0 -67
  177. package/src/legacy-runtime/hooks/AttachmentContext.ts +0 -99
  178. package/src/legacy-runtime/hooks/ComposerContext.ts +0 -131
  179. package/src/legacy-runtime/hooks/MessageContext.ts +0 -127
  180. package/src/legacy-runtime/hooks/MessagePartContext.ts +0 -32
  181. package/src/legacy-runtime/hooks/ThreadContext.ts +0 -120
  182. package/src/legacy-runtime/hooks/ThreadListItemContext.ts +0 -36
  183. package/src/tests/BaseComposerRuntimeCore.test.ts +0 -621
  184. package/src/tests/BaseSubscribable.test.ts +0 -54
  185. package/src/tests/DataRenderers.test.ts +0 -355
  186. package/src/tests/ExternalStoreThreadRuntimeCore.test.ts +0 -685
  187. package/src/tests/LocalThreadRuntimeCore.test.ts +0 -324
  188. package/src/tests/auiV0Encode.test.ts +0 -188
  189. package/src/tests/external-message-converter.test.ts +0 -346
  190. package/src/tests/shouldContinue.test.ts +0 -122
@@ -66,37 +66,6 @@ describe("MessagePrimitive.GenerativeUI (same-realm renderer)", () => {
66
66
  expect(out).toContain("hello world");
67
67
  });
68
68
 
69
- it("throws GenerativeUIRenderError for unknown components", () => {
70
- const spec: GenerativeUISpec = {
71
- root: { component: "NotAllowed", props: {} },
72
- };
73
-
74
- expect(() =>
75
- renderToStaticMarkup(
76
- <GenerativeUIRender spec={spec} components={{ Card }} />,
77
- ),
78
- ).toThrow(GenerativeUIRenderError);
79
- });
80
-
81
- it("uses Fallback when provided for unknown components", () => {
82
- const spec: GenerativeUISpec = {
83
- root: { component: "NotAllowed", props: { foo: 1 } },
84
- };
85
- const Fallback = ({ component }: any) => (
86
- <span data-fallback={component}>missing</span>
87
- );
88
-
89
- const out = renderToStaticMarkup(
90
- <GenerativeUIRender
91
- spec={spec}
92
- components={{ Card }}
93
- Fallback={Fallback}
94
- />,
95
- );
96
- expect(out).toContain('data-fallback="NotAllowed"');
97
- expect(out).toContain("missing");
98
- });
99
-
100
69
  it("handles empty / partial specs gracefully (stream-friendly)", () => {
101
70
  const out1 = renderToStaticMarkup(
102
71
  <GenerativeUIRender spec={{ root: [] }} components={{ Card }} />,
@@ -133,21 +102,7 @@ describe("MessagePrimitive.GenerativeUI (same-realm renderer)", () => {
133
102
  }
134
103
  });
135
104
 
136
- it("respects user-provided keys", () => {
137
- const spec: GenerativeUISpec = {
138
- root: [
139
- { component: "Card", props: { title: "x" }, key: "stable-1" },
140
- { component: "Card", props: { title: "y" }, key: "stable-2" },
141
- ],
142
- };
143
- expect(() =>
144
- renderToStaticMarkup(
145
- <GenerativeUIRender spec={spec} components={{ Card }} />,
146
- ),
147
- ).not.toThrow();
148
- });
149
-
150
- it("is a typed error subclass with the offending component name", () => {
105
+ it("throws a typed error naming the offending unknown component", () => {
151
106
  try {
152
107
  renderToStaticMarkup(
153
108
  <GenerativeUIRender
@@ -27,8 +27,7 @@ const createCountingAdapter = () => {
27
27
  };
28
28
 
29
29
  const userTexts = (aui: ReturnType<typeof useAui>) =>
30
- aui
31
- .thread()
30
+ aui.thread
32
31
  .getState()
33
32
  .messages.filter((m) => m.role === "user")
34
33
  .map((m) =>
@@ -57,8 +56,8 @@ const renderWithRuntime = (adapter: ChatModelAdapter, enableQueue: boolean) => {
57
56
 
58
57
  const send = async (aui: ReturnType<typeof useAui>, text: string) => {
59
58
  await act(async () => {
60
- aui.thread().composer().setText(text);
61
- aui.thread().composer().send();
59
+ aui.thread.composer().setText(text);
60
+ aui.thread.composer().send();
62
61
  await flush();
63
62
  });
64
63
  };
@@ -69,13 +68,12 @@ describe("local runtime message queue", () => {
69
68
  const aui = renderWithRuntime(adapter, true);
70
69
 
71
70
  await send(aui, "first");
72
- expect(aui.thread().getState().isRunning).toBe(true);
73
- expect(aui.thread().getState().capabilities.queue).toBe(true);
71
+ expect(aui.thread.getState().isRunning).toBe(true);
72
+ expect(aui.thread.getState().capabilities.queue).toBe(true);
74
73
 
75
74
  await send(aui, "second");
76
75
  expect(
77
- aui
78
- .thread()
76
+ aui.thread
79
77
  .composer()
80
78
  .getState()
81
79
  .queue.map((q) => q.prompt),
@@ -87,7 +85,7 @@ describe("local runtime message queue", () => {
87
85
  await flush();
88
86
  await flush();
89
87
  });
90
- expect(aui.thread().composer().getState().queue).toEqual([]);
88
+ expect(aui.thread.composer().getState().queue).toEqual([]);
91
89
  expect(userTexts(aui)).toContain("second");
92
90
  });
93
91
 
@@ -100,7 +98,7 @@ describe("local runtime message queue", () => {
100
98
 
101
99
  await send(aui, "a");
102
100
  await send(aui, "b");
103
- expect(aui.thread().composer().getState().queue).toHaveLength(2);
101
+ expect(aui.thread.composer().getState().queue).toHaveLength(2);
104
102
 
105
103
  await act(async () => {
106
104
  releases[0]!();
@@ -109,8 +107,7 @@ describe("local runtime message queue", () => {
109
107
  });
110
108
  expect(getRunCount()).toBe(2);
111
109
  expect(
112
- aui
113
- .thread()
110
+ aui.thread
114
111
  .composer()
115
112
  .getState()
116
113
  .queue.map((q) => q.prompt),
@@ -124,13 +121,13 @@ describe("local runtime message queue", () => {
124
121
  await send(aui, "first");
125
122
  await send(aui, "a");
126
123
  await send(aui, "b");
127
- expect(aui.thread().composer().getState().queue).toHaveLength(2);
124
+ expect(aui.thread.composer().getState().queue).toHaveLength(2);
128
125
 
129
126
  await act(async () => {
130
- aui.thread().composer().queueItem({ index: 0 }).remove();
127
+ aui.thread.composer().queueItem({ index: 0 }).remove();
131
128
  await flush();
132
129
  });
133
- const queue = aui.thread().composer().getState().queue;
130
+ const queue = aui.thread.composer().getState().queue;
134
131
  expect(queue).toHaveLength(1);
135
132
  expect(queue[0]!.prompt).toBe("b");
136
133
  });
@@ -142,15 +139,15 @@ describe("local runtime message queue", () => {
142
139
  await send(aui, "first");
143
140
  await send(aui, "a");
144
141
  await send(aui, "b");
145
- expect(aui.thread().composer().getState().queue).toHaveLength(2);
142
+ expect(aui.thread.composer().getState().queue).toHaveLength(2);
146
143
 
147
144
  await act(async () => {
148
- aui.thread().cancelRun();
145
+ aui.thread.cancelRun();
149
146
  await flush();
150
147
  await flush();
151
148
  });
152
149
 
153
- expect(aui.thread().composer().getState().queue).toEqual([]);
150
+ expect(aui.thread.composer().getState().queue).toEqual([]);
154
151
  // cancelling must not start the next queued message
155
152
  expect(getRunCount()).toBe(1);
156
153
  });
@@ -168,11 +165,11 @@ describe("local runtime message queue", () => {
168
165
  // start a second run and queue a message behind it
169
166
  await send(aui, "second");
170
167
  await send(aui, "queued");
171
- expect(aui.thread().composer().getState().queue).toHaveLength(1);
168
+ expect(aui.thread.composer().getState().queue).toHaveLength(1);
172
169
 
173
170
  // edit the first message while the run is in progress
174
171
  await act(async () => {
175
- const message = aui.thread().message({ index: 0 });
172
+ const message = aui.thread.message({ index: 0 });
176
173
  message.composer().beginEdit();
177
174
  message.composer().setText("edited");
178
175
  message.composer().send();
@@ -180,7 +177,7 @@ describe("local runtime message queue", () => {
180
177
  });
181
178
 
182
179
  // the edit is applied (branches the thread) and the stale queue is cleared
183
- expect(aui.thread().composer().getState().queue).toEqual([]);
180
+ expect(aui.thread.composer().getState().queue).toEqual([]);
184
181
  });
185
182
 
186
183
  it("buffers a send during a regenerate instead of interrupting it", async () => {
@@ -196,22 +193,21 @@ describe("local runtime message queue", () => {
196
193
 
197
194
  // regenerate the assistant message: a run started outside the queue
198
195
  await act(async () => {
199
- aui.thread().message({ index: 1 }).reload();
196
+ aui.thread.message({ index: 1 }).reload();
200
197
  await flush();
201
198
  });
202
199
  expect(getRunCount()).toBe(2);
203
- expect(aui.thread().getState().isRunning).toBe(true);
200
+ expect(aui.thread.getState().isRunning).toBe(true);
204
201
 
205
202
  // sending now must buffer, not interrupt the regenerate
206
203
  await act(async () => {
207
- aui.thread().composer().setText("Y");
208
- aui.thread().composer().send();
204
+ aui.thread.composer().setText("Y");
205
+ aui.thread.composer().send();
209
206
  await flush();
210
207
  });
211
208
  expect(getRunCount()).toBe(2);
212
209
  expect(
213
- aui
214
- .thread()
210
+ aui.thread
215
211
  .composer()
216
212
  .getState()
217
213
  .queue.map((q) => q.prompt),
@@ -239,7 +235,7 @@ describe("local runtime message queue", () => {
239
235
  await send(aui, "first");
240
236
  await send(aui, "a");
241
237
  await send(aui, "b");
242
- expect(aui.thread().composer().getState().queue).toHaveLength(2);
238
+ expect(aui.thread.composer().getState().queue).toHaveLength(2);
243
239
 
244
240
  // run 1 settles -> "a" drains (run 2 throws) -> "b" drains (run 3)
245
241
  await act(async () => {
@@ -248,18 +244,17 @@ describe("local runtime message queue", () => {
248
244
  await flush();
249
245
  });
250
246
  expect(runCount).toBe(3);
251
- expect(aui.thread().composer().getState().queue).toEqual([]);
247
+ expect(aui.thread.composer().getState().queue).toEqual([]);
252
248
 
253
249
  // "b" is running; a new send must buffer behind it, not interrupt it
254
250
  await act(async () => {
255
- aui.thread().composer().setText("c");
256
- aui.thread().composer().send();
251
+ aui.thread.composer().setText("c");
252
+ aui.thread.composer().send();
257
253
  await flush();
258
254
  });
259
255
  expect(runCount).toBe(3);
260
256
  expect(
261
- aui
262
- .thread()
257
+ aui.thread
263
258
  .composer()
264
259
  .getState()
265
260
  .queue.map((q) => q.prompt),
@@ -269,7 +264,7 @@ describe("local runtime message queue", () => {
269
264
  it("does not expose the queue capability when the flag is off", async () => {
270
265
  const { adapter } = createCountingAdapter();
271
266
  const aui = renderWithRuntime(adapter, false);
272
- expect(aui.thread().getState().capabilities.queue).toBe(false);
267
+ expect(aui.thread.getState().capabilities.queue).toBe(false);
273
268
  });
274
269
 
275
270
  it("tears the queue down when the flag is toggled off at runtime", async () => {
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { act, cleanup, render, waitFor } from "@testing-library/react";
4
4
  import { flushSync } from "react-dom";
5
- import { useEffect, useState, type FC } from "react";
6
- import { afterEach, describe, expect, it } from "vitest";
5
+ import { Component, useEffect, useState, type FC, type ReactNode } from "react";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
7
  import { useAui, useAuiState } from "@assistant-ui/store";
8
8
  import { AssistantRuntimeProvider, PartByIndexProvider } from "../context";
9
9
  import { useLocalRuntime } from "../legacy-runtime/runtime-cores/local/useLocalRuntime";
@@ -83,14 +83,35 @@ const ChildrenMessage: FC = () => (
83
83
  );
84
84
 
85
85
  const InvalidPartReader: FC = () => {
86
- useAuiState((s) => s.part.type);
87
- return null;
86
+ const part = useAuiState((s) => s.part);
87
+ return (
88
+ <p data-testid="invalid-part">
89
+ {part.type === "text" ? part.text : part.type}
90
+ </p>
91
+ );
88
92
  };
89
93
 
94
+ class PartErrorBoundary extends Component<
95
+ { children: ReactNode },
96
+ { error: Error | null }
97
+ > {
98
+ override state: { error: Error | null } = { error: null };
99
+ static getDerivedStateFromError(error: Error) {
100
+ return { error };
101
+ }
102
+ override render() {
103
+ if (this.state.error)
104
+ return <p data-testid="part-error">{this.state.error.message}</p>;
105
+ return this.props.children;
106
+ }
107
+ }
108
+
90
109
  const InvalidPartMessage: FC = () => (
91
- <PartByIndexProvider index={2}>
92
- <InvalidPartReader />
93
- </PartByIndexProvider>
110
+ <PartErrorBoundary>
111
+ <PartByIndexProvider index={2}>
112
+ <InvalidPartReader />
113
+ </PartByIndexProvider>
114
+ </PartErrorBoundary>
94
115
  );
95
116
 
96
117
  const App: FC<{ MessageComponent?: FC }> = ({ MessageComponent = Message }) => {
@@ -111,8 +132,8 @@ afterEach(() => {
111
132
  harness.runtime = null;
112
133
  });
113
134
 
114
- describe("part hooks under a thread-switch race (#5118)", () => {
115
- it("an out-of-band flushSync re-render during the swap window does not crash", async () => {
135
+ describe("part hooks under a thread-switch race", () => {
136
+ it("swaps every part when the incoming message has different part types", async () => {
116
137
  let view!: ReturnType<typeof render>;
117
138
  await act(async () => {
118
139
  view = render(<App />);
@@ -148,11 +169,21 @@ describe("part hooks under a thread-switch race (#5118)", () => {
148
169
  },
149
170
  );
150
171
 
151
- it("still rejects an index that was never valid", async () => {
152
- await expect(
153
- act(async () => {
154
- render(<App MessageComponent={InvalidPartMessage} />);
155
- }),
156
- ).rejects.toThrow("useClientLookup: Index 2 out of bounds (length: 2)");
172
+ it("throws to the error boundary for an index that was never valid", async () => {
173
+ const consoleError = vi
174
+ .spyOn(console, "error")
175
+ .mockImplementation(() => {});
176
+ try {
177
+ let view!: ReturnType<typeof render>;
178
+ await act(async () => {
179
+ view = render(<App MessageComponent={InvalidPartMessage} />);
180
+ });
181
+ expect(view.queryByTestId("invalid-part")).toBeNull();
182
+ expect(view.getByTestId("part-error").textContent).toContain(
183
+ "useClientLookup: index 2 out of bounds (length: 2)",
184
+ );
185
+ } finally {
186
+ consoleError.mockRestore();
187
+ }
157
188
  });
158
189
  });
@@ -69,7 +69,7 @@ const renderToolCallMessage = (renderPart: () => ReactNode) => {
69
69
  );
70
70
  };
71
71
 
72
- describe("part hooks tolerate a transient part-type mismatch (#5118)", () => {
72
+ describe("part hooks tolerate a transient part-type mismatch", () => {
73
73
  it("MessagePartPrimitive.Text renders empty when the part is a tool-call", async () => {
74
74
  renderToolCallMessage(() => (
75
75
  <MessagePartPrimitive.Text data-testid="text" />
@@ -121,7 +121,7 @@ describe("part hooks tolerate a transient part-type mismatch (#5118)", () => {
121
121
  expect(file.textContent).toBe("");
122
122
  });
123
123
 
124
- it("a real text part still renders its text (no regression in the happy path)", async () => {
124
+ it("a real text part still renders its text", async () => {
125
125
  const textMessage: ThreadMessageLike[] = [
126
126
  {
127
127
  role: "assistant",
@@ -215,10 +215,3 @@ describe("ThreadPrimitive.Unstable_MessageById", () => {
215
215
  expect(screen.getByTestId("by-id").textContent).toBe("first");
216
216
  });
217
217
  });
218
-
219
- describe("exports", () => {
220
- it("exposes the unstable id-keyed surface", () => {
221
- expect(typeof unstable_useThreadMessageIds).toBe("function");
222
- expect(ThreadPrimitive.Unstable_MessageById).toBeDefined();
223
- });
224
- });
@@ -18,11 +18,11 @@ const flushTapSync = vi.fn((fn: () => void) => fn());
18
18
 
19
19
  vi.mock("@assistant-ui/store", () => ({
20
20
  useAui: () => ({
21
- composer: () => ({
21
+ composer: {
22
22
  getState: () => ({ isEditing: fixture.composer.isEditing }),
23
23
  setText: composerSetText,
24
24
  send: composerSend,
25
- }),
25
+ },
26
26
  }),
27
27
  useAuiState: (selector: (s: unknown) => unknown) =>
28
28
  selector({
@@ -83,9 +83,9 @@ export function unstable_useComposerInput(
83
83
 
84
84
  const setText = useCallback(
85
85
  (text: string) => {
86
- if (!aui.composer().getState().isEditing) return;
86
+ if (!aui.composer.getState().isEditing) return;
87
87
  flushTapSync(() => {
88
- aui.composer().setText(text);
88
+ aui.composer.setText(text);
89
89
  });
90
90
  },
91
91
  [aui],
@@ -12,11 +12,11 @@ const setText = vi.fn();
12
12
 
13
13
  vi.mock("@assistant-ui/store", () => ({
14
14
  useAui: () => ({
15
- composer: () => ({
15
+ composer: {
16
16
  getState: () => ({ type: fixture.composerType }),
17
17
  setText,
18
- }),
19
- thread: () => ({ getState: () => ({ messages: fixture.messages }) }),
18
+ },
19
+ thread: { getState: () => ({ messages: fixture.messages }) },
20
20
  on: (_event: string, cb: () => void) => {
21
21
  fixture.switchedToHandlers.push(cb);
22
22
  return () => {};
@@ -70,7 +70,7 @@ export function unstable_useComposerInputHistory(): Unstable_ComposerInputHistor
70
70
  const browseRef = useRef<BrowseState | null>(null);
71
71
 
72
72
  useEffect(() => {
73
- if (aui.composer().getState().type !== "thread") return undefined;
73
+ if (aui.composer.getState().type !== "thread") return undefined;
74
74
 
75
75
  return aui.on("threadListItem.switchedTo", () => {
76
76
  browseRef.current = null;
@@ -84,7 +84,7 @@ export function unstable_useComposerInputHistory(): Unstable_ComposerInputHistor
84
84
  if (e.nativeEvent.isComposing) return;
85
85
  if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
86
86
  if (popoverCtx && popoverCtx.getActiveAria() !== null) return;
87
- if (aui.composer().getState().type !== "thread") return;
87
+ if (aui.composer.getState().type !== "thread") return;
88
88
 
89
89
  const textarea = e.currentTarget;
90
90
  const { selectionStart, selectionEnd, value } = textarea;
@@ -96,7 +96,7 @@ export function unstable_useComposerInputHistory(): Unstable_ComposerInputHistor
96
96
  const browse = browseRef.current;
97
97
 
98
98
  const commitText = (text: string): void => {
99
- flushTapSync(() => aui.composer().setText(text));
99
+ flushTapSync(() => aui.composer.setText(text));
100
100
  // React's controlled-value commit restores the pre-recall caret;
101
101
  // reposition after the commit, before paint.
102
102
  requestAnimationFrame(() => {
@@ -124,13 +124,13 @@ export function unstable_useComposerInputHistory(): Unstable_ComposerInputHistor
124
124
 
125
125
  if (!browse) {
126
126
  if (value.trim() !== "") return;
127
- const history = deriveHistory(aui.thread().getState().messages);
127
+ const history = deriveHistory(aui.thread.getState().messages);
128
128
  if (history.length === 0) return;
129
129
  recall(history, 0, value);
130
130
  return;
131
131
  }
132
132
 
133
- const history = deriveHistory(aui.thread().getState().messages);
133
+ const history = deriveHistory(aui.thread.getState().messages);
134
134
  const next = browse.cursor + 1;
135
135
  if (next >= history.length) {
136
136
  e.preventDefault();
@@ -150,7 +150,7 @@ export function unstable_useComposerInputHistory(): Unstable_ComposerInputHistor
150
150
  return;
151
151
  }
152
152
 
153
- const history = deriveHistory(aui.thread().getState().messages);
153
+ const history = deriveHistory(aui.thread.getState().messages);
154
154
  recall(history, next, browse.draftSnapshot);
155
155
  },
156
156
  [aui, popoverCtx],
@@ -107,7 +107,7 @@ export function unstable_useMentionAdapter(
107
107
  const adapter = useMemo<Unstable_TriggerAdapter>(() => {
108
108
  const getModelContextTools = (): Unstable_TriggerItem[] => {
109
109
  if (!wantsTools) return [];
110
- const ctx = aui.thread().getModelContext();
110
+ const ctx = aui.thread.getModelContext();
111
111
  const tools = ctx.tools;
112
112
  if (!tools) return [];
113
113
  const formatLabel = toolsConfig?.formatLabel;
@@ -38,7 +38,7 @@ export const SmoothContextProvider: FC<PropsWithChildren> = ({ children }) => {
38
38
  const aui = useAui();
39
39
 
40
40
  const [context] = useState(() =>
41
- makeSmoothContext(aui.part().getState().status),
41
+ makeSmoothContext(aui.part.getState().status),
42
42
  );
43
43
 
44
44
  // do not wrap if there is an outer SmoothContextProvider
@@ -9,7 +9,7 @@ import type {
9
9
 
10
10
  const part = {};
11
11
  vi.mock("@assistant-ui/store", () => ({
12
- useAui: () => ({ part: () => part }),
12
+ useAui: () => ({ part }),
13
13
  useAuiState: (selector: () => unknown) => selector(),
14
14
  }));
15
15
  vi.mock("./SmoothContext", () => ({
@@ -58,10 +58,13 @@ class TextStreamAnimator {
58
58
  public maxCharsPerFrame: number = Infinity;
59
59
  public minCommitMs: number = 0;
60
60
 
61
- constructor(
62
- public currentText: string,
63
- private setText: (newText: string) => void,
64
- ) {}
61
+ public currentText: string;
62
+ private setText: (newText: string) => void;
63
+
64
+ constructor(currentText: string, setText: (newText: string) => void) {
65
+ this.currentText = currentText;
66
+ this.setText = setText;
67
+ }
65
68
 
66
69
  start() {
67
70
  if (this.animationFrameId !== null) return;
@@ -178,7 +181,7 @@ export const useSmooth = (
178
181
  // of relying on a render-time proxy reference that may be stable
179
182
  // across thread swaps.
180
183
  const aui = useAui();
181
- const part = useAuiState(() => aui.part());
184
+ const part = useAuiState(() => aui.part);
182
185
  const [prevPart, setPrevPart] = useState(part);
183
186
  if (part !== prevPart || !text.startsWith(displayedText)) {
184
187
  setPrevPart(part);
@@ -1,34 +0,0 @@
1
- import { SubscribableRuntime } from "./useRuntimeState.js";
2
- //#region src/context/react/utils/createStateHookForRuntime.d.ts
3
- declare function createStateHookForRuntime<TState>(useRuntime: (options: {
4
- optional: boolean | undefined;
5
- }) => SubscribableRuntime<TState> | null): {
6
- (): TState;
7
- <TSelected>(selector: (state: TState) => TSelected): TSelected;
8
- <TSelected>(selector: ((state: TState) => TSelected) | undefined): TSelected | TState;
9
- (options: {
10
- optional?: false | undefined;
11
- }): TState;
12
- (options: {
13
- optional?: boolean | undefined;
14
- }): TState | null;
15
- <TSelected>(options: {
16
- optional?: false | undefined;
17
- selector: (state: TState) => TSelected;
18
- }): TSelected;
19
- <TSelected>(options: {
20
- optional?: false | undefined;
21
- selector: ((state: TState) => TSelected) | undefined;
22
- }): TSelected | TState;
23
- <TSelected>(options: {
24
- optional?: boolean | undefined;
25
- selector: (state: TState) => TSelected;
26
- }): TSelected | null;
27
- <TSelected>(options: {
28
- optional?: boolean | undefined;
29
- selector: ((state: TState) => TSelected) | undefined;
30
- }): TSelected | TState | null;
31
- };
32
- //#endregion
33
- export { createStateHookForRuntime };
34
- //# sourceMappingURL=createStateHookForRuntime.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"createStateHookForRuntime.d.ts","names":[],"sources":["../../../../src/context/react/utils/createStateHookForRuntime.ts"],"mappings":";;iBAKgB,0BAA0B,QACxC,aAAa;EACX;MACI,oBAAoB;MAGD;GAGH,WAAS,WAClB,OAAO,WAAW,YAC5B;GAGmB,WAAS,YACjB,OAAO,WAAW,yBAC7B,YAAY;GAGgB;IAAE;MAAiC;GAGnC;IAC7B;MACE;GAGkB,WAAS;IAC7B;IACA,WAAW,OAAO,WAAW;MAC3B;GAGkB,WAAS;IAC7B;IACA,YAAY,OAAO,WAAW;MAC5B,YAAY;GAGM,WAAS;IAC7B;IACA,WAAW,OAAO,WAAW;MAC3B;GAGkB,WAAS;IAC7B;IACA,YAAY,OAAO,WAAW;MAC5B,YAAY"}
@@ -1,21 +0,0 @@
1
- import { useRuntimeStateInternal } from "./useRuntimeState.js";
2
- //#region src/context/react/utils/createStateHookForRuntime.ts
3
- function createStateHookForRuntime(useRuntime) {
4
- function useStoreHook(param) {
5
- let optional = false;
6
- let selector;
7
- if (typeof param === "function") selector = param;
8
- else if (param) {
9
- optional = !!param.optional;
10
- selector = param.selector;
11
- }
12
- const store = useRuntime({ optional });
13
- if (!store) return null;
14
- return useRuntimeStateInternal(store, selector);
15
- }
16
- return useStoreHook;
17
- }
18
- //#endregion
19
- export { createStateHookForRuntime };
20
-
21
- //# sourceMappingURL=createStateHookForRuntime.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"createStateHookForRuntime.js","names":["SubscribableRuntime","useRuntimeStateInternal","createStateHookForRuntime","useRuntime","options","optional","TState","useStoreHook","selector","state","TSelected","param","store"],"sources":["../../../../src/context/react/utils/createStateHookForRuntime.ts"],"sourcesContent":["import {\n type SubscribableRuntime,\n useRuntimeStateInternal,\n} from \"./useRuntimeState\";\n\nexport function createStateHookForRuntime<TState>(\n useRuntime: (options: {\n optional: boolean | undefined;\n }) => SubscribableRuntime<TState> | null,\n) {\n // empty\n function useStoreHook(): TState;\n\n // selector\n function useStoreHook<TSelected>(\n selector: (state: TState) => TSelected,\n ): TSelected;\n\n // selector?\n function useStoreHook<TSelected>(\n selector: ((state: TState) => TSelected) | undefined,\n ): TSelected | TState;\n\n // optional=false\n function useStoreHook(options: { optional?: false | undefined }): TState;\n\n // optional?\n function useStoreHook(options: {\n optional?: boolean | undefined;\n }): TState | null;\n\n // optional=false, selector\n function useStoreHook<TSelected>(options: {\n optional?: false | undefined;\n selector: (state: TState) => TSelected;\n }): TSelected;\n\n // optional=false, selector?\n function useStoreHook<TSelected>(options: {\n optional?: false | undefined;\n selector: ((state: TState) => TSelected) | undefined;\n }): TSelected | TState;\n\n // optional?, selector\n function useStoreHook<TSelected>(options: {\n optional?: boolean | undefined;\n selector: (state: TState) => TSelected;\n }): TSelected | null;\n\n // optional?, selector?\n function useStoreHook<TSelected>(options: {\n optional?: boolean | undefined;\n selector: ((state: TState) => TSelected) | undefined;\n }): TSelected | TState | null;\n\n function useStoreHook<TSelected>(\n param?:\n | ((state: TState) => TSelected)\n | {\n optional?: boolean | undefined;\n selector?: ((state: TState) => TSelected) | undefined;\n },\n ): TSelected | TState | null {\n let optional = false;\n let selector: ((state: TState) => TSelected) | undefined;\n\n if (typeof param === \"function\") {\n selector = param;\n } else if (param) {\n optional = !!param.optional;\n selector = param.selector;\n }\n\n const store = useRuntime({ optional });\n if (!store) return null;\n\n // it is ok to call useRuntimeStateInternal conditionally because it will never become null if its available\n return useRuntimeStateInternal(store, selector);\n }\n\n return useStoreHook;\n}\n"],"mappings":";;AAKA,SAAgBE,0BACdC,YAGA;CA8CA,SAASI,aACPI,OAM2B;EAC3B,IAAIN,WAAW;EACf,IAAIG;EAEJ,IAAI,OAAOG,UAAU,YACnBH,WAAWG;OACN,IAAIA,OAAO;GAChBN,WAAW,CAAC,CAACM,MAAMN;GACnBG,WAAWG,MAAMH;EACnB;EAEA,MAAMI,QAAQT,WAAW,EAAEE,SAAS,CAAC;EACrC,IAAI,CAACO,OAAO,OAAO;EAGnB,OAAOX,wBAAwBW,OAAOJ,QAAQ;CAChD;CAEA,OAAOD;AACT"}