@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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agents24 Chat React
2
2
 
3
- Last Updated: 2026-06-10
3
+ Last Updated: 2026-06-17
4
4
 
5
5
  Headless React primitives for Agents24 chat surfaces.
6
6
 
@@ -11,6 +11,9 @@ The package owns thread hydration, older-page pagination, stream/reattach state,
11
11
  - `useAgents24ChatController(options)`
12
12
  - `useLatestThreadViewport(options)`
13
13
  - `LatestThreadViewport`
14
+ - `useStreamingText(options)`
15
+ - `getActiveStreamingTextPartId(message, streamingMessageId)`
16
+ - `isActiveStreamingTextPart(message, part, streamingMessageId)`
14
17
  - `createFetchChatTransport(options)`
15
18
  - `consumeSseResponse(response, onEvent)`
16
19
  - normalized chat message, part, transport, and storage types
@@ -19,4 +22,18 @@ The package owns thread hydration, older-page pagination, stream/reattach state,
19
22
 
20
23
  `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.
21
24
 
25
+ `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.
26
+
22
27
  Network access is always injected by the host. This browser package never requires platform API keys.
28
+
29
+ ## Latest Viewport Behavior
30
+
31
+ The package owns the standard streaming scroll policy:
32
+
33
+ - a new active stream starts in latest-follow mode
34
+ - streaming text growth stays pinned to latest while the user remains there
35
+ - user scrolling away detaches latest-follow mode
36
+ - manually reaching latest again, or clicking the latest button, reattaches follow mode
37
+ - older-page prepends preserve the visible viewport instead of snapping to latest
38
+
39
+ Hosts should wire `handleScroll` and `scrollToLatest` from `useLatestThreadViewport`, or use `LatestThreadViewport` directly. `handleUserScrollIntent` is available for hosts that need to mark wheel, touch, or keyboard intent before a scroll event lands.
package/dist/index.cjs CHANGED
@@ -31,14 +31,17 @@ __export(index_exports, {
31
31
  consumeSseResponse: () => consumeSseResponse,
32
32
  createChatId: () => createChatId,
33
33
  createFetchChatTransport: () => createFetchChatTransport,
34
+ getActiveStreamingTextPartId: () => getActiveStreamingTextPartId,
34
35
  getLatestScrollTop: () => getLatestScrollTop,
35
36
  hasStaleUnfinishedAssistantCache: () => hasStaleUnfinishedAssistantCache,
36
37
  hasUnfinishedAssistantMessage: () => hasUnfinishedAssistantMessage,
38
+ isActiveStreamingTextPart: () => isActiveStreamingTextPart,
37
39
  isAtTimelineLatestEdge: () => isAtTimelineLatestEdge,
38
40
  isRunningThreadStatus: () => isRunningThreadStatus,
39
41
  isScrollable: () => isScrollable,
40
42
  latestContextWindowFromThread: () => latestContextWindowFromThread,
41
43
  mergeReasoningSteps: () => mergeReasoningSteps,
44
+ nextLatestFollowStateOnScroll: () => nextLatestFollowStateOnScroll,
42
45
  parseSseBlock: () => parseSseBlock,
43
46
  partsFromResponseBlocks: () => partsFromResponseBlocks,
44
47
  reasoningStepsFromParts: () => reasoningStepsFromParts,
@@ -52,7 +55,8 @@ __export(index_exports, {
52
55
  titleFromMessage: () => titleFromMessage,
53
56
  toolStateFromStatus: () => toolStateFromStatus,
54
57
  useAgents24ChatController: () => useAgents24ChatController,
55
- useLatestThreadViewport: () => useLatestThreadViewport
58
+ useLatestThreadViewport: () => useLatestThreadViewport,
59
+ useStreamingText: () => useStreamingText
56
60
  });
57
61
  module.exports = __toCommonJS(index_exports);
58
62
 
@@ -1093,6 +1097,140 @@ var consumeSseResponse = async (response, onEvent) => {
1093
1097
  return { threadId, runId };
1094
1098
  };
1095
1099
 
1100
+ // src/streaming-text.ts
1101
+ var import_react2 = require("react");
1102
+ var DEFAULT_COMPLETED_TEXT_CACHE_SIZE = 200;
1103
+ var defaultCompletedTextCache = /* @__PURE__ */ new Map();
1104
+ var defaultStreamingTextCache = {
1105
+ get: (id) => defaultCompletedTextCache.get(id),
1106
+ set: (id, text) => {
1107
+ defaultCompletedTextCache.set(id, text);
1108
+ if (defaultCompletedTextCache.size > DEFAULT_COMPLETED_TEXT_CACHE_SIZE) {
1109
+ const firstKey = defaultCompletedTextCache.keys().next().value;
1110
+ if (firstKey) defaultCompletedTextCache.delete(firstKey);
1111
+ }
1112
+ }
1113
+ };
1114
+ var getActiveStreamingTextPartId = (message, streamingMessageId) => {
1115
+ if (message.role !== "assistant" || streamingMessageId !== message.id) return null;
1116
+ return message.parts.slice().reverse().find((part) => part.kind === "text")?.id || null;
1117
+ };
1118
+ var isActiveStreamingTextPart = (message, part, streamingMessageId) => part.kind === "text" && part.id === getActiveStreamingTextPartId(message, streamingMessageId);
1119
+ function useStreamingText({
1120
+ id,
1121
+ isStreaming,
1122
+ text,
1123
+ cache = defaultStreamingTextCache,
1124
+ charsPerSecond = 72,
1125
+ catchupThreshold = 40,
1126
+ maxCatchupChars = 20
1127
+ }) {
1128
+ const cacheAdapter = cache === false ? null : cache;
1129
+ const [displayedText, setDisplayedText] = (0, import_react2.useState)(() => {
1130
+ const cachedText = cacheAdapter?.get(id);
1131
+ return isStreaming && cachedText !== text ? "" : text;
1132
+ });
1133
+ const targetRef = (0, import_react2.useRef)(text);
1134
+ const displayedRef = (0, import_react2.useRef)(displayedText);
1135
+ const rafRef = (0, import_react2.useRef)(null);
1136
+ const lastFrameAtRef = (0, import_react2.useRef)(null);
1137
+ const idRef = (0, import_react2.useRef)(id);
1138
+ const shouldAnimateRef = (0, import_react2.useRef)(isStreaming);
1139
+ (0, import_react2.useEffect)(() => {
1140
+ if (isStreaming || !text) return;
1141
+ cacheAdapter?.set(id, text);
1142
+ }, [cacheAdapter, id, isStreaming, text]);
1143
+ (0, import_react2.useEffect)(() => {
1144
+ targetRef.current = text;
1145
+ }, [text]);
1146
+ (0, import_react2.useEffect)(() => {
1147
+ if (idRef.current === id) return;
1148
+ idRef.current = id;
1149
+ const cachedText = cacheAdapter?.get(id);
1150
+ const initial = isStreaming && cachedText !== text ? "" : text;
1151
+ shouldAnimateRef.current = isStreaming && initial !== text;
1152
+ displayedRef.current = initial;
1153
+ setDisplayedText(initial);
1154
+ if (rafRef.current !== null) {
1155
+ cancelAnimationFrame(rafRef.current);
1156
+ rafRef.current = null;
1157
+ }
1158
+ lastFrameAtRef.current = null;
1159
+ }, [cacheAdapter, id, isStreaming, text]);
1160
+ (0, import_react2.useEffect)(() => {
1161
+ if (typeof window === "undefined") {
1162
+ displayedRef.current = text;
1163
+ setDisplayedText(text);
1164
+ return;
1165
+ }
1166
+ const reduceMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1167
+ if (reduceMotion) {
1168
+ displayedRef.current = text;
1169
+ setDisplayedText(text);
1170
+ shouldAnimateRef.current = false;
1171
+ return;
1172
+ }
1173
+ const cachedText = cacheAdapter?.get(id);
1174
+ if (cachedText === text) {
1175
+ displayedRef.current = text;
1176
+ setDisplayedText(text);
1177
+ shouldAnimateRef.current = false;
1178
+ return;
1179
+ }
1180
+ if (isStreaming) {
1181
+ shouldAnimateRef.current = true;
1182
+ }
1183
+ if (!shouldAnimateRef.current) {
1184
+ displayedRef.current = text;
1185
+ setDisplayedText(text);
1186
+ return;
1187
+ }
1188
+ if (!text.startsWith(displayedRef.current)) {
1189
+ displayedRef.current = "";
1190
+ setDisplayedText("");
1191
+ }
1192
+ const tick = (timestamp) => {
1193
+ const previousTimestamp = lastFrameAtRef.current ?? timestamp;
1194
+ lastFrameAtRef.current = timestamp;
1195
+ const target = targetRef.current;
1196
+ const current = displayedRef.current;
1197
+ if (current.length >= target.length) {
1198
+ shouldAnimateRef.current = false;
1199
+ rafRef.current = null;
1200
+ return;
1201
+ }
1202
+ const elapsedMs = Math.max(8, timestamp - previousTimestamp);
1203
+ const charsFromTime = Math.max(1, Math.floor(elapsedMs / 1e3 * charsPerSecond));
1204
+ const gap = target.length - current.length;
1205
+ const catchupStep = gap > catchupThreshold ? Math.min(maxCatchupChars, Math.ceil(gap / 10)) : charsFromTime;
1206
+ const nextLength = Math.min(
1207
+ target.length,
1208
+ current.length + Math.max(charsFromTime, catchupStep)
1209
+ );
1210
+ const next = target.slice(0, nextLength);
1211
+ displayedRef.current = next;
1212
+ setDisplayedText(next);
1213
+ rafRef.current = window.requestAnimationFrame(tick);
1214
+ };
1215
+ if (rafRef.current === null && displayedRef.current.length < text.length) {
1216
+ rafRef.current = window.requestAnimationFrame(tick);
1217
+ }
1218
+ return () => {
1219
+ if (rafRef.current !== null) {
1220
+ cancelAnimationFrame(rafRef.current);
1221
+ rafRef.current = null;
1222
+ }
1223
+ lastFrameAtRef.current = null;
1224
+ };
1225
+ }, [cacheAdapter, catchupThreshold, charsPerSecond, id, isStreaming, maxCatchupChars, text]);
1226
+ return {
1227
+ displayedText,
1228
+ isAnimating: isStreaming,
1229
+ mode: isStreaming ? "streaming" : "static",
1230
+ parseIncompleteMarkdown: isStreaming
1231
+ };
1232
+ }
1233
+
1096
1234
  // src/transport.ts
1097
1235
  var jsonHeaders = (headers) => ({
1098
1236
  ...headers || {},
@@ -1174,7 +1312,7 @@ var createFetchChatTransport = ({
1174
1312
  };
1175
1313
 
1176
1314
  // src/viewport.tsx
1177
- var import_react2 = require("react");
1315
+ var import_react3 = require("react");
1178
1316
  var import_jsx_runtime2 = require("react/jsx-runtime");
1179
1317
  var LATEST_EDGE_THRESHOLD_PX = 2;
1180
1318
  var MIN_OLDER_PREFETCH_PX = 320;
@@ -1191,6 +1329,14 @@ var shouldUseTopOriginTimeline = ({
1191
1329
  itemCount,
1192
1330
  topOriginMaxItems = 4
1193
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
+ };
1194
1340
  function useLatestThreadViewport({
1195
1341
  itemCount,
1196
1342
  hasOlder,
@@ -1200,17 +1346,24 @@ function useLatestThreadViewport({
1200
1346
  shouldAutoFollow = true,
1201
1347
  topOriginMaxItems = 4
1202
1348
  }) {
1203
- const scrollContainerRef = (0, import_react2.useRef)(null);
1204
- const olderPagePreserveRef = (0, import_react2.useRef)(null);
1205
- const olderPageRequestInFlightRef = (0, import_react2.useRef)(false);
1206
- const intrinsicResizePreserveRef = (0, import_react2.useRef)(null);
1207
- const autoFollowLatestRef = (0, import_react2.useRef)(true);
1208
- const programmaticScrollRef = (0, import_react2.useRef)(false);
1209
- const programmaticScrollTimeoutRef = (0, import_react2.useRef)(null);
1210
- const activeStreamKeyRef = (0, import_react2.useRef)(null);
1211
- const [isAtLatest, setIsAtLatest] = (0, import_react2.useState)(true);
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");
1212
1360
  const isTopOrigin = shouldUseTopOriginTimeline({ hasOlder, itemCount, topOriginMaxItems });
1213
- const markProgrammaticScroll = (0, import_react2.useCallback)(() => {
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)(() => {
1214
1367
  programmaticScrollRef.current = true;
1215
1368
  if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
1216
1369
  programmaticScrollTimeoutRef.current = setTimeout(() => {
@@ -1218,24 +1371,38 @@ function useLatestThreadViewport({
1218
1371
  programmaticScrollTimeoutRef.current = null;
1219
1372
  }, 80);
1220
1373
  }, []);
1221
- const scrollToLatest = (0, import_react2.useCallback)(() => {
1374
+ const scrollToLatest = (0, import_react3.useCallback)((options) => {
1222
1375
  const element = scrollContainerRef.current;
1223
1376
  if (!element) return;
1377
+ if (options?.reattach !== false) setLatestFollowState("following");
1224
1378
  markProgrammaticScroll();
1225
1379
  element.scrollTop = getLatestScrollTop(element, isTopOrigin);
1226
1380
  setIsAtLatest(true);
1227
- }, [isTopOrigin, markProgrammaticScroll]);
1228
- (0, import_react2.useEffect)(() => {
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)(() => {
1229
1395
  return () => {
1230
1396
  if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
1231
1397
  const preserve = intrinsicResizePreserveRef.current;
1232
1398
  if (preserve?.frame !== null && preserve?.frame !== void 0) cancelAnimationFrame(preserve.frame);
1233
1399
  };
1234
1400
  }, []);
1235
- (0, import_react2.useLayoutEffect)(() => {
1401
+ (0, import_react3.useLayoutEffect)(() => {
1236
1402
  const element = scrollContainerRef.current;
1237
1403
  const preserve = olderPagePreserveRef.current;
1238
1404
  if (!element) return;
1405
+ observedScrollHeightRef.current = element.scrollHeight;
1239
1406
  if (!preserve) {
1240
1407
  setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
1241
1408
  return;
@@ -1247,12 +1414,18 @@ function useLatestThreadViewport({
1247
1414
  olderPageRequestInFlightRef.current = false;
1248
1415
  setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
1249
1416
  }, [isTopOrigin, itemCount, markProgrammaticScroll]);
1250
- const handleScroll = (0, import_react2.useCallback)(
1417
+ const handleScroll = (0, import_react3.useCallback)(
1251
1418
  (event) => {
1252
1419
  const element = event.currentTarget;
1253
1420
  const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
1254
1421
  setIsAtLatest(atLatest);
1255
- if (!programmaticScrollRef.current) autoFollowLatestRef.current = atLatest;
1422
+ setLatestFollowState(
1423
+ nextLatestFollowStateOnScroll({
1424
+ atLatest,
1425
+ current: followStateRef.current,
1426
+ isProgrammatic: programmaticScrollRef.current
1427
+ })
1428
+ );
1256
1429
  if (isTopOrigin || !hasOlder || isLoadingOlder || olderPageRequestInFlightRef.current || !shouldPrefetchOlder(element)) {
1257
1430
  return;
1258
1431
  }
@@ -1269,9 +1442,9 @@ function useLatestThreadViewport({
1269
1442
  });
1270
1443
  });
1271
1444
  },
1272
- [hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder]
1445
+ [hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder, setLatestFollowState]
1273
1446
  );
1274
- const preserveIntrinsicResize = (0, import_react2.useCallback)(() => {
1447
+ const preserveIntrinsicResize = (0, import_react3.useCallback)(() => {
1275
1448
  const element = scrollContainerRef.current;
1276
1449
  if (!element || isTopOrigin) return;
1277
1450
  const active = intrinsicResizePreserveRef.current;
@@ -1300,37 +1473,60 @@ function useLatestThreadViewport({
1300
1473
  };
1301
1474
  intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
1302
1475
  }, [isTopOrigin, markProgrammaticScroll]);
1303
- (0, import_react2.useEffect)(() => {
1476
+ (0, import_react3.useEffect)(() => {
1304
1477
  const key = activeStreamKey || null;
1305
1478
  if (!key || activeStreamKeyRef.current === key) {
1306
1479
  activeStreamKeyRef.current = key;
1307
1480
  return;
1308
1481
  }
1309
- autoFollowLatestRef.current = true;
1310
- if (isTopOrigin) {
1311
- activeStreamKeyRef.current = key;
1312
- return;
1313
- }
1314
- const frame = requestAnimationFrame(scrollToLatest);
1482
+ setLatestFollowState("following");
1483
+ const frame = requestAnimationFrame(() => scrollToLatest());
1315
1484
  activeStreamKeyRef.current = key;
1316
1485
  return () => cancelAnimationFrame(frame);
1317
- }, [activeStreamKey, isTopOrigin, scrollToLatest]);
1318
- (0, import_react2.useLayoutEffect)(() => {
1319
- if (isTopOrigin) return;
1320
- if (!activeStreamKey || !shouldAutoFollow || !autoFollowLatestRef.current) return;
1486
+ }, [activeStreamKey, scrollToLatest, setLatestFollowState]);
1487
+ (0, import_react3.useLayoutEffect)(() => {
1488
+ if (!activeStreamKey || !shouldAutoFollow || followStateRef.current !== "following") return;
1321
1489
  if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
1322
1490
  const frame = requestAnimationFrame(() => {
1323
- if (autoFollowLatestRef.current && !isLoadingOlder && !olderPageRequestInFlightRef.current) scrollToLatest();
1491
+ if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1492
+ scrollToLatest({ reattach: false });
1493
+ }
1324
1494
  });
1325
1495
  return () => cancelAnimationFrame(frame);
1326
- }, [activeStreamKey, isLoadingOlder, isTopOrigin, itemCount, scrollToLatest, shouldAutoFollow]);
1496
+ }, [activeStreamKey, isLoadingOlder, itemCount, scrollToLatest, shouldAutoFollow]);
1497
+ (0, import_react3.useEffect)(() => {
1498
+ if (!activeStreamKey) {
1499
+ observedScrollHeightRef.current = scrollContainerRef.current?.scrollHeight ?? null;
1500
+ return;
1501
+ }
1502
+ let frame = null;
1503
+ const watchStreamResize = () => {
1504
+ const element = scrollContainerRef.current;
1505
+ if (!element) return;
1506
+ const previousHeight = observedScrollHeightRef.current;
1507
+ const nextHeight = element.scrollHeight;
1508
+ observedScrollHeightRef.current = nextHeight;
1509
+ if (previousHeight !== null && Math.abs(nextHeight - previousHeight) >= 1 && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1510
+ scrollToLatest({ reattach: false });
1511
+ }
1512
+ frame = requestAnimationFrame(watchStreamResize);
1513
+ };
1514
+ frame = requestAnimationFrame(watchStreamResize);
1515
+ return () => {
1516
+ if (frame !== null) cancelAnimationFrame(frame);
1517
+ };
1518
+ }, [activeStreamKey, isLoadingOlder, scrollToLatest, shouldAutoFollow]);
1327
1519
  return {
1328
1520
  scrollContainerRef,
1329
1521
  isAtLatest,
1522
+ isFollowingLatest: followState === "following",
1330
1523
  isTopOrigin,
1331
1524
  shouldShowLatestButton: !isAtLatest,
1332
1525
  handleScroll,
1526
+ handleUserScrollIntent,
1333
1527
  scrollToLatest,
1528
+ detachFromLatest,
1529
+ reattachToLatest,
1334
1530
  preserveIntrinsicResize
1335
1531
  };
1336
1532
  }
@@ -1352,13 +1548,16 @@ function LatestThreadViewport({
1352
1548
  onLoadOlder,
1353
1549
  activeStreamKey
1354
1550
  });
1355
- const timelineItems = (0, import_react2.useMemo)(() => viewport.isTopOrigin ? items : [...items].reverse(), [items, viewport.isTopOrigin]);
1551
+ const timelineItems = (0, import_react3.useMemo)(() => viewport.isTopOrigin ? items : [...items].reverse(), [items, viewport.isTopOrigin]);
1356
1552
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1357
1553
  "div",
1358
1554
  {
1359
1555
  ref: viewport.scrollContainerRef,
1360
1556
  className,
1557
+ onKeyDown: viewport.handleUserScrollIntent,
1361
1558
  onScroll: viewport.handleScroll,
1559
+ onTouchMove: viewport.handleUserScrollIntent,
1560
+ onWheel: viewport.handleUserScrollIntent,
1362
1561
  role: "log",
1363
1562
  style: { overflowAnchor: "none" },
1364
1563
  children: [
@@ -1382,14 +1581,17 @@ function LatestThreadViewport({
1382
1581
  consumeSseResponse,
1383
1582
  createChatId,
1384
1583
  createFetchChatTransport,
1584
+ getActiveStreamingTextPartId,
1385
1585
  getLatestScrollTop,
1386
1586
  hasStaleUnfinishedAssistantCache,
1387
1587
  hasUnfinishedAssistantMessage,
1588
+ isActiveStreamingTextPart,
1388
1589
  isAtTimelineLatestEdge,
1389
1590
  isRunningThreadStatus,
1390
1591
  isScrollable,
1391
1592
  latestContextWindowFromThread,
1392
1593
  mergeReasoningSteps,
1594
+ nextLatestFollowStateOnScroll,
1393
1595
  parseSseBlock,
1394
1596
  partsFromResponseBlocks,
1395
1597
  reasoningStepsFromParts,
@@ -1403,6 +1605,7 @@ function LatestThreadViewport({
1403
1605
  titleFromMessage,
1404
1606
  toolStateFromStatus,
1405
1607
  useAgents24ChatController,
1406
- useLatestThreadViewport
1608
+ useLatestThreadViewport,
1609
+ useStreamingText
1407
1610
  });
1408
1611
  //# sourceMappingURL=index.cjs.map