@assistant-ui/core 0.2.7 → 0.2.8

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 (149) hide show
  1. package/dist/adapters/attachment.d.ts.map +1 -1
  2. package/dist/adapters/speech.d.ts.map +1 -1
  3. package/dist/adapters/speech.js.map +1 -1
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +2 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/internal/duplicate-detection.d.ts.map +1 -1
  8. package/dist/internal.d.ts +2 -2
  9. package/dist/internal.js +2 -2
  10. package/dist/model-context/frame/host.d.ts.map +1 -1
  11. package/dist/model-context/frame/host.js.map +1 -1
  12. package/dist/model-context/frame/provider.d.ts.map +1 -1
  13. package/dist/model-context/frame/provider.js.map +1 -1
  14. package/dist/model-context/registry.d.ts.map +1 -1
  15. package/dist/model-context/tool.d.ts.map +1 -1
  16. package/dist/react/client/Interactables.js.map +1 -1
  17. package/dist/react/client/Tools.d.ts.map +1 -1
  18. package/dist/react/client/Tools.js +26 -15
  19. package/dist/react/client/Tools.js.map +1 -1
  20. package/dist/react/index.d.ts +4 -3
  21. package/dist/react/index.js +2 -1
  22. package/dist/react/model-context/toolbox.d.ts +29 -2
  23. package/dist/react/model-context/toolbox.d.ts.map +1 -1
  24. package/dist/react/model-context/toolbox.js +18 -0
  25. package/dist/react/model-context/toolbox.js.map +1 -0
  26. package/dist/react/model-context/useAssistantTool.d.ts.map +1 -1
  27. package/dist/react/model-context/useAssistantTool.js +6 -3
  28. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  29. package/dist/react/model-context/useAssistantToolUI.d.ts +6 -0
  30. package/dist/react/model-context/useAssistantToolUI.d.ts.map +1 -1
  31. package/dist/react/model-context/useAssistantToolUI.js +4 -2
  32. package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
  33. package/dist/react/model-context/useInlineRender.js.map +1 -1
  34. package/dist/react/primitives/message/MessageGroupedParts.d.ts +49 -7
  35. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  36. package/dist/react/primitives/message/MessageGroupedParts.js +28 -3
  37. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  38. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  39. package/dist/react/primitives/message/MessageParts.js +2 -7
  40. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  41. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  42. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  43. package/dist/react/runtimes/RuntimeAdapterProvider.d.ts.map +1 -1
  44. package/dist/react/runtimes/RuntimeAdapterProvider.js +6 -5
  45. package/dist/react/runtimes/RuntimeAdapterProvider.js.map +1 -1
  46. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  47. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  48. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  49. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  50. package/dist/react/runtimes/external-message-converter.js +1 -0
  51. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  52. package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts +7 -0
  53. package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts.map +1 -0
  54. package/dist/react/runtimes/useExternalStoreSharedOptions.js +21 -0
  55. package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -0
  56. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  57. package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
  58. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  59. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  60. package/dist/react/types/scopes/tools.d.ts +19 -2
  61. package/dist/react/types/scopes/tools.d.ts.map +1 -1
  62. package/dist/react/utils/groupParts.d.ts +32 -11
  63. package/dist/react/utils/groupParts.d.ts.map +1 -1
  64. package/dist/react/utils/groupParts.js +13 -6
  65. package/dist/react/utils/groupParts.js.map +1 -1
  66. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  67. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  68. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  69. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  70. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  71. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  72. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  73. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  74. package/dist/runtime/base/base-assistant-runtime-core.d.ts.map +1 -1
  75. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  76. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  77. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  78. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  79. package/dist/runtime/utils/message-repository.d.ts +9 -1
  80. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  81. package/dist/runtime/utils/message-repository.js +34 -14
  82. package/dist/runtime/utils/message-repository.js.map +1 -1
  83. package/dist/runtime/utils/thread-message-like.d.ts +1 -0
  84. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  85. package/dist/runtime/utils/thread-message-like.js +2 -1
  86. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  87. package/dist/runtimes/external-store/external-store-shared-options.d.ts +8 -0
  88. package/dist/runtimes/external-store/external-store-shared-options.d.ts.map +1 -0
  89. package/dist/runtimes/external-store/external-store-shared-options.js +11 -0
  90. package/dist/runtimes/external-store/external-store-shared-options.js.map +1 -0
  91. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  92. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +0 -2
  93. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  94. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +12 -23
  95. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  96. package/dist/runtimes/external-store/thread-message-converter.d.ts.map +1 -1
  97. package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
  98. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  99. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  100. package/dist/runtimes/remote-thread-list/adapter/in-memory.d.ts.map +1 -1
  101. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  102. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  103. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  104. package/dist/subscribable/subscribable.d.ts.map +1 -1
  105. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  106. package/dist/types/message.d.ts +6 -0
  107. package/dist/types/message.d.ts.map +1 -1
  108. package/dist/types/message.js.map +1 -1
  109. package/dist/utils/composite-context-provider.d.ts.map +1 -1
  110. package/dist/utils/id.d.ts +1 -3
  111. package/dist/utils/id.d.ts.map +1 -1
  112. package/dist/utils/id.js +1 -4
  113. package/dist/utils/id.js.map +1 -1
  114. package/package.json +10 -10
  115. package/src/adapters/speech.ts +0 -1
  116. package/src/index.ts +2 -0
  117. package/src/internal.ts +0 -2
  118. package/src/model-context/frame/host.ts +0 -1
  119. package/src/model-context/frame/provider.ts +0 -1
  120. package/src/react/client/Interactables.ts +0 -1
  121. package/src/react/client/Tools.ts +50 -25
  122. package/src/react/index.ts +8 -2
  123. package/src/react/model-context/toolbox.ts +46 -1
  124. package/src/react/model-context/useAssistantTool.ts +8 -3
  125. package/src/react/model-context/useAssistantToolUI.ts +9 -2
  126. package/src/react/model-context/useInlineRender.ts +0 -1
  127. package/src/react/primitives/message/MessageGroupedParts.tsx +101 -12
  128. package/src/react/primitives/message/MessageParts.tsx +4 -7
  129. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +0 -3
  130. package/src/react/runtimes/RuntimeAdapterProvider.tsx +12 -7
  131. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +0 -3
  132. package/src/react/runtimes/external-message-converter.ts +4 -0
  133. package/src/react/runtimes/useExternalStoreSharedOptions.ts +23 -0
  134. package/src/react/runtimes/useLocalRuntime.ts +0 -10
  135. package/src/react/runtimes/useRemoteThreadListRuntime.ts +0 -6
  136. package/src/react/types/scopes/tools.ts +20 -1
  137. package/src/react/utils/groupParts.ts +49 -18
  138. package/src/runtime/utils/message-repository.ts +57 -16
  139. package/src/runtime/utils/thread-message-like.ts +2 -0
  140. package/src/runtimes/external-store/external-store-shared-options.ts +18 -0
  141. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +18 -33
  142. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +0 -1
  143. package/src/tests/MessageRepository.test.ts +83 -52
  144. package/src/tests/OptimisticState-list-race.test.ts +0 -4
  145. package/src/tests/external-store-thread-runtime-core.test.ts +105 -73
  146. package/src/tests/groupParts.test.ts +70 -0
  147. package/src/tests/remote-thread-list-isLoading.test.ts +0 -5
  148. package/src/types/message.ts +6 -0
  149. package/src/utils/id.ts +0 -4
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
2
2
  import { ExternalStoreThreadRuntimeCore } from "../runtimes/external-store/external-store-thread-runtime-core";
3
3
  import type { ExternalStoreAdapter } from "../runtimes/external-store/external-store-adapter";
4
4
  import type { ModelContextProvider } from "../model-context/types";
5
+ import type { ThreadMessageLike } from "../runtime/utils/thread-message-like";
5
6
 
6
7
  const mockContextProvider: ModelContextProvider = {
7
8
  getModelContext: () => ({}),
@@ -151,117 +152,148 @@ describe("ExternalStoreThreadRuntimeCore - state reference stability", () => {
151
152
  expect(runtime.capabilities).toBe(capsBefore);
152
153
  });
153
154
  });
155
+ describe("ExternalStoreThreadRuntimeCore - optimistic message reconciliation", () => {
156
+ type Raw = {
157
+ id: string;
158
+ role: "user" | "assistant";
159
+ text: string;
160
+ optimistic?: boolean;
161
+ };
162
+
163
+ const convertMessage = (m: Raw): ThreadMessageLike => ({
164
+ id: m.id,
165
+ role: m.role,
166
+ content: [{ type: "text", text: m.text }],
167
+ ...(m.optimistic && { metadata: { isOptimistic: true } }),
168
+ });
154
169
 
155
- describe("ExternalStoreThreadRuntimeCore - messages reconciliation", () => {
156
- const user = { id: "u", role: "user" as const, content: [] };
157
-
158
- it("drops ids that disappear between syncs (same length, swapped assistant id)", () => {
159
- const a1 = { id: "a1", role: "assistant" as const, content: [] };
160
- const a2 = { id: "a2", role: "assistant" as const, content: [] };
170
+ const childrenOf = (
171
+ runtime: ExternalStoreThreadRuntimeCore,
172
+ parentId: string,
173
+ ) =>
174
+ runtime
175
+ .export()
176
+ .messages.filter((m) => m.parentId === parentId)
177
+ .map((m) => m.message.id);
161
178
 
179
+ it("drops the orphaned placeholder when an optimistic id is swapped mid-run", () => {
180
+ const u: Raw = { id: "u", role: "user", text: "hi" };
162
181
  const runtime = new ExternalStoreThreadRuntimeCore(
163
182
  mockContextProvider,
164
- makeStore({ messages: [user, a1] }),
183
+ makeStore({
184
+ messages: [
185
+ u,
186
+ { id: "client_id", role: "assistant", text: "", optimistic: true },
187
+ ],
188
+ convertMessage,
189
+ isRunning: true,
190
+ }),
165
191
  );
166
192
 
167
- runtime.__internal_setAdapter(makeStore({ messages: [user, a2] }));
193
+ // AI SDK v6 swaps the client-generated id for the server-provided one.
194
+ runtime.__internal_setAdapter(
195
+ makeStore({
196
+ messages: [
197
+ u,
198
+ {
199
+ id: "server_id",
200
+ role: "assistant",
201
+ text: "hello",
202
+ optimistic: true,
203
+ },
204
+ ],
205
+ convertMessage,
206
+ isRunning: true,
207
+ }),
208
+ );
168
209
 
169
- const exported = runtime.export();
170
- expect(exported.messages.map((m) => m.message.id)).toEqual(["u", "a2"]);
171
- const userChildren = exported.messages
172
- .filter((m) => m.parentId === "u")
173
- .map((m) => m.message.id);
174
- expect(userChildren).toEqual(["a2"]);
210
+ // No phantom sibling in the live tree (what BranchPicker reads): the user
211
+ // message has a single child. export() omits the still-optimistic
212
+ // streaming message, so assert against the live getBranches instead.
213
+ expect(runtime.getBranches("server_id")).toEqual(["server_id"]);
175
214
  });
176
215
 
177
- it("keeps prior ids when they remain in the new sync", () => {
178
- const a = { id: "a", role: "assistant" as const, content: [] };
179
-
216
+ it("clears the optimistic flag once the run settles", () => {
217
+ const u: Raw = { id: "u", role: "user", text: "hi" };
180
218
  const runtime = new ExternalStoreThreadRuntimeCore(
181
219
  mockContextProvider,
182
- makeStore({ messages: [user, a] }),
220
+ makeStore({
221
+ messages: [
222
+ u,
223
+ { id: "a", role: "assistant", text: "...", optimistic: true },
224
+ ],
225
+ convertMessage,
226
+ isRunning: true,
227
+ }),
183
228
  );
184
229
 
185
- runtime.__internal_setAdapter(makeStore({ messages: [user, a] }));
230
+ runtime.__internal_setAdapter(
231
+ makeStore({
232
+ messages: [u, { id: "a", role: "assistant", text: "done" }],
233
+ convertMessage,
234
+ isRunning: false,
235
+ }),
236
+ );
186
237
 
187
- expect(runtime.export().messages.map((m) => m.message.id)).toEqual([
188
- "u",
189
- "a",
190
- ]);
238
+ const settled = runtime.export().messages.find((m) => m.message.id === "a");
239
+ expect(settled?.message.metadata.isOptimistic).toBeFalsy();
240
+ expect(childrenOf(runtime, "u")).toEqual(["a"]);
191
241
  });
192
242
 
193
- it("removes trailing messages dropped from the new sync", () => {
194
- const a = { id: "a", role: "assistant" as const, content: [] };
195
- const u2 = { id: "u2", role: "user" as const, content: [] };
196
-
243
+ it("removes the runtime placeholder once the store provides the assistant message", () => {
244
+ const u: Raw = { id: "u", role: "user", text: "hi" };
197
245
  const runtime = new ExternalStoreThreadRuntimeCore(
198
246
  mockContextProvider,
199
- makeStore({ messages: [user, a, u2] }),
247
+ makeStore({ messages: [u], convertMessage, isRunning: true }),
200
248
  );
201
249
 
202
- runtime.__internal_setAdapter(makeStore({ messages: [user, a] }));
250
+ // Running with a trailing user message: a placeholder is appended to the
251
+ // live tree. export() omits optimistic messages, so inspect the live
252
+ // messages (which include the placeholder on the head path). It's a plain
253
+ // assistant message flagged optimistic (no special id scheme).
254
+ const live = runtime.messages;
255
+ expect(live).toHaveLength(2);
256
+ expect(live[1]!.role).toBe("assistant");
257
+ expect(live[1]!.metadata.isOptimistic).toBe(true);
258
+
259
+ // The store now yields the real assistant message; the placeholder (whose
260
+ // synthetic id never appears in the snapshot) must be gone, leaving a
261
+ // single child under the user message.
262
+ runtime.__internal_setAdapter(
263
+ makeStore({
264
+ messages: [u, { id: "a", role: "assistant", text: "done" }],
265
+ convertMessage,
266
+ isRunning: false,
267
+ }),
268
+ );
203
269
 
204
270
  expect(runtime.export().messages.map((m) => m.message.id)).toEqual([
205
271
  "u",
206
272
  "a",
207
273
  ]);
274
+ expect(runtime.getBranches("a")).toEqual(["a"]);
208
275
  });
209
276
 
210
- it("does not crash on the next sync after cancelRun removes a leaf user", () => {
211
- const userWithText = {
212
- id: "u",
213
- role: "user" as const,
214
- content: [{ type: "text" as const, text: "hi" }],
215
- };
216
-
277
+ it("keeps real sibling branches that were never flagged optimistic", () => {
278
+ const u: Raw = { id: "u", role: "user", text: "hi" };
217
279
  const runtime = new ExternalStoreThreadRuntimeCore(
218
280
  mockContextProvider,
219
281
  makeStore({
220
- messages: [userWithText],
221
- onCancel: vi.fn(),
222
- isRunning: true,
223
- }),
224
- );
225
-
226
- runtime.cancelRun();
227
-
228
- expect(() => {
229
- runtime.__internal_setAdapter(makeStore({ messages: [] }));
230
- }).not.toThrow();
231
- });
232
-
233
- it("drops phantom sibling when convertMessage swaps the assistant id", () => {
234
- type Raw = { id: string; role: "user" | "assistant"; text: string };
235
- const rawU: Raw = { id: "u", role: "user", text: "hi" };
236
- const rawA1: Raw = { id: "client_id", role: "assistant", text: "" };
237
- const rawA2: Raw = { id: "server_id", role: "assistant", text: "" };
238
-
239
- const convertMessage = (m: Raw) => ({
240
- id: m.id,
241
- role: m.role,
242
- content: [{ type: "text" as const, text: m.text }],
243
- });
244
-
245
- const runtime = new ExternalStoreThreadRuntimeCore(
246
- mockContextProvider,
247
- makeStore({
248
- messages: [rawU, rawA1] as any,
249
- convertMessage: convertMessage as any,
282
+ messages: [u, { id: "a1", role: "assistant", text: "first" }],
283
+ convertMessage,
250
284
  }),
251
285
  );
252
286
 
287
+ // Simulates onEdit/onReload producing a new branch under the same parent;
288
+ // the prior branch must survive (regression guard for #4131).
253
289
  runtime.__internal_setAdapter(
254
290
  makeStore({
255
- messages: [rawU, rawA2] as any,
256
- convertMessage: convertMessage as any,
291
+ messages: [u, { id: "a2", role: "assistant", text: "second" }],
292
+ convertMessage,
257
293
  }),
258
294
  );
259
295
 
260
- const userChildren = runtime
261
- .export()
262
- .messages.filter((m) => m.parentId === "u")
263
- .map((m) => m.message.id);
264
- expect(userChildren).toEqual(["server_id"]);
296
+ expect(childrenOf(runtime, "u")).toEqual(["a1", "a2"]);
265
297
  });
266
298
  });
267
299
 
@@ -160,6 +160,76 @@ describe("groupPartByType", () => {
160
160
  expect(fn(notMcp)).toEqual(["group-tool"]);
161
161
  });
162
162
 
163
+ const standaloneContext = (...names: string[]) => ({
164
+ toolUIs: Object.fromEntries(
165
+ names.map((name) => [name, [{ render: () => null, standalone: true }]]),
166
+ ),
167
+ });
168
+
169
+ it("routes context-standalone tool calls through the 'standalone-tool-call' entry", () => {
170
+ const fn = groupPartByType({
171
+ "tool-call": ["group-tool"],
172
+ "standalone-tool-call": [],
173
+ });
174
+ const standalone = part({
175
+ type: "tool-call",
176
+ toolName: "ask_user",
177
+ } as Partial<PartState>);
178
+ const regular = part({
179
+ type: "tool-call",
180
+ toolName: "search",
181
+ } as Partial<PartState>);
182
+ expect(fn(standalone, standaloneContext("ask_user"))).toEqual([]);
183
+ expect(fn(regular, standaloneContext("ask_user"))).toEqual(["group-tool"]);
184
+ // No context → not standalone, falls through to "tool-call".
185
+ expect(fn(standalone)).toEqual(["group-tool"]);
186
+ // Registered but not standalone → also falls through to "tool-call".
187
+ const inlineCtx = {
188
+ toolUIs: { ask_user: [{ render: () => null, standalone: false }] },
189
+ };
190
+ expect(fn(standalone, inlineCtx)).toEqual(["group-tool"]);
191
+ });
192
+
193
+ it("routes MCP-app parts through 'standalone-tool-call' from the part alone", () => {
194
+ const fn = groupPartByType({
195
+ "tool-call": ["group-tool"],
196
+ "standalone-tool-call": [],
197
+ });
198
+ const mcpApp = part({
199
+ type: "tool-call",
200
+ toolName: "render",
201
+ mcp: { app: { resourceUri: "ui://my-app" } },
202
+ } as Partial<PartState>);
203
+ expect(fn(mcpApp)).toEqual([]);
204
+ });
205
+
206
+ it("routes MCP-app parts through the deprecated 'mcp-app' entry", () => {
207
+ const fn = groupPartByType({
208
+ "tool-call": ["group-tool"],
209
+ "mcp-app": ["group-mcp"],
210
+ });
211
+ const mcpApp = part({
212
+ type: "tool-call",
213
+ toolName: "render",
214
+ mcp: { app: { resourceUri: "ui://my-app" } },
215
+ } as Partial<PartState>);
216
+ expect(fn(mcpApp)).toEqual(["group-mcp"]);
217
+ });
218
+
219
+ it("prefers 'standalone-tool-call' over the deprecated 'mcp-app' entry", () => {
220
+ const fn = groupPartByType({
221
+ "tool-call": ["group-tool"],
222
+ "standalone-tool-call": ["group-standalone"],
223
+ "mcp-app": ["group-mcp"],
224
+ });
225
+ const mcpApp = part({
226
+ type: "tool-call",
227
+ toolName: "render",
228
+ mcp: { app: { resourceUri: "ui://x" } },
229
+ } as Partial<PartState>);
230
+ expect(fn(mcpApp)).toEqual(["group-standalone"]);
231
+ });
232
+
163
233
  it("tags the function with a GROUPBY_MEMO_KEY fingerprint", () => {
164
234
  const fn = groupPartByType({ reasoning: ["group-r"] });
165
235
  const memoKey = (fn as unknown as { [GROUPBY_MEMO_KEY]: string })[
@@ -86,7 +86,6 @@ describe("RemoteThreadList isLoading lifecycle", () => {
86
86
  state.optimisticUpdate({
87
87
  execute: () => d.promise,
88
88
  loading: (s) => ({ ...s, isLoading: true }),
89
- // biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
90
89
  then: applyListResult,
91
90
  });
92
91
 
@@ -100,7 +99,6 @@ describe("RemoteThreadList isLoading lifecycle", () => {
100
99
  const promise = state.optimisticUpdate({
101
100
  execute: () => d.promise,
102
101
  loading: (s) => ({ ...s, isLoading: true }),
103
- // biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
104
102
  then: applyListResult,
105
103
  });
106
104
 
@@ -117,7 +115,6 @@ describe("RemoteThreadList isLoading lifecycle", () => {
117
115
  const promise = state.optimisticUpdate({
118
116
  execute: () => d.promise,
119
117
  loading: (s) => ({ ...s, isLoading: true }),
120
- // biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
121
118
  then: applyListResult,
122
119
  });
123
120
 
@@ -142,7 +139,6 @@ describe("RemoteThreadList isLoading lifecycle", () => {
142
139
  const promise = state.optimisticUpdate({
143
140
  execute: () => d.promise,
144
141
  loading: (s) => ({ ...s, isLoading: true }),
145
- // biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
146
142
  then: applyListResult,
147
143
  });
148
144
 
@@ -166,7 +162,6 @@ describe("RemoteThreadList isLoading error path", () => {
166
162
  .optimisticUpdate({
167
163
  execute: () => d.promise,
168
164
  loading: (s) => ({ ...s, isLoading: true }),
169
- // biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
170
165
  then: applyListResult,
171
166
  })
172
167
  .catch(() => {
@@ -314,6 +314,11 @@ export type ThreadAssistantMessage = MessageCommonProps & {
314
314
  readonly steps: readonly ThreadStep[];
315
315
  readonly submittedFeedback?: { readonly type: "positive" | "negative" };
316
316
  readonly timing?: MessageTiming;
317
+ /**
318
+ * Marks a client-side optimistic placeholder. Such messages are evicted
319
+ * once off the head branch and are never persisted.
320
+ */
321
+ readonly isOptimistic?: boolean;
317
322
  readonly custom: Record<string, unknown>;
318
323
  };
319
324
  };
@@ -327,6 +332,7 @@ type BaseThreadMessage = {
327
332
  readonly steps?: readonly ThreadStep[];
328
333
  readonly submittedFeedback?: { readonly type: "positive" | "negative" };
329
334
  readonly timing?: MessageTiming;
335
+ readonly isOptimistic?: boolean;
330
336
  readonly custom: Record<string, unknown>;
331
337
  };
332
338
  readonly attachments?: ThreadUserMessage["attachments"];
package/src/utils/id.ts CHANGED
@@ -5,10 +5,6 @@ export const generateId = customAlphabet(
5
5
  7,
6
6
  );
7
7
 
8
- const optimisticPrefix = "__optimistic__";
9
- export const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
10
- export const isOptimisticId = (id: string) => id.startsWith(optimisticPrefix);
11
-
12
8
  const errorPrefix = "__error__";
13
9
  export const generateErrorMessageId = () => `${errorPrefix}${generateId()}`;
14
10
  export const isErrorMessageId = (id: string) => id.startsWith(errorPrefix);