@agents24/chat-react 0.1.5 → 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/README.md CHANGED
@@ -20,6 +20,10 @@ The package owns thread hydration, older-page pagination, stream/reattach state,
20
20
  - `partsFromResponseBlocks(blocks, fallbackText)`
21
21
  - `renderChatPart(part, message, renderOptions)`
22
22
 
23
+ `useAgents24ChatController` exposes reactive `threads`, `activeThreadId`, `activeThread`, `activeRunId`, `isRefreshingThreads`, `loadThreadById(threadId)`, and `startNewThread()` in addition to message and stream state. It hydrates selected threads, detects active runs from `active_run`/`last_run_status`, attaches through the host transport, detaches local SSE streams on navigation, and refreshes history after terminal events.
24
+
25
+ Hosts can pass `onRuntimeEvent(event, context)` and `onThreadDetailLoaded(detail)` to layer product-specific runtime UI, trace panels, analytics, or metadata over the shared chat lifecycle without forking stream handling. That surface is enough for running-history badges and read-only live thread inspectors without a separate host-owned reattach state machine.
26
+
23
27
  `ChatMessage.parts` is the only render model. Backend `response_blocks` are normalized into ordered parts such as `text`, `tool-get_meteo`, `ui-blocks`, `reasoning`, `approval`, `error`, and `data`. Tool UI is client-owned through `renderPart`, `toolRenderers`, and `fallbackToolRenderer`; the package does not infer English tool labels or group tool rows.
24
28
 
25
29
  `useStreamingText` owns reusable visual pacing for active assistant text. It returns `displayedText`, `isAnimating`, `mode`, and `parseIncompleteMarkdown` so host renderers can pass those values into their markdown component without the package depending on any markdown/UI library. The package only chooses text timing, cache behavior, reduced-motion handling, and active text-part helpers.
package/dist/index.cjs CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  activeRunIdFromThreadDetail: () => activeRunIdFromThreadDetail,
29
29
  assistantTextFromParts: () => assistantTextFromParts,
30
30
  assistantTextFromResponseBlocks: () => assistantTextFromResponseBlocks,
31
+ compressionFromContextWindow: () => compressionFromContextWindow,
31
32
  consumeSseResponse: () => consumeSseResponse,
32
33
  createChatId: () => createChatId,
33
34
  createFetchChatTransport: () => createFetchChatTransport,
@@ -39,9 +40,14 @@ __export(index_exports, {
39
40
  isAtTimelineLatestEdge: () => isAtTimelineLatestEdge,
40
41
  isRunningThreadStatus: () => isRunningThreadStatus,
41
42
  isScrollable: () => isScrollable,
43
+ isUserScrollIntentAwayFromLatest: () => isUserScrollIntentAwayFromLatest,
42
44
  latestContextWindowFromThread: () => latestContextWindowFromThread,
45
+ mergeContextWindow: () => mergeContextWindow,
46
+ mergeContextWindowUpdate: () => mergeContextWindowUpdate,
43
47
  mergeReasoningSteps: () => mergeReasoningSteps,
44
48
  nextLatestFollowStateOnScroll: () => nextLatestFollowStateOnScroll,
49
+ normalizeContextCompression: () => normalizeContextCompression,
50
+ normalizeContextWindow: () => normalizeContextWindow,
45
51
  parseSseBlock: () => parseSseBlock,
46
52
  partsFromResponseBlocks: () => partsFromResponseBlocks,
47
53
  reasoningStepsFromParts: () => reasoningStepsFromParts,
@@ -63,9 +69,128 @@ module.exports = __toCommonJS(index_exports);
63
69
  // src/controller.ts
64
70
  var import_react = require("react");
65
71
 
72
+ // src/context-window.ts
73
+ var SOURCE_PRIORITY = {
74
+ unknown: 0,
75
+ heuristic_estimate: 1,
76
+ tokenizer_estimate: 1,
77
+ text_estimate: 1,
78
+ estimated: 1,
79
+ multimodal_estimate: 2,
80
+ hf_tokenizer: 3,
81
+ runtime_tokenizer: 3,
82
+ provider_count_api: 4,
83
+ provider_usage: 5,
84
+ exact: 5
85
+ };
86
+ var STAGE_PRIORITY = {
87
+ preflight: 0,
88
+ sent_prompt: 1,
89
+ final_usage: 2
90
+ };
91
+ function numberOrNull(value) {
92
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
93
+ }
94
+ function stagePriority(window2) {
95
+ return STAGE_PRIORITY[window2?.stage || "sent_prompt"] ?? STAGE_PRIORITY.sent_prompt;
96
+ }
97
+ function hasRenderableContextWindow(window2) {
98
+ const maxTokens = window2?.max_tokens;
99
+ return typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0;
100
+ }
101
+ function windowWeight(window2) {
102
+ if (!window2) return [0, 0];
103
+ return [stagePriority(window2), SOURCE_PRIORITY[window2.source] || 0];
104
+ }
105
+ function normalizeContextCompression(value) {
106
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
107
+ const payload = value;
108
+ return {
109
+ active: Boolean(payload.active),
110
+ reason: typeof payload.reason === "string" ? payload.reason : null,
111
+ input_tokens: numberOrNull(payload.input_tokens),
112
+ max_tokens: numberOrNull(payload.max_tokens),
113
+ usage_ratio: numberOrNull(payload.usage_ratio),
114
+ full_frame_count: numberOrNull(payload.full_frame_count),
115
+ compact_frame_count: numberOrNull(payload.compact_frame_count),
116
+ dropped_frame_count: numberOrNull(payload.dropped_frame_count),
117
+ artifact_ref_count: numberOrNull(payload.artifact_ref_count),
118
+ compression_trigger_budget: numberOrNull(payload.compression_trigger_budget),
119
+ compression_target_budget: numberOrNull(payload.compression_target_budget),
120
+ compression_target_ratio: numberOrNull(payload.compression_target_ratio),
121
+ threshold_used: numberOrNull(payload.threshold_used)
122
+ };
123
+ }
124
+ function normalizeContextWindow(value) {
125
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
126
+ const payload = value;
127
+ const rawSource = String(payload.source || "").trim();
128
+ 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";
129
+ const rawStage = String(payload.stage || "").trim();
130
+ const stage = rawStage === "preflight" || rawStage === "sent_prompt" || rawStage === "final_usage" ? rawStage : "sent_prompt";
131
+ const rawConfidence = String(payload.confidence || "").trim();
132
+ const confidence = rawConfidence === "exact" || rawConfidence === "high" || rawConfidence === "medium" || rawConfidence === "low" || rawConfidence === "unknown" ? rawConfidence : null;
133
+ return {
134
+ source,
135
+ run_id: typeof payload.run_id === "string" ? payload.run_id : null,
136
+ stage,
137
+ confidence,
138
+ counter: typeof payload.counter === "string" ? payload.counter : null,
139
+ model_id: typeof payload.model_id === "string" ? payload.model_id : null,
140
+ max_tokens: numberOrNull(payload.max_tokens),
141
+ max_tokens_source: typeof payload.max_tokens_source === "string" ? payload.max_tokens_source : null,
142
+ input_tokens: numberOrNull(payload.input_tokens),
143
+ remaining_tokens: numberOrNull(payload.remaining_tokens),
144
+ usage_ratio: numberOrNull(payload.usage_ratio),
145
+ assembly: payload.assembly && typeof payload.assembly === "object" && !Array.isArray(payload.assembly) ? payload.assembly : null,
146
+ context_compression: normalizeContextCompression(payload.context_compression)
147
+ };
148
+ }
149
+ function mergeContextWindow(current, incoming) {
150
+ if (!incoming) return current ?? null;
151
+ if (!current) return incoming;
152
+ if (hasRenderableContextWindow(current) && !hasRenderableContextWindow(incoming)) return current;
153
+ const currentRunId = current.run_id || null;
154
+ const incomingRunId = incoming.run_id || null;
155
+ if (incomingRunId && currentRunId && incomingRunId !== currentRunId) {
156
+ return stagePriority(incoming) >= STAGE_PRIORITY.sent_prompt ? incoming : current;
157
+ }
158
+ const currentWeight = windowWeight(current);
159
+ const incomingWeight = windowWeight(incoming);
160
+ return incomingWeight[0] > currentWeight[0] || incomingWeight[0] === currentWeight[0] && incomingWeight[1] >= currentWeight[1] ? incoming : current;
161
+ }
162
+ function mergeContextWindowUpdate(current, incoming) {
163
+ return mergeContextWindow(current, normalizeContextWindow(incoming));
164
+ }
165
+ function compressionFromContextWindow(contextWindow) {
166
+ if (!contextWindow) return null;
167
+ if (contextWindow.context_compression) return contextWindow.context_compression;
168
+ const assembly = contextWindow.assembly;
169
+ if (!assembly || typeof assembly !== "object" || Array.isArray(assembly)) return null;
170
+ const artifactRefs = Array.isArray(assembly.artifact_refs) ? assembly.artifact_refs : [];
171
+ const compactFrameCount = numberOrNull(assembly.compact_frame_count) ?? 0;
172
+ const droppedFrameCount = numberOrNull(assembly.dropped_frame_count) ?? 0;
173
+ const reason = typeof assembly.compaction_reason === "string" ? assembly.compaction_reason : "none";
174
+ return {
175
+ active: reason !== "none" || compactFrameCount > 0 || droppedFrameCount > 0,
176
+ reason,
177
+ input_tokens: contextWindow.input_tokens ?? null,
178
+ max_tokens: contextWindow.max_tokens ?? null,
179
+ usage_ratio: contextWindow.usage_ratio ?? null,
180
+ full_frame_count: numberOrNull(assembly.full_frame_count),
181
+ compact_frame_count: compactFrameCount,
182
+ dropped_frame_count: droppedFrameCount,
183
+ artifact_ref_count: artifactRefs.length,
184
+ compression_trigger_budget: numberOrNull(assembly.compression_trigger_budget),
185
+ compression_target_budget: numberOrNull(assembly.compression_target_budget),
186
+ compression_target_ratio: numberOrNull(assembly.compression_target_ratio),
187
+ threshold_used: numberOrNull(assembly.threshold_used)
188
+ };
189
+ }
190
+
66
191
  // src/model.ts
67
192
  var DEFAULT_THREAD_PAGE_SIZE = 5;
68
- var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running"])).has(String(status || "").toLowerCase());
193
+ var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
69
194
  var createChatId = () => {
70
195
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
71
196
  return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
@@ -104,6 +229,16 @@ var attachmentsFromTurn = (turn) => (turn.attachments || []).map((attachment, in
104
229
  mediaType: String(record.mime_type || record.mediaType || record.type || "application/octet-stream")
105
230
  };
106
231
  });
232
+ var tokenUsageFromTurn = (turn) => {
233
+ const usage = turn.run_usage;
234
+ if (!usage || typeof usage !== "object") return void 0;
235
+ return {
236
+ inputTokens: usage.input_tokens ?? null,
237
+ outputTokens: usage.output_tokens ?? null,
238
+ totalTokens: usage.total_tokens ?? null,
239
+ usageSource: usage.source ?? null
240
+ };
241
+ };
107
242
  var latestEventPayloadValue = (events, key) => {
108
243
  for (let index = (events || []).length - 1; index >= 0; index -= 1) {
109
244
  const payload = events?.[index]?.payload;
@@ -305,7 +440,8 @@ var turnToMessages = (turn, activeRunId) => {
305
440
  createdAt: turn.completed_at ? new Date(turn.completed_at) : createdAt,
306
441
  parts,
307
442
  reasoningSteps: mergeReasoningSteps(reasoningStepsFromParts(parts), { finalize: !isRunning }),
308
- isFinal: !isRunning
443
+ isFinal: !isRunning,
444
+ tokenUsage: tokenUsageFromTurn(turn)
309
445
  });
310
446
  }
311
447
  return messages;
@@ -322,10 +458,11 @@ var threadPaging = (thread) => {
322
458
  };
323
459
  };
324
460
  var latestContextWindowFromThread = (thread) => {
325
- if (thread.context_window && typeof thread.context_window === "object") return thread.context_window;
461
+ const threadContextWindow = normalizeContextWindow(thread.context_window);
462
+ if (threadContextWindow) return threadContextWindow;
326
463
  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;
464
+ const contextWindow = normalizeContextWindow(thread.turns?.[index]?.context_window);
465
+ if (contextWindow) return contextWindow;
329
466
  }
330
467
  return null;
331
468
  };
@@ -356,10 +493,6 @@ var activeRunIdFromThreadDetail = (thread) => {
356
493
  };
357
494
 
358
495
  // src/controller.ts
359
- var mergeContextWindow = (current, incoming) => {
360
- if (!incoming || typeof incoming !== "object") return current;
361
- return { ...current || {}, ...incoming };
362
- };
363
496
  var threadSummaryToStored = (thread) => ({
364
497
  ...thread,
365
498
  id: String(thread.id),
@@ -377,7 +510,9 @@ function useAgents24ChatController({
377
510
  createId = createChatId,
378
511
  onActiveThreadIdChange,
379
512
  onSourceClick,
380
- onStreamErrorMessage
513
+ onStreamErrorMessage,
514
+ onRuntimeEvent,
515
+ onThreadDetailLoaded
381
516
  }) {
382
517
  const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
383
518
  const initialCached = activeThreadId ? storage.getThread(activeThreadId) : void 0;
@@ -394,6 +529,9 @@ function useAgents24ChatController({
394
529
  const [disliked, setDisliked] = (0, import_react.useState)({});
395
530
  const [copiedMessageId, setCopiedMessageId] = (0, import_react.useState)(null);
396
531
  const [lastThinkingDurationMs, setLastThinkingDurationMs] = (0, import_react.useState)(null);
532
+ const [threads, setThreads] = (0, import_react.useState)(() => storage.listThreads());
533
+ const [isRefreshingThreads, setIsRefreshingThreads] = (0, import_react.useState)(false);
534
+ const [activeRunId, setActiveRunId] = (0, import_react.useState)(null);
397
535
  const textareaRef = (0, import_react.useRef)(null);
398
536
  const activeThreadIdRef = (0, import_react.useRef)(activeThreadId);
399
537
  const messagesRef = (0, import_react.useRef)(messages);
@@ -410,6 +548,20 @@ function useAgents24ChatController({
410
548
  const streamingMessageIdRef = (0, import_react.useRef)(null);
411
549
  const reasoningRef = (0, import_react.useRef)([]);
412
550
  const liveVoiceIdsRef = (0, import_react.useRef)({});
551
+ const setActiveRunIdValue = (0, import_react.useCallback)((runId) => {
552
+ activeRunIdRef.current = runId;
553
+ setActiveRunId(runId);
554
+ }, []);
555
+ const syncThreadsFromStorage = (0, import_react.useCallback)(() => {
556
+ setThreads(storage.listThreads());
557
+ }, [storage]);
558
+ const upsertStoredThread = (0, import_react.useCallback)(
559
+ (thread) => {
560
+ storage.upsertThread(thread);
561
+ syncThreadsFromStorage();
562
+ },
563
+ [storage, syncThreadsFromStorage]
564
+ );
413
565
  (0, import_react.useEffect)(() => {
414
566
  messagesRef.current = messages;
415
567
  }, [messages]);
@@ -417,7 +569,7 @@ function useAgents24ChatController({
417
569
  (threadId, nextMessages, paging, options) => {
418
570
  const existing = storage.getThread(threadId);
419
571
  const firstUser = nextMessages.find((message) => message.role === "user");
420
- storage.upsertThread({
572
+ upsertStoredThread({
421
573
  ...existing || {},
422
574
  id: threadId,
423
575
  title: existing?.title || (firstUser ? titleFromMessage(firstUser.content, firstUser.attachments || []) : "New chat"),
@@ -428,13 +580,13 @@ function useAgents24ChatController({
428
580
  nextBeforeTurnIndex: paging?.nextBeforeTurnIndex ?? nextBeforeTurnIndexRef.current
429
581
  });
430
582
  },
431
- [storage]
583
+ [storage, upsertStoredThread]
432
584
  );
433
585
  const markThreadRunStatus = (0, import_react.useCallback)(
434
586
  (threadId, runId, status, lastEventSeq) => {
435
587
  const existing = storage.getThread(threadId);
436
588
  if (!existing || !runId) return;
437
- storage.upsertThread({
589
+ upsertStoredThread({
438
590
  ...existing,
439
591
  last_run_id: runId,
440
592
  last_run_status: status,
@@ -451,14 +603,25 @@ function useAgents24ChatController({
451
603
  created_at: existing.activeRun?.created_at ?? existing.active_run?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
452
604
  },
453
605
  lastEventSeq: typeof lastEventSeq === "number" ? lastEventSeq : existing.lastEventSeq ?? null,
454
- isRunning: status === "queued" || status === "running"
606
+ isRunning: activeRunIdFromThread({
607
+ lastRunId: runId,
608
+ lastRunStatus: status,
609
+ activeRun: { run_id: runId, status }
610
+ }) !== null
455
611
  });
456
612
  },
457
- [storage]
613
+ [storage, upsertStoredThread]
458
614
  );
459
615
  const refresh = (0, import_react.useCallback)(async () => {
460
- const data = await transport.listThreads();
461
- storage.setThreads((data.items || []).map((item) => threadSummaryToStored(item)));
616
+ setIsRefreshingThreads(true);
617
+ try {
618
+ const data = await transport.listThreads();
619
+ const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
620
+ storage.setThreads(nextThreads);
621
+ setThreads(storage.listThreads());
622
+ } finally {
623
+ setIsRefreshingThreads(false);
624
+ }
462
625
  }, [storage, transport]);
463
626
  const applyThreadId = (0, import_react.useCallback)(
464
627
  (threadId, baseMessages) => {
@@ -486,7 +649,7 @@ function useAgents24ChatController({
486
649
  const controller = abortControllerRef.current;
487
650
  abortControllerRef.current = null;
488
651
  controller?.abort();
489
- activeRunIdRef.current = null;
652
+ setActiveRunIdValue(null);
490
653
  reattachedRunIdRef.current = null;
491
654
  streamingMessageIdRef.current = null;
492
655
  streamingContentRef.current = "";
@@ -495,7 +658,7 @@ function useAgents24ChatController({
495
658
  setIsLoading(false);
496
659
  setStreamingContent("");
497
660
  setCurrentReasoning([]);
498
- }, []);
661
+ }, [setActiveRunIdValue]);
499
662
  const setLiveAssistantMessage = (0, import_react.useCallback)(
500
663
  (input) => {
501
664
  setMessages((prev) => {
@@ -565,7 +728,7 @@ function useAgents24ChatController({
565
728
  persistThread(input.threadId, completed);
566
729
  const existingThread = storage.getThread(input.threadId);
567
730
  if (existingThread && input.runId) {
568
- storage.upsertThread({
731
+ upsertStoredThread({
569
732
  ...existingThread,
570
733
  messages: completed,
571
734
  last_run_id: input.runId,
@@ -580,7 +743,7 @@ function useAgents24ChatController({
580
743
  }
581
744
  return completed;
582
745
  },
583
- [createId, persistThread, storage]
746
+ [createId, persistThread, storage, upsertStoredThread]
584
747
  );
585
748
  const loadThread = (0, import_react.useCallback)(
586
749
  async (threadId) => {
@@ -595,6 +758,7 @@ function useAgents24ChatController({
595
758
  includeRunEvents: false
596
759
  });
597
760
  if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
761
+ void onThreadDetailLoaded?.(detail);
598
762
  const nextMessages = threadDetailToMessages(detail);
599
763
  const paging = threadPaging(detail);
600
764
  setContextStatus(latestContextWindowFromThread(detail));
@@ -604,7 +768,7 @@ function useAgents24ChatController({
604
768
  hasOlderTurnsRef.current = paging.hasOlderTurns;
605
769
  nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
606
770
  loadedThreadIdRef.current = threadId;
607
- storage.upsertThread({
771
+ upsertStoredThread({
608
772
  ...threadSummaryToStored(detail),
609
773
  messages: nextMessages,
610
774
  isHydrated: true,
@@ -616,7 +780,7 @@ function useAgents24ChatController({
616
780
  if (activeThreadIdRef.current === threadId) setLoadingHistory(false);
617
781
  }
618
782
  },
619
- [pageSize, setLoadingHistory, storage, transport]
783
+ [onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
620
784
  );
621
785
  const loadOlderTurns = (0, import_react.useCallback)(async () => {
622
786
  const threadId = activeThreadIdRef.current;
@@ -648,11 +812,18 @@ function useAgents24ChatController({
648
812
  }, [pageSize, persistThread, transport]);
649
813
  const handleStreamEvent = (0, import_react.useCallback)(
650
814
  (input) => {
651
- const { event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
815
+ const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
652
816
  const payload = event.payload || {};
653
817
  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));
818
+ if (event.run_id) setActiveRunIdValue(event.run_id);
819
+ void onRuntimeEvent?.(event, {
820
+ mode,
821
+ assistantMessageId,
822
+ threadId: streamThreadIdRef.current,
823
+ runId: event.run_id || activeRunIdRef.current,
824
+ startedAt
825
+ });
826
+ setContextStatus((current) => mergeContextWindowUpdate(current, payload.context_window));
656
827
  if (responseBlocks) {
657
828
  const blockText = String(payload.assistant_output_text || "") || assistantTextFromResponseBlocks(responseBlocks) || streamingContentRef.current;
658
829
  if (blockText) setStreamingText(blockText);
@@ -698,7 +869,7 @@ function useAgents24ChatController({
698
869
  error: isFailed ? String(payload.message || payload.error || event.diagnostics?.[0]?.message || onStreamErrorMessage?.(event) || "The chat run failed.") : void 0
699
870
  });
700
871
  },
701
- [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onStreamErrorMessage, setLiveAssistantMessage, setReasoningSteps]
872
+ [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
702
873
  );
703
874
  const runStream = (0, import_react.useCallback)(
704
875
  async (input) => {
@@ -760,16 +931,16 @@ function useAgents24ChatController({
760
931
  }
761
932
  try {
762
933
  if (input.mode === "attach") {
763
- activeRunIdRef.current = input.runId;
934
+ setActiveRunIdValue(input.runId);
764
935
  reattachedRunIdRef.current = input.runId;
765
936
  await transport.attachRun(
766
937
  { runId: input.runId, signal: controller.signal },
767
- (event) => handleStreamEvent({ event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
938
+ (event) => handleStreamEvent({ mode: "attach", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
768
939
  );
769
940
  } else {
770
941
  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 })
942
+ { ...input.message, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
943
+ (event) => handleStreamEvent({ mode: "submit", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
773
944
  );
774
945
  }
775
946
  if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
@@ -801,7 +972,7 @@ function useAgents24ChatController({
801
972
  const isCurrentStream = abortControllerRef.current === controller || streamingMessageIdRef.current === assistantMessageId;
802
973
  if (abortControllerRef.current === controller) abortControllerRef.current = null;
803
974
  if (isCurrentStream) {
804
- activeRunIdRef.current = null;
975
+ setActiveRunIdValue(null);
805
976
  streamingMessageIdRef.current = null;
806
977
  reattachedRunIdRef.current = null;
807
978
  setStreamingMessageId(null);
@@ -812,7 +983,7 @@ function useAgents24ChatController({
812
983
  }
813
984
  }
814
985
  },
815
- [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setReasoningSteps, transport]
986
+ [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
816
987
  );
817
988
  const handleSubmit = (0, import_react.useCallback)(
818
989
  async (message) => {
@@ -827,7 +998,7 @@ function useAgents24ChatController({
827
998
  const liveMessageId = streamingMessageIdRef.current;
828
999
  abortControllerRef.current?.abort();
829
1000
  abortControllerRef.current = null;
830
- activeRunIdRef.current = null;
1001
+ setActiveRunIdValue(null);
831
1002
  streamingMessageIdRef.current = null;
832
1003
  setStreamingMessageId(null);
833
1004
  setIsLoading(false);
@@ -844,10 +1015,16 @@ function useAgents24ChatController({
844
1015
  messageId: liveMessageId
845
1016
  });
846
1017
  }
847
- }, [finalizeAssistantMessage, lastThinkingDurationMs, setReasoningSteps, transport]);
1018
+ }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
848
1019
  (0, import_react.useEffect)(() => {
849
- refresh().catch(() => storage.setThreads([]));
1020
+ refresh().catch(() => {
1021
+ storage.setThreads([]);
1022
+ setThreads([]);
1023
+ });
850
1024
  }, [refresh, storage]);
1025
+ (0, import_react.useEffect)(() => {
1026
+ syncThreadsFromStorage();
1027
+ }, [storageKey, syncThreadsFromStorage]);
851
1028
  (0, import_react.useEffect)(() => {
852
1029
  const previous = activeThreadIdRef.current;
853
1030
  activeThreadIdRef.current = activeThreadId;
@@ -955,7 +1132,40 @@ function useAgents24ChatController({
955
1132
  return next;
956
1133
  });
957
1134
  }, [createId, persistThread]);
1135
+ const startNewThread = (0, import_react.useCallback)(() => {
1136
+ detachActiveStream();
1137
+ requestSeqRef.current += 1;
1138
+ activeThreadIdRef.current = null;
1139
+ loadedThreadIdRef.current = null;
1140
+ nextBeforeTurnIndexRef.current = null;
1141
+ hasOlderTurnsRef.current = false;
1142
+ storage.setActiveThreadId?.(null);
1143
+ onActiveThreadIdChange?.(null);
1144
+ setMessages([]);
1145
+ messagesRef.current = [];
1146
+ setHasOlderTurns(false);
1147
+ setIsLoadingOlder(false);
1148
+ setContextStatus(null);
1149
+ setLoadingHistory(false);
1150
+ }, [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage]);
1151
+ const loadThreadById = (0, import_react.useCallback)(
1152
+ async (threadId) => {
1153
+ if (!threadId) return;
1154
+ if (activeThreadIdRef.current !== threadId) {
1155
+ if (abortControllerRef.current) detachActiveStream();
1156
+ activeThreadIdRef.current = threadId;
1157
+ storage.setActiveThreadId?.(threadId);
1158
+ onActiveThreadIdChange?.(threadId);
1159
+ }
1160
+ await loadThread(threadId);
1161
+ },
1162
+ [detachActiveStream, loadThread, onActiveThreadIdChange, storage]
1163
+ );
1164
+ const activeThread = activeThreadId ? storage.getThread(activeThreadId) || threads.find((thread) => thread.id === activeThreadId) || null : null;
958
1165
  return (0, import_react.useMemo)(() => ({
1166
+ threads,
1167
+ activeThreadId,
1168
+ activeThread,
959
1169
  messages,
960
1170
  streamingContent,
961
1171
  streamingMessageId,
@@ -964,12 +1174,13 @@ function useAgents24ChatController({
964
1174
  isLoading,
965
1175
  isLoadingHistory,
966
1176
  isLoadingOlder,
1177
+ isRefreshingThreads,
967
1178
  hasOlderTurns,
968
1179
  liked,
969
1180
  disliked,
970
1181
  copiedMessageId,
971
1182
  lastThinkingDurationMs,
972
- activeRunId: activeRunIdRef.current,
1183
+ activeRunId,
973
1184
  handleSubmit,
974
1185
  handleStop,
975
1186
  handleCopy,
@@ -978,11 +1189,16 @@ function useAgents24ChatController({
978
1189
  handleRetry,
979
1190
  handleSourceClick: (citations) => onSourceClick?.(citations),
980
1191
  upsertLiveVoiceMessage,
1192
+ startNewThread,
1193
+ loadThreadById,
981
1194
  loadOlderTurns,
982
1195
  refresh,
983
1196
  textareaRef
984
1197
  }), [
985
1198
  copiedMessageId,
1199
+ activeRunId,
1200
+ activeThread,
1201
+ activeThreadId,
986
1202
  contextStatus,
987
1203
  currentReasoning,
988
1204
  disliked,
@@ -996,14 +1212,18 @@ function useAgents24ChatController({
996
1212
  isLoading,
997
1213
  isLoadingHistory,
998
1214
  isLoadingOlder,
1215
+ isRefreshingThreads,
999
1216
  lastThinkingDurationMs,
1000
1217
  liked,
1218
+ loadThreadById,
1001
1219
  loadOlderTurns,
1002
1220
  messages,
1003
1221
  onSourceClick,
1004
1222
  refresh,
1005
1223
  streamingContent,
1006
1224
  streamingMessageId,
1225
+ startNewThread,
1226
+ threads,
1007
1227
  upsertLiveVoiceMessage
1008
1228
  ]);
1009
1229
  }
@@ -1337,6 +1557,18 @@ var nextLatestFollowStateOnScroll = ({
1337
1557
  if (isProgrammatic) return current;
1338
1558
  return atLatest ? "following" : "detached";
1339
1559
  };
1560
+ var isUserScrollIntentAwayFromLatest = (intent) => {
1561
+ if (!intent) return false;
1562
+ if ("deltaY" in intent && typeof intent.deltaY === "number") {
1563
+ return intent.deltaY < 0;
1564
+ }
1565
+ if ("key" in intent && typeof intent.key === "string") {
1566
+ if (intent.key === "ArrowUp" || intent.key === "PageUp" || intent.key === "Home") return true;
1567
+ if ((intent.key === " " || intent.key === "Spacebar") && intent.shiftKey) return true;
1568
+ return false;
1569
+ }
1570
+ return "touches" in intent;
1571
+ };
1340
1572
  function useLatestThreadViewport({
1341
1573
  itemCount,
1342
1574
  hasOlder,
@@ -1386,10 +1618,11 @@ function useLatestThreadViewport({
1386
1618
  const reattachToLatest = (0, import_react3.useCallback)(() => {
1387
1619
  scrollToLatest({ reattach: true });
1388
1620
  }, [scrollToLatest]);
1389
- const handleUserScrollIntent = (0, import_react3.useCallback)(() => {
1621
+ const handleUserScrollIntent = (0, import_react3.useCallback)((intent) => {
1390
1622
  const element = scrollContainerRef.current;
1391
1623
  if (!activeStreamKey || !element) return;
1392
- if (!isAtTimelineLatestEdge(element, isTopOrigin)) detachFromLatest();
1624
+ const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
1625
+ if (!atLatest || isUserScrollIntentAwayFromLatest(intent)) detachFromLatest();
1393
1626
  }, [activeStreamKey, detachFromLatest, isTopOrigin]);
1394
1627
  (0, import_react3.useEffect)(() => {
1395
1628
  return () => {
@@ -1473,9 +1706,22 @@ function useLatestThreadViewport({
1473
1706
  };
1474
1707
  intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
1475
1708
  }, [isTopOrigin, markProgrammaticScroll]);
1476
- (0, import_react3.useEffect)(() => {
1709
+ (0, import_react3.useLayoutEffect)(() => {
1477
1710
  const key = activeStreamKey || null;
1478
- if (!key || activeStreamKeyRef.current === key) {
1711
+ const previousKey = activeStreamKeyRef.current;
1712
+ if (!key) {
1713
+ activeStreamKeyRef.current = null;
1714
+ if (previousKey && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1715
+ const frame2 = requestAnimationFrame(() => {
1716
+ if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1717
+ scrollToLatest({ reattach: false });
1718
+ }
1719
+ });
1720
+ return () => cancelAnimationFrame(frame2);
1721
+ }
1722
+ return;
1723
+ }
1724
+ if (previousKey === key) {
1479
1725
  activeStreamKeyRef.current = key;
1480
1726
  return;
1481
1727
  }
@@ -1483,7 +1729,7 @@ function useLatestThreadViewport({
1483
1729
  const frame = requestAnimationFrame(() => scrollToLatest());
1484
1730
  activeStreamKeyRef.current = key;
1485
1731
  return () => cancelAnimationFrame(frame);
1486
- }, [activeStreamKey, scrollToLatest, setLatestFollowState]);
1732
+ }, [activeStreamKey, isLoadingOlder, scrollToLatest, setLatestFollowState, shouldAutoFollow]);
1487
1733
  (0, import_react3.useLayoutEffect)(() => {
1488
1734
  if (!activeStreamKey || !shouldAutoFollow || followStateRef.current !== "following") return;
1489
1735
  if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
@@ -1554,10 +1800,10 @@ function LatestThreadViewport({
1554
1800
  {
1555
1801
  ref: viewport.scrollContainerRef,
1556
1802
  className,
1557
- onKeyDown: viewport.handleUserScrollIntent,
1803
+ onKeyDownCapture: viewport.handleUserScrollIntent,
1558
1804
  onScroll: viewport.handleScroll,
1559
- onTouchMove: viewport.handleUserScrollIntent,
1560
- onWheel: viewport.handleUserScrollIntent,
1805
+ onTouchMoveCapture: viewport.handleUserScrollIntent,
1806
+ onWheelCapture: viewport.handleUserScrollIntent,
1561
1807
  role: "log",
1562
1808
  style: { overflowAnchor: "none" },
1563
1809
  children: [
@@ -1578,6 +1824,7 @@ function LatestThreadViewport({
1578
1824
  activeRunIdFromThreadDetail,
1579
1825
  assistantTextFromParts,
1580
1826
  assistantTextFromResponseBlocks,
1827
+ compressionFromContextWindow,
1581
1828
  consumeSseResponse,
1582
1829
  createChatId,
1583
1830
  createFetchChatTransport,
@@ -1589,9 +1836,14 @@ function LatestThreadViewport({
1589
1836
  isAtTimelineLatestEdge,
1590
1837
  isRunningThreadStatus,
1591
1838
  isScrollable,
1839
+ isUserScrollIntentAwayFromLatest,
1592
1840
  latestContextWindowFromThread,
1841
+ mergeContextWindow,
1842
+ mergeContextWindowUpdate,
1593
1843
  mergeReasoningSteps,
1594
1844
  nextLatestFollowStateOnScroll,
1845
+ normalizeContextCompression,
1846
+ normalizeContextWindow,
1595
1847
  parseSseBlock,
1596
1848
  partsFromResponseBlocks,
1597
1849
  reasoningStepsFromParts,