@agents24/chat-react 0.1.10 → 0.3.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/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  LatestThreadScrollerProvider,
8
8
  LatestThreadScrollerRoot,
9
9
  LatestThreadScrollerViewport
10
- } from "./chunk-E3TJ3I6G.js";
10
+ } from "./chunk-FFO6KEB4.js";
11
11
  import {
12
12
  getActiveStreamingTextPartId,
13
13
  isActiveStreamingTextPart,
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-EWZO4QJI.js";
16
16
 
17
17
  // src/controller.ts
18
- import { useCallback as useCallback2, useEffect, useMemo, useRef, useState } from "react";
18
+ import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
19
19
 
20
20
  // src/controller-actions.ts
21
21
  import { useCallback } from "react";
@@ -89,6 +89,47 @@ function useControllerMessageActions(input) {
89
89
  return { handleCopy, handleDislike, handleLike, handleRetry, startNewThread, upsertLiveVoiceMessage };
90
90
  }
91
91
 
92
+ // src/controller-hitl.ts
93
+ import { useCallback as useCallback2, useMemo, useState } from "react";
94
+ function useControllerHitl({
95
+ messages,
96
+ transport,
97
+ activeRunId,
98
+ activeRunIdRef,
99
+ activeThreadIdRef,
100
+ runStream
101
+ }) {
102
+ const [isResolvingHitl, setIsResolvingHitl] = useState(false);
103
+ const pendingHitl = useMemo(() => {
104
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
105
+ const parts = messages[messageIndex]?.parts || [];
106
+ for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) {
107
+ const part = parts[partIndex];
108
+ if (part?.kind === "hitl" && part.status === "pending") return part;
109
+ }
110
+ }
111
+ return null;
112
+ }, [messages]);
113
+ const resumeHitl = useCallback2(async (input) => {
114
+ if (!transport.resumeHitl) throw new Error("HITL resume is not configured.");
115
+ const runId = input.runId || activeRunIdRef.current || activeRunId;
116
+ if (!runId) throw new Error("No paused run is available to resume.");
117
+ setIsResolvingHitl(true);
118
+ try {
119
+ const result = await transport.resumeHitl({ ...input, runId });
120
+ const threadId = result.thread_id || activeThreadIdRef.current;
121
+ if (threadId) await runStream({ mode: "attach", runId: result.run_id || runId, threadId });
122
+ return result;
123
+ } finally {
124
+ setIsResolvingHitl(false);
125
+ }
126
+ }, [activeRunId, activeRunIdRef, activeThreadIdRef, runStream, transport]);
127
+ return { pendingHitl, isResolvingHitl, resumeHitl };
128
+ }
129
+
130
+ // src/controller-thread-events.ts
131
+ import { useEffect, useRef } from "react";
132
+
92
133
  // src/context-window.ts
93
134
  var SOURCE_PRIORITY = {
94
135
  unknown: 0,
@@ -223,12 +264,6 @@ var titleFromMessage = (text, files = []) => {
223
264
  var threadActivityDate = (thread) => String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString());
224
265
  var assistantTextFromResponseBlocks = (blocks) => (blocks || []).filter((block) => block.kind === "assistant_text" && typeof block.text === "string").map((block) => String(block.text)).join("\n\n").trim();
225
266
  var assistantTextFromParts = (parts) => (parts || []).filter((part) => part.kind === "text").map((part) => part.text).join("\n\n").trim();
226
- var textFromFinalOutput = (value) => {
227
- if (typeof value === "string") return value;
228
- if (!value || typeof value !== "object") return "";
229
- const record = value;
230
- return String(record.message || record.text || record.answer || "");
231
- };
232
267
  var displayTextWithoutInlineAttachments = (text, hasAttachments) => {
233
268
  if (!hasAttachments) return text;
234
269
  const marker = "Attached text file (";
@@ -275,7 +310,7 @@ var responseBlocksFromTurn = (turn) => {
275
310
  var assistantTextFromEvents = (events) => {
276
311
  const assistantText = latestEventPayloadValue(events, "assistant_output_text");
277
312
  if (typeof assistantText === "string" && assistantText.trim()) return assistantText;
278
- return textFromFinalOutput(latestEventPayloadValue(events, "final_output"));
313
+ return "";
279
314
  };
280
315
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
281
316
  var optionalString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -379,13 +414,34 @@ var partsFromResponseBlocks = (blocks, fallbackText) => {
379
414
  }
380
415
  if (block.kind === "hitl_request") {
381
416
  const hitl = asRecord(block.hitl) || block;
417
+ if (hitl.schema_version !== "agents24.hitl.interrupt.v2") {
418
+ throw new Error("Invalid V2 HITL response block.");
419
+ }
420
+ const interruptId = optionalString(block.interruptId) || optionalString(hitl.interrupt_id);
421
+ const hitlKind = optionalString(block.hitlKind) || optionalString(hitl.kind);
422
+ const status = optionalString(block.status) || optionalString(hitl.status) || "pending";
423
+ const allowedActions = Array.isArray(hitl.allowed_actions) ? hitl.allowed_actions.filter((value) => ["approve", "reject", "connect", "skip"].includes(String(value))) : [];
424
+ if (!interruptId || allowedActions.length === 0 || !["tool_review", "mcp_auth", "user_approval", "app_data_permission"].includes(String(hitlKind))) {
425
+ throw new Error("Invalid V2 HITL response block.");
426
+ }
427
+ const resolution = asRecord(block.resolution);
382
428
  parts.push({
383
429
  id,
384
430
  type: "hitl",
385
431
  kind: "hitl",
386
- hitl,
387
- interruptId: optionalString(block.interruptId) || optionalString(hitl.interrupt_id) || null,
388
- hitlKind: optionalString(block.hitlKind) || optionalString(hitl.kind) || null,
432
+ interruptId,
433
+ hitlKind,
434
+ message: optionalString(hitl.message) || optionalString(block.text) || "Input is required to continue.",
435
+ allowedActions,
436
+ status,
437
+ presentation: asRecord(hitl.presentation) || {},
438
+ resolution: resolution ? {
439
+ action: ["approve", "reject", "connect", "skip"].includes(String(resolution.action)) ? String(resolution.action) : null,
440
+ outcome: optionalString(resolution.outcome),
441
+ reason: optionalString(resolution.reason),
442
+ resolvedAt: optionalString(resolution.resolved_at),
443
+ resolver: asRecord(resolution.resolver)
444
+ } : null,
389
445
  raw: block
390
446
  });
391
447
  return;
@@ -459,7 +515,7 @@ var turnToMessages = (turn, activeRunId) => {
459
515
  attachments
460
516
  });
461
517
  }
462
- const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events) || textFromFinalOutput(turn.final_output);
518
+ const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events);
463
519
  if (assistantText || responseBlocks.length > 0 || isRunning) {
464
520
  const parts = partsFromResponseBlocks(responseBlocks, assistantText);
465
521
  messages.push({
@@ -625,6 +681,47 @@ var applyThreadSummaryEvent = (currentThreads, event) => {
625
681
  return sortByActivity(next);
626
682
  };
627
683
 
684
+ // src/controller-thread-events.ts
685
+ function useControllerThreadEvents({
686
+ transport,
687
+ storage,
688
+ refresh,
689
+ setThreads
690
+ }) {
691
+ const cursorRef = useRef(null);
692
+ useEffect(() => {
693
+ if (!transport.subscribeThreadEvents) return;
694
+ let cancelled = false;
695
+ let retryTimeout = null;
696
+ let controller = null;
697
+ const connect = () => {
698
+ if (cancelled) return;
699
+ controller = new AbortController();
700
+ transport.subscribeThreadEvents?.(
701
+ { cursor: cursorRef.current, signal: controller.signal },
702
+ async (event) => {
703
+ if (typeof event.cursor === "number") cursorRef.current = event.cursor;
704
+ if (event.event === "snapshot_required") {
705
+ await refresh().catch(() => void 0);
706
+ return;
707
+ }
708
+ storage.setThreads(applyThreadSummaryEvent(storage.listThreads(), event));
709
+ setThreads(storage.listThreads());
710
+ }
711
+ ).catch((error) => {
712
+ if (cancelled || isAbortError(error)) return;
713
+ retryTimeout = setTimeout(connect, 1500);
714
+ });
715
+ };
716
+ connect();
717
+ return () => {
718
+ cancelled = true;
719
+ if (retryTimeout) clearTimeout(retryTimeout);
720
+ controller?.abort();
721
+ };
722
+ }, [refresh, setThreads, storage, transport]);
723
+ }
724
+
628
725
  // src/message-lifecycle.ts
629
726
  function findStableAssistantMessageIndex(messages, input) {
630
727
  return messages.findIndex(
@@ -663,82 +760,83 @@ function upsertStableAssistantMessage(messages, input) {
663
760
  }
664
761
 
665
762
  // src/controller.ts
666
- function useAgents24ChatController({
667
- transport,
668
- storage,
669
- activeThreadId: controlledActiveThreadId,
670
- pageSize = DEFAULT_THREAD_PAGE_SIZE,
671
- storageKey,
672
- createId = createChatId,
673
- onActiveThreadIdChange,
674
- onSourceClick,
675
- onStreamErrorMessage,
676
- onRuntimeEvent,
677
- onThreadDetailLoaded
678
- }) {
763
+ function useAgents24ChatController(options) {
764
+ const {
765
+ transport,
766
+ storage,
767
+ activeThreadId: controlledActiveThreadId,
768
+ pageSize = DEFAULT_THREAD_PAGE_SIZE,
769
+ storageKey,
770
+ createId = createChatId,
771
+ onActiveThreadIdChange,
772
+ onSourceClick,
773
+ onStreamErrorMessage,
774
+ onRuntimeEvent,
775
+ onThreadDetailLoaded
776
+ } = options;
679
777
  const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
680
778
  const initialCached = activeThreadId ? storage.getThread(activeThreadId) : void 0;
681
- const [messages, setMessages] = useState(() => initialCached?.messages || []);
682
- const [isLoading, setIsLoading] = useState(false);
683
- const [isLoadingHistory, setIsLoadingHistory] = useState(() => Boolean(activeThreadId) && !initialCached?.messages?.length);
684
- const [isLoadingOlder, setIsLoadingOlder] = useState(false);
685
- const [hasOlderTurns, setHasOlderTurns] = useState(Boolean(initialCached?.hasOlderTurns));
686
- const [streamingContent, setStreamingContent] = useState("");
687
- const [streamingMessageId, setStreamingMessageId] = useState(null);
688
- const [contextStatus, setContextStatus] = useState(null);
689
- const [currentReasoning, setCurrentReasoning] = useState([]);
690
- const [liked, setLiked] = useState({});
691
- const [disliked, setDisliked] = useState({});
692
- const [copiedMessageId, setCopiedMessageId] = useState(null);
693
- const [lastThinkingDurationMs, setLastThinkingDurationMs] = useState(null);
694
- const [threads, setThreads] = useState(() => storage.listThreads());
695
- const [isRefreshingThreads, setIsRefreshingThreads] = useState(false);
696
- const [isSelectingThread, setIsSelectingThread] = useState(false);
697
- const [activeRunId, setActiveRunId] = useState(null);
698
- const textareaRef = useRef(null);
699
- const activeThreadIdRef = useRef(activeThreadId);
700
- const messagesRef = useRef(messages);
701
- const nextBeforeTurnIndexRef = useRef(initialCached?.nextBeforeTurnIndex ?? null);
702
- const hasOlderTurnsRef = useRef(Boolean(initialCached?.hasOlderTurns));
703
- const isLoadingOlderRef = useRef(false);
704
- const loadedThreadIdRef = useRef(initialCached?.messages?.length ? activeThreadId : null);
705
- const requestSeqRef = useRef(0);
706
- const isLoadingHistoryRef = useRef(isLoadingHistory);
707
- const activeRunIdRef = useRef(null);
708
- const reattachedRunIdRef = useRef(null);
709
- const abortControllerRef = useRef(null);
710
- const streamingContentRef = useRef("");
711
- const streamingMessageIdRef = useRef(null);
712
- const reasoningRef = useRef([]);
713
- const liveVoiceIdsRef = useRef({});
714
- const refreshSeqRef = useRef(0);
715
- const threadEventsCursorRef = useRef(null);
716
- const setActiveRunIdValue = useCallback2((runId) => {
779
+ const [messages, setMessages] = useState2(() => initialCached?.messages || []);
780
+ const [isLoading, setIsLoading] = useState2(false);
781
+ const [isLoadingHistory, setIsLoadingHistory] = useState2(() => Boolean(activeThreadId) && !initialCached?.messages?.length);
782
+ const [isLoadingOlder, setIsLoadingOlder] = useState2(false);
783
+ const [hasOlderTurns, setHasOlderTurns] = useState2(Boolean(initialCached?.hasOlderTurns));
784
+ const [streamingContent, setStreamingContent] = useState2("");
785
+ const [streamingMessageId, setStreamingMessageId] = useState2(null);
786
+ const [contextStatus, setContextStatus] = useState2(null);
787
+ const [currentReasoning, setCurrentReasoning] = useState2([]);
788
+ const [liked, setLiked] = useState2({});
789
+ const [disliked, setDisliked] = useState2({});
790
+ const [copiedMessageId, setCopiedMessageId] = useState2(null);
791
+ const [lastThinkingDurationMs, setLastThinkingDurationMs] = useState2(null);
792
+ const [threads, setThreads] = useState2(() => storage.listThreads());
793
+ const [isRefreshingThreads, setIsRefreshingThreads] = useState2(false);
794
+ const [isSelectingThread, setIsSelectingThread] = useState2(false);
795
+ const [activeRunId, setActiveRunId] = useState2(null);
796
+ const textareaRef = useRef2(null);
797
+ const activeThreadIdRef = useRef2(activeThreadId);
798
+ const messagesRef = useRef2(messages);
799
+ const nextBeforeTurnIndexRef = useRef2(initialCached?.nextBeforeTurnIndex ?? null);
800
+ const hasOlderTurnsRef = useRef2(Boolean(initialCached?.hasOlderTurns));
801
+ const isLoadingOlderRef = useRef2(false);
802
+ const loadedThreadIdRef = useRef2(initialCached?.messages?.length ? activeThreadId : null);
803
+ const requestSeqRef = useRef2(0);
804
+ const isLoadingHistoryRef = useRef2(isLoadingHistory);
805
+ const activeRunIdRef = useRef2(null);
806
+ const reattachedRunIdRef = useRef2(null);
807
+ const abortControllerRef = useRef2(null);
808
+ const [lifecycleAbortController] = useState2(() => new AbortController());
809
+ const streamingContentRef = useRef2("");
810
+ const streamingMessageIdRef = useRef2(null);
811
+ const reasoningRef = useRef2([]);
812
+ const liveVoiceIdsRef = useRef2({});
813
+ const refreshSeqRef = useRef2(0);
814
+ const setActiveRunIdValue = useCallback3((runId) => {
717
815
  activeRunIdRef.current = runId;
718
816
  setActiveRunId(runId);
719
817
  }, []);
720
- const syncThreadsFromStorage = useCallback2(() => {
818
+ const syncThreadsFromStorage = useCallback3(() => {
721
819
  setThreads(storage.listThreads());
722
820
  }, [storage]);
723
- const upsertStoredThread = useCallback2(
821
+ const upsertStoredThread = useCallback3(
724
822
  (thread) => {
725
823
  storage.upsertThread(thread);
726
824
  syncThreadsFromStorage();
727
825
  },
728
826
  [storage, syncThreadsFromStorage]
729
827
  );
730
- useEffect(() => {
828
+ useEffect2(() => {
731
829
  messagesRef.current = messages;
732
830
  }, [messages]);
733
- const persistThread = useCallback2(
734
- (threadId, nextMessages, paging, options) => {
831
+ const persistThread = useCallback3(
832
+ (threadId, nextMessages, paging, options2) => {
735
833
  const existing = storage.getThread(threadId);
736
834
  const firstUser = nextMessages.find((message) => message.role === "user");
737
835
  upsertStoredThread({
738
836
  ...existing || {},
739
837
  id: threadId,
740
838
  title: existing?.title || (firstUser ? titleFromMessage(firstUser.content, firstUser.attachments || []) : "New chat"),
741
- updated_at: options?.updatedAt || (options?.touch ? (/* @__PURE__ */ new Date()).toISOString() : existing?.updated_at) || (/* @__PURE__ */ new Date()).toISOString(),
839
+ updated_at: options2?.updatedAt || (options2?.touch ? (/* @__PURE__ */ new Date()).toISOString() : existing?.updated_at) || (/* @__PURE__ */ new Date()).toISOString(),
742
840
  messages: nextMessages,
743
841
  isHydrated: true,
744
842
  hasOlderTurns: paging?.hasOlderTurns ?? hasOlderTurnsRef.current,
@@ -747,7 +845,7 @@ function useAgents24ChatController({
747
845
  },
748
846
  [storage, upsertStoredThread]
749
847
  );
750
- const markThreadRunStatus = useCallback2(
848
+ const markThreadRunStatus = useCallback3(
751
849
  (threadId, runId, status, lastEventSeq) => {
752
850
  const existing = storage.getThread(threadId);
753
851
  if (!existing || !runId) return;
@@ -777,20 +875,21 @@ function useAgents24ChatController({
777
875
  },
778
876
  [storage, upsertStoredThread]
779
877
  );
780
- const refresh = useCallback2(async () => {
878
+ const refresh = useCallback3(async () => {
781
879
  const seq = ++refreshSeqRef.current;
782
880
  setIsRefreshingThreads(true);
783
881
  try {
784
- const data = await transport.listThreads();
882
+ const data = await transport.listThreads({ signal: lifecycleAbortController.signal });
883
+ if (lifecycleAbortController.signal.aborted) return;
785
884
  if (seq !== refreshSeqRef.current) return;
786
885
  const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
787
886
  storage.setThreads(mergeStoredThreadsForRefresh(storage.listThreads(), nextThreads));
788
887
  setThreads(storage.listThreads());
789
888
  } finally {
790
- if (seq === refreshSeqRef.current) setIsRefreshingThreads(false);
889
+ if (!lifecycleAbortController.signal.aborted && seq === refreshSeqRef.current) setIsRefreshingThreads(false);
791
890
  }
792
- }, [storage, transport]);
793
- const applyThreadId = useCallback2(
891
+ }, [lifecycleAbortController, storage, transport]);
892
+ const applyThreadId = useCallback3(
794
893
  (threadId, baseMessages) => {
795
894
  if (!threadId || activeThreadIdRef.current === threadId) return;
796
895
  activeThreadIdRef.current = threadId;
@@ -804,15 +903,15 @@ function useAgents24ChatController({
804
903
  streamingContentRef.current = value;
805
904
  setStreamingContent(value);
806
905
  };
807
- const setLoadingHistory = useCallback2((value) => {
906
+ const setLoadingHistory = useCallback3((value) => {
808
907
  isLoadingHistoryRef.current = value;
809
908
  setIsLoadingHistory(value);
810
909
  }, []);
811
- const setReasoningSteps = useCallback2((value) => {
910
+ const setReasoningSteps = useCallback3((value) => {
812
911
  reasoningRef.current = value || [];
813
912
  setCurrentReasoning(value || []);
814
913
  }, []);
815
- const detachActiveStream = useCallback2(() => {
914
+ const detachActiveStream = useCallback3(() => {
816
915
  const controller = abortControllerRef.current;
817
916
  abortControllerRef.current = null;
818
917
  abortDetachedStream(controller);
@@ -826,7 +925,7 @@ function useAgents24ChatController({
826
925
  setStreamingContent("");
827
926
  setCurrentReasoning([]);
828
927
  }, [setActiveRunIdValue]);
829
- const clearMissingThread = useCallback2(
928
+ const clearMissingThread = useCallback3(
830
929
  (threadId) => {
831
930
  storage.deleteThread?.(threadId);
832
931
  syncThreadsFromStorage();
@@ -848,7 +947,7 @@ function useAgents24ChatController({
848
947
  },
849
948
  [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage, syncThreadsFromStorage]
850
949
  );
851
- const setLiveAssistantMessage = useCallback2(
950
+ const setLiveAssistantMessage = useCallback3(
852
951
  (input) => {
853
952
  setMessages((prev) => {
854
953
  const next = upsertStableAssistantMessage(prev, {
@@ -880,7 +979,7 @@ function useAgents24ChatController({
880
979
  },
881
980
  []
882
981
  );
883
- const finalizeAssistantMessage = useCallback2(
982
+ const finalizeAssistantMessage = useCallback3(
884
983
  (input) => {
885
984
  const content = input.error || input.assistantText.trim();
886
985
  if (!content) return input.baseMessages;
@@ -934,7 +1033,7 @@ function useAgents24ChatController({
934
1033
  },
935
1034
  [createId, persistThread, storage, upsertStoredThread]
936
1035
  );
937
- const loadThread = useCallback2(
1036
+ const loadThread = useCallback3(
938
1037
  async (threadId) => {
939
1038
  const seq = ++requestSeqRef.current;
940
1039
  setLoadingHistory(true);
@@ -944,7 +1043,8 @@ function useAgents24ChatController({
944
1043
  const detail = await transport.getThread({
945
1044
  threadId,
946
1045
  limit: pageSize,
947
- includeRunEvents: false
1046
+ includeRunEvents: false,
1047
+ signal: lifecycleAbortController.signal
948
1048
  });
949
1049
  if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
950
1050
  void onThreadDetailLoaded?.(detail);
@@ -973,12 +1073,12 @@ function useAgents24ChatController({
973
1073
  }
974
1074
  throw error;
975
1075
  } finally {
976
- if (activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
1076
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
977
1077
  }
978
1078
  },
979
- [clearMissingThread, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
1079
+ [clearMissingThread, lifecycleAbortController, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
980
1080
  );
981
- const loadOlderTurns = useCallback2(async () => {
1081
+ const loadOlderTurns = useCallback3(async () => {
982
1082
  const threadId = activeThreadIdRef.current;
983
1083
  const beforeTurnIndex = nextBeforeTurnIndexRef.current;
984
1084
  if (!threadId || beforeTurnIndex === null || isLoadingOlderRef.current) return;
@@ -989,7 +1089,8 @@ function useAgents24ChatController({
989
1089
  threadId,
990
1090
  limit: pageSize,
991
1091
  beforeTurnIndex,
992
- includeRunEvents: false
1092
+ includeRunEvents: false,
1093
+ signal: lifecycleAbortController.signal
993
1094
  });
994
1095
  if (activeThreadIdRef.current !== threadId) return;
995
1096
  const older = threadDetailToMessages(detail);
@@ -1009,11 +1110,11 @@ function useAgents24ChatController({
1009
1110
  }
1010
1111
  throw error;
1011
1112
  } finally {
1012
- if (activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
1113
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
1013
1114
  isLoadingOlderRef.current = false;
1014
1115
  }
1015
- }, [clearMissingThread, pageSize, persistThread, transport]);
1016
- const handleStreamEvent = useCallback2(
1116
+ }, [clearMissingThread, lifecycleAbortController, pageSize, persistThread, transport]);
1117
+ const handleStreamEvent = useCallback3(
1017
1118
  (input) => {
1018
1119
  const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
1019
1120
  const payload = event.payload || {};
@@ -1059,7 +1160,7 @@ function useAgents24ChatController({
1059
1160
  streamThreadIdRef.current = terminalThreadId;
1060
1161
  applyThreadId(terminalThreadId, baseMessages);
1061
1162
  }
1062
- const finalText = String(payload.assistant_output_text || textFromFinalOutput(payload.final_output) || streamingContentRef.current || "");
1163
+ const finalText = String(payload.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks || []) || streamingContentRef.current || "");
1063
1164
  finalizeAssistantMessage({
1064
1165
  threadId: streamThreadIdRef.current || activeThreadIdRef.current,
1065
1166
  baseMessages: messagesRef.current,
@@ -1074,7 +1175,7 @@ function useAgents24ChatController({
1074
1175
  },
1075
1176
  [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
1076
1177
  );
1077
- const runStream = useCallback2(
1178
+ const runStream = useCallback3(
1078
1179
  async (input) => {
1079
1180
  const startedAt = Date.now();
1080
1181
  const controller = new AbortController();
@@ -1197,18 +1298,26 @@ function useAgents24ChatController({
1197
1298
  },
1198
1299
  [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
1199
1300
  );
1200
- const handleSubmit = useCallback2(
1301
+ const handleSubmit = useCallback3(
1201
1302
  async (message) => {
1202
1303
  if (!message.text.trim() && !(message.files || []).length) return;
1203
1304
  await runStream({ mode: "submit", message });
1204
1305
  },
1205
1306
  [runStream]
1206
1307
  );
1207
- const attachRun = useCallback2(async (runId, threadId) => {
1308
+ const attachRun = useCallback3(async (runId, threadId) => {
1208
1309
  const resolvedThreadId = threadId ?? activeThreadIdRef.current;
1209
1310
  if (runId && resolvedThreadId) await runStream({ mode: "attach", runId, threadId: resolvedThreadId });
1210
1311
  }, [runStream]);
1211
- const handleStop = useCallback2(() => {
1312
+ const { pendingHitl, isResolvingHitl, resumeHitl } = useControllerHitl({
1313
+ messages,
1314
+ transport,
1315
+ activeRunId,
1316
+ activeRunIdRef,
1317
+ activeThreadIdRef,
1318
+ runStream
1319
+ });
1320
+ const handleStop = useCallback3(() => {
1212
1321
  const runId = activeRunIdRef.current;
1213
1322
  const partial = streamingContentRef.current;
1214
1323
  const liveMessageId = streamingMessageIdRef.current;
@@ -1232,45 +1341,25 @@ function useAgents24ChatController({
1232
1341
  });
1233
1342
  }
1234
1343
  }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
1235
- useEffect(() => {
1236
- refresh().catch(() => setThreads(storage.listThreads()));
1237
- }, [refresh, storage]);
1238
- useEffect(() => {
1239
- if (!transport.subscribeThreadEvents) return;
1240
- let cancelled = false;
1241
- let retryTimeout = null;
1242
- let controller = null;
1243
- const connect = () => {
1244
- if (cancelled) return;
1245
- controller = new AbortController();
1246
- transport.subscribeThreadEvents?.(
1247
- { cursor: threadEventsCursorRef.current, signal: controller.signal },
1248
- async (event) => {
1249
- if (typeof event.cursor === "number") threadEventsCursorRef.current = event.cursor;
1250
- if (event.event === "snapshot_required") {
1251
- await refresh().catch(() => void 0);
1252
- return;
1253
- }
1254
- const next = applyThreadSummaryEvent(storage.listThreads(), event);
1255
- storage.setThreads(next);
1256
- setThreads(storage.listThreads());
1257
- }
1258
- ).catch((error) => {
1259
- if (cancelled || isAbortError(error)) return;
1260
- retryTimeout = setTimeout(connect, 1500);
1261
- });
1262
- };
1263
- connect();
1344
+ useEffect2(() => {
1345
+ refresh().catch((error) => {
1346
+ if (!isAbortError(error) && !lifecycleAbortController.signal.aborted) {
1347
+ setThreads(storage.listThreads());
1348
+ }
1349
+ });
1350
+ }, [lifecycleAbortController, refresh, storage]);
1351
+ useEffect2(() => {
1264
1352
  return () => {
1265
- cancelled = true;
1266
- if (retryTimeout) clearTimeout(retryTimeout);
1267
- controller?.abort();
1353
+ lifecycleAbortController.abort();
1354
+ abortDetachedStream(abortControllerRef.current);
1355
+ abortControllerRef.current = null;
1268
1356
  };
1269
- }, [refresh, storage, transport]);
1270
- useEffect(() => {
1357
+ }, [lifecycleAbortController]);
1358
+ useControllerThreadEvents({ transport, storage, refresh, setThreads });
1359
+ useEffect2(() => {
1271
1360
  syncThreadsFromStorage();
1272
1361
  }, [storageKey, syncThreadsFromStorage]);
1273
- useEffect(() => {
1362
+ useEffect2(() => {
1274
1363
  const previous = activeThreadIdRef.current;
1275
1364
  activeThreadIdRef.current = activeThreadId;
1276
1365
  if (activeThreadId && previous === activeThreadId && (activeRunIdRef.current || streamingMessageIdRef.current)) {
@@ -1314,14 +1403,14 @@ function useAgents24ChatController({
1314
1403
  }
1315
1404
  void loadThread(activeThreadId).catch(() => setLoadingHistory(false));
1316
1405
  }, [activeThreadId, detachActiveStream, loadThread, setLoadingHistory, storage]);
1317
- useEffect(() => {
1406
+ useEffect2(() => {
1318
1407
  const threadId = activeThreadId;
1319
1408
  if (!threadId || isLoadingHistory || loadedThreadIdRef.current !== threadId || activeRunIdRef.current) return;
1320
1409
  const runId = autoAttachRunIdFromThread(storage.getThread(threadId));
1321
1410
  if (!runId || reattachedRunIdRef.current === runId) return;
1322
1411
  void runStream({ mode: "attach", threadId, runId });
1323
1412
  }, [activeThreadId, isLoadingHistory, runStream, storage, storageKey]);
1324
- useEffect(() => {
1413
+ useEffect2(() => {
1325
1414
  const threadId = activeThreadId;
1326
1415
  if (!threadId || isLoadingHistoryRef.current || activeRunIdRef.current || streamingMessageIdRef.current) {
1327
1416
  return;
@@ -1362,7 +1451,7 @@ function useAgents24ChatController({
1362
1451
  setMessages,
1363
1452
  storage
1364
1453
  });
1365
- const loadThreadById = useCallback2(
1454
+ const loadThreadById = useCallback3(
1366
1455
  async (threadId) => {
1367
1456
  if (!threadId) return;
1368
1457
  setIsSelectingThread(true);
@@ -1381,7 +1470,7 @@ function useAgents24ChatController({
1381
1470
  [detachActiveStream, loadThread, onActiveThreadIdChange, storage]
1382
1471
  );
1383
1472
  const activeThread = activeThreadId ? storage.getThread(activeThreadId) || threads.find((thread) => thread.id === activeThreadId) || null : null;
1384
- return useMemo(() => ({
1473
+ return useMemo2(() => ({
1385
1474
  threads,
1386
1475
  activeThreadId,
1387
1476
  activeThread,
@@ -1401,8 +1490,11 @@ function useAgents24ChatController({
1401
1490
  copiedMessageId,
1402
1491
  lastThinkingDurationMs,
1403
1492
  activeRunId,
1493
+ pendingHitl,
1494
+ isResolvingHitl,
1404
1495
  handleSubmit,
1405
1496
  attachRun,
1497
+ resumeHitl,
1406
1498
  handleStop,
1407
1499
  handleCopy,
1408
1500
  handleLike,
@@ -1441,8 +1533,11 @@ function useAgents24ChatController({
1441
1533
  loadThreadById,
1442
1534
  loadOlderTurns,
1443
1535
  messages,
1536
+ isResolvingHitl,
1444
1537
  onSourceClick,
1538
+ pendingHitl,
1445
1539
  refresh,
1540
+ resumeHitl,
1446
1541
  streamingContent,
1447
1542
  streamingMessageId,
1448
1543
  startNewThread,
@@ -1531,7 +1626,9 @@ var consumeSseResponse = async (response, onEvent, options = {}) => {
1531
1626
  let closed = false;
1532
1627
  const closeReader = () => {
1533
1628
  closed = true;
1534
- void reader.cancel().catch(() => {
1629
+ queueMicrotask(() => {
1630
+ void reader.cancel().catch(() => {
1631
+ });
1535
1632
  });
1536
1633
  };
1537
1634
  if (options.signal?.aborted) {
@@ -1621,7 +1718,7 @@ var createFetchChatTransport = ({
1621
1718
  }
1622
1719
  )
1623
1720
  });
1624
- return consumeSseResponse(response, onEvent);
1721
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
1625
1722
  },
1626
1723
  async attachRun(input, onEvent) {
1627
1724
  const response = await fetchImpl(routes.attachRun(input), {
@@ -1630,7 +1727,7 @@ var createFetchChatTransport = ({
1630
1727
  headers: streamHeaders(await loadHeaders()),
1631
1728
  body: JSON.stringify({})
1632
1729
  });
1633
- return consumeSseResponse(response, onEvent);
1730
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
1634
1731
  },
1635
1732
  async cancelRun(input) {
1636
1733
  const response = await fetchImpl(routes.cancelRun(input), {
@@ -1657,7 +1754,11 @@ var createFetchChatTransport = ({
1657
1754
  signal: input.signal,
1658
1755
  headers: streamHeaders(await loadHeaders())
1659
1756
  });
1660
- await consumeSseResponse(response, (event) => onEvent(event));
1757
+ await consumeSseResponse(
1758
+ response,
1759
+ (event) => onEvent(event),
1760
+ { signal: input.signal }
1761
+ );
1661
1762
  },
1662
1763
  async resumeHitl(input) {
1663
1764
  if (!routes.resumeRun) throw new ChatTransportError("HITL resume is not configured.", 501);
@@ -1665,9 +1766,10 @@ var createFetchChatTransport = ({
1665
1766
  method: "POST",
1666
1767
  headers: jsonHeaders(await loadHeaders()),
1667
1768
  body: JSON.stringify({
1668
- schema_version: "agents24.hitl.resume.v1",
1769
+ schema_version: "agents24.hitl.resume.v2",
1669
1770
  interrupt_id: input.interruptId,
1670
- decisions: input.decisions,
1771
+ action: input.action,
1772
+ comment: input.comment,
1671
1773
  client: input.client
1672
1774
  })
1673
1775
  });
@@ -1741,7 +1843,6 @@ export {
1741
1843
  partsFromResponseBlocks,
1742
1844
  reasoningStepsFromParts,
1743
1845
  renderChatPart,
1744
- textFromFinalOutput,
1745
1846
  threadActivityDate,
1746
1847
  threadDetailToMessages,
1747
1848
  threadPaging,