@assistant-ui/core 0.1.7 → 0.1.9

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 (152) hide show
  1. package/dist/adapters/attachment.d.ts +4 -0
  2. package/dist/adapters/attachment.d.ts.map +1 -1
  3. package/dist/adapters/attachment.js +1 -1
  4. package/dist/adapters/attachment.js.map +1 -1
  5. package/dist/adapters/index.d.ts +10 -0
  6. package/dist/adapters/index.d.ts.map +1 -0
  7. package/dist/adapters/index.js +4 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/mention.d.ts +24 -0
  10. package/dist/adapters/mention.d.ts.map +1 -0
  11. package/dist/adapters/mention.js +42 -0
  12. package/dist/adapters/mention.js.map +1 -0
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/react/RuntimeAdapter.js +5 -6
  18. package/dist/react/RuntimeAdapter.js.map +1 -1
  19. package/dist/react/client/Interactables.d.ts +3 -0
  20. package/dist/react/client/Interactables.d.ts.map +1 -0
  21. package/dist/react/client/Interactables.js +173 -0
  22. package/dist/react/client/Interactables.js.map +1 -0
  23. package/dist/react/client/Tools.js +5 -6
  24. package/dist/react/client/Tools.js.map +1 -1
  25. package/dist/react/index.d.ts +6 -0
  26. package/dist/react/index.d.ts.map +1 -1
  27. package/dist/react/index.js +5 -0
  28. package/dist/react/index.js.map +1 -1
  29. package/dist/react/model-context/makeInteractable.d.ts +10 -0
  30. package/dist/react/model-context/makeInteractable.d.ts.map +1 -0
  31. package/dist/react/model-context/makeInteractable.js +10 -0
  32. package/dist/react/model-context/makeInteractable.js.map +1 -0
  33. package/dist/react/model-context/useInteractable.d.ts +16 -0
  34. package/dist/react/model-context/useInteractable.d.ts.map +1 -0
  35. package/dist/react/model-context/useInteractable.js +36 -0
  36. package/dist/react/model-context/useInteractable.js.map +1 -0
  37. package/dist/react/primitive-hooks/useComposerSend.d.ts +2 -1
  38. package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
  39. package/dist/react/primitive-hooks/useComposerSend.js +5 -3
  40. package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
  41. package/dist/react/primitives/composer/ComposerQueue.d.ts +31 -0
  42. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -0
  43. package/dist/react/primitives/composer/ComposerQueue.js +30 -0
  44. package/dist/react/primitives/composer/ComposerQueue.js.map +1 -0
  45. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  46. package/dist/react/primitives/message/MessageParts.js +2 -0
  47. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  48. package/dist/react/providers/QueueItemByIndexProvider.d.ts +6 -0
  49. package/dist/react/providers/QueueItemByIndexProvider.d.ts.map +1 -0
  50. package/dist/react/providers/QueueItemByIndexProvider.js +13 -0
  51. package/dist/react/providers/QueueItemByIndexProvider.js.map +1 -0
  52. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  53. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +1 -0
  54. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  55. package/dist/react/types/scopes/interactables.d.ts +39 -0
  56. package/dist/react/types/scopes/interactables.d.ts.map +1 -0
  57. package/dist/react/types/scopes/interactables.js +2 -0
  58. package/dist/react/types/scopes/interactables.js.map +1 -0
  59. package/dist/react/types/store-augmentation.d.ts +2 -0
  60. package/dist/react/types/store-augmentation.d.ts.map +1 -1
  61. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  62. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  63. package/dist/runtime/base/base-composer-runtime-core.js +33 -8
  64. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  65. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  66. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  67. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -0
  68. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  69. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  70. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +2 -0
  71. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  72. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
  73. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  74. package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
  75. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  76. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -0
  77. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  78. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -0
  79. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  80. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  81. package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -0
  82. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  83. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +9 -0
  84. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  85. package/dist/runtimes/remote-thread-list/optimistic-state.js +20 -0
  86. package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
  87. package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
  88. package/dist/store/clients/no-op-composer-client.js +4 -0
  89. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  90. package/dist/store/clients/runtime-adapter.d.ts +1 -1
  91. package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
  92. package/dist/store/clients/runtime-adapter.js +19 -26
  93. package/dist/store/clients/runtime-adapter.js.map +1 -1
  94. package/dist/store/index.d.ts +2 -1
  95. package/dist/store/index.d.ts.map +1 -1
  96. package/dist/store/index.js.map +1 -1
  97. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  98. package/dist/store/runtime-clients/composer-runtime-client.js +16 -5
  99. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  100. package/dist/store/scope-registration.d.ts +2 -0
  101. package/dist/store/scope-registration.d.ts.map +1 -1
  102. package/dist/store/scopes/composer.d.ts +25 -1
  103. package/dist/store/scopes/composer.d.ts.map +1 -1
  104. package/dist/store/scopes/queue-item.d.ts +20 -0
  105. package/dist/store/scopes/queue-item.d.ts.map +1 -0
  106. package/dist/store/scopes/queue-item.js +2 -0
  107. package/dist/store/scopes/queue-item.js.map +1 -0
  108. package/dist/types/index.d.ts +6 -0
  109. package/dist/types/index.d.ts.map +1 -0
  110. package/dist/types/index.js +2 -0
  111. package/dist/types/index.js.map +1 -0
  112. package/dist/types/mention.d.ts +32 -0
  113. package/dist/types/mention.d.ts.map +1 -0
  114. package/dist/types/mention.js +2 -0
  115. package/dist/types/mention.js.map +1 -0
  116. package/package.json +11 -11
  117. package/src/adapters/attachment.ts +1 -1
  118. package/src/adapters/index.ts +34 -0
  119. package/src/adapters/mention.ts +77 -0
  120. package/src/index.ts +11 -0
  121. package/src/react/RuntimeAdapter.ts +5 -7
  122. package/src/react/client/Interactables.ts +233 -0
  123. package/src/react/client/Tools.ts +5 -6
  124. package/src/react/index.ts +24 -0
  125. package/src/react/model-context/makeInteractable.ts +21 -0
  126. package/src/react/model-context/useInteractable.ts +73 -0
  127. package/src/react/primitive-hooks/useComposerSend.ts +11 -4
  128. package/src/react/primitives/composer/ComposerQueue.tsx +58 -0
  129. package/src/react/primitives/message/MessageParts.tsx +2 -0
  130. package/src/react/providers/QueueItemByIndexProvider.tsx +21 -0
  131. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +1 -0
  132. package/src/react/types/scopes/interactables.ts +44 -0
  133. package/src/react/types/store-augmentation.ts +2 -0
  134. package/src/runtime/base/base-composer-runtime-core.ts +45 -9
  135. package/src/runtime/interfaces/composer-runtime-core.ts +4 -1
  136. package/src/runtime/interfaces/thread-runtime-core.ts +1 -0
  137. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +2 -0
  138. package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
  139. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -0
  140. package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -0
  141. package/src/runtimes/remote-thread-list/optimistic-state.ts +27 -0
  142. package/src/store/clients/no-op-composer-client.ts +4 -0
  143. package/src/store/clients/runtime-adapter.ts +20 -31
  144. package/src/store/index.ts +7 -0
  145. package/src/store/runtime-clients/composer-runtime-client.ts +22 -7
  146. package/src/store/scope-registration.ts +2 -0
  147. package/src/store/scopes/composer.ts +26 -1
  148. package/src/store/scopes/queue-item.ts +20 -0
  149. package/src/tests/OptimisticState-list-race.test.ts +256 -0
  150. package/src/tests/mention-formatter.test.ts +112 -0
  151. package/src/types/index.ts +47 -0
  152. package/src/types/mention.ts +50 -0
@@ -0,0 +1,233 @@
1
+ import {
2
+ resource,
3
+ tapState,
4
+ tapEffect,
5
+ tapCallback,
6
+ tapRef,
7
+ tapMemo,
8
+ } from "@assistant-ui/tap";
9
+ import {
10
+ tapAssistantClientRef,
11
+ type ClientOutput,
12
+ attachTransformScopes,
13
+ } from "@assistant-ui/store";
14
+ import type {
15
+ InteractablesState,
16
+ InteractableDefinition,
17
+ InteractableRegistration,
18
+ InteractableStateSchema,
19
+ } from "../types/scopes/interactables";
20
+ import { toJSONSchema, toPartialJSONSchema } from "assistant-stream";
21
+ import type { Tool } from "assistant-stream";
22
+ import { ModelContext } from "../../store";
23
+
24
+ function shallowMerge(prev: unknown, partial: unknown): unknown {
25
+ if (
26
+ typeof prev !== "object" ||
27
+ prev === null ||
28
+ typeof partial !== "object" ||
29
+ partial === null ||
30
+ Array.isArray(prev) ||
31
+ Array.isArray(partial)
32
+ ) {
33
+ return partial;
34
+ }
35
+ return {
36
+ ...(prev as Record<string, unknown>),
37
+ ...(partial as Record<string, unknown>),
38
+ };
39
+ }
40
+
41
+ export const Interactables = resource((): ClientOutput<"interactables"> => {
42
+ const [state, setState] = tapState<InteractablesState>(() => ({
43
+ definitions: {},
44
+ }));
45
+
46
+ const clientRef = tapAssistantClientRef();
47
+
48
+ const stateRef = tapRef(state);
49
+ tapEffect(() => {
50
+ stateRef.current = state;
51
+ }, [state]);
52
+
53
+ const subscribersRef = tapRef(new Set<() => void>());
54
+
55
+ const partialSchemaCacheRef = tapRef(
56
+ new Map<string, InteractableStateSchema>(),
57
+ );
58
+ const detachedStateRef = tapRef(new Map<string, unknown>());
59
+
60
+ const setDefState = tapCallback(
61
+ (id: string, updater: (prev: unknown) => unknown) => {
62
+ setState((prev) => {
63
+ const existing = prev.definitions[id];
64
+ if (!existing) return prev;
65
+ return {
66
+ ...prev,
67
+ definitions: {
68
+ ...prev.definitions,
69
+ [id]: { ...existing, state: updater(existing.state) },
70
+ },
71
+ };
72
+ });
73
+ },
74
+ [],
75
+ );
76
+
77
+ const setDefSelected = tapCallback((id: string, selected: boolean) => {
78
+ setState((prev) => {
79
+ const existing = prev.definitions[id];
80
+ if (!existing) return prev;
81
+ return {
82
+ ...prev,
83
+ definitions: {
84
+ ...prev.definitions,
85
+ [id]: { ...existing, selected },
86
+ },
87
+ };
88
+ });
89
+ }, []);
90
+
91
+ const provider = tapMemo(
92
+ () => ({
93
+ getModelContext: () => {
94
+ const defs = stateRef.current.definitions;
95
+ const entries = Object.values(defs);
96
+ if (entries.length === 0) return {};
97
+
98
+ const byName = new Map<string, InteractableDefinition[]>();
99
+ for (const def of entries) {
100
+ const list = byName.get(def.name) ?? [];
101
+ list.push(def);
102
+ byName.set(def.name, list);
103
+ }
104
+
105
+ const systemParts: string[] = [];
106
+ const tools: Record<string, Tool<any, any>> = {};
107
+
108
+ for (const [name, instances] of byName) {
109
+ const isMulti = instances.length > 1;
110
+
111
+ for (const def of instances) {
112
+ const selectedTag = def.selected ? " (SELECTED)" : "";
113
+ const idTag = isMulti ? ` [id="${def.id}"]` : "";
114
+
115
+ systemParts.push(
116
+ `Interactable component "${name}"${idTag}${selectedTag} (${def.description}). Current state: ${JSON.stringify(def.state)}`,
117
+ );
118
+
119
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
120
+ const safeId = def.id.replace(/[^a-zA-Z0-9_-]/g, "_");
121
+ const toolName = isMulti
122
+ ? `update_${safeName}_${safeId}`
123
+ : `update_${safeName}`;
124
+
125
+ const partialSchema =
126
+ partialSchemaCacheRef.current.get(def.id) ?? def.stateSchema;
127
+
128
+ tools[toolName] = {
129
+ type: "frontend" as const,
130
+ description: `Update the state of interactable component "${name}"${isMulti ? ` (id: ${def.id})` : ""}. Only include the fields you want to change; omitted fields keep their current values. ${def.description}`,
131
+ parameters: partialSchema,
132
+ streamCall: async (reader) => {
133
+ try {
134
+ for await (const partialArgs of reader.args.streamValues()) {
135
+ setDefState(def.id, (prev) =>
136
+ shallowMerge(prev, partialArgs),
137
+ );
138
+ }
139
+ } catch {
140
+ // Non-fatal: execute handles the final state
141
+ }
142
+ },
143
+ execute: async (partialState: unknown) => {
144
+ setDefState(def.id, (prev) => shallowMerge(prev, partialState));
145
+ return { success: true };
146
+ },
147
+ };
148
+ }
149
+ }
150
+
151
+ return { system: systemParts.join("\n"), tools };
152
+ },
153
+ subscribe: (callback: () => void) => {
154
+ subscribersRef.current.add(callback);
155
+ return () => {
156
+ subscribersRef.current.delete(callback);
157
+ };
158
+ },
159
+ }),
160
+ [setDefState],
161
+ );
162
+
163
+ // biome-ignore lint/correctness/useExhaustiveDependencies: state dep triggers notification
164
+ tapEffect(() => {
165
+ for (const cb of subscribersRef.current) {
166
+ cb();
167
+ }
168
+ }, [state]);
169
+
170
+ tapEffect(() => {
171
+ return clientRef.current!.modelContext().register(provider);
172
+ }, [clientRef, provider]);
173
+
174
+ const register = tapCallback((def: InteractableRegistration) => {
175
+ try {
176
+ const jsonSchema = toJSONSchema(def.stateSchema);
177
+ partialSchemaCacheRef.current.set(
178
+ def.id,
179
+ toPartialJSONSchema(jsonSchema),
180
+ );
181
+ } catch (e) {
182
+ // Fall back to the raw schema; partial updates may not work correctly
183
+ console.warn(
184
+ `[Interactables] Failed to create partial schema for "${def.name}". The update tool will require all fields.`,
185
+ e,
186
+ );
187
+ }
188
+
189
+ const detached = detachedStateRef.current.get(def.id);
190
+ detachedStateRef.current.delete(def.id);
191
+
192
+ setState((prev) => ({
193
+ ...prev,
194
+ definitions: {
195
+ ...prev.definitions,
196
+ [def.id]: {
197
+ id: def.id,
198
+ name: def.name,
199
+ description: def.description,
200
+ stateSchema: def.stateSchema,
201
+ state:
202
+ prev.definitions[def.id]?.state ?? detached ?? def.initialState,
203
+ selected: def.selected,
204
+ },
205
+ },
206
+ }));
207
+
208
+ return () => {
209
+ setState((prev) => {
210
+ const existing = prev.definitions[def.id];
211
+ if (existing) {
212
+ detachedStateRef.current.set(def.id, existing.state);
213
+ }
214
+ partialSchemaCacheRef.current.delete(def.id);
215
+ const { [def.id]: _, ...rest } = prev.definitions;
216
+ return { ...prev, definitions: rest };
217
+ });
218
+ };
219
+ }, []);
220
+
221
+ return {
222
+ getState: () => state,
223
+ register,
224
+ setState: setDefState,
225
+ setSelected: setDefSelected,
226
+ };
227
+ });
228
+
229
+ attachTransformScopes(Interactables, (scopes, parent) => {
230
+ if (!scopes.modelContext && parent.modelContext.source === null) {
231
+ scopes.modelContext = ModelContext();
232
+ }
233
+ });
@@ -89,9 +89,8 @@ export const Tools = resource(
89
89
  },
90
90
  );
91
91
 
92
- attachTransformScopes(Tools, (scopes, parent) => ({
93
- ...scopes,
94
- ...(scopes.modelContext || parent.modelContext.source !== null
95
- ? {}
96
- : { modelContext: ModelContext() }),
97
- }));
92
+ attachTransformScopes(Tools, (scopes, parent) => {
93
+ if (!scopes.modelContext && parent.modelContext.source === null) {
94
+ scopes.modelContext = ModelContext();
95
+ }
96
+ });
@@ -28,10 +28,21 @@ export {
28
28
  } from "./model-context/useAssistantDataUI";
29
29
  export { useInlineRender } from "./model-context/useInlineRender";
30
30
  export type { Toolkit, ToolDefinition } from "./model-context/toolbox";
31
+ export {
32
+ useInteractable,
33
+ type UseInteractableConfig,
34
+ type UseInteractableMetadata,
35
+ } from "./model-context/useInteractable";
36
+ export {
37
+ makeInteractable,
38
+ type InteractableConfig,
39
+ type AssistantInteractable,
40
+ } from "./model-context/makeInteractable";
31
41
 
32
42
  // client
33
43
  export { Tools } from "./client/Tools";
34
44
  export { DataRenderers } from "./client/DataRenderers";
45
+ export { Interactables } from "./client/Interactables";
35
46
 
36
47
  // types
37
48
  export type {
@@ -68,6 +79,14 @@ export type {
68
79
  DataRenderersMethods,
69
80
  DataRenderersClientSchema,
70
81
  } from "./types/scopes/dataRenderers";
82
+ export type {
83
+ InteractableStateSchema,
84
+ InteractablesState,
85
+ InteractableDefinition,
86
+ InteractableRegistration,
87
+ InteractablesMethods,
88
+ InteractablesClientSchema,
89
+ } from "./types/scopes/interactables";
71
90
 
72
91
  // providers
73
92
  export {
@@ -85,6 +104,10 @@ export {
85
104
  SuggestionByIndexProvider,
86
105
  type SuggestionByIndexProviderProps,
87
106
  } from "./providers/SuggestionByIndexProvider";
107
+ export {
108
+ QueueItemByIndexProvider,
109
+ type QueueItemByIndexProviderProps,
110
+ } from "./providers/QueueItemByIndexProvider";
88
111
  export { ReadonlyThreadProvider } from "./providers/ReadonlyThreadProvider";
89
112
 
90
113
  // RuntimeAdapter
@@ -144,6 +167,7 @@ export {
144
167
  ComposerPrimitiveAttachments,
145
168
  ComposerPrimitiveAttachmentByIndex,
146
169
  } from "./primitives/composer/ComposerAttachments";
170
+ export { ComposerPrimitiveQueue } from "./primitives/composer/ComposerQueue";
147
171
  export {
148
172
  ThreadListPrimitiveItems,
149
173
  ThreadListPrimitiveItemByIndex,
@@ -0,0 +1,21 @@
1
+ import { FC } from "react";
2
+ import { useInteractable, type UseInteractableConfig } from "./useInteractable";
3
+
4
+ export type InteractableConfig<TState> = UseInteractableConfig<TState> & {
5
+ name: string;
6
+ };
7
+
8
+ export type AssistantInteractable = FC & {
9
+ unstable_interactable: InteractableConfig<any>;
10
+ };
11
+
12
+ export const makeInteractable = <TState>(
13
+ config: InteractableConfig<TState>,
14
+ ) => {
15
+ const Interactable: AssistantInteractable = () => {
16
+ useInteractable(config.name, config);
17
+ return null;
18
+ };
19
+ Interactable.unstable_interactable = config;
20
+ return Interactable;
21
+ };
@@ -0,0 +1,73 @@
1
+ import { useEffect, useCallback, useId, useRef } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+ import type { InteractableStateSchema } from "../types/scopes/interactables";
4
+
5
+ export type UseInteractableConfig<TState> = {
6
+ description: string;
7
+ stateSchema: InteractableStateSchema;
8
+ initialState: TState;
9
+ id?: string;
10
+ selected?: boolean;
11
+ };
12
+
13
+ type StateUpdater<TState> = TState | ((prev: TState) => TState);
14
+
15
+ export type UseInteractableMetadata = {
16
+ id: string;
17
+ setSelected: (selected: boolean) => void;
18
+ };
19
+
20
+ export const useInteractable = <TState>(
21
+ name: string,
22
+ config: UseInteractableConfig<TState>,
23
+ ): [
24
+ TState,
25
+ (updater: StateUpdater<TState>) => void,
26
+ UseInteractableMetadata,
27
+ ] => {
28
+ const aui = useAui();
29
+
30
+ const autoId = useId().replace(/[^a-zA-Z0-9]/g, "");
31
+ const id = config.id ?? autoId;
32
+
33
+ const stateSchemaRef = useRef(config.stateSchema);
34
+ stateSchemaRef.current = config.stateSchema;
35
+ const initialStateRef = useRef(config.initialState);
36
+ initialStateRef.current = config.initialState;
37
+
38
+ useEffect(() => {
39
+ return aui.interactables().register({
40
+ id,
41
+ name,
42
+ description: config.description,
43
+ stateSchema: stateSchemaRef.current,
44
+ initialState: initialStateRef.current,
45
+ selected: config.selected,
46
+ });
47
+ }, [aui, id, name, config.description, config.selected]);
48
+
49
+ const state =
50
+ (useAuiState((s) => s.interactables.definitions[id]?.state) as TState) ??
51
+ config.initialState;
52
+
53
+ const setState = useCallback(
54
+ (updater: StateUpdater<TState>) => {
55
+ aui.interactables().setState(id, (prev) => {
56
+ if (typeof updater === "function") {
57
+ return (updater as (prev: TState) => TState)(prev as TState);
58
+ }
59
+ return updater;
60
+ });
61
+ },
62
+ [aui, id],
63
+ );
64
+
65
+ const setSelected = useCallback(
66
+ (selected: boolean) => {
67
+ aui.interactables().setSelected(id, selected);
68
+ },
69
+ [aui, id],
70
+ );
71
+
72
+ return [state, setState, { id, setSelected }];
73
+ };
@@ -1,15 +1,22 @@
1
1
  import { useCallback } from "react";
2
2
  import { useAui, useAuiState } from "@assistant-ui/store";
3
+ import type { ComposerSendOptions } from "../../store/scopes/composer";
3
4
 
4
5
  export const useComposerSend = () => {
5
6
  const aui = useAui();
6
7
  const disabled = useAuiState(
7
- (s) => s.thread.isRunning || !s.composer.isEditing || s.composer.isEmpty,
8
+ (s) =>
9
+ (s.thread.isRunning && !s.thread.capabilities.queue) ||
10
+ !s.composer.isEditing ||
11
+ s.composer.isEmpty,
8
12
  );
9
13
 
10
- const send = useCallback(() => {
11
- aui.composer().send();
12
- }, [aui]);
14
+ const send = useCallback(
15
+ (opts?: ComposerSendOptions) => {
16
+ aui.composer().send(opts);
17
+ },
18
+ [aui],
19
+ );
13
20
 
14
21
  return { send, disabled };
15
22
  };
@@ -0,0 +1,58 @@
1
+ import { type FC, type ReactNode, memo, useMemo } from "react";
2
+ import { RenderChildrenWithAccessor, useAuiState } from "@assistant-ui/store";
3
+ import type { QueueItemState } from "../../../store/scopes/queue-item";
4
+ import { QueueItemByIndexProvider } from "../../providers/QueueItemByIndexProvider";
5
+
6
+ export namespace ComposerPrimitiveQueue {
7
+ export type Props = {
8
+ /** Render function called for each queue item. Receives the queue item state. */
9
+ children: (value: { queueItem: QueueItemState }) => ReactNode;
10
+ };
11
+ }
12
+
13
+ const ComposerPrimitiveQueueInner: FC<{
14
+ children: (value: { queueItem: QueueItemState }) => ReactNode;
15
+ }> = ({ children }) => {
16
+ const queue = useAuiState((s) => s.composer.queue.length);
17
+
18
+ return useMemo(
19
+ () =>
20
+ Array.from({ length: queue }, (_, index) => (
21
+ <QueueItemByIndexProvider key={index} index={index}>
22
+ <RenderChildrenWithAccessor
23
+ getItemState={(aui) =>
24
+ aui.composer().queueItem({ index }).getState()
25
+ }
26
+ >
27
+ {(getItem) =>
28
+ children({
29
+ get queueItem() {
30
+ return getItem();
31
+ },
32
+ })
33
+ }
34
+ </RenderChildrenWithAccessor>
35
+ </QueueItemByIndexProvider>
36
+ )),
37
+ [queue, children],
38
+ );
39
+ };
40
+
41
+ /**
42
+ * Renders all queue items in the composer.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * <ComposerPrimitive.Queue>
47
+ * {({ queueItem }) => (
48
+ * <div>
49
+ * <QueueItemPrimitive.Text />
50
+ * <QueueItemPrimitive.Steer>Run Now</QueueItemPrimitive.Steer>
51
+ * </div>
52
+ * )}
53
+ * </ComposerPrimitive.Queue>
54
+ * ```
55
+ */
56
+ export const ComposerPrimitiveQueue = memo(ComposerPrimitiveQueueInner);
57
+
58
+ ComposerPrimitiveQueue.displayName = "ComposerPrimitive.Queue";
@@ -440,6 +440,8 @@ const EmptyPartsImpl: FC<MessagePartComponentProps> = ({ components }) => {
440
440
 
441
441
  if (components?.Empty) return <components.Empty status={status} />;
442
442
 
443
+ if (status.type !== "running") return null;
444
+
443
445
  return (
444
446
  <EmptyPartFallback
445
447
  status={status}
@@ -0,0 +1,21 @@
1
+ import { type FC, type PropsWithChildren } from "react";
2
+ import { AuiProvider, Derived, useAui } from "@assistant-ui/store";
3
+
4
+ export type QueueItemByIndexProviderProps = PropsWithChildren<{
5
+ index: number;
6
+ }>;
7
+
8
+ export const QueueItemByIndexProvider: FC<QueueItemByIndexProviderProps> = ({
9
+ index,
10
+ children,
11
+ }) => {
12
+ const aui = useAui({
13
+ queueItem: Derived({
14
+ source: "composer",
15
+ query: { index },
16
+ get: (aui) => aui.composer().queueItem({ index }),
17
+ }),
18
+ });
19
+
20
+ return <AuiProvider value={aui}>{children}</AuiProvider>;
21
+ };
@@ -480,6 +480,7 @@ export class RemoteThreadListThreadListRuntimeCore
480
480
  throw new Error("Thread is not yet initialized");
481
481
 
482
482
  await this._ensureThreadIsNotMain(data.id);
483
+ this._hookManager.stopThreadRuntime(data.id);
483
484
 
484
485
  return this._state.optimisticUpdate({
485
486
  execute: async () => {
@@ -0,0 +1,44 @@
1
+ import type { Tool } from "assistant-stream";
2
+ import type { Unsubscribe } from "../../..";
3
+
4
+ /**
5
+ * Schema type matching Tool["parameters"] from assistant-stream.
6
+ * Accepts both StandardSchemaV1 and JSONSchema7.
7
+ */
8
+ export type InteractableStateSchema = NonNullable<
9
+ Extract<Tool, { parameters: unknown }>["parameters"]
10
+ >;
11
+
12
+ export type InteractableDefinition = {
13
+ id: string;
14
+ name: string;
15
+ description: string;
16
+ stateSchema: InteractableStateSchema;
17
+ state: unknown;
18
+ selected?: boolean | undefined;
19
+ };
20
+
21
+ export type InteractableRegistration = {
22
+ id: string;
23
+ name: string;
24
+ description: string;
25
+ stateSchema: InteractableStateSchema;
26
+ initialState: unknown;
27
+ selected?: boolean | undefined;
28
+ };
29
+
30
+ export type InteractablesState = {
31
+ /** Keyed by instance id */
32
+ definitions: Record<string, InteractableDefinition>;
33
+ };
34
+
35
+ export type InteractablesMethods = {
36
+ getState(): InteractablesState;
37
+ register(def: InteractableRegistration): Unsubscribe;
38
+ setState(id: string, updater: (prev: unknown) => unknown): void;
39
+ setSelected(id: string, selected: boolean): void;
40
+ };
41
+
42
+ export type InteractablesClientSchema = {
43
+ methods: InteractablesMethods;
44
+ };
@@ -1,9 +1,11 @@
1
1
  import type { ToolsClientSchema } from "./scopes/tools";
2
2
  import type { DataRenderersClientSchema } from "./scopes/dataRenderers";
3
+ import type { InteractablesClientSchema } from "./scopes/interactables";
3
4
 
4
5
  declare module "@assistant-ui/store" {
5
6
  interface ScopeRegistry {
6
7
  tools: ToolsClientSchema;
7
8
  dataRenderers: DataRenderersClientSchema;
9
+ interactables: InteractablesClientSchema;
8
10
  }
9
11
  }
@@ -9,7 +9,10 @@ import type { QuoteInfo } from "../../types/quote";
9
9
  import type { Unsubscribe } from "../../types/unsubscribe";
10
10
  import type { RunConfig } from "../../types/message";
11
11
  import { BaseSubscribable } from "../../subscribable/subscribable";
12
- import type { AttachmentAdapter } from "../../adapters/attachment";
12
+ import {
13
+ type AttachmentAdapter,
14
+ fileMatchesAccept,
15
+ } from "../../adapters/attachment";
13
16
  import type {
14
17
  ComposerRuntimeCore,
15
18
  ComposerRuntimeEventType,
@@ -216,6 +219,17 @@ export abstract class BaseComposerRuntimeCore
216
219
  const adapter = this.getAttachmentAdapter();
217
220
  if (!adapter) throw new Error("Attachments are not supported");
218
221
 
222
+ if (
223
+ !fileMatchesAccept(
224
+ { name: fileOrAttachment.name, type: fileOrAttachment.type },
225
+ adapter.accept,
226
+ )
227
+ ) {
228
+ throw new Error(
229
+ `File type ${fileOrAttachment.type || "unknown"} is not accepted. Accepted types: ${adapter.accept}`,
230
+ );
231
+ }
232
+
219
233
  const upsertAttachment = (a: PendingAttachment) => {
220
234
  const idx = this._attachments.findIndex(
221
235
  (attachment) => attachment.id === a.id,
@@ -233,17 +247,39 @@ export abstract class BaseComposerRuntimeCore
233
247
  this._notifySubscribers();
234
248
  };
235
249
 
236
- const promiseOrGenerator = adapter.add({ file: fileOrAttachment });
237
- if (Symbol.asyncIterator in promiseOrGenerator) {
238
- for await (const r of promiseOrGenerator) {
239
- upsertAttachment(r);
250
+ let lastAttachment: PendingAttachment | undefined;
251
+ try {
252
+ const promiseOrGenerator = adapter.add({ file: fileOrAttachment });
253
+ if (Symbol.asyncIterator in promiseOrGenerator) {
254
+ for await (const r of promiseOrGenerator) {
255
+ lastAttachment = r;
256
+ upsertAttachment(r);
257
+ }
258
+ } else {
259
+ lastAttachment = await promiseOrGenerator;
260
+ upsertAttachment(lastAttachment);
240
261
  }
241
- } else {
242
- upsertAttachment(await promiseOrGenerator);
262
+ } catch (e) {
263
+ if (lastAttachment) {
264
+ upsertAttachment({
265
+ ...lastAttachment,
266
+ status: { type: "incomplete", reason: "error" },
267
+ });
268
+ }
269
+ try {
270
+ this._notifyEventSubscribers("attachmentAddError");
271
+ } catch {
272
+ // prevent subscriber errors from masking the adapter error
273
+ }
274
+ throw e;
243
275
  }
244
276
 
245
- this._notifyEventSubscribers("attachmentAdd");
246
- this._notifySubscribers();
277
+ const hasError =
278
+ lastAttachment?.status.type === "incomplete" &&
279
+ lastAttachment.status.reason === "error";
280
+ this._notifyEventSubscribers(
281
+ hasError ? "attachmentAddError" : "attachmentAdd",
282
+ );
247
283
  }
248
284
 
249
285
  async removeAttachment(attachmentId: string) {
@@ -5,7 +5,10 @@ import type { Unsubscribe } from "../../types/unsubscribe";
5
5
  import type { RunConfig } from "../../types/message";
6
6
  import type { DictationAdapter } from "../../adapters/speech";
7
7
 
8
- export type ComposerRuntimeEventType = "send" | "attachmentAdd";
8
+ export type ComposerRuntimeEventType =
9
+ | "send"
10
+ | "attachmentAdd"
11
+ | "attachmentAddError";
9
12
 
10
13
  export type DictationState = {
11
14
  readonly status: DictationAdapter.Status;
@@ -26,6 +26,7 @@ export type RuntimeCapabilities = {
26
26
  readonly dictation: boolean;
27
27
  readonly attachments: boolean;
28
28
  readonly feedback: boolean;
29
+ readonly queue: boolean;
29
30
  };
30
31
 
31
32
  export type AddToolResultOptions = {