@assistant-ui/core 0.2.0 → 0.2.3

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 (201) hide show
  1. package/README.md +45 -0
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/model-context/tool.d.ts +25 -0
  7. package/dist/model-context/tool.d.ts.map +1 -1
  8. package/dist/model-context/tool.js +25 -0
  9. package/dist/model-context/tool.js.map +1 -1
  10. package/dist/react/AssistantRuntimeProvider.d.ts +33 -0
  11. package/dist/react/AssistantRuntimeProvider.d.ts.map +1 -1
  12. package/dist/react/AssistantRuntimeProvider.js +22 -0
  13. package/dist/react/AssistantRuntimeProvider.js.map +1 -1
  14. package/dist/react/client/DataRenderers.d.ts +7 -0
  15. package/dist/react/client/DataRenderers.d.ts.map +1 -1
  16. package/dist/react/client/DataRenderers.js +7 -0
  17. package/dist/react/client/DataRenderers.js.map +1 -1
  18. package/dist/react/client/Tools.d.ts +18 -1
  19. package/dist/react/client/Tools.d.ts.map +1 -1
  20. package/dist/react/client/Tools.js +24 -19
  21. package/dist/react/client/Tools.js.map +1 -1
  22. package/dist/react/index.d.ts +2 -1
  23. package/dist/react/index.d.ts.map +1 -1
  24. package/dist/react/index.js +1 -0
  25. package/dist/react/index.js.map +1 -1
  26. package/dist/react/model-context/makeAssistantDataUI.d.ts +13 -0
  27. package/dist/react/model-context/makeAssistantDataUI.d.ts.map +1 -1
  28. package/dist/react/model-context/makeAssistantDataUI.js +6 -0
  29. package/dist/react/model-context/makeAssistantDataUI.js.map +1 -1
  30. package/dist/react/model-context/makeAssistantTool.d.ts +15 -0
  31. package/dist/react/model-context/makeAssistantTool.d.ts.map +1 -1
  32. package/dist/react/model-context/makeAssistantTool.js +8 -0
  33. package/dist/react/model-context/makeAssistantTool.js.map +1 -1
  34. package/dist/react/model-context/makeAssistantToolUI.d.ts +15 -0
  35. package/dist/react/model-context/makeAssistantToolUI.d.ts.map +1 -1
  36. package/dist/react/model-context/makeAssistantToolUI.js +8 -0
  37. package/dist/react/model-context/makeAssistantToolUI.js.map +1 -1
  38. package/dist/react/model-context/toolbox.d.ts +29 -0
  39. package/dist/react/model-context/toolbox.d.ts.map +1 -1
  40. package/dist/react/model-context/useAssistantDataUI.d.ts +9 -0
  41. package/dist/react/model-context/useAssistantDataUI.d.ts.map +1 -1
  42. package/dist/react/model-context/useAssistantDataUI.js +6 -0
  43. package/dist/react/model-context/useAssistantDataUI.js.map +1 -1
  44. package/dist/react/model-context/useAssistantTool.d.ts +34 -0
  45. package/dist/react/model-context/useAssistantTool.d.ts.map +1 -1
  46. package/dist/react/model-context/useAssistantTool.js +30 -0
  47. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  48. package/dist/react/model-context/useAssistantToolUI.d.ts +12 -0
  49. package/dist/react/model-context/useAssistantToolUI.d.ts.map +1 -1
  50. package/dist/react/model-context/useAssistantToolUI.js +9 -0
  51. package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
  52. package/dist/react/model-context/useToolArgsStatus.d.ts +29 -0
  53. package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
  54. package/dist/react/model-context/useToolArgsStatus.js +24 -0
  55. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  56. package/dist/react/primitive-hooks/useActionBarCopy.d.ts.map +1 -1
  57. package/dist/react/primitive-hooks/useActionBarCopy.js +4 -3
  58. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  59. package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
  60. package/dist/react/primitive-hooks/useComposerSend.js +2 -3
  61. package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
  62. package/dist/react/primitives/message/MessageAttachments.js +1 -1
  63. package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
  64. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  65. package/dist/react/primitives/message/MessageParts.js +14 -10
  66. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  67. package/dist/react/primitives/messagePart/MessagePartInProgress.d.ts +6 -0
  68. package/dist/react/primitives/messagePart/MessagePartInProgress.d.ts.map +1 -0
  69. package/dist/react/primitives/messagePart/MessagePartInProgress.js +7 -0
  70. package/dist/react/primitives/messagePart/MessagePartInProgress.js.map +1 -0
  71. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +2 -0
  72. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  73. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +2 -0
  74. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  75. package/dist/react/runtimes/cloud/auiV0.d.ts +10 -1
  76. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  77. package/dist/react/runtimes/cloud/auiV0.js +21 -3
  78. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  79. package/dist/react/runtimes/useToolInvocations.d.ts +11 -1
  80. package/dist/react/runtimes/useToolInvocations.d.ts.map +1 -1
  81. package/dist/react/runtimes/useToolInvocations.js +325 -256
  82. package/dist/react/runtimes/useToolInvocations.js.map +1 -1
  83. package/dist/react/types/MessagePartComponentTypes.d.ts +11 -0
  84. package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
  85. package/dist/react/types/scopes/tools.d.ts +4 -0
  86. package/dist/react/types/scopes/tools.d.ts.map +1 -1
  87. package/dist/runtime/api/composer-runtime.d.ts +1 -0
  88. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  89. package/dist/runtime/api/composer-runtime.js +2 -0
  90. package/dist/runtime/api/composer-runtime.js.map +1 -1
  91. package/dist/runtime/api/thread-runtime.d.ts +2 -0
  92. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  93. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -0
  94. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  95. package/dist/runtime/base/base-composer-runtime-core.js +1 -1
  96. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  97. package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -0
  98. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  99. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  100. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +1 -0
  101. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  102. package/dist/runtime/base/default-edit-composer-runtime-core.js +3 -0
  103. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  104. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -0
  105. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  106. package/dist/runtime/base/default-thread-composer-runtime-core.js +12 -1
  107. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  108. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -0
  109. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  110. package/dist/runtime/interfaces/thread-runtime-core.d.ts +6 -0
  111. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  112. package/dist/runtimes/external-store/external-store-adapter.d.ts +15 -0
  113. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  114. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
  115. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  116. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +14 -12
  117. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  118. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -0
  119. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  120. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +13 -0
  121. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  122. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
  123. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  124. package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
  125. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  126. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +2 -0
  127. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  128. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +2 -0
  129. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  130. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  131. package/dist/runtimes/remote-thread-list/empty-thread-core.js +2 -0
  132. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  133. package/dist/store/clients/model-context-client.d.ts.map +1 -1
  134. package/dist/store/clients/model-context-client.js +24 -4
  135. package/dist/store/clients/model-context-client.js.map +1 -1
  136. package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
  137. package/dist/store/clients/no-op-composer-client.js +1 -0
  138. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  139. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  140. package/dist/store/runtime-clients/composer-runtime-client.js +1 -0
  141. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  142. package/dist/store/scopes/composer.d.ts +9 -0
  143. package/dist/store/scopes/composer.d.ts.map +1 -1
  144. package/dist/store/scopes/model-context.d.ts +4 -1
  145. package/dist/store/scopes/model-context.d.ts.map +1 -1
  146. package/dist/types/index.d.ts +1 -1
  147. package/dist/types/index.d.ts.map +1 -1
  148. package/dist/types/message.d.ts +50 -1
  149. package/dist/types/message.d.ts.map +1 -1
  150. package/dist/types/message.js +2 -1
  151. package/dist/types/message.js.map +1 -1
  152. package/package.json +7 -7
  153. package/src/index.ts +6 -0
  154. package/src/model-context/tool.ts +25 -0
  155. package/src/react/AssistantRuntimeProvider.tsx +33 -0
  156. package/src/react/client/DataRenderers.ts +7 -0
  157. package/src/react/client/Tools.ts +56 -22
  158. package/src/react/index.ts +2 -1
  159. package/src/react/model-context/makeAssistantDataUI.ts +13 -0
  160. package/src/react/model-context/makeAssistantTool.ts +15 -0
  161. package/src/react/model-context/makeAssistantToolUI.ts +15 -0
  162. package/src/react/model-context/toolbox.ts +32 -1
  163. package/src/react/model-context/useAssistantDataUI.ts +9 -0
  164. package/src/react/model-context/useAssistantTool.ts +34 -0
  165. package/src/react/model-context/useAssistantToolUI.ts +12 -0
  166. package/src/react/model-context/useToolArgsStatus.ts +29 -0
  167. package/src/react/primitive-hooks/useActionBarCopy.ts +9 -5
  168. package/src/react/primitive-hooks/useComposerSend.ts +2 -3
  169. package/src/react/primitives/message/MessageAttachments.test.tsx +50 -0
  170. package/src/react/primitives/message/MessageAttachments.tsx +1 -1
  171. package/src/react/primitives/message/MessageParts.tsx +20 -9
  172. package/src/react/primitives/messagePart/MessagePartInProgress.ts +15 -0
  173. package/src/react/runtimes/cloud/auiV0.ts +37 -4
  174. package/src/react/runtimes/useToolInvocations.ts +422 -333
  175. package/src/react/types/MessagePartComponentTypes.ts +11 -0
  176. package/src/react/types/scopes/tools.ts +5 -0
  177. package/src/runtime/api/composer-runtime.ts +3 -0
  178. package/src/runtime/base/base-composer-runtime-core.ts +2 -1
  179. package/src/runtime/base/base-thread-runtime-core.ts +1 -0
  180. package/src/runtime/base/default-edit-composer-runtime-core.ts +4 -0
  181. package/src/runtime/base/default-thread-composer-runtime-core.ts +12 -1
  182. package/src/runtime/interfaces/composer-runtime-core.ts +1 -0
  183. package/src/runtime/interfaces/thread-runtime-core.ts +6 -0
  184. package/src/runtimes/external-store/external-store-adapter.ts +15 -0
  185. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +15 -9
  186. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +13 -0
  187. package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
  188. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +2 -0
  189. package/src/runtimes/remote-thread-list/empty-thread-core.ts +2 -0
  190. package/src/store/clients/model-context-client.test.ts +108 -0
  191. package/src/store/clients/model-context-client.ts +36 -6
  192. package/src/store/clients/no-op-composer-client.ts +1 -0
  193. package/src/store/runtime-clients/composer-runtime-client.ts +1 -0
  194. package/src/store/scopes/composer.ts +9 -0
  195. package/src/store/scopes/model-context.ts +4 -1
  196. package/src/tests/auiV0Encode.test.ts +55 -0
  197. package/src/tests/composer-can-send.test.ts +112 -0
  198. package/src/tests/external-store-thread-list-runtime-core.test.ts +34 -0
  199. package/src/tests/external-store-thread-runtime-core.test.ts +113 -0
  200. package/src/types/index.ts +2 -0
  201. package/src/types/message.ts +66 -7
@@ -57,10 +57,21 @@ export type ToolCallMessagePartProps<
57
57
  TResult = unknown,
58
58
  > = MessagePartState &
59
59
  ToolCallMessagePart<TArgs, TResult> & {
60
+ /**
61
+ * Sets the result for this tool-call message part.
62
+ *
63
+ * Use when the renderer, rather than a tool `execute` function, is the
64
+ * source of the result.
65
+ */
60
66
  addResult: (result: TResult | ToolResponse<TResult>) => void;
67
+ /**
68
+ * Supplies the payload requested by `context.human(...)` and resumes the
69
+ * paused frontend tool execution.
70
+ */
61
71
  resume: (payload: unknown) => void;
62
72
  };
63
73
 
74
+ /** Component used to render a tool-call message part. */
64
75
  export type ToolCallMessagePartComponent<
65
76
  TArgs = any,
66
77
  TResult = any,
@@ -1,8 +1,13 @@
1
1
  import type { ToolCallMessagePartComponent } from "../MessagePartComponentTypes";
2
2
  import type { Unsubscribe } from "../../..";
3
3
 
4
+ export type McpAppResourceOutput = {
5
+ readonly render: ToolCallMessagePartComponent;
6
+ };
7
+
4
8
  export type ToolsState = {
5
9
  tools: Record<string, ToolCallMessagePartComponent[]>;
10
+ mcpApp?: McpAppResourceOutput | undefined;
6
11
  };
7
12
 
8
13
  export type ToolsMethods = {
@@ -41,6 +41,7 @@ export type {
41
41
 
42
42
  type BaseComposerState = {
43
43
  readonly canCancel: boolean;
44
+ readonly canSend: boolean;
44
45
  readonly isEditing: boolean;
45
46
  readonly isEmpty: boolean;
46
47
 
@@ -86,6 +87,7 @@ const getThreadComposerState = (
86
87
 
87
88
  isEditing: runtime?.isEditing ?? false,
88
89
  canCancel: runtime?.canCancel ?? false,
90
+ canSend: runtime?.canSend ?? false,
89
91
  isEmpty: runtime?.isEmpty ?? true,
90
92
 
91
93
  attachments: runtime?.attachments ?? EMPTY_ARRAY,
@@ -108,6 +110,7 @@ const getEditComposerState = (
108
110
 
109
111
  isEditing: runtime?.isEditing ?? false,
110
112
  canCancel: runtime?.canCancel ?? false,
113
+ canSend: runtime?.canSend ?? false,
111
114
  isEmpty: runtime?.isEmpty ?? true,
112
115
 
113
116
  text: runtime?.text ?? "",
@@ -52,6 +52,7 @@ export abstract class BaseComposerRuntimeCore
52
52
  }
53
53
 
54
54
  public abstract get canCancel(): boolean;
55
+ public abstract get canSend(): boolean;
55
56
 
56
57
  public get isEmpty() {
57
58
  return !this.text.trim() && !this.attachments.length;
@@ -157,7 +158,7 @@ export abstract class BaseComposerRuntimeCore
157
158
  }
158
159
 
159
160
  public async send(options?: SendOptions) {
160
- if (this.isEmpty) return;
161
+ if (!this.canSend) return;
161
162
 
162
163
  if (this._dictationSession) {
163
164
  this._dictationSession.cancel();
@@ -48,6 +48,7 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
48
48
  protected readonly repository = new MessageRepository();
49
49
  public abstract get adapters(): BaseThreadAdapters | undefined;
50
50
  public abstract get isDisabled(): boolean;
51
+ public abstract get isSendDisabled(): boolean;
51
52
  public abstract get isLoading(): boolean;
52
53
  public abstract get suggestions(): readonly ThreadSuggestion[];
53
54
  public abstract get extras(): unknown;
@@ -13,6 +13,10 @@ export class DefaultEditComposerRuntimeCore extends BaseComposerRuntimeCore {
13
13
  return true;
14
14
  }
15
15
 
16
+ public get canSend() {
17
+ return !this.isEmpty;
18
+ }
19
+
16
20
  protected getAttachmentAdapter() {
17
21
  return this.runtime.adapters?.attachments;
18
22
  }
@@ -17,6 +17,10 @@ export class DefaultThreadComposerRuntimeCore
17
17
  return this._canCancel;
18
18
  }
19
19
 
20
+ public get canSend() {
21
+ return !this.isEmpty && !this.runtime.isSendDisabled;
22
+ }
23
+
20
24
  protected getAttachmentAdapter() {
21
25
  return this.runtime.adapters?.attachments;
22
26
  }
@@ -40,11 +44,18 @@ export class DefaultThreadComposerRuntimeCore
40
44
  }
41
45
 
42
46
  public connect() {
47
+ let lastIsSendDisabled = this.runtime.isSendDisabled;
43
48
  return this.runtime.subscribe(() => {
49
+ let changed = false;
44
50
  if (this.canCancel !== this.runtime.capabilities.cancel) {
45
51
  this._canCancel = this.runtime.capabilities.cancel;
46
- this._notifySubscribers();
52
+ changed = true;
53
+ }
54
+ if (lastIsSendDisabled !== this.runtime.isSendDisabled) {
55
+ lastIsSendDisabled = this.runtime.isSendDisabled;
56
+ changed = true;
47
57
  }
58
+ if (changed) this._notifySubscribers();
48
59
  });
49
60
  }
50
61
 
@@ -51,6 +51,7 @@ export type ComposerRuntimeCore = Readonly<{
51
51
  isEditing: boolean;
52
52
 
53
53
  canCancel: boolean;
54
+ canSend: boolean;
54
55
  isEmpty: boolean;
55
56
 
56
57
  attachments: readonly Attachment[];
@@ -158,6 +158,12 @@ export type ThreadRuntimeCore = Readonly<{
158
158
 
159
159
  capabilities: Readonly<RuntimeCapabilities>;
160
160
  isDisabled: boolean;
161
+ /**
162
+ * Whether sending from this thread's composer is disabled. Surfaces the
163
+ * `isSendDisabled` flag from external-store adapters; internal runtimes
164
+ * default to `false`. Composer state derives `canSend` from this.
165
+ */
166
+ isSendDisabled: boolean;
161
167
  isLoading: boolean;
162
168
  /**
163
169
  * Optional explicit thread-level running flag. When provided, takes
@@ -59,7 +59,22 @@ type ExternalStoreMessageConverterAdapter<T> = {
59
59
  };
60
60
 
61
61
  type ExternalStoreAdapterBase<T> = {
62
+ /**
63
+ * Whether the entire thread is disabled. When `true`, the composer's input
64
+ * is also disabled (the user cannot type, attach files, or submit). For a
65
+ * narrower gate that keeps the input usable but blocks only sending, use
66
+ * `isSendDisabled`.
67
+ */
62
68
  isDisabled?: boolean | undefined;
69
+ /**
70
+ * Whether sending new messages is currently disabled. When `true`, the
71
+ * thread composer's input remains usable but `send()` becomes a no-op
72
+ * and the thread composer's `canSend` is `false`. Use this to gate
73
+ * sending on external React state (e.g. while tool config is loading)
74
+ * without disabling the input itself the way `isDisabled` does. Edit
75
+ * composers (saving message edits) intentionally ignore this flag.
76
+ */
77
+ isSendDisabled?: boolean | undefined;
63
78
  /**
64
79
  * Whether the thread is running. When provided, this value flows directly
65
80
  * to `thread.isRunning`, letting the application keep the thread in a
@@ -78,14 +78,7 @@ export class ExternalStoreThreadListRuntimeCore
78
78
  }
79
79
 
80
80
  public getItemById(threadId: string) {
81
- for (const thread of this.adapter.threads ?? []) {
82
- if (thread.id === threadId) return thread as any;
83
- }
84
- for (const thread of this.adapter.archivedThreads ?? []) {
85
- if (thread.id === threadId) return thread as any;
86
- }
87
- if (threadId === DEFAULT_THREAD_ID) return DEFAULT_THREAD;
88
- return undefined;
81
+ return this._threadData[threadId];
89
82
  }
90
83
 
91
84
  public __internal_setAdapter(
@@ -115,7 +108,8 @@ export class ExternalStoreThreadListRuntimeCore
115
108
 
116
109
  if (
117
110
  previousThreads !== newThreads ||
118
- previousArchivedThreads !== newArchivedThreads
111
+ previousArchivedThreads !== newArchivedThreads ||
112
+ previousThreadId !== newThreadId
119
113
  ) {
120
114
  this._threadData = {
121
115
  ...DEFAULT_THREAD_DATA,
@@ -159,6 +153,18 @@ export class ExternalStoreThreadListRuntimeCore
159
153
  this._mainThread = this.threadFactory();
160
154
  }
161
155
 
156
+ if (!this._threadData[this._mainThreadId]) {
157
+ this._threadData = {
158
+ ...this._threadData,
159
+ [this._mainThreadId]: {
160
+ id: this._mainThreadId,
161
+ remoteId: undefined,
162
+ externalId: undefined,
163
+ status: "regular",
164
+ },
165
+ };
166
+ }
167
+
162
168
  this._notifySubscribers();
163
169
  }
164
170
 
@@ -53,6 +53,7 @@ export class ExternalStoreThreadRuntimeCore
53
53
  implements ThreadRuntimeCore
54
54
  {
55
55
  private _assistantOptimisticId: string | null = null;
56
+ private _lastSyncedMessageIds = new Set<string>();
56
57
 
57
58
  private _capabilities: RuntimeCapabilities = {
58
59
  switchToBranch: false,
@@ -75,6 +76,7 @@ export class ExternalStoreThreadRuntimeCore
75
76
 
76
77
  private _messages!: readonly ThreadMessage[];
77
78
  public isDisabled!: boolean;
79
+ public isSendDisabled!: boolean;
78
80
  public get isLoading() {
79
81
  return this._store.isLoading ?? false;
80
82
  }
@@ -122,6 +124,7 @@ export class ExternalStoreThreadRuntimeCore
122
124
 
123
125
  const isRunning = store.isRunning ?? false;
124
126
  this.isDisabled = store.isDisabled ?? false;
127
+ this.isSendDisabled = store.isSendDisabled ?? false;
125
128
 
126
129
  const oldStore = this._store as ExternalStoreAdapter<any> | undefined;
127
130
  this._store = store;
@@ -168,6 +171,7 @@ export class ExternalStoreThreadRuntimeCore
168
171
  // Clear and import the message repository
169
172
  this.repository.clear();
170
173
  this._assistantOptimisticId = null;
174
+ this._lastSyncedMessageIds = new Set();
171
175
  this.repository.import(store.messageRepository);
172
176
 
173
177
  messages = this.repository.getMessages();
@@ -220,6 +224,12 @@ export class ExternalStoreThreadRuntimeCore
220
224
  return newMessage;
221
225
  });
222
226
 
227
+ const nextIds = new Set(messages.map((m) => m.id));
228
+ for (const prevId of this._lastSyncedMessageIds) {
229
+ if (!nextIds.has(prevId)) this.repository.deleteMessage(prevId);
230
+ }
231
+ this._lastSyncedMessageIds = nextIds;
232
+
223
233
  for (let i = 0; i < messages.length; i++) {
224
234
  const message = messages[i]!;
225
235
  const parent = messages[i - 1];
@@ -334,6 +344,7 @@ export class ExternalStoreThreadRuntimeCore
334
344
  previousMessage.id === messages.at(-1)?.id // ensure the previous message is a leaf node
335
345
  ) {
336
346
  this.repository.deleteMessage(previousMessage.id);
347
+ this._lastSyncedMessageIds.delete(previousMessage.id);
337
348
  if (!this.composer.text.trim()) {
338
349
  this.composer.setText(getThreadMessageText(previousMessage));
339
350
  }
@@ -362,6 +373,7 @@ export class ExternalStoreThreadRuntimeCore
362
373
  }
363
374
 
364
375
  public override reset(initialMessages?: readonly ThreadMessageLike[]) {
376
+ this._lastSyncedMessageIds = new Set();
365
377
  const repo = new MessageRepository();
366
378
  repo.import(ExportedMessageRepository.fromArray(initialMessages ?? []));
367
379
  this.updateMessages(repo.getMessages());
@@ -369,6 +381,7 @@ export class ExternalStoreThreadRuntimeCore
369
381
 
370
382
  public override import(data: ExportedMessageRepository) {
371
383
  this._assistantOptimisticId = null;
384
+ this._lastSyncedMessageIds = new Set();
372
385
 
373
386
  super.import(data);
374
387
 
@@ -54,6 +54,7 @@ export class LocalThreadRuntimeCore
54
54
  private abortController: AbortController | null = null;
55
55
 
56
56
  public readonly isDisabled = false;
57
+ public readonly isSendDisabled = false;
57
58
 
58
59
  private _isLoading = false;
59
60
  public get isLoading() {
@@ -118,6 +118,7 @@ export class ReadonlyThreadRuntimeCore
118
118
 
119
119
  isEditing: false as const,
120
120
  canCancel: false,
121
+ canSend: false,
121
122
  isEmpty: true,
122
123
  text: "",
123
124
 
@@ -205,6 +206,7 @@ export class ReadonlyThreadRuntimeCore
205
206
  } as const;
206
207
 
207
208
  isDisabled = false;
209
+ isSendDisabled = false;
208
210
  isLoading = false;
209
211
 
210
212
  state = null;
@@ -98,6 +98,7 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
98
98
  isEditing: true,
99
99
 
100
100
  canCancel: false,
101
+ canSend: false,
101
102
  isEmpty: true,
102
103
 
103
104
  text: "",
@@ -186,6 +187,7 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
186
187
  },
187
188
 
188
189
  isDisabled: false,
190
+ isSendDisabled: false,
189
191
  isLoading: true,
190
192
 
191
193
  messages: [],
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createResourceRoot } from "@assistant-ui/tap";
3
+ import type { Tool } from "assistant-stream";
4
+ import { ModelContext } from "./model-context-client";
5
+ import type {
6
+ ModelContext as ModelContextValue,
7
+ ModelContextProvider,
8
+ } from "../../model-context/types";
9
+
10
+ const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
11
+
12
+ const provider = (ctx: ModelContextValue): ModelContextProvider => ({
13
+ getModelContext: () => ctx,
14
+ });
15
+
16
+ const stubTool = (): Tool<any, any> =>
17
+ ({ description: "", parameters: {} as any }) as unknown as Tool<any, any>;
18
+
19
+ const render = () => {
20
+ const root = createResourceRoot();
21
+ const sub = root.render(ModelContext());
22
+ return { sub, unmount: () => root.unmount() };
23
+ };
24
+
25
+ describe("ModelContext", () => {
26
+ it("starts with undefined modelName and an empty toolNames array", () => {
27
+ const { sub, unmount } = render();
28
+ try {
29
+ const state = sub.getValue().getState();
30
+ expect(state.modelName).toBeUndefined();
31
+ expect(state.toolNames).toEqual([]);
32
+ } finally {
33
+ unmount();
34
+ }
35
+ });
36
+
37
+ it("reflects modelName from a registered provider", async () => {
38
+ const { sub, unmount } = render();
39
+ try {
40
+ sub.getValue().register(provider({ config: { modelName: "gpt-4" } }));
41
+ await tick();
42
+
43
+ expect(sub.getValue().getState().modelName).toBe("gpt-4");
44
+ } finally {
45
+ unmount();
46
+ }
47
+ });
48
+
49
+ it("reflects tool names from a registered provider", async () => {
50
+ const { sub, unmount } = render();
51
+ try {
52
+ sub
53
+ .getValue()
54
+ .register(provider({ tools: { foo: stubTool(), bar: stubTool() } }));
55
+ await tick();
56
+
57
+ expect(sub.getValue().getState().toolNames).toEqual(["bar", "foo"]);
58
+ } finally {
59
+ unmount();
60
+ }
61
+ });
62
+
63
+ it("keeps the same state reference when an extra provider does not change the merged values", async () => {
64
+ const { sub, unmount } = render();
65
+ try {
66
+ sub.getValue().register(provider({ config: { modelName: "gpt-4" } }));
67
+ await tick();
68
+ const before = sub.getValue().getState();
69
+
70
+ sub.getValue().register(provider({ config: { modelName: "gpt-4" } }));
71
+ await tick();
72
+ const after = sub.getValue().getState();
73
+
74
+ expect(after).toBe(before);
75
+ } finally {
76
+ unmount();
77
+ }
78
+ });
79
+
80
+ it("clears modelName when the last contributing provider unsubscribes", async () => {
81
+ const { sub, unmount } = render();
82
+ try {
83
+ const unsubscribe = sub
84
+ .getValue()
85
+ .register(provider({ config: { modelName: "gpt-4" } }));
86
+ await tick();
87
+ expect(sub.getValue().getState().modelName).toBe("gpt-4");
88
+
89
+ unsubscribe();
90
+ await tick();
91
+ expect(sub.getValue().getState().modelName).toBeUndefined();
92
+ expect(sub.getValue().getState().toolNames).toEqual([]);
93
+ } finally {
94
+ unmount();
95
+ }
96
+ });
97
+
98
+ it("reflects modelName synchronously after register without awaiting", () => {
99
+ const { sub, unmount } = render();
100
+ try {
101
+ sub.getValue().register(provider({ config: { modelName: "gpt-4" } }));
102
+
103
+ expect(sub.getValue().getState().modelName).toBe("gpt-4");
104
+ } finally {
105
+ unmount();
106
+ }
107
+ });
108
+ });
@@ -1,18 +1,48 @@
1
- import { resource, tapMemo, tapState } from "@assistant-ui/tap";
1
+ import { resource, tapEffect, tapMemo, tapState } from "@assistant-ui/tap";
2
2
  import type { ClientOutput } from "@assistant-ui/store";
3
3
  import { CompositeContextProvider } from "../../utils/composite-context-provider";
4
4
  import type { ModelContextState } from "../scopes/model-context";
5
5
 
6
- const version = 1;
6
+ const EMPTY_TOOL_NAMES: readonly string[] = [];
7
+
8
+ const INITIAL_STATE: ModelContextState = {
9
+ modelName: undefined,
10
+ toolNames: EMPTY_TOOL_NAMES,
11
+ };
12
+
13
+ const toolNamesEqual = (a: readonly string[], b: readonly string[]): boolean =>
14
+ a === b || (a.length === b.length && a.every((v, i) => v === b[i]));
15
+
16
+ const deriveState = (
17
+ composite: CompositeContextProvider,
18
+ prev: ModelContextState,
19
+ ): ModelContextState => {
20
+ const ctx = composite.getModelContext();
21
+ const modelName = ctx.config?.modelName;
22
+ const keys = ctx.tools ? Object.keys(ctx.tools).sort() : EMPTY_TOOL_NAMES;
23
+ const toolNames = keys.length ? keys : EMPTY_TOOL_NAMES;
24
+
25
+ if (modelName === prev.modelName && toolNamesEqual(toolNames, prev.toolNames))
26
+ return prev;
27
+
28
+ return { modelName, toolNames };
29
+ };
7
30
 
8
31
  export const ModelContext = resource((): ClientOutput<"modelContext"> => {
9
- const [state] = tapState<ModelContextState>(
10
- () => ({ version: version + 1 }) as unknown as ModelContextState,
11
- );
12
32
  const composite = tapMemo(() => new CompositeContextProvider(), []);
33
+ const [state, setState] = tapState<ModelContextState>(() =>
34
+ deriveState(composite, INITIAL_STATE),
35
+ );
36
+
37
+ tapEffect(() => {
38
+ setState((prev) => deriveState(composite, prev));
39
+ return composite.subscribe(() => {
40
+ setState((prev) => deriveState(composite, prev));
41
+ });
42
+ }, [composite]);
13
43
 
14
44
  return {
15
- getState: () => state,
45
+ getState: () => deriveState(composite, state),
16
46
  getModelContext: () => composite.getModelContext(),
17
47
  subscribe: (callback) => composite.subscribe(callback),
18
48
  register: (provider) => composite.registerModelContextProvider(provider),
@@ -14,6 +14,7 @@ export const NoOpComposerClient = resource(
14
14
  role: "user",
15
15
  runConfig: {},
16
16
  canCancel: false,
17
+ canSend: false,
17
18
  type: type,
18
19
  dictation: undefined,
19
20
  quote: undefined,
@@ -103,6 +103,7 @@ export const ComposerClient = resource(
103
103
  runConfig: runtimeState.runConfig,
104
104
  isEditing: runtimeState.isEditing,
105
105
  canCancel: runtimeState.canCancel,
106
+ canSend: runtimeState.canSend,
106
107
  attachmentAccept: runtimeState.attachmentAccept,
107
108
  isEmpty: runtimeState.isEmpty,
108
109
  type: runtimeState.type ?? "thread",
@@ -26,6 +26,15 @@ export type ComposerState = {
26
26
  readonly runConfig: RunConfig;
27
27
  readonly isEditing: boolean;
28
28
  readonly canCancel: boolean;
29
+ /**
30
+ * Whether the composer is currently willing to send. `true` when the
31
+ * composer is in editing mode and has non-empty content; for thread
32
+ * composers also requires the thread's `isSendDisabled` flag to be unset.
33
+ * Edit composers (saving message edits) ignore `isSendDisabled` since it
34
+ * is a thread-scoped gate. Cross-thread gating (running, queue capability)
35
+ * is layered on top by `useComposerSend`.
36
+ */
37
+ readonly canSend: boolean;
29
38
  readonly attachmentAccept: string;
30
39
  readonly isEmpty: boolean;
31
40
  readonly type: "thread" | "edit";
@@ -1,7 +1,10 @@
1
1
  import type { Unsubscribe } from "../../types/unsubscribe";
2
2
  import type { ModelContextProvider } from "../../model-context/types";
3
3
 
4
- export type ModelContextState = Record<string, never>;
4
+ export type ModelContextState = {
5
+ readonly modelName?: string | undefined;
6
+ readonly toolNames: readonly string[];
7
+ };
5
8
 
6
9
  export type ModelContextMethods = ModelContextProvider & {
7
10
  getState(): ModelContextState;
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { auiV0Encode } from "../react/runtimes/cloud/auiV0";
3
+
4
+ describe("auiV0Encode", () => {
5
+ it("preserves document source parts in the core cloud encoder", () => {
6
+ const encoded = auiV0Encode({
7
+ id: "m1",
8
+ createdAt: new Date("2026-03-15T00:00:00.000Z"),
9
+ role: "assistant",
10
+ status: { type: "complete", reason: "stop" },
11
+ metadata: {
12
+ unstable_state: undefined,
13
+ unstable_annotations: [],
14
+ unstable_data: [],
15
+ steps: [],
16
+ custom: {},
17
+ },
18
+ content: [
19
+ {
20
+ type: "source",
21
+ sourceType: "document",
22
+ id: "doc_123",
23
+ title: "proposal.pdf",
24
+ mediaType: "application/pdf",
25
+ filename: "proposal.pdf",
26
+ providerMetadata: {
27
+ openai: {
28
+ type: "file_citation",
29
+ fileId: "file_123",
30
+ index: 0,
31
+ },
32
+ },
33
+ },
34
+ ],
35
+ });
36
+
37
+ expect(encoded.content).toEqual([
38
+ {
39
+ type: "source",
40
+ sourceType: "document",
41
+ id: "doc_123",
42
+ title: "proposal.pdf",
43
+ mediaType: "application/pdf",
44
+ filename: "proposal.pdf",
45
+ providerMetadata: {
46
+ openai: {
47
+ type: "file_citation",
48
+ fileId: "file_123",
49
+ index: 0,
50
+ },
51
+ },
52
+ },
53
+ ]);
54
+ });
55
+ });