@agents24/chat-react 0.1.6 → 0.1.7

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.
package/dist/index.js CHANGED
@@ -1,9 +1,128 @@
1
1
  // src/controller.ts
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
 
4
+ // src/context-window.ts
5
+ var SOURCE_PRIORITY = {
6
+ unknown: 0,
7
+ heuristic_estimate: 1,
8
+ tokenizer_estimate: 1,
9
+ text_estimate: 1,
10
+ estimated: 1,
11
+ multimodal_estimate: 2,
12
+ hf_tokenizer: 3,
13
+ runtime_tokenizer: 3,
14
+ provider_count_api: 4,
15
+ provider_usage: 5,
16
+ exact: 5
17
+ };
18
+ var STAGE_PRIORITY = {
19
+ preflight: 0,
20
+ sent_prompt: 1,
21
+ final_usage: 2
22
+ };
23
+ function numberOrNull(value) {
24
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
25
+ }
26
+ function stagePriority(window2) {
27
+ return STAGE_PRIORITY[window2?.stage || "sent_prompt"] ?? STAGE_PRIORITY.sent_prompt;
28
+ }
29
+ function hasRenderableContextWindow(window2) {
30
+ const maxTokens = window2?.max_tokens;
31
+ return typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0;
32
+ }
33
+ function windowWeight(window2) {
34
+ if (!window2) return [0, 0];
35
+ return [stagePriority(window2), SOURCE_PRIORITY[window2.source] || 0];
36
+ }
37
+ function normalizeContextCompression(value) {
38
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
39
+ const payload = value;
40
+ return {
41
+ active: Boolean(payload.active),
42
+ reason: typeof payload.reason === "string" ? payload.reason : null,
43
+ input_tokens: numberOrNull(payload.input_tokens),
44
+ max_tokens: numberOrNull(payload.max_tokens),
45
+ usage_ratio: numberOrNull(payload.usage_ratio),
46
+ full_frame_count: numberOrNull(payload.full_frame_count),
47
+ compact_frame_count: numberOrNull(payload.compact_frame_count),
48
+ dropped_frame_count: numberOrNull(payload.dropped_frame_count),
49
+ artifact_ref_count: numberOrNull(payload.artifact_ref_count),
50
+ compression_trigger_budget: numberOrNull(payload.compression_trigger_budget),
51
+ compression_target_budget: numberOrNull(payload.compression_target_budget),
52
+ compression_target_ratio: numberOrNull(payload.compression_target_ratio),
53
+ threshold_used: numberOrNull(payload.threshold_used)
54
+ };
55
+ }
56
+ function normalizeContextWindow(value) {
57
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
58
+ const payload = value;
59
+ const rawSource = String(payload.source || "").trim();
60
+ 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";
61
+ const rawStage = String(payload.stage || "").trim();
62
+ const stage = rawStage === "preflight" || rawStage === "sent_prompt" || rawStage === "final_usage" ? rawStage : "sent_prompt";
63
+ const rawConfidence = String(payload.confidence || "").trim();
64
+ const confidence = rawConfidence === "exact" || rawConfidence === "high" || rawConfidence === "medium" || rawConfidence === "low" || rawConfidence === "unknown" ? rawConfidence : null;
65
+ return {
66
+ source,
67
+ run_id: typeof payload.run_id === "string" ? payload.run_id : null,
68
+ stage,
69
+ confidence,
70
+ counter: typeof payload.counter === "string" ? payload.counter : null,
71
+ model_id: typeof payload.model_id === "string" ? payload.model_id : null,
72
+ max_tokens: numberOrNull(payload.max_tokens),
73
+ max_tokens_source: typeof payload.max_tokens_source === "string" ? payload.max_tokens_source : null,
74
+ input_tokens: numberOrNull(payload.input_tokens),
75
+ remaining_tokens: numberOrNull(payload.remaining_tokens),
76
+ usage_ratio: numberOrNull(payload.usage_ratio),
77
+ assembly: payload.assembly && typeof payload.assembly === "object" && !Array.isArray(payload.assembly) ? payload.assembly : null,
78
+ context_compression: normalizeContextCompression(payload.context_compression)
79
+ };
80
+ }
81
+ function mergeContextWindow(current, incoming) {
82
+ if (!incoming) return current ?? null;
83
+ if (!current) return incoming;
84
+ if (hasRenderableContextWindow(current) && !hasRenderableContextWindow(incoming)) return current;
85
+ const currentRunId = current.run_id || null;
86
+ const incomingRunId = incoming.run_id || null;
87
+ if (incomingRunId && currentRunId && incomingRunId !== currentRunId) {
88
+ return stagePriority(incoming) >= STAGE_PRIORITY.sent_prompt ? incoming : current;
89
+ }
90
+ const currentWeight = windowWeight(current);
91
+ const incomingWeight = windowWeight(incoming);
92
+ return incomingWeight[0] > currentWeight[0] || incomingWeight[0] === currentWeight[0] && incomingWeight[1] >= currentWeight[1] ? incoming : current;
93
+ }
94
+ function mergeContextWindowUpdate(current, incoming) {
95
+ return mergeContextWindow(current, normalizeContextWindow(incoming));
96
+ }
97
+ function compressionFromContextWindow(contextWindow) {
98
+ if (!contextWindow) return null;
99
+ if (contextWindow.context_compression) return contextWindow.context_compression;
100
+ const assembly = contextWindow.assembly;
101
+ if (!assembly || typeof assembly !== "object" || Array.isArray(assembly)) return null;
102
+ const artifactRefs = Array.isArray(assembly.artifact_refs) ? assembly.artifact_refs : [];
103
+ const compactFrameCount = numberOrNull(assembly.compact_frame_count) ?? 0;
104
+ const droppedFrameCount = numberOrNull(assembly.dropped_frame_count) ?? 0;
105
+ const reason = typeof assembly.compaction_reason === "string" ? assembly.compaction_reason : "none";
106
+ return {
107
+ active: reason !== "none" || compactFrameCount > 0 || droppedFrameCount > 0,
108
+ reason,
109
+ input_tokens: contextWindow.input_tokens ?? null,
110
+ max_tokens: contextWindow.max_tokens ?? null,
111
+ usage_ratio: contextWindow.usage_ratio ?? null,
112
+ full_frame_count: numberOrNull(assembly.full_frame_count),
113
+ compact_frame_count: compactFrameCount,
114
+ dropped_frame_count: droppedFrameCount,
115
+ artifact_ref_count: artifactRefs.length,
116
+ compression_trigger_budget: numberOrNull(assembly.compression_trigger_budget),
117
+ compression_target_budget: numberOrNull(assembly.compression_target_budget),
118
+ compression_target_ratio: numberOrNull(assembly.compression_target_ratio),
119
+ threshold_used: numberOrNull(assembly.threshold_used)
120
+ };
121
+ }
122
+
4
123
  // src/model.ts
5
124
  var DEFAULT_THREAD_PAGE_SIZE = 5;
6
- var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running"])).has(String(status || "").toLowerCase());
125
+ var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
7
126
  var createChatId = () => {
8
127
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
9
128
  return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
@@ -42,6 +161,16 @@ var attachmentsFromTurn = (turn) => (turn.attachments || []).map((attachment, in
42
161
  mediaType: String(record.mime_type || record.mediaType || record.type || "application/octet-stream")
43
162
  };
44
163
  });
164
+ var tokenUsageFromTurn = (turn) => {
165
+ const usage = turn.run_usage;
166
+ if (!usage || typeof usage !== "object") return void 0;
167
+ return {
168
+ inputTokens: usage.input_tokens ?? null,
169
+ outputTokens: usage.output_tokens ?? null,
170
+ totalTokens: usage.total_tokens ?? null,
171
+ usageSource: usage.source ?? null
172
+ };
173
+ };
45
174
  var latestEventPayloadValue = (events, key) => {
46
175
  for (let index = (events || []).length - 1; index >= 0; index -= 1) {
47
176
  const payload = events?.[index]?.payload;
@@ -243,7 +372,8 @@ var turnToMessages = (turn, activeRunId) => {
243
372
  createdAt: turn.completed_at ? new Date(turn.completed_at) : createdAt,
244
373
  parts,
245
374
  reasoningSteps: mergeReasoningSteps(reasoningStepsFromParts(parts), { finalize: !isRunning }),
246
- isFinal: !isRunning
375
+ isFinal: !isRunning,
376
+ tokenUsage: tokenUsageFromTurn(turn)
247
377
  });
248
378
  }
249
379
  return messages;
@@ -260,10 +390,11 @@ var threadPaging = (thread) => {
260
390
  };
261
391
  };
262
392
  var latestContextWindowFromThread = (thread) => {
263
- if (thread.context_window && typeof thread.context_window === "object") return thread.context_window;
393
+ const threadContextWindow = normalizeContextWindow(thread.context_window);
394
+ if (threadContextWindow) return threadContextWindow;
264
395
  for (let index = (thread.turns || []).length - 1; index >= 0; index -= 1) {
265
- const value = thread.turns?.[index]?.context_window;
266
- if (value && typeof value === "object") return value;
396
+ const contextWindow = normalizeContextWindow(thread.turns?.[index]?.context_window);
397
+ if (contextWindow) return contextWindow;
267
398
  }
268
399
  return null;
269
400
  };
@@ -294,10 +425,6 @@ var activeRunIdFromThreadDetail = (thread) => {
294
425
  };
295
426
 
296
427
  // src/controller.ts
297
- var mergeContextWindow = (current, incoming) => {
298
- if (!incoming || typeof incoming !== "object") return current;
299
- return { ...current || {}, ...incoming };
300
- };
301
428
  var threadSummaryToStored = (thread) => ({
302
429
  ...thread,
303
430
  id: String(thread.id),
@@ -315,7 +442,9 @@ function useAgents24ChatController({
315
442
  createId = createChatId,
316
443
  onActiveThreadIdChange,
317
444
  onSourceClick,
318
- onStreamErrorMessage
445
+ onStreamErrorMessage,
446
+ onRuntimeEvent,
447
+ onThreadDetailLoaded
319
448
  }) {
320
449
  const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
321
450
  const initialCached = activeThreadId ? storage.getThread(activeThreadId) : void 0;
@@ -332,6 +461,9 @@ function useAgents24ChatController({
332
461
  const [disliked, setDisliked] = useState({});
333
462
  const [copiedMessageId, setCopiedMessageId] = useState(null);
334
463
  const [lastThinkingDurationMs, setLastThinkingDurationMs] = useState(null);
464
+ const [threads, setThreads] = useState(() => storage.listThreads());
465
+ const [isRefreshingThreads, setIsRefreshingThreads] = useState(false);
466
+ const [activeRunId, setActiveRunId] = useState(null);
335
467
  const textareaRef = useRef(null);
336
468
  const activeThreadIdRef = useRef(activeThreadId);
337
469
  const messagesRef = useRef(messages);
@@ -348,6 +480,20 @@ function useAgents24ChatController({
348
480
  const streamingMessageIdRef = useRef(null);
349
481
  const reasoningRef = useRef([]);
350
482
  const liveVoiceIdsRef = useRef({});
483
+ const setActiveRunIdValue = useCallback((runId) => {
484
+ activeRunIdRef.current = runId;
485
+ setActiveRunId(runId);
486
+ }, []);
487
+ const syncThreadsFromStorage = useCallback(() => {
488
+ setThreads(storage.listThreads());
489
+ }, [storage]);
490
+ const upsertStoredThread = useCallback(
491
+ (thread) => {
492
+ storage.upsertThread(thread);
493
+ syncThreadsFromStorage();
494
+ },
495
+ [storage, syncThreadsFromStorage]
496
+ );
351
497
  useEffect(() => {
352
498
  messagesRef.current = messages;
353
499
  }, [messages]);
@@ -355,7 +501,7 @@ function useAgents24ChatController({
355
501
  (threadId, nextMessages, paging, options) => {
356
502
  const existing = storage.getThread(threadId);
357
503
  const firstUser = nextMessages.find((message) => message.role === "user");
358
- storage.upsertThread({
504
+ upsertStoredThread({
359
505
  ...existing || {},
360
506
  id: threadId,
361
507
  title: existing?.title || (firstUser ? titleFromMessage(firstUser.content, firstUser.attachments || []) : "New chat"),
@@ -366,13 +512,13 @@ function useAgents24ChatController({
366
512
  nextBeforeTurnIndex: paging?.nextBeforeTurnIndex ?? nextBeforeTurnIndexRef.current
367
513
  });
368
514
  },
369
- [storage]
515
+ [storage, upsertStoredThread]
370
516
  );
371
517
  const markThreadRunStatus = useCallback(
372
518
  (threadId, runId, status, lastEventSeq) => {
373
519
  const existing = storage.getThread(threadId);
374
520
  if (!existing || !runId) return;
375
- storage.upsertThread({
521
+ upsertStoredThread({
376
522
  ...existing,
377
523
  last_run_id: runId,
378
524
  last_run_status: status,
@@ -389,14 +535,25 @@ function useAgents24ChatController({
389
535
  created_at: existing.activeRun?.created_at ?? existing.active_run?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
390
536
  },
391
537
  lastEventSeq: typeof lastEventSeq === "number" ? lastEventSeq : existing.lastEventSeq ?? null,
392
- isRunning: status === "queued" || status === "running"
538
+ isRunning: activeRunIdFromThread({
539
+ lastRunId: runId,
540
+ lastRunStatus: status,
541
+ activeRun: { run_id: runId, status }
542
+ }) !== null
393
543
  });
394
544
  },
395
- [storage]
545
+ [storage, upsertStoredThread]
396
546
  );
397
547
  const refresh = useCallback(async () => {
398
- const data = await transport.listThreads();
399
- storage.setThreads((data.items || []).map((item) => threadSummaryToStored(item)));
548
+ setIsRefreshingThreads(true);
549
+ try {
550
+ const data = await transport.listThreads();
551
+ const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
552
+ storage.setThreads(nextThreads);
553
+ setThreads(storage.listThreads());
554
+ } finally {
555
+ setIsRefreshingThreads(false);
556
+ }
400
557
  }, [storage, transport]);
401
558
  const applyThreadId = useCallback(
402
559
  (threadId, baseMessages) => {
@@ -424,7 +581,7 @@ function useAgents24ChatController({
424
581
  const controller = abortControllerRef.current;
425
582
  abortControllerRef.current = null;
426
583
  controller?.abort();
427
- activeRunIdRef.current = null;
584
+ setActiveRunIdValue(null);
428
585
  reattachedRunIdRef.current = null;
429
586
  streamingMessageIdRef.current = null;
430
587
  streamingContentRef.current = "";
@@ -433,7 +590,7 @@ function useAgents24ChatController({
433
590
  setIsLoading(false);
434
591
  setStreamingContent("");
435
592
  setCurrentReasoning([]);
436
- }, []);
593
+ }, [setActiveRunIdValue]);
437
594
  const setLiveAssistantMessage = useCallback(
438
595
  (input) => {
439
596
  setMessages((prev) => {
@@ -503,7 +660,7 @@ function useAgents24ChatController({
503
660
  persistThread(input.threadId, completed);
504
661
  const existingThread = storage.getThread(input.threadId);
505
662
  if (existingThread && input.runId) {
506
- storage.upsertThread({
663
+ upsertStoredThread({
507
664
  ...existingThread,
508
665
  messages: completed,
509
666
  last_run_id: input.runId,
@@ -518,7 +675,7 @@ function useAgents24ChatController({
518
675
  }
519
676
  return completed;
520
677
  },
521
- [createId, persistThread, storage]
678
+ [createId, persistThread, storage, upsertStoredThread]
522
679
  );
523
680
  const loadThread = useCallback(
524
681
  async (threadId) => {
@@ -533,6 +690,7 @@ function useAgents24ChatController({
533
690
  includeRunEvents: false
534
691
  });
535
692
  if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
693
+ void onThreadDetailLoaded?.(detail);
536
694
  const nextMessages = threadDetailToMessages(detail);
537
695
  const paging = threadPaging(detail);
538
696
  setContextStatus(latestContextWindowFromThread(detail));
@@ -542,7 +700,7 @@ function useAgents24ChatController({
542
700
  hasOlderTurnsRef.current = paging.hasOlderTurns;
543
701
  nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
544
702
  loadedThreadIdRef.current = threadId;
545
- storage.upsertThread({
703
+ upsertStoredThread({
546
704
  ...threadSummaryToStored(detail),
547
705
  messages: nextMessages,
548
706
  isHydrated: true,
@@ -554,7 +712,7 @@ function useAgents24ChatController({
554
712
  if (activeThreadIdRef.current === threadId) setLoadingHistory(false);
555
713
  }
556
714
  },
557
- [pageSize, setLoadingHistory, storage, transport]
715
+ [onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
558
716
  );
559
717
  const loadOlderTurns = useCallback(async () => {
560
718
  const threadId = activeThreadIdRef.current;
@@ -586,11 +744,18 @@ function useAgents24ChatController({
586
744
  }, [pageSize, persistThread, transport]);
587
745
  const handleStreamEvent = useCallback(
588
746
  (input) => {
589
- const { event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
747
+ const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
590
748
  const payload = event.payload || {};
591
749
  const responseBlocks = Array.isArray(payload.response_blocks) ? payload.response_blocks : null;
592
- if (event.run_id) activeRunIdRef.current = event.run_id;
593
- setContextStatus((current) => mergeContextWindow(current, payload.context_window));
750
+ if (event.run_id) setActiveRunIdValue(event.run_id);
751
+ void onRuntimeEvent?.(event, {
752
+ mode,
753
+ assistantMessageId,
754
+ threadId: streamThreadIdRef.current,
755
+ runId: event.run_id || activeRunIdRef.current,
756
+ startedAt
757
+ });
758
+ setContextStatus((current) => mergeContextWindowUpdate(current, payload.context_window));
594
759
  if (responseBlocks) {
595
760
  const blockText = String(payload.assistant_output_text || "") || assistantTextFromResponseBlocks(responseBlocks) || streamingContentRef.current;
596
761
  if (blockText) setStreamingText(blockText);
@@ -636,7 +801,7 @@ function useAgents24ChatController({
636
801
  error: isFailed ? String(payload.message || payload.error || event.diagnostics?.[0]?.message || onStreamErrorMessage?.(event) || "The chat run failed.") : void 0
637
802
  });
638
803
  },
639
- [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onStreamErrorMessage, setLiveAssistantMessage, setReasoningSteps]
804
+ [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
640
805
  );
641
806
  const runStream = useCallback(
642
807
  async (input) => {
@@ -698,16 +863,16 @@ function useAgents24ChatController({
698
863
  }
699
864
  try {
700
865
  if (input.mode === "attach") {
701
- activeRunIdRef.current = input.runId;
866
+ setActiveRunIdValue(input.runId);
702
867
  reattachedRunIdRef.current = input.runId;
703
868
  await transport.attachRun(
704
869
  { runId: input.runId, signal: controller.signal },
705
- (event) => handleStreamEvent({ event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
870
+ (event) => handleStreamEvent({ mode: "attach", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
706
871
  );
707
872
  } else {
708
873
  await transport.streamMessage(
709
- { text: input.message.text, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
710
- (event) => handleStreamEvent({ event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
874
+ { ...input.message, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
875
+ (event) => handleStreamEvent({ mode: "submit", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
711
876
  );
712
877
  }
713
878
  if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
@@ -739,7 +904,7 @@ function useAgents24ChatController({
739
904
  const isCurrentStream = abortControllerRef.current === controller || streamingMessageIdRef.current === assistantMessageId;
740
905
  if (abortControllerRef.current === controller) abortControllerRef.current = null;
741
906
  if (isCurrentStream) {
742
- activeRunIdRef.current = null;
907
+ setActiveRunIdValue(null);
743
908
  streamingMessageIdRef.current = null;
744
909
  reattachedRunIdRef.current = null;
745
910
  setStreamingMessageId(null);
@@ -750,7 +915,7 @@ function useAgents24ChatController({
750
915
  }
751
916
  }
752
917
  },
753
- [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setReasoningSteps, transport]
918
+ [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
754
919
  );
755
920
  const handleSubmit = useCallback(
756
921
  async (message) => {
@@ -765,7 +930,7 @@ function useAgents24ChatController({
765
930
  const liveMessageId = streamingMessageIdRef.current;
766
931
  abortControllerRef.current?.abort();
767
932
  abortControllerRef.current = null;
768
- activeRunIdRef.current = null;
933
+ setActiveRunIdValue(null);
769
934
  streamingMessageIdRef.current = null;
770
935
  setStreamingMessageId(null);
771
936
  setIsLoading(false);
@@ -782,10 +947,16 @@ function useAgents24ChatController({
782
947
  messageId: liveMessageId
783
948
  });
784
949
  }
785
- }, [finalizeAssistantMessage, lastThinkingDurationMs, setReasoningSteps, transport]);
950
+ }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
786
951
  useEffect(() => {
787
- refresh().catch(() => storage.setThreads([]));
952
+ refresh().catch(() => {
953
+ storage.setThreads([]);
954
+ setThreads([]);
955
+ });
788
956
  }, [refresh, storage]);
957
+ useEffect(() => {
958
+ syncThreadsFromStorage();
959
+ }, [storageKey, syncThreadsFromStorage]);
789
960
  useEffect(() => {
790
961
  const previous = activeThreadIdRef.current;
791
962
  activeThreadIdRef.current = activeThreadId;
@@ -893,7 +1064,40 @@ function useAgents24ChatController({
893
1064
  return next;
894
1065
  });
895
1066
  }, [createId, persistThread]);
1067
+ const startNewThread = useCallback(() => {
1068
+ detachActiveStream();
1069
+ requestSeqRef.current += 1;
1070
+ activeThreadIdRef.current = null;
1071
+ loadedThreadIdRef.current = null;
1072
+ nextBeforeTurnIndexRef.current = null;
1073
+ hasOlderTurnsRef.current = false;
1074
+ storage.setActiveThreadId?.(null);
1075
+ onActiveThreadIdChange?.(null);
1076
+ setMessages([]);
1077
+ messagesRef.current = [];
1078
+ setHasOlderTurns(false);
1079
+ setIsLoadingOlder(false);
1080
+ setContextStatus(null);
1081
+ setLoadingHistory(false);
1082
+ }, [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage]);
1083
+ const loadThreadById = useCallback(
1084
+ async (threadId) => {
1085
+ if (!threadId) return;
1086
+ if (activeThreadIdRef.current !== threadId) {
1087
+ if (abortControllerRef.current) detachActiveStream();
1088
+ activeThreadIdRef.current = threadId;
1089
+ storage.setActiveThreadId?.(threadId);
1090
+ onActiveThreadIdChange?.(threadId);
1091
+ }
1092
+ await loadThread(threadId);
1093
+ },
1094
+ [detachActiveStream, loadThread, onActiveThreadIdChange, storage]
1095
+ );
1096
+ const activeThread = activeThreadId ? storage.getThread(activeThreadId) || threads.find((thread) => thread.id === activeThreadId) || null : null;
896
1097
  return useMemo(() => ({
1098
+ threads,
1099
+ activeThreadId,
1100
+ activeThread,
897
1101
  messages,
898
1102
  streamingContent,
899
1103
  streamingMessageId,
@@ -902,12 +1106,13 @@ function useAgents24ChatController({
902
1106
  isLoading,
903
1107
  isLoadingHistory,
904
1108
  isLoadingOlder,
1109
+ isRefreshingThreads,
905
1110
  hasOlderTurns,
906
1111
  liked,
907
1112
  disliked,
908
1113
  copiedMessageId,
909
1114
  lastThinkingDurationMs,
910
- activeRunId: activeRunIdRef.current,
1115
+ activeRunId,
911
1116
  handleSubmit,
912
1117
  handleStop,
913
1118
  handleCopy,
@@ -916,11 +1121,16 @@ function useAgents24ChatController({
916
1121
  handleRetry,
917
1122
  handleSourceClick: (citations) => onSourceClick?.(citations),
918
1123
  upsertLiveVoiceMessage,
1124
+ startNewThread,
1125
+ loadThreadById,
919
1126
  loadOlderTurns,
920
1127
  refresh,
921
1128
  textareaRef
922
1129
  }), [
923
1130
  copiedMessageId,
1131
+ activeRunId,
1132
+ activeThread,
1133
+ activeThreadId,
924
1134
  contextStatus,
925
1135
  currentReasoning,
926
1136
  disliked,
@@ -934,14 +1144,18 @@ function useAgents24ChatController({
934
1144
  isLoading,
935
1145
  isLoadingHistory,
936
1146
  isLoadingOlder,
1147
+ isRefreshingThreads,
937
1148
  lastThinkingDurationMs,
938
1149
  liked,
1150
+ loadThreadById,
939
1151
  loadOlderTurns,
940
1152
  messages,
941
1153
  onSourceClick,
942
1154
  refresh,
943
1155
  streamingContent,
944
1156
  streamingMessageId,
1157
+ startNewThread,
1158
+ threads,
945
1159
  upsertLiveVoiceMessage
946
1160
  ]);
947
1161
  }
@@ -1282,6 +1496,18 @@ var nextLatestFollowStateOnScroll = ({
1282
1496
  if (isProgrammatic) return current;
1283
1497
  return atLatest ? "following" : "detached";
1284
1498
  };
1499
+ var isUserScrollIntentAwayFromLatest = (intent) => {
1500
+ if (!intent) return false;
1501
+ if ("deltaY" in intent && typeof intent.deltaY === "number") {
1502
+ return intent.deltaY < 0;
1503
+ }
1504
+ if ("key" in intent && typeof intent.key === "string") {
1505
+ if (intent.key === "ArrowUp" || intent.key === "PageUp" || intent.key === "Home") return true;
1506
+ if ((intent.key === " " || intent.key === "Spacebar") && intent.shiftKey) return true;
1507
+ return false;
1508
+ }
1509
+ return "touches" in intent;
1510
+ };
1285
1511
  function useLatestThreadViewport({
1286
1512
  itemCount,
1287
1513
  hasOlder,
@@ -1331,10 +1557,11 @@ function useLatestThreadViewport({
1331
1557
  const reattachToLatest = useCallback2(() => {
1332
1558
  scrollToLatest({ reattach: true });
1333
1559
  }, [scrollToLatest]);
1334
- const handleUserScrollIntent = useCallback2(() => {
1560
+ const handleUserScrollIntent = useCallback2((intent) => {
1335
1561
  const element = scrollContainerRef.current;
1336
1562
  if (!activeStreamKey || !element) return;
1337
- if (!isAtTimelineLatestEdge(element, isTopOrigin)) detachFromLatest();
1563
+ const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
1564
+ if (!atLatest || isUserScrollIntentAwayFromLatest(intent)) detachFromLatest();
1338
1565
  }, [activeStreamKey, detachFromLatest, isTopOrigin]);
1339
1566
  useEffect3(() => {
1340
1567
  return () => {
@@ -1512,10 +1739,10 @@ function LatestThreadViewport({
1512
1739
  {
1513
1740
  ref: viewport.scrollContainerRef,
1514
1741
  className,
1515
- onKeyDown: viewport.handleUserScrollIntent,
1742
+ onKeyDownCapture: viewport.handleUserScrollIntent,
1516
1743
  onScroll: viewport.handleScroll,
1517
- onTouchMove: viewport.handleUserScrollIntent,
1518
- onWheel: viewport.handleUserScrollIntent,
1744
+ onTouchMoveCapture: viewport.handleUserScrollIntent,
1745
+ onWheelCapture: viewport.handleUserScrollIntent,
1519
1746
  role: "log",
1520
1747
  style: { overflowAnchor: "none" },
1521
1748
  children: [
@@ -1535,6 +1762,7 @@ export {
1535
1762
  activeRunIdFromThreadDetail,
1536
1763
  assistantTextFromParts,
1537
1764
  assistantTextFromResponseBlocks,
1765
+ compressionFromContextWindow,
1538
1766
  consumeSseResponse,
1539
1767
  createChatId,
1540
1768
  createFetchChatTransport,
@@ -1546,9 +1774,14 @@ export {
1546
1774
  isAtTimelineLatestEdge,
1547
1775
  isRunningThreadStatus,
1548
1776
  isScrollable,
1777
+ isUserScrollIntentAwayFromLatest,
1549
1778
  latestContextWindowFromThread,
1779
+ mergeContextWindow,
1780
+ mergeContextWindowUpdate,
1550
1781
  mergeReasoningSteps,
1551
1782
  nextLatestFollowStateOnScroll,
1783
+ normalizeContextCompression,
1784
+ normalizeContextWindow,
1552
1785
  parseSseBlock,
1553
1786
  partsFromResponseBlocks,
1554
1787
  reasoningStepsFromParts,