@agents24/chat-react 0.1.10 → 0.2.0

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-07-07
3
+ Last Updated: 2026-07-13
4
4
 
5
5
  Headless React primitives for Agents24 chat surfaces.
6
6
 
@@ -18,7 +18,7 @@ The package owns thread hydration, older-page pagination, stream/reattach state,
18
18
  - `getActiveStreamingTextPartId(message, streamingMessageId)`
19
19
  - `isActiveStreamingTextPart(message, part, streamingMessageId)`
20
20
  - `createFetchChatTransport(options)`
21
- - `consumeSseResponse(response, onEvent)`
21
+ - `consumeSseResponse(response, onEvent, { signal })`
22
22
  - `appendStableStreamingTurn(messages, userMessage, assistantMessage)`
23
23
  - `upsertStableAssistantMessage(messages, input)`
24
24
  - normalized chat message, part, transport, and storage types
@@ -37,10 +37,14 @@ Hosts can pass `onRuntimeEvent(event, context)` and `onThreadDetailLoaded(detail
37
37
 
38
38
  Network access is always injected by the host. This browser package never requires platform API keys.
39
39
 
40
+ `streamMessage`, `attachRun`, and `subscribeThreadEvents` require owner-provided abort signals. `createFetchChatTransport` forwards each signal through connection setup and SSE consumption, and `useAgents24ChatController` aborts owned requests when it unmounts. Local abort detaches the client stream; use `cancelRun` when the durable backend run must also stop.
41
+
40
42
  ## Message Lifecycle
41
43
 
42
44
  Chat surfaces should keep active assistant turns as stable rows in `messages`: append the user row and assistant placeholder together, stream updates into that assistant `messageId`, finalize that same row in place, then clear the streaming marker. `appendStableStreamingTurn` and `upsertStableAssistantMessage` centralize that invariant for custom hosts. Avoid host-side synthetic assistant rows because removing and re-adding the same response can flicker and can make MessageScroller treat the turn as new content.
43
45
 
46
+ The server owns assistant projection through `response_blocks` and `assistant_output_text`. Package version `0.2.0` removes the former public `textFromFinalOutput` helper; clients must not infer chat text from workflow `final_output`.
47
+
44
48
  ## MessageScroller Behavior
45
49
 
46
50
  The package re-exports shadcn `@shadcn/react/message-scroller` and provides `LatestThreadScroller`, a thin native preset around the official primitives. Host chat shells should prefer:
@@ -216,6 +216,7 @@ function LatestThreadScrollerOutline({
216
216
  );
217
217
  };
218
218
  updateSideRoom();
219
+ if (typeof ResizeObserver === "undefined") return;
219
220
  const resizeObserver = new ResizeObserver(updateSideRoom);
220
221
  resizeObserver.observe(root);
221
222
  const content = layout?.contentElement;
@@ -355,4 +356,4 @@ export {
355
356
  LatestThreadScrollerOutline,
356
357
  LatestThreadScroller
357
358
  };
358
- //# sourceMappingURL=chunk-E3TJ3I6G.js.map
359
+ //# sourceMappingURL=chunk-FFO6KEB4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/latest-thread-scroller.tsx"],"sourcesContent":["import * as React from \"react\";\nimport {\n MessageScroller,\n useMessageScroller,\n useMessageScrollerVisibility,\n} from \"@shadcn/react/message-scroller\";\n\nconst cx = (...values: Array<string | false | null | undefined>) =>\n values.filter(Boolean).join(\" \");\n\ntype MessageScrollerProviderProps = React.ComponentProps<\n typeof MessageScroller.Provider\n>;\ntype MessageScrollerRootProps = React.ComponentProps<typeof MessageScroller.Root>;\ntype MessageScrollerViewportProps = React.ComponentProps<\n typeof MessageScroller.Viewport\n>;\ntype MessageScrollerContentProps = React.ComponentProps<\n typeof MessageScroller.Content\n>;\ntype MessageScrollerItemProps = React.ComponentProps<typeof MessageScroller.Item>;\ntype MessageScrollerButtonProps = React.ComponentProps<typeof MessageScroller.Button>;\n\ntype LatestThreadScrollerLayoutContextValue = {\n contentElement: HTMLElement | null;\n rootElement: HTMLElement | null;\n setContentElement: (node: HTMLElement | null) => void;\n setRootElement: (node: HTMLElement | null) => void;\n};\n\nconst LatestThreadScrollerLayoutContext =\n React.createContext<LatestThreadScrollerLayoutContextValue | null>(null);\n\nconst useLatestThreadScrollerLayout = () =>\n React.useContext(LatestThreadScrollerLayoutContext);\n\nexport type LatestThreadScrollerProviderProps = MessageScrollerProviderProps;\n\nexport function LatestThreadScrollerProvider({\n autoScroll = true,\n defaultScrollPosition = \"last-anchor\",\n scrollPreviousItemPeek = 64,\n ...props\n}: LatestThreadScrollerProviderProps) {\n return (\n <MessageScroller.Provider\n autoScroll={autoScroll}\n defaultScrollPosition={defaultScrollPosition}\n scrollPreviousItemPeek={scrollPreviousItemPeek}\n {...props}\n />\n );\n}\n\nexport type LatestThreadScrollerRootProps = MessageScrollerRootProps;\n\nexport function LatestThreadScrollerRoot({\n className,\n ref,\n ...props\n}: LatestThreadScrollerRootProps) {\n const [rootElement, setRootElement] = React.useState<HTMLElement | null>(null);\n const [contentElement, setContentElement] =\n React.useState<HTMLElement | null>(null);\n const layoutContext = React.useMemo<LatestThreadScrollerLayoutContextValue>(\n () => ({\n contentElement,\n rootElement,\n setContentElement,\n setRootElement,\n }),\n [contentElement, rootElement]\n );\n const setRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n setRootElement(node);\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n },\n [ref]\n );\n\n return (\n <LatestThreadScrollerLayoutContext.Provider value={layoutContext}>\n <MessageScroller.Root\n ref={setRef}\n className={cx(\n \"relative flex size-full min-h-0 flex-1 flex-col overflow-hidden\",\n className\n )}\n {...props}\n />\n </LatestThreadScrollerLayoutContext.Provider>\n );\n}\n\nexport type LatestThreadScrollerViewportProps =\n MessageScrollerViewportProps & {\n onViewportChange?: (node: HTMLElement | null) => void;\n };\n\nexport function LatestThreadScrollerViewport({\n className,\n onViewportChange,\n preserveScrollOnPrepend = true,\n ref,\n ...props\n}: LatestThreadScrollerViewportProps) {\n const setRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n onViewportChange?.(node);\n },\n [onViewportChange, ref]\n );\n\n return (\n <MessageScroller.Viewport\n ref={setRef}\n className={cx(\n \"size-full min-h-0 min-w-0 flex-1 overflow-y-auto overscroll-contain\",\n className\n )}\n preserveScrollOnPrepend={preserveScrollOnPrepend}\n {...props}\n />\n );\n}\n\nexport type LatestThreadScrollerContentProps = MessageScrollerContentProps;\n\nexport function LatestThreadScrollerContent({\n className,\n ref,\n ...props\n}: LatestThreadScrollerContentProps) {\n const layout = useLatestThreadScrollerLayout();\n const setRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n layout?.setContentElement(node);\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n },\n [layout, ref]\n );\n\n return (\n <MessageScroller.Content\n ref={setRef}\n data-agents24-latest-thread-content=\"\"\n className={cx(\"flex h-max min-h-full flex-col gap-8 p-4\", className)}\n {...props}\n />\n );\n}\n\nexport type LatestThreadScrollerItemProps = Omit<\n MessageScrollerItemProps,\n \"messageId\"\n> & {\n messageId: string;\n};\n\nexport function LatestThreadScrollerItem({\n className,\n messageId,\n ...props\n}: LatestThreadScrollerItemProps) {\n return (\n <MessageScroller.Item\n className={cx(\n \"min-w-0 shrink-0 overflow-visible\",\n className\n )}\n messageId={messageId}\n {...props}\n />\n );\n}\n\nexport type LatestThreadScrollerButtonProps = Omit<\n MessageScrollerButtonProps,\n \"render\"\n> & {\n className?: string;\n render?: MessageScrollerButtonProps[\"render\"];\n};\n\nexport function LatestThreadScrollerButton({\n children,\n className,\n direction = \"end\",\n render,\n ...props\n}: LatestThreadScrollerButtonProps) {\n if (render) {\n return (\n <MessageScroller.Button\n direction={direction}\n render={render}\n {...props}\n />\n );\n }\n\n return (\n <MessageScroller.Button\n direction={direction}\n render={(buttonProps) => (\n <button\n {...(buttonProps as Record<string, unknown>)}\n aria-label={\n direction === \"end\"\n ? \"Scroll to latest message\"\n : \"Scroll to first message\"\n }\n className={cx(\n \"absolute bottom-4 left-1/2 z-50 flex size-9 -translate-x-1/2 items-center justify-center rounded-full border border-border bg-background text-foreground shadow-md transition-all duration-200 data-[active=false]:pointer-events-none data-[active=false]:translate-y-2 data-[active=false]:opacity-0 data-[active=true]:translate-y-0 data-[active=true]:opacity-100\",\n className\n )}\n type=\"button\"\n >\n {children ?? (direction === \"end\" ? \"↓\" : \"↑\")}\n </button>\n )}\n {...props}\n />\n );\n}\n\nexport type LatestThreadScrollerOutlineItem = {\n id: string;\n label: string;\n messageIds?: string[];\n responsePreview?: string;\n};\n\nexport type LatestThreadScrollerOutlineProps = {\n cardClassName?: string;\n className?: string;\n contentMaxWidth?: number;\n dir?: \"ltr\" | \"rtl\";\n items: LatestThreadScrollerOutlineItem[];\n minContainerWidthForRail?: number;\n minSideGutterForRail?: number;\n side?: \"left\" | \"right\";\n};\n\nexport function LatestThreadScrollerOutline({\n cardClassName,\n className,\n contentMaxWidth = 768,\n dir = \"ltr\",\n items,\n minContainerWidthForRail = 0,\n minSideGutterForRail = 48,\n side,\n}: LatestThreadScrollerOutlineProps) {\n const navRef = React.useRef<HTMLElement | null>(null);\n const layout = useLatestThreadScrollerLayout();\n const [hasSideRoom, setHasSideRoom] = React.useState(false);\n const [hoveredIndex, setHoveredIndex] = React.useState<number | null>(null);\n const { scrollToMessage } = useMessageScroller();\n const { currentAnchorId, visibleMessageIds } = useMessageScrollerVisibility();\n const visibleMessageIdSet = React.useMemo(\n () => new Set(visibleMessageIds),\n [visibleMessageIds]\n );\n const focusIndex = hoveredIndex ?? -1;\n const railSide = side ?? (dir === \"rtl\" ? \"right\" : \"left\");\n\n React.useEffect(() => {\n const root = layout?.rootElement ?? navRef.current?.parentElement;\n if (!root) return;\n\n const updateSideRoom = () => {\n const rootRect = root.getBoundingClientRect();\n const rootWidth = rootRect.width;\n const content = layout?.contentElement;\n const sideGutter = content\n ? Math.max(\n 0,\n railSide === \"left\"\n ? content.getBoundingClientRect().left - rootRect.left\n : rootRect.right - content.getBoundingClientRect().right\n )\n : Math.max(0, (rootWidth - contentMaxWidth) / 2);\n const nextHasSideRoom =\n rootWidth >= minContainerWidthForRail &&\n sideGutter >= minSideGutterForRail;\n setHasSideRoom((current) =>\n current === nextHasSideRoom ? current : nextHasSideRoom\n );\n };\n\n updateSideRoom();\n if (typeof ResizeObserver === \"undefined\") return;\n const resizeObserver = new ResizeObserver(updateSideRoom);\n resizeObserver.observe(root);\n const content = layout?.contentElement;\n if (content) {\n resizeObserver.observe(content);\n }\n return () => resizeObserver.disconnect();\n }, [\n contentMaxWidth,\n layout?.contentElement,\n layout?.rootElement,\n minContainerWidthForRail,\n minSideGutterForRail,\n railSide,\n ]);\n\n if (items.length < 2) {\n return null;\n }\n\n return (\n <nav\n ref={navRef}\n aria-label=\"Transcript outline\"\n className={cx(\n \"pointer-events-none absolute top-[46%] z-10 max-h-[min(18rem,calc(100%-10rem))] w-16 -translate-y-1/2 overflow-visible\",\n hasSideRoom ? \"hidden md:block\" : \"hidden\",\n railSide === \"left\" ? \"left-1\" : \"right-1\",\n className\n )}\n dir={dir}\n >\n <div className=\"pointer-events-auto flex flex-col gap-0 py-1\">\n {items.map((item, index) => {\n const itemMessageIds =\n item.messageIds && item.messageIds.length > 0\n ? item.messageIds\n : [item.id];\n const active = itemMessageIds.some(\n (messageId) =>\n visibleMessageIdSet.has(messageId) || messageId === currentAnchorId\n );\n const distance =\n focusIndex >= 0 ? Math.abs(index - focusIndex) : Number.POSITIVE_INFINITY;\n const isFocused = distance === 0;\n const isNear = distance === 1;\n const isNextNear = distance === 2;\n const barWidth = isFocused\n ? 50\n : isNear\n ? 36\n : isNextNear\n ? 23\n : 10;\n const barOpacity = isFocused ? 1 : isNear ? 0.78 : isNextNear ? 0.58 : 0.36;\n const fallbackLabel = `Turn ${index + 1}`;\n const previewLabel =\n item.label && item.label !== fallbackLabel ? item.label : \"\";\n const responsePreview = (item.responsePreview || \"\").trim();\n\n return (\n <button\n key={item.id}\n type=\"button\"\n data-active={active}\n dir=\"ltr\"\n className={cx(\n \"group relative flex h-2 w-16 items-center rounded-sm outline-none\",\n railSide === \"left\" ? \"justify-start\" : \"justify-end\"\n )}\n aria-label={`Jump to ${item.label || fallbackLabel}`}\n onBlur={() => setHoveredIndex(null)}\n onFocus={() => setHoveredIndex(index)}\n onMouseEnter={() => setHoveredIndex(index)}\n onMouseLeave={() => setHoveredIndex(null)}\n onClick={() =>\n scrollToMessage(item.id, {\n align: \"start\",\n behavior: \"smooth\",\n })\n }\n >\n <span\n className=\"h-0.5 rounded-full bg-muted-foreground/55 transition-[width,background-color,opacity] duration-150 ease-out group-hover:bg-foreground group-focus-visible:bg-foreground group-data-[active=true]:bg-foreground\"\n style={{\n opacity: barOpacity,\n width: barWidth,\n }}\n />\n {previewLabel ? (\n <span\n dir={dir}\n className={cx(\n \"pointer-events-none absolute top-1/2 z-20 max-h-32 w-64 -translate-y-1/2 overflow-hidden rounded-lg border border-border/80 bg-background p-3 text-start opacity-0 shadow-lg transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100 group-focus-visible:pointer-events-auto group-focus-visible:opacity-100\",\n railSide === \"left\"\n ? \"left-full ml-2 -translate-x-1 group-hover:translate-x-0 group-focus-visible:translate-x-0\"\n : \"right-full mr-2 translate-x-1 group-hover:translate-x-0 group-focus-visible:translate-x-0\",\n cardClassName\n )}\n >\n <span\n className=\"block overflow-hidden text-sm leading-5 text-foreground\"\n style={{\n display: \"-webkit-box\",\n WebkitBoxOrient: \"vertical\",\n WebkitLineClamp: responsePreview ? 2 : 4,\n }}\n >\n {previewLabel}\n </span>\n {responsePreview ? (\n <span\n className=\"mt-1 block overflow-hidden text-sm leading-5 text-muted-foreground\"\n style={{\n display: \"-webkit-box\",\n WebkitBoxOrient: \"vertical\",\n WebkitLineClamp: 2,\n }}\n >\n {responsePreview}\n </span>\n ) : null}\n </span>\n ) : null}\n </button>\n );\n })}\n </div>\n </nav>\n );\n}\n\nexport const LatestThreadScroller = {\n Provider: LatestThreadScrollerProvider,\n Root: LatestThreadScrollerRoot,\n Viewport: LatestThreadScrollerViewport,\n Content: LatestThreadScrollerContent,\n Item: LatestThreadScrollerItem,\n Button: LatestThreadScrollerButton,\n Outline: LatestThreadScrollerOutline,\n};\n"],"mappings":"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","names":["content"]}
package/dist/index.cjs CHANGED
@@ -71,7 +71,6 @@ __export(index_exports, {
71
71
  partsFromResponseBlocks: () => partsFromResponseBlocks,
72
72
  reasoningStepsFromParts: () => reasoningStepsFromParts,
73
73
  renderChatPart: () => renderChatPart,
74
- textFromFinalOutput: () => textFromFinalOutput,
75
74
  threadActivityDate: () => threadActivityDate,
76
75
  threadDetailToMessages: () => threadDetailToMessages,
77
76
  threadPaging: () => threadPaging,
@@ -295,12 +294,6 @@ var titleFromMessage = (text, files = []) => {
295
294
  var threadActivityDate = (thread) => String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString());
296
295
  var assistantTextFromResponseBlocks = (blocks) => (blocks || []).filter((block) => block.kind === "assistant_text" && typeof block.text === "string").map((block) => String(block.text)).join("\n\n").trim();
297
296
  var assistantTextFromParts = (parts) => (parts || []).filter((part) => part.kind === "text").map((part) => part.text).join("\n\n").trim();
298
- var textFromFinalOutput = (value) => {
299
- if (typeof value === "string") return value;
300
- if (!value || typeof value !== "object") return "";
301
- const record = value;
302
- return String(record.message || record.text || record.answer || "");
303
- };
304
297
  var displayTextWithoutInlineAttachments = (text, hasAttachments) => {
305
298
  if (!hasAttachments) return text;
306
299
  const marker = "Attached text file (";
@@ -347,7 +340,7 @@ var responseBlocksFromTurn = (turn) => {
347
340
  var assistantTextFromEvents = (events) => {
348
341
  const assistantText = latestEventPayloadValue(events, "assistant_output_text");
349
342
  if (typeof assistantText === "string" && assistantText.trim()) return assistantText;
350
- return textFromFinalOutput(latestEventPayloadValue(events, "final_output"));
343
+ return "";
351
344
  };
352
345
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
353
346
  var optionalString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -531,7 +524,7 @@ var turnToMessages = (turn, activeRunId) => {
531
524
  attachments
532
525
  });
533
526
  }
534
- const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events) || textFromFinalOutput(turn.final_output);
527
+ const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events);
535
528
  if (assistantText || responseBlocks.length > 0 || isRunning) {
536
529
  const parts = partsFromResponseBlocks(responseBlocks, assistantText);
537
530
  messages.push({
@@ -779,6 +772,7 @@ function useAgents24ChatController({
779
772
  const activeRunIdRef = (0, import_react2.useRef)(null);
780
773
  const reattachedRunIdRef = (0, import_react2.useRef)(null);
781
774
  const abortControllerRef = (0, import_react2.useRef)(null);
775
+ const [lifecycleAbortController] = (0, import_react2.useState)(() => new AbortController());
782
776
  const streamingContentRef = (0, import_react2.useRef)("");
783
777
  const streamingMessageIdRef = (0, import_react2.useRef)(null);
784
778
  const reasoningRef = (0, import_react2.useRef)([]);
@@ -853,15 +847,16 @@ function useAgents24ChatController({
853
847
  const seq = ++refreshSeqRef.current;
854
848
  setIsRefreshingThreads(true);
855
849
  try {
856
- const data = await transport.listThreads();
850
+ const data = await transport.listThreads({ signal: lifecycleAbortController.signal });
851
+ if (lifecycleAbortController.signal.aborted) return;
857
852
  if (seq !== refreshSeqRef.current) return;
858
853
  const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
859
854
  storage.setThreads(mergeStoredThreadsForRefresh(storage.listThreads(), nextThreads));
860
855
  setThreads(storage.listThreads());
861
856
  } finally {
862
- if (seq === refreshSeqRef.current) setIsRefreshingThreads(false);
857
+ if (!lifecycleAbortController.signal.aborted && seq === refreshSeqRef.current) setIsRefreshingThreads(false);
863
858
  }
864
- }, [storage, transport]);
859
+ }, [lifecycleAbortController, storage, transport]);
865
860
  const applyThreadId = (0, import_react2.useCallback)(
866
861
  (threadId, baseMessages) => {
867
862
  if (!threadId || activeThreadIdRef.current === threadId) return;
@@ -1016,7 +1011,8 @@ function useAgents24ChatController({
1016
1011
  const detail = await transport.getThread({
1017
1012
  threadId,
1018
1013
  limit: pageSize,
1019
- includeRunEvents: false
1014
+ includeRunEvents: false,
1015
+ signal: lifecycleAbortController.signal
1020
1016
  });
1021
1017
  if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
1022
1018
  void onThreadDetailLoaded?.(detail);
@@ -1045,10 +1041,10 @@ function useAgents24ChatController({
1045
1041
  }
1046
1042
  throw error;
1047
1043
  } finally {
1048
- if (activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
1044
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
1049
1045
  }
1050
1046
  },
1051
- [clearMissingThread, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
1047
+ [clearMissingThread, lifecycleAbortController, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
1052
1048
  );
1053
1049
  const loadOlderTurns = (0, import_react2.useCallback)(async () => {
1054
1050
  const threadId = activeThreadIdRef.current;
@@ -1061,7 +1057,8 @@ function useAgents24ChatController({
1061
1057
  threadId,
1062
1058
  limit: pageSize,
1063
1059
  beforeTurnIndex,
1064
- includeRunEvents: false
1060
+ includeRunEvents: false,
1061
+ signal: lifecycleAbortController.signal
1065
1062
  });
1066
1063
  if (activeThreadIdRef.current !== threadId) return;
1067
1064
  const older = threadDetailToMessages(detail);
@@ -1081,10 +1078,10 @@ function useAgents24ChatController({
1081
1078
  }
1082
1079
  throw error;
1083
1080
  } finally {
1084
- if (activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
1081
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
1085
1082
  isLoadingOlderRef.current = false;
1086
1083
  }
1087
- }, [clearMissingThread, pageSize, persistThread, transport]);
1084
+ }, [clearMissingThread, lifecycleAbortController, pageSize, persistThread, transport]);
1088
1085
  const handleStreamEvent = (0, import_react2.useCallback)(
1089
1086
  (input) => {
1090
1087
  const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
@@ -1131,7 +1128,7 @@ function useAgents24ChatController({
1131
1128
  streamThreadIdRef.current = terminalThreadId;
1132
1129
  applyThreadId(terminalThreadId, baseMessages);
1133
1130
  }
1134
- const finalText = String(payload.assistant_output_text || textFromFinalOutput(payload.final_output) || streamingContentRef.current || "");
1131
+ const finalText = String(payload.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks || []) || streamingContentRef.current || "");
1135
1132
  finalizeAssistantMessage({
1136
1133
  threadId: streamThreadIdRef.current || activeThreadIdRef.current,
1137
1134
  baseMessages: messagesRef.current,
@@ -1305,8 +1302,19 @@ function useAgents24ChatController({
1305
1302
  }
1306
1303
  }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
1307
1304
  (0, import_react2.useEffect)(() => {
1308
- refresh().catch(() => setThreads(storage.listThreads()));
1309
- }, [refresh, storage]);
1305
+ refresh().catch((error) => {
1306
+ if (!isAbortError(error) && !lifecycleAbortController.signal.aborted) {
1307
+ setThreads(storage.listThreads());
1308
+ }
1309
+ });
1310
+ }, [lifecycleAbortController, refresh, storage]);
1311
+ (0, import_react2.useEffect)(() => {
1312
+ return () => {
1313
+ lifecycleAbortController.abort();
1314
+ abortDetachedStream(abortControllerRef.current);
1315
+ abortControllerRef.current = null;
1316
+ };
1317
+ }, [lifecycleAbortController]);
1310
1318
  (0, import_react2.useEffect)(() => {
1311
1319
  if (!transport.subscribeThreadEvents) return;
1312
1320
  let cancelled = false;
@@ -1737,6 +1745,7 @@ function LatestThreadScrollerOutline({
1737
1745
  );
1738
1746
  };
1739
1747
  updateSideRoom();
1748
+ if (typeof ResizeObserver === "undefined") return;
1740
1749
  const resizeObserver = new ResizeObserver(updateSideRoom);
1741
1750
  resizeObserver.observe(root);
1742
1751
  const content = layout?.contentElement;
@@ -1946,7 +1955,9 @@ var consumeSseResponse = async (response, onEvent, options = {}) => {
1946
1955
  let closed = false;
1947
1956
  const closeReader = () => {
1948
1957
  closed = true;
1949
- void reader.cancel().catch(() => {
1958
+ queueMicrotask(() => {
1959
+ void reader.cancel().catch(() => {
1960
+ });
1950
1961
  });
1951
1962
  };
1952
1963
  if (options.signal?.aborted) {
@@ -2170,7 +2181,7 @@ var createFetchChatTransport = ({
2170
2181
  }
2171
2182
  )
2172
2183
  });
2173
- return consumeSseResponse(response, onEvent);
2184
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
2174
2185
  },
2175
2186
  async attachRun(input, onEvent) {
2176
2187
  const response = await fetchImpl(routes.attachRun(input), {
@@ -2179,7 +2190,7 @@ var createFetchChatTransport = ({
2179
2190
  headers: streamHeaders(await loadHeaders()),
2180
2191
  body: JSON.stringify({})
2181
2192
  });
2182
- return consumeSseResponse(response, onEvent);
2193
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
2183
2194
  },
2184
2195
  async cancelRun(input) {
2185
2196
  const response = await fetchImpl(routes.cancelRun(input), {
@@ -2206,7 +2217,11 @@ var createFetchChatTransport = ({
2206
2217
  signal: input.signal,
2207
2218
  headers: streamHeaders(await loadHeaders())
2208
2219
  });
2209
- await consumeSseResponse(response, (event) => onEvent(event));
2220
+ await consumeSseResponse(
2221
+ response,
2222
+ (event) => onEvent(event),
2223
+ { signal: input.signal }
2224
+ );
2210
2225
  },
2211
2226
  async resumeHitl(input) {
2212
2227
  if (!routes.resumeRun) throw new ChatTransportError("HITL resume is not configured.", 501);
@@ -2286,7 +2301,6 @@ var import_message_scroller2 = require("@shadcn/react/message-scroller");
2286
2301
  partsFromResponseBlocks,
2287
2302
  reasoningStepsFromParts,
2288
2303
  renderChatPart,
2289
- textFromFinalOutput,
2290
2304
  threadActivityDate,
2291
2305
  threadDetailToMessages,
2292
2306
  threadPaging,