@agents24/chat-react 0.1.8 → 0.1.10

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
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ChatTransportError: () => ChatTransportError,
33
34
  DEFAULT_THREAD_PAGE_SIZE: () => DEFAULT_THREAD_PAGE_SIZE,
34
35
  DefaultChatPart: () => DefaultChatPart,
35
36
  DefaultToolPart: () => DefaultToolPart,
@@ -44,21 +45,26 @@ __export(index_exports, {
44
45
  MessageScroller: () => import_message_scroller2.MessageScroller,
45
46
  activeRunIdFromThread: () => activeRunIdFromThread,
46
47
  activeRunIdFromThreadDetail: () => activeRunIdFromThreadDetail,
48
+ appendStableStreamingTurn: () => appendStableStreamingTurn,
47
49
  assistantTextFromParts: () => assistantTextFromParts,
48
50
  assistantTextFromResponseBlocks: () => assistantTextFromResponseBlocks,
51
+ autoAttachRunIdFromThread: () => autoAttachRunIdFromThread,
49
52
  compressionFromContextWindow: () => compressionFromContextWindow,
50
53
  consumeSseResponse: () => consumeSseResponse,
51
54
  createChatId: () => createChatId,
52
55
  createFetchChatTransport: () => createFetchChatTransport,
56
+ findStableAssistantMessageIndex: () => findStableAssistantMessageIndex,
53
57
  getActiveStreamingTextPartId: () => getActiveStreamingTextPartId,
54
58
  hasStaleUnfinishedAssistantCache: () => hasStaleUnfinishedAssistantCache,
55
59
  hasUnfinishedAssistantMessage: () => hasUnfinishedAssistantMessage,
56
60
  isActiveStreamingTextPart: () => isActiveStreamingTextPart,
61
+ isAutoAttachThreadStatus: () => isAutoAttachThreadStatus,
57
62
  isRunningThreadStatus: () => isRunningThreadStatus,
58
63
  latestContextWindowFromThread: () => latestContextWindowFromThread,
59
64
  mergeContextWindow: () => mergeContextWindow,
60
65
  mergeContextWindowUpdate: () => mergeContextWindowUpdate,
61
66
  mergeReasoningSteps: () => mergeReasoningSteps,
67
+ mergeStoredThreadsForRefresh: () => mergeStoredThreadsForRefresh,
62
68
  normalizeContextCompression: () => normalizeContextCompression,
63
69
  normalizeContextWindow: () => normalizeContextWindow,
64
70
  parseSseBlock: () => parseSseBlock,
@@ -71,6 +77,7 @@ __export(index_exports, {
71
77
  threadPaging: () => threadPaging,
72
78
  titleFromMessage: () => titleFromMessage,
73
79
  toolStateFromStatus: () => toolStateFromStatus,
80
+ upsertStableAssistantMessage: () => upsertStableAssistantMessage,
74
81
  useAgents24ChatController: () => useAgents24ChatController,
75
82
  useMessageScroller: () => import_message_scroller2.useMessageScroller,
76
83
  useMessageScrollerScrollable: () => import_message_scroller2.useMessageScrollerScrollable,
@@ -80,7 +87,79 @@ __export(index_exports, {
80
87
  module.exports = __toCommonJS(index_exports);
81
88
 
82
89
  // src/controller.ts
90
+ var import_react2 = require("react");
91
+
92
+ // src/controller-actions.ts
83
93
  var import_react = require("react");
94
+ function useControllerMessageActions(input) {
95
+ const handleCopy = (0, import_react.useCallback)((content, messageId) => {
96
+ navigator.clipboard?.writeText(content);
97
+ input.setCopiedMessageId(messageId);
98
+ setTimeout(() => input.setCopiedMessageId(null), 200);
99
+ }, [input]);
100
+ const handleLike = (0, import_react.useCallback)(async (msg) => {
101
+ const nextLiked = !input.liked[msg.id];
102
+ input.setLiked((prev) => ({ ...prev, [msg.id]: nextLiked }));
103
+ if (nextLiked) input.setDisliked((prev) => ({ ...prev, [msg.id]: false }));
104
+ }, [input]);
105
+ const handleDislike = (0, import_react.useCallback)(async (msg) => {
106
+ const nextDisliked = !input.disliked[msg.id];
107
+ input.setDisliked((prev) => ({ ...prev, [msg.id]: nextDisliked }));
108
+ if (nextDisliked) input.setLiked((prev) => ({ ...prev, [msg.id]: false }));
109
+ }, [input]);
110
+ const handleRetry = (0, import_react.useCallback)(async (msg) => {
111
+ const index = input.messagesRef.current.findIndex((message) => message.id === msg.id);
112
+ if (index <= 0) return;
113
+ const userMessage = input.messagesRef.current[index - 1];
114
+ if (userMessage.role !== "user") return;
115
+ const trimmed = input.messagesRef.current.slice(0, index);
116
+ input.setMessages(trimmed);
117
+ input.messagesRef.current = trimmed;
118
+ if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, trimmed);
119
+ await input.handleSubmit({ text: userMessage.content, files: userMessage.attachments || [] });
120
+ }, [input]);
121
+ const upsertLiveVoiceMessage = (0, import_react.useCallback)((payload) => {
122
+ const content = payload.content?.trim() ?? "";
123
+ if (!content && !payload.citations?.length && !payload.reasoningSteps?.length) return;
124
+ input.setMessages((prev) => {
125
+ const currentId = input.liveVoiceIdsRef.current[payload.role];
126
+ const index = currentId ? prev.findIndex((message) => message.id === currentId) : -1;
127
+ const nextMessage = {
128
+ id: currentId || input.createId(),
129
+ role: payload.role,
130
+ content,
131
+ createdAt: /* @__PURE__ */ new Date(),
132
+ isFinal: Boolean(payload.isFinal),
133
+ isVoice: payload.role === "user",
134
+ parts: content ? [{ id: input.createId(), type: "text", kind: "text", text: content }] : [],
135
+ citations: payload.citations,
136
+ reasoningSteps: payload.reasoningSteps
137
+ };
138
+ const next = index === -1 ? [...prev, nextMessage] : prev.map((message, itemIndex) => itemIndex === index ? { ...message, ...nextMessage } : message);
139
+ input.liveVoiceIdsRef.current[payload.role] = payload.isFinal ? void 0 : nextMessage.id;
140
+ input.messagesRef.current = next;
141
+ if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, next);
142
+ return next;
143
+ });
144
+ }, [input]);
145
+ const startNewThread = (0, import_react.useCallback)(() => {
146
+ input.detachActiveStream();
147
+ input.requestSeqRef.current += 1;
148
+ input.activeThreadIdRef.current = null;
149
+ input.loadedThreadIdRef.current = null;
150
+ input.nextBeforeTurnIndexRef.current = null;
151
+ input.hasOlderTurnsRef.current = false;
152
+ input.storage.setActiveThreadId?.(null);
153
+ input.onActiveThreadIdChange?.(null);
154
+ input.setMessages([]);
155
+ input.messagesRef.current = [];
156
+ input.setHasOlderTurns(false);
157
+ input.setIsLoadingOlder(false);
158
+ input.setContextStatus(null);
159
+ input.setLoadingHistory(false);
160
+ }, [input]);
161
+ return { handleCopy, handleDislike, handleLike, handleRetry, startNewThread, upsertLiveVoiceMessage };
162
+ }
84
163
 
85
164
  // src/context-window.ts
86
165
  var SOURCE_PRIORITY = {
@@ -203,7 +282,8 @@ function compressionFromContextWindow(contextWindow) {
203
282
 
204
283
  // src/model.ts
205
284
  var DEFAULT_THREAD_PAGE_SIZE = 5;
206
- var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
285
+ var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling", "paused"])).has(String(status || "").toLowerCase());
286
+ var isAutoAttachThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
207
287
  var createChatId = () => {
208
288
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
209
289
  return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
@@ -369,8 +449,17 @@ var partsFromResponseBlocks = (blocks, fallbackText) => {
369
449
  });
370
450
  return;
371
451
  }
372
- if (block.kind === "approval_request") {
373
- parts.push({ id, type: "approval", kind: "approval", raw: block });
452
+ if (block.kind === "hitl_request") {
453
+ const hitl = asRecord(block.hitl) || block;
454
+ parts.push({
455
+ id,
456
+ type: "hitl",
457
+ kind: "hitl",
458
+ hitl,
459
+ interruptId: optionalString(block.interruptId) || optionalString(hitl.interrupt_id) || null,
460
+ hitlKind: optionalString(block.hitlKind) || optionalString(hitl.kind) || null,
461
+ raw: block
462
+ });
374
463
  return;
375
464
  }
376
465
  if (block.kind === "error") {
@@ -496,6 +585,23 @@ var activeRunIdFromThread = (thread) => {
496
585
  const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
497
586
  return lastRunId && isRunningThreadStatus(lastRunStatus) ? String(lastRunId) : null;
498
587
  };
588
+ var autoAttachRunIdFromThread = (thread) => {
589
+ const hasCamelActiveRun = Boolean(
590
+ thread && Object.prototype.hasOwnProperty.call(thread, "activeRun")
591
+ );
592
+ const hasCamelLastRunStatus = Boolean(
593
+ thread && Object.prototype.hasOwnProperty.call(thread, "lastRunStatus")
594
+ );
595
+ const hasCamelLastRunId = Boolean(
596
+ thread && Object.prototype.hasOwnProperty.call(thread, "lastRunId")
597
+ );
598
+ const activeRun = hasCamelActiveRun ? thread?.activeRun || null : thread?.active_run || null;
599
+ const lastRunStatus = hasCamelLastRunStatus ? thread?.lastRunStatus || null : thread?.last_run_status || thread?.lastRunStatus || null;
600
+ const activeRunId = activeRun?.run_id ? String(activeRun.run_id) : "";
601
+ if (activeRunId && isAutoAttachThreadStatus(activeRun?.status || lastRunStatus)) return activeRunId;
602
+ const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
603
+ return lastRunId && isAutoAttachThreadStatus(lastRunStatus) ? String(lastRunId) : null;
604
+ };
499
605
  var hasUnfinishedAssistantMessage = (messages) => Boolean(messages?.some((message) => message.role === "assistant" && message.isFinal === false));
500
606
  var hasStaleUnfinishedAssistantCache = (thread) => hasUnfinishedAssistantMessage(thread?.messages) && !activeRunIdFromThread(thread);
501
607
  var activeRunIdFromThreadDetail = (thread) => {
@@ -505,7 +611,7 @@ var activeRunIdFromThreadDetail = (thread) => {
505
611
  return runningTurn?.run_id ? String(runningTurn.run_id) : null;
506
612
  };
507
613
 
508
- // src/controller.ts
614
+ // src/controller-helpers.ts
509
615
  var threadSummaryToStored = (thread) => ({
510
616
  ...thread,
511
617
  id: String(thread.id),
@@ -514,6 +620,121 @@ var threadSummaryToStored = (thread) => ({
514
620
  messages: [],
515
621
  isHydrated: false
516
622
  });
623
+ var isThreadNotFoundError = (error) => {
624
+ if (!error || typeof error !== "object" || !("status" in error)) return false;
625
+ return Number(error.status) === 404;
626
+ };
627
+ var isAbortError = (error) => {
628
+ if (!error || typeof error !== "object") return false;
629
+ const maybe = error;
630
+ return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
631
+ };
632
+ var abortDetachedStream = (controller) => {
633
+ if (!controller || controller.signal.aborted) return;
634
+ try {
635
+ const reason = typeof DOMException !== "undefined" ? new DOMException("Chat stream detached.", "AbortError") : new Error("Chat stream detached.");
636
+ controller.abort(reason);
637
+ } catch {
638
+ }
639
+ };
640
+ var hasRunState = (thread) => Boolean(
641
+ thread.active_run || thread.activeRun || thread.last_run_id || thread.lastRunId || thread.last_run_status || thread.lastRunStatus || thread.isRunning
642
+ );
643
+ var mergeStoredThreadsForRefresh = (currentThreads, serverThreads) => {
644
+ const currentById = new Map(currentThreads.map((thread) => [thread.id, thread]));
645
+ const serverIds = new Set(serverThreads.map((thread) => thread.id));
646
+ const mergedServerThreads = serverThreads.map((serverThread) => {
647
+ const current = currentById.get(serverThread.id);
648
+ if (!current) return serverThread;
649
+ const incomingHasRunState = hasRunState(serverThread);
650
+ return {
651
+ ...current,
652
+ ...serverThread,
653
+ messages: current.messages || [],
654
+ isHydrated: current.isHydrated,
655
+ hasOlderTurns: current.hasOlderTurns,
656
+ nextBeforeTurnIndex: current.nextBeforeTurnIndex,
657
+ active_run: incomingHasRunState ? serverThread.active_run : current.active_run,
658
+ activeRun: incomingHasRunState ? serverThread.activeRun : current.activeRun,
659
+ last_run_id: incomingHasRunState ? serverThread.last_run_id : current.last_run_id,
660
+ lastRunId: incomingHasRunState ? serverThread.lastRunId : current.lastRunId,
661
+ last_run_status: incomingHasRunState ? serverThread.last_run_status : current.last_run_status,
662
+ lastRunStatus: incomingHasRunState ? serverThread.lastRunStatus : current.lastRunStatus,
663
+ lastEventSeq: incomingHasRunState ? serverThread.lastEventSeq : current.lastEventSeq,
664
+ isRunning: incomingHasRunState ? serverThread.isRunning : current.isRunning
665
+ };
666
+ });
667
+ const localOnlyThreads = currentThreads.filter((thread) => {
668
+ if (serverIds.has(thread.id)) return false;
669
+ return Boolean(thread.isHydrated || thread.messages?.length || thread.isRunning || thread.activeRun || thread.active_run);
670
+ });
671
+ return [...mergedServerThreads, ...localOnlyThreads];
672
+ };
673
+ var stableStringify = (value) => JSON.stringify(value ?? null);
674
+ var sameStoredThread = (left, right) => {
675
+ if (left === right) return true;
676
+ if (!left || !right) return false;
677
+ return stableStringify(left) === stableStringify(right);
678
+ };
679
+ var applyThreadSummaryEvent = (currentThreads, event) => {
680
+ if (event.event === "snapshot_required") return currentThreads;
681
+ if (event.event === "thread.deleted") {
682
+ const next2 = currentThreads.filter((thread) => thread.id !== event.thread_id);
683
+ return next2.length === currentThreads.length ? currentThreads : next2;
684
+ }
685
+ const incoming = threadSummaryToStored(event.thread);
686
+ const index = currentThreads.findIndex((thread) => thread.id === incoming.id);
687
+ const sortByActivity = (threads) => [...threads].sort((a, b) => {
688
+ const left = Date.parse(String(a.updated_at || a.last_activity_at || a.created_at || ""));
689
+ const right = Date.parse(String(b.updated_at || b.last_activity_at || b.created_at || ""));
690
+ return (Number.isFinite(right) ? right : 0) - (Number.isFinite(left) ? left : 0);
691
+ });
692
+ if (index === -1) return sortByActivity([...currentThreads, incoming]);
693
+ const [merged] = mergeStoredThreadsForRefresh([currentThreads[index]], [incoming]);
694
+ if (sameStoredThread(currentThreads[index], merged)) return currentThreads;
695
+ const next = [...currentThreads];
696
+ next[index] = merged;
697
+ return sortByActivity(next);
698
+ };
699
+
700
+ // src/message-lifecycle.ts
701
+ function findStableAssistantMessageIndex(messages, input) {
702
+ return messages.findIndex(
703
+ (message) => message.role === "assistant" && (input.messageId && message.id === input.messageId || Boolean(input.runId && message.runId === input.runId))
704
+ );
705
+ }
706
+ function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
707
+ const next = [...messages];
708
+ if (!next.some((message) => message.id === userMessage.id)) {
709
+ next.push(userMessage);
710
+ }
711
+ if (!next.some(
712
+ (message) => message.id === assistantMessage.id || Boolean(
713
+ assistantMessage.runId && message.role === "assistant" && message.runId === assistantMessage.runId
714
+ )
715
+ )) {
716
+ next.push(assistantMessage);
717
+ }
718
+ return next;
719
+ }
720
+ function upsertStableAssistantMessage(messages, input) {
721
+ const next = [...messages];
722
+ for (const baseMessage of input.baseMessages || []) {
723
+ const matchesTarget = baseMessage.role === "assistant" && (input.messageId && baseMessage.id === input.messageId || Boolean(input.runId && baseMessage.runId === input.runId));
724
+ if (!matchesTarget && !next.some((message) => message.id === baseMessage.id)) {
725
+ next.push(baseMessage);
726
+ }
727
+ }
728
+ const index = findStableAssistantMessageIndex(next, input);
729
+ if (index === -1) {
730
+ next.push(input.create());
731
+ return next;
732
+ }
733
+ next[index] = input.update(next[index]);
734
+ return next;
735
+ }
736
+
737
+ // src/controller.ts
517
738
  function useAgents24ChatController({
518
739
  transport,
519
740
  storage,
@@ -529,56 +750,59 @@ function useAgents24ChatController({
529
750
  }) {
530
751
  const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
531
752
  const initialCached = activeThreadId ? storage.getThread(activeThreadId) : void 0;
532
- const [messages, setMessages] = (0, import_react.useState)(() => initialCached?.messages || []);
533
- const [isLoading, setIsLoading] = (0, import_react.useState)(false);
534
- const [isLoadingHistory, setIsLoadingHistory] = (0, import_react.useState)(() => Boolean(activeThreadId) && !initialCached?.messages?.length);
535
- const [isLoadingOlder, setIsLoadingOlder] = (0, import_react.useState)(false);
536
- const [hasOlderTurns, setHasOlderTurns] = (0, import_react.useState)(Boolean(initialCached?.hasOlderTurns));
537
- const [streamingContent, setStreamingContent] = (0, import_react.useState)("");
538
- const [streamingMessageId, setStreamingMessageId] = (0, import_react.useState)(null);
539
- const [contextStatus, setContextStatus] = (0, import_react.useState)(null);
540
- const [currentReasoning, setCurrentReasoning] = (0, import_react.useState)([]);
541
- const [liked, setLiked] = (0, import_react.useState)({});
542
- const [disliked, setDisliked] = (0, import_react.useState)({});
543
- const [copiedMessageId, setCopiedMessageId] = (0, import_react.useState)(null);
544
- const [lastThinkingDurationMs, setLastThinkingDurationMs] = (0, import_react.useState)(null);
545
- const [threads, setThreads] = (0, import_react.useState)(() => storage.listThreads());
546
- const [isRefreshingThreads, setIsRefreshingThreads] = (0, import_react.useState)(false);
547
- const [activeRunId, setActiveRunId] = (0, import_react.useState)(null);
548
- const textareaRef = (0, import_react.useRef)(null);
549
- const activeThreadIdRef = (0, import_react.useRef)(activeThreadId);
550
- const messagesRef = (0, import_react.useRef)(messages);
551
- const nextBeforeTurnIndexRef = (0, import_react.useRef)(initialCached?.nextBeforeTurnIndex ?? null);
552
- const hasOlderTurnsRef = (0, import_react.useRef)(Boolean(initialCached?.hasOlderTurns));
553
- const isLoadingOlderRef = (0, import_react.useRef)(false);
554
- const loadedThreadIdRef = (0, import_react.useRef)(initialCached?.messages?.length ? activeThreadId : null);
555
- const requestSeqRef = (0, import_react.useRef)(0);
556
- const isLoadingHistoryRef = (0, import_react.useRef)(isLoadingHistory);
557
- const activeRunIdRef = (0, import_react.useRef)(null);
558
- const reattachedRunIdRef = (0, import_react.useRef)(null);
559
- const abortControllerRef = (0, import_react.useRef)(null);
560
- const streamingContentRef = (0, import_react.useRef)("");
561
- const streamingMessageIdRef = (0, import_react.useRef)(null);
562
- const reasoningRef = (0, import_react.useRef)([]);
563
- const liveVoiceIdsRef = (0, import_react.useRef)({});
564
- const setActiveRunIdValue = (0, import_react.useCallback)((runId) => {
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) => {
565
789
  activeRunIdRef.current = runId;
566
790
  setActiveRunId(runId);
567
791
  }, []);
568
- const syncThreadsFromStorage = (0, import_react.useCallback)(() => {
792
+ const syncThreadsFromStorage = (0, import_react2.useCallback)(() => {
569
793
  setThreads(storage.listThreads());
570
794
  }, [storage]);
571
- const upsertStoredThread = (0, import_react.useCallback)(
795
+ const upsertStoredThread = (0, import_react2.useCallback)(
572
796
  (thread) => {
573
797
  storage.upsertThread(thread);
574
798
  syncThreadsFromStorage();
575
799
  },
576
800
  [storage, syncThreadsFromStorage]
577
801
  );
578
- (0, import_react.useEffect)(() => {
802
+ (0, import_react2.useEffect)(() => {
579
803
  messagesRef.current = messages;
580
804
  }, [messages]);
581
- const persistThread = (0, import_react.useCallback)(
805
+ const persistThread = (0, import_react2.useCallback)(
582
806
  (threadId, nextMessages, paging, options) => {
583
807
  const existing = storage.getThread(threadId);
584
808
  const firstUser = nextMessages.find((message) => message.role === "user");
@@ -595,7 +819,7 @@ function useAgents24ChatController({
595
819
  },
596
820
  [storage, upsertStoredThread]
597
821
  );
598
- const markThreadRunStatus = (0, import_react.useCallback)(
822
+ const markThreadRunStatus = (0, import_react2.useCallback)(
599
823
  (threadId, runId, status, lastEventSeq) => {
600
824
  const existing = storage.getThread(threadId);
601
825
  if (!existing || !runId) return;
@@ -625,18 +849,20 @@ function useAgents24ChatController({
625
849
  },
626
850
  [storage, upsertStoredThread]
627
851
  );
628
- const refresh = (0, import_react.useCallback)(async () => {
852
+ const refresh = (0, import_react2.useCallback)(async () => {
853
+ const seq = ++refreshSeqRef.current;
629
854
  setIsRefreshingThreads(true);
630
855
  try {
631
856
  const data = await transport.listThreads();
857
+ if (seq !== refreshSeqRef.current) return;
632
858
  const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
633
- storage.setThreads(nextThreads);
859
+ storage.setThreads(mergeStoredThreadsForRefresh(storage.listThreads(), nextThreads));
634
860
  setThreads(storage.listThreads());
635
861
  } finally {
636
- setIsRefreshingThreads(false);
862
+ if (seq === refreshSeqRef.current) setIsRefreshingThreads(false);
637
863
  }
638
864
  }, [storage, transport]);
639
- const applyThreadId = (0, import_react.useCallback)(
865
+ const applyThreadId = (0, import_react2.useCallback)(
640
866
  (threadId, baseMessages) => {
641
867
  if (!threadId || activeThreadIdRef.current === threadId) return;
642
868
  activeThreadIdRef.current = threadId;
@@ -650,18 +876,18 @@ function useAgents24ChatController({
650
876
  streamingContentRef.current = value;
651
877
  setStreamingContent(value);
652
878
  };
653
- const setLoadingHistory = (0, import_react.useCallback)((value) => {
879
+ const setLoadingHistory = (0, import_react2.useCallback)((value) => {
654
880
  isLoadingHistoryRef.current = value;
655
881
  setIsLoadingHistory(value);
656
882
  }, []);
657
- const setReasoningSteps = (0, import_react.useCallback)((value) => {
883
+ const setReasoningSteps = (0, import_react2.useCallback)((value) => {
658
884
  reasoningRef.current = value || [];
659
885
  setCurrentReasoning(value || []);
660
886
  }, []);
661
- const detachActiveStream = (0, import_react.useCallback)(() => {
887
+ const detachActiveStream = (0, import_react2.useCallback)(() => {
662
888
  const controller = abortControllerRef.current;
663
889
  abortControllerRef.current = null;
664
- controller?.abort();
890
+ abortDetachedStream(controller);
665
891
  setActiveRunIdValue(null);
666
892
  reattachedRunIdRef.current = null;
667
893
  streamingMessageIdRef.current = null;
@@ -672,21 +898,36 @@ function useAgents24ChatController({
672
898
  setStreamingContent("");
673
899
  setCurrentReasoning([]);
674
900
  }, [setActiveRunIdValue]);
675
- const setLiveAssistantMessage = (0, import_react.useCallback)(
901
+ const clearMissingThread = (0, import_react2.useCallback)(
902
+ (threadId) => {
903
+ storage.deleteThread?.(threadId);
904
+ syncThreadsFromStorage();
905
+ if (activeThreadIdRef.current !== threadId) return;
906
+ requestSeqRef.current += 1;
907
+ detachActiveStream();
908
+ activeThreadIdRef.current = null;
909
+ loadedThreadIdRef.current = null;
910
+ nextBeforeTurnIndexRef.current = null;
911
+ hasOlderTurnsRef.current = false;
912
+ storage.setActiveThreadId?.(null);
913
+ onActiveThreadIdChange?.(null);
914
+ setMessages([]);
915
+ messagesRef.current = [];
916
+ setHasOlderTurns(false);
917
+ setIsLoadingOlder(false);
918
+ setContextStatus(null);
919
+ setLoadingHistory(false);
920
+ },
921
+ [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage, syncThreadsFromStorage]
922
+ );
923
+ const setLiveAssistantMessage = (0, import_react2.useCallback)(
676
924
  (input) => {
677
925
  setMessages((prev) => {
678
- const index = prev.findIndex(
679
- (message) => message.id === input.messageId || input.runId && message.role === "assistant" && message.runId === input.runId
680
- );
681
- if (index === -1) {
682
- const next2 = [...prev];
683
- (input.baseMessages || []).forEach((message) => {
684
- const isSameAssistant = message.id === input.messageId || Boolean(input.runId && message.role === "assistant" && message.runId === input.runId);
685
- if (!isSameAssistant && !next2.some((item) => item.id === message.id)) {
686
- next2.push(message);
687
- }
688
- });
689
- next2.push({
926
+ const next = upsertStableAssistantMessage(prev, {
927
+ baseMessages: input.baseMessages,
928
+ messageId: input.messageId,
929
+ runId: input.runId,
930
+ create: () => ({
690
931
  id: input.messageId,
691
932
  role: "assistant",
692
933
  runId: input.runId ?? null,
@@ -695,26 +936,23 @@ function useAgents24ChatController({
695
936
  reasoningSteps: input.reasoning,
696
937
  isFinal: false,
697
938
  parts: input.parts || []
698
- });
699
- messagesRef.current = next2;
700
- return next2;
701
- }
702
- const next = [...prev];
703
- next[index] = {
704
- ...next[index],
705
- runId: input.runId ?? next[index].runId ?? null,
706
- content: input.content,
707
- reasoningSteps: input.reasoning,
708
- isFinal: false,
709
- parts: input.parts ?? next[index].parts
710
- };
939
+ }),
940
+ update: (message) => ({
941
+ ...message,
942
+ runId: input.runId ?? message.runId ?? null,
943
+ content: input.content,
944
+ reasoningSteps: input.reasoning,
945
+ isFinal: false,
946
+ parts: input.parts ?? message.parts
947
+ })
948
+ });
711
949
  messagesRef.current = next;
712
950
  return next;
713
951
  });
714
952
  },
715
953
  []
716
954
  );
717
- const finalizeAssistantMessage = (0, import_react.useCallback)(
955
+ const finalizeAssistantMessage = (0, import_react2.useCallback)(
718
956
  (input) => {
719
957
  const content = input.error || input.assistantText.trim();
720
958
  if (!content) return input.baseMessages;
@@ -734,7 +972,17 @@ function useAgents24ChatController({
734
972
  reasoningSteps: mergeReasoningSteps(input.reasoning, { finalize: true }),
735
973
  thinkingDurationMs: input.thinkingDurationMs
736
974
  };
737
- const completed = existingIndex >= 0 ? input.baseMessages.map((message, index) => index === existingIndex ? assistant : message) : [...input.baseMessages, assistant];
975
+ const completed = upsertStableAssistantMessage(input.baseMessages, {
976
+ messageId: input.messageId,
977
+ runId: input.runId,
978
+ create: () => assistant,
979
+ update: (message) => ({
980
+ ...message,
981
+ ...assistant,
982
+ id: message.id,
983
+ createdAt: message.createdAt
984
+ })
985
+ });
738
986
  setMessages(completed);
739
987
  messagesRef.current = completed;
740
988
  if (input.threadId) {
@@ -758,7 +1006,7 @@ function useAgents24ChatController({
758
1006
  },
759
1007
  [createId, persistThread, storage, upsertStoredThread]
760
1008
  );
761
- const loadThread = (0, import_react.useCallback)(
1009
+ const loadThread = (0, import_react2.useCallback)(
762
1010
  async (threadId) => {
763
1011
  const seq = ++requestSeqRef.current;
764
1012
  setLoadingHistory(true);
@@ -789,13 +1037,20 @@ function useAgents24ChatController({
789
1037
  nextBeforeTurnIndex: paging.nextBeforeTurnIndex,
790
1038
  updated_at: threadActivityDate(detail)
791
1039
  });
1040
+ } catch (error) {
1041
+ if (isAbortError(error)) return;
1042
+ if (isThreadNotFoundError(error)) {
1043
+ clearMissingThread(threadId);
1044
+ return;
1045
+ }
1046
+ throw error;
792
1047
  } finally {
793
- if (activeThreadIdRef.current === threadId) setLoadingHistory(false);
1048
+ if (activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
794
1049
  }
795
1050
  },
796
- [onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
1051
+ [clearMissingThread, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
797
1052
  );
798
- const loadOlderTurns = (0, import_react.useCallback)(async () => {
1053
+ const loadOlderTurns = (0, import_react2.useCallback)(async () => {
799
1054
  const threadId = activeThreadIdRef.current;
800
1055
  const beforeTurnIndex = nextBeforeTurnIndexRef.current;
801
1056
  if (!threadId || beforeTurnIndex === null || isLoadingOlderRef.current) return;
@@ -818,12 +1073,19 @@ function useAgents24ChatController({
818
1073
  hasOlderTurnsRef.current = paging.hasOlderTurns;
819
1074
  nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
820
1075
  persistThread(threadId, next, paging);
1076
+ } catch (error) {
1077
+ if (isAbortError(error)) return;
1078
+ if (isThreadNotFoundError(error)) {
1079
+ clearMissingThread(threadId);
1080
+ return;
1081
+ }
1082
+ throw error;
821
1083
  } finally {
822
1084
  if (activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
823
1085
  isLoadingOlderRef.current = false;
824
1086
  }
825
- }, [pageSize, persistThread, transport]);
826
- const handleStreamEvent = (0, import_react.useCallback)(
1087
+ }, [clearMissingThread, pageSize, persistThread, transport]);
1088
+ const handleStreamEvent = (0, import_react2.useCallback)(
827
1089
  (input) => {
828
1090
  const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
829
1091
  const payload = event.payload || {};
@@ -884,7 +1146,7 @@ function useAgents24ChatController({
884
1146
  },
885
1147
  [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
886
1148
  );
887
- const runStream = (0, import_react.useCallback)(
1149
+ const runStream = (0, import_react2.useCallback)(
888
1150
  async (input) => {
889
1151
  const startedAt = Date.now();
890
1152
  const controller = new AbortController();
@@ -943,19 +1205,28 @@ function useAgents24ChatController({
943
1205
  );
944
1206
  }
945
1207
  try {
1208
+ let streamResult = null;
946
1209
  if (input.mode === "attach") {
947
1210
  setActiveRunIdValue(input.runId);
948
1211
  reattachedRunIdRef.current = input.runId;
949
- await transport.attachRun(
1212
+ streamResult = await transport.attachRun(
950
1213
  { runId: input.runId, signal: controller.signal },
951
1214
  (event) => handleStreamEvent({ mode: "attach", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
952
1215
  );
953
1216
  } else {
954
- await transport.streamMessage(
1217
+ streamResult = await transport.streamMessage(
955
1218
  { ...input.message, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
956
1219
  (event) => handleStreamEvent({ mode: "submit", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
957
1220
  );
958
1221
  }
1222
+ if (streamResult?.runId) setActiveRunIdValue(streamResult.runId);
1223
+ if (streamResult?.threadId) {
1224
+ streamThreadIdRef.current = streamResult.threadId;
1225
+ applyThreadId(streamResult.threadId, baseMessages);
1226
+ if (streamResult.runId) {
1227
+ markThreadRunStatus(streamResult.threadId, streamResult.runId, "running");
1228
+ }
1229
+ }
959
1230
  if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
960
1231
  finalizeAssistantMessage({
961
1232
  threadId: streamThreadIdRef.current,
@@ -969,7 +1240,7 @@ function useAgents24ChatController({
969
1240
  }
970
1241
  await refresh().catch(() => void 0);
971
1242
  } catch (error) {
972
- if (error.name !== "AbortError") {
1243
+ if (!isAbortError(error)) {
973
1244
  finalizeAssistantMessage({
974
1245
  threadId: streamThreadIdRef.current,
975
1246
  baseMessages: messagesRef.current,
@@ -998,18 +1269,22 @@ function useAgents24ChatController({
998
1269
  },
999
1270
  [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
1000
1271
  );
1001
- const handleSubmit = (0, import_react.useCallback)(
1272
+ const handleSubmit = (0, import_react2.useCallback)(
1002
1273
  async (message) => {
1003
1274
  if (!message.text.trim() && !(message.files || []).length) return;
1004
1275
  await runStream({ mode: "submit", message });
1005
1276
  },
1006
1277
  [runStream]
1007
1278
  );
1008
- const handleStop = (0, import_react.useCallback)(() => {
1279
+ const attachRun = (0, import_react2.useCallback)(async (runId, threadId) => {
1280
+ const resolvedThreadId = threadId ?? activeThreadIdRef.current;
1281
+ if (runId && resolvedThreadId) await runStream({ mode: "attach", runId, threadId: resolvedThreadId });
1282
+ }, [runStream]);
1283
+ const handleStop = (0, import_react2.useCallback)(() => {
1009
1284
  const runId = activeRunIdRef.current;
1010
1285
  const partial = streamingContentRef.current;
1011
1286
  const liveMessageId = streamingMessageIdRef.current;
1012
- abortControllerRef.current?.abort();
1287
+ abortDetachedStream(abortControllerRef.current);
1013
1288
  abortControllerRef.current = null;
1014
1289
  setActiveRunIdValue(null);
1015
1290
  streamingMessageIdRef.current = null;
@@ -1029,16 +1304,45 @@ function useAgents24ChatController({
1029
1304
  });
1030
1305
  }
1031
1306
  }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
1032
- (0, import_react.useEffect)(() => {
1033
- refresh().catch(() => {
1034
- storage.setThreads([]);
1035
- setThreads([]);
1036
- });
1307
+ (0, import_react2.useEffect)(() => {
1308
+ refresh().catch(() => setThreads(storage.listThreads()));
1037
1309
  }, [refresh, storage]);
1038
- (0, import_react.useEffect)(() => {
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();
1336
+ return () => {
1337
+ cancelled = true;
1338
+ if (retryTimeout) clearTimeout(retryTimeout);
1339
+ controller?.abort();
1340
+ };
1341
+ }, [refresh, storage, transport]);
1342
+ (0, import_react2.useEffect)(() => {
1039
1343
  syncThreadsFromStorage();
1040
1344
  }, [storageKey, syncThreadsFromStorage]);
1041
- (0, import_react.useEffect)(() => {
1345
+ (0, import_react2.useEffect)(() => {
1042
1346
  const previous = activeThreadIdRef.current;
1043
1347
  activeThreadIdRef.current = activeThreadId;
1044
1348
  if (activeThreadId && previous === activeThreadId && (activeRunIdRef.current || streamingMessageIdRef.current)) {
@@ -1048,6 +1352,9 @@ function useAgents24ChatController({
1048
1352
  detachActiveStream();
1049
1353
  }
1050
1354
  if (!activeThreadId) {
1355
+ if (previous === null && loadedThreadIdRef.current === null && messagesRef.current.length === 0 && !hasOlderTurnsRef.current && !isLoadingHistoryRef.current && !isLoadingOlderRef.current && nextBeforeTurnIndexRef.current === null) {
1356
+ return;
1357
+ }
1051
1358
  requestSeqRef.current += 1;
1052
1359
  loadedThreadIdRef.current = null;
1053
1360
  setMessages([]);
@@ -1079,14 +1386,14 @@ function useAgents24ChatController({
1079
1386
  }
1080
1387
  void loadThread(activeThreadId).catch(() => setLoadingHistory(false));
1081
1388
  }, [activeThreadId, detachActiveStream, loadThread, setLoadingHistory, storage]);
1082
- (0, import_react.useEffect)(() => {
1389
+ (0, import_react2.useEffect)(() => {
1083
1390
  const threadId = activeThreadId;
1084
1391
  if (!threadId || isLoadingHistory || loadedThreadIdRef.current !== threadId || activeRunIdRef.current) return;
1085
- const runId = activeRunIdFromThread(storage.getThread(threadId));
1392
+ const runId = autoAttachRunIdFromThread(storage.getThread(threadId));
1086
1393
  if (!runId || reattachedRunIdRef.current === runId) return;
1087
1394
  void runStream({ mode: "attach", threadId, runId });
1088
1395
  }, [activeThreadId, isLoadingHistory, runStream, storage, storageKey]);
1089
- (0, import_react.useEffect)(() => {
1396
+ (0, import_react2.useEffect)(() => {
1090
1397
  const threadId = activeThreadId;
1091
1398
  if (!threadId || isLoadingHistoryRef.current || activeRunIdRef.current || streamingMessageIdRef.current) {
1092
1399
  return;
@@ -1095,87 +1402,58 @@ function useAgents24ChatController({
1095
1402
  if (!hasStaleUnfinishedAssistantCache(cached)) return;
1096
1403
  void loadThread(threadId).catch(() => setLoadingHistory(false));
1097
1404
  }, [activeThreadId, loadThread, setLoadingHistory, storage, storageKey]);
1098
- const handleCopy = (0, import_react.useCallback)((content, messageId) => {
1099
- navigator.clipboard?.writeText(content);
1100
- setCopiedMessageId(messageId);
1101
- setTimeout(() => setCopiedMessageId(null), 200);
1102
- }, []);
1103
- const handleLike = (0, import_react.useCallback)(async (msg) => {
1104
- const nextLiked = !liked[msg.id];
1105
- setLiked((prev) => ({ ...prev, [msg.id]: nextLiked }));
1106
- if (nextLiked) setDisliked((prev) => ({ ...prev, [msg.id]: false }));
1107
- }, [liked]);
1108
- const handleDislike = (0, import_react.useCallback)(async (msg) => {
1109
- const nextDisliked = !disliked[msg.id];
1110
- setDisliked((prev) => ({ ...prev, [msg.id]: nextDisliked }));
1111
- if (nextDisliked) setLiked((prev) => ({ ...prev, [msg.id]: false }));
1112
- }, [disliked]);
1113
- const handleRetry = (0, import_react.useCallback)(async (msg) => {
1114
- const index = messagesRef.current.findIndex((message) => message.id === msg.id);
1115
- if (index <= 0) return;
1116
- const userMessage = messagesRef.current[index - 1];
1117
- if (userMessage.role !== "user") return;
1118
- const trimmed = messagesRef.current.slice(0, index);
1119
- setMessages(trimmed);
1120
- messagesRef.current = trimmed;
1121
- if (activeThreadIdRef.current) persistThread(activeThreadIdRef.current, trimmed);
1122
- await handleSubmit({ text: userMessage.content, files: userMessage.attachments || [] });
1123
- }, [handleSubmit, persistThread]);
1124
- const upsertLiveVoiceMessage = (0, import_react.useCallback)((input) => {
1125
- const content = input.content?.trim() ?? "";
1126
- if (!content && !input.citations?.length && !input.reasoningSteps?.length) return;
1127
- setMessages((prev) => {
1128
- const currentId = liveVoiceIdsRef.current[input.role];
1129
- const index = currentId ? prev.findIndex((message) => message.id === currentId) : -1;
1130
- const nextMessage = {
1131
- id: currentId || createId(),
1132
- role: input.role,
1133
- content,
1134
- createdAt: /* @__PURE__ */ new Date(),
1135
- isFinal: Boolean(input.isFinal),
1136
- isVoice: input.role === "user",
1137
- parts: content ? [{ id: createId(), type: "text", kind: "text", text: content }] : [],
1138
- citations: input.citations,
1139
- reasoningSteps: input.reasoningSteps
1140
- };
1141
- const next = index === -1 ? [...prev, nextMessage] : prev.map((message, itemIndex) => itemIndex === index ? { ...message, ...nextMessage } : message);
1142
- liveVoiceIdsRef.current[input.role] = input.isFinal ? void 0 : nextMessage.id;
1143
- messagesRef.current = next;
1144
- if (activeThreadIdRef.current) persistThread(activeThreadIdRef.current, next);
1145
- return next;
1146
- });
1147
- }, [createId, persistThread]);
1148
- const startNewThread = (0, import_react.useCallback)(() => {
1149
- detachActiveStream();
1150
- requestSeqRef.current += 1;
1151
- activeThreadIdRef.current = null;
1152
- loadedThreadIdRef.current = null;
1153
- nextBeforeTurnIndexRef.current = null;
1154
- hasOlderTurnsRef.current = false;
1155
- storage.setActiveThreadId?.(null);
1156
- onActiveThreadIdChange?.(null);
1157
- setMessages([]);
1158
- messagesRef.current = [];
1159
- setHasOlderTurns(false);
1160
- setIsLoadingOlder(false);
1161
- setContextStatus(null);
1162
- setLoadingHistory(false);
1163
- }, [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage]);
1164
- const loadThreadById = (0, import_react.useCallback)(
1405
+ const {
1406
+ handleCopy,
1407
+ handleDislike,
1408
+ handleLike,
1409
+ handleRetry,
1410
+ startNewThread,
1411
+ upsertLiveVoiceMessage
1412
+ } = useControllerMessageActions({
1413
+ activeThreadIdRef,
1414
+ createId,
1415
+ detachActiveStream,
1416
+ disliked,
1417
+ handleSubmit,
1418
+ hasOlderTurnsRef,
1419
+ liked,
1420
+ liveVoiceIdsRef,
1421
+ loadedThreadIdRef,
1422
+ messagesRef,
1423
+ nextBeforeTurnIndexRef,
1424
+ onActiveThreadIdChange,
1425
+ persistThread,
1426
+ requestSeqRef,
1427
+ setContextStatus,
1428
+ setCopiedMessageId,
1429
+ setDisliked,
1430
+ setHasOlderTurns,
1431
+ setIsLoadingOlder,
1432
+ setLiked,
1433
+ setLoadingHistory,
1434
+ setMessages,
1435
+ storage
1436
+ });
1437
+ const loadThreadById = (0, import_react2.useCallback)(
1165
1438
  async (threadId) => {
1166
1439
  if (!threadId) return;
1440
+ setIsSelectingThread(true);
1167
1441
  if (activeThreadIdRef.current !== threadId) {
1168
1442
  if (abortControllerRef.current) detachActiveStream();
1169
1443
  activeThreadIdRef.current = threadId;
1170
1444
  storage.setActiveThreadId?.(threadId);
1171
1445
  onActiveThreadIdChange?.(threadId);
1172
1446
  }
1173
- await loadThread(threadId);
1447
+ try {
1448
+ await loadThread(threadId);
1449
+ } finally {
1450
+ if (activeThreadIdRef.current === threadId) setIsSelectingThread(false);
1451
+ }
1174
1452
  },
1175
1453
  [detachActiveStream, loadThread, onActiveThreadIdChange, storage]
1176
1454
  );
1177
1455
  const activeThread = activeThreadId ? storage.getThread(activeThreadId) || threads.find((thread) => thread.id === activeThreadId) || null : null;
1178
- return (0, import_react.useMemo)(() => ({
1456
+ return (0, import_react2.useMemo)(() => ({
1179
1457
  threads,
1180
1458
  activeThreadId,
1181
1459
  activeThread,
@@ -1188,6 +1466,7 @@ function useAgents24ChatController({
1188
1466
  isLoadingHistory,
1189
1467
  isLoadingOlder,
1190
1468
  isRefreshingThreads,
1469
+ isSelectingThread,
1191
1470
  hasOlderTurns,
1192
1471
  liked,
1193
1472
  disliked,
@@ -1195,6 +1474,7 @@ function useAgents24ChatController({
1195
1474
  lastThinkingDurationMs,
1196
1475
  activeRunId,
1197
1476
  handleSubmit,
1477
+ attachRun,
1198
1478
  handleStop,
1199
1479
  handleCopy,
1200
1480
  handleLike,
@@ -1212,6 +1492,7 @@ function useAgents24ChatController({
1212
1492
  activeRunId,
1213
1493
  activeThread,
1214
1494
  activeThreadId,
1495
+ attachRun,
1215
1496
  contextStatus,
1216
1497
  currentReasoning,
1217
1498
  disliked,
@@ -1226,6 +1507,7 @@ function useAgents24ChatController({
1226
1507
  isLoadingHistory,
1227
1508
  isLoadingOlder,
1228
1509
  isRefreshingThreads,
1510
+ isSelectingThread,
1229
1511
  lastThinkingDurationMs,
1230
1512
  liked,
1231
1513
  loadThreadById,
@@ -1613,13 +1895,16 @@ var DefaultChatPart = ({
1613
1895
  if (part.kind === "ui-blocks") {
1614
1896
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-ui-blocks-part": true, "data-state": part.state });
1615
1897
  }
1616
- if (part.kind === "approval") {
1617
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-approval-part": true });
1898
+ if (part.kind === "hitl") {
1899
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-hitl-part": part.interruptId || "" });
1618
1900
  }
1619
1901
  if (part.kind === "error") {
1620
1902
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-error-part": true, children: part.errorText });
1621
1903
  }
1622
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-data-part": part.name });
1904
+ if (part.kind === "data") {
1905
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-data-part": part.name });
1906
+ }
1907
+ return null;
1623
1908
  };
1624
1909
  var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1625
1910
  DefaultChatPart,
@@ -1632,12 +1917,17 @@ var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ (0, impor
1632
1917
  );
1633
1918
 
1634
1919
  // src/sse.ts
1920
+ var isAbortError2 = (error) => {
1921
+ if (!error || typeof error !== "object") return false;
1922
+ const maybe = error;
1923
+ return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
1924
+ };
1635
1925
  var parseSseBlock = (block) => {
1636
1926
  const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.replace(/^data:\s?/, "")).join("\n").trim();
1637
1927
  if (!data) return null;
1638
1928
  return JSON.parse(data);
1639
1929
  };
1640
- var consumeSseResponse = async (response, onEvent) => {
1930
+ var consumeSseResponse = async (response, onEvent, options = {}) => {
1641
1931
  if (!response.ok) {
1642
1932
  let message = response.statusText || "Failed to open chat stream.";
1643
1933
  try {
@@ -1651,30 +1941,47 @@ var consumeSseResponse = async (response, onEvent) => {
1651
1941
  if (!reader) throw new Error("The chat stream did not return a readable body.");
1652
1942
  const decoder = new TextDecoder();
1653
1943
  let buffer = "";
1654
- let threadId = null;
1655
- let runId = null;
1656
- while (true) {
1657
- const { value, done } = await reader.read();
1658
- if (done) break;
1659
- buffer += decoder.decode(value, { stream: true });
1660
- let boundary = buffer.indexOf("\n\n");
1661
- while (boundary !== -1) {
1662
- const block = buffer.slice(0, boundary);
1663
- buffer = buffer.slice(boundary + 2);
1664
- boundary = buffer.indexOf("\n\n");
1665
- const event = parseSseBlock(block);
1666
- if (!event) continue;
1667
- if (event.run_id) runId = event.run_id;
1668
- const payloadThreadId = event.payload?.thread_id;
1669
- if (payloadThreadId) threadId = String(payloadThreadId);
1670
- await onEvent(event);
1944
+ let threadId = response.headers.get("X-Thread-ID") || null;
1945
+ let runId = response.headers.get("X-Run-ID") || null;
1946
+ let closed = false;
1947
+ const closeReader = () => {
1948
+ closed = true;
1949
+ void reader.cancel().catch(() => {
1950
+ });
1951
+ };
1952
+ if (options.signal?.aborted) {
1953
+ closeReader();
1954
+ return { threadId, runId };
1955
+ }
1956
+ options.signal?.addEventListener("abort", closeReader, { once: true });
1957
+ try {
1958
+ while (!closed) {
1959
+ const { value, done } = await reader.read();
1960
+ if (done) break;
1961
+ buffer += decoder.decode(value, { stream: true });
1962
+ let boundary = buffer.indexOf("\n\n");
1963
+ while (boundary !== -1) {
1964
+ const block = buffer.slice(0, boundary);
1965
+ buffer = buffer.slice(boundary + 2);
1966
+ boundary = buffer.indexOf("\n\n");
1967
+ const event = parseSseBlock(block);
1968
+ if (!event) continue;
1969
+ if (event.run_id) runId = event.run_id;
1970
+ const payloadThreadId = event.payload?.thread_id;
1971
+ if (payloadThreadId) threadId = String(payloadThreadId);
1972
+ await onEvent(event);
1973
+ }
1671
1974
  }
1975
+ } catch (error) {
1976
+ if (!isAbortError2(error)) throw error;
1977
+ } finally {
1978
+ options.signal?.removeEventListener("abort", closeReader);
1672
1979
  }
1673
1980
  return { threadId, runId };
1674
1981
  };
1675
1982
 
1676
1983
  // src/streaming-text.ts
1677
- var import_react2 = require("react");
1984
+ var import_react3 = require("react");
1678
1985
  var DEFAULT_COMPLETED_TEXT_CACHE_SIZE = 200;
1679
1986
  var defaultCompletedTextCache = /* @__PURE__ */ new Map();
1680
1987
  var defaultStreamingTextCache = {
@@ -1702,24 +2009,24 @@ function useStreamingText({
1702
2009
  maxCatchupChars = 20
1703
2010
  }) {
1704
2011
  const cacheAdapter = cache === false ? null : cache;
1705
- const [displayedText, setDisplayedText] = (0, import_react2.useState)(() => {
2012
+ const [displayedText, setDisplayedText] = (0, import_react3.useState)(() => {
1706
2013
  const cachedText = cacheAdapter?.get(id);
1707
2014
  return isStreaming && cachedText !== text ? "" : text;
1708
2015
  });
1709
- const targetRef = (0, import_react2.useRef)(text);
1710
- const displayedRef = (0, import_react2.useRef)(displayedText);
1711
- const rafRef = (0, import_react2.useRef)(null);
1712
- const lastFrameAtRef = (0, import_react2.useRef)(null);
1713
- const idRef = (0, import_react2.useRef)(id);
1714
- const shouldAnimateRef = (0, import_react2.useRef)(isStreaming);
1715
- (0, import_react2.useEffect)(() => {
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)(() => {
1716
2023
  if (isStreaming || !text) return;
1717
2024
  cacheAdapter?.set(id, text);
1718
2025
  }, [cacheAdapter, id, isStreaming, text]);
1719
- (0, import_react2.useEffect)(() => {
2026
+ (0, import_react3.useEffect)(() => {
1720
2027
  targetRef.current = text;
1721
2028
  }, [text]);
1722
- (0, import_react2.useEffect)(() => {
2029
+ (0, import_react3.useEffect)(() => {
1723
2030
  if (idRef.current === id) return;
1724
2031
  idRef.current = id;
1725
2032
  const cachedText = cacheAdapter?.get(id);
@@ -1733,7 +2040,7 @@ function useStreamingText({
1733
2040
  }
1734
2041
  lastFrameAtRef.current = null;
1735
2042
  }, [cacheAdapter, id, isStreaming, text]);
1736
- (0, import_react2.useEffect)(() => {
2043
+ (0, import_react3.useEffect)(() => {
1737
2044
  if (typeof window === "undefined") {
1738
2045
  displayedRef.current = text;
1739
2046
  setDisplayedText(text);
@@ -1818,6 +2125,14 @@ var streamHeaders = (headers) => ({
1818
2125
  Accept: "text/event-stream",
1819
2126
  "Content-Type": "application/json"
1820
2127
  });
2128
+ var ChatTransportError = class extends Error {
2129
+ constructor(message, status) {
2130
+ super(message);
2131
+ this.name = "ChatTransportError";
2132
+ this.status = status;
2133
+ }
2134
+ };
2135
+ var transportError = (response, fallback) => new ChatTransportError(response.statusText || fallback, response.status);
1821
2136
  var createFetchChatTransport = ({
1822
2137
  routes,
1823
2138
  fetchImpl = fetch,
@@ -1831,7 +2146,7 @@ var createFetchChatTransport = ({
1831
2146
  signal: input?.signal,
1832
2147
  headers: jsonHeaders(await loadHeaders())
1833
2148
  });
1834
- if (!response.ok) throw new Error(response.statusText || "Failed to list chat threads.");
2149
+ if (!response.ok) throw transportError(response, "Failed to list chat threads.");
1835
2150
  return response.json();
1836
2151
  },
1837
2152
  async getThread(input) {
@@ -1839,7 +2154,7 @@ var createFetchChatTransport = ({
1839
2154
  signal: input.signal,
1840
2155
  headers: jsonHeaders(await loadHeaders())
1841
2156
  });
1842
- if (!response.ok) throw new Error(response.statusText || "Failed to load chat thread.");
2157
+ if (!response.ok) throw transportError(response, "Failed to load chat thread.");
1843
2158
  return response.json();
1844
2159
  },
1845
2160
  async streamMessage(input, onEvent) {
@@ -1872,7 +2187,7 @@ var createFetchChatTransport = ({
1872
2187
  headers: jsonHeaders(await loadHeaders()),
1873
2188
  body: JSON.stringify({ assistant_output_text: input.assistantOutputText || void 0 })
1874
2189
  });
1875
- if (!response.ok) throw new Error(response.statusText || "Failed to cancel chat run.");
2190
+ if (!response.ok) throw transportError(response, "Failed to cancel chat run.");
1876
2191
  return response.json();
1877
2192
  },
1878
2193
  async deleteThread(input) {
@@ -1881,7 +2196,46 @@ var createFetchChatTransport = ({
1881
2196
  method: "DELETE",
1882
2197
  headers: jsonHeaders(await loadHeaders())
1883
2198
  });
1884
- if (!response.ok) throw new Error(response.statusText || "Failed to delete chat thread.");
2199
+ if (!response.ok) throw transportError(response, "Failed to delete chat thread.");
2200
+ return response.json();
2201
+ },
2202
+ async subscribeThreadEvents(input, onEvent) {
2203
+ if (!routes.threadEvents) return;
2204
+ const response = await fetchImpl(routes.threadEvents({ cursor: input.cursor }), {
2205
+ method: "GET",
2206
+ signal: input.signal,
2207
+ headers: streamHeaders(await loadHeaders())
2208
+ });
2209
+ await consumeSseResponse(response, (event) => onEvent(event));
2210
+ },
2211
+ async resumeHitl(input) {
2212
+ if (!routes.resumeRun) throw new ChatTransportError("HITL resume is not configured.", 501);
2213
+ const response = await fetchImpl(routes.resumeRun({ runId: input.runId }), {
2214
+ method: "POST",
2215
+ headers: jsonHeaders(await loadHeaders()),
2216
+ body: JSON.stringify({
2217
+ schema_version: "agents24.hitl.resume.v1",
2218
+ interrupt_id: input.interruptId,
2219
+ decisions: input.decisions,
2220
+ client: input.client
2221
+ })
2222
+ });
2223
+ if (!response.ok) throw transportError(response, "Failed to resume chat run.");
2224
+ return response.json();
2225
+ },
2226
+ async startMcpAuth(input) {
2227
+ if (!routes.startMcpAuth) throw new ChatTransportError("MCP auth is not configured.", 501);
2228
+ const response = await fetchImpl(routes.startMcpAuth({ runId: input.runId, serverId: input.serverId }), {
2229
+ method: "POST",
2230
+ headers: jsonHeaders(await loadHeaders()),
2231
+ body: JSON.stringify({
2232
+ interrupt_id: input.interruptId || void 0,
2233
+ principal_type: input.principalType || void 0,
2234
+ principal_id: input.principalId || void 0,
2235
+ client: input.client
2236
+ })
2237
+ });
2238
+ if (!response.ok) throw transportError(response, "Failed to start MCP authorization.");
1885
2239
  return response.json();
1886
2240
  }
1887
2241
  };
@@ -1891,6 +2245,7 @@ var createFetchChatTransport = ({
1891
2245
  var import_message_scroller2 = require("@shadcn/react/message-scroller");
1892
2246
  // Annotate the CommonJS export names for ESM import in node:
1893
2247
  0 && (module.exports = {
2248
+ ChatTransportError,
1894
2249
  DEFAULT_THREAD_PAGE_SIZE,
1895
2250
  DefaultChatPart,
1896
2251
  DefaultToolPart,
@@ -1905,21 +2260,26 @@ var import_message_scroller2 = require("@shadcn/react/message-scroller");
1905
2260
  MessageScroller,
1906
2261
  activeRunIdFromThread,
1907
2262
  activeRunIdFromThreadDetail,
2263
+ appendStableStreamingTurn,
1908
2264
  assistantTextFromParts,
1909
2265
  assistantTextFromResponseBlocks,
2266
+ autoAttachRunIdFromThread,
1910
2267
  compressionFromContextWindow,
1911
2268
  consumeSseResponse,
1912
2269
  createChatId,
1913
2270
  createFetchChatTransport,
2271
+ findStableAssistantMessageIndex,
1914
2272
  getActiveStreamingTextPartId,
1915
2273
  hasStaleUnfinishedAssistantCache,
1916
2274
  hasUnfinishedAssistantMessage,
1917
2275
  isActiveStreamingTextPart,
2276
+ isAutoAttachThreadStatus,
1918
2277
  isRunningThreadStatus,
1919
2278
  latestContextWindowFromThread,
1920
2279
  mergeContextWindow,
1921
2280
  mergeContextWindowUpdate,
1922
2281
  mergeReasoningSteps,
2282
+ mergeStoredThreadsForRefresh,
1923
2283
  normalizeContextCompression,
1924
2284
  normalizeContextWindow,
1925
2285
  parseSseBlock,
@@ -1932,6 +2292,7 @@ var import_message_scroller2 = require("@shadcn/react/message-scroller");
1932
2292
  threadPaging,
1933
2293
  titleFromMessage,
1934
2294
  toolStateFromStatus,
2295
+ upsertStableAssistantMessage,
1935
2296
  useAgents24ChatController,
1936
2297
  useMessageScroller,
1937
2298
  useMessageScrollerScrollable,