@convex-dev/agent 0.2.3-alpha.0 → 0.2.4-alpha.0

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 (59) hide show
  1. package/dist/client/definePlaygroundAPI.d.ts +59 -5
  2. package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
  3. package/dist/client/files.d.ts +1 -1
  4. package/dist/client/files.d.ts.map +1 -1
  5. package/dist/client/files.js.map +1 -1
  6. package/dist/client/index.d.ts +46 -4
  7. package/dist/client/index.d.ts.map +1 -1
  8. package/dist/client/index.js +14 -13
  9. package/dist/client/index.js.map +1 -1
  10. package/dist/client/messages.d.ts +14 -2
  11. package/dist/client/messages.d.ts.map +1 -1
  12. package/dist/client/search.d.ts +14 -2
  13. package/dist/client/search.d.ts.map +1 -1
  14. package/dist/client/search.js +2 -4
  15. package/dist/client/search.js.map +1 -1
  16. package/dist/client/streaming.d.ts +132 -23
  17. package/dist/client/streaming.d.ts.map +1 -1
  18. package/dist/component/_generated/api.d.ts +189 -0
  19. package/dist/component/messages.d.ts +149 -20
  20. package/dist/component/messages.d.ts.map +1 -1
  21. package/dist/component/messages.js +4 -4
  22. package/dist/component/messages.js.map +1 -1
  23. package/dist/component/schema.d.ts +342 -63
  24. package/dist/component/schema.d.ts.map +1 -1
  25. package/dist/mapping.d.ts +4 -4
  26. package/dist/mapping.d.ts.map +1 -1
  27. package/dist/mapping.js +67 -36
  28. package/dist/mapping.js.map +1 -1
  29. package/dist/react/deltas.d.ts +3 -3
  30. package/dist/react/deltas.d.ts.map +1 -1
  31. package/dist/react/deltas.js +302 -30
  32. package/dist/react/deltas.js.map +1 -1
  33. package/dist/react/toUIMessages.d.ts.map +1 -1
  34. package/dist/react/toUIMessages.js +289 -180
  35. package/dist/react/toUIMessages.js.map +1 -1
  36. package/dist/react/useSmoothText.d.ts.map +1 -1
  37. package/dist/react/useSmoothText.js +15 -6
  38. package/dist/react/useSmoothText.js.map +1 -1
  39. package/dist/shared.d.ts +4 -0
  40. package/dist/shared.d.ts.map +1 -1
  41. package/dist/shared.js +8 -0
  42. package/dist/shared.js.map +1 -1
  43. package/dist/validators.d.ts +662 -104
  44. package/dist/validators.d.ts.map +1 -1
  45. package/dist/validators.js +8 -0
  46. package/dist/validators.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/client/files.ts +4 -4
  49. package/src/client/index.ts +21 -20
  50. package/src/client/search.ts +2 -6
  51. package/src/component/_generated/api.d.ts +189 -0
  52. package/src/component/messages.ts +4 -3
  53. package/src/mapping.ts +99 -51
  54. package/src/react/deltas.ts +368 -48
  55. package/src/react/toUIMessages.test.ts +186 -0
  56. package/src/react/toUIMessages.ts +366 -192
  57. package/src/react/useSmoothText.ts +25 -13
  58. package/src/shared.ts +14 -0
  59. package/src/validators.ts +8 -0
@@ -13,6 +13,7 @@ import type {
13
13
  import { extractText, type MessageDoc } from "../client/index.js";
14
14
  import { deserializeMessage, toUIFilePart } from "../mapping.js";
15
15
  import type { MessageStatus } from "../validators.js";
16
+ import { sorted } from "../shared.js";
16
17
 
17
18
  export type UIMessage<
18
19
  METADATA = unknown,
@@ -35,216 +36,389 @@ export function toUIMessages<
35
36
  >(
36
37
  messages: (MessageDoc & { streaming?: boolean })[],
37
38
  ): UIMessage<METADATA, DATA_PARTS, TOOLS>[] {
39
+ // Group assistant and tool messages together
40
+ const assistantGroups = groupAssistantMessages(messages);
41
+
38
42
  const uiMessages: UIMessage<METADATA, DATA_PARTS, TOOLS>[] = [];
39
- let assistantMessage: UIMessage<METADATA, DATA_PARTS, TOOLS> | undefined;
40
- for (const message of messages) {
43
+ for (const group of assistantGroups) {
44
+ if (group.role === "system") {
45
+ uiMessages.push(createSystemUIMessage(group.message));
46
+ } else if (group.role === "user") {
47
+ uiMessages.push(createUserUIMessage(group.message));
48
+ } else {
49
+ // Assistant/tool group
50
+ uiMessages.push(createAssistantUIMessage(group.messages));
51
+ }
52
+ }
53
+
54
+ return uiMessages;
55
+ }
56
+
57
+ type Group =
58
+ | {
59
+ role: "user";
60
+ message: MessageDoc & { streaming?: boolean };
61
+ }
62
+ | {
63
+ role: "system";
64
+ message: MessageDoc & { streaming?: boolean };
65
+ }
66
+ | {
67
+ role: "assistant";
68
+ messages: (MessageDoc & { streaming?: boolean })[];
69
+ };
70
+
71
+ function groupAssistantMessages(
72
+ messages: (MessageDoc & { streaming?: boolean })[],
73
+ ): Group[] {
74
+ const groups: Group[] = [];
75
+
76
+ // Sort messages by order and stepOrder first to handle out-of-order arrivals
77
+ const sortedMessages = sorted(messages);
78
+
79
+ let currentAssistantGroup: (MessageDoc & { streaming?: boolean })[] = [];
80
+ let currentOrder: number | undefined;
81
+
82
+ for (const message of sortedMessages) {
41
83
  const coreMessage = message.message && deserializeMessage(message.message);
42
- const text =
43
- message.text || ((message.message && extractText(message.message)) ?? "");
44
- const content = coreMessage?.content;
84
+ if (!coreMessage) continue;
85
+
86
+ if (coreMessage.role === "user" || coreMessage.role === "system") {
87
+ // Finish any current assistant group
88
+ if (currentAssistantGroup.length > 0) {
89
+ groups.push({
90
+ role: "assistant",
91
+ messages: currentAssistantGroup,
92
+ });
93
+ currentAssistantGroup = [];
94
+ currentOrder = undefined;
95
+ }
96
+ // Add singleton group
97
+ groups.push({
98
+ role: coreMessage.role,
99
+ message,
100
+ });
101
+ } else {
102
+ // Assistant or tool message
103
+
104
+ // Start new group if order changes or this is the first assistant/tool message
105
+ if (currentOrder !== undefined && message.order !== currentOrder) {
106
+ if (currentAssistantGroup.length > 0) {
107
+ groups.push({
108
+ role: "assistant",
109
+ messages: currentAssistantGroup,
110
+ });
111
+ currentAssistantGroup = [];
112
+ }
113
+ }
114
+
115
+ currentOrder = message.order;
116
+ currentAssistantGroup.push(message);
117
+
118
+ // End group if this is an assistant message without tool calls
119
+ // But only if we're processing messages in order (which we are now due to sorting)
120
+ if (coreMessage.role === "assistant" && !message.tool) {
121
+ groups.push({
122
+ role: "assistant",
123
+ messages: currentAssistantGroup,
124
+ });
125
+ currentAssistantGroup = [];
126
+ currentOrder = undefined;
127
+ }
128
+ }
129
+ }
130
+
131
+ // Add any remaining assistant group
132
+ if (currentAssistantGroup.length > 0) {
133
+ groups.push({
134
+ role: "assistant",
135
+ messages: currentAssistantGroup,
136
+ });
137
+ }
138
+
139
+ return groups;
140
+ }
141
+
142
+ function createSystemUIMessage<
143
+ METADATA = unknown,
144
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
145
+ TOOLS extends UITools = UITools,
146
+ >(
147
+ message: MessageDoc & { streaming?: boolean },
148
+ ): UIMessage<METADATA, DATA_PARTS, TOOLS> {
149
+ const text = extractTextFromMessageDoc(message);
150
+ const partCommon = {
151
+ state: message.streaming ? ("streaming" as const) : ("done" as const),
152
+ ...(message.providerMetadata
153
+ ? { providerMetadata: message.providerMetadata }
154
+ : {}),
155
+ };
156
+
157
+ return {
158
+ id: message._id,
159
+ _creationTime: message._creationTime,
160
+ order: message.order,
161
+ stepOrder: message.stepOrder,
162
+ status: message.streaming ? ("streaming" as const) : message.status,
163
+ key: `${message.threadId}-${message.order}-${message.stepOrder}`,
164
+ text,
165
+ role: "system",
166
+ agentName: message.agentName,
167
+ parts: [{ type: "text", text, ...partCommon } satisfies TextUIPart],
168
+ };
169
+ }
170
+
171
+ function extractTextFromMessageDoc(message: MessageDoc): string {
172
+ return (
173
+ message.text || (message.message && extractText(message.message)) || ""
174
+ );
175
+ }
176
+
177
+ function createUserUIMessage<
178
+ METADATA = unknown,
179
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
180
+ TOOLS extends UITools = UITools,
181
+ >(
182
+ message: MessageDoc & { streaming?: boolean },
183
+ ): UIMessage<METADATA, DATA_PARTS, TOOLS> {
184
+ const text = extractTextFromMessageDoc(message);
185
+ const coreMessage = deserializeMessage(message.message!);
186
+ const content = coreMessage.content;
187
+ const nonStringContent =
188
+ content && typeof content !== "string" ? content : [];
189
+
190
+ const partCommon = {
191
+ state: message.streaming ? ("streaming" as const) : ("done" as const),
192
+ ...(message.providerMetadata
193
+ ? { providerMetadata: message.providerMetadata }
194
+ : {}),
195
+ };
196
+
197
+ const parts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
198
+ if (text && !nonStringContent.length) {
199
+ parts.push({ type: "text", text });
200
+ }
201
+ for (const contentPart of nonStringContent) {
202
+ switch (contentPart.type) {
203
+ case "text":
204
+ parts.push({ type: "text", text: contentPart.text, ...partCommon });
205
+ break;
206
+ case "file":
207
+ case "image":
208
+ parts.push(toUIFilePart(contentPart));
209
+ break;
210
+ default:
211
+ console.warn("Unknown content part type for user", contentPart);
212
+ break;
213
+ }
214
+ }
215
+
216
+ return {
217
+ id: message._id,
218
+ _creationTime: message._creationTime,
219
+ order: message.order,
220
+ stepOrder: message.stepOrder,
221
+ status: message.streaming ? ("streaming" as const) : message.status,
222
+ key: `${message.threadId}-${message.order}-${message.stepOrder}`,
223
+ text,
224
+ role: "user",
225
+ parts,
226
+ };
227
+ }
228
+
229
+ function createAssistantUIMessage<
230
+ METADATA = unknown,
231
+ DATA_PARTS extends UIDataTypes = UIDataTypes,
232
+ TOOLS extends UITools = UITools,
233
+ >(
234
+ groupUnordered: (MessageDoc & { streaming?: boolean })[],
235
+ ): UIMessage<METADATA, DATA_PARTS, TOOLS> {
236
+ const group = sorted(groupUnordered);
237
+ const firstMessage = group[0];
238
+
239
+ // Use first message for special fields
240
+ const common = {
241
+ id: firstMessage._id,
242
+ _creationTime: firstMessage._creationTime,
243
+ order: firstMessage.order,
244
+ stepOrder: firstMessage.stepOrder,
245
+ key: `${firstMessage.threadId}-${firstMessage.order}-${firstMessage.stepOrder}`,
246
+ agentName: firstMessage.agentName,
247
+ };
248
+
249
+ // Concatenate text from all messages in group
250
+ const allText = group
251
+ .map((msg) => msg.text || ((msg.message && extractText(msg.message)) ?? ""))
252
+ .filter(Boolean)
253
+ .join("");
254
+
255
+ // Get status from last message
256
+ const lastMessage = group[group.length - 1];
257
+ const status = lastMessage.streaming
258
+ ? ("streaming" as const)
259
+ : lastMessage.status;
260
+
261
+ // Collect all parts from all messages
262
+ const allParts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
263
+
264
+ for (const message of group) {
265
+ const coreMessage = message.message && deserializeMessage(message.message);
266
+ if (!coreMessage) continue;
267
+
268
+ const content = coreMessage.content;
45
269
  const nonStringContent =
46
270
  content && typeof content !== "string" ? content : [];
47
- if (!coreMessage) continue;
48
- const common = {
49
- id: message._id,
50
- _creationTime: message._creationTime,
51
- order: message.order,
52
- stepOrder: message.stepOrder,
53
- status: message.streaming ? ("streaming" as const) : message.status,
54
- key: `${message.threadId}-${message.order}-${message.stepOrder}`,
55
- text,
56
- } satisfies Partial<UIMessage<METADATA, DATA_PARTS, TOOLS>>;
271
+ const text =
272
+ message.text || ((message.message && extractText(message.message)) ?? "");
273
+
57
274
  const partCommon = {
58
275
  state: message.streaming ? ("streaming" as const) : ("done" as const),
59
276
  ...(message.providerMetadata
60
277
  ? { providerMetadata: message.providerMetadata }
61
278
  : {}),
62
279
  };
63
- if (coreMessage.role === "system") {
64
- uiMessages.push({
65
- ...common,
66
- role: "system",
67
- agentName: message.agentName,
68
- parts: [{ type: "text", text, ...partCommon } satisfies TextUIPart],
69
- });
70
- } else if (coreMessage.role === "user") {
71
- const parts: UIMessage<METADATA, DATA_PARTS, TOOLS>["parts"] = [];
72
- if (text && !nonStringContent.length) {
73
- parts.push({ type: "text", text });
74
- }
75
- nonStringContent.forEach((contentPart) => {
76
- switch (contentPart.type) {
77
- case "text":
78
- parts.push({ type: "text", text: contentPart.text, ...partCommon });
79
- break;
80
- case "file":
81
- case "image":
82
- parts.push(toUIFilePart(contentPart));
83
- break;
280
+
281
+ // Add reasoning parts
282
+ if (
283
+ message.reasoning &&
284
+ !nonStringContent.some((c) => c.type === "reasoning")
285
+ ) {
286
+ allParts.push({
287
+ type: "reasoning",
288
+ text: message.reasoning,
289
+ ...partCommon,
290
+ } satisfies ReasoningUIPart);
291
+ }
292
+
293
+ // Add text parts if no structured content
294
+ if (text && !nonStringContent.length) {
295
+ allParts.push({
296
+ type: "text",
297
+ text: text,
298
+ ...partCommon,
299
+ } satisfies TextUIPart);
300
+ }
301
+
302
+ // Add all structured content parts
303
+ for (const contentPart of nonStringContent) {
304
+ switch (contentPart.type) {
305
+ case "text":
306
+ allParts.push({
307
+ ...partCommon,
308
+ ...contentPart,
309
+ } satisfies TextUIPart);
310
+ break;
311
+ case "reasoning":
312
+ allParts.push({
313
+ ...partCommon,
314
+ ...contentPart,
315
+ } satisfies ReasoningUIPart);
316
+ break;
317
+ case "file":
318
+ case "image":
319
+ allParts.push(toUIFilePart(contentPart));
320
+ break;
321
+ case "tool-call": {
322
+ allParts.push({
323
+ type: "step-start",
324
+ } satisfies StepStartUIPart);
325
+ const toolPart: ToolUIPart<TOOLS> = {
326
+ type: `tool-${contentPart.toolName as keyof TOOLS & string}`,
327
+ toolCallId: contentPart.toolCallId,
328
+ input: contentPart.input as DeepPartial<
329
+ TOOLS[keyof TOOLS & string]["input"]
330
+ >,
331
+ providerExecuted: contentPart.providerExecuted,
332
+ ...(message.streaming
333
+ ? { state: "input-streaming" }
334
+ : {
335
+ state: "input-available",
336
+ callProviderMetadata: message.providerMetadata,
337
+ }),
338
+ };
339
+ allParts.push(toolPart);
340
+ break;
84
341
  }
85
- });
86
- uiMessages.push({ ...common, role: "user", parts });
87
- } else {
88
- if (coreMessage.role === "tool" && !assistantMessage) {
89
- console.warn(
90
- "Tool message without preceding assistant message.. skipping",
91
- message,
92
- );
93
- continue;
94
- }
95
- if (!assistantMessage) {
96
- assistantMessage = {
97
- ...common,
98
- role: "assistant",
99
- agentName: message.agentName,
100
- parts: [],
101
- };
102
- uiMessages.push(assistantMessage);
103
- } else {
104
- assistantMessage.status = message.streaming
105
- ? "streaming"
106
- : message.status;
107
- }
108
- // update it to the last message's id
109
- assistantMessage.id = message._id;
110
- if (
111
- message.reasoning &&
112
- !nonStringContent.some((c) => c.type === "reasoning")
113
- ) {
114
- assistantMessage.parts.push({
115
- type: "reasoning",
116
- text: message.reasoning,
117
- ...partCommon,
118
- } satisfies ReasoningUIPart);
119
- }
120
- if (message.text && !nonStringContent.length) {
121
- assistantMessage.parts.push({
122
- type: "text",
123
- text: message.text,
124
- ...partCommon,
125
- } satisfies TextUIPart);
126
- }
127
- for (const contentPart of nonStringContent) {
128
- switch (contentPart.type) {
129
- case "text":
130
- assistantMessage.parts.push({
131
- ...partCommon,
132
- ...contentPart,
133
- } satisfies TextUIPart);
134
- break;
135
- case "reasoning":
136
- assistantMessage.parts.push({
137
- ...partCommon,
138
- ...contentPart,
139
- } satisfies ReasoningUIPart);
140
- break;
141
- case "file":
142
- case "image":
143
- assistantMessage.parts.push(toUIFilePart(contentPart));
144
- break;
145
- case "tool-call": {
146
- assistantMessage.parts.push({
147
- type: "step-start",
148
- } satisfies StepStartUIPart);
149
- const toolPart: ToolUIPart<TOOLS> = {
150
- type: `tool-${contentPart.toolName as keyof TOOLS & string}`,
151
- toolCallId: contentPart.toolCallId,
152
- input: contentPart.input as DeepPartial<
153
- TOOLS[keyof TOOLS & string]["input"]
154
- >,
155
- providerExecuted: contentPart.providerExecuted,
156
- ...(message.streaming
157
- ? { state: "input-streaming" }
158
- : {
159
- state: "input-available",
160
- callProviderMetadata: message.providerMetadata,
161
- }),
162
- };
163
- assistantMessage.parts.push(toolPart);
164
- break;
165
- }
166
- case "tool-result": {
167
- const call = assistantMessage.parts.find(
168
- (part) =>
169
- part.type === `tool-${contentPart.toolName}` &&
170
- "toolCallId" in part &&
171
- part.toolCallId === contentPart.toolCallId,
172
- ) as ToolUIPart | undefined;
173
- if (call) {
174
- if (message.error) {
175
- call.state = "output-error";
176
- call.errorText = message.error;
177
- call.output = contentPart.output;
178
- } else {
179
- call.state = "output-available";
180
- call.output =
342
+ case "tool-result": {
343
+ const call = allParts.find(
344
+ (part) =>
345
+ part.type === `tool-${contentPart.toolName}` &&
346
+ "toolCallId" in part &&
347
+ part.toolCallId === contentPart.toolCallId,
348
+ ) as ToolUIPart | undefined;
349
+ if (call) {
350
+ if (message.error) {
351
+ call.state = "output-error";
352
+ call.errorText = message.error;
353
+ call.output = contentPart.output;
354
+ } else {
355
+ call.state = "output-available";
356
+ call.output =
357
+ contentPart.output?.type === "json"
358
+ ? contentPart.output.value
359
+ : contentPart.output;
360
+ }
361
+ } else {
362
+ console.warn(
363
+ "Tool result without preceding tool call.. adding anyways",
364
+ contentPart,
365
+ );
366
+ if (message.error) {
367
+ allParts.push({
368
+ type: `tool-${contentPart.toolName}`,
369
+ toolCallId: contentPart.toolCallId,
370
+ state: "output-error",
371
+ input: undefined,
372
+ errorText: message.error,
373
+ callProviderMetadata: message.providerMetadata,
374
+ } satisfies ToolUIPart<TOOLS>);
375
+ } else {
376
+ allParts.push({
377
+ type: `tool-${contentPart.toolName}`,
378
+ toolCallId: contentPart.toolCallId,
379
+ state: "output-available",
380
+ input: undefined,
381
+ output:
181
382
  contentPart.output?.type === "json"
182
383
  ? contentPart.output.value
183
- : contentPart.output;
184
- // Technically we could pull this from the doc.message
185
- // but the ModelMessage doesn't have it
186
- // call.providerExecuted = contentPart.providerExecuted;
187
- }
188
- } else {
189
- console.warn(
190
- "Tool result without preceding tool call.. adding anyways",
191
- contentPart,
192
- );
193
- if (message.error) {
194
- assistantMessage.parts.push({
195
- type: `tool-${contentPart.toolName}`,
196
- toolCallId: contentPart.toolCallId,
197
- state: "output-error",
198
- input: undefined,
199
- errorText: message.error,
200
- // Technically we could pull this from the doc.message
201
- // but the ModelMessage doesn't have it
202
- // providerExecuted: contentPart.providerExecuted,
203
- callProviderMetadata: message.providerMetadata,
204
- } satisfies ToolUIPart<TOOLS>);
205
- } else {
206
- assistantMessage.parts.push({
207
- type: `tool-${contentPart.toolName}`,
208
- toolCallId: contentPart.toolCallId,
209
- state: "output-available",
210
- input: undefined,
211
- output:
212
- contentPart.output?.type === "json"
213
- ? contentPart.output.value
214
- : contentPart.output,
215
- callProviderMetadata: message.providerMetadata,
216
- } satisfies ToolUIPart<TOOLS>);
217
- }
384
+ : contentPart.output,
385
+ callProviderMetadata: message.providerMetadata,
386
+ } satisfies ToolUIPart<TOOLS>);
218
387
  }
219
- break;
220
388
  }
221
- }
222
- }
223
- for (const source of message.sources ?? []) {
224
- if (source.sourceType === "url") {
225
- assistantMessage.parts.push({
226
- type: "source-url",
227
- url: source.url!,
228
- sourceId: source.id,
229
- providerMetadata: message.providerMetadata,
230
- title: source.title,
231
- } satisfies SourceUrlUIPart);
232
- } else {
233
- assistantMessage.parts.push({
234
- type: "source-document",
235
- mediaType: source.mediaType,
236
- sourceId: source.id,
237
- title: source.title,
238
- filename: source.filename,
239
- providerMetadata: message.providerMetadata,
240
- } satisfies SourceDocumentUIPart);
389
+ break;
241
390
  }
242
391
  }
243
392
  }
244
- if (!message.tool && assistantMessage) {
245
- // Reset it so the next set of tool calls will create a new assistant message
246
- assistantMessage = undefined;
393
+
394
+ // Add source parts
395
+ for (const source of message.sources ?? []) {
396
+ if (source.sourceType === "url") {
397
+ allParts.push({
398
+ type: "source-url",
399
+ url: source.url!,
400
+ sourceId: source.id,
401
+ providerMetadata: message.providerMetadata,
402
+ title: source.title,
403
+ } satisfies SourceUrlUIPart);
404
+ } else {
405
+ allParts.push({
406
+ type: "source-document",
407
+ mediaType: source.mediaType,
408
+ sourceId: source.id,
409
+ title: source.title,
410
+ filename: source.filename,
411
+ providerMetadata: message.providerMetadata,
412
+ } satisfies SourceDocumentUIPart);
413
+ }
247
414
  }
248
415
  }
249
- return uiMessages;
416
+
417
+ return {
418
+ ...common,
419
+ role: "assistant",
420
+ text: allText,
421
+ status,
422
+ parts: allParts,
423
+ };
250
424
  }
@@ -30,7 +30,9 @@ export function useSmoothText(
30
30
  text: string,
31
31
  { charsPerSec = 256, startStreaming = false }: SmoothTextOptions = {},
32
32
  ): [string, { cursor: number; isStreaming: boolean }] {
33
- const [visibleText, setVisibleText] = useState(startStreaming ? "" : text);
33
+ const [visibleText, setVisibleText] = useState(
34
+ startStreaming ? "" : text || "",
35
+ );
34
36
  const smoothState = useRef({
35
37
  tick: Date.now() + (visibleText.length * 1000) / charsPerSec,
36
38
  cursor: visibleText.length,
@@ -45,18 +47,28 @@ export function useSmoothText(
45
47
  if (!isStreaming) {
46
48
  return;
47
49
  }
48
- const latestCharsPerMs =
49
- (text.length - smoothState.current.initialLength) /
50
- (Date.now() - smoothState.current.start);
51
- // Smooth out the charsPerSec by averaging it with the previous value.
52
- smoothState.current.charsPerMs = Math.min(
53
- (2 * latestCharsPerMs + smoothState.current.charsPerMs) / 3,
54
- smoothState.current.charsPerMs * 2,
55
- );
56
- smoothState.current.tick = Math.max(
57
- smoothState.current.tick,
58
- Date.now() - 2 * MS_PER_FRAME,
59
- );
50
+ if (
51
+ smoothState.current.cursor === 0 &&
52
+ smoothState.current.initialLength === 0
53
+ ) {
54
+ smoothState.current.cursor = text.length;
55
+ smoothState.current.start = Date.now();
56
+ smoothState.current.initialLength = text.length;
57
+ smoothState.current.charsPerMs = charsPerSec / 1000;
58
+ } else {
59
+ const latestCharsPerMs =
60
+ (text.length - smoothState.current.initialLength) /
61
+ (Date.now() - smoothState.current.start);
62
+ // Smooth out the charsPerSec by averaging it with the previous value.
63
+ smoothState.current.charsPerMs = Math.min(
64
+ (2 * latestCharsPerMs + smoothState.current.charsPerMs) / 3,
65
+ smoothState.current.charsPerMs * 2,
66
+ );
67
+ smoothState.current.tick = Math.max(
68
+ smoothState.current.tick,
69
+ Date.now() - 2 * MS_PER_FRAME,
70
+ );
71
+ }
60
72
 
61
73
  function update() {
62
74
  if (smoothState.current.cursor >= text.length) {
package/src/shared.ts CHANGED
@@ -30,8 +30,22 @@ export function extractText(message: Message | ModelMessage) {
30
30
  .filter((c) => c.type === "text")
31
31
  .map((c) => c.text)
32
32
  .join("");
33
+ case "system":
34
+ return message.content;
35
+ // we don't extract text from tool messages
33
36
  }
34
37
  return undefined;
35
38
  }
36
39
 
37
40
  export const DEFAULT_MESSAGE_RANGE = { before: 2, after: 1 };
41
+
42
+ export function sorted<T extends { order: number; stepOrder: number }>(
43
+ messages: T[],
44
+ order: "asc" | "desc" = "asc",
45
+ ): T[] {
46
+ return [...messages].sort(
47
+ order === "asc"
48
+ ? (a, b) => a.order - b.order || a.stepOrder - b.stepOrder
49
+ : (a, b) => b.order - a.order || b.stepOrder - a.stepOrder,
50
+ );
51
+ }