@agents24/chat-react 0.1.4 → 0.1.6

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
@@ -405,6 +405,7 @@ type FetchChatTransportOptions = {
405
405
  };
406
406
  declare const createFetchChatTransport: ({ routes, fetchImpl, headers, streamBody, }: FetchChatTransportOptions) => ChatTransport;
407
407
 
408
+ type LatestFollowState = "following" | "detached";
408
409
  declare const isScrollable: (element: Pick<HTMLDivElement, "scrollHeight" | "clientHeight">) => boolean;
409
410
  declare const isAtTimelineLatestEdge: (element: Pick<HTMLDivElement, "scrollHeight" | "clientHeight" | "scrollTop">, isTopOrigin: boolean) => boolean;
410
411
  declare const shouldPrefetchOlder: (element: Pick<HTMLDivElement, "scrollHeight" | "clientHeight" | "scrollTop">) => boolean;
@@ -414,6 +415,11 @@ declare const shouldUseTopOriginTimeline: ({ hasOlder, itemCount, topOriginMaxIt
414
415
  itemCount: number;
415
416
  topOriginMaxItems?: number;
416
417
  }) => boolean;
418
+ declare const nextLatestFollowStateOnScroll: ({ atLatest, current, isProgrammatic, }: {
419
+ atLatest: boolean;
420
+ current: LatestFollowState;
421
+ isProgrammatic: boolean;
422
+ }) => LatestFollowState;
417
423
  type UseLatestThreadViewportOptions = {
418
424
  itemCount: number;
419
425
  hasOlder: boolean;
@@ -426,15 +432,22 @@ type UseLatestThreadViewportOptions = {
426
432
  type LatestThreadViewportState = {
427
433
  scrollContainerRef: RefObject<HTMLDivElement | null>;
428
434
  isAtLatest: boolean;
435
+ isFollowingLatest: boolean;
429
436
  isTopOrigin: boolean;
430
437
  shouldShowLatestButton: boolean;
431
438
  handleScroll: (event: UIEvent<HTMLDivElement>) => void;
432
- scrollToLatest: () => void;
439
+ handleUserScrollIntent: () => void;
440
+ scrollToLatest: (options?: {
441
+ reattach?: boolean;
442
+ }) => void;
443
+ detachFromLatest: () => void;
444
+ reattachToLatest: () => void;
433
445
  preserveIntrinsicResize: () => void;
434
446
  };
435
447
  declare function useLatestThreadViewport({ itemCount, hasOlder, isLoadingOlder, onLoadOlder, activeStreamKey, shouldAutoFollow, topOriginMaxItems, }: UseLatestThreadViewportOptions): LatestThreadViewportState;
436
448
  type LatestThreadViewportRenderContext = {
437
449
  preserveIntrinsicResize: () => void;
450
+ isFollowingLatest: boolean;
438
451
  isTopOrigin: boolean;
439
452
  };
440
453
  type LatestThreadViewportProps<T> = {
@@ -450,4 +463,4 @@ type LatestThreadViewportProps<T> = {
450
463
  };
451
464
  declare function LatestThreadViewport<T>({ items, hasOlder, isLoadingOlder, onLoadOlder, activeStreamKey, className, latestSpacer, trailingSpacer, renderItem, }: LatestThreadViewportProps<T>): react.JSX.Element;
452
465
 
453
- 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 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, parseSseBlock, partsFromResponseBlocks, reasoningStepsFromParts, renderChatPart, shouldPrefetchOlder, shouldUseTopOriginTimeline, textFromFinalOutput, threadActivityDate, threadDetailToMessages, threadPaging, titleFromMessage, toolStateFromStatus, useAgents24ChatController, useLatestThreadViewport, useStreamingText };
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
@@ -1274,6 +1274,14 @@ var shouldUseTopOriginTimeline = ({
1274
1274
  itemCount,
1275
1275
  topOriginMaxItems = 4
1276
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
+ };
1277
1285
  function useLatestThreadViewport({
1278
1286
  itemCount,
1279
1287
  hasOlder,
@@ -1287,12 +1295,19 @@ function useLatestThreadViewport({
1287
1295
  const olderPagePreserveRef = useRef3(null);
1288
1296
  const olderPageRequestInFlightRef = useRef3(false);
1289
1297
  const intrinsicResizePreserveRef = useRef3(null);
1290
- const autoFollowLatestRef = useRef3(true);
1298
+ const followStateRef = useRef3("following");
1291
1299
  const programmaticScrollRef = useRef3(false);
1292
1300
  const programmaticScrollTimeoutRef = useRef3(null);
1293
1301
  const activeStreamKeyRef = useRef3(null);
1302
+ const observedScrollHeightRef = useRef3(null);
1294
1303
  const [isAtLatest, setIsAtLatest] = useState3(true);
1304
+ const [followState, setFollowState] = useState3("following");
1295
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
+ }, []);
1296
1311
  const markProgrammaticScroll = useCallback2(() => {
1297
1312
  programmaticScrollRef.current = true;
1298
1313
  if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
@@ -1301,13 +1316,26 @@ function useLatestThreadViewport({
1301
1316
  programmaticScrollTimeoutRef.current = null;
1302
1317
  }, 80);
1303
1318
  }, []);
1304
- const scrollToLatest = useCallback2(() => {
1319
+ const scrollToLatest = useCallback2((options) => {
1305
1320
  const element = scrollContainerRef.current;
1306
1321
  if (!element) return;
1322
+ if (options?.reattach !== false) setLatestFollowState("following");
1307
1323
  markProgrammaticScroll();
1308
1324
  element.scrollTop = getLatestScrollTop(element, isTopOrigin);
1309
1325
  setIsAtLatest(true);
1310
- }, [isTopOrigin, markProgrammaticScroll]);
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]);
1311
1339
  useEffect3(() => {
1312
1340
  return () => {
1313
1341
  if (programmaticScrollTimeoutRef.current) clearTimeout(programmaticScrollTimeoutRef.current);
@@ -1319,6 +1347,7 @@ function useLatestThreadViewport({
1319
1347
  const element = scrollContainerRef.current;
1320
1348
  const preserve = olderPagePreserveRef.current;
1321
1349
  if (!element) return;
1350
+ observedScrollHeightRef.current = element.scrollHeight;
1322
1351
  if (!preserve) {
1323
1352
  setIsAtLatest(isAtTimelineLatestEdge(element, isTopOrigin));
1324
1353
  return;
@@ -1335,7 +1364,13 @@ function useLatestThreadViewport({
1335
1364
  const element = event.currentTarget;
1336
1365
  const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
1337
1366
  setIsAtLatest(atLatest);
1338
- if (!programmaticScrollRef.current) autoFollowLatestRef.current = atLatest;
1367
+ setLatestFollowState(
1368
+ nextLatestFollowStateOnScroll({
1369
+ atLatest,
1370
+ current: followStateRef.current,
1371
+ isProgrammatic: programmaticScrollRef.current
1372
+ })
1373
+ );
1339
1374
  if (isTopOrigin || !hasOlder || isLoadingOlder || olderPageRequestInFlightRef.current || !shouldPrefetchOlder(element)) {
1340
1375
  return;
1341
1376
  }
@@ -1352,7 +1387,7 @@ function useLatestThreadViewport({
1352
1387
  });
1353
1388
  });
1354
1389
  },
1355
- [hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder]
1390
+ [hasOlder, isLoadingOlder, isTopOrigin, onLoadOlder, setLatestFollowState]
1356
1391
  );
1357
1392
  const preserveIntrinsicResize = useCallback2(() => {
1358
1393
  const element = scrollContainerRef.current;
@@ -1383,37 +1418,73 @@ function useLatestThreadViewport({
1383
1418
  };
1384
1419
  intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
1385
1420
  }, [isTopOrigin, markProgrammaticScroll]);
1386
- useEffect3(() => {
1421
+ useLayoutEffect(() => {
1387
1422
  const key = activeStreamKey || null;
1388
- if (!key || activeStreamKeyRef.current === key) {
1389
- activeStreamKeyRef.current = key;
1423
+ const previousKey = activeStreamKeyRef.current;
1424
+ if (!key) {
1425
+ activeStreamKeyRef.current = null;
1426
+ if (previousKey && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1427
+ const frame2 = requestAnimationFrame(() => {
1428
+ if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1429
+ scrollToLatest({ reattach: false });
1430
+ }
1431
+ });
1432
+ return () => cancelAnimationFrame(frame2);
1433
+ }
1390
1434
  return;
1391
1435
  }
1392
- autoFollowLatestRef.current = true;
1393
- if (isTopOrigin) {
1436
+ if (previousKey === key) {
1394
1437
  activeStreamKeyRef.current = key;
1395
1438
  return;
1396
1439
  }
1397
- const frame = requestAnimationFrame(scrollToLatest);
1440
+ setLatestFollowState("following");
1441
+ const frame = requestAnimationFrame(() => scrollToLatest());
1398
1442
  activeStreamKeyRef.current = key;
1399
1443
  return () => cancelAnimationFrame(frame);
1400
- }, [activeStreamKey, isTopOrigin, scrollToLatest]);
1444
+ }, [activeStreamKey, isLoadingOlder, scrollToLatest, setLatestFollowState, shouldAutoFollow]);
1401
1445
  useLayoutEffect(() => {
1402
- if (isTopOrigin) return;
1403
- if (!activeStreamKey || !shouldAutoFollow || !autoFollowLatestRef.current) return;
1446
+ if (!activeStreamKey || !shouldAutoFollow || followStateRef.current !== "following") return;
1404
1447
  if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
1405
1448
  const frame = requestAnimationFrame(() => {
1406
- if (autoFollowLatestRef.current && !isLoadingOlder && !olderPageRequestInFlightRef.current) scrollToLatest();
1449
+ if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1450
+ scrollToLatest({ reattach: false });
1451
+ }
1407
1452
  });
1408
1453
  return () => cancelAnimationFrame(frame);
1409
- }, [activeStreamKey, isLoadingOlder, isTopOrigin, itemCount, scrollToLatest, shouldAutoFollow]);
1454
+ }, [activeStreamKey, isLoadingOlder, itemCount, scrollToLatest, shouldAutoFollow]);
1455
+ useEffect3(() => {
1456
+ if (!activeStreamKey) {
1457
+ observedScrollHeightRef.current = scrollContainerRef.current?.scrollHeight ?? null;
1458
+ return;
1459
+ }
1460
+ let frame = null;
1461
+ const watchStreamResize = () => {
1462
+ const element = scrollContainerRef.current;
1463
+ if (!element) return;
1464
+ const previousHeight = observedScrollHeightRef.current;
1465
+ const nextHeight = element.scrollHeight;
1466
+ observedScrollHeightRef.current = nextHeight;
1467
+ if (previousHeight !== null && Math.abs(nextHeight - previousHeight) >= 1 && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
1468
+ scrollToLatest({ reattach: false });
1469
+ }
1470
+ frame = requestAnimationFrame(watchStreamResize);
1471
+ };
1472
+ frame = requestAnimationFrame(watchStreamResize);
1473
+ return () => {
1474
+ if (frame !== null) cancelAnimationFrame(frame);
1475
+ };
1476
+ }, [activeStreamKey, isLoadingOlder, scrollToLatest, shouldAutoFollow]);
1410
1477
  return {
1411
1478
  scrollContainerRef,
1412
1479
  isAtLatest,
1480
+ isFollowingLatest: followState === "following",
1413
1481
  isTopOrigin,
1414
1482
  shouldShowLatestButton: !isAtLatest,
1415
1483
  handleScroll,
1484
+ handleUserScrollIntent,
1416
1485
  scrollToLatest,
1486
+ detachFromLatest,
1487
+ reattachToLatest,
1417
1488
  preserveIntrinsicResize
1418
1489
  };
1419
1490
  }
@@ -1441,7 +1512,10 @@ function LatestThreadViewport({
1441
1512
  {
1442
1513
  ref: viewport.scrollContainerRef,
1443
1514
  className,
1515
+ onKeyDown: viewport.handleUserScrollIntent,
1444
1516
  onScroll: viewport.handleScroll,
1517
+ onTouchMove: viewport.handleUserScrollIntent,
1518
+ onWheel: viewport.handleUserScrollIntent,
1445
1519
  role: "log",
1446
1520
  style: { overflowAnchor: "none" },
1447
1521
  children: [
@@ -1474,6 +1548,7 @@ export {
1474
1548
  isScrollable,
1475
1549
  latestContextWindowFromThread,
1476
1550
  mergeReasoningSteps,
1551
+ nextLatestFollowStateOnScroll,
1477
1552
  parseSseBlock,
1478
1553
  partsFromResponseBlocks,
1479
1554
  reasoningStepsFromParts,