@assistant-ui/core 0.1.15 → 0.1.17

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 (113) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  5. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  6. package/dist/react/index.d.ts +2 -1
  7. package/dist/react/index.d.ts.map +1 -1
  8. package/dist/react/index.js +1 -0
  9. package/dist/react/index.js.map +1 -1
  10. package/dist/react/primitives/message/MessageGroupedParts.d.ts +104 -0
  11. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -0
  12. package/dist/react/primitives/message/MessageGroupedParts.js +74 -0
  13. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -0
  14. package/dist/react/primitives/message/MessageParts.d.ts +14 -1
  15. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  16. package/dist/react/primitives/message/MessageParts.js +55 -35
  17. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  18. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +3 -3
  19. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  20. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +26 -5
  21. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  22. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +2 -2
  23. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  24. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +6 -5
  25. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  26. package/dist/react/utils/groupParts.d.ts +49 -0
  27. package/dist/react/utils/groupParts.d.ts.map +1 -0
  28. package/dist/react/utils/groupParts.js +97 -0
  29. package/dist/react/utils/groupParts.js.map +1 -0
  30. package/dist/runtime/api/bindings.d.ts +1 -0
  31. package/dist/runtime/api/bindings.d.ts.map +1 -1
  32. package/dist/runtime/api/composer-runtime.d.ts +7 -5
  33. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  34. package/dist/runtime/api/composer-runtime.js +1 -1
  35. package/dist/runtime/api/composer-runtime.js.map +1 -1
  36. package/dist/runtime/api/thread-list-item-runtime.d.ts +18 -3
  37. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  38. package/dist/runtime/api/thread-list-item-runtime.js +1 -1
  39. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  40. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  41. package/dist/runtime/api/thread-list-runtime.js +1 -0
  42. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  43. package/dist/runtime/api/thread-runtime.d.ts +5 -5
  44. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  45. package/dist/runtime/api/thread-runtime.js +1 -1
  46. package/dist/runtime/api/thread-runtime.js.map +1 -1
  47. package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -3
  48. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  49. package/dist/runtime/base/base-composer-runtime-core.js +54 -26
  50. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  51. package/dist/runtime/base/base-thread-runtime-core.d.ts +3 -3
  52. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  53. package/dist/runtime/base/base-thread-runtime-core.js +11 -11
  54. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  55. package/dist/runtime/interfaces/composer-runtime-core.d.ts +28 -2
  56. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  57. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +1 -0
  58. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  59. package/dist/runtime/interfaces/thread-runtime-core.d.ts +37 -2
  60. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  61. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +2 -2
  62. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  63. package/dist/runtimes/local/local-thread-runtime-core.js +2 -2
  64. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  65. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +3 -0
  66. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  67. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  68. package/dist/runtimes/remote-thread-list/types.d.ts +13 -1
  69. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  70. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  71. package/dist/store/runtime-clients/composer-runtime-client.js +5 -6
  72. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  73. package/dist/store/scopes/composer.d.ts +11 -1
  74. package/dist/store/scopes/composer.d.ts.map +1 -1
  75. package/dist/store/scopes/thread-list-item.d.ts +11 -0
  76. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  77. package/dist/store/scopes/thread.d.ts +17 -0
  78. package/dist/store/scopes/thread.d.ts.map +1 -1
  79. package/dist/subscribable/subscribable.d.ts +4 -4
  80. package/dist/subscribable/subscribable.d.ts.map +1 -1
  81. package/dist/subscribable/subscribable.js +4 -4
  82. package/dist/subscribable/subscribable.js.map +1 -1
  83. package/package.json +21 -9
  84. package/src/index.ts +10 -0
  85. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +2 -0
  86. package/src/react/index.ts +2 -0
  87. package/src/react/primitives/message/MessageGroupedParts.tsx +186 -0
  88. package/src/react/primitives/message/MessageParts.tsx +101 -49
  89. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +40 -7
  90. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +6 -12
  91. package/src/react/utils/groupParts.ts +152 -0
  92. package/src/runtime/api/bindings.ts +1 -0
  93. package/src/runtime/api/composer-runtime.ts +14 -11
  94. package/src/runtime/api/thread-list-item-runtime.ts +28 -6
  95. package/src/runtime/api/thread-list-runtime.ts +1 -0
  96. package/src/runtime/api/thread-runtime.ts +11 -7
  97. package/src/runtime/base/base-composer-runtime-core.ts +99 -35
  98. package/src/runtime/base/base-thread-runtime-core.ts +21 -12
  99. package/src/runtime/interfaces/composer-runtime-core.ts +39 -7
  100. package/src/runtime/interfaces/thread-list-runtime-core.ts +1 -0
  101. package/src/runtime/interfaces/thread-runtime-core.ts +44 -6
  102. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +2 -2
  103. package/src/runtimes/local/local-thread-runtime-core.ts +2 -2
  104. package/src/runtimes/remote-thread-list/remote-thread-state.ts +3 -0
  105. package/src/runtimes/remote-thread-list/types.ts +13 -1
  106. package/src/store/runtime-clients/composer-runtime-client.ts +5 -9
  107. package/src/store/scopes/composer.ts +11 -0
  108. package/src/store/scopes/thread-list-item.ts +11 -0
  109. package/src/store/scopes/thread.ts +17 -0
  110. package/src/subscribable/subscribable.ts +10 -7
  111. package/src/tests/RemoteThreadListThreadListRuntimeCore-custom-metadata.test.ts +123 -0
  112. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +217 -0
  113. package/src/tests/groupParts.test.ts +114 -0
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  type FC,
3
+ type RefObject,
3
4
  useCallback,
4
5
  useRef,
5
6
  useEffect,
7
+ useLayoutEffect,
6
8
  memo,
7
9
  type PropsWithChildren,
8
10
  type ComponentType,
9
11
  useMemo,
12
+ Fragment,
10
13
  } from "react";
11
14
  import { type UseBoundStore, type StoreApi, create } from "zustand";
12
15
  import { useAui } from "@assistant-ui/store";
@@ -23,6 +26,15 @@ type RemoteThreadListHook = () => AssistantRuntime;
23
26
  type RemoteThreadListHookInstance = {
24
27
  runtime?: ThreadRuntimeCore;
25
28
  };
29
+
30
+ const ProviderRenderDetector: FC<{
31
+ detectorRef: RefObject<boolean>;
32
+ }> = ({ detectorRef }) => {
33
+ useLayoutEffect(() => {
34
+ detectorRef.current = true;
35
+ }, [detectorRef]);
36
+ return null;
37
+ };
26
38
  export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
27
39
  private useRuntimeHook: UseBoundStore<
28
40
  StoreApi<{ useRuntime: RemoteThreadListHook }>
@@ -82,9 +94,11 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
82
94
  }
83
95
  }
84
96
 
85
- private _InnerActiveThreadProvider: FC<{
86
- threadId: string;
87
- }> = ({ threadId }) => {
97
+ // Rendered outside the user's Provider so deferred `children` cannot strand the binding.
98
+ private _RuntimeBinder: FC<PropsWithChildren<{ threadId: string }>> = ({
99
+ threadId,
100
+ children,
101
+ }) => {
88
102
  const { useRuntime } = this.useRuntimeHook();
89
103
  const runtime = useRuntime();
90
104
 
@@ -138,7 +152,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
138
152
  });
139
153
  }, [runtime, aui]);
140
154
 
141
- return null;
155
+ return <>{children}</>;
142
156
  };
143
157
 
144
158
  private _OuterActiveThreadProvider: FC<{
@@ -150,11 +164,30 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
150
164
  [threadId],
151
165
  );
152
166
 
167
+ const detectorRef = useRef(false);
168
+ useEffect(() => {
169
+ if (process.env.NODE_ENV !== "production" && Provider !== Fragment) {
170
+ const id = setTimeout(() => {
171
+ if (!detectorRef.current) {
172
+ console.warn(
173
+ "RemoteThreadListAdapter.unstable_Provider did not render its `children` synchronously. " +
174
+ "Render `children` on first commit; deferring them behind a loading state, Suspense boundary, " +
175
+ "or `useEffect` gate leaves thread context unavailable to downstream consumers.",
176
+ );
177
+ }
178
+ }, 100);
179
+ return () => clearTimeout(id);
180
+ }
181
+ return undefined;
182
+ }, [Provider]);
183
+
153
184
  return (
154
185
  <ThreadListItemRuntimeProvider runtime={runtime}>
155
- <Provider>
156
- <this._InnerActiveThreadProvider threadId={threadId} />
157
- </Provider>
186
+ <this._RuntimeBinder threadId={threadId}>
187
+ <Provider>
188
+ <ProviderRenderDetector detectorRef={detectorRef} />
189
+ </Provider>
190
+ </this._RuntimeBinder>
158
191
  </ThreadListItemRuntimeProvider>
159
192
  );
160
193
  });
@@ -15,14 +15,7 @@ import {
15
15
  } from "../../runtimes/remote-thread-list/remote-thread-state";
16
16
  import type { RemoteThreadListOptions } from "../../runtimes/remote-thread-list/types";
17
17
  import { RemoteThreadListHookInstanceManager } from "./RemoteThreadListHookInstanceManager";
18
- import {
19
- type ComponentType,
20
- type FC,
21
- Fragment,
22
- type PropsWithChildren,
23
- useEffect,
24
- useId,
25
- } from "react";
18
+ import { type FC, Fragment, useEffect, useId } from "react";
26
19
  import { create } from "zustand";
27
20
  import { AssistantMessageStream } from "assistant-stream";
28
21
  import type { ModelContextProvider } from "../../model-context/types";
@@ -98,6 +91,7 @@ export class RemoteThreadListThreadListRuntimeCore
98
91
  externalId: thread.externalId,
99
92
  status: thread.status,
100
93
  title: thread.title,
94
+ custom: thread.custom,
101
95
  initializeTask: Promise.resolve({
102
96
  remoteId: thread.remoteId,
103
97
  externalId: thread.externalId,
@@ -150,8 +144,7 @@ export class RemoteThreadListThreadListRuntimeCore
150
144
  this,
151
145
  );
152
146
  this.useProvider = create(() => ({
153
- Provider: (options.adapter.unstable_Provider ??
154
- Fragment) as ComponentType<PropsWithChildren>,
147
+ Provider: options.adapter.unstable_Provider ?? Fragment,
155
148
  }));
156
149
  this.__internal_setOptions(options);
157
150
  this.switchToNewThread();
@@ -165,8 +158,7 @@ export class RemoteThreadListThreadListRuntimeCore
165
158
 
166
159
  this._options = options;
167
160
 
168
- const Provider = (options.adapter.unstable_Provider ??
169
- Fragment) as ComponentType<PropsWithChildren>;
161
+ const Provider = options.adapter.unstable_Provider ?? Fragment;
170
162
  if (Provider !== this.useProvider.getState().Provider) {
171
163
  this.useProvider.setState({ Provider }, true);
172
164
  }
@@ -250,6 +242,7 @@ export class RemoteThreadListThreadListRuntimeCore
250
242
  externalId: remoteMetadata.externalId,
251
243
  status: remoteMetadata.status,
252
244
  title: remoteMetadata.title,
245
+ custom: remoteMetadata.custom,
253
246
  } as RemoteThreadData,
254
247
  };
255
248
 
@@ -335,6 +328,7 @@ export class RemoteThreadListThreadListRuntimeCore
335
328
  remoteId: undefined,
336
329
  externalId: undefined,
337
330
  title: undefined,
331
+ custom: undefined,
338
332
  } satisfies RemoteThreadData,
339
333
  },
340
334
  });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Hierarchical adjacent-coalescing grouping for message parts.
3
+ *
4
+ * Given a group path per part (from `groupBy`), builds a tree of group
5
+ * nodes wrapping individual parts. Adjacent parts sharing a path prefix
6
+ * coalesce into the same group; ungrouped parts are direct children of
7
+ * the root.
8
+ *
9
+ * Each node gets a structural `nodeKey` built from sibling indices
10
+ * (`"0.1.0"`), stable under append-only streaming.
11
+ */
12
+
13
+ /**
14
+ * Public group key type. Group keys must be prefixed with `group-` so
15
+ * that a unified `switch (part.type)` in the renderer can distinguish
16
+ * a group key (e.g. `"group-thought"`) from a real part type
17
+ * (`"text"`, `"tool-call"`).
18
+ */
19
+ export type GroupKey<TKey extends `group-${string}` = `group-${string}`> =
20
+ | TKey
21
+ | readonly TKey[]
22
+ | null
23
+ | undefined;
24
+
25
+ export type GroupNode = GroupNodeGroup | GroupNodePart;
26
+
27
+ export interface GroupNodeGroup {
28
+ readonly type: "group";
29
+ /** Current-level group key (last segment of the path). */
30
+ readonly key: string;
31
+ /** Structural React key: sibling-index path, e.g. `"0.1.0"`. */
32
+ readonly nodeKey: string;
33
+ /** Indices of parts in this subtree, in order. */
34
+ readonly indices: readonly number[];
35
+ readonly children: readonly GroupNode[];
36
+ }
37
+
38
+ export interface GroupNodePart {
39
+ readonly type: "part";
40
+ /** Index of the part in the message. */
41
+ readonly index: number;
42
+ /** Structural React key: sibling-index path within parent. */
43
+ readonly nodeKey: string;
44
+ }
45
+
46
+ const EMPTY_PATH: readonly string[] = Object.freeze([]);
47
+
48
+ /**
49
+ * Normalize a `groupBy` return value to a path array.
50
+ * `null`/`undefined`/`[]` → `[]` (ungrouped).
51
+ * `"foo"` → `["foo"]`. Arrays pass through.
52
+ */
53
+ export const normalizeGroupKey = (key: GroupKey): readonly string[] => {
54
+ if (key == null) return EMPTY_PATH;
55
+ if (typeof key === "string") return [key];
56
+ return key;
57
+ };
58
+
59
+ interface BuildFrame {
60
+ key: string;
61
+ nodeKey: string;
62
+ indices: number[];
63
+ children: GroupNode[];
64
+ nextChildIdx: number;
65
+ }
66
+
67
+ const makeChildNodeKey = (parent: BuildFrame): string => {
68
+ const idx = parent.nextChildIdx++;
69
+ return parent.nodeKey === "" ? String(idx) : `${parent.nodeKey}.${idx}`;
70
+ };
71
+
72
+ /**
73
+ * Build the group tree from an array of normalized group paths.
74
+ * `paths[i]` is the path for part `i`. The output tree contains one
75
+ * `part` node per part and one `group` node per coalesced run.
76
+ */
77
+ export const buildGroupTree = (
78
+ paths: readonly (readonly string[])[],
79
+ ): readonly GroupNode[] => {
80
+ const root: BuildFrame = {
81
+ key: "",
82
+ nodeKey: "",
83
+ indices: [],
84
+ children: [],
85
+ nextChildIdx: 0,
86
+ };
87
+ const stack: BuildFrame[] = [root];
88
+
89
+ const closeTop = (): void => {
90
+ const closing = stack.pop()!;
91
+ const parent = stack[stack.length - 1]!;
92
+ parent.children.push({
93
+ type: "group",
94
+ key: closing.key,
95
+ nodeKey: closing.nodeKey,
96
+ indices: closing.indices,
97
+ children: closing.children,
98
+ });
99
+ };
100
+
101
+ for (let i = 0; i < paths.length; i++) {
102
+ const path = paths[i]!;
103
+
104
+ // Find the longest prefix shared between currently-open groups
105
+ // (excluding root) and this part's path.
106
+ let common = 0;
107
+ while (
108
+ common < stack.length - 1 &&
109
+ common < path.length &&
110
+ stack[common + 1]!.key === path[common]
111
+ ) {
112
+ common++;
113
+ }
114
+
115
+ // Close groups not on this path.
116
+ while (stack.length - 1 > common) {
117
+ closeTop();
118
+ }
119
+
120
+ // Open new groups down to the part's depth.
121
+ while (stack.length - 1 < path.length) {
122
+ const parent = stack[stack.length - 1]!;
123
+ stack.push({
124
+ key: path[stack.length - 1]!,
125
+ nodeKey: makeChildNodeKey(parent),
126
+ indices: [],
127
+ children: [],
128
+ nextChildIdx: 0,
129
+ });
130
+ }
131
+
132
+ // Push this part as a leaf in the deepest open group (or root).
133
+ const top = stack[stack.length - 1]!;
134
+ top.children.push({
135
+ type: "part",
136
+ index: i,
137
+ nodeKey: makeChildNodeKey(top),
138
+ });
139
+
140
+ // Record the part index in every open ancestor group.
141
+ for (let s = 1; s < stack.length; s++) {
142
+ stack[s]!.indices.push(i);
143
+ }
144
+ }
145
+
146
+ // Close any still-open groups.
147
+ while (stack.length > 1) {
148
+ closeTop();
149
+ }
150
+
151
+ return root.children;
152
+ };
@@ -47,4 +47,5 @@ export type ThreadListItemState = {
47
47
  readonly externalId: string | undefined;
48
48
  readonly status: import("../interfaces/thread-list-runtime-core").ThreadListItemStatus;
49
49
  readonly title?: string | undefined;
50
+ readonly custom?: Record<string, unknown> | undefined;
50
51
  };
@@ -12,6 +12,7 @@ import {
12
12
  SKIP_UPDATE,
13
13
  } from "../../subscribable/subscribable";
14
14
  import type {
15
+ ComposerRuntimeEventCallback,
15
16
  ComposerRuntimeEventType,
16
17
  DictationState,
17
18
  EditComposerRuntimeCore,
@@ -136,8 +137,10 @@ export type ComposerRuntime = {
136
137
  /**
137
138
  * Add an attachment to the composer. Accepts either a standard File object
138
139
  * (processed through the AttachmentAdapter) or a CreateAttachment descriptor
139
- * for external-source attachments (URLs, API data, CMS references) that
140
- * bypasses the adapter entirely.
140
+ * for external-source attachments (URLs, API data, CMS references). External
141
+ * descriptors bypass the adapter's `add()` step but still respect
142
+ * `adapter.accept` when an adapter is configured; without an adapter they
143
+ * are added as-is.
141
144
  * @param fileOrAttachment The file or attachment descriptor to add.
142
145
  */
143
146
  addAttachment(fileOrAttachment: File | CreateAttachment): Promise<void>;
@@ -220,9 +223,9 @@ export type ComposerRuntime = {
220
223
  /**
221
224
  * @deprecated This API is still under active development and might change without notice.
222
225
  */
223
- unstable_on(
224
- event: ComposerRuntimeEventType,
225
- callback: () => void,
226
+ unstable_on<E extends ComposerRuntimeEventType>(
227
+ event: E,
228
+ callback: ComposerRuntimeEventCallback<E>,
226
229
  ): Unsubscribe;
227
230
  };
228
231
 
@@ -330,19 +333,19 @@ export abstract class ComposerRuntimeImpl implements ComposerRuntime {
330
333
  EventSubscriptionSubject<ComposerRuntimeEventType>
331
334
  >();
332
335
 
333
- public unstable_on(
334
- event: ComposerRuntimeEventType,
335
- callback: () => void,
336
+ public unstable_on<E extends ComposerRuntimeEventType>(
337
+ event: E,
338
+ callback: ComposerRuntimeEventCallback<E>,
336
339
  ): Unsubscribe {
337
340
  let subject = this._eventSubscriptionSubjects.get(event);
338
341
  if (!subject) {
339
- subject = new EventSubscriptionSubject({
340
- event: event,
342
+ subject = new EventSubscriptionSubject<ComposerRuntimeEventType>({
343
+ event,
341
344
  binding: this._core,
342
345
  });
343
346
  this._eventSubscriptionSubjects.set(event, subject);
344
347
  }
345
- return subject.subscribe(callback);
348
+ return subject.subscribe(callback as (payload?: unknown) => void);
346
349
  }
347
350
 
348
351
  public abstract getAttachmentByIndex(idx: number): AttachmentRuntime;
@@ -3,7 +3,26 @@ import type { SubscribableWithState } from "../../subscribable/subscribable";
3
3
  import type { ThreadListItemRuntimePath } from "./paths";
4
4
  import type { ThreadListRuntimeCoreBinding } from "./thread-list-runtime";
5
5
 
6
- export type ThreadListItemEventType = "switchedTo" | "switchedAway";
6
+ export type ThreadListItemEventPayload = {
7
+ /**
8
+ * @deprecated State-derivable. Compare `s.threads.mainThreadId` against the
9
+ * item's `s.threadListItem.id` via `useAuiState` instead. Kept for backward
10
+ * compatibility.
11
+ */
12
+ switchedTo: Record<string, never>;
13
+ /**
14
+ * @deprecated State-derivable. Compare `s.threads.mainThreadId` against the
15
+ * item's `s.threadListItem.id` via `useAuiState` instead. Kept for backward
16
+ * compatibility.
17
+ */
18
+ switchedAway: Record<string, never>;
19
+ };
20
+
21
+ export type ThreadListItemEventType = keyof ThreadListItemEventPayload;
22
+
23
+ export type ThreadListItemEventCallback<E extends ThreadListItemEventType> = (
24
+ payload: ThreadListItemEventPayload[E],
25
+ ) => void;
7
26
 
8
27
  import type { ThreadListItemState } from "./bindings";
9
28
  import type { ThreadListItemStatus } from "../interfaces/thread-list-runtime-core";
@@ -27,9 +46,9 @@ export type ThreadListItemRuntime = {
27
46
 
28
47
  subscribe(callback: () => void): Unsubscribe;
29
48
 
30
- unstable_on(
31
- event: ThreadListItemEventType,
32
- callback: () => void,
49
+ unstable_on<E extends ThreadListItemEventType>(
50
+ event: E,
51
+ callback: ThreadListItemEventCallback<E>,
33
52
  ): Unsubscribe;
34
53
 
35
54
  __internal_getRuntime(): ThreadListItemRuntime;
@@ -112,7 +131,10 @@ export class ThreadListItemRuntimeImpl implements ThreadListItemRuntime {
112
131
  return this._threadListBinding.generateTitle(state.id);
113
132
  }
114
133
 
115
- public unstable_on(event: ThreadListItemEventType, callback: () => void) {
134
+ public unstable_on<E extends ThreadListItemEventType>(
135
+ event: E,
136
+ callback: ThreadListItemEventCallback<E>,
137
+ ) {
116
138
  let prevIsMain = this._core.getState().isMain;
117
139
  let prevThreadId = this._core.getState().id;
118
140
  return this.subscribe(() => {
@@ -125,7 +147,7 @@ export class ThreadListItemRuntimeImpl implements ThreadListItemRuntime {
125
147
 
126
148
  if (event === "switchedTo" && !newIsMain) return;
127
149
  if (event === "switchedAway" && newIsMain) return;
128
- callback();
150
+ (callback as (payload?: unknown) => void)({});
129
151
  });
130
152
  }
131
153
 
@@ -80,6 +80,7 @@ const getThreadListItemState = (
80
80
  externalId: threadData.externalId,
81
81
  title: threadData.title,
82
82
  status: threadData.status,
83
+ custom: threadData.custom,
83
84
  isMain: threadData.id === threadList.mainThreadId,
84
85
  };
85
86
  };
@@ -4,6 +4,7 @@ import type {
4
4
  ThreadRuntimeCore,
5
5
  SpeechState,
6
6
  VoiceSessionState,
7
+ ThreadRuntimeEventCallback,
7
8
  ThreadRuntimeEventType,
8
9
  StartRunConfig,
9
10
  ResumeRunConfig,
@@ -329,7 +330,10 @@ export type ThreadRuntime = {
329
330
  muteVoice(): void;
330
331
  unmuteVoice(): void;
331
332
 
332
- unstable_on(event: ThreadRuntimeEventType, callback: () => void): Unsubscribe;
333
+ unstable_on<E extends ThreadRuntimeEventType>(
334
+ event: E,
335
+ callback: ThreadRuntimeEventCallback<E>,
336
+ ): Unsubscribe;
333
337
  };
334
338
 
335
339
  export class ThreadRuntimeImpl implements ThreadRuntime {
@@ -601,18 +605,18 @@ export class ThreadRuntimeImpl implements ThreadRuntime {
601
605
  EventSubscriptionSubject<ThreadRuntimeEventType>
602
606
  >();
603
607
 
604
- public unstable_on(
605
- event: ThreadRuntimeEventType,
606
- callback: () => void,
608
+ public unstable_on<E extends ThreadRuntimeEventType>(
609
+ event: E,
610
+ callback: ThreadRuntimeEventCallback<E>,
607
611
  ): Unsubscribe {
608
612
  let subject = this._eventSubscriptionSubjects.get(event);
609
613
  if (!subject) {
610
- subject = new EventSubscriptionSubject({
611
- event: event,
614
+ subject = new EventSubscriptionSubject<ThreadRuntimeEventType>({
615
+ event,
612
616
  binding: this._threadBinding,
613
617
  });
614
618
  this._eventSubscriptionSubjects.set(event, subject);
615
619
  }
616
- return subject.subscribe(callback);
620
+ return subject.subscribe(callback as (payload?: unknown) => void);
617
621
  }
618
622
  }
@@ -14,7 +14,10 @@ import {
14
14
  fileMatchesAccept,
15
15
  } from "../../adapters/attachment";
16
16
  import type {
17
+ AttachmentAddErrorReason,
17
18
  ComposerRuntimeCore,
19
+ ComposerRuntimeEventCallback,
20
+ ComposerRuntimeEventPayload,
18
21
  ComposerRuntimeEventType,
19
22
  DictationState,
20
23
  SendOptions,
@@ -189,7 +192,7 @@ export abstract class BaseComposerRuntimeCore
189
192
  };
190
193
 
191
194
  this.handleSend(message, options);
192
- this._notifyEventSubscribers("send");
195
+ this._notifyEventSubscribers("send", {});
193
196
  }
194
197
 
195
198
  public cancel() {
@@ -204,6 +207,28 @@ export abstract class BaseComposerRuntimeCore
204
207
 
205
208
  async addAttachment(fileOrAttachment: File | CreateAttachment) {
206
209
  if (!(fileOrAttachment instanceof File)) {
210
+ const adapter = this.getAttachmentAdapter();
211
+ if (
212
+ adapter &&
213
+ !fileMatchesAccept(
214
+ {
215
+ name: fileOrAttachment.name,
216
+ type: fileOrAttachment.contentType ?? "",
217
+ },
218
+ adapter.accept,
219
+ )
220
+ ) {
221
+ const message = `File type ${fileOrAttachment.contentType || "unknown"} is not accepted. Accepted types: ${adapter.accept}`;
222
+ const err = new Error(message);
223
+ this._safeEmitAttachmentAddError(
224
+ "not-accepted",
225
+ message,
226
+ undefined,
227
+ err,
228
+ );
229
+ throw err;
230
+ }
231
+
207
232
  const a: CompleteAttachment = {
208
233
  id: fileOrAttachment.id ?? generateId(),
209
234
  type: fileOrAttachment.type ?? "document",
@@ -213,25 +238,11 @@ export abstract class BaseComposerRuntimeCore
213
238
  status: { type: "complete" },
214
239
  };
215
240
  this._attachments = [...this._attachments, a];
216
- this._notifyEventSubscribers("attachmentAdd");
217
241
  this._notifySubscribers();
242
+ this._notifyEventSubscribers("attachmentAdd", {});
218
243
  return;
219
244
  }
220
245
 
221
- const adapter = this.getAttachmentAdapter();
222
- if (!adapter) throw new Error("Attachments are not supported");
223
-
224
- if (
225
- !fileMatchesAccept(
226
- { name: fileOrAttachment.name, type: fileOrAttachment.type },
227
- adapter.accept,
228
- )
229
- ) {
230
- throw new Error(
231
- `File type ${fileOrAttachment.type || "unknown"} is not accepted. Accepted types: ${adapter.accept}`,
232
- );
233
- }
234
-
235
246
  const upsertAttachment = (a: PendingAttachment) => {
236
247
  const idx = this._attachments.findIndex(
237
248
  (attachment) => attachment.id === a.id,
@@ -249,6 +260,26 @@ export abstract class BaseComposerRuntimeCore
249
260
  this._notifySubscribers();
250
261
  };
251
262
 
263
+ const adapter = this.getAttachmentAdapter();
264
+ if (!adapter) {
265
+ const message = "Attachments are not supported";
266
+ const err = new Error(message);
267
+ this._safeEmitAttachmentAddError("no-adapter", message, undefined, err);
268
+ throw err;
269
+ }
270
+
271
+ if (
272
+ !fileMatchesAccept(
273
+ { name: fileOrAttachment.name, type: fileOrAttachment.type },
274
+ adapter.accept,
275
+ )
276
+ ) {
277
+ const message = `File type ${fileOrAttachment.type || "unknown"} is not accepted. Accepted types: ${adapter.accept}`;
278
+ const err = new Error(message);
279
+ this._safeEmitAttachmentAddError("not-accepted", message, undefined, err);
280
+ throw err;
281
+ }
282
+
252
283
  let lastAttachment: PendingAttachment | undefined;
253
284
  try {
254
285
  const promiseOrGenerator = adapter.add({ file: fileOrAttachment });
@@ -268,20 +299,48 @@ export abstract class BaseComposerRuntimeCore
268
299
  status: { type: "incomplete", reason: "error" },
269
300
  });
270
301
  }
271
- try {
272
- this._notifyEventSubscribers("attachmentAddError");
273
- } catch {
274
- // prevent subscriber errors from masking the adapter error
275
- }
302
+ this._safeEmitAttachmentAddError(
303
+ "adapter-error",
304
+ e instanceof Error ? e.message : String(e),
305
+ lastAttachment?.id,
306
+ e instanceof Error ? e : undefined,
307
+ );
276
308
  throw e;
277
309
  }
278
310
 
279
311
  const hasError =
280
312
  lastAttachment?.status.type === "incomplete" &&
281
313
  lastAttachment.status.reason === "error";
282
- this._notifyEventSubscribers(
283
- hasError ? "attachmentAddError" : "attachmentAdd",
284
- );
314
+ if (hasError) {
315
+ this._safeEmitAttachmentAddError(
316
+ "adapter-error",
317
+ "Attachment upload did not complete successfully.",
318
+ lastAttachment?.id,
319
+ );
320
+ } else {
321
+ this._notifyEventSubscribers("attachmentAdd", {});
322
+ }
323
+ }
324
+
325
+ private _safeEmitAttachmentAddError(
326
+ reason: AttachmentAddErrorReason,
327
+ message: string,
328
+ attachmentId?: string,
329
+ error?: Error,
330
+ ) {
331
+ try {
332
+ this._notifyEventSubscribers("attachmentAddError", {
333
+ reason,
334
+ message,
335
+ ...(attachmentId !== undefined && { attachmentId }),
336
+ ...(error !== undefined && { error }),
337
+ });
338
+ } catch (subscriberError) {
339
+ console.error(
340
+ "[assistant-ui] attachmentAddError subscriber threw:",
341
+ subscriberError,
342
+ );
343
+ }
285
344
  }
286
345
 
287
346
  async removeAttachment(attachmentId: string) {
@@ -450,28 +509,33 @@ export abstract class BaseComposerRuntimeCore
450
509
 
451
510
  private _eventSubscribers = new Map<
452
511
  ComposerRuntimeEventType,
453
- Set<() => void>
512
+ Set<(payload?: unknown) => void>
454
513
  >();
455
514
 
456
- protected _notifyEventSubscribers(event: ComposerRuntimeEventType) {
515
+ protected _notifyEventSubscribers<E extends ComposerRuntimeEventType>(
516
+ event: E,
517
+ payload: ComposerRuntimeEventPayload[E],
518
+ ) {
457
519
  const subscribers = this._eventSubscribers.get(event);
458
520
  if (!subscribers) return;
459
521
 
460
- for (const callback of subscribers) callback();
522
+ for (const callback of subscribers) callback(payload);
461
523
  }
462
524
 
463
- public unstable_on(event: ComposerRuntimeEventType, callback: () => void) {
464
- const subscribers = this._eventSubscribers.get(event);
525
+ public unstable_on<E extends ComposerRuntimeEventType>(
526
+ event: E,
527
+ callback: ComposerRuntimeEventCallback<E>,
528
+ ) {
529
+ const wrapped = callback as (payload?: unknown) => void;
530
+ let subscribers = this._eventSubscribers.get(event);
465
531
  if (!subscribers) {
466
- this._eventSubscribers.set(event, new Set([callback]));
467
- } else {
468
- subscribers.add(callback);
532
+ subscribers = new Set();
533
+ this._eventSubscribers.set(event, subscribers);
469
534
  }
535
+ subscribers.add(wrapped);
470
536
 
471
537
  return () => {
472
- const subscribers = this._eventSubscribers.get(event);
473
- if (!subscribers) return;
474
- subscribers.delete(callback);
538
+ this._eventSubscribers.get(event)?.delete(wrapped);
475
539
  };
476
540
  }
477
541
  }