@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
@@ -21,6 +21,8 @@ import type {
21
21
  SpeechState,
22
22
  VoiceSessionState,
23
23
  RuntimeCapabilities,
24
+ ThreadRuntimeEventCallback,
25
+ ThreadRuntimeEventPayload,
24
26
  ThreadRuntimeEventType,
25
27
  StartRunConfig,
26
28
  ResumeRunConfig,
@@ -168,11 +170,14 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
168
170
  for (const callback of this._subscriptions) callback();
169
171
  }
170
172
 
171
- public _notifyEventSubscribers(event: ThreadRuntimeEventType) {
173
+ public _notifyEventSubscribers<E extends ThreadRuntimeEventType>(
174
+ event: E,
175
+ payload: ThreadRuntimeEventPayload[E],
176
+ ) {
172
177
  const subscribers = this._eventSubscribers.get(event);
173
178
  if (!subscribers) return;
174
179
 
175
- for (const callback of subscribers) callback();
180
+ for (const callback of subscribers) callback(payload);
176
181
  }
177
182
 
178
183
  public subscribe(callback: () => void): Unsubscribe {
@@ -432,7 +437,7 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
432
437
  protected ensureInitialized() {
433
438
  if (!this._isInitialized) {
434
439
  this._isInitialized = true;
435
- this._notifyEventSubscribers("initialize");
440
+ this._notifyEventSubscribers("initialize", {});
436
441
  }
437
442
  }
438
443
 
@@ -453,24 +458,28 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
453
458
 
454
459
  private _eventSubscribers = new Map<
455
460
  ThreadRuntimeEventType,
456
- Set<() => void>
461
+ Set<(payload?: unknown) => void>
457
462
  >();
458
463
 
459
- public unstable_on(event: ThreadRuntimeEventType, callback: () => void) {
464
+ public unstable_on<E extends ThreadRuntimeEventType>(
465
+ event: E,
466
+ callback: ThreadRuntimeEventCallback<E>,
467
+ ) {
468
+ const wrapped = callback as (payload?: unknown) => void;
460
469
  if (event === "modelContextUpdate") {
461
- return this._contextProvider.subscribe?.(callback) ?? (() => {});
470
+ // provider.subscribe is `() => void`; pump the typed empty payload to the user callback.
471
+ return this._contextProvider.subscribe?.(() => wrapped({})) ?? (() => {});
462
472
  }
463
473
 
464
- const subscribers = this._eventSubscribers.get(event);
474
+ let subscribers = this._eventSubscribers.get(event);
465
475
  if (!subscribers) {
466
- this._eventSubscribers.set(event, new Set([callback]));
467
- } else {
468
- subscribers.add(callback);
476
+ subscribers = new Set();
477
+ this._eventSubscribers.set(event, subscribers);
469
478
  }
479
+ subscribers.add(wrapped);
470
480
 
471
481
  return () => {
472
- const subscribers = this._eventSubscribers.get(event)!;
473
- subscribers.delete(callback);
482
+ this._eventSubscribers.get(event)?.delete(wrapped);
474
483
  };
475
484
  }
476
485
  }
@@ -5,10 +5,37 @@ 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 =
9
- | "send"
10
- | "attachmentAdd"
11
- | "attachmentAddError";
8
+ export type AttachmentAddErrorReason =
9
+ | "no-adapter"
10
+ | "not-accepted"
11
+ | "adapter-error";
12
+
13
+ export type AttachmentAddErrorEvent = {
14
+ readonly reason: AttachmentAddErrorReason;
15
+ readonly message: string;
16
+ readonly attachmentId?: string;
17
+ readonly error?: Error;
18
+ };
19
+
20
+ export type ComposerRuntimeEventPayload = {
21
+ /**
22
+ * @deprecated State-derivable. Observe `state.text` clearing via
23
+ * `subscribe` + `getState` instead. Kept for backward compatibility.
24
+ */
25
+ send: Record<string, never>;
26
+ /**
27
+ * @deprecated State-derivable. Observe `state.attachments` via `subscribe` +
28
+ * `getState` instead. Kept for backward compatibility.
29
+ */
30
+ attachmentAdd: Record<string, never>;
31
+ attachmentAddError: AttachmentAddErrorEvent;
32
+ };
33
+
34
+ export type ComposerRuntimeEventType = keyof ComposerRuntimeEventPayload;
35
+
36
+ export type ComposerRuntimeEventCallback<E extends ComposerRuntimeEventType> = (
37
+ payload: ComposerRuntimeEventPayload[E],
38
+ ) => void;
12
39
 
13
40
  export type DictationState = {
14
41
  readonly status: DictationAdapter.Status;
@@ -56,9 +83,14 @@ export type ComposerRuntimeCore = Readonly<{
56
83
 
57
84
  subscribe: (callback: () => void) => Unsubscribe;
58
85
 
59
- unstable_on: (
60
- event: ComposerRuntimeEventType,
61
- callback: () => void,
86
+ /**
87
+ * @deprecated This API is still under active development and might change without notice.
88
+ * For state-derivable transitions, prefer `subscribe` + `getState`. This channel is the
89
+ * escape hatch for transient occurrences not represented in state.
90
+ */
91
+ unstable_on: <E extends ComposerRuntimeEventType>(
92
+ event: E,
93
+ callback: ComposerRuntimeEventCallback<E>,
62
94
  ) => Unsubscribe;
63
95
  }>;
64
96
 
@@ -10,6 +10,7 @@ export type ThreadListItemCoreState = {
10
10
 
11
11
  readonly status: ThreadListItemStatus;
12
12
  readonly title?: string | undefined;
13
+ readonly custom?: Record<string, unknown> | undefined;
13
14
 
14
15
  readonly runtime?: ThreadRuntimeCore | undefined;
15
16
  };
@@ -69,11 +69,41 @@ export type SubmittedFeedback = {
69
69
  readonly type: "negative" | "positive";
70
70
  };
71
71
 
72
- export type ThreadRuntimeEventType =
73
- | "runStart"
74
- | "runEnd"
75
- | "initialize"
76
- | "modelContextUpdate";
72
+ export type ThreadRuntimeEventPayload = {
73
+ /**
74
+ * @deprecated State-derivable. Observe `state.isRunning` flipping to `true`
75
+ * via `subscribe` + `getState` instead. Note: this event fires at the
76
+ * transition point and may run before the next subscriber notification.
77
+ * Kept for backward compatibility.
78
+ */
79
+ runStart: Record<string, never>;
80
+ /**
81
+ * @deprecated State-derivable. Observe `state.isRunning` flipping to `false`
82
+ * via `subscribe` + `getState` instead. Note: this event fires at the
83
+ * transition point and may run before the next subscriber notification.
84
+ * Kept for backward compatibility.
85
+ */
86
+ runEnd: Record<string, never>;
87
+ /**
88
+ * @deprecated State-derivable. This event fires at the initialization
89
+ * transition immediately BEFORE the first message is added, so reading state
90
+ * inside the handler still sees an empty thread; observe `state.messages`
91
+ * becoming non-empty via a regular `subscribe` callback instead. Kept for
92
+ * backward compatibility.
93
+ */
94
+ initialize: Record<string, never>;
95
+ /**
96
+ * Truly transient. The model context lives in a provider, not in thread
97
+ * state, so this event has no state-derivable equivalent.
98
+ */
99
+ modelContextUpdate: Record<string, never>;
100
+ };
101
+
102
+ export type ThreadRuntimeEventType = keyof ThreadRuntimeEventPayload;
103
+
104
+ export type ThreadRuntimeEventCallback<E extends ThreadRuntimeEventType> = (
105
+ payload: ThreadRuntimeEventPayload[E],
106
+ ) => void;
77
107
 
78
108
  export type StartRunConfig = {
79
109
  parentId: string | null;
@@ -155,7 +185,15 @@ export type ThreadRuntimeCore = Readonly<{
155
185
 
156
186
  reset(initialMessages?: readonly ThreadMessageLike[]): void;
157
187
 
158
- unstable_on(event: ThreadRuntimeEventType, callback: () => void): Unsubscribe;
188
+ /**
189
+ * @deprecated This API is still under active development and might change without notice.
190
+ * For state-derivable transitions, prefer `subscribe` + `getState`. This channel is the
191
+ * escape hatch for transient occurrences not represented in state.
192
+ */
193
+ unstable_on<E extends ThreadRuntimeEventType>(
194
+ event: E,
195
+ callback: ThreadRuntimeEventCallback<E>,
196
+ ): Unsubscribe;
159
197
 
160
198
  /**
161
199
  * @deprecated Use importExternalState instead. This method will be removed in 0.12.0.
@@ -236,9 +236,9 @@ export class ExternalStoreThreadRuntimeCore
236
236
 
237
237
  if ((oldStore?.isRunning ?? false) !== (store.isRunning ?? false)) {
238
238
  if (store.isRunning) {
239
- this._notifyEventSubscribers("runStart");
239
+ this._notifyEventSubscribers("runStart", {});
240
240
  } else {
241
- this._notifyEventSubscribers("runEnd");
241
+ this._notifyEventSubscribers("runEnd", {});
242
242
  }
243
243
  }
244
244
 
@@ -253,7 +253,7 @@ export class LocalThreadRuntimeCore
253
253
  createdAt: new Date(),
254
254
  };
255
255
 
256
- this._notifyEventSubscribers("runStart");
256
+ this._notifyEventSubscribers("runStart", {});
257
257
 
258
258
  try {
259
259
  this._suggestions = [];
@@ -271,7 +271,7 @@ export class LocalThreadRuntimeCore
271
271
  runCallback = undefined;
272
272
  } while (shouldContinue(message, this._options.unstable_humanToolNames));
273
273
  } finally {
274
- this._notifyEventSubscribers("runEnd");
274
+ this._notifyEventSubscribers("runEnd", {});
275
275
  }
276
276
 
277
277
  this._suggestionsController = new AbortController();
@@ -7,6 +7,7 @@ export type RemoteThreadData =
7
7
  readonly externalId: undefined;
8
8
  readonly status: "new";
9
9
  readonly title: undefined;
10
+ readonly custom: undefined;
10
11
  }
11
12
  | {
12
13
  readonly id: string;
@@ -15,6 +16,7 @@ export type RemoteThreadData =
15
16
  readonly externalId: undefined;
16
17
  readonly status: "regular" | "archived";
17
18
  readonly title?: string | undefined;
19
+ readonly custom: undefined;
18
20
  }
19
21
  | {
20
22
  readonly id: string;
@@ -23,6 +25,7 @@ export type RemoteThreadData =
23
25
  readonly externalId: string | undefined;
24
26
  readonly status: "regular" | "archived";
25
27
  readonly title?: string | undefined;
28
+ readonly custom?: Record<string, unknown> | undefined;
26
29
  };
27
30
 
28
31
  export type THREAD_MAPPING_ID = string & { __brand: "THREAD_MAPPING_ID" };
@@ -1,3 +1,4 @@
1
+ import type { ComponentType, PropsWithChildren } from "react";
1
2
  import type { ThreadMessage } from "../../types/message";
2
3
  import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
3
4
  import type { AssistantStream } from "assistant-stream";
@@ -12,6 +13,7 @@ export type RemoteThreadMetadata = {
12
13
  readonly remoteId: string;
13
14
  readonly externalId?: string | undefined;
14
15
  readonly title?: string | undefined;
16
+ readonly custom?: Record<string, unknown> | undefined;
15
17
  };
16
18
 
17
19
  export type RemoteThreadListResponse = {
@@ -32,7 +34,17 @@ export type RemoteThreadListAdapter = {
32
34
  ): Promise<AssistantStream>;
33
35
  fetch(threadId: string): Promise<RemoteThreadMetadata>;
34
36
 
35
- unstable_Provider?: ((...args: any[]) => unknown) | undefined;
37
+ /**
38
+ * Optional React component wrapped around each active thread. Use it to
39
+ * inject per-thread context such as a history or attachments adapter (see
40
+ * `useCloudThreadListAdapter` for the canonical shape).
41
+ *
42
+ * The Provider must render `children` on its first commit; deferring them
43
+ * behind a loading state, a Suspense boundary, or a `useEffect`-gated render
44
+ * is unsupported and leaves thread context unavailable to downstream
45
+ * consumers. Load data inside an always-mounted child instead.
46
+ */
47
+ unstable_Provider?: ComponentType<PropsWithChildren> | undefined;
36
48
  };
37
49
 
38
50
  export type RemoteThreadListOptions = {
@@ -63,19 +63,15 @@ export const ComposerClient = resource(
63
63
  unsubscribers.push(unsubscribe);
64
64
  }
65
65
 
66
- // attachmentAddError carries the failed attachment ID
67
66
  unsubscribers.push(
68
- runtime.unstable_on("attachmentAddError", () => {
69
- const errorAttachment = runtime
70
- .getState()
71
- .attachments.findLast(
72
- (a) =>
73
- a.status.type === "incomplete" && a.status.reason === "error",
74
- );
67
+ runtime.unstable_on("attachmentAddError", (payload) => {
68
+ // payload.error omitted: raw Error is not store-serializable; use runtime.unstable_on for it.
75
69
  emit("composer.attachmentAddError", {
76
70
  threadId: threadIdRef.current,
77
71
  ...(messageIdRef && { messageId: messageIdRef.current }),
78
- ...(errorAttachment && { attachmentId: errorAttachment.id }),
72
+ ...(payload.attachmentId && { attachmentId: payload.attachmentId }),
73
+ reason: payload.reason,
74
+ message: payload.message,
79
75
  });
80
76
  }),
81
77
  );
@@ -4,6 +4,7 @@ import type { QuoteInfo } from "../../types/quote";
4
4
  import type { RunConfig } from "../../types/message";
5
5
  import type { ComposerRuntime } from "../../runtime/api/composer-runtime";
6
6
  import type {
7
+ AttachmentAddErrorReason,
7
8
  DictationState,
8
9
  SendOptions,
9
10
  } from "../../runtime/interfaces/composer-runtime-core";
@@ -91,12 +92,22 @@ export type ComposerMeta = {
91
92
  };
92
93
 
93
94
  export type ComposerEvents = {
95
+ /**
96
+ * @deprecated State-derivable. Observe composer `text` clearing via
97
+ * `useAuiState` instead. Kept for backward compatibility.
98
+ */
94
99
  "composer.send": { threadId: string; messageId?: string };
100
+ /**
101
+ * @deprecated State-derivable. Observe composer `attachments` via
102
+ * `useAuiState` instead. Kept for backward compatibility.
103
+ */
95
104
  "composer.attachmentAdd": { threadId: string; messageId?: string };
96
105
  "composer.attachmentAddError": {
97
106
  threadId: string;
98
107
  messageId?: string;
99
108
  attachmentId?: string;
109
+ reason: AttachmentAddErrorReason;
110
+ message: string;
100
111
  };
101
112
  };
102
113
 
@@ -7,6 +7,7 @@ export type ThreadListItemState = {
7
7
  readonly externalId: string | undefined;
8
8
  readonly title?: string | undefined;
9
9
  readonly status: ThreadListItemStatus;
10
+ readonly custom?: Record<string, unknown> | undefined;
10
11
  };
11
12
 
12
13
  export type ThreadListItemMethods = {
@@ -31,7 +32,17 @@ export type ThreadListItemMeta = {
31
32
  };
32
33
 
33
34
  export type ThreadListItemEvents = {
35
+ /**
36
+ * @deprecated State-derivable. Compare `s.threads.mainThreadId` against the
37
+ * item's `s.threadListItem.id` via `useAuiState` instead. Kept for backward
38
+ * compatibility.
39
+ */
34
40
  "threadListItem.switchedTo": { threadId: string };
41
+ /**
42
+ * @deprecated State-derivable. Compare `s.threads.mainThreadId` against the
43
+ * item's `s.threadListItem.id` via `useAuiState` instead. Kept for backward
44
+ * compatibility.
45
+ */
35
46
  "threadListItem.switchedAway": { threadId: string };
36
47
  };
37
48
 
@@ -130,9 +130,26 @@ export type ThreadMeta = {
130
130
  };
131
131
 
132
132
  export type ThreadEvents = {
133
+ /**
134
+ * @deprecated State-derivable. Observe `isRunning` flipping to `true` via
135
+ * `useAuiState` instead. Kept for backward compatibility.
136
+ */
133
137
  "thread.runStart": { threadId: string };
138
+ /**
139
+ * @deprecated State-derivable. Observe `isRunning` flipping to `false` via
140
+ * `useAuiState` instead. Kept for backward compatibility.
141
+ */
134
142
  "thread.runEnd": { threadId: string };
143
+ /**
144
+ * @deprecated State-derivable. This event fires before the first message is
145
+ * added; observe `messages` becoming non-empty via `useAuiState` instead of
146
+ * reading state inside this event handler. Kept for backward compatibility.
147
+ */
135
148
  "thread.initialize": { threadId: string };
149
+ /**
150
+ * Truly transient. Model context lives in a provider, not in thread state,
151
+ * so this event has no state-derivable equivalent.
152
+ */
136
153
  "thread.modelContextUpdate": { threadId: string };
137
154
  };
138
155
 
@@ -21,7 +21,10 @@ export type EventSubscribable<TEvent extends string> = {
21
21
  event: TEvent;
22
22
  binding: SubscribableWithState<
23
23
  | {
24
- unstable_on: (event: TEvent, callback: () => void) => Unsubscribe;
24
+ unstable_on: (
25
+ event: TEvent,
26
+ callback: (payload?: unknown) => void,
27
+ ) => Unsubscribe;
25
28
  }
26
29
  | undefined,
27
30
  unknown
@@ -87,7 +90,7 @@ export class BaseSubscribable {
87
90
 
88
91
  // lazy connect/disconnect: only opens upstream subscription while it has subscribers
89
92
  export abstract class BaseSubject {
90
- private _subscriptions = new Set<() => void>();
93
+ private _subscriptions = new Set<(payload?: unknown) => void>();
91
94
  private _connection: Unsubscribe | undefined;
92
95
 
93
96
  protected get isConnected() {
@@ -96,8 +99,8 @@ export abstract class BaseSubject {
96
99
 
97
100
  protected abstract _connect(): Unsubscribe;
98
101
 
99
- protected notifySubscribers() {
100
- for (const callback of this._subscriptions) callback();
102
+ protected notifySubscribers(payload?: unknown) {
103
+ for (const callback of this._subscriptions) callback(payload);
101
104
  }
102
105
 
103
106
  private _updateConnection() {
@@ -110,7 +113,7 @@ export abstract class BaseSubject {
110
113
  }
111
114
  }
112
115
 
113
- public subscribe(callback: () => void) {
116
+ public subscribe(callback: (payload?: unknown) => void) {
114
117
  this._subscriptions.add(callback);
115
118
  this._updateConnection();
116
119
 
@@ -270,8 +273,8 @@ export class EventSubscriptionSubject<
270
273
  }
271
274
 
272
275
  protected _connect(): Unsubscribe {
273
- const callback = () => {
274
- this.notifySubscribers();
276
+ const callback = (payload?: unknown) => {
277
+ this.notifySubscribers(payload);
275
278
  };
276
279
 
277
280
  let lastState = this.config.binding.getState();
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { createCore, makeAdapter } from "./remote-thread-list-test-helpers";
3
+
4
+ describe("RemoteThreadListThreadListRuntimeCore custom metadata", () => {
5
+ it("preserves custom field from list() through to threadItems", async () => {
6
+ const adapter = makeAdapter({
7
+ list: vi.fn(async () => ({
8
+ threads: [
9
+ {
10
+ status: "regular" as const,
11
+ remoteId: "thread-1",
12
+ externalId: "ext-1",
13
+ title: "Test",
14
+ custom: { workspaceId: "ws-1", createdAt: "2026-04-26" },
15
+ },
16
+ ],
17
+ })),
18
+ });
19
+
20
+ const core = createCore(adapter);
21
+ await core.getLoadThreadsPromise();
22
+
23
+ const item = core.getItemById("thread-1");
24
+ expect(item?.custom).toEqual({
25
+ workspaceId: "ws-1",
26
+ createdAt: "2026-04-26",
27
+ });
28
+ });
29
+
30
+ it("preserves custom field from fetch() through switchToThread", async () => {
31
+ const adapter = makeAdapter({
32
+ fetch: vi.fn(async (id: string) => ({
33
+ status: "regular" as const,
34
+ remoteId: id,
35
+ externalId: id,
36
+ title: "Test",
37
+ custom: { ownerId: "user-42" },
38
+ })),
39
+ });
40
+
41
+ const core = createCore(adapter);
42
+ await core.switchToThread("thread-2");
43
+
44
+ const item = core.getItemById("thread-2");
45
+ expect(item?.custom).toEqual({ ownerId: "user-42" });
46
+ });
47
+
48
+ it("leaves custom undefined when adapter omits it", async () => {
49
+ const adapter = makeAdapter({
50
+ list: vi.fn(async () => ({
51
+ threads: [
52
+ {
53
+ status: "regular" as const,
54
+ remoteId: "thread-3",
55
+ externalId: "ext-3",
56
+ title: "Test",
57
+ },
58
+ ],
59
+ })),
60
+ });
61
+
62
+ const core = createCore(adapter);
63
+ await core.getLoadThreadsPromise();
64
+
65
+ const item = core.getItemById("thread-3");
66
+ expect(item?.custom).toBeUndefined();
67
+ });
68
+
69
+ it("preserves custom across rename optimistic update", async () => {
70
+ const adapter = makeAdapter({
71
+ list: vi.fn(async () => ({
72
+ threads: [
73
+ {
74
+ status: "regular" as const,
75
+ remoteId: "thread-4",
76
+ externalId: "ext-4",
77
+ title: "Old",
78
+ custom: { tag: "important" },
79
+ },
80
+ ],
81
+ })),
82
+ });
83
+
84
+ const core = createCore(adapter);
85
+ await core.getLoadThreadsPromise();
86
+ await core.rename("thread-4", "New");
87
+
88
+ const item = core.getItemById("thread-4");
89
+ expect(item?.title).toBe("New");
90
+ expect(item?.custom).toEqual({ tag: "important" });
91
+ });
92
+
93
+ it("preserves custom across archive and unarchive optimistic updates", async () => {
94
+ const adapter = makeAdapter({
95
+ list: vi.fn(async () => ({
96
+ threads: [
97
+ {
98
+ status: "regular" as const,
99
+ remoteId: "thread-5",
100
+ externalId: "ext-5",
101
+ title: "Test",
102
+ custom: { workspaceId: "ws-1" },
103
+ },
104
+ ],
105
+ })),
106
+ });
107
+
108
+ const core = createCore(adapter);
109
+ await core.getLoadThreadsPromise();
110
+
111
+ await core.archive("thread-5");
112
+ expect(core.getItemById("thread-5")?.status).toBe("archived");
113
+ expect(core.getItemById("thread-5")?.custom).toEqual({
114
+ workspaceId: "ws-1",
115
+ });
116
+
117
+ await core.unarchive("thread-5");
118
+ expect(core.getItemById("thread-5")?.status).toBe("regular");
119
+ expect(core.getItemById("thread-5")?.custom).toEqual({
120
+ workspaceId: "ws-1",
121
+ });
122
+ });
123
+ });