@agents24/chat-react 0.1.3 → 0.1.5

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.d.ts CHANGED
@@ -349,6 +349,30 @@ declare const renderChatPart: (part: ChatPart, message: ChatMessage, renderOptio
349
349
  declare const parseSseBlock: (block: string) => ChatRuntimeEvent | null;
350
350
  declare const consumeSseResponse: (response: Response, onEvent: (event: ChatRuntimeEvent) => void | Promise<void>) => Promise<ChatStreamResult>;
351
351
 
352
+ type StreamingTextMode = "streaming" | "static";
353
+ type StreamingTextCache = {
354
+ get: (id: string) => string | undefined;
355
+ set: (id: string, text: string) => void;
356
+ };
357
+ type UseStreamingTextOptions = {
358
+ id: string;
359
+ isStreaming: boolean;
360
+ text: string;
361
+ cache?: StreamingTextCache | false;
362
+ charsPerSecond?: number;
363
+ catchupThreshold?: number;
364
+ maxCatchupChars?: number;
365
+ };
366
+ type UseStreamingTextResult = {
367
+ displayedText: string;
368
+ isAnimating: boolean;
369
+ mode: StreamingTextMode;
370
+ parseIncompleteMarkdown: boolean;
371
+ };
372
+ declare const getActiveStreamingTextPartId: (message: ChatMessage, streamingMessageId: string | null) => string | null;
373
+ declare const isActiveStreamingTextPart: (message: ChatMessage, part: ChatPart, streamingMessageId: string | null) => boolean;
374
+ declare function useStreamingText({ id, isStreaming, text, cache, charsPerSecond, catchupThreshold, maxCatchupChars, }: UseStreamingTextOptions): UseStreamingTextResult;
375
+
352
376
  type MaybePromise<T> = T | Promise<T>;
353
377
  type FetchChatTransportRoutes = {
354
378
  listThreads: () => string;
@@ -381,6 +405,7 @@ type FetchChatTransportOptions = {
381
405
  };
382
406
  declare const createFetchChatTransport: ({ routes, fetchImpl, headers, streamBody, }: FetchChatTransportOptions) => ChatTransport;
383
407
 
408
+ type LatestFollowState = "following" | "detached";
384
409
  declare const isScrollable: (element: Pick<HTMLDivElement, "scrollHeight" | "clientHeight">) => boolean;
385
410
  declare const isAtTimelineLatestEdge: (element: Pick<HTMLDivElement, "scrollHeight" | "clientHeight" | "scrollTop">, isTopOrigin: boolean) => boolean;
386
411
  declare const shouldPrefetchOlder: (element: Pick<HTMLDivElement, "scrollHeight" | "clientHeight" | "scrollTop">) => boolean;
@@ -390,6 +415,11 @@ declare const shouldUseTopOriginTimeline: ({ hasOlder, itemCount, topOriginMaxIt
390
415
  itemCount: number;
391
416
  topOriginMaxItems?: number;
392
417
  }) => boolean;
418
+ declare const nextLatestFollowStateOnScroll: ({ atLatest, current, isProgrammatic, }: {
419
+ atLatest: boolean;
420
+ current: LatestFollowState;
421
+ isProgrammatic: boolean;
422
+ }) => LatestFollowState;
393
423
  type UseLatestThreadViewportOptions = {
394
424
  itemCount: number;
395
425
  hasOlder: boolean;
@@ -402,15 +432,22 @@ type UseLatestThreadViewportOptions = {
402
432
  type LatestThreadViewportState = {
403
433
  scrollContainerRef: RefObject<HTMLDivElement | null>;
404
434
  isAtLatest: boolean;
435
+ isFollowingLatest: boolean;
405
436
  isTopOrigin: boolean;
406
437
  shouldShowLatestButton: boolean;
407
438
  handleScroll: (event: UIEvent<HTMLDivElement>) => void;
408
- scrollToLatest: () => void;
439
+ handleUserScrollIntent: () => void;
440
+ scrollToLatest: (options?: {
441
+ reattach?: boolean;
442
+ }) => void;
443
+ detachFromLatest: () => void;
444
+ reattachToLatest: () => void;
409
445
  preserveIntrinsicResize: () => void;
410
446
  };
411
447
  declare function useLatestThreadViewport({ itemCount, hasOlder, isLoadingOlder, onLoadOlder, activeStreamKey, shouldAutoFollow, topOriginMaxItems, }: UseLatestThreadViewportOptions): LatestThreadViewportState;
412
448
  type LatestThreadViewportRenderContext = {
413
449
  preserveIntrinsicResize: () => void;
450
+ isFollowingLatest: boolean;
414
451
  isTopOrigin: boolean;
415
452
  };
416
453
  type LatestThreadViewportProps<T> = {
@@ -426,4 +463,4 @@ type LatestThreadViewportProps<T> = {
426
463
  };
427
464
  declare function LatestThreadViewport<T>({ items, hasOlder, isLoadingOlder, onLoadOlder, activeStreamKey, className, latestSpacer, trailingSpacer, renderItem, }: LatestThreadViewportProps<T>): react.JSX.Element;
428
465
 
429
- export { type Agents24ChatRenderOptions, type ChatActiveRun, type ChatApprovalPart, type ChatAttachment, type ChatCitation, type ChatContextWindow, type ChatController, type ChatDataPart, type ChatErrorPart, type ChatMessage, type ChatPart, type ChatReasoningPart, type ChatReasoningStep, type ChatRunStatus, type ChatRuntimeEvent, type ChatStorageAdapter, type ChatStreamResult, type ChatTextPart, type ChatThreadDetail, type ChatThreadSummary, type ChatThreadTurn, type ChatToolPart, type ChatToolState, type ChatTransport, type ChatUiBlocksPart, DEFAULT_THREAD_PAGE_SIZE, DefaultChatPart, DefaultToolPart, type FetchChatTransportOptions, type FetchChatTransportRoutes, LatestThreadViewport, type LatestThreadViewportProps, type LatestThreadViewportRenderContext, type LatestThreadViewportState, type StoredChatThread, type ToolPresentation, type ToolRenderer, type UseAgents24ChatControllerOptions, type UseLatestThreadViewportOptions, activeRunIdFromThread, activeRunIdFromThreadDetail, assistantTextFromParts, assistantTextFromResponseBlocks, consumeSseResponse, createChatId, createFetchChatTransport, getLatestScrollTop, hasStaleUnfinishedAssistantCache, hasUnfinishedAssistantMessage, isAtTimelineLatestEdge, isRunningThreadStatus, isScrollable, latestContextWindowFromThread, mergeReasoningSteps, parseSseBlock, partsFromResponseBlocks, reasoningStepsFromParts, renderChatPart, shouldPrefetchOlder, shouldUseTopOriginTimeline, textFromFinalOutput, threadActivityDate, threadDetailToMessages, threadPaging, titleFromMessage, toolStateFromStatus, useAgents24ChatController, useLatestThreadViewport };
466
+ export { type Agents24ChatRenderOptions, type ChatActiveRun, type ChatApprovalPart, type ChatAttachment, type ChatCitation, type ChatContextWindow, type ChatController, type ChatDataPart, type ChatErrorPart, type ChatMessage, type ChatPart, type ChatReasoningPart, type ChatReasoningStep, type ChatRunStatus, type ChatRuntimeEvent, type ChatStorageAdapter, type ChatStreamResult, type ChatTextPart, type ChatThreadDetail, type ChatThreadSummary, type ChatThreadTurn, type ChatToolPart, type ChatToolState, type ChatTransport, type ChatUiBlocksPart, DEFAULT_THREAD_PAGE_SIZE, DefaultChatPart, DefaultToolPart, type FetchChatTransportOptions, type FetchChatTransportRoutes, type LatestFollowState, LatestThreadViewport, type LatestThreadViewportProps, type LatestThreadViewportRenderContext, type LatestThreadViewportState, type StoredChatThread, type StreamingTextCache, type StreamingTextMode, type ToolPresentation, type ToolRenderer, type UseAgents24ChatControllerOptions, type UseLatestThreadViewportOptions, type UseStreamingTextOptions, type UseStreamingTextResult, activeRunIdFromThread, activeRunIdFromThreadDetail, assistantTextFromParts, assistantTextFromResponseBlocks, consumeSseResponse, createChatId, createFetchChatTransport, getActiveStreamingTextPartId, getLatestScrollTop, hasStaleUnfinishedAssistantCache, hasUnfinishedAssistantMessage, isActiveStreamingTextPart, isAtTimelineLatestEdge, isRunningThreadStatus, isScrollable, latestContextWindowFromThread, mergeReasoningSteps, nextLatestFollowStateOnScroll, parseSseBlock, partsFromResponseBlocks, reasoningStepsFromParts, renderChatPart, shouldPrefetchOlder, shouldUseTopOriginTimeline, textFromFinalOutput, threadActivityDate, threadDetailToMessages, threadPaging, titleFromMessage, toolStateFromStatus, useAgents24ChatController, useLatestThreadViewport, useStreamingText };
package/dist/index.js CHANGED
@@ -1035,6 +1035,140 @@ var consumeSseResponse = async (response, onEvent) => {
1035
1035
  return { threadId, runId };
1036
1036
  };
1037
1037
 
1038
+ // src/streaming-text.ts
1039
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
1040
+ var DEFAULT_COMPLETED_TEXT_CACHE_SIZE = 200;
1041
+ var defaultCompletedTextCache = /* @__PURE__ */ new Map();
1042
+ var defaultStreamingTextCache = {
1043
+ get: (id) => defaultCompletedTextCache.get(id),
1044
+ set: (id, text) => {
1045
+ defaultCompletedTextCache.set(id, text);
1046
+ if (defaultCompletedTextCache.size > DEFAULT_COMPLETED_TEXT_CACHE_SIZE) {
1047
+ const firstKey = defaultCompletedTextCache.keys().next().value;
1048
+ if (firstKey) defaultCompletedTextCache.delete(firstKey);
1049
+ }
1050
+ }
1051
+ };
1052
+ var getActiveStreamingTextPartId = (message, streamingMessageId) => {
1053
+ if (message.role !== "assistant" || streamingMessageId !== message.id) return null;
1054
+ return message.parts.slice().reverse().find((part) => part.kind === "text")?.id || null;
1055
+ };
1056
+ var isActiveStreamingTextPart = (message, part, streamingMessageId) => part.kind === "text" && part.id === getActiveStreamingTextPartId(message, streamingMessageId);
1057
+ function useStreamingText({
1058
+ id,
1059
+ isStreaming,
1060
+ text,
1061
+ cache = defaultStreamingTextCache,
1062
+ charsPerSecond = 72,
1063
+ catchupThreshold = 40,
1064
+ maxCatchupChars = 20
1065
+ }) {
1066
+ const cacheAdapter = cache === false ? null : cache;
1067
+ const [displayedText, setDisplayedText] = useState2(() => {
1068
+ const cachedText = cacheAdapter?.get(id);
1069
+ return isStreaming && cachedText !== text ? "" : text;
1070
+ });
1071
+ const targetRef = useRef2(text);
1072
+ const displayedRef = useRef2(displayedText);
1073
+ const rafRef = useRef2(null);
1074
+ const lastFrameAtRef = useRef2(null);
1075
+ const idRef = useRef2(id);
1076
+ const shouldAnimateRef = useRef2(isStreaming);
1077
+ useEffect2(() => {
1078
+ if (isStreaming || !text) return;
1079
+ cacheAdapter?.set(id, text);
1080
+ }, [cacheAdapter, id, isStreaming, text]);
1081
+ useEffect2(() => {
1082
+ targetRef.current = text;
1083
+ }, [text]);
1084
+ useEffect2(() => {
1085
+ if (idRef.current === id) return;
1086
+ idRef.current = id;
1087
+ const cachedText = cacheAdapter?.get(id);
1088
+ const initial = isStreaming && cachedText !== text ? "" : text;
1089
+ shouldAnimateRef.current = isStreaming && initial !== text;
1090
+ displayedRef.current = initial;
1091
+ setDisplayedText(initial);
1092
+ if (rafRef.current !== null) {
1093
+ cancelAnimationFrame(rafRef.current);
1094
+ rafRef.current = null;
1095
+ }
1096
+ lastFrameAtRef.current = null;
1097
+ }, [cacheAdapter, id, isStreaming, text]);
1098
+ useEffect2(() => {
1099
+ if (typeof window === "undefined") {
1100
+ displayedRef.current = text;
1101
+ setDisplayedText(text);
1102
+ return;
1103
+ }
1104
+ const reduceMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1105
+ if (reduceMotion) {
1106
+ displayedRef.current = text;
1107
+ setDisplayedText(text);
1108
+ shouldAnimateRef.current = false;
1109
+ return;
1110
+ }
1111
+ const cachedText = cacheAdapter?.get(id);
1112
+ if (cachedText === text) {
1113
+ displayedRef.current = text;
1114
+ setDisplayedText(text);
1115
+ shouldAnimateRef.current = false;
1116
+ return;
1117
+ }
1118
+ if (isStreaming) {
1119
+ shouldAnimateRef.current = true;
1120
+ }
1121
+ if (!shouldAnimateRef.current) {
1122
+ displayedRef.current = text;
1123
+ setDisplayedText(text);
1124
+ return;
1125
+ }
1126
+ if (!text.startsWith(displayedRef.current)) {
1127
+ displayedRef.current = "";
1128
+ setDisplayedText("");
1129
+ }
1130
+ const tick = (timestamp) => {
1131
+ const previousTimestamp = lastFrameAtRef.current ?? timestamp;
1132
+ lastFrameAtRef.current = timestamp;
1133
+ const target = targetRef.current;
1134
+ const current = displayedRef.current;
1135
+ if (current.length >= target.length) {
1136
+ shouldAnimateRef.current = false;
1137
+ rafRef.current = null;
1138
+ return;
1139
+ }
1140
+ const elapsedMs = Math.max(8, timestamp - previousTimestamp);
1141
+ const charsFromTime = Math.max(1, Math.floor(elapsedMs / 1e3 * charsPerSecond));
1142
+ const gap = target.length - current.length;
1143
+ const catchupStep = gap > catchupThreshold ? Math.min(maxCatchupChars, Math.ceil(gap / 10)) : charsFromTime;
1144
+ const nextLength = Math.min(
1145
+ target.length,
1146
+ current.length + Math.max(charsFromTime, catchupStep)
1147
+ );
1148
+ const next = target.slice(0, nextLength);
1149
+ displayedRef.current = next;
1150
+ setDisplayedText(next);
1151
+ rafRef.current = window.requestAnimationFrame(tick);
1152
+ };
1153
+ if (rafRef.current === null && displayedRef.current.length < text.length) {
1154
+ rafRef.current = window.requestAnimationFrame(tick);
1155
+ }
1156
+ return () => {
1157
+ if (rafRef.current !== null) {
1158
+ cancelAnimationFrame(rafRef.current);
1159
+ rafRef.current = null;
1160
+ }
1161
+ lastFrameAtRef.current = null;
1162
+ };
1163
+ }, [cacheAdapter, catchupThreshold, charsPerSecond, id, isStreaming, maxCatchupChars, text]);
1164
+ return {
1165
+ displayedText,
1166
+ isAnimating: isStreaming,
1167
+ mode: isStreaming ? "streaming" : "static",
1168
+ parseIncompleteMarkdown: isStreaming
1169
+ };
1170
+ }
1171
+
1038
1172
  // src/transport.ts
1039
1173
  var jsonHeaders = (headers) => ({
1040
1174
  ...headers || {},
@@ -1118,11 +1252,11 @@ var createFetchChatTransport = ({
1118
1252
  // src/viewport.tsx
1119
1253
  import {
1120
1254
  useCallback as useCallback2,
1121
- useEffect as useEffect2,
1255
+ useEffect as useEffect3,
1122
1256
  useLayoutEffect,
1123
1257
  useMemo as useMemo2,
1124
- useRef as useRef2,
1125
- useState as useState2
1258
+ useRef as useRef3,
1259
+ useState as useState3
1126
1260
  } from "react";
1127
1261
  import { jsxs as jsxs2 } from "react/jsx-runtime";
1128
1262
  var LATEST_EDGE_THRESHOLD_PX = 2;
@@ -1140,6 +1274,14 @@ var shouldUseTopOriginTimeline = ({
1140
1274
  itemCount,
1141
1275
  topOriginMaxItems = 4
1142
1276
  }) => !hasOlder && itemCount > 0 && itemCount <= topOriginMaxItems;
1277
+ var nextLatestFollowStateOnScroll = ({
1278
+ atLatest,
1279
+ current,
1280
+ isProgrammatic
1281
+ }) => {
1282
+ if (isProgrammatic) return current;
1283
+ return atLatest ? "following" : "detached";
1284
+ };
1143
1285
  function useLatestThreadViewport({
1144
1286
  itemCount,
1145
1287
  hasOlder,
@@ -1149,16 +1291,23 @@ function useLatestThreadViewport({
1149
1291
  shouldAutoFollow = true,
1150
1292
  topOriginMaxItems = 4
1151
1293
  }) {
1152
- const scrollContainerRef = useRef2(null);
1153
- const olderPagePreserveRef = useRef2(null);
1154
- const olderPageRequestInFlightRef = useRef2(false);
1155
- const intrinsicResizePreserveRef = useRef2(null);
1156
- const autoFollowLatestRef = useRef2(true);
1157
- const programmaticScrollRef = useRef2(false);
1158
- const programmaticScrollTimeoutRef = useRef2(null);
1159
- const activeStreamKeyRef = useRef2(null);
1160
- const [isAtLatest, setIsAtLatest] = useState2(true);
1294
+ const scrollContainerRef = useRef3(null);
1295
+ const olderPagePreserveRef = useRef3(null);
1296
+ const olderPageRequestInFlightRef = useRef3(false);
1297
+ const intrinsicResizePreserveRef = useRef3(null);
1298
+ const followStateRef = useRef3("following");
1299
+ const programmaticScrollRef = useRef3(false);
1300
+ const programmaticScrollTimeoutRef = useRef3(null);
1301
+ const activeStreamKeyRef = useRef3(null);
1302
+ const observedScrollHeightRef = useRef3(null);
1303
+ const [isAtLatest, setIsAtLatest] = useState3(true);
1304
+ const [followState, setFollowState] = useState3("following");
1161
1305
  const isTopOrigin = shouldUseTopOriginTimeline({ hasOlder, itemCount, topOriginMaxItems });
1306
+ const setLatestFollowState = useCallback2((next) => {
1307
+ if (followStateRef.current === next) return;
1308
+ followStateRef.current = next;
1309
+ setFollowState(next);
1310
+ }, []);
1162
1311
  const markProgrammaticScroll = useCallback2(() => {
1163
1312
  programmaticScrollRef.current = true;
1164
1313
  if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
@@ -1167,14 +1316,27 @@ function useLatestThreadViewport({
1167
1316
  programmaticScrollTimeoutRef.current = null;
1168
1317
  }, 80);
1169
1318
  }, []);
1170
- const scrollToLatest = useCallback2(() => {
1319
+ const scrollToLatest = useCallback2((options) => {
1171
1320
  const element = scrollContainerRef.current;
1172
1321
  if (!element) return;
1322
+ if (options?.reattach !== false) setLatestFollowState("following");
1173
1323
  markProgrammaticScroll();
1174
1324
  element.scrollTop = getLatestScrollTop(element, isTopOrigin);
1175
1325
  setIsAtLatest(true);
1176
- }, [isTopOrigin, markProgrammaticScroll]);
1177
- useEffect2(() => {
1326
+ observedScrollHeightRef.current = element.scrollHeight;
1327
+ }, [isTopOrigin, markProgrammaticScroll, setLatestFollowState]);
1328
+ const detachFromLatest = useCallback2(() => {
1329
+ setLatestFollowState("detached");
1330
+ }, [setLatestFollowState]);
1331
+ const reattachToLatest = useCallback2(() => {
1332
+ scrollToLatest({ reattach: true });
1333
+ }, [scrollToLatest]);
1334
+ const handleUserScrollIntent = useCallback2(() => {
1335
+ const element = scrollContainerRef.current;
1336
+ if (!activeStreamKey || !element) return;
1337
+ if (!isAtTimelineLatestEdge(element, isTopOrigin)) detachFromLatest();
1338
+ }, [activeStreamKey, detachFromLatest, isTopOrigin]);
1339
+ useEffect3(() => {
1178
1340
  return () => {
1179
1341
  if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
1180
1342
  const preserve = intrinsicResizePreserveRef.current;
@@ -1185,6 +1347,7 @@ function useLatestThreadViewport({
1185
1347
  const element = scrollContainerRef.current;
1186
1348
  const preserve = olderPagePreserveRef.current;
1187
1349
  if (!element) return;
1350
+ observedScrollHeightRef.current = element.scrollHeight;
1188
1351
  if (!preserve) {
1189
1352
  setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
1190
1353
  return;
@@ -1201,7 +1364,13 @@ function useLatestThreadViewport({
1201
1364
  const element = event.currentTarget;
1202
1365
  const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
1203
1366
  setIsAtLatest(atLatest);
1204
- if (!programmaticScrollRef.current) autoFollowLatestRef.current = atLatest;
1367
+ setLatestFollowState(
1368
+ nextLatestFollowStateOnScroll({
1369
+ atLatest,
1370
+ current: followStateRef.current,
1371
+ isProgrammatic: programmaticScrollRef.current
1372
+ })
1373
+ );
1205
1374
  if (isTopOrigin || !hasOlder || isLoadingOlder || olderPageRequestInFlightRef.current || !shouldPrefetchOlder(element)) {
1206
1375
  return;
1207
1376
  }
@@ -1218,7 +1387,7 @@ function useLatestThreadViewport({
1218
1387
  });
1219
1388
  });
1220
1389
  },
1221
- [hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder]
1390
+ [hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder, setLatestFollowState]
1222
1391
  );
1223
1392
  const preserveIntrinsicResize = useCallback2(() => {
1224
1393
  const element = scrollContainerRef.current;
@@ -1249,37 +1418,60 @@ function useLatestThreadViewport({
1249
1418
  };
1250
1419
  intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
1251
1420
  }, [isTopOrigin, markProgrammaticScroll]);
1252
- useEffect2(() => {
1421
+ useEffect3(() => {
1253
1422
  const key = activeStreamKey || null;
1254
1423
  if (!key || activeStreamKeyRef.current === key) {
1255
1424
  activeStreamKeyRef.current = key;
1256
1425
  return;
1257
1426
  }
1258
- autoFollowLatestRef.current = true;
1259
- if (isTopOrigin) {
1260
- activeStreamKeyRef.current = key;
1261
- return;
1262
- }
1263
- const frame = requestAnimationFrame(scrollToLatest);
1427
+ setLatestFollowState("following");
1428
+ const frame = requestAnimationFrame(() => scrollToLatest());
1264
1429
  activeStreamKeyRef.current = key;
1265
1430
  return () => cancelAnimationFrame(frame);
1266
- }, [activeStreamKey, isTopOrigin, scrollToLatest]);
1431
+ }, [activeStreamKey, scrollToLatest, setLatestFollowState]);
1267
1432
  useLayoutEffect(() => {
1268
- if (isTopOrigin) return;
1269
- if (!activeStreamKey || !shouldAutoFollow || !autoFollowLatestRef.current) return;
1433
+ if (!activeStreamKey || !shouldAutoFollow || followStateRef.current !== "following") return;
1270
1434
  if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
1271
1435
  const frame = requestAnimationFrame(() => {
1272
- if (autoFollowLatestRef.current && !isLoadingOlder && !olderPageRequestInFlightRef.current) scrollToLatest();
1436
+ if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1437
+ scrollToLatest({ reattach: false });
1438
+ }
1273
1439
  });
1274
1440
  return () => cancelAnimationFrame(frame);
1275
- }, [activeStreamKey, isLoadingOlder, isTopOrigin, itemCount, scrollToLatest, shouldAutoFollow]);
1441
+ }, [activeStreamKey, isLoadingOlder, itemCount, scrollToLatest, shouldAutoFollow]);
1442
+ useEffect3(() => {
1443
+ if (!activeStreamKey) {
1444
+ observedScrollHeightRef.current = scrollContainerRef.current?.scrollHeight ?? null;
1445
+ return;
1446
+ }
1447
+ let frame = null;
1448
+ const watchStreamResize = () => {
1449
+ const element = scrollContainerRef.current;
1450
+ if (!element) return;
1451
+ const previousHeight = observedScrollHeightRef.current;
1452
+ const nextHeight = element.scrollHeight;
1453
+ observedScrollHeightRef.current = nextHeight;
1454
+ if (previousHeight !== null && Math.abs(nextHeight - previousHeight) >= 1 && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1455
+ scrollToLatest({ reattach: false });
1456
+ }
1457
+ frame = requestAnimationFrame(watchStreamResize);
1458
+ };
1459
+ frame = requestAnimationFrame(watchStreamResize);
1460
+ return () => {
1461
+ if (frame !== null) cancelAnimationFrame(frame);
1462
+ };
1463
+ }, [activeStreamKey, isLoadingOlder, scrollToLatest, shouldAutoFollow]);
1276
1464
  return {
1277
1465
  scrollContainerRef,
1278
1466
  isAtLatest,
1467
+ isFollowingLatest: followState === "following",
1279
1468
  isTopOrigin,
1280
1469
  shouldShowLatestButton: !isAtLatest,
1281
1470
  handleScroll,
1471
+ handleUserScrollIntent,
1282
1472
  scrollToLatest,
1473
+ detachFromLatest,
1474
+ reattachToLatest,
1283
1475
  preserveIntrinsicResize
1284
1476
  };
1285
1477
  }
@@ -1307,7 +1499,10 @@ function LatestThreadViewport({
1307
1499
  {
1308
1500
  ref: viewport.scrollContainerRef,
1309
1501
  className,
1502
+ onKeyDown: viewport.handleUserScrollIntent,
1310
1503
  onScroll: viewport.handleScroll,
1504
+ onTouchMove: viewport.handleUserScrollIntent,
1505
+ onWheel: viewport.handleUserScrollIntent,
1311
1506
  role: "log",
1312
1507
  style: { overflowAnchor: "none" },
1313
1508
  children: [
@@ -1330,14 +1525,17 @@ export {
1330
1525
  consumeSseResponse,
1331
1526
  createChatId,
1332
1527
  createFetchChatTransport,
1528
+ getActiveStreamingTextPartId,
1333
1529
  getLatestScrollTop,
1334
1530
  hasStaleUnfinishedAssistantCache,
1335
1531
  hasUnfinishedAssistantMessage,
1532
+ isActiveStreamingTextPart,
1336
1533
  isAtTimelineLatestEdge,
1337
1534
  isRunningThreadStatus,
1338
1535
  isScrollable,
1339
1536
  latestContextWindowFromThread,
1340
1537
  mergeReasoningSteps,
1538
+ nextLatestFollowStateOnScroll,
1341
1539
  parseSseBlock,
1342
1540
  partsFromResponseBlocks,
1343
1541
  reasoningStepsFromParts,
@@ -1351,6 +1549,7 @@ export {
1351
1549
  titleFromMessage,
1352
1550
  toolStateFromStatus,
1353
1551
  useAgents24ChatController,
1354
- useLatestThreadViewport
1552
+ useLatestThreadViewport,
1553
+ useStreamingText
1355
1554
  };
1356
1555
  //# sourceMappingURL=index.js.map