@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
@@ -1,4 +1,11 @@
1
- import { useState, useMemo, useEffect, useEffectEvent } from "react";
1
+ import {
2
+ useState,
3
+ useMemo,
4
+ useEffect,
5
+ useEffectEvent,
6
+ useCallback,
7
+ useRef,
8
+ } from "react";
2
9
  import { resource, withKey } from "@assistant-ui/tap";
3
10
  import {
4
11
  type ClientElement,
@@ -10,19 +17,28 @@ import {
10
17
  } from "@assistant-ui/store";
11
18
 
12
19
  import type {
20
+ AddToolResultOptions,
13
21
  AppendMessage,
14
22
  Attachment,
23
+ AttachmentAdapter,
15
24
  CreateAttachment,
25
+ PendingAttachment,
16
26
  RespondToToolApprovalOptions,
27
+ ResumeToolCallOptions,
28
+ MessagePartStatus,
17
29
  ThreadAssistantMessagePart,
18
30
  ThreadUserMessagePart,
19
31
  ThreadMessage,
32
+ ToolCallMessagePartStatus,
20
33
  ExternalThreadQueueAdapter,
21
34
  ExternalThreadBranchAdapter,
22
35
  } from "@assistant-ui/core";
36
+ import { ToolResponse } from "assistant-stream";
37
+ import type { ReadonlyJSONValue } from "assistant-stream/utils";
23
38
  import type { QueueItemState } from "@assistant-ui/core/store";
24
39
  import type { ComposerSendOptions } from "@assistant-ui/core/store";
25
40
  import {
41
+ fileMatchesAccept,
26
42
  getThreadMessageText,
27
43
  isCreateAttachment,
28
44
  resolveToolApprovalResponse,
@@ -38,9 +54,35 @@ export type ExternalThreadMessage = ThreadMessage & {
38
54
  id: string;
39
55
  };
40
56
 
57
+ const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
58
+ type: "complete",
59
+ });
60
+
61
+ // Legacy runtime parity (toMessagePartStatus): a tool call without a result
62
+ // carries its message's status; the last part of a streaming message streams.
63
+ const derivePartStatus = (
64
+ message: ExternalThreadMessage,
65
+ partIndex: number,
66
+ part: ThreadAssistantMessagePart | ThreadUserMessagePart,
67
+ ): ToolCallMessagePartStatus => {
68
+ if (message.role !== "assistant" || !message.status) return COMPLETE_STATUS;
69
+
70
+ if (part.type === "tool-call") {
71
+ if (!part.result) return message.status as ToolCallMessagePartStatus;
72
+ return COMPLETE_STATUS;
73
+ }
74
+
75
+ const isLastPart = partIndex === Math.max(0, message.content.length - 1);
76
+ if (message.status.type === "requires-action") return COMPLETE_STATUS;
77
+ return isLastPart ? (message.status as MessagePartStatus) : COMPLETE_STATUS;
78
+ };
79
+
41
80
  export type ExternalThreadProps = {
42
81
  messages: readonly ExternalThreadMessage[];
43
82
  isRunning?: boolean;
83
+ isLoading?: boolean | undefined;
84
+ state?: ReadonlyJSONValue | undefined;
85
+ extras?: unknown;
44
86
  /**
45
87
  * Whether sending new messages is currently disabled. When `true`, the
46
88
  * thread composer's input remains usable but `send()` is a no-op and
@@ -57,6 +99,12 @@ export type ExternalThreadProps = {
57
99
  onReload?: (parentId: string | null) => void;
58
100
  onStartRun?: () => void;
59
101
  onCancel?: () => void;
102
+ onResume?: (() => void) | undefined;
103
+ onAddToolResult?: ((options: AddToolResultOptions) => void) | undefined;
104
+ /** Callback for resuming a tool call that is waiting for human input. */
105
+ onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
106
+ onLoadExternalState?: ((state: unknown) => void) | undefined;
107
+ attachmentAdapter?: AttachmentAdapter | undefined;
60
108
  /** Queue adapter for runtimes that support message queuing and steering. */
61
109
  queue?: ExternalThreadQueueAdapter;
62
110
  /** Branch adapter for runtimes that track sibling variants of messages. */
@@ -68,6 +116,7 @@ export type ExternalThreadProps = {
68
116
  type MessageClientProps = {
69
117
  message: ExternalThreadMessage;
70
118
  index: number;
119
+ parentId: string | null;
71
120
  onEdit?: (message: AppendMessage) => void;
72
121
  onReload?: () => void;
73
122
  queue?: ExternalThreadQueueAdapter | undefined;
@@ -75,17 +124,24 @@ type MessageClientProps = {
75
124
  onRespondToToolApproval?:
76
125
  | ((options: RespondToToolApprovalOptions) => void)
77
126
  | undefined;
127
+ onAddToolResult?: ((options: AddToolResultOptions) => void) | undefined;
128
+ onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
129
+ attachmentAdapter?: AttachmentAdapter | undefined;
78
130
  };
79
131
 
80
132
  // Message Client - minimal implementation
81
133
  const useMessageClient = ({
82
134
  message,
83
135
  index,
136
+ parentId,
84
137
  onEdit,
85
138
  onReload,
86
139
  queue,
87
140
  branches,
88
141
  onRespondToToolApproval,
142
+ onAddToolResult,
143
+ onResumeToolCall,
144
+ attachmentAdapter,
89
145
  }: MessageClientProps): ClientOutput<"message"> => {
90
146
  const [isCopied, setIsCopied] = useState(false);
91
147
  const [isHovering, setIsHovering] = useState(false);
@@ -93,7 +149,17 @@ const useMessageClient = ({
93
149
 
94
150
  const partClients = useClientLookup(
95
151
  message.content.map((part, idx) =>
96
- withKey(idx, PartResource({ part, onRespondToToolApproval })),
152
+ withKey(
153
+ idx,
154
+ PartResource({
155
+ part,
156
+ status: derivePartStatus(message, idx, part),
157
+ messageId: message.id,
158
+ onRespondToToolApproval,
159
+ onAddToolResult,
160
+ onResumeToolCall,
161
+ }),
162
+ ),
97
163
  ),
98
164
  );
99
165
 
@@ -121,7 +187,7 @@ const useMessageClient = ({
121
187
  queue?.clear("edit");
122
188
  onEdit?.({
123
189
  ...msg,
124
- parentId: message.id,
190
+ parentId,
125
191
  sourceId: message.id,
126
192
  });
127
193
  setIsEditing(false);
@@ -137,6 +203,7 @@ const useMessageClient = ({
137
203
  onSend: handleSendEdit,
138
204
  message,
139
205
  queue,
206
+ attachmentAdapter,
140
207
  }),
141
208
  );
142
209
 
@@ -149,7 +216,7 @@ const useMessageClient = ({
149
216
  return {
150
217
  ...message,
151
218
  attachments: message.attachments ?? [],
152
- parentId: null,
219
+ parentId,
153
220
  isLast: false, // Will be set by thread
154
221
  branchNumber,
155
222
  branchCount,
@@ -162,6 +229,7 @@ const useMessageClient = ({
162
229
  };
163
230
  }, [
164
231
  message,
232
+ parentId,
165
233
  isCopied,
166
234
  isHovering,
167
235
  index,
@@ -220,28 +288,65 @@ const MessageClient = resource(useMessageClient);
220
288
 
221
289
  type PartResourceProps = {
222
290
  part: ThreadAssistantMessagePart | ThreadUserMessagePart;
291
+ status: ToolCallMessagePartStatus;
292
+ messageId: string;
223
293
  onRespondToToolApproval?:
224
294
  | ((options: RespondToToolApprovalOptions) => void)
225
295
  | undefined;
296
+ onAddToolResult?: ((options: AddToolResultOptions) => void) | undefined;
297
+ onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
226
298
  };
227
299
 
228
300
  // Part Client - minimal implementation
229
301
  const usePartResource = ({
230
302
  part,
303
+ status,
304
+ messageId,
231
305
  onRespondToToolApproval,
306
+ onAddToolResult,
307
+ onResumeToolCall,
232
308
  }: PartResourceProps): ClientOutput<"part"> => {
233
309
  const state = useMemo(
234
310
  () => ({
235
311
  ...part,
236
- status: { type: "complete" as const },
312
+ status,
237
313
  }),
238
- [part],
314
+ [part, status],
239
315
  );
240
316
 
241
317
  return {
242
318
  getState: () => state,
243
- addToolResult: () => {},
244
- resumeToolCall: () => {},
319
+ addToolResult: (result) => {
320
+ if (!onAddToolResult)
321
+ throw new Error(
322
+ "Runtime does not support tool results (onAddToolResult is not set).",
323
+ );
324
+ if (part.type !== "tool-call")
325
+ throw new Error("Tried to add tool result on non-tool message part");
326
+
327
+ const response = ToolResponse.toResponse(result);
328
+ onAddToolResult({
329
+ messageId,
330
+ toolName: part.toolName,
331
+ toolCallId: part.toolCallId,
332
+ result: response.result as ReadonlyJSONValue,
333
+ isError: response.isError,
334
+ ...(response.artifact !== undefined && { artifact: response.artifact }),
335
+ ...(response.modelContent !== undefined && {
336
+ modelContent: response.modelContent,
337
+ }),
338
+ });
339
+ },
340
+ resumeToolCall: (payload) => {
341
+ if (!onResumeToolCall)
342
+ throw new Error(
343
+ "Runtime does not support resuming tool calls (onResumeToolCall is not set).",
344
+ );
345
+ if (part.type !== "tool-call")
346
+ throw new Error("Tried to resume tool call on non-tool message part");
347
+
348
+ onResumeToolCall({ toolCallId: part.toolCallId, payload });
349
+ },
245
350
  respondToToolApproval: (response) => {
246
351
  if (!onRespondToToolApproval)
247
352
  throw new Error("Runtime does not support tool approvals.");
@@ -269,7 +374,7 @@ const PartResource = resource(usePartResource);
269
374
 
270
375
  type AttachmentResourceProps = {
271
376
  attachment: Attachment;
272
- onRemove?: () => void;
377
+ onRemove?: () => void | Promise<void>;
273
378
  };
274
379
 
275
380
  // Attachment Client - minimal implementation
@@ -280,7 +385,7 @@ const useAttachmentResource = ({
280
385
  return {
281
386
  getState: () => attachment,
282
387
  remove: async () => {
283
- onRemove?.();
388
+ await onRemove?.();
284
389
  },
285
390
  };
286
391
  };
@@ -297,6 +402,7 @@ type ComposerClientResourceProps = {
297
402
  onSend?: (message: AppendMessage) => void;
298
403
  message?: ExternalThreadMessage;
299
404
  queue?: ExternalThreadQueueAdapter | undefined;
405
+ attachmentAdapter?: AttachmentAdapter | undefined;
300
406
  };
301
407
 
302
408
  const useQueueItemClient = ({
@@ -317,6 +423,33 @@ const useQueueItemClient = ({
317
423
 
318
424
  const QueueItemClient = resource(useQueueItemClient);
319
425
 
426
+ const drainAdapterAdd = async (
427
+ result: ReturnType<AttachmentAdapter["add"]>,
428
+ upsert: (attachment: Attachment) => void,
429
+ ) => {
430
+ if (Symbol.asyncIterator in result) {
431
+ for await (const attachment of result) {
432
+ upsert(attachment);
433
+ }
434
+ } else {
435
+ upsert(await result);
436
+ }
437
+ };
438
+
439
+ // State whose setter tracks the latest value in a ref, so imperative
440
+ // call sequences (setText immediately followed by send) observe the write
441
+ // before React re-renders — legacy composer parity.
442
+ const useLiveState = <T>(initial: T) => {
443
+ const [state, setState] = useState(initial);
444
+ const ref = useRef(state);
445
+ const set = useCallback((next: T | ((prev: T) => T)) => {
446
+ ref.current =
447
+ typeof next === "function" ? (next as (prev: T) => T)(ref.current) : next;
448
+ setState(ref.current);
449
+ }, []);
450
+ return [state, set, ref] as const;
451
+ };
452
+
320
453
  // Composer Client - minimal implementation
321
454
  const useComposerClientResource = ({
322
455
  type,
@@ -328,12 +461,19 @@ const useComposerClientResource = ({
328
461
  onSend,
329
462
  message,
330
463
  queue,
464
+ attachmentAdapter,
331
465
  }: ComposerClientResourceProps): ClientOutput<"composer"> => {
332
- const [text, setText] = useState("");
333
- const [role, setRole] = useState<"user" | "assistant" | "system">("user");
334
- const [runConfig, setRunConfig] = useState<Record<string, unknown>>({});
335
- const [attachments, setAttachments] = useState<readonly Attachment[]>([]);
336
- const [quote, setQuote] = useState<
466
+ const [text, setText, textRef] = useLiveState("");
467
+ const [role, setRole, roleRef] = useLiveState<
468
+ "user" | "assistant" | "system"
469
+ >("user");
470
+ const [runConfig, setRunConfig, runConfigRef] = useLiveState<
471
+ Record<string, unknown>
472
+ >({});
473
+ const [attachments, setAttachments, attachmentsRef] = useLiveState<
474
+ readonly Attachment[]
475
+ >([]);
476
+ const [quote, setQuote, quoteRef] = useLiveState<
337
477
  { readonly text: string; readonly messageId: string } | undefined
338
478
  >(undefined);
339
479
 
@@ -359,19 +499,41 @@ const useComposerClientResource = ({
359
499
  }, [isEditing]);
360
500
 
361
501
  const attachmentClients = useClientLookup(
362
- attachments.map((attachment, idx) =>
502
+ attachments.map((attachment) =>
363
503
  withKey(
364
504
  attachment.id,
365
505
  AttachmentResource({
366
506
  attachment,
367
- onRemove: () => {
368
- setAttachments(attachments.filter((_, i) => i !== idx));
507
+ onRemove: async () => {
508
+ await attachmentAdapter?.remove(attachment);
509
+ setAttachments((prev) =>
510
+ prev.filter((a) => a.id !== attachment.id),
511
+ );
369
512
  },
370
513
  }),
371
514
  ),
372
515
  ),
373
516
  );
374
517
 
518
+ const removePendingAttachments = async (removed: readonly Attachment[]) => {
519
+ if (!attachmentAdapter) return;
520
+ await Promise.all(
521
+ removed
522
+ .filter((a) => a.status.type !== "complete")
523
+ .map((a) => attachmentAdapter.remove(a)),
524
+ );
525
+ };
526
+
527
+ const upsertAttachment = (attachment: Attachment) => {
528
+ setAttachments((prev) => {
529
+ const idx = prev.findIndex((a) => a.id === attachment.id);
530
+ if (idx === -1) return [...prev, attachment];
531
+ const next = [...prev];
532
+ next[idx] = attachment;
533
+ return next;
534
+ });
535
+ };
536
+
375
537
  const queueItems = queue?.items ?? EMPTY_QUEUE_ITEMS;
376
538
  const queueItemClients = useClientLookup(
377
539
  queueItems.map((item) =>
@@ -396,7 +558,7 @@ const useComposerClientResource = ({
396
558
  isEditing,
397
559
  canCancel,
398
560
  canSend: isEditing && !isEmpty && !isSendDisabled,
399
- attachmentAccept: "*",
561
+ attachmentAccept: attachmentAdapter?.accept ?? "*",
400
562
  isEmpty,
401
563
  type,
402
564
  dictation: undefined,
@@ -415,6 +577,7 @@ const useComposerClientResource = ({
415
577
  attachments.length,
416
578
  quote,
417
579
  queueItems,
580
+ attachmentAdapter?.accept,
418
581
  ]);
419
582
 
420
583
  return {
@@ -423,7 +586,24 @@ const useComposerClientResource = ({
423
586
  setRole,
424
587
  setRunConfig,
425
588
  addAttachment: async (fileOrAttachment: File | CreateAttachment) => {
426
- if (!isCreateAttachment(fileOrAttachment)) {
589
+ if (attachmentAdapter) {
590
+ const file = isCreateAttachment(fileOrAttachment)
591
+ ? {
592
+ name: fileOrAttachment.name,
593
+ type: fileOrAttachment.contentType ?? "",
594
+ }
595
+ : { name: fileOrAttachment.name, type: fileOrAttachment.type };
596
+ if (!fileMatchesAccept(file, attachmentAdapter.accept))
597
+ throw new Error(
598
+ `File type ${file.type || "unknown"} is not accepted. Accepted types: ${attachmentAdapter.accept}`,
599
+ );
600
+ }
601
+ if (!isCreateAttachment(fileOrAttachment) && attachmentAdapter) {
602
+ await drainAdapterAdd(
603
+ attachmentAdapter.add({ file: fileOrAttachment }),
604
+ upsertAttachment,
605
+ );
606
+ } else if (!isCreateAttachment(fileOrAttachment)) {
427
607
  const newAttachment: Attachment = {
428
608
  id: Math.random().toString(36).substring(7),
429
609
  type: "file",
@@ -433,7 +613,7 @@ const useComposerClientResource = ({
433
613
  status: { type: "complete" },
434
614
  content: [],
435
615
  };
436
- setAttachments([...attachments, newAttachment]);
616
+ setAttachments((prev) => [...prev, newAttachment]);
437
617
  } else {
438
618
  const newAttachment: Attachment = {
439
619
  id: fileOrAttachment.id ?? Math.random().toString(36).substring(7),
@@ -443,11 +623,13 @@ const useComposerClientResource = ({
443
623
  content: fileOrAttachment.content,
444
624
  status: { type: "complete" },
445
625
  };
446
- setAttachments([...attachments, newAttachment]);
626
+ setAttachments((prev) => [...prev, newAttachment]);
447
627
  }
448
628
  },
449
629
  clearAttachments: async () => {
630
+ const removed = attachmentsRef.current;
450
631
  setAttachments([]);
632
+ await removePendingAttachments(removed);
451
633
  },
452
634
  attachment: (selector) => {
453
635
  if ("id" in selector) {
@@ -456,37 +638,69 @@ const useComposerClientResource = ({
456
638
  return attachmentClients.get(selector);
457
639
  },
458
640
  reset: async () => {
641
+ const removed = attachmentsRef.current;
459
642
  setText("");
460
643
  setRole("user");
461
644
  setRunConfig({});
462
645
  setAttachments([]);
463
646
  setQuote(undefined);
647
+ await removePendingAttachments(removed);
464
648
  },
465
649
  send: (opts?: ComposerSendOptions) => {
466
- if (!state.canSend) return;
467
-
468
- const currentQuote = quote;
469
- const composedMessage: AppendMessage = {
470
- role,
471
- content: text ? [{ type: "text" as const, text }] : [],
472
- attachments: attachments as any,
473
- createdAt: new Date(),
474
- parentId: null,
475
- sourceId: null,
476
- runConfig,
477
- startRun: opts?.startRun,
478
- metadata: {
479
- custom: { ...(currentQuote ? { quote: currentQuote } : {}) },
480
- },
481
- };
482
- if (queue) {
483
- queue.enqueue(composedMessage, { steer: opts?.steer ?? false });
484
- } else {
485
- onSend?.(composedMessage);
486
- }
650
+ const currentQuote = quoteRef.current;
651
+ const currentText = textRef.current;
652
+ const currentAttachments = attachmentsRef.current;
653
+ const isEmpty = !currentText.trim() && !currentAttachments.length;
654
+ if (!isEditing || isEmpty || isSendDisabled) return;
655
+
487
656
  setText("");
488
657
  setAttachments([]);
489
658
  setQuote(undefined);
659
+
660
+ const dispatch = (sendAttachments: readonly Attachment[]) => {
661
+ const composedMessage: AppendMessage = {
662
+ role: roleRef.current,
663
+ content: currentText
664
+ ? [{ type: "text" as const, text: currentText }]
665
+ : [],
666
+ attachments: sendAttachments as any,
667
+ createdAt: new Date(),
668
+ parentId: null,
669
+ sourceId: null,
670
+ runConfig: runConfigRef.current,
671
+ startRun: opts?.startRun,
672
+ metadata: {
673
+ custom: { ...(currentQuote ? { quote: currentQuote } : {}) },
674
+ },
675
+ };
676
+ if (queue) {
677
+ queue.enqueue(composedMessage, { steer: opts?.steer ?? false });
678
+ } else {
679
+ onSend?.(composedMessage);
680
+ }
681
+ };
682
+
683
+ if (attachmentAdapter && currentAttachments.length > 0) {
684
+ void Promise.all(
685
+ currentAttachments.map((attachment) =>
686
+ attachment.status.type === "complete"
687
+ ? attachment
688
+ : attachmentAdapter.send(attachment as PendingAttachment),
689
+ ),
690
+ ).then(dispatch, (error) => {
691
+ // Upload failed: merge the failed send back into the draft.
692
+ setText((prev) =>
693
+ currentText && prev
694
+ ? currentText + "\n" + prev
695
+ : currentText || prev,
696
+ );
697
+ setQuote((prev) => prev ?? currentQuote);
698
+ setAttachments((prev) => [...currentAttachments, ...prev]);
699
+ console.error("Failed to send attachments", error);
700
+ });
701
+ } else {
702
+ dispatch(currentAttachments);
703
+ }
490
704
  },
491
705
  cancel: onCancel,
492
706
  beginEdit: () => {
@@ -503,20 +717,50 @@ const useComposerClientResource = ({
503
717
 
504
718
  const ComposerClientResource = resource(useComposerClientResource);
505
719
 
720
+ const dedupeMessagesById = (messages: readonly ExternalThreadMessage[]) => {
721
+ const seenIds = new Set<string>();
722
+ const deduped: ExternalThreadMessage[] = [];
723
+ for (let i = messages.length - 1; i >= 0; i--) {
724
+ const message = messages[i]!;
725
+ if (seenIds.has(message.id)) {
726
+ console.warn(
727
+ `ExternalThread: duplicate message id "${message.id}" in the provided messages array; keeping the last occurrence.`,
728
+ );
729
+ continue;
730
+ }
731
+ seenIds.add(message.id);
732
+ deduped.push(message);
733
+ }
734
+ return deduped.length === messages.length ? messages : deduped.reverse();
735
+ };
736
+
506
737
  // External Thread Client
507
738
  const useExternalThread = ({
508
- messages,
739
+ messages: messagesProp,
509
740
  isRunning = false,
741
+ isLoading = false,
742
+ state: threadState,
743
+ extras,
510
744
  isSendDisabled = false,
511
745
  onNew,
512
746
  onEdit,
513
747
  onReload,
514
748
  onStartRun,
515
749
  onCancel,
750
+ onResume,
751
+ onAddToolResult,
752
+ onResumeToolCall,
753
+ onLoadExternalState,
754
+ attachmentAdapter,
516
755
  queue,
517
756
  branches,
518
757
  onRespondToToolApproval,
519
758
  }: ExternalThreadProps): ClientOutput<"thread"> => {
759
+ const messages = useMemo(
760
+ () => dedupeMessagesById(messagesProp),
761
+ [messagesProp],
762
+ );
763
+
520
764
  const handleReload = (messageId: string) => {
521
765
  const messageIndex = messages.findIndex((m) => m.id === messageId);
522
766
  if (messageIndex === -1) return;
@@ -531,10 +775,14 @@ const useExternalThread = ({
531
775
  const props: MessageClientProps = {
532
776
  message: msg,
533
777
  index,
778
+ parentId: index > 0 ? messages[index - 1]!.id : null,
534
779
  onReload: () => handleReload(msg.id),
535
780
  queue,
536
781
  branches,
537
782
  onRespondToToolApproval,
783
+ onAddToolResult,
784
+ onResumeToolCall,
785
+ attachmentAdapter,
538
786
  };
539
787
  if (onEdit) props.onEdit = onEdit;
540
788
  return withKey(msg.id, MessageClient(props));
@@ -547,9 +795,25 @@ const useExternalThread = ({
547
795
  };
548
796
 
549
797
  const handleSendNew = (message: AppendMessage) => {
550
- onNew?.(message);
798
+ // The composer does not know the thread; stamp the current head as the
799
+ // parent (legacy composer parity).
800
+ onNew?.({ ...message, parentId: messages.at(-1)?.id ?? null });
551
801
  };
552
802
 
803
+ const headId = messages.at(-1)?.id ?? null;
804
+ const composerQueue = useMemo(
805
+ (): ExternalThreadQueueAdapter | undefined =>
806
+ queue && {
807
+ ...queue,
808
+ enqueue: (message, options) =>
809
+ queue.enqueue(
810
+ { ...message, parentId: message.parentId ?? headId },
811
+ options,
812
+ ),
813
+ },
814
+ [queue, headId],
815
+ );
816
+
553
817
  const composerClient = useClientResource(
554
818
  ComposerClientResource({
555
819
  type: "thread",
@@ -558,12 +822,16 @@ const useExternalThread = ({
558
822
  isSendDisabled,
559
823
  onCancel: handleCancelRun,
560
824
  onSend: handleSendNew,
561
- queue,
825
+ queue: composerQueue,
826
+ attachmentAdapter,
562
827
  }),
563
828
  );
564
829
 
565
830
  const hasQueue = !!queue;
566
831
  const hasBranches = !!branches;
832
+ const hasEdit = !!onEdit;
833
+ const hasReload = !!onReload;
834
+ const hasAttachments = !!attachmentAdapter;
567
835
  const state = useMemo(() => {
568
836
  const messageStates = messageClients.state.map((s, idx, arr) => ({
569
837
  ...s,
@@ -573,15 +841,15 @@ const useExternalThread = ({
573
841
  return {
574
842
  isEmpty: messages.length === 0,
575
843
  isDisabled: false,
576
- isLoading: false,
844
+ isLoading,
577
845
  isRunning,
578
846
  capabilities: {
579
- edit: false,
847
+ edit: hasEdit,
580
848
  delete: false,
581
- reload: false,
849
+ reload: hasReload,
582
850
  cancel: isRunning,
583
851
  speech: false,
584
- attachments: false,
852
+ attachments: hasAttachments,
585
853
  feedback: false,
586
854
  voice: false,
587
855
  switchToBranch: hasBranches,
@@ -591,9 +859,9 @@ const useExternalThread = ({
591
859
  queue: hasQueue,
592
860
  },
593
861
  messages: messageStates,
594
- state: {},
862
+ state: threadState ?? {},
595
863
  suggestions: [],
596
- extras: undefined,
864
+ extras,
597
865
  speech: undefined,
598
866
  voice: undefined,
599
867
  composer: composerClient.state,
@@ -601,8 +869,14 @@ const useExternalThread = ({
601
869
  }, [
602
870
  messages,
603
871
  isRunning,
872
+ isLoading,
873
+ threadState,
874
+ extras,
604
875
  hasQueue,
605
876
  hasBranches,
877
+ hasEdit,
878
+ hasReload,
879
+ hasAttachments,
606
880
  messageClients.state,
607
881
  composerClient.state,
608
882
  ]);
@@ -644,8 +918,21 @@ const useExternalThread = ({
644
918
  startRun: () => {
645
919
  onStartRun?.();
646
920
  },
647
- resumeRun: () => {},
921
+ resumeRun: () => {
922
+ if (!onResume)
923
+ throw new Error(
924
+ "Runtime does not support resuming runs (onResume is not set).",
925
+ );
926
+ onResume();
927
+ },
648
928
  cancelRun: handleCancelRun,
929
+ importExternalState: (state: unknown) => {
930
+ if (!onLoadExternalState)
931
+ throw new Error(
932
+ "Runtime does not support importing external states (onLoadExternalState is not set).",
933
+ );
934
+ onLoadExternalState(state);
935
+ },
649
936
  getModelContext: () => ({ tools: {}, config: {} }),
650
937
  export: () => ({ messages: [] }),
651
938
  import: () => {},
@@ -672,10 +959,12 @@ attachTransformScopes(useExternalThread, (scopes, parent) => {
672
959
  if (!scopes.threads && parent.threads.source === null) {
673
960
  const threadElement = scopes.thread as ClientElement<"thread">;
674
961
  scopes.threads = SingleThreadList({ thread: threadElement });
962
+ // scopes mount in key order; re-declare thread after the threads source it resolves from
963
+ delete scopes.thread;
675
964
  scopes.thread = Derived({
676
965
  source: "threads",
677
966
  query: { type: "main" },
678
- get: (aui) => aui.threads().thread("main"),
967
+ get: (aui) => aui.threads.thread("main"),
679
968
  });
680
969
  }
681
970
 
@@ -683,14 +972,14 @@ attachTransformScopes(useExternalThread, (scopes, parent) => {
683
972
  scopes.threadListItem = Derived({
684
973
  source: "threads",
685
974
  query: { type: "main" },
686
- get: (aui) => aui.threads().item("main"),
975
+ get: (aui) => aui.threads.item("main"),
687
976
  });
688
977
  }
689
978
 
690
979
  scopes.composer ??= Derived({
691
980
  source: "thread",
692
981
  query: {},
693
- get: (aui) => aui.thread().composer(),
982
+ get: (aui) => aui.thread.composer(),
694
983
  });
695
984
 
696
985
  if (!scopes.modelContext && parent.modelContext.source === null) {