@agents24/chat-react 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  LatestThreadScrollerProvider,
8
8
  LatestThreadScrollerRoot,
9
9
  LatestThreadScrollerViewport
10
- } from "./chunk-E3TJ3I6G.js";
10
+ } from "./chunk-FFO6KEB4.js";
11
11
  import {
12
12
  getActiveStreamingTextPartId,
13
13
  isActiveStreamingTextPart,
@@ -15,7 +15,79 @@ import {
15
15
  } from "./chunk-EWZO4QJI.js";
16
16
 
17
17
  // src/controller.ts
18
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
18
+ import { useCallback as useCallback2, useEffect, useMemo, useRef, useState } from "react";
19
+
20
+ // src/controller-actions.ts
21
+ import { useCallback } from "react";
22
+ function useControllerMessageActions(input) {
23
+ const handleCopy = useCallback((content, messageId) => {
24
+ navigator.clipboard?.writeText(content);
25
+ input.setCopiedMessageId(messageId);
26
+ setTimeout(() => input.setCopiedMessageId(null), 200);
27
+ }, [input]);
28
+ const handleLike = useCallback(async (msg) => {
29
+ const nextLiked = !input.liked[msg.id];
30
+ input.setLiked((prev) => ({ ...prev, [msg.id]: nextLiked }));
31
+ if (nextLiked) input.setDisliked((prev) => ({ ...prev, [msg.id]: false }));
32
+ }, [input]);
33
+ const handleDislike = useCallback(async (msg) => {
34
+ const nextDisliked = !input.disliked[msg.id];
35
+ input.setDisliked((prev) => ({ ...prev, [msg.id]: nextDisliked }));
36
+ if (nextDisliked) input.setLiked((prev) => ({ ...prev, [msg.id]: false }));
37
+ }, [input]);
38
+ const handleRetry = useCallback(async (msg) => {
39
+ const index = input.messagesRef.current.findIndex((message) => message.id === msg.id);
40
+ if (index <= 0) return;
41
+ const userMessage = input.messagesRef.current[index - 1];
42
+ if (userMessage.role !== "user") return;
43
+ const trimmed = input.messagesRef.current.slice(0, index);
44
+ input.setMessages(trimmed);
45
+ input.messagesRef.current = trimmed;
46
+ if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, trimmed);
47
+ await input.handleSubmit({ text: userMessage.content, files: userMessage.attachments || [] });
48
+ }, [input]);
49
+ const upsertLiveVoiceMessage = useCallback((payload) => {
50
+ const content = payload.content?.trim() ?? "";
51
+ if (!content && !payload.citations?.length && !payload.reasoningSteps?.length) return;
52
+ input.setMessages((prev) => {
53
+ const currentId = input.liveVoiceIdsRef.current[payload.role];
54
+ const index = currentId ? prev.findIndex((message) => message.id === currentId) : -1;
55
+ const nextMessage = {
56
+ id: currentId || input.createId(),
57
+ role: payload.role,
58
+ content,
59
+ createdAt: /* @__PURE__ */ new Date(),
60
+ isFinal: Boolean(payload.isFinal),
61
+ isVoice: payload.role === "user",
62
+ parts: content ? [{ id: input.createId(), type: "text", kind: "text", text: content }] : [],
63
+ citations: payload.citations,
64
+ reasoningSteps: payload.reasoningSteps
65
+ };
66
+ const next = index === -1 ? [...prev, nextMessage] : prev.map((message, itemIndex) => itemIndex === index ? { ...message, ...nextMessage } : message);
67
+ input.liveVoiceIdsRef.current[payload.role] = payload.isFinal ? void 0 : nextMessage.id;
68
+ input.messagesRef.current = next;
69
+ if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, next);
70
+ return next;
71
+ });
72
+ }, [input]);
73
+ const startNewThread = useCallback(() => {
74
+ input.detachActiveStream();
75
+ input.requestSeqRef.current += 1;
76
+ input.activeThreadIdRef.current = null;
77
+ input.loadedThreadIdRef.current = null;
78
+ input.nextBeforeTurnIndexRef.current = null;
79
+ input.hasOlderTurnsRef.current = false;
80
+ input.storage.setActiveThreadId?.(null);
81
+ input.onActiveThreadIdChange?.(null);
82
+ input.setMessages([]);
83
+ input.messagesRef.current = [];
84
+ input.setHasOlderTurns(false);
85
+ input.setIsLoadingOlder(false);
86
+ input.setContextStatus(null);
87
+ input.setLoadingHistory(false);
88
+ }, [input]);
89
+ return { handleCopy, handleDislike, handleLike, handleRetry, startNewThread, upsertLiveVoiceMessage };
90
+ }
19
91
 
20
92
  // src/context-window.ts
21
93
  var SOURCE_PRIORITY = {
@@ -138,7 +210,8 @@ function compressionFromContextWindow(contextWindow) {
138
210
 
139
211
  // src/model.ts
140
212
  var DEFAULT_THREAD_PAGE_SIZE = 5;
141
- var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
213
+ var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling", "paused"])).has(String(status || "").toLowerCase());
214
+ var isAutoAttachThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
142
215
  var createChatId = () => {
143
216
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
144
217
  return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
@@ -150,12 +223,6 @@ var titleFromMessage = (text, files = []) => {
150
223
  var threadActivityDate = (thread) => String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString());
151
224
  var assistantTextFromResponseBlocks = (blocks) => (blocks || []).filter((block) => block.kind === "assistant_text" && typeof block.text === "string").map((block) => String(block.text)).join("\n\n").trim();
152
225
  var assistantTextFromParts = (parts) => (parts || []).filter((part) => part.kind === "text").map((part) => part.text).join("\n\n").trim();
153
- var textFromFinalOutput = (value) => {
154
- if (typeof value === "string") return value;
155
- if (!value || typeof value !== "object") return "";
156
- const record = value;
157
- return String(record.message || record.text || record.answer || "");
158
- };
159
226
  var displayTextWithoutInlineAttachments = (text, hasAttachments) => {
160
227
  if (!hasAttachments) return text;
161
228
  const marker = "Attached text file (";
@@ -202,7 +269,7 @@ var responseBlocksFromTurn = (turn) => {
202
269
  var assistantTextFromEvents = (events) => {
203
270
  const assistantText = latestEventPayloadValue(events, "assistant_output_text");
204
271
  if (typeof assistantText === "string" && assistantText.trim()) return assistantText;
205
- return textFromFinalOutput(latestEventPayloadValue(events, "final_output"));
272
+ return "";
206
273
  };
207
274
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
208
275
  var optionalString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -304,8 +371,17 @@ var partsFromResponseBlocks = (blocks, fallbackText) => {
304
371
  });
305
372
  return;
306
373
  }
307
- if (block.kind === "approval_request") {
308
- parts.push({ id, type: "approval", kind: "approval", raw: block });
374
+ if (block.kind === "hitl_request") {
375
+ const hitl = asRecord(block.hitl) || block;
376
+ parts.push({
377
+ id,
378
+ type: "hitl",
379
+ kind: "hitl",
380
+ hitl,
381
+ interruptId: optionalString(block.interruptId) || optionalString(hitl.interrupt_id) || null,
382
+ hitlKind: optionalString(block.hitlKind) || optionalString(hitl.kind) || null,
383
+ raw: block
384
+ });
309
385
  return;
310
386
  }
311
387
  if (block.kind === "error") {
@@ -377,7 +453,7 @@ var turnToMessages = (turn, activeRunId) => {
377
453
  attachments
378
454
  });
379
455
  }
380
- const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events) || textFromFinalOutput(turn.final_output);
456
+ const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events);
381
457
  if (assistantText || responseBlocks.length > 0 || isRunning) {
382
458
  const parts = partsFromResponseBlocks(responseBlocks, assistantText);
383
459
  messages.push({
@@ -431,6 +507,23 @@ var activeRunIdFromThread = (thread) => {
431
507
  const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
432
508
  return lastRunId && isRunningThreadStatus(lastRunStatus) ? String(lastRunId) : null;
433
509
  };
510
+ var autoAttachRunIdFromThread = (thread) => {
511
+ const hasCamelActiveRun = Boolean(
512
+ thread && Object.prototype.hasOwnProperty.call(thread, "activeRun")
513
+ );
514
+ const hasCamelLastRunStatus = Boolean(
515
+ thread && Object.prototype.hasOwnProperty.call(thread, "lastRunStatus")
516
+ );
517
+ const hasCamelLastRunId = Boolean(
518
+ thread && Object.prototype.hasOwnProperty.call(thread, "lastRunId")
519
+ );
520
+ const activeRun = hasCamelActiveRun ? thread?.activeRun || null : thread?.active_run || null;
521
+ const lastRunStatus = hasCamelLastRunStatus ? thread?.lastRunStatus || null : thread?.last_run_status || thread?.lastRunStatus || null;
522
+ const activeRunId = activeRun?.run_id ? String(activeRun.run_id) : "";
523
+ if (activeRunId && isAutoAttachThreadStatus(activeRun?.status || lastRunStatus)) return activeRunId;
524
+ const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
525
+ return lastRunId && isAutoAttachThreadStatus(lastRunStatus) ? String(lastRunId) : null;
526
+ };
434
527
  var hasUnfinishedAssistantMessage = (messages) => Boolean(messages?.some((message) => message.role === "assistant" && message.isFinal === false));
435
528
  var hasStaleUnfinishedAssistantCache = (thread) => hasUnfinishedAssistantMessage(thread?.messages) && !activeRunIdFromThread(thread);
436
529
  var activeRunIdFromThreadDetail = (thread) => {
@@ -440,7 +533,7 @@ var activeRunIdFromThreadDetail = (thread) => {
440
533
  return runningTurn?.run_id ? String(runningTurn.run_id) : null;
441
534
  };
442
535
 
443
- // src/controller.ts
536
+ // src/controller-helpers.ts
444
537
  var threadSummaryToStored = (thread) => ({
445
538
  ...thread,
446
539
  id: String(thread.id),
@@ -449,6 +542,121 @@ var threadSummaryToStored = (thread) => ({
449
542
  messages: [],
450
543
  isHydrated: false
451
544
  });
545
+ var isThreadNotFoundError = (error) => {
546
+ if (!error || typeof error !== "object" || !("status" in error)) return false;
547
+ return Number(error.status) === 404;
548
+ };
549
+ var isAbortError = (error) => {
550
+ if (!error || typeof error !== "object") return false;
551
+ const maybe = error;
552
+ return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
553
+ };
554
+ var abortDetachedStream = (controller) => {
555
+ if (!controller || controller.signal.aborted) return;
556
+ try {
557
+ const reason = typeof DOMException !== "undefined" ? new DOMException("Chat stream detached.", "AbortError") : new Error("Chat stream detached.");
558
+ controller.abort(reason);
559
+ } catch {
560
+ }
561
+ };
562
+ var hasRunState = (thread) => Boolean(
563
+ thread.active_run || thread.activeRun || thread.last_run_id || thread.lastRunId || thread.last_run_status || thread.lastRunStatus || thread.isRunning
564
+ );
565
+ var mergeStoredThreadsForRefresh = (currentThreads, serverThreads) => {
566
+ const currentById = new Map(currentThreads.map((thread) => [thread.id, thread]));
567
+ const serverIds = new Set(serverThreads.map((thread) => thread.id));
568
+ const mergedServerThreads = serverThreads.map((serverThread) => {
569
+ const current = currentById.get(serverThread.id);
570
+ if (!current) return serverThread;
571
+ const incomingHasRunState = hasRunState(serverThread);
572
+ return {
573
+ ...current,
574
+ ...serverThread,
575
+ messages: current.messages || [],
576
+ isHydrated: current.isHydrated,
577
+ hasOlderTurns: current.hasOlderTurns,
578
+ nextBeforeTurnIndex: current.nextBeforeTurnIndex,
579
+ active_run: incomingHasRunState ? serverThread.active_run : current.active_run,
580
+ activeRun: incomingHasRunState ? serverThread.activeRun : current.activeRun,
581
+ last_run_id: incomingHasRunState ? serverThread.last_run_id : current.last_run_id,
582
+ lastRunId: incomingHasRunState ? serverThread.lastRunId : current.lastRunId,
583
+ last_run_status: incomingHasRunState ? serverThread.last_run_status : current.last_run_status,
584
+ lastRunStatus: incomingHasRunState ? serverThread.lastRunStatus : current.lastRunStatus,
585
+ lastEventSeq: incomingHasRunState ? serverThread.lastEventSeq : current.lastEventSeq,
586
+ isRunning: incomingHasRunState ? serverThread.isRunning : current.isRunning
587
+ };
588
+ });
589
+ const localOnlyThreads = currentThreads.filter((thread) => {
590
+ if (serverIds.has(thread.id)) return false;
591
+ return Boolean(thread.isHydrated || thread.messages?.length || thread.isRunning || thread.activeRun || thread.active_run);
592
+ });
593
+ return [...mergedServerThreads, ...localOnlyThreads];
594
+ };
595
+ var stableStringify = (value) => JSON.stringify(value ?? null);
596
+ var sameStoredThread = (left, right) => {
597
+ if (left === right) return true;
598
+ if (!left || !right) return false;
599
+ return stableStringify(left) === stableStringify(right);
600
+ };
601
+ var applyThreadSummaryEvent = (currentThreads, event) => {
602
+ if (event.event === "snapshot_required") return currentThreads;
603
+ if (event.event === "thread.deleted") {
604
+ const next2 = currentThreads.filter((thread) => thread.id !== event.thread_id);
605
+ return next2.length === currentThreads.length ? currentThreads : next2;
606
+ }
607
+ const incoming = threadSummaryToStored(event.thread);
608
+ const index = currentThreads.findIndex((thread) => thread.id === incoming.id);
609
+ const sortByActivity = (threads) => [...threads].sort((a, b) => {
610
+ const left = Date.parse(String(a.updated_at || a.last_activity_at || a.created_at || ""));
611
+ const right = Date.parse(String(b.updated_at || b.last_activity_at || b.created_at || ""));
612
+ return (Number.isFinite(right) ? right : 0) - (Number.isFinite(left) ? left : 0);
613
+ });
614
+ if (index === -1) return sortByActivity([...currentThreads, incoming]);
615
+ const [merged] = mergeStoredThreadsForRefresh([currentThreads[index]], [incoming]);
616
+ if (sameStoredThread(currentThreads[index], merged)) return currentThreads;
617
+ const next = [...currentThreads];
618
+ next[index] = merged;
619
+ return sortByActivity(next);
620
+ };
621
+
622
+ // src/message-lifecycle.ts
623
+ function findStableAssistantMessageIndex(messages, input) {
624
+ return messages.findIndex(
625
+ (message) => message.role === "assistant" && (input.messageId && message.id === input.messageId || Boolean(input.runId && message.runId === input.runId))
626
+ );
627
+ }
628
+ function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
629
+ const next = [...messages];
630
+ if (!next.some((message) => message.id === userMessage.id)) {
631
+ next.push(userMessage);
632
+ }
633
+ if (!next.some(
634
+ (message) => message.id === assistantMessage.id || Boolean(
635
+ assistantMessage.runId && message.role === "assistant" && message.runId === assistantMessage.runId
636
+ )
637
+ )) {
638
+ next.push(assistantMessage);
639
+ }
640
+ return next;
641
+ }
642
+ function upsertStableAssistantMessage(messages, input) {
643
+ const next = [...messages];
644
+ for (const baseMessage of input.baseMessages || []) {
645
+ const matchesTarget = baseMessage.role === "assistant" && (input.messageId && baseMessage.id === input.messageId || Boolean(input.runId && baseMessage.runId === input.runId));
646
+ if (!matchesTarget && !next.some((message) => message.id === baseMessage.id)) {
647
+ next.push(baseMessage);
648
+ }
649
+ }
650
+ const index = findStableAssistantMessageIndex(next, input);
651
+ if (index === -1) {
652
+ next.push(input.create());
653
+ return next;
654
+ }
655
+ next[index] = input.update(next[index]);
656
+ return next;
657
+ }
658
+
659
+ // src/controller.ts
452
660
  function useAgents24ChatController({
453
661
  transport,
454
662
  storage,
@@ -479,6 +687,7 @@ function useAgents24ChatController({
479
687
  const [lastThinkingDurationMs, setLastThinkingDurationMs] = useState(null);
480
688
  const [threads, setThreads] = useState(() => storage.listThreads());
481
689
  const [isRefreshingThreads, setIsRefreshingThreads] = useState(false);
690
+ const [isSelectingThread, setIsSelectingThread] = useState(false);
482
691
  const [activeRunId, setActiveRunId] = useState(null);
483
692
  const textareaRef = useRef(null);
484
693
  const activeThreadIdRef = useRef(activeThreadId);
@@ -492,18 +701,21 @@ function useAgents24ChatController({
492
701
  const activeRunIdRef = useRef(null);
493
702
  const reattachedRunIdRef = useRef(null);
494
703
  const abortControllerRef = useRef(null);
704
+ const [lifecycleAbortController] = useState(() => new AbortController());
495
705
  const streamingContentRef = useRef("");
496
706
  const streamingMessageIdRef = useRef(null);
497
707
  const reasoningRef = useRef([]);
498
708
  const liveVoiceIdsRef = useRef({});
499
- const setActiveRunIdValue = useCallback((runId) => {
709
+ const refreshSeqRef = useRef(0);
710
+ const threadEventsCursorRef = useRef(null);
711
+ const setActiveRunIdValue = useCallback2((runId) => {
500
712
  activeRunIdRef.current = runId;
501
713
  setActiveRunId(runId);
502
714
  }, []);
503
- const syncThreadsFromStorage = useCallback(() => {
715
+ const syncThreadsFromStorage = useCallback2(() => {
504
716
  setThreads(storage.listThreads());
505
717
  }, [storage]);
506
- const upsertStoredThread = useCallback(
718
+ const upsertStoredThread = useCallback2(
507
719
  (thread) => {
508
720
  storage.upsertThread(thread);
509
721
  syncThreadsFromStorage();
@@ -513,7 +725,7 @@ function useAgents24ChatController({
513
725
  useEffect(() => {
514
726
  messagesRef.current = messages;
515
727
  }, [messages]);
516
- const persistThread = useCallback(
728
+ const persistThread = useCallback2(
517
729
  (threadId, nextMessages, paging, options) => {
518
730
  const existing = storage.getThread(threadId);
519
731
  const firstUser = nextMessages.find((message) => message.role === "user");
@@ -530,7 +742,7 @@ function useAgents24ChatController({
530
742
  },
531
743
  [storage, upsertStoredThread]
532
744
  );
533
- const markThreadRunStatus = useCallback(
745
+ const markThreadRunStatus = useCallback2(
534
746
  (threadId, runId, status, lastEventSeq) => {
535
747
  const existing = storage.getThread(threadId);
536
748
  if (!existing || !runId) return;
@@ -560,18 +772,21 @@ function useAgents24ChatController({
560
772
  },
561
773
  [storage, upsertStoredThread]
562
774
  );
563
- const refresh = useCallback(async () => {
775
+ const refresh = useCallback2(async () => {
776
+ const seq = ++refreshSeqRef.current;
564
777
  setIsRefreshingThreads(true);
565
778
  try {
566
- const data = await transport.listThreads();
779
+ const data = await transport.listThreads({ signal: lifecycleAbortController.signal });
780
+ if (lifecycleAbortController.signal.aborted) return;
781
+ if (seq !== refreshSeqRef.current) return;
567
782
  const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
568
- storage.setThreads(nextThreads);
783
+ storage.setThreads(mergeStoredThreadsForRefresh(storage.listThreads(), nextThreads));
569
784
  setThreads(storage.listThreads());
570
785
  } finally {
571
- setIsRefreshingThreads(false);
786
+ if (!lifecycleAbortController.signal.aborted && seq === refreshSeqRef.current) setIsRefreshingThreads(false);
572
787
  }
573
- }, [storage, transport]);
574
- const applyThreadId = useCallback(
788
+ }, [lifecycleAbortController, storage, transport]);
789
+ const applyThreadId = useCallback2(
575
790
  (threadId, baseMessages) => {
576
791
  if (!threadId || activeThreadIdRef.current === threadId) return;
577
792
  activeThreadIdRef.current = threadId;
@@ -585,18 +800,18 @@ function useAgents24ChatController({
585
800
  streamingContentRef.current = value;
586
801
  setStreamingContent(value);
587
802
  };
588
- const setLoadingHistory = useCallback((value) => {
803
+ const setLoadingHistory = useCallback2((value) => {
589
804
  isLoadingHistoryRef.current = value;
590
805
  setIsLoadingHistory(value);
591
806
  }, []);
592
- const setReasoningSteps = useCallback((value) => {
807
+ const setReasoningSteps = useCallback2((value) => {
593
808
  reasoningRef.current = value || [];
594
809
  setCurrentReasoning(value || []);
595
810
  }, []);
596
- const detachActiveStream = useCallback(() => {
811
+ const detachActiveStream = useCallback2(() => {
597
812
  const controller = abortControllerRef.current;
598
813
  abortControllerRef.current = null;
599
- controller?.abort();
814
+ abortDetachedStream(controller);
600
815
  setActiveRunIdValue(null);
601
816
  reattachedRunIdRef.current = null;
602
817
  streamingMessageIdRef.current = null;
@@ -607,21 +822,36 @@ function useAgents24ChatController({
607
822
  setStreamingContent("");
608
823
  setCurrentReasoning([]);
609
824
  }, [setActiveRunIdValue]);
610
- const setLiveAssistantMessage = useCallback(
825
+ const clearMissingThread = useCallback2(
826
+ (threadId) => {
827
+ storage.deleteThread?.(threadId);
828
+ syncThreadsFromStorage();
829
+ if (activeThreadIdRef.current !== threadId) return;
830
+ requestSeqRef.current += 1;
831
+ detachActiveStream();
832
+ activeThreadIdRef.current = null;
833
+ loadedThreadIdRef.current = null;
834
+ nextBeforeTurnIndexRef.current = null;
835
+ hasOlderTurnsRef.current = false;
836
+ storage.setActiveThreadId?.(null);
837
+ onActiveThreadIdChange?.(null);
838
+ setMessages([]);
839
+ messagesRef.current = [];
840
+ setHasOlderTurns(false);
841
+ setIsLoadingOlder(false);
842
+ setContextStatus(null);
843
+ setLoadingHistory(false);
844
+ },
845
+ [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage, syncThreadsFromStorage]
846
+ );
847
+ const setLiveAssistantMessage = useCallback2(
611
848
  (input) => {
612
849
  setMessages((prev) => {
613
- const index = prev.findIndex(
614
- (message) => message.id === input.messageId || input.runId && message.role === "assistant" && message.runId === input.runId
615
- );
616
- if (index === -1) {
617
- const next2 = [...prev];
618
- (input.baseMessages || []).forEach((message) => {
619
- const isSameAssistant = message.id === input.messageId || Boolean(input.runId && message.role === "assistant" && message.runId === input.runId);
620
- if (!isSameAssistant && !next2.some((item) => item.id === message.id)) {
621
- next2.push(message);
622
- }
623
- });
624
- next2.push({
850
+ const next = upsertStableAssistantMessage(prev, {
851
+ baseMessages: input.baseMessages,
852
+ messageId: input.messageId,
853
+ runId: input.runId,
854
+ create: () => ({
625
855
  id: input.messageId,
626
856
  role: "assistant",
627
857
  runId: input.runId ?? null,
@@ -630,26 +860,23 @@ function useAgents24ChatController({
630
860
  reasoningSteps: input.reasoning,
631
861
  isFinal: false,
632
862
  parts: input.parts || []
633
- });
634
- messagesRef.current = next2;
635
- return next2;
636
- }
637
- const next = [...prev];
638
- next[index] = {
639
- ...next[index],
640
- runId: input.runId ?? next[index].runId ?? null,
641
- content: input.content,
642
- reasoningSteps: input.reasoning,
643
- isFinal: false,
644
- parts: input.parts ?? next[index].parts
645
- };
863
+ }),
864
+ update: (message) => ({
865
+ ...message,
866
+ runId: input.runId ?? message.runId ?? null,
867
+ content: input.content,
868
+ reasoningSteps: input.reasoning,
869
+ isFinal: false,
870
+ parts: input.parts ?? message.parts
871
+ })
872
+ });
646
873
  messagesRef.current = next;
647
874
  return next;
648
875
  });
649
876
  },
650
877
  []
651
878
  );
652
- const finalizeAssistantMessage = useCallback(
879
+ const finalizeAssistantMessage = useCallback2(
653
880
  (input) => {
654
881
  const content = input.error || input.assistantText.trim();
655
882
  if (!content) return input.baseMessages;
@@ -669,7 +896,17 @@ function useAgents24ChatController({
669
896
  reasoningSteps: mergeReasoningSteps(input.reasoning, { finalize: true }),
670
897
  thinkingDurationMs: input.thinkingDurationMs
671
898
  };
672
- const completed = existingIndex >= 0 ? input.baseMessages.map((message, index) => index === existingIndex ? assistant : message) : [...input.baseMessages, assistant];
899
+ const completed = upsertStableAssistantMessage(input.baseMessages, {
900
+ messageId: input.messageId,
901
+ runId: input.runId,
902
+ create: () => assistant,
903
+ update: (message) => ({
904
+ ...message,
905
+ ...assistant,
906
+ id: message.id,
907
+ createdAt: message.createdAt
908
+ })
909
+ });
673
910
  setMessages(completed);
674
911
  messagesRef.current = completed;
675
912
  if (input.threadId) {
@@ -693,7 +930,7 @@ function useAgents24ChatController({
693
930
  },
694
931
  [createId, persistThread, storage, upsertStoredThread]
695
932
  );
696
- const loadThread = useCallback(
933
+ const loadThread = useCallback2(
697
934
  async (threadId) => {
698
935
  const seq = ++requestSeqRef.current;
699
936
  setLoadingHistory(true);
@@ -703,7 +940,8 @@ function useAgents24ChatController({
703
940
  const detail = await transport.getThread({
704
941
  threadId,
705
942
  limit: pageSize,
706
- includeRunEvents: false
943
+ includeRunEvents: false,
944
+ signal: lifecycleAbortController.signal
707
945
  });
708
946
  if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
709
947
  void onThreadDetailLoaded?.(detail);
@@ -724,13 +962,20 @@ function useAgents24ChatController({
724
962
  nextBeforeTurnIndex: paging.nextBeforeTurnIndex,
725
963
  updated_at: threadActivityDate(detail)
726
964
  });
965
+ } catch (error) {
966
+ if (isAbortError(error)) return;
967
+ if (isThreadNotFoundError(error)) {
968
+ clearMissingThread(threadId);
969
+ return;
970
+ }
971
+ throw error;
727
972
  } finally {
728
- if (activeThreadIdRef.current === threadId) setLoadingHistory(false);
973
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
729
974
  }
730
975
  },
731
- [onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
976
+ [clearMissingThread, lifecycleAbortController, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
732
977
  );
733
- const loadOlderTurns = useCallback(async () => {
978
+ const loadOlderTurns = useCallback2(async () => {
734
979
  const threadId = activeThreadIdRef.current;
735
980
  const beforeTurnIndex = nextBeforeTurnIndexRef.current;
736
981
  if (!threadId || beforeTurnIndex === null || isLoadingOlderRef.current) return;
@@ -741,7 +986,8 @@ function useAgents24ChatController({
741
986
  threadId,
742
987
  limit: pageSize,
743
988
  beforeTurnIndex,
744
- includeRunEvents: false
989
+ includeRunEvents: false,
990
+ signal: lifecycleAbortController.signal
745
991
  });
746
992
  if (activeThreadIdRef.current !== threadId) return;
747
993
  const older = threadDetailToMessages(detail);
@@ -753,12 +999,19 @@ function useAgents24ChatController({
753
999
  hasOlderTurnsRef.current = paging.hasOlderTurns;
754
1000
  nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
755
1001
  persistThread(threadId, next, paging);
1002
+ } catch (error) {
1003
+ if (isAbortError(error)) return;
1004
+ if (isThreadNotFoundError(error)) {
1005
+ clearMissingThread(threadId);
1006
+ return;
1007
+ }
1008
+ throw error;
756
1009
  } finally {
757
- if (activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
1010
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
758
1011
  isLoadingOlderRef.current = false;
759
1012
  }
760
- }, [pageSize, persistThread, transport]);
761
- const handleStreamEvent = useCallback(
1013
+ }, [clearMissingThread, lifecycleAbortController, pageSize, persistThread, transport]);
1014
+ const handleStreamEvent = useCallback2(
762
1015
  (input) => {
763
1016
  const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
764
1017
  const payload = event.payload || {};
@@ -804,7 +1057,7 @@ function useAgents24ChatController({
804
1057
  streamThreadIdRef.current = terminalThreadId;
805
1058
  applyThreadId(terminalThreadId, baseMessages);
806
1059
  }
807
- const finalText = String(payload.assistant_output_text || textFromFinalOutput(payload.final_output) || streamingContentRef.current || "");
1060
+ const finalText = String(payload.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks || []) || streamingContentRef.current || "");
808
1061
  finalizeAssistantMessage({
809
1062
  threadId: streamThreadIdRef.current || activeThreadIdRef.current,
810
1063
  baseMessages: messagesRef.current,
@@ -819,7 +1072,7 @@ function useAgents24ChatController({
819
1072
  },
820
1073
  [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
821
1074
  );
822
- const runStream = useCallback(
1075
+ const runStream = useCallback2(
823
1076
  async (input) => {
824
1077
  const startedAt = Date.now();
825
1078
  const controller = new AbortController();
@@ -878,19 +1131,28 @@ function useAgents24ChatController({
878
1131
  );
879
1132
  }
880
1133
  try {
1134
+ let streamResult = null;
881
1135
  if (input.mode === "attach") {
882
1136
  setActiveRunIdValue(input.runId);
883
1137
  reattachedRunIdRef.current = input.runId;
884
- await transport.attachRun(
1138
+ streamResult = await transport.attachRun(
885
1139
  { runId: input.runId, signal: controller.signal },
886
1140
  (event) => handleStreamEvent({ mode: "attach", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
887
1141
  );
888
1142
  } else {
889
- await transport.streamMessage(
1143
+ streamResult = await transport.streamMessage(
890
1144
  { ...input.message, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
891
1145
  (event) => handleStreamEvent({ mode: "submit", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
892
1146
  );
893
1147
  }
1148
+ if (streamResult?.runId) setActiveRunIdValue(streamResult.runId);
1149
+ if (streamResult?.threadId) {
1150
+ streamThreadIdRef.current = streamResult.threadId;
1151
+ applyThreadId(streamResult.threadId, baseMessages);
1152
+ if (streamResult.runId) {
1153
+ markThreadRunStatus(streamResult.threadId, streamResult.runId, "running");
1154
+ }
1155
+ }
894
1156
  if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
895
1157
  finalizeAssistantMessage({
896
1158
  threadId: streamThreadIdRef.current,
@@ -904,7 +1166,7 @@ function useAgents24ChatController({
904
1166
  }
905
1167
  await refresh().catch(() => void 0);
906
1168
  } catch (error) {
907
- if (error.name !== "AbortError") {
1169
+ if (!isAbortError(error)) {
908
1170
  finalizeAssistantMessage({
909
1171
  threadId: streamThreadIdRef.current,
910
1172
  baseMessages: messagesRef.current,
@@ -933,18 +1195,22 @@ function useAgents24ChatController({
933
1195
  },
934
1196
  [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
935
1197
  );
936
- const handleSubmit = useCallback(
1198
+ const handleSubmit = useCallback2(
937
1199
  async (message) => {
938
1200
  if (!message.text.trim() && !(message.files || []).length) return;
939
1201
  await runStream({ mode: "submit", message });
940
1202
  },
941
1203
  [runStream]
942
1204
  );
943
- const handleStop = useCallback(() => {
1205
+ const attachRun = useCallback2(async (runId, threadId) => {
1206
+ const resolvedThreadId = threadId ?? activeThreadIdRef.current;
1207
+ if (runId && resolvedThreadId) await runStream({ mode: "attach", runId, threadId: resolvedThreadId });
1208
+ }, [runStream]);
1209
+ const handleStop = useCallback2(() => {
944
1210
  const runId = activeRunIdRef.current;
945
1211
  const partial = streamingContentRef.current;
946
1212
  const liveMessageId = streamingMessageIdRef.current;
947
- abortControllerRef.current?.abort();
1213
+ abortDetachedStream(abortControllerRef.current);
948
1214
  abortControllerRef.current = null;
949
1215
  setActiveRunIdValue(null);
950
1216
  streamingMessageIdRef.current = null;
@@ -965,11 +1231,51 @@ function useAgents24ChatController({
965
1231
  }
966
1232
  }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
967
1233
  useEffect(() => {
968
- refresh().catch(() => {
969
- storage.setThreads([]);
970
- setThreads([]);
1234
+ refresh().catch((error) => {
1235
+ if (!isAbortError(error) && !lifecycleAbortController.signal.aborted) {
1236
+ setThreads(storage.listThreads());
1237
+ }
971
1238
  });
972
- }, [refresh, storage]);
1239
+ }, [lifecycleAbortController, refresh, storage]);
1240
+ useEffect(() => {
1241
+ return () => {
1242
+ lifecycleAbortController.abort();
1243
+ abortDetachedStream(abortControllerRef.current);
1244
+ abortControllerRef.current = null;
1245
+ };
1246
+ }, [lifecycleAbortController]);
1247
+ useEffect(() => {
1248
+ if (!transport.subscribeThreadEvents) return;
1249
+ let cancelled = false;
1250
+ let retryTimeout = null;
1251
+ let controller = null;
1252
+ const connect = () => {
1253
+ if (cancelled) return;
1254
+ controller = new AbortController();
1255
+ transport.subscribeThreadEvents?.(
1256
+ { cursor: threadEventsCursorRef.current, signal: controller.signal },
1257
+ async (event) => {
1258
+ if (typeof event.cursor === "number") threadEventsCursorRef.current = event.cursor;
1259
+ if (event.event === "snapshot_required") {
1260
+ await refresh().catch(() => void 0);
1261
+ return;
1262
+ }
1263
+ const next = applyThreadSummaryEvent(storage.listThreads(), event);
1264
+ storage.setThreads(next);
1265
+ setThreads(storage.listThreads());
1266
+ }
1267
+ ).catch((error) => {
1268
+ if (cancelled || isAbortError(error)) return;
1269
+ retryTimeout = setTimeout(connect, 1500);
1270
+ });
1271
+ };
1272
+ connect();
1273
+ return () => {
1274
+ cancelled = true;
1275
+ if (retryTimeout) clearTimeout(retryTimeout);
1276
+ controller?.abort();
1277
+ };
1278
+ }, [refresh, storage, transport]);
973
1279
  useEffect(() => {
974
1280
  syncThreadsFromStorage();
975
1281
  }, [storageKey, syncThreadsFromStorage]);
@@ -983,6 +1289,9 @@ function useAgents24ChatController({
983
1289
  detachActiveStream();
984
1290
  }
985
1291
  if (!activeThreadId) {
1292
+ if (previous === null && loadedThreadIdRef.current === null && messagesRef.current.length === 0 && !hasOlderTurnsRef.current && !isLoadingHistoryRef.current && !isLoadingOlderRef.current && nextBeforeTurnIndexRef.current === null) {
1293
+ return;
1294
+ }
986
1295
  requestSeqRef.current += 1;
987
1296
  loadedThreadIdRef.current = null;
988
1297
  setMessages([]);
@@ -1017,7 +1326,7 @@ function useAgents24ChatController({
1017
1326
  useEffect(() => {
1018
1327
  const threadId = activeThreadId;
1019
1328
  if (!threadId || isLoadingHistory || loadedThreadIdRef.current !== threadId || activeRunIdRef.current) return;
1020
- const runId = activeRunIdFromThread(storage.getThread(threadId));
1329
+ const runId = autoAttachRunIdFromThread(storage.getThread(threadId));
1021
1330
  if (!runId || reattachedRunIdRef.current === runId) return;
1022
1331
  void runStream({ mode: "attach", threadId, runId });
1023
1332
  }, [activeThreadId, isLoadingHistory, runStream, storage, storageKey]);
@@ -1030,82 +1339,53 @@ function useAgents24ChatController({
1030
1339
  if (!hasStaleUnfinishedAssistantCache(cached)) return;
1031
1340
  void loadThread(threadId).catch(() => setLoadingHistory(false));
1032
1341
  }, [activeThreadId, loadThread, setLoadingHistory, storage, storageKey]);
1033
- const handleCopy = useCallback((content, messageId) => {
1034
- navigator.clipboard?.writeText(content);
1035
- setCopiedMessageId(messageId);
1036
- setTimeout(() => setCopiedMessageId(null), 200);
1037
- }, []);
1038
- const handleLike = useCallback(async (msg) => {
1039
- const nextLiked = !liked[msg.id];
1040
- setLiked((prev) => ({ ...prev, [msg.id]: nextLiked }));
1041
- if (nextLiked) setDisliked((prev) => ({ ...prev, [msg.id]: false }));
1042
- }, [liked]);
1043
- const handleDislike = useCallback(async (msg) => {
1044
- const nextDisliked = !disliked[msg.id];
1045
- setDisliked((prev) => ({ ...prev, [msg.id]: nextDisliked }));
1046
- if (nextDisliked) setLiked((prev) => ({ ...prev, [msg.id]: false }));
1047
- }, [disliked]);
1048
- const handleRetry = useCallback(async (msg) => {
1049
- const index = messagesRef.current.findIndex((message) => message.id === msg.id);
1050
- if (index <= 0) return;
1051
- const userMessage = messagesRef.current[index - 1];
1052
- if (userMessage.role !== "user") return;
1053
- const trimmed = messagesRef.current.slice(0, index);
1054
- setMessages(trimmed);
1055
- messagesRef.current = trimmed;
1056
- if (activeThreadIdRef.current) persistThread(activeThreadIdRef.current, trimmed);
1057
- await handleSubmit({ text: userMessage.content, files: userMessage.attachments || [] });
1058
- }, [handleSubmit, persistThread]);
1059
- const upsertLiveVoiceMessage = useCallback((input) => {
1060
- const content = input.content?.trim() ?? "";
1061
- if (!content && !input.citations?.length && !input.reasoningSteps?.length) return;
1062
- setMessages((prev) => {
1063
- const currentId = liveVoiceIdsRef.current[input.role];
1064
- const index = currentId ? prev.findIndex((message) => message.id === currentId) : -1;
1065
- const nextMessage = {
1066
- id: currentId || createId(),
1067
- role: input.role,
1068
- content,
1069
- createdAt: /* @__PURE__ */ new Date(),
1070
- isFinal: Boolean(input.isFinal),
1071
- isVoice: input.role === "user",
1072
- parts: content ? [{ id: createId(), type: "text", kind: "text", text: content }] : [],
1073
- citations: input.citations,
1074
- reasoningSteps: input.reasoningSteps
1075
- };
1076
- const next = index === -1 ? [...prev, nextMessage] : prev.map((message, itemIndex) => itemIndex === index ? { ...message, ...nextMessage } : message);
1077
- liveVoiceIdsRef.current[input.role] = input.isFinal ? void 0 : nextMessage.id;
1078
- messagesRef.current = next;
1079
- if (activeThreadIdRef.current) persistThread(activeThreadIdRef.current, next);
1080
- return next;
1081
- });
1082
- }, [createId, persistThread]);
1083
- const startNewThread = useCallback(() => {
1084
- detachActiveStream();
1085
- requestSeqRef.current += 1;
1086
- activeThreadIdRef.current = null;
1087
- loadedThreadIdRef.current = null;
1088
- nextBeforeTurnIndexRef.current = null;
1089
- hasOlderTurnsRef.current = false;
1090
- storage.setActiveThreadId?.(null);
1091
- onActiveThreadIdChange?.(null);
1092
- setMessages([]);
1093
- messagesRef.current = [];
1094
- setHasOlderTurns(false);
1095
- setIsLoadingOlder(false);
1096
- setContextStatus(null);
1097
- setLoadingHistory(false);
1098
- }, [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage]);
1099
- const loadThreadById = useCallback(
1342
+ const {
1343
+ handleCopy,
1344
+ handleDislike,
1345
+ handleLike,
1346
+ handleRetry,
1347
+ startNewThread,
1348
+ upsertLiveVoiceMessage
1349
+ } = useControllerMessageActions({
1350
+ activeThreadIdRef,
1351
+ createId,
1352
+ detachActiveStream,
1353
+ disliked,
1354
+ handleSubmit,
1355
+ hasOlderTurnsRef,
1356
+ liked,
1357
+ liveVoiceIdsRef,
1358
+ loadedThreadIdRef,
1359
+ messagesRef,
1360
+ nextBeforeTurnIndexRef,
1361
+ onActiveThreadIdChange,
1362
+ persistThread,
1363
+ requestSeqRef,
1364
+ setContextStatus,
1365
+ setCopiedMessageId,
1366
+ setDisliked,
1367
+ setHasOlderTurns,
1368
+ setIsLoadingOlder,
1369
+ setLiked,
1370
+ setLoadingHistory,
1371
+ setMessages,
1372
+ storage
1373
+ });
1374
+ const loadThreadById = useCallback2(
1100
1375
  async (threadId) => {
1101
1376
  if (!threadId) return;
1377
+ setIsSelectingThread(true);
1102
1378
  if (activeThreadIdRef.current !== threadId) {
1103
1379
  if (abortControllerRef.current) detachActiveStream();
1104
1380
  activeThreadIdRef.current = threadId;
1105
1381
  storage.setActiveThreadId?.(threadId);
1106
1382
  onActiveThreadIdChange?.(threadId);
1107
1383
  }
1108
- await loadThread(threadId);
1384
+ try {
1385
+ await loadThread(threadId);
1386
+ } finally {
1387
+ if (activeThreadIdRef.current === threadId) setIsSelectingThread(false);
1388
+ }
1109
1389
  },
1110
1390
  [detachActiveStream, loadThread, onActiveThreadIdChange, storage]
1111
1391
  );
@@ -1123,6 +1403,7 @@ function useAgents24ChatController({
1123
1403
  isLoadingHistory,
1124
1404
  isLoadingOlder,
1125
1405
  isRefreshingThreads,
1406
+ isSelectingThread,
1126
1407
  hasOlderTurns,
1127
1408
  liked,
1128
1409
  disliked,
@@ -1130,6 +1411,7 @@ function useAgents24ChatController({
1130
1411
  lastThinkingDurationMs,
1131
1412
  activeRunId,
1132
1413
  handleSubmit,
1414
+ attachRun,
1133
1415
  handleStop,
1134
1416
  handleCopy,
1135
1417
  handleLike,
@@ -1147,6 +1429,7 @@ function useAgents24ChatController({
1147
1429
  activeRunId,
1148
1430
  activeThread,
1149
1431
  activeThreadId,
1432
+ attachRun,
1150
1433
  contextStatus,
1151
1434
  currentReasoning,
1152
1435
  disliked,
@@ -1161,6 +1444,7 @@ function useAgents24ChatController({
1161
1444
  isLoadingHistory,
1162
1445
  isLoadingOlder,
1163
1446
  isRefreshingThreads,
1447
+ isSelectingThread,
1164
1448
  lastThinkingDurationMs,
1165
1449
  liked,
1166
1450
  loadThreadById,
@@ -1205,13 +1489,16 @@ var DefaultChatPart = ({
1205
1489
  if (part.kind === "ui-blocks") {
1206
1490
  return /* @__PURE__ */ jsx("div", { "data-agents24-ui-blocks-part": true, "data-state": part.state });
1207
1491
  }
1208
- if (part.kind === "approval") {
1209
- return /* @__PURE__ */ jsx("div", { "data-agents24-approval-part": true });
1492
+ if (part.kind === "hitl") {
1493
+ return /* @__PURE__ */ jsx("div", { "data-agents24-hitl-part": part.interruptId || "" });
1210
1494
  }
1211
1495
  if (part.kind === "error") {
1212
1496
  return /* @__PURE__ */ jsx("div", { "data-agents24-error-part": true, children: part.errorText });
1213
1497
  }
1214
- return /* @__PURE__ */ jsx("div", { "data-agents24-data-part": part.name });
1498
+ if (part.kind === "data") {
1499
+ return /* @__PURE__ */ jsx("div", { "data-agents24-data-part": part.name });
1500
+ }
1501
+ return null;
1215
1502
  };
1216
1503
  var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ jsx(
1217
1504
  DefaultChatPart,
@@ -1224,12 +1511,17 @@ var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ jsx(
1224
1511
  );
1225
1512
 
1226
1513
  // src/sse.ts
1514
+ var isAbortError2 = (error) => {
1515
+ if (!error || typeof error !== "object") return false;
1516
+ const maybe = error;
1517
+ return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
1518
+ };
1227
1519
  var parseSseBlock = (block) => {
1228
1520
  const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.replace(/^data:\s?/, "")).join("\n").trim();
1229
1521
  if (!data) return null;
1230
1522
  return JSON.parse(data);
1231
1523
  };
1232
- var consumeSseResponse = async (response, onEvent) => {
1524
+ var consumeSseResponse = async (response, onEvent, options = {}) => {
1233
1525
  if (!response.ok) {
1234
1526
  let message = response.statusText || "Failed to open chat stream.";
1235
1527
  try {
@@ -1243,24 +1535,43 @@ var consumeSseResponse = async (response, onEvent) => {
1243
1535
  if (!reader) throw new Error("The chat stream did not return a readable body.");
1244
1536
  const decoder = new TextDecoder();
1245
1537
  let buffer = "";
1246
- let threadId = null;
1247
- let runId = null;
1248
- while (true) {
1249
- const { value, done } = await reader.read();
1250
- if (done) break;
1251
- buffer += decoder.decode(value, { stream: true });
1252
- let boundary = buffer.indexOf("\n\n");
1253
- while (boundary !== -1) {
1254
- const block = buffer.slice(0, boundary);
1255
- buffer = buffer.slice(boundary + 2);
1256
- boundary = buffer.indexOf("\n\n");
1257
- const event = parseSseBlock(block);
1258
- if (!event) continue;
1259
- if (event.run_id) runId = event.run_id;
1260
- const payloadThreadId = event.payload?.thread_id;
1261
- if (payloadThreadId) threadId = String(payloadThreadId);
1262
- await onEvent(event);
1538
+ let threadId = response.headers.get("X-Thread-ID") || null;
1539
+ let runId = response.headers.get("X-Run-ID") || null;
1540
+ let closed = false;
1541
+ const closeReader = () => {
1542
+ closed = true;
1543
+ queueMicrotask(() => {
1544
+ void reader.cancel().catch(() => {
1545
+ });
1546
+ });
1547
+ };
1548
+ if (options.signal?.aborted) {
1549
+ closeReader();
1550
+ return { threadId, runId };
1551
+ }
1552
+ options.signal?.addEventListener("abort", closeReader, { once: true });
1553
+ try {
1554
+ while (!closed) {
1555
+ const { value, done } = await reader.read();
1556
+ if (done) break;
1557
+ buffer += decoder.decode(value, { stream: true });
1558
+ let boundary = buffer.indexOf("\n\n");
1559
+ while (boundary !== -1) {
1560
+ const block = buffer.slice(0, boundary);
1561
+ buffer = buffer.slice(boundary + 2);
1562
+ boundary = buffer.indexOf("\n\n");
1563
+ const event = parseSseBlock(block);
1564
+ if (!event) continue;
1565
+ if (event.run_id) runId = event.run_id;
1566
+ const payloadThreadId = event.payload?.thread_id;
1567
+ if (payloadThreadId) threadId = String(payloadThreadId);
1568
+ await onEvent(event);
1569
+ }
1263
1570
  }
1571
+ } catch (error) {
1572
+ if (!isAbortError2(error)) throw error;
1573
+ } finally {
1574
+ options.signal?.removeEventListener("abort", closeReader);
1264
1575
  }
1265
1576
  return { threadId, runId };
1266
1577
  };
@@ -1276,6 +1587,14 @@ var streamHeaders = (headers) => ({
1276
1587
  Accept: "text/event-stream",
1277
1588
  "Content-Type": "application/json"
1278
1589
  });
1590
+ var ChatTransportError = class extends Error {
1591
+ constructor(message, status) {
1592
+ super(message);
1593
+ this.name = "ChatTransportError";
1594
+ this.status = status;
1595
+ }
1596
+ };
1597
+ var transportError = (response, fallback) => new ChatTransportError(response.statusText || fallback, response.status);
1279
1598
  var createFetchChatTransport = ({
1280
1599
  routes,
1281
1600
  fetchImpl = fetch,
@@ -1289,7 +1608,7 @@ var createFetchChatTransport = ({
1289
1608
  signal: input?.signal,
1290
1609
  headers: jsonHeaders(await loadHeaders())
1291
1610
  });
1292
- if (!response.ok) throw new Error(response.statusText || "Failed to list chat threads.");
1611
+ if (!response.ok) throw transportError(response, "Failed to list chat threads.");
1293
1612
  return response.json();
1294
1613
  },
1295
1614
  async getThread(input) {
@@ -1297,7 +1616,7 @@ var createFetchChatTransport = ({
1297
1616
  signal: input.signal,
1298
1617
  headers: jsonHeaders(await loadHeaders())
1299
1618
  });
1300
- if (!response.ok) throw new Error(response.statusText || "Failed to load chat thread.");
1619
+ if (!response.ok) throw transportError(response, "Failed to load chat thread.");
1301
1620
  return response.json();
1302
1621
  },
1303
1622
  async streamMessage(input, onEvent) {
@@ -1313,7 +1632,7 @@ var createFetchChatTransport = ({
1313
1632
  }
1314
1633
  )
1315
1634
  });
1316
- return consumeSseResponse(response, onEvent);
1635
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
1317
1636
  },
1318
1637
  async attachRun(input, onEvent) {
1319
1638
  const response = await fetchImpl(routes.attachRun(input), {
@@ -1322,7 +1641,7 @@ var createFetchChatTransport = ({
1322
1641
  headers: streamHeaders(await loadHeaders()),
1323
1642
  body: JSON.stringify({})
1324
1643
  });
1325
- return consumeSseResponse(response, onEvent);
1644
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
1326
1645
  },
1327
1646
  async cancelRun(input) {
1328
1647
  const response = await fetchImpl(routes.cancelRun(input), {
@@ -1330,7 +1649,7 @@ var createFetchChatTransport = ({
1330
1649
  headers: jsonHeaders(await loadHeaders()),
1331
1650
  body: JSON.stringify({ assistant_output_text: input.assistantOutputText || void 0 })
1332
1651
  });
1333
- if (!response.ok) throw new Error(response.statusText || "Failed to cancel chat run.");
1652
+ if (!response.ok) throw transportError(response, "Failed to cancel chat run.");
1334
1653
  return response.json();
1335
1654
  },
1336
1655
  async deleteThread(input) {
@@ -1339,7 +1658,50 @@ var createFetchChatTransport = ({
1339
1658
  method: "DELETE",
1340
1659
  headers: jsonHeaders(await loadHeaders())
1341
1660
  });
1342
- if (!response.ok) throw new Error(response.statusText || "Failed to delete chat thread.");
1661
+ if (!response.ok) throw transportError(response, "Failed to delete chat thread.");
1662
+ return response.json();
1663
+ },
1664
+ async subscribeThreadEvents(input, onEvent) {
1665
+ if (!routes.threadEvents) return;
1666
+ const response = await fetchImpl(routes.threadEvents({ cursor: input.cursor }), {
1667
+ method: "GET",
1668
+ signal: input.signal,
1669
+ headers: streamHeaders(await loadHeaders())
1670
+ });
1671
+ await consumeSseResponse(
1672
+ response,
1673
+ (event) => onEvent(event),
1674
+ { signal: input.signal }
1675
+ );
1676
+ },
1677
+ async resumeHitl(input) {
1678
+ if (!routes.resumeRun) throw new ChatTransportError("HITL resume is not configured.", 501);
1679
+ const response = await fetchImpl(routes.resumeRun({ runId: input.runId }), {
1680
+ method: "POST",
1681
+ headers: jsonHeaders(await loadHeaders()),
1682
+ body: JSON.stringify({
1683
+ schema_version: "agents24.hitl.resume.v1",
1684
+ interrupt_id: input.interruptId,
1685
+ decisions: input.decisions,
1686
+ client: input.client
1687
+ })
1688
+ });
1689
+ if (!response.ok) throw transportError(response, "Failed to resume chat run.");
1690
+ return response.json();
1691
+ },
1692
+ async startMcpAuth(input) {
1693
+ if (!routes.startMcpAuth) throw new ChatTransportError("MCP auth is not configured.", 501);
1694
+ const response = await fetchImpl(routes.startMcpAuth({ runId: input.runId, serverId: input.serverId }), {
1695
+ method: "POST",
1696
+ headers: jsonHeaders(await loadHeaders()),
1697
+ body: JSON.stringify({
1698
+ interrupt_id: input.interruptId || void 0,
1699
+ principal_type: input.principalType || void 0,
1700
+ principal_id: input.principalId || void 0,
1701
+ client: input.client
1702
+ })
1703
+ });
1704
+ if (!response.ok) throw transportError(response, "Failed to start MCP authorization.");
1343
1705
  return response.json();
1344
1706
  }
1345
1707
  };
@@ -1353,6 +1715,7 @@ import {
1353
1715
  useMessageScrollerVisibility
1354
1716
  } from "@shadcn/react/message-scroller";
1355
1717
  export {
1718
+ ChatTransportError,
1356
1719
  DEFAULT_THREAD_PAGE_SIZE,
1357
1720
  DefaultChatPart,
1358
1721
  DefaultToolPart,
@@ -1367,33 +1730,38 @@ export {
1367
1730
  MessageScroller,
1368
1731
  activeRunIdFromThread,
1369
1732
  activeRunIdFromThreadDetail,
1733
+ appendStableStreamingTurn,
1370
1734
  assistantTextFromParts,
1371
1735
  assistantTextFromResponseBlocks,
1736
+ autoAttachRunIdFromThread,
1372
1737
  compressionFromContextWindow,
1373
1738
  consumeSseResponse,
1374
1739
  createChatId,
1375
1740
  createFetchChatTransport,
1741
+ findStableAssistantMessageIndex,
1376
1742
  getActiveStreamingTextPartId,
1377
1743
  hasStaleUnfinishedAssistantCache,
1378
1744
  hasUnfinishedAssistantMessage,
1379
1745
  isActiveStreamingTextPart,
1746
+ isAutoAttachThreadStatus,
1380
1747
  isRunningThreadStatus,
1381
1748
  latestContextWindowFromThread,
1382
1749
  mergeContextWindow,
1383
1750
  mergeContextWindowUpdate,
1384
1751
  mergeReasoningSteps,
1752
+ mergeStoredThreadsForRefresh,
1385
1753
  normalizeContextCompression,
1386
1754
  normalizeContextWindow,
1387
1755
  parseSseBlock,
1388
1756
  partsFromResponseBlocks,
1389
1757
  reasoningStepsFromParts,
1390
1758
  renderChatPart,
1391
- textFromFinalOutput,
1392
1759
  threadActivityDate,
1393
1760
  threadDetailToMessages,
1394
1761
  threadPaging,
1395
1762
  titleFromMessage,
1396
1763
  toolStateFromStatus,
1764
+ upsertStableAssistantMessage,
1397
1765
  useAgents24ChatController,
1398
1766
  useMessageScroller,
1399
1767
  useMessageScrollerScrollable,