@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.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,
@@ -87,7 +86,7 @@ __export(index_exports, {
87
86
  module.exports = __toCommonJS(index_exports);
88
87
 
89
88
  // src/controller.ts
90
- var import_react2 = require("react");
89
+ var import_react4 = require("react");
91
90
 
92
91
  // src/controller-actions.ts
93
92
  var import_react = require("react");
@@ -161,6 +160,47 @@ function useControllerMessageActions(input) {
161
160
  return { handleCopy, handleDislike, handleLike, handleRetry, startNewThread, upsertLiveVoiceMessage };
162
161
  }
163
162
 
163
+ // src/controller-hitl.ts
164
+ var import_react2 = require("react");
165
+ function useControllerHitl({
166
+ messages,
167
+ transport,
168
+ activeRunId,
169
+ activeRunIdRef,
170
+ activeThreadIdRef,
171
+ runStream
172
+ }) {
173
+ const [isResolvingHitl, setIsResolvingHitl] = (0, import_react2.useState)(false);
174
+ const pendingHitl = (0, import_react2.useMemo)(() => {
175
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
176
+ const parts = messages[messageIndex]?.parts || [];
177
+ for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) {
178
+ const part = parts[partIndex];
179
+ if (part?.kind === "hitl" && part.status === "pending") return part;
180
+ }
181
+ }
182
+ return null;
183
+ }, [messages]);
184
+ const resumeHitl = (0, import_react2.useCallback)(async (input) => {
185
+ if (!transport.resumeHitl) throw new Error("HITL resume is not configured.");
186
+ const runId = input.runId || activeRunIdRef.current || activeRunId;
187
+ if (!runId) throw new Error("No paused run is available to resume.");
188
+ setIsResolvingHitl(true);
189
+ try {
190
+ const result = await transport.resumeHitl({ ...input, runId });
191
+ const threadId = result.thread_id || activeThreadIdRef.current;
192
+ if (threadId) await runStream({ mode: "attach", runId: result.run_id || runId, threadId });
193
+ return result;
194
+ } finally {
195
+ setIsResolvingHitl(false);
196
+ }
197
+ }, [activeRunId, activeRunIdRef, activeThreadIdRef, runStream, transport]);
198
+ return { pendingHitl, isResolvingHitl, resumeHitl };
199
+ }
200
+
201
+ // src/controller-thread-events.ts
202
+ var import_react3 = require("react");
203
+
164
204
  // src/context-window.ts
165
205
  var SOURCE_PRIORITY = {
166
206
  unknown: 0,
@@ -295,12 +335,6 @@ var titleFromMessage = (text, files = []) => {
295
335
  var threadActivityDate = (thread) => String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString());
296
336
  var assistantTextFromResponseBlocks = (blocks) => (blocks || []).filter((block) => block.kind === "assistant_text" && typeof block.text === "string").map((block) => String(block.text)).join("\n\n").trim();
297
337
  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
338
  var displayTextWithoutInlineAttachments = (text, hasAttachments) => {
305
339
  if (!hasAttachments) return text;
306
340
  const marker = "Attached text file (";
@@ -347,7 +381,7 @@ var responseBlocksFromTurn = (turn) => {
347
381
  var assistantTextFromEvents = (events) => {
348
382
  const assistantText = latestEventPayloadValue(events, "assistant_output_text");
349
383
  if (typeof assistantText === "string" && assistantText.trim()) return assistantText;
350
- return textFromFinalOutput(latestEventPayloadValue(events, "final_output"));
384
+ return "";
351
385
  };
352
386
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
353
387
  var optionalString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -451,13 +485,34 @@ var partsFromResponseBlocks = (blocks, fallbackText) => {
451
485
  }
452
486
  if (block.kind === "hitl_request") {
453
487
  const hitl = asRecord(block.hitl) || block;
488
+ if (hitl.schema_version !== "agents24.hitl.interrupt.v2") {
489
+ throw new Error("Invalid V2 HITL response block.");
490
+ }
491
+ const interruptId = optionalString(block.interruptId) || optionalString(hitl.interrupt_id);
492
+ const hitlKind = optionalString(block.hitlKind) || optionalString(hitl.kind);
493
+ const status = optionalString(block.status) || optionalString(hitl.status) || "pending";
494
+ const allowedActions = Array.isArray(hitl.allowed_actions) ? hitl.allowed_actions.filter((value) => ["approve", "reject", "connect", "skip"].includes(String(value))) : [];
495
+ if (!interruptId || allowedActions.length === 0 || !["tool_review", "mcp_auth", "user_approval", "app_data_permission"].includes(String(hitlKind))) {
496
+ throw new Error("Invalid V2 HITL response block.");
497
+ }
498
+ const resolution = asRecord(block.resolution);
454
499
  parts.push({
455
500
  id,
456
501
  type: "hitl",
457
502
  kind: "hitl",
458
- hitl,
459
- interruptId: optionalString(block.interruptId) || optionalString(hitl.interrupt_id) || null,
460
- hitlKind: optionalString(block.hitlKind) || optionalString(hitl.kind) || null,
503
+ interruptId,
504
+ hitlKind,
505
+ message: optionalString(hitl.message) || optionalString(block.text) || "Input is required to continue.",
506
+ allowedActions,
507
+ status,
508
+ presentation: asRecord(hitl.presentation) || {},
509
+ resolution: resolution ? {
510
+ action: ["approve", "reject", "connect", "skip"].includes(String(resolution.action)) ? String(resolution.action) : null,
511
+ outcome: optionalString(resolution.outcome),
512
+ reason: optionalString(resolution.reason),
513
+ resolvedAt: optionalString(resolution.resolved_at),
514
+ resolver: asRecord(resolution.resolver)
515
+ } : null,
461
516
  raw: block
462
517
  });
463
518
  return;
@@ -531,7 +586,7 @@ var turnToMessages = (turn, activeRunId) => {
531
586
  attachments
532
587
  });
533
588
  }
534
- const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events) || textFromFinalOutput(turn.final_output);
589
+ const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events);
535
590
  if (assistantText || responseBlocks.length > 0 || isRunning) {
536
591
  const parts = partsFromResponseBlocks(responseBlocks, assistantText);
537
592
  messages.push({
@@ -697,6 +752,47 @@ var applyThreadSummaryEvent = (currentThreads, event) => {
697
752
  return sortByActivity(next);
698
753
  };
699
754
 
755
+ // src/controller-thread-events.ts
756
+ function useControllerThreadEvents({
757
+ transport,
758
+ storage,
759
+ refresh,
760
+ setThreads
761
+ }) {
762
+ const cursorRef = (0, import_react3.useRef)(null);
763
+ (0, import_react3.useEffect)(() => {
764
+ if (!transport.subscribeThreadEvents) return;
765
+ let cancelled = false;
766
+ let retryTimeout = null;
767
+ let controller = null;
768
+ const connect = () => {
769
+ if (cancelled) return;
770
+ controller = new AbortController();
771
+ transport.subscribeThreadEvents?.(
772
+ { cursor: cursorRef.current, signal: controller.signal },
773
+ async (event) => {
774
+ if (typeof event.cursor === "number") cursorRef.current = event.cursor;
775
+ if (event.event === "snapshot_required") {
776
+ await refresh().catch(() => void 0);
777
+ return;
778
+ }
779
+ storage.setThreads(applyThreadSummaryEvent(storage.listThreads(), event));
780
+ setThreads(storage.listThreads());
781
+ }
782
+ ).catch((error) => {
783
+ if (cancelled || isAbortError(error)) return;
784
+ retryTimeout = setTimeout(connect, 1500);
785
+ });
786
+ };
787
+ connect();
788
+ return () => {
789
+ cancelled = true;
790
+ if (retryTimeout) clearTimeout(retryTimeout);
791
+ controller?.abort();
792
+ };
793
+ }, [refresh, setThreads, storage, transport]);
794
+ }
795
+
700
796
  // src/message-lifecycle.ts
701
797
  function findStableAssistantMessageIndex(messages, input) {
702
798
  return messages.findIndex(
@@ -735,82 +831,83 @@ function upsertStableAssistantMessage(messages, input) {
735
831
  }
736
832
 
737
833
  // src/controller.ts
738
- function useAgents24ChatController({
739
- transport,
740
- storage,
741
- activeThreadId: controlledActiveThreadId,
742
- pageSize = DEFAULT_THREAD_PAGE_SIZE,
743
- storageKey,
744
- createId = createChatId,
745
- onActiveThreadIdChange,
746
- onSourceClick,
747
- onStreamErrorMessage,
748
- onRuntimeEvent,
749
- onThreadDetailLoaded
750
- }) {
834
+ function useAgents24ChatController(options) {
835
+ const {
836
+ transport,
837
+ storage,
838
+ activeThreadId: controlledActiveThreadId,
839
+ pageSize = DEFAULT_THREAD_PAGE_SIZE,
840
+ storageKey,
841
+ createId = createChatId,
842
+ onActiveThreadIdChange,
843
+ onSourceClick,
844
+ onStreamErrorMessage,
845
+ onRuntimeEvent,
846
+ onThreadDetailLoaded
847
+ } = options;
751
848
  const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
752
849
  const initialCached = activeThreadId ? storage.getThread(activeThreadId) : void 0;
753
- const [messages, setMessages] = (0, import_react2.useState)(() => initialCached?.messages || []);
754
- const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
755
- const [isLoadingHistory, setIsLoadingHistory] = (0, import_react2.useState)(() => Boolean(activeThreadId) && !initialCached?.messages?.length);
756
- const [isLoadingOlder, setIsLoadingOlder] = (0, import_react2.useState)(false);
757
- const [hasOlderTurns, setHasOlderTurns] = (0, import_react2.useState)(Boolean(initialCached?.hasOlderTurns));
758
- const [streamingContent, setStreamingContent] = (0, import_react2.useState)("");
759
- const [streamingMessageId, setStreamingMessageId] = (0, import_react2.useState)(null);
760
- const [contextStatus, setContextStatus] = (0, import_react2.useState)(null);
761
- const [currentReasoning, setCurrentReasoning] = (0, import_react2.useState)([]);
762
- const [liked, setLiked] = (0, import_react2.useState)({});
763
- const [disliked, setDisliked] = (0, import_react2.useState)({});
764
- const [copiedMessageId, setCopiedMessageId] = (0, import_react2.useState)(null);
765
- const [lastThinkingDurationMs, setLastThinkingDurationMs] = (0, import_react2.useState)(null);
766
- const [threads, setThreads] = (0, import_react2.useState)(() => storage.listThreads());
767
- const [isRefreshingThreads, setIsRefreshingThreads] = (0, import_react2.useState)(false);
768
- const [isSelectingThread, setIsSelectingThread] = (0, import_react2.useState)(false);
769
- const [activeRunId, setActiveRunId] = (0, import_react2.useState)(null);
770
- const textareaRef = (0, import_react2.useRef)(null);
771
- const activeThreadIdRef = (0, import_react2.useRef)(activeThreadId);
772
- const messagesRef = (0, import_react2.useRef)(messages);
773
- const nextBeforeTurnIndexRef = (0, import_react2.useRef)(initialCached?.nextBeforeTurnIndex ?? null);
774
- const hasOlderTurnsRef = (0, import_react2.useRef)(Boolean(initialCached?.hasOlderTurns));
775
- const isLoadingOlderRef = (0, import_react2.useRef)(false);
776
- const loadedThreadIdRef = (0, import_react2.useRef)(initialCached?.messages?.length ? activeThreadId : null);
777
- const requestSeqRef = (0, import_react2.useRef)(0);
778
- const isLoadingHistoryRef = (0, import_react2.useRef)(isLoadingHistory);
779
- const activeRunIdRef = (0, import_react2.useRef)(null);
780
- const reattachedRunIdRef = (0, import_react2.useRef)(null);
781
- const abortControllerRef = (0, import_react2.useRef)(null);
782
- const streamingContentRef = (0, import_react2.useRef)("");
783
- const streamingMessageIdRef = (0, import_react2.useRef)(null);
784
- const reasoningRef = (0, import_react2.useRef)([]);
785
- const liveVoiceIdsRef = (0, import_react2.useRef)({});
786
- const refreshSeqRef = (0, import_react2.useRef)(0);
787
- const threadEventsCursorRef = (0, import_react2.useRef)(null);
788
- const setActiveRunIdValue = (0, import_react2.useCallback)((runId) => {
850
+ const [messages, setMessages] = (0, import_react4.useState)(() => initialCached?.messages || []);
851
+ const [isLoading, setIsLoading] = (0, import_react4.useState)(false);
852
+ const [isLoadingHistory, setIsLoadingHistory] = (0, import_react4.useState)(() => Boolean(activeThreadId) && !initialCached?.messages?.length);
853
+ const [isLoadingOlder, setIsLoadingOlder] = (0, import_react4.useState)(false);
854
+ const [hasOlderTurns, setHasOlderTurns] = (0, import_react4.useState)(Boolean(initialCached?.hasOlderTurns));
855
+ const [streamingContent, setStreamingContent] = (0, import_react4.useState)("");
856
+ const [streamingMessageId, setStreamingMessageId] = (0, import_react4.useState)(null);
857
+ const [contextStatus, setContextStatus] = (0, import_react4.useState)(null);
858
+ const [currentReasoning, setCurrentReasoning] = (0, import_react4.useState)([]);
859
+ const [liked, setLiked] = (0, import_react4.useState)({});
860
+ const [disliked, setDisliked] = (0, import_react4.useState)({});
861
+ const [copiedMessageId, setCopiedMessageId] = (0, import_react4.useState)(null);
862
+ const [lastThinkingDurationMs, setLastThinkingDurationMs] = (0, import_react4.useState)(null);
863
+ const [threads, setThreads] = (0, import_react4.useState)(() => storage.listThreads());
864
+ const [isRefreshingThreads, setIsRefreshingThreads] = (0, import_react4.useState)(false);
865
+ const [isSelectingThread, setIsSelectingThread] = (0, import_react4.useState)(false);
866
+ const [activeRunId, setActiveRunId] = (0, import_react4.useState)(null);
867
+ const textareaRef = (0, import_react4.useRef)(null);
868
+ const activeThreadIdRef = (0, import_react4.useRef)(activeThreadId);
869
+ const messagesRef = (0, import_react4.useRef)(messages);
870
+ const nextBeforeTurnIndexRef = (0, import_react4.useRef)(initialCached?.nextBeforeTurnIndex ?? null);
871
+ const hasOlderTurnsRef = (0, import_react4.useRef)(Boolean(initialCached?.hasOlderTurns));
872
+ const isLoadingOlderRef = (0, import_react4.useRef)(false);
873
+ const loadedThreadIdRef = (0, import_react4.useRef)(initialCached?.messages?.length ? activeThreadId : null);
874
+ const requestSeqRef = (0, import_react4.useRef)(0);
875
+ const isLoadingHistoryRef = (0, import_react4.useRef)(isLoadingHistory);
876
+ const activeRunIdRef = (0, import_react4.useRef)(null);
877
+ const reattachedRunIdRef = (0, import_react4.useRef)(null);
878
+ const abortControllerRef = (0, import_react4.useRef)(null);
879
+ const [lifecycleAbortController] = (0, import_react4.useState)(() => new AbortController());
880
+ const streamingContentRef = (0, import_react4.useRef)("");
881
+ const streamingMessageIdRef = (0, import_react4.useRef)(null);
882
+ const reasoningRef = (0, import_react4.useRef)([]);
883
+ const liveVoiceIdsRef = (0, import_react4.useRef)({});
884
+ const refreshSeqRef = (0, import_react4.useRef)(0);
885
+ const setActiveRunIdValue = (0, import_react4.useCallback)((runId) => {
789
886
  activeRunIdRef.current = runId;
790
887
  setActiveRunId(runId);
791
888
  }, []);
792
- const syncThreadsFromStorage = (0, import_react2.useCallback)(() => {
889
+ const syncThreadsFromStorage = (0, import_react4.useCallback)(() => {
793
890
  setThreads(storage.listThreads());
794
891
  }, [storage]);
795
- const upsertStoredThread = (0, import_react2.useCallback)(
892
+ const upsertStoredThread = (0, import_react4.useCallback)(
796
893
  (thread) => {
797
894
  storage.upsertThread(thread);
798
895
  syncThreadsFromStorage();
799
896
  },
800
897
  [storage, syncThreadsFromStorage]
801
898
  );
802
- (0, import_react2.useEffect)(() => {
899
+ (0, import_react4.useEffect)(() => {
803
900
  messagesRef.current = messages;
804
901
  }, [messages]);
805
- const persistThread = (0, import_react2.useCallback)(
806
- (threadId, nextMessages, paging, options) => {
902
+ const persistThread = (0, import_react4.useCallback)(
903
+ (threadId, nextMessages, paging, options2) => {
807
904
  const existing = storage.getThread(threadId);
808
905
  const firstUser = nextMessages.find((message) => message.role === "user");
809
906
  upsertStoredThread({
810
907
  ...existing || {},
811
908
  id: threadId,
812
909
  title: existing?.title || (firstUser ? titleFromMessage(firstUser.content, firstUser.attachments || []) : "New chat"),
813
- updated_at: options?.updatedAt || (options?.touch ? (/* @__PURE__ */ new Date()).toISOString() : existing?.updated_at) || (/* @__PURE__ */ new Date()).toISOString(),
910
+ updated_at: options2?.updatedAt || (options2?.touch ? (/* @__PURE__ */ new Date()).toISOString() : existing?.updated_at) || (/* @__PURE__ */ new Date()).toISOString(),
814
911
  messages: nextMessages,
815
912
  isHydrated: true,
816
913
  hasOlderTurns: paging?.hasOlderTurns ?? hasOlderTurnsRef.current,
@@ -819,7 +916,7 @@ function useAgents24ChatController({
819
916
  },
820
917
  [storage, upsertStoredThread]
821
918
  );
822
- const markThreadRunStatus = (0, import_react2.useCallback)(
919
+ const markThreadRunStatus = (0, import_react4.useCallback)(
823
920
  (threadId, runId, status, lastEventSeq) => {
824
921
  const existing = storage.getThread(threadId);
825
922
  if (!existing || !runId) return;
@@ -849,20 +946,21 @@ function useAgents24ChatController({
849
946
  },
850
947
  [storage, upsertStoredThread]
851
948
  );
852
- const refresh = (0, import_react2.useCallback)(async () => {
949
+ const refresh = (0, import_react4.useCallback)(async () => {
853
950
  const seq = ++refreshSeqRef.current;
854
951
  setIsRefreshingThreads(true);
855
952
  try {
856
- const data = await transport.listThreads();
953
+ const data = await transport.listThreads({ signal: lifecycleAbortController.signal });
954
+ if (lifecycleAbortController.signal.aborted) return;
857
955
  if (seq !== refreshSeqRef.current) return;
858
956
  const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
859
957
  storage.setThreads(mergeStoredThreadsForRefresh(storage.listThreads(), nextThreads));
860
958
  setThreads(storage.listThreads());
861
959
  } finally {
862
- if (seq === refreshSeqRef.current) setIsRefreshingThreads(false);
960
+ if (!lifecycleAbortController.signal.aborted && seq === refreshSeqRef.current) setIsRefreshingThreads(false);
863
961
  }
864
- }, [storage, transport]);
865
- const applyThreadId = (0, import_react2.useCallback)(
962
+ }, [lifecycleAbortController, storage, transport]);
963
+ const applyThreadId = (0, import_react4.useCallback)(
866
964
  (threadId, baseMessages) => {
867
965
  if (!threadId || activeThreadIdRef.current === threadId) return;
868
966
  activeThreadIdRef.current = threadId;
@@ -876,15 +974,15 @@ function useAgents24ChatController({
876
974
  streamingContentRef.current = value;
877
975
  setStreamingContent(value);
878
976
  };
879
- const setLoadingHistory = (0, import_react2.useCallback)((value) => {
977
+ const setLoadingHistory = (0, import_react4.useCallback)((value) => {
880
978
  isLoadingHistoryRef.current = value;
881
979
  setIsLoadingHistory(value);
882
980
  }, []);
883
- const setReasoningSteps = (0, import_react2.useCallback)((value) => {
981
+ const setReasoningSteps = (0, import_react4.useCallback)((value) => {
884
982
  reasoningRef.current = value || [];
885
983
  setCurrentReasoning(value || []);
886
984
  }, []);
887
- const detachActiveStream = (0, import_react2.useCallback)(() => {
985
+ const detachActiveStream = (0, import_react4.useCallback)(() => {
888
986
  const controller = abortControllerRef.current;
889
987
  abortControllerRef.current = null;
890
988
  abortDetachedStream(controller);
@@ -898,7 +996,7 @@ function useAgents24ChatController({
898
996
  setStreamingContent("");
899
997
  setCurrentReasoning([]);
900
998
  }, [setActiveRunIdValue]);
901
- const clearMissingThread = (0, import_react2.useCallback)(
999
+ const clearMissingThread = (0, import_react4.useCallback)(
902
1000
  (threadId) => {
903
1001
  storage.deleteThread?.(threadId);
904
1002
  syncThreadsFromStorage();
@@ -920,7 +1018,7 @@ function useAgents24ChatController({
920
1018
  },
921
1019
  [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage, syncThreadsFromStorage]
922
1020
  );
923
- const setLiveAssistantMessage = (0, import_react2.useCallback)(
1021
+ const setLiveAssistantMessage = (0, import_react4.useCallback)(
924
1022
  (input) => {
925
1023
  setMessages((prev) => {
926
1024
  const next = upsertStableAssistantMessage(prev, {
@@ -952,7 +1050,7 @@ function useAgents24ChatController({
952
1050
  },
953
1051
  []
954
1052
  );
955
- const finalizeAssistantMessage = (0, import_react2.useCallback)(
1053
+ const finalizeAssistantMessage = (0, import_react4.useCallback)(
956
1054
  (input) => {
957
1055
  const content = input.error || input.assistantText.trim();
958
1056
  if (!content) return input.baseMessages;
@@ -1006,7 +1104,7 @@ function useAgents24ChatController({
1006
1104
  },
1007
1105
  [createId, persistThread, storage, upsertStoredThread]
1008
1106
  );
1009
- const loadThread = (0, import_react2.useCallback)(
1107
+ const loadThread = (0, import_react4.useCallback)(
1010
1108
  async (threadId) => {
1011
1109
  const seq = ++requestSeqRef.current;
1012
1110
  setLoadingHistory(true);
@@ -1016,7 +1114,8 @@ function useAgents24ChatController({
1016
1114
  const detail = await transport.getThread({
1017
1115
  threadId,
1018
1116
  limit: pageSize,
1019
- includeRunEvents: false
1117
+ includeRunEvents: false,
1118
+ signal: lifecycleAbortController.signal
1020
1119
  });
1021
1120
  if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
1022
1121
  void onThreadDetailLoaded?.(detail);
@@ -1045,12 +1144,12 @@ function useAgents24ChatController({
1045
1144
  }
1046
1145
  throw error;
1047
1146
  } finally {
1048
- if (activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
1147
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
1049
1148
  }
1050
1149
  },
1051
- [clearMissingThread, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
1150
+ [clearMissingThread, lifecycleAbortController, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
1052
1151
  );
1053
- const loadOlderTurns = (0, import_react2.useCallback)(async () => {
1152
+ const loadOlderTurns = (0, import_react4.useCallback)(async () => {
1054
1153
  const threadId = activeThreadIdRef.current;
1055
1154
  const beforeTurnIndex = nextBeforeTurnIndexRef.current;
1056
1155
  if (!threadId || beforeTurnIndex === null || isLoadingOlderRef.current) return;
@@ -1061,7 +1160,8 @@ function useAgents24ChatController({
1061
1160
  threadId,
1062
1161
  limit: pageSize,
1063
1162
  beforeTurnIndex,
1064
- includeRunEvents: false
1163
+ includeRunEvents: false,
1164
+ signal: lifecycleAbortController.signal
1065
1165
  });
1066
1166
  if (activeThreadIdRef.current !== threadId) return;
1067
1167
  const older = threadDetailToMessages(detail);
@@ -1081,11 +1181,11 @@ function useAgents24ChatController({
1081
1181
  }
1082
1182
  throw error;
1083
1183
  } finally {
1084
- if (activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
1184
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
1085
1185
  isLoadingOlderRef.current = false;
1086
1186
  }
1087
- }, [clearMissingThread, pageSize, persistThread, transport]);
1088
- const handleStreamEvent = (0, import_react2.useCallback)(
1187
+ }, [clearMissingThread, lifecycleAbortController, pageSize, persistThread, transport]);
1188
+ const handleStreamEvent = (0, import_react4.useCallback)(
1089
1189
  (input) => {
1090
1190
  const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
1091
1191
  const payload = event.payload || {};
@@ -1131,7 +1231,7 @@ function useAgents24ChatController({
1131
1231
  streamThreadIdRef.current = terminalThreadId;
1132
1232
  applyThreadId(terminalThreadId, baseMessages);
1133
1233
  }
1134
- const finalText = String(payload.assistant_output_text || textFromFinalOutput(payload.final_output) || streamingContentRef.current || "");
1234
+ const finalText = String(payload.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks || []) || streamingContentRef.current || "");
1135
1235
  finalizeAssistantMessage({
1136
1236
  threadId: streamThreadIdRef.current || activeThreadIdRef.current,
1137
1237
  baseMessages: messagesRef.current,
@@ -1146,7 +1246,7 @@ function useAgents24ChatController({
1146
1246
  },
1147
1247
  [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
1148
1248
  );
1149
- const runStream = (0, import_react2.useCallback)(
1249
+ const runStream = (0, import_react4.useCallback)(
1150
1250
  async (input) => {
1151
1251
  const startedAt = Date.now();
1152
1252
  const controller = new AbortController();
@@ -1269,18 +1369,26 @@ function useAgents24ChatController({
1269
1369
  },
1270
1370
  [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
1271
1371
  );
1272
- const handleSubmit = (0, import_react2.useCallback)(
1372
+ const handleSubmit = (0, import_react4.useCallback)(
1273
1373
  async (message) => {
1274
1374
  if (!message.text.trim() && !(message.files || []).length) return;
1275
1375
  await runStream({ mode: "submit", message });
1276
1376
  },
1277
1377
  [runStream]
1278
1378
  );
1279
- const attachRun = (0, import_react2.useCallback)(async (runId, threadId) => {
1379
+ const attachRun = (0, import_react4.useCallback)(async (runId, threadId) => {
1280
1380
  const resolvedThreadId = threadId ?? activeThreadIdRef.current;
1281
1381
  if (runId && resolvedThreadId) await runStream({ mode: "attach", runId, threadId: resolvedThreadId });
1282
1382
  }, [runStream]);
1283
- const handleStop = (0, import_react2.useCallback)(() => {
1383
+ const { pendingHitl, isResolvingHitl, resumeHitl } = useControllerHitl({
1384
+ messages,
1385
+ transport,
1386
+ activeRunId,
1387
+ activeRunIdRef,
1388
+ activeThreadIdRef,
1389
+ runStream
1390
+ });
1391
+ const handleStop = (0, import_react4.useCallback)(() => {
1284
1392
  const runId = activeRunIdRef.current;
1285
1393
  const partial = streamingContentRef.current;
1286
1394
  const liveMessageId = streamingMessageIdRef.current;
@@ -1304,45 +1412,25 @@ function useAgents24ChatController({
1304
1412
  });
1305
1413
  }
1306
1414
  }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
1307
- (0, import_react2.useEffect)(() => {
1308
- refresh().catch(() => setThreads(storage.listThreads()));
1309
- }, [refresh, storage]);
1310
- (0, import_react2.useEffect)(() => {
1311
- if (!transport.subscribeThreadEvents) return;
1312
- let cancelled = false;
1313
- let retryTimeout = null;
1314
- let controller = null;
1315
- const connect = () => {
1316
- if (cancelled) return;
1317
- controller = new AbortController();
1318
- transport.subscribeThreadEvents?.(
1319
- { cursor: threadEventsCursorRef.current, signal: controller.signal },
1320
- async (event) => {
1321
- if (typeof event.cursor === "number") threadEventsCursorRef.current = event.cursor;
1322
- if (event.event === "snapshot_required") {
1323
- await refresh().catch(() => void 0);
1324
- return;
1325
- }
1326
- const next = applyThreadSummaryEvent(storage.listThreads(), event);
1327
- storage.setThreads(next);
1328
- setThreads(storage.listThreads());
1329
- }
1330
- ).catch((error) => {
1331
- if (cancelled || isAbortError(error)) return;
1332
- retryTimeout = setTimeout(connect, 1500);
1333
- });
1334
- };
1335
- connect();
1415
+ (0, import_react4.useEffect)(() => {
1416
+ refresh().catch((error) => {
1417
+ if (!isAbortError(error) && !lifecycleAbortController.signal.aborted) {
1418
+ setThreads(storage.listThreads());
1419
+ }
1420
+ });
1421
+ }, [lifecycleAbortController, refresh, storage]);
1422
+ (0, import_react4.useEffect)(() => {
1336
1423
  return () => {
1337
- cancelled = true;
1338
- if (retryTimeout) clearTimeout(retryTimeout);
1339
- controller?.abort();
1424
+ lifecycleAbortController.abort();
1425
+ abortDetachedStream(abortControllerRef.current);
1426
+ abortControllerRef.current = null;
1340
1427
  };
1341
- }, [refresh, storage, transport]);
1342
- (0, import_react2.useEffect)(() => {
1428
+ }, [lifecycleAbortController]);
1429
+ useControllerThreadEvents({ transport, storage, refresh, setThreads });
1430
+ (0, import_react4.useEffect)(() => {
1343
1431
  syncThreadsFromStorage();
1344
1432
  }, [storageKey, syncThreadsFromStorage]);
1345
- (0, import_react2.useEffect)(() => {
1433
+ (0, import_react4.useEffect)(() => {
1346
1434
  const previous = activeThreadIdRef.current;
1347
1435
  activeThreadIdRef.current = activeThreadId;
1348
1436
  if (activeThreadId && previous === activeThreadId && (activeRunIdRef.current || streamingMessageIdRef.current)) {
@@ -1386,14 +1474,14 @@ function useAgents24ChatController({
1386
1474
  }
1387
1475
  void loadThread(activeThreadId).catch(() => setLoadingHistory(false));
1388
1476
  }, [activeThreadId, detachActiveStream, loadThread, setLoadingHistory, storage]);
1389
- (0, import_react2.useEffect)(() => {
1477
+ (0, import_react4.useEffect)(() => {
1390
1478
  const threadId = activeThreadId;
1391
1479
  if (!threadId || isLoadingHistory || loadedThreadIdRef.current !== threadId || activeRunIdRef.current) return;
1392
1480
  const runId = autoAttachRunIdFromThread(storage.getThread(threadId));
1393
1481
  if (!runId || reattachedRunIdRef.current === runId) return;
1394
1482
  void runStream({ mode: "attach", threadId, runId });
1395
1483
  }, [activeThreadId, isLoadingHistory, runStream, storage, storageKey]);
1396
- (0, import_react2.useEffect)(() => {
1484
+ (0, import_react4.useEffect)(() => {
1397
1485
  const threadId = activeThreadId;
1398
1486
  if (!threadId || isLoadingHistoryRef.current || activeRunIdRef.current || streamingMessageIdRef.current) {
1399
1487
  return;
@@ -1434,7 +1522,7 @@ function useAgents24ChatController({
1434
1522
  setMessages,
1435
1523
  storage
1436
1524
  });
1437
- const loadThreadById = (0, import_react2.useCallback)(
1525
+ const loadThreadById = (0, import_react4.useCallback)(
1438
1526
  async (threadId) => {
1439
1527
  if (!threadId) return;
1440
1528
  setIsSelectingThread(true);
@@ -1453,7 +1541,7 @@ function useAgents24ChatController({
1453
1541
  [detachActiveStream, loadThread, onActiveThreadIdChange, storage]
1454
1542
  );
1455
1543
  const activeThread = activeThreadId ? storage.getThread(activeThreadId) || threads.find((thread) => thread.id === activeThreadId) || null : null;
1456
- return (0, import_react2.useMemo)(() => ({
1544
+ return (0, import_react4.useMemo)(() => ({
1457
1545
  threads,
1458
1546
  activeThreadId,
1459
1547
  activeThread,
@@ -1473,8 +1561,11 @@ function useAgents24ChatController({
1473
1561
  copiedMessageId,
1474
1562
  lastThinkingDurationMs,
1475
1563
  activeRunId,
1564
+ pendingHitl,
1565
+ isResolvingHitl,
1476
1566
  handleSubmit,
1477
1567
  attachRun,
1568
+ resumeHitl,
1478
1569
  handleStop,
1479
1570
  handleCopy,
1480
1571
  handleLike,
@@ -1513,8 +1604,11 @@ function useAgents24ChatController({
1513
1604
  loadThreadById,
1514
1605
  loadOlderTurns,
1515
1606
  messages,
1607
+ isResolvingHitl,
1516
1608
  onSourceClick,
1609
+ pendingHitl,
1517
1610
  refresh,
1611
+ resumeHitl,
1518
1612
  streamingContent,
1519
1613
  streamingMessageId,
1520
1614
  startNewThread,
@@ -1737,6 +1831,7 @@ function LatestThreadScrollerOutline({
1737
1831
  );
1738
1832
  };
1739
1833
  updateSideRoom();
1834
+ if (typeof ResizeObserver === "undefined") return;
1740
1835
  const resizeObserver = new ResizeObserver(updateSideRoom);
1741
1836
  resizeObserver.observe(root);
1742
1837
  const content = layout?.contentElement;
@@ -1946,7 +2041,9 @@ var consumeSseResponse = async (response, onEvent, options = {}) => {
1946
2041
  let closed = false;
1947
2042
  const closeReader = () => {
1948
2043
  closed = true;
1949
- void reader.cancel().catch(() => {
2044
+ queueMicrotask(() => {
2045
+ void reader.cancel().catch(() => {
2046
+ });
1950
2047
  });
1951
2048
  };
1952
2049
  if (options.signal?.aborted) {
@@ -1981,7 +2078,7 @@ var consumeSseResponse = async (response, onEvent, options = {}) => {
1981
2078
  };
1982
2079
 
1983
2080
  // src/streaming-text.ts
1984
- var import_react3 = require("react");
2081
+ var import_react5 = require("react");
1985
2082
  var DEFAULT_COMPLETED_TEXT_CACHE_SIZE = 200;
1986
2083
  var defaultCompletedTextCache = /* @__PURE__ */ new Map();
1987
2084
  var defaultStreamingTextCache = {
@@ -2009,24 +2106,24 @@ function useStreamingText({
2009
2106
  maxCatchupChars = 20
2010
2107
  }) {
2011
2108
  const cacheAdapter = cache === false ? null : cache;
2012
- const [displayedText, setDisplayedText] = (0, import_react3.useState)(() => {
2109
+ const [displayedText, setDisplayedText] = (0, import_react5.useState)(() => {
2013
2110
  const cachedText = cacheAdapter?.get(id);
2014
2111
  return isStreaming && cachedText !== text ? "" : text;
2015
2112
  });
2016
- const targetRef = (0, import_react3.useRef)(text);
2017
- const displayedRef = (0, import_react3.useRef)(displayedText);
2018
- const rafRef = (0, import_react3.useRef)(null);
2019
- const lastFrameAtRef = (0, import_react3.useRef)(null);
2020
- const idRef = (0, import_react3.useRef)(id);
2021
- const shouldAnimateRef = (0, import_react3.useRef)(isStreaming);
2022
- (0, import_react3.useEffect)(() => {
2113
+ const targetRef = (0, import_react5.useRef)(text);
2114
+ const displayedRef = (0, import_react5.useRef)(displayedText);
2115
+ const rafRef = (0, import_react5.useRef)(null);
2116
+ const lastFrameAtRef = (0, import_react5.useRef)(null);
2117
+ const idRef = (0, import_react5.useRef)(id);
2118
+ const shouldAnimateRef = (0, import_react5.useRef)(isStreaming);
2119
+ (0, import_react5.useEffect)(() => {
2023
2120
  if (isStreaming || !text) return;
2024
2121
  cacheAdapter?.set(id, text);
2025
2122
  }, [cacheAdapter, id, isStreaming, text]);
2026
- (0, import_react3.useEffect)(() => {
2123
+ (0, import_react5.useEffect)(() => {
2027
2124
  targetRef.current = text;
2028
2125
  }, [text]);
2029
- (0, import_react3.useEffect)(() => {
2126
+ (0, import_react5.useEffect)(() => {
2030
2127
  if (idRef.current === id) return;
2031
2128
  idRef.current = id;
2032
2129
  const cachedText = cacheAdapter?.get(id);
@@ -2040,7 +2137,7 @@ function useStreamingText({
2040
2137
  }
2041
2138
  lastFrameAtRef.current = null;
2042
2139
  }, [cacheAdapter, id, isStreaming, text]);
2043
- (0, import_react3.useEffect)(() => {
2140
+ (0, import_react5.useEffect)(() => {
2044
2141
  if (typeof window === "undefined") {
2045
2142
  displayedRef.current = text;
2046
2143
  setDisplayedText(text);
@@ -2170,7 +2267,7 @@ var createFetchChatTransport = ({
2170
2267
  }
2171
2268
  )
2172
2269
  });
2173
- return consumeSseResponse(response, onEvent);
2270
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
2174
2271
  },
2175
2272
  async attachRun(input, onEvent) {
2176
2273
  const response = await fetchImpl(routes.attachRun(input), {
@@ -2179,7 +2276,7 @@ var createFetchChatTransport = ({
2179
2276
  headers: streamHeaders(await loadHeaders()),
2180
2277
  body: JSON.stringify({})
2181
2278
  });
2182
- return consumeSseResponse(response, onEvent);
2279
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
2183
2280
  },
2184
2281
  async cancelRun(input) {
2185
2282
  const response = await fetchImpl(routes.cancelRun(input), {
@@ -2206,7 +2303,11 @@ var createFetchChatTransport = ({
2206
2303
  signal: input.signal,
2207
2304
  headers: streamHeaders(await loadHeaders())
2208
2305
  });
2209
- await consumeSseResponse(response, (event) => onEvent(event));
2306
+ await consumeSseResponse(
2307
+ response,
2308
+ (event) => onEvent(event),
2309
+ { signal: input.signal }
2310
+ );
2210
2311
  },
2211
2312
  async resumeHitl(input) {
2212
2313
  if (!routes.resumeRun) throw new ChatTransportError("HITL resume is not configured.", 501);
@@ -2214,9 +2315,10 @@ var createFetchChatTransport = ({
2214
2315
  method: "POST",
2215
2316
  headers: jsonHeaders(await loadHeaders()),
2216
2317
  body: JSON.stringify({
2217
- schema_version: "agents24.hitl.resume.v1",
2318
+ schema_version: "agents24.hitl.resume.v2",
2218
2319
  interrupt_id: input.interruptId,
2219
- decisions: input.decisions,
2320
+ action: input.action,
2321
+ comment: input.comment,
2220
2322
  client: input.client
2221
2323
  })
2222
2324
  });
@@ -2286,7 +2388,6 @@ var import_message_scroller2 = require("@shadcn/react/message-scroller");
2286
2388
  partsFromResponseBlocks,
2287
2389
  reasoningStepsFromParts,
2288
2390
  renderChatPart,
2289
- textFromFinalOutput,
2290
2391
  threadActivityDate,
2291
2392
  threadDetailToMessages,
2292
2393
  threadPaging,