@agents24/chat-react 0.1.6 → 0.1.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 (42) hide show
  1. package/README.md +29 -12
  2. package/dist/chunk-E3TJ3I6G.js +358 -0
  3. package/dist/chunk-E3TJ3I6G.js.map +1 -0
  4. package/dist/chunk-EWZO4QJI.js +140 -0
  5. package/dist/chunk-EWZO4QJI.js.map +1 -0
  6. package/dist/chunk-UU7OXHL3.js +11 -0
  7. package/dist/chunk-UU7OXHL3.js.map +1 -0
  8. package/dist/context-window.d.ts +38 -0
  9. package/dist/controller.d.ts +22 -0
  10. package/dist/index.cjs +653 -336
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +10 -466
  13. package/dist/index.js +290 -455
  14. package/dist/index.js.map +1 -1
  15. package/dist/latest-thread-scroller.d.ts +54 -0
  16. package/dist/model.d.ts +29 -0
  17. package/dist/renderers.d.ts +10 -0
  18. package/dist/sse.d.ts +3 -0
  19. package/dist/streaming-text.d.ts +24 -0
  20. package/dist/templates/agent-chat-shell.d.ts +25 -0
  21. package/dist/templates/index.cjs +523 -0
  22. package/dist/templates/index.cjs.map +1 -0
  23. package/dist/templates/index.d.ts +1 -0
  24. package/dist/templates/index.js +140 -0
  25. package/dist/templates/index.js.map +1 -0
  26. package/dist/transport.d.ts +29 -0
  27. package/dist/types.d.ts +312 -0
  28. package/dist/ui/adapters.d.ts +36 -0
  29. package/dist/ui/attachment.d.ts +23 -0
  30. package/dist/ui/bubble.d.ts +17 -0
  31. package/dist/ui/index.cjs +943 -0
  32. package/dist/ui/index.cjs.map +1 -0
  33. package/dist/ui/index.d.ts +7 -0
  34. package/dist/ui/index.js +734 -0
  35. package/dist/ui/index.js.map +1 -0
  36. package/dist/ui/marker.d.ts +11 -0
  37. package/dist/ui/message-response.d.ts +7 -0
  38. package/dist/ui/message.d.ts +10 -0
  39. package/dist/ui/utils.d.ts +2 -0
  40. package/dist/viewport.d.ts +2 -0
  41. package/package.json +22 -4
  42. package/dist/index.d.cts +0 -466
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -23,31 +33,38 @@ __export(index_exports, {
23
33
  DEFAULT_THREAD_PAGE_SIZE: () => DEFAULT_THREAD_PAGE_SIZE,
24
34
  DefaultChatPart: () => DefaultChatPart,
25
35
  DefaultToolPart: () => DefaultToolPart,
26
- LatestThreadViewport: () => LatestThreadViewport,
36
+ LatestThreadScroller: () => LatestThreadScroller,
37
+ LatestThreadScrollerButton: () => LatestThreadScrollerButton,
38
+ LatestThreadScrollerContent: () => LatestThreadScrollerContent,
39
+ LatestThreadScrollerItem: () => LatestThreadScrollerItem,
40
+ LatestThreadScrollerOutline: () => LatestThreadScrollerOutline,
41
+ LatestThreadScrollerProvider: () => LatestThreadScrollerProvider,
42
+ LatestThreadScrollerRoot: () => LatestThreadScrollerRoot,
43
+ LatestThreadScrollerViewport: () => LatestThreadScrollerViewport,
44
+ MessageScroller: () => import_message_scroller2.MessageScroller,
27
45
  activeRunIdFromThread: () => activeRunIdFromThread,
28
46
  activeRunIdFromThreadDetail: () => activeRunIdFromThreadDetail,
29
47
  assistantTextFromParts: () => assistantTextFromParts,
30
48
  assistantTextFromResponseBlocks: () => assistantTextFromResponseBlocks,
49
+ compressionFromContextWindow: () => compressionFromContextWindow,
31
50
  consumeSseResponse: () => consumeSseResponse,
32
51
  createChatId: () => createChatId,
33
52
  createFetchChatTransport: () => createFetchChatTransport,
34
53
  getActiveStreamingTextPartId: () => getActiveStreamingTextPartId,
35
- getLatestScrollTop: () => getLatestScrollTop,
36
54
  hasStaleUnfinishedAssistantCache: () => hasStaleUnfinishedAssistantCache,
37
55
  hasUnfinishedAssistantMessage: () => hasUnfinishedAssistantMessage,
38
56
  isActiveStreamingTextPart: () => isActiveStreamingTextPart,
39
- isAtTimelineLatestEdge: () => isAtTimelineLatestEdge,
40
57
  isRunningThreadStatus: () => isRunningThreadStatus,
41
- isScrollable: () => isScrollable,
42
58
  latestContextWindowFromThread: () => latestContextWindowFromThread,
59
+ mergeContextWindow: () => mergeContextWindow,
60
+ mergeContextWindowUpdate: () => mergeContextWindowUpdate,
43
61
  mergeReasoningSteps: () => mergeReasoningSteps,
44
- nextLatestFollowStateOnScroll: () => nextLatestFollowStateOnScroll,
62
+ normalizeContextCompression: () => normalizeContextCompression,
63
+ normalizeContextWindow: () => normalizeContextWindow,
45
64
  parseSseBlock: () => parseSseBlock,
46
65
  partsFromResponseBlocks: () => partsFromResponseBlocks,
47
66
  reasoningStepsFromParts: () => reasoningStepsFromParts,
48
67
  renderChatPart: () => renderChatPart,
49
- shouldPrefetchOlder: () => shouldPrefetchOlder,
50
- shouldUseTopOriginTimeline: () => shouldUseTopOriginTimeline,
51
68
  textFromFinalOutput: () => textFromFinalOutput,
52
69
  threadActivityDate: () => threadActivityDate,
53
70
  threadDetailToMessages: () => threadDetailToMessages,
@@ -55,7 +72,9 @@ __export(index_exports, {
55
72
  titleFromMessage: () => titleFromMessage,
56
73
  toolStateFromStatus: () => toolStateFromStatus,
57
74
  useAgents24ChatController: () => useAgents24ChatController,
58
- useLatestThreadViewport: () => useLatestThreadViewport,
75
+ useMessageScroller: () => import_message_scroller2.useMessageScroller,
76
+ useMessageScrollerScrollable: () => import_message_scroller2.useMessageScrollerScrollable,
77
+ useMessageScrollerVisibility: () => import_message_scroller2.useMessageScrollerVisibility,
59
78
  useStreamingText: () => useStreamingText
60
79
  });
61
80
  module.exports = __toCommonJS(index_exports);
@@ -63,9 +82,128 @@ module.exports = __toCommonJS(index_exports);
63
82
  // src/controller.ts
64
83
  var import_react = require("react");
65
84
 
85
+ // src/context-window.ts
86
+ var SOURCE_PRIORITY = {
87
+ unknown: 0,
88
+ heuristic_estimate: 1,
89
+ tokenizer_estimate: 1,
90
+ text_estimate: 1,
91
+ estimated: 1,
92
+ multimodal_estimate: 2,
93
+ hf_tokenizer: 3,
94
+ runtime_tokenizer: 3,
95
+ provider_count_api: 4,
96
+ provider_usage: 5,
97
+ exact: 5
98
+ };
99
+ var STAGE_PRIORITY = {
100
+ preflight: 0,
101
+ sent_prompt: 1,
102
+ final_usage: 2
103
+ };
104
+ function numberOrNull(value) {
105
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
106
+ }
107
+ function stagePriority(window2) {
108
+ return STAGE_PRIORITY[window2?.stage || "sent_prompt"] ?? STAGE_PRIORITY.sent_prompt;
109
+ }
110
+ function hasRenderableContextWindow(window2) {
111
+ const maxTokens = window2?.max_tokens;
112
+ return typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0;
113
+ }
114
+ function windowWeight(window2) {
115
+ if (!window2) return [0, 0];
116
+ return [stagePriority(window2), SOURCE_PRIORITY[window2.source] || 0];
117
+ }
118
+ function normalizeContextCompression(value) {
119
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
120
+ const payload = value;
121
+ return {
122
+ active: Boolean(payload.active),
123
+ reason: typeof payload.reason === "string" ? payload.reason : null,
124
+ input_tokens: numberOrNull(payload.input_tokens),
125
+ max_tokens: numberOrNull(payload.max_tokens),
126
+ usage_ratio: numberOrNull(payload.usage_ratio),
127
+ full_frame_count: numberOrNull(payload.full_frame_count),
128
+ compact_frame_count: numberOrNull(payload.compact_frame_count),
129
+ dropped_frame_count: numberOrNull(payload.dropped_frame_count),
130
+ artifact_ref_count: numberOrNull(payload.artifact_ref_count),
131
+ compression_trigger_budget: numberOrNull(payload.compression_trigger_budget),
132
+ compression_target_budget: numberOrNull(payload.compression_target_budget),
133
+ compression_target_ratio: numberOrNull(payload.compression_target_ratio),
134
+ threshold_used: numberOrNull(payload.threshold_used)
135
+ };
136
+ }
137
+ function normalizeContextWindow(value) {
138
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
139
+ const payload = value;
140
+ const rawSource = String(payload.source || "").trim();
141
+ const source = rawSource === "exact" || rawSource === "estimated" || rawSource === "unknown" || rawSource === "provider_count_api" || rawSource === "provider_usage" || rawSource === "runtime_tokenizer" || rawSource === "hf_tokenizer" || rawSource === "multimodal_estimate" || rawSource === "text_estimate" || rawSource === "tokenizer_estimate" || rawSource === "heuristic_estimate" ? rawSource : rawSource ? "estimated" : "unknown";
142
+ const rawStage = String(payload.stage || "").trim();
143
+ const stage = rawStage === "preflight" || rawStage === "sent_prompt" || rawStage === "final_usage" ? rawStage : "sent_prompt";
144
+ const rawConfidence = String(payload.confidence || "").trim();
145
+ const confidence = rawConfidence === "exact" || rawConfidence === "high" || rawConfidence === "medium" || rawConfidence === "low" || rawConfidence === "unknown" ? rawConfidence : null;
146
+ return {
147
+ source,
148
+ run_id: typeof payload.run_id === "string" ? payload.run_id : null,
149
+ stage,
150
+ confidence,
151
+ counter: typeof payload.counter === "string" ? payload.counter : null,
152
+ model_id: typeof payload.model_id === "string" ? payload.model_id : null,
153
+ max_tokens: numberOrNull(payload.max_tokens),
154
+ max_tokens_source: typeof payload.max_tokens_source === "string" ? payload.max_tokens_source : null,
155
+ input_tokens: numberOrNull(payload.input_tokens),
156
+ remaining_tokens: numberOrNull(payload.remaining_tokens),
157
+ usage_ratio: numberOrNull(payload.usage_ratio),
158
+ assembly: payload.assembly && typeof payload.assembly === "object" && !Array.isArray(payload.assembly) ? payload.assembly : null,
159
+ context_compression: normalizeContextCompression(payload.context_compression)
160
+ };
161
+ }
162
+ function mergeContextWindow(current, incoming) {
163
+ if (!incoming) return current ?? null;
164
+ if (!current) return incoming;
165
+ if (hasRenderableContextWindow(current) && !hasRenderableContextWindow(incoming)) return current;
166
+ const currentRunId = current.run_id || null;
167
+ const incomingRunId = incoming.run_id || null;
168
+ if (incomingRunId && currentRunId && incomingRunId !== currentRunId) {
169
+ return stagePriority(incoming) >= STAGE_PRIORITY.sent_prompt ? incoming : current;
170
+ }
171
+ const currentWeight = windowWeight(current);
172
+ const incomingWeight = windowWeight(incoming);
173
+ return incomingWeight[0] > currentWeight[0] || incomingWeight[0] === currentWeight[0] && incomingWeight[1] >= currentWeight[1] ? incoming : current;
174
+ }
175
+ function mergeContextWindowUpdate(current, incoming) {
176
+ return mergeContextWindow(current, normalizeContextWindow(incoming));
177
+ }
178
+ function compressionFromContextWindow(contextWindow) {
179
+ if (!contextWindow) return null;
180
+ if (contextWindow.context_compression) return contextWindow.context_compression;
181
+ const assembly = contextWindow.assembly;
182
+ if (!assembly || typeof assembly !== "object" || Array.isArray(assembly)) return null;
183
+ const artifactRefs = Array.isArray(assembly.artifact_refs) ? assembly.artifact_refs : [];
184
+ const compactFrameCount = numberOrNull(assembly.compact_frame_count) ?? 0;
185
+ const droppedFrameCount = numberOrNull(assembly.dropped_frame_count) ?? 0;
186
+ const reason = typeof assembly.compaction_reason === "string" ? assembly.compaction_reason : "none";
187
+ return {
188
+ active: reason !== "none" || compactFrameCount > 0 || droppedFrameCount > 0,
189
+ reason,
190
+ input_tokens: contextWindow.input_tokens ?? null,
191
+ max_tokens: contextWindow.max_tokens ?? null,
192
+ usage_ratio: contextWindow.usage_ratio ?? null,
193
+ full_frame_count: numberOrNull(assembly.full_frame_count),
194
+ compact_frame_count: compactFrameCount,
195
+ dropped_frame_count: droppedFrameCount,
196
+ artifact_ref_count: artifactRefs.length,
197
+ compression_trigger_budget: numberOrNull(assembly.compression_trigger_budget),
198
+ compression_target_budget: numberOrNull(assembly.compression_target_budget),
199
+ compression_target_ratio: numberOrNull(assembly.compression_target_ratio),
200
+ threshold_used: numberOrNull(assembly.threshold_used)
201
+ };
202
+ }
203
+
66
204
  // src/model.ts
67
205
  var DEFAULT_THREAD_PAGE_SIZE = 5;
68
- var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running"])).has(String(status || "").toLowerCase());
206
+ var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
69
207
  var createChatId = () => {
70
208
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
71
209
  return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
@@ -104,6 +242,16 @@ var attachmentsFromTurn = (turn) => (turn.attachments || []).map((attachment, in
104
242
  mediaType: String(record.mime_type || record.mediaType || record.type || "application/octet-stream")
105
243
  };
106
244
  });
245
+ var tokenUsageFromTurn = (turn) => {
246
+ const usage = turn.run_usage;
247
+ if (!usage || typeof usage !== "object") return void 0;
248
+ return {
249
+ inputTokens: usage.input_tokens ?? null,
250
+ outputTokens: usage.output_tokens ?? null,
251
+ totalTokens: usage.total_tokens ?? null,
252
+ usageSource: usage.source ?? null
253
+ };
254
+ };
107
255
  var latestEventPayloadValue = (events, key) => {
108
256
  for (let index = (events || []).length - 1; index >= 0; index -= 1) {
109
257
  const payload = events?.[index]?.payload;
@@ -305,7 +453,8 @@ var turnToMessages = (turn, activeRunId) => {
305
453
  createdAt: turn.completed_at ? new Date(turn.completed_at) : createdAt,
306
454
  parts,
307
455
  reasoningSteps: mergeReasoningSteps(reasoningStepsFromParts(parts), { finalize: !isRunning }),
308
- isFinal: !isRunning
456
+ isFinal: !isRunning,
457
+ tokenUsage: tokenUsageFromTurn(turn)
309
458
  });
310
459
  }
311
460
  return messages;
@@ -322,10 +471,11 @@ var threadPaging = (thread) => {
322
471
  };
323
472
  };
324
473
  var latestContextWindowFromThread = (thread) => {
325
- if (thread.context_window && typeof thread.context_window === "object") return thread.context_window;
474
+ const threadContextWindow = normalizeContextWindow(thread.context_window);
475
+ if (threadContextWindow) return threadContextWindow;
326
476
  for (let index = (thread.turns || []).length - 1; index >= 0; index -= 1) {
327
- const value = thread.turns?.[index]?.context_window;
328
- if (value && typeof value === "object") return value;
477
+ const contextWindow = normalizeContextWindow(thread.turns?.[index]?.context_window);
478
+ if (contextWindow) return contextWindow;
329
479
  }
330
480
  return null;
331
481
  };
@@ -356,10 +506,6 @@ var activeRunIdFromThreadDetail = (thread) => {
356
506
  };
357
507
 
358
508
  // src/controller.ts
359
- var mergeContextWindow = (current, incoming) => {
360
- if (!incoming || typeof incoming !== "object") return current;
361
- return { ...current || {}, ...incoming };
362
- };
363
509
  var threadSummaryToStored = (thread) => ({
364
510
  ...thread,
365
511
  id: String(thread.id),
@@ -377,7 +523,9 @@ function useAgents24ChatController({
377
523
  createId = createChatId,
378
524
  onActiveThreadIdChange,
379
525
  onSourceClick,
380
- onStreamErrorMessage
526
+ onStreamErrorMessage,
527
+ onRuntimeEvent,
528
+ onThreadDetailLoaded
381
529
  }) {
382
530
  const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
383
531
  const initialCached = activeThreadId ? storage.getThread(activeThreadId) : void 0;
@@ -394,6 +542,9 @@ function useAgents24ChatController({
394
542
  const [disliked, setDisliked] = (0, import_react.useState)({});
395
543
  const [copiedMessageId, setCopiedMessageId] = (0, import_react.useState)(null);
396
544
  const [lastThinkingDurationMs, setLastThinkingDurationMs] = (0, import_react.useState)(null);
545
+ const [threads, setThreads] = (0, import_react.useState)(() => storage.listThreads());
546
+ const [isRefreshingThreads, setIsRefreshingThreads] = (0, import_react.useState)(false);
547
+ const [activeRunId, setActiveRunId] = (0, import_react.useState)(null);
397
548
  const textareaRef = (0, import_react.useRef)(null);
398
549
  const activeThreadIdRef = (0, import_react.useRef)(activeThreadId);
399
550
  const messagesRef = (0, import_react.useRef)(messages);
@@ -410,6 +561,20 @@ function useAgents24ChatController({
410
561
  const streamingMessageIdRef = (0, import_react.useRef)(null);
411
562
  const reasoningRef = (0, import_react.useRef)([]);
412
563
  const liveVoiceIdsRef = (0, import_react.useRef)({});
564
+ const setActiveRunIdValue = (0, import_react.useCallback)((runId) => {
565
+ activeRunIdRef.current = runId;
566
+ setActiveRunId(runId);
567
+ }, []);
568
+ const syncThreadsFromStorage = (0, import_react.useCallback)(() => {
569
+ setThreads(storage.listThreads());
570
+ }, [storage]);
571
+ const upsertStoredThread = (0, import_react.useCallback)(
572
+ (thread) => {
573
+ storage.upsertThread(thread);
574
+ syncThreadsFromStorage();
575
+ },
576
+ [storage, syncThreadsFromStorage]
577
+ );
413
578
  (0, import_react.useEffect)(() => {
414
579
  messagesRef.current = messages;
415
580
  }, [messages]);
@@ -417,7 +582,7 @@ function useAgents24ChatController({
417
582
  (threadId, nextMessages, paging, options) => {
418
583
  const existing = storage.getThread(threadId);
419
584
  const firstUser = nextMessages.find((message) => message.role === "user");
420
- storage.upsertThread({
585
+ upsertStoredThread({
421
586
  ...existing || {},
422
587
  id: threadId,
423
588
  title: existing?.title || (firstUser ? titleFromMessage(firstUser.content, firstUser.attachments || []) : "New chat"),
@@ -428,13 +593,13 @@ function useAgents24ChatController({
428
593
  nextBeforeTurnIndex: paging?.nextBeforeTurnIndex ?? nextBeforeTurnIndexRef.current
429
594
  });
430
595
  },
431
- [storage]
596
+ [storage, upsertStoredThread]
432
597
  );
433
598
  const markThreadRunStatus = (0, import_react.useCallback)(
434
599
  (threadId, runId, status, lastEventSeq) => {
435
600
  const existing = storage.getThread(threadId);
436
601
  if (!existing || !runId) return;
437
- storage.upsertThread({
602
+ upsertStoredThread({
438
603
  ...existing,
439
604
  last_run_id: runId,
440
605
  last_run_status: status,
@@ -451,14 +616,25 @@ function useAgents24ChatController({
451
616
  created_at: existing.activeRun?.created_at ?? existing.active_run?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
452
617
  },
453
618
  lastEventSeq: typeof lastEventSeq === "number" ? lastEventSeq : existing.lastEventSeq ?? null,
454
- isRunning: status === "queued" || status === "running"
619
+ isRunning: activeRunIdFromThread({
620
+ lastRunId: runId,
621
+ lastRunStatus: status,
622
+ activeRun: { run_id: runId, status }
623
+ }) !== null
455
624
  });
456
625
  },
457
- [storage]
626
+ [storage, upsertStoredThread]
458
627
  );
459
628
  const refresh = (0, import_react.useCallback)(async () => {
460
- const data = await transport.listThreads();
461
- storage.setThreads((data.items || []).map((item) => threadSummaryToStored(item)));
629
+ setIsRefreshingThreads(true);
630
+ try {
631
+ const data = await transport.listThreads();
632
+ const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
633
+ storage.setThreads(nextThreads);
634
+ setThreads(storage.listThreads());
635
+ } finally {
636
+ setIsRefreshingThreads(false);
637
+ }
462
638
  }, [storage, transport]);
463
639
  const applyThreadId = (0, import_react.useCallback)(
464
640
  (threadId, baseMessages) => {
@@ -486,7 +662,7 @@ function useAgents24ChatController({
486
662
  const controller = abortControllerRef.current;
487
663
  abortControllerRef.current = null;
488
664
  controller?.abort();
489
- activeRunIdRef.current = null;
665
+ setActiveRunIdValue(null);
490
666
  reattachedRunIdRef.current = null;
491
667
  streamingMessageIdRef.current = null;
492
668
  streamingContentRef.current = "";
@@ -495,7 +671,7 @@ function useAgents24ChatController({
495
671
  setIsLoading(false);
496
672
  setStreamingContent("");
497
673
  setCurrentReasoning([]);
498
- }, []);
674
+ }, [setActiveRunIdValue]);
499
675
  const setLiveAssistantMessage = (0, import_react.useCallback)(
500
676
  (input) => {
501
677
  setMessages((prev) => {
@@ -565,7 +741,7 @@ function useAgents24ChatController({
565
741
  persistThread(input.threadId, completed);
566
742
  const existingThread = storage.getThread(input.threadId);
567
743
  if (existingThread && input.runId) {
568
- storage.upsertThread({
744
+ upsertStoredThread({
569
745
  ...existingThread,
570
746
  messages: completed,
571
747
  last_run_id: input.runId,
@@ -580,7 +756,7 @@ function useAgents24ChatController({
580
756
  }
581
757
  return completed;
582
758
  },
583
- [createId, persistThread, storage]
759
+ [createId, persistThread, storage, upsertStoredThread]
584
760
  );
585
761
  const loadThread = (0, import_react.useCallback)(
586
762
  async (threadId) => {
@@ -595,6 +771,7 @@ function useAgents24ChatController({
595
771
  includeRunEvents: false
596
772
  });
597
773
  if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
774
+ void onThreadDetailLoaded?.(detail);
598
775
  const nextMessages = threadDetailToMessages(detail);
599
776
  const paging = threadPaging(detail);
600
777
  setContextStatus(latestContextWindowFromThread(detail));
@@ -604,7 +781,7 @@ function useAgents24ChatController({
604
781
  hasOlderTurnsRef.current = paging.hasOlderTurns;
605
782
  nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
606
783
  loadedThreadIdRef.current = threadId;
607
- storage.upsertThread({
784
+ upsertStoredThread({
608
785
  ...threadSummaryToStored(detail),
609
786
  messages: nextMessages,
610
787
  isHydrated: true,
@@ -616,7 +793,7 @@ function useAgents24ChatController({
616
793
  if (activeThreadIdRef.current === threadId) setLoadingHistory(false);
617
794
  }
618
795
  },
619
- [pageSize, setLoadingHistory, storage, transport]
796
+ [onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
620
797
  );
621
798
  const loadOlderTurns = (0, import_react.useCallback)(async () => {
622
799
  const threadId = activeThreadIdRef.current;
@@ -648,11 +825,18 @@ function useAgents24ChatController({
648
825
  }, [pageSize, persistThread, transport]);
649
826
  const handleStreamEvent = (0, import_react.useCallback)(
650
827
  (input) => {
651
- const { event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
828
+ const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
652
829
  const payload = event.payload || {};
653
830
  const responseBlocks = Array.isArray(payload.response_blocks) ? payload.response_blocks : null;
654
- if (event.run_id) activeRunIdRef.current = event.run_id;
655
- setContextStatus((current) => mergeContextWindow(current, payload.context_window));
831
+ if (event.run_id) setActiveRunIdValue(event.run_id);
832
+ void onRuntimeEvent?.(event, {
833
+ mode,
834
+ assistantMessageId,
835
+ threadId: streamThreadIdRef.current,
836
+ runId: event.run_id || activeRunIdRef.current,
837
+ startedAt
838
+ });
839
+ setContextStatus((current) => mergeContextWindowUpdate(current, payload.context_window));
656
840
  if (responseBlocks) {
657
841
  const blockText = String(payload.assistant_output_text || "") || assistantTextFromResponseBlocks(responseBlocks) || streamingContentRef.current;
658
842
  if (blockText) setStreamingText(blockText);
@@ -698,7 +882,7 @@ function useAgents24ChatController({
698
882
  error: isFailed ? String(payload.message || payload.error || event.diagnostics?.[0]?.message || onStreamErrorMessage?.(event) || "The chat run failed.") : void 0
699
883
  });
700
884
  },
701
- [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onStreamErrorMessage, setLiveAssistantMessage, setReasoningSteps]
885
+ [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
702
886
  );
703
887
  const runStream = (0, import_react.useCallback)(
704
888
  async (input) => {
@@ -760,16 +944,16 @@ function useAgents24ChatController({
760
944
  }
761
945
  try {
762
946
  if (input.mode === "attach") {
763
- activeRunIdRef.current = input.runId;
947
+ setActiveRunIdValue(input.runId);
764
948
  reattachedRunIdRef.current = input.runId;
765
949
  await transport.attachRun(
766
950
  { runId: input.runId, signal: controller.signal },
767
- (event) => handleStreamEvent({ event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
951
+ (event) => handleStreamEvent({ mode: "attach", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
768
952
  );
769
953
  } else {
770
954
  await transport.streamMessage(
771
- { text: input.message.text, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
772
- (event) => handleStreamEvent({ event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
955
+ { ...input.message, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
956
+ (event) => handleStreamEvent({ mode: "submit", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
773
957
  );
774
958
  }
775
959
  if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
@@ -801,7 +985,7 @@ function useAgents24ChatController({
801
985
  const isCurrentStream = abortControllerRef.current === controller || streamingMessageIdRef.current === assistantMessageId;
802
986
  if (abortControllerRef.current === controller) abortControllerRef.current = null;
803
987
  if (isCurrentStream) {
804
- activeRunIdRef.current = null;
988
+ setActiveRunIdValue(null);
805
989
  streamingMessageIdRef.current = null;
806
990
  reattachedRunIdRef.current = null;
807
991
  setStreamingMessageId(null);
@@ -812,7 +996,7 @@ function useAgents24ChatController({
812
996
  }
813
997
  }
814
998
  },
815
- [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setReasoningSteps, transport]
999
+ [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
816
1000
  );
817
1001
  const handleSubmit = (0, import_react.useCallback)(
818
1002
  async (message) => {
@@ -827,7 +1011,7 @@ function useAgents24ChatController({
827
1011
  const liveMessageId = streamingMessageIdRef.current;
828
1012
  abortControllerRef.current?.abort();
829
1013
  abortControllerRef.current = null;
830
- activeRunIdRef.current = null;
1014
+ setActiveRunIdValue(null);
831
1015
  streamingMessageIdRef.current = null;
832
1016
  setStreamingMessageId(null);
833
1017
  setIsLoading(false);
@@ -844,10 +1028,16 @@ function useAgents24ChatController({
844
1028
  messageId: liveMessageId
845
1029
  });
846
1030
  }
847
- }, [finalizeAssistantMessage, lastThinkingDurationMs, setReasoningSteps, transport]);
1031
+ }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
848
1032
  (0, import_react.useEffect)(() => {
849
- refresh().catch(() => storage.setThreads([]));
1033
+ refresh().catch(() => {
1034
+ storage.setThreads([]);
1035
+ setThreads([]);
1036
+ });
850
1037
  }, [refresh, storage]);
1038
+ (0, import_react.useEffect)(() => {
1039
+ syncThreadsFromStorage();
1040
+ }, [storageKey, syncThreadsFromStorage]);
851
1041
  (0, import_react.useEffect)(() => {
852
1042
  const previous = activeThreadIdRef.current;
853
1043
  activeThreadIdRef.current = activeThreadId;
@@ -955,7 +1145,40 @@ function useAgents24ChatController({
955
1145
  return next;
956
1146
  });
957
1147
  }, [createId, persistThread]);
1148
+ const startNewThread = (0, import_react.useCallback)(() => {
1149
+ detachActiveStream();
1150
+ requestSeqRef.current += 1;
1151
+ activeThreadIdRef.current = null;
1152
+ loadedThreadIdRef.current = null;
1153
+ nextBeforeTurnIndexRef.current = null;
1154
+ hasOlderTurnsRef.current = false;
1155
+ storage.setActiveThreadId?.(null);
1156
+ onActiveThreadIdChange?.(null);
1157
+ setMessages([]);
1158
+ messagesRef.current = [];
1159
+ setHasOlderTurns(false);
1160
+ setIsLoadingOlder(false);
1161
+ setContextStatus(null);
1162
+ setLoadingHistory(false);
1163
+ }, [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage]);
1164
+ const loadThreadById = (0, import_react.useCallback)(
1165
+ async (threadId) => {
1166
+ if (!threadId) return;
1167
+ if (activeThreadIdRef.current !== threadId) {
1168
+ if (abortControllerRef.current) detachActiveStream();
1169
+ activeThreadIdRef.current = threadId;
1170
+ storage.setActiveThreadId?.(threadId);
1171
+ onActiveThreadIdChange?.(threadId);
1172
+ }
1173
+ await loadThread(threadId);
1174
+ },
1175
+ [detachActiveStream, loadThread, onActiveThreadIdChange, storage]
1176
+ );
1177
+ const activeThread = activeThreadId ? storage.getThread(activeThreadId) || threads.find((thread) => thread.id === activeThreadId) || null : null;
958
1178
  return (0, import_react.useMemo)(() => ({
1179
+ threads,
1180
+ activeThreadId,
1181
+ activeThread,
959
1182
  messages,
960
1183
  streamingContent,
961
1184
  streamingMessageId,
@@ -964,12 +1187,13 @@ function useAgents24ChatController({
964
1187
  isLoading,
965
1188
  isLoadingHistory,
966
1189
  isLoadingOlder,
1190
+ isRefreshingThreads,
967
1191
  hasOlderTurns,
968
1192
  liked,
969
1193
  disliked,
970
1194
  copiedMessageId,
971
1195
  lastThinkingDurationMs,
972
- activeRunId: activeRunIdRef.current,
1196
+ activeRunId,
973
1197
  handleSubmit,
974
1198
  handleStop,
975
1199
  handleCopy,
@@ -978,11 +1202,16 @@ function useAgents24ChatController({
978
1202
  handleRetry,
979
1203
  handleSourceClick: (citations) => onSourceClick?.(citations),
980
1204
  upsertLiveVoiceMessage,
1205
+ startNewThread,
1206
+ loadThreadById,
981
1207
  loadOlderTurns,
982
1208
  refresh,
983
1209
  textareaRef
984
1210
  }), [
985
1211
  copiedMessageId,
1212
+ activeRunId,
1213
+ activeThread,
1214
+ activeThreadId,
986
1215
  contextStatus,
987
1216
  currentReasoning,
988
1217
  disliked,
@@ -996,26 +1225,373 @@ function useAgents24ChatController({
996
1225
  isLoading,
997
1226
  isLoadingHistory,
998
1227
  isLoadingOlder,
1228
+ isRefreshingThreads,
999
1229
  lastThinkingDurationMs,
1000
1230
  liked,
1231
+ loadThreadById,
1001
1232
  loadOlderTurns,
1002
1233
  messages,
1003
1234
  onSourceClick,
1004
1235
  refresh,
1005
1236
  streamingContent,
1006
1237
  streamingMessageId,
1238
+ startNewThread,
1239
+ threads,
1007
1240
  upsertLiveVoiceMessage
1008
1241
  ]);
1009
1242
  }
1010
1243
 
1011
- // src/renderers.tsx
1244
+ // src/latest-thread-scroller.tsx
1245
+ var React = __toESM(require("react"), 1);
1246
+ var import_message_scroller = require("@shadcn/react/message-scroller");
1012
1247
  var import_jsx_runtime = require("react/jsx-runtime");
1248
+ var cx = (...values) => values.filter(Boolean).join(" ");
1249
+ var LatestThreadScrollerLayoutContext = React.createContext(null);
1250
+ var useLatestThreadScrollerLayout = () => React.useContext(LatestThreadScrollerLayoutContext);
1251
+ function LatestThreadScrollerProvider({
1252
+ autoScroll = true,
1253
+ defaultScrollPosition = "last-anchor",
1254
+ scrollPreviousItemPeek = 64,
1255
+ ...props
1256
+ }) {
1257
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1258
+ import_message_scroller.MessageScroller.Provider,
1259
+ {
1260
+ autoScroll,
1261
+ defaultScrollPosition,
1262
+ scrollPreviousItemPeek,
1263
+ ...props
1264
+ }
1265
+ );
1266
+ }
1267
+ function LatestThreadScrollerRoot({
1268
+ className,
1269
+ ref,
1270
+ ...props
1271
+ }) {
1272
+ const [rootElement, setRootElement] = React.useState(null);
1273
+ const [contentElement, setContentElement] = React.useState(null);
1274
+ const layoutContext = React.useMemo(
1275
+ () => ({
1276
+ contentElement,
1277
+ rootElement,
1278
+ setContentElement,
1279
+ setRootElement
1280
+ }),
1281
+ [contentElement, rootElement]
1282
+ );
1283
+ const setRef = React.useCallback(
1284
+ (node) => {
1285
+ setRootElement(node);
1286
+ if (typeof ref === "function") {
1287
+ ref(node);
1288
+ } else if (ref) {
1289
+ ref.current = node;
1290
+ }
1291
+ },
1292
+ [ref]
1293
+ );
1294
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LatestThreadScrollerLayoutContext.Provider, { value: layoutContext, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1295
+ import_message_scroller.MessageScroller.Root,
1296
+ {
1297
+ ref: setRef,
1298
+ className: cx(
1299
+ "relative flex size-full min-h-0 flex-1 flex-col overflow-hidden",
1300
+ className
1301
+ ),
1302
+ ...props
1303
+ }
1304
+ ) });
1305
+ }
1306
+ function LatestThreadScrollerViewport({
1307
+ className,
1308
+ onViewportChange,
1309
+ preserveScrollOnPrepend = true,
1310
+ ref,
1311
+ ...props
1312
+ }) {
1313
+ const setRef = React.useCallback(
1314
+ (node) => {
1315
+ if (typeof ref === "function") {
1316
+ ref(node);
1317
+ } else if (ref) {
1318
+ ref.current = node;
1319
+ }
1320
+ onViewportChange?.(node);
1321
+ },
1322
+ [onViewportChange, ref]
1323
+ );
1324
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1325
+ import_message_scroller.MessageScroller.Viewport,
1326
+ {
1327
+ ref: setRef,
1328
+ className: cx(
1329
+ "size-full min-h-0 min-w-0 flex-1 overflow-y-auto overscroll-contain",
1330
+ className
1331
+ ),
1332
+ preserveScrollOnPrepend,
1333
+ ...props
1334
+ }
1335
+ );
1336
+ }
1337
+ function LatestThreadScrollerContent({
1338
+ className,
1339
+ ref,
1340
+ ...props
1341
+ }) {
1342
+ const layout = useLatestThreadScrollerLayout();
1343
+ const setRef = React.useCallback(
1344
+ (node) => {
1345
+ layout?.setContentElement(node);
1346
+ if (typeof ref === "function") {
1347
+ ref(node);
1348
+ } else if (ref) {
1349
+ ref.current = node;
1350
+ }
1351
+ },
1352
+ [layout, ref]
1353
+ );
1354
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1355
+ import_message_scroller.MessageScroller.Content,
1356
+ {
1357
+ ref: setRef,
1358
+ "data-agents24-latest-thread-content": "",
1359
+ className: cx("flex h-max min-h-full flex-col gap-8 p-4", className),
1360
+ ...props
1361
+ }
1362
+ );
1363
+ }
1364
+ function LatestThreadScrollerItem({
1365
+ className,
1366
+ messageId,
1367
+ ...props
1368
+ }) {
1369
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1370
+ import_message_scroller.MessageScroller.Item,
1371
+ {
1372
+ className: cx(
1373
+ "min-w-0 shrink-0 overflow-visible",
1374
+ className
1375
+ ),
1376
+ messageId,
1377
+ ...props
1378
+ }
1379
+ );
1380
+ }
1381
+ function LatestThreadScrollerButton({
1382
+ children,
1383
+ className,
1384
+ direction = "end",
1385
+ render,
1386
+ ...props
1387
+ }) {
1388
+ if (render) {
1389
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1390
+ import_message_scroller.MessageScroller.Button,
1391
+ {
1392
+ direction,
1393
+ render,
1394
+ ...props
1395
+ }
1396
+ );
1397
+ }
1398
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1399
+ import_message_scroller.MessageScroller.Button,
1400
+ {
1401
+ direction,
1402
+ render: (buttonProps) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1403
+ "button",
1404
+ {
1405
+ ...buttonProps,
1406
+ "aria-label": direction === "end" ? "Scroll to latest message" : "Scroll to first message",
1407
+ className: cx(
1408
+ "absolute bottom-4 left-1/2 z-50 flex size-9 -translate-x-1/2 items-center justify-center rounded-full border border-border bg-background text-foreground shadow-md transition-all duration-200 data-[active=false]:pointer-events-none data-[active=false]:translate-y-2 data-[active=false]:opacity-0 data-[active=true]:translate-y-0 data-[active=true]:opacity-100",
1409
+ className
1410
+ ),
1411
+ type: "button",
1412
+ children: children ?? (direction === "end" ? "\u2193" : "\u2191")
1413
+ }
1414
+ ),
1415
+ ...props
1416
+ }
1417
+ );
1418
+ }
1419
+ function LatestThreadScrollerOutline({
1420
+ cardClassName,
1421
+ className,
1422
+ contentMaxWidth = 768,
1423
+ dir = "ltr",
1424
+ items,
1425
+ minContainerWidthForRail = 0,
1426
+ minSideGutterForRail = 48,
1427
+ side
1428
+ }) {
1429
+ const navRef = React.useRef(null);
1430
+ const layout = useLatestThreadScrollerLayout();
1431
+ const [hasSideRoom, setHasSideRoom] = React.useState(false);
1432
+ const [hoveredIndex, setHoveredIndex] = React.useState(null);
1433
+ const { scrollToMessage } = (0, import_message_scroller.useMessageScroller)();
1434
+ const { currentAnchorId, visibleMessageIds } = (0, import_message_scroller.useMessageScrollerVisibility)();
1435
+ const visibleMessageIdSet = React.useMemo(
1436
+ () => new Set(visibleMessageIds),
1437
+ [visibleMessageIds]
1438
+ );
1439
+ const focusIndex = hoveredIndex ?? -1;
1440
+ const railSide = side ?? (dir === "rtl" ? "right" : "left");
1441
+ React.useEffect(() => {
1442
+ const root = layout?.rootElement ?? navRef.current?.parentElement;
1443
+ if (!root) return;
1444
+ const updateSideRoom = () => {
1445
+ const rootRect = root.getBoundingClientRect();
1446
+ const rootWidth = rootRect.width;
1447
+ const content2 = layout?.contentElement;
1448
+ const sideGutter = content2 ? Math.max(
1449
+ 0,
1450
+ railSide === "left" ? content2.getBoundingClientRect().left - rootRect.left : rootRect.right - content2.getBoundingClientRect().right
1451
+ ) : Math.max(0, (rootWidth - contentMaxWidth) / 2);
1452
+ const nextHasSideRoom = rootWidth >= minContainerWidthForRail && sideGutter >= minSideGutterForRail;
1453
+ setHasSideRoom(
1454
+ (current) => current === nextHasSideRoom ? current : nextHasSideRoom
1455
+ );
1456
+ };
1457
+ updateSideRoom();
1458
+ const resizeObserver = new ResizeObserver(updateSideRoom);
1459
+ resizeObserver.observe(root);
1460
+ const content = layout?.contentElement;
1461
+ if (content) {
1462
+ resizeObserver.observe(content);
1463
+ }
1464
+ return () => resizeObserver.disconnect();
1465
+ }, [
1466
+ contentMaxWidth,
1467
+ layout?.contentElement,
1468
+ layout?.rootElement,
1469
+ minContainerWidthForRail,
1470
+ minSideGutterForRail,
1471
+ railSide
1472
+ ]);
1473
+ if (items.length < 2) {
1474
+ return null;
1475
+ }
1476
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1477
+ "nav",
1478
+ {
1479
+ ref: navRef,
1480
+ "aria-label": "Transcript outline",
1481
+ className: cx(
1482
+ "pointer-events-none absolute top-[46%] z-10 max-h-[min(18rem,calc(100%-10rem))] w-16 -translate-y-1/2 overflow-visible",
1483
+ hasSideRoom ? "hidden md:block" : "hidden",
1484
+ railSide === "left" ? "left-1" : "right-1",
1485
+ className
1486
+ ),
1487
+ dir,
1488
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pointer-events-auto flex flex-col gap-0 py-1", children: items.map((item, index) => {
1489
+ const itemMessageIds = item.messageIds && item.messageIds.length > 0 ? item.messageIds : [item.id];
1490
+ const active = itemMessageIds.some(
1491
+ (messageId) => visibleMessageIdSet.has(messageId) || messageId === currentAnchorId
1492
+ );
1493
+ const distance = focusIndex >= 0 ? Math.abs(index - focusIndex) : Number.POSITIVE_INFINITY;
1494
+ const isFocused = distance === 0;
1495
+ const isNear = distance === 1;
1496
+ const isNextNear = distance === 2;
1497
+ const barWidth = isFocused ? 50 : isNear ? 36 : isNextNear ? 23 : 10;
1498
+ const barOpacity = isFocused ? 1 : isNear ? 0.78 : isNextNear ? 0.58 : 0.36;
1499
+ const fallbackLabel = `Turn ${index + 1}`;
1500
+ const previewLabel = item.label && item.label !== fallbackLabel ? item.label : "";
1501
+ const responsePreview = (item.responsePreview || "").trim();
1502
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1503
+ "button",
1504
+ {
1505
+ type: "button",
1506
+ "data-active": active,
1507
+ dir: "ltr",
1508
+ className: cx(
1509
+ "group relative flex h-2 w-16 items-center rounded-sm outline-none",
1510
+ railSide === "left" ? "justify-start" : "justify-end"
1511
+ ),
1512
+ "aria-label": `Jump to ${item.label || fallbackLabel}`,
1513
+ onBlur: () => setHoveredIndex(null),
1514
+ onFocus: () => setHoveredIndex(index),
1515
+ onMouseEnter: () => setHoveredIndex(index),
1516
+ onMouseLeave: () => setHoveredIndex(null),
1517
+ onClick: () => scrollToMessage(item.id, {
1518
+ align: "start",
1519
+ behavior: "smooth"
1520
+ }),
1521
+ children: [
1522
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1523
+ "span",
1524
+ {
1525
+ className: "h-0.5 rounded-full bg-muted-foreground/55 transition-[width,background-color,opacity] duration-150 ease-out group-hover:bg-foreground group-focus-visible:bg-foreground group-data-[active=true]:bg-foreground",
1526
+ style: {
1527
+ opacity: barOpacity,
1528
+ width: barWidth
1529
+ }
1530
+ }
1531
+ ),
1532
+ previewLabel ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1533
+ "span",
1534
+ {
1535
+ dir,
1536
+ className: cx(
1537
+ "pointer-events-none absolute top-1/2 z-20 max-h-32 w-64 -translate-y-1/2 overflow-hidden rounded-lg border border-border/80 bg-background p-3 text-start opacity-0 shadow-lg transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100 group-focus-visible:pointer-events-auto group-focus-visible:opacity-100",
1538
+ railSide === "left" ? "left-full ml-2 -translate-x-1 group-hover:translate-x-0 group-focus-visible:translate-x-0" : "right-full mr-2 translate-x-1 group-hover:translate-x-0 group-focus-visible:translate-x-0",
1539
+ cardClassName
1540
+ ),
1541
+ children: [
1542
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1543
+ "span",
1544
+ {
1545
+ className: "block overflow-hidden text-sm leading-5 text-foreground",
1546
+ style: {
1547
+ display: "-webkit-box",
1548
+ WebkitBoxOrient: "vertical",
1549
+ WebkitLineClamp: responsePreview ? 2 : 4
1550
+ },
1551
+ children: previewLabel
1552
+ }
1553
+ ),
1554
+ responsePreview ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1555
+ "span",
1556
+ {
1557
+ className: "mt-1 block overflow-hidden text-sm leading-5 text-muted-foreground",
1558
+ style: {
1559
+ display: "-webkit-box",
1560
+ WebkitBoxOrient: "vertical",
1561
+ WebkitLineClamp: 2
1562
+ },
1563
+ children: responsePreview
1564
+ }
1565
+ ) : null
1566
+ ]
1567
+ }
1568
+ ) : null
1569
+ ]
1570
+ },
1571
+ item.id
1572
+ );
1573
+ }) })
1574
+ }
1575
+ );
1576
+ }
1577
+ var LatestThreadScroller = {
1578
+ Provider: LatestThreadScrollerProvider,
1579
+ Root: LatestThreadScrollerRoot,
1580
+ Viewport: LatestThreadScrollerViewport,
1581
+ Content: LatestThreadScrollerContent,
1582
+ Item: LatestThreadScrollerItem,
1583
+ Button: LatestThreadScrollerButton,
1584
+ Outline: LatestThreadScrollerOutline
1585
+ };
1586
+
1587
+ // src/renderers.tsx
1588
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1013
1589
  var DefaultToolPart = ({ part }) => {
1014
1590
  const label = typeof part.presentation?.activity === "string" ? part.presentation.activity : typeof part.presentation?.title === "string" ? part.presentation.title : part.toolName;
1015
1591
  const detail = typeof part.presentation?.detail === "string" ? part.presentation.detail : part.errorText || part.state;
1016
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-agents24-tool-part": part.type, "data-state": part.state, children: [
1017
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: label }),
1018
- detail ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: detail }) : null
1592
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { "data-agents24-tool-part": part.type, "data-state": part.state, children: [
1593
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: label }),
1594
+ detail ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: detail }) : null
1019
1595
  ] });
1020
1596
  };
1021
1597
  var DefaultChatPart = ({
@@ -1024,28 +1600,28 @@ var DefaultChatPart = ({
1024
1600
  renderOptions
1025
1601
  }) => {
1026
1602
  if (renderOptions?.renderPart) {
1027
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: renderOptions.renderPart(part, message) });
1603
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: renderOptions.renderPart(part, message) });
1028
1604
  }
1029
- if (part.kind === "text") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: part.text });
1605
+ if (part.kind === "text") return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: part.text });
1030
1606
  if (part.kind === "tool") {
1031
1607
  const ToolRenderer = renderOptions?.toolRenderers?.[part.type] || renderOptions?.fallbackToolRenderer || DefaultToolPart;
1032
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: ToolRenderer({ part, message }) });
1608
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: ToolRenderer({ part, message }) });
1033
1609
  }
1034
1610
  if (part.kind === "reasoning") {
1035
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-agents24-reasoning-part": true, children: part.text || part.label });
1611
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-reasoning-part": true, children: part.text || part.label });
1036
1612
  }
1037
1613
  if (part.kind === "ui-blocks") {
1038
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-agents24-ui-blocks-part": true, "data-state": part.state });
1614
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-ui-blocks-part": true, "data-state": part.state });
1039
1615
  }
1040
1616
  if (part.kind === "approval") {
1041
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-agents24-approval-part": true });
1617
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-approval-part": true });
1042
1618
  }
1043
1619
  if (part.kind === "error") {
1044
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-agents24-error-part": true, children: part.errorText });
1620
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-error-part": true, children: part.errorText });
1045
1621
  }
1046
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-agents24-data-part": part.name });
1622
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-data-part": part.name });
1047
1623
  };
1048
- var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1624
+ var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1049
1625
  DefaultChatPart,
1050
1626
  {
1051
1627
  part,
@@ -1311,306 +1887,45 @@ var createFetchChatTransport = ({
1311
1887
  };
1312
1888
  };
1313
1889
 
1314
- // src/viewport.tsx
1315
- var import_react3 = require("react");
1316
- var import_jsx_runtime2 = require("react/jsx-runtime");
1317
- var LATEST_EDGE_THRESHOLD_PX = 2;
1318
- var MIN_OLDER_PREFETCH_PX = 320;
1319
- var MAX_OLDER_PREFETCH_PX = 900;
1320
- var isScrollable = (element) => element.scrollHeight - element.clientHeight > LATEST_EDGE_THRESHOLD_PX;
1321
- var isAtTimelineLatestEdge = (element, isTopOrigin) => {
1322
- if (!isScrollable(element)) return true;
1323
- return isTopOrigin ? element.scrollHeight - element.clientHeight - element.scrollTop <= LATEST_EDGE_THRESHOLD_PX : Math.abs(element.scrollTop) <= LATEST_EDGE_THRESHOLD_PX;
1324
- };
1325
- var shouldPrefetchOlder = (element) => Math.abs(element.scrollTop) + element.clientHeight >= element.scrollHeight - Math.min(MAX_OLDER_PREFETCH_PX, Math.max(MIN_OLDER_PREFETCH_PX, element.clientHeight * 0.75));
1326
- var getLatestScrollTop = (element, isTopOrigin) => isTopOrigin ? Math.max(0, element.scrollHeight - element.clientHeight) : 0;
1327
- var shouldUseTopOriginTimeline = ({
1328
- hasOlder,
1329
- itemCount,
1330
- topOriginMaxItems = 4
1331
- }) => !hasOlder && itemCount > 0 && itemCount <= topOriginMaxItems;
1332
- var nextLatestFollowStateOnScroll = ({
1333
- atLatest,
1334
- current,
1335
- isProgrammatic
1336
- }) => {
1337
- if (isProgrammatic) return current;
1338
- return atLatest ? "following" : "detached";
1339
- };
1340
- function useLatestThreadViewport({
1341
- itemCount,
1342
- hasOlder,
1343
- isLoadingOlder,
1344
- onLoadOlder,
1345
- activeStreamKey,
1346
- shouldAutoFollow = true,
1347
- topOriginMaxItems = 4
1348
- }) {
1349
- const scrollContainerRef = (0, import_react3.useRef)(null);
1350
- const olderPagePreserveRef = (0, import_react3.useRef)(null);
1351
- const olderPageRequestInFlightRef = (0, import_react3.useRef)(false);
1352
- const intrinsicResizePreserveRef = (0, import_react3.useRef)(null);
1353
- const followStateRef = (0, import_react3.useRef)("following");
1354
- const programmaticScrollRef = (0, import_react3.useRef)(false);
1355
- const programmaticScrollTimeoutRef = (0, import_react3.useRef)(null);
1356
- const activeStreamKeyRef = (0, import_react3.useRef)(null);
1357
- const observedScrollHeightRef = (0, import_react3.useRef)(null);
1358
- const [isAtLatest, setIsAtLatest] = (0, import_react3.useState)(true);
1359
- const [followState, setFollowState] = (0, import_react3.useState)("following");
1360
- const isTopOrigin = shouldUseTopOriginTimeline({ hasOlder, itemCount, topOriginMaxItems });
1361
- const setLatestFollowState = (0, import_react3.useCallback)((next) => {
1362
- if (followStateRef.current === next) return;
1363
- followStateRef.current = next;
1364
- setFollowState(next);
1365
- }, []);
1366
- const markProgrammaticScroll = (0, import_react3.useCallback)(() => {
1367
- programmaticScrollRef.current = true;
1368
- if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
1369
- programmaticScrollTimeoutRef.current = setTimeout(() => {
1370
- programmaticScrollRef.current = false;
1371
- programmaticScrollTimeoutRef.current = null;
1372
- }, 80);
1373
- }, []);
1374
- const scrollToLatest = (0, import_react3.useCallback)((options) => {
1375
- const element = scrollContainerRef.current;
1376
- if (!element) return;
1377
- if (options?.reattach !== false) setLatestFollowState("following");
1378
- markProgrammaticScroll();
1379
- element.scrollTop = getLatestScrollTop(element, isTopOrigin);
1380
- setIsAtLatest(true);
1381
- observedScrollHeightRef.current = element.scrollHeight;
1382
- }, [isTopOrigin, markProgrammaticScroll, setLatestFollowState]);
1383
- const detachFromLatest = (0, import_react3.useCallback)(() => {
1384
- setLatestFollowState("detached");
1385
- }, [setLatestFollowState]);
1386
- const reattachToLatest = (0, import_react3.useCallback)(() => {
1387
- scrollToLatest({ reattach: true });
1388
- }, [scrollToLatest]);
1389
- const handleUserScrollIntent = (0, import_react3.useCallback)(() => {
1390
- const element = scrollContainerRef.current;
1391
- if (!activeStreamKey || !element) return;
1392
- if (!isAtTimelineLatestEdge(element, isTopOrigin)) detachFromLatest();
1393
- }, [activeStreamKey, detachFromLatest, isTopOrigin]);
1394
- (0, import_react3.useEffect)(() => {
1395
- return () => {
1396
- if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
1397
- const preserve = intrinsicResizePreserveRef.current;
1398
- if (preserve?.frame !== null && preserve?.frame !== void 0) cancelAnimationFrame(preserve.frame);
1399
- };
1400
- }, []);
1401
- (0, import_react3.useLayoutEffect)(() => {
1402
- const element = scrollContainerRef.current;
1403
- const preserve = olderPagePreserveRef.current;
1404
- if (!element) return;
1405
- observedScrollHeightRef.current = element.scrollHeight;
1406
- if (!preserve) {
1407
- setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
1408
- return;
1409
- }
1410
- const addedHeight = element.scrollHeight - preserve.previousHeight;
1411
- markProgrammaticScroll();
1412
- element.scrollTop = preserve.previousTop - addedHeight;
1413
- olderPagePreserveRef.current = null;
1414
- olderPageRequestInFlightRef.current = false;
1415
- setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
1416
- }, [isTopOrigin, itemCount, markProgrammaticScroll]);
1417
- const handleScroll = (0, import_react3.useCallback)(
1418
- (event) => {
1419
- const element = event.currentTarget;
1420
- const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
1421
- setIsAtLatest(atLatest);
1422
- setLatestFollowState(
1423
- nextLatestFollowStateOnScroll({
1424
- atLatest,
1425
- current: followStateRef.current,
1426
- isProgrammatic: programmaticScrollRef.current
1427
- })
1428
- );
1429
- if (isTopOrigin || !hasOlder || isLoadingOlder || olderPageRequestInFlightRef.current || !shouldPrefetchOlder(element)) {
1430
- return;
1431
- }
1432
- olderPageRequestInFlightRef.current = true;
1433
- olderPagePreserveRef.current = {
1434
- previousHeight: element.scrollHeight,
1435
- previousTop: element.scrollTop
1436
- };
1437
- void Promise.resolve(onLoadOlder()).finally(() => {
1438
- requestAnimationFrame(() => {
1439
- if (!olderPagePreserveRef.current) return;
1440
- olderPagePreserveRef.current = null;
1441
- olderPageRequestInFlightRef.current = false;
1442
- });
1443
- });
1444
- },
1445
- [hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder, setLatestFollowState]
1446
- );
1447
- const preserveIntrinsicResize = (0, import_react3.useCallback)(() => {
1448
- const element = scrollContainerRef.current;
1449
- if (!element || isTopOrigin) return;
1450
- const active = intrinsicResizePreserveRef.current;
1451
- if (active?.frame !== null && active?.frame !== void 0) cancelAnimationFrame(active.frame);
1452
- intrinsicResizePreserveRef.current = {
1453
- lastHeight: element.scrollHeight,
1454
- endAt: Date.now() + 260,
1455
- frame: null
1456
- };
1457
- const preserveFrame = () => {
1458
- const nextElement = scrollContainerRef.current;
1459
- const preserve = intrinsicResizePreserveRef.current;
1460
- if (!nextElement || !preserve) return;
1461
- const heightDelta = nextElement.scrollHeight - preserve.lastHeight;
1462
- if (Math.abs(heightDelta) >= 1) {
1463
- markProgrammaticScroll();
1464
- nextElement.scrollTop -= heightDelta;
1465
- preserve.lastHeight = nextElement.scrollHeight;
1466
- setIsAtLatest(isAtTimelineLatestEdge(nextElement, isTopOrigin));
1467
- }
1468
- if (Date.now() < preserve.endAt) {
1469
- preserve.frame = requestAnimationFrame(preserveFrame);
1470
- } else {
1471
- intrinsicResizePreserveRef.current = null;
1472
- }
1473
- };
1474
- intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
1475
- }, [isTopOrigin, markProgrammaticScroll]);
1476
- (0, import_react3.useLayoutEffect)(() => {
1477
- const key = activeStreamKey || null;
1478
- const previousKey = activeStreamKeyRef.current;
1479
- if (!key) {
1480
- activeStreamKeyRef.current = null;
1481
- if (previousKey && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1482
- const frame2 = requestAnimationFrame(() => {
1483
- if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1484
- scrollToLatest({ reattach: false });
1485
- }
1486
- });
1487
- return () => cancelAnimationFrame(frame2);
1488
- }
1489
- return;
1490
- }
1491
- if (previousKey === key) {
1492
- activeStreamKeyRef.current = key;
1493
- return;
1494
- }
1495
- setLatestFollowState("following");
1496
- const frame = requestAnimationFrame(() => scrollToLatest());
1497
- activeStreamKeyRef.current = key;
1498
- return () => cancelAnimationFrame(frame);
1499
- }, [activeStreamKey, isLoadingOlder, scrollToLatest, setLatestFollowState, shouldAutoFollow]);
1500
- (0, import_react3.useLayoutEffect)(() => {
1501
- if (!activeStreamKey || !shouldAutoFollow || followStateRef.current !== "following") return;
1502
- if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
1503
- const frame = requestAnimationFrame(() => {
1504
- if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1505
- scrollToLatest({ reattach: false });
1506
- }
1507
- });
1508
- return () => cancelAnimationFrame(frame);
1509
- }, [activeStreamKey, isLoadingOlder, itemCount, scrollToLatest, shouldAutoFollow]);
1510
- (0, import_react3.useEffect)(() => {
1511
- if (!activeStreamKey) {
1512
- observedScrollHeightRef.current = scrollContainerRef.current?.scrollHeight ?? null;
1513
- return;
1514
- }
1515
- let frame = null;
1516
- const watchStreamResize = () => {
1517
- const element = scrollContainerRef.current;
1518
- if (!element) return;
1519
- const previousHeight = observedScrollHeightRef.current;
1520
- const nextHeight = element.scrollHeight;
1521
- observedScrollHeightRef.current = nextHeight;
1522
- if (previousHeight !== null && Math.abs(nextHeight - previousHeight) >= 1 && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1523
- scrollToLatest({ reattach: false });
1524
- }
1525
- frame = requestAnimationFrame(watchStreamResize);
1526
- };
1527
- frame = requestAnimationFrame(watchStreamResize);
1528
- return () => {
1529
- if (frame !== null) cancelAnimationFrame(frame);
1530
- };
1531
- }, [activeStreamKey, isLoadingOlder, scrollToLatest, shouldAutoFollow]);
1532
- return {
1533
- scrollContainerRef,
1534
- isAtLatest,
1535
- isFollowingLatest: followState === "following",
1536
- isTopOrigin,
1537
- shouldShowLatestButton: !isAtLatest,
1538
- handleScroll,
1539
- handleUserScrollIntent,
1540
- scrollToLatest,
1541
- detachFromLatest,
1542
- reattachToLatest,
1543
- preserveIntrinsicResize
1544
- };
1545
- }
1546
- function LatestThreadViewport({
1547
- items,
1548
- hasOlder,
1549
- isLoadingOlder,
1550
- onLoadOlder,
1551
- activeStreamKey,
1552
- className,
1553
- latestSpacer,
1554
- trailingSpacer,
1555
- renderItem
1556
- }) {
1557
- const viewport = useLatestThreadViewport({
1558
- itemCount: items.length,
1559
- hasOlder,
1560
- isLoadingOlder,
1561
- onLoadOlder,
1562
- activeStreamKey
1563
- });
1564
- const timelineItems = (0, import_react3.useMemo)(() => viewport.isTopOrigin ? items : [...items].reverse(), [items, viewport.isTopOrigin]);
1565
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1566
- "div",
1567
- {
1568
- ref: viewport.scrollContainerRef,
1569
- className,
1570
- onKeyDown: viewport.handleUserScrollIntent,
1571
- onScroll: viewport.handleScroll,
1572
- onTouchMove: viewport.handleUserScrollIntent,
1573
- onWheel: viewport.handleUserScrollIntent,
1574
- role: "log",
1575
- style: { overflowAnchor: "none" },
1576
- children: [
1577
- !viewport.isTopOrigin && (latestSpacer ?? null),
1578
- timelineItems.map((item) => renderItem(item, viewport)),
1579
- viewport.isTopOrigin && (trailingSpacer ?? latestSpacer ?? null)
1580
- ]
1581
- }
1582
- );
1583
- }
1890
+ // src/viewport.ts
1891
+ var import_message_scroller2 = require("@shadcn/react/message-scroller");
1584
1892
  // Annotate the CommonJS export names for ESM import in node:
1585
1893
  0 && (module.exports = {
1586
1894
  DEFAULT_THREAD_PAGE_SIZE,
1587
1895
  DefaultChatPart,
1588
1896
  DefaultToolPart,
1589
- LatestThreadViewport,
1897
+ LatestThreadScroller,
1898
+ LatestThreadScrollerButton,
1899
+ LatestThreadScrollerContent,
1900
+ LatestThreadScrollerItem,
1901
+ LatestThreadScrollerOutline,
1902
+ LatestThreadScrollerProvider,
1903
+ LatestThreadScrollerRoot,
1904
+ LatestThreadScrollerViewport,
1905
+ MessageScroller,
1590
1906
  activeRunIdFromThread,
1591
1907
  activeRunIdFromThreadDetail,
1592
1908
  assistantTextFromParts,
1593
1909
  assistantTextFromResponseBlocks,
1910
+ compressionFromContextWindow,
1594
1911
  consumeSseResponse,
1595
1912
  createChatId,
1596
1913
  createFetchChatTransport,
1597
1914
  getActiveStreamingTextPartId,
1598
- getLatestScrollTop,
1599
1915
  hasStaleUnfinishedAssistantCache,
1600
1916
  hasUnfinishedAssistantMessage,
1601
1917
  isActiveStreamingTextPart,
1602
- isAtTimelineLatestEdge,
1603
1918
  isRunningThreadStatus,
1604
- isScrollable,
1605
1919
  latestContextWindowFromThread,
1920
+ mergeContextWindow,
1921
+ mergeContextWindowUpdate,
1606
1922
  mergeReasoningSteps,
1607
- nextLatestFollowStateOnScroll,
1923
+ normalizeContextCompression,
1924
+ normalizeContextWindow,
1608
1925
  parseSseBlock,
1609
1926
  partsFromResponseBlocks,
1610
1927
  reasoningStepsFromParts,
1611
1928
  renderChatPart,
1612
- shouldPrefetchOlder,
1613
- shouldUseTopOriginTimeline,
1614
1929
  textFromFinalOutput,
1615
1930
  threadActivityDate,
1616
1931
  threadDetailToMessages,
@@ -1618,7 +1933,9 @@ function LatestThreadViewport({
1618
1933
  titleFromMessage,
1619
1934
  toolStateFromStatus,
1620
1935
  useAgents24ChatController,
1621
- useLatestThreadViewport,
1936
+ useMessageScroller,
1937
+ useMessageScrollerScrollable,
1938
+ useMessageScrollerVisibility,
1622
1939
  useStreamingText
1623
1940
  });
1624
1941
  //# sourceMappingURL=index.cjs.map