@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.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,33 +45,38 @@ __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,
65
71
  partsFromResponseBlocks: () => partsFromResponseBlocks,
66
72
  reasoningStepsFromParts: () => reasoningStepsFromParts,
67
73
  renderChatPart: () => renderChatPart,
68
- textFromFinalOutput: () => textFromFinalOutput,
69
74
  threadActivityDate: () => threadActivityDate,
70
75
  threadDetailToMessages: () => threadDetailToMessages,
71
76
  threadPaging: () => threadPaging,
72
77
  titleFromMessage: () => titleFromMessage,
73
78
  toolStateFromStatus: () => toolStateFromStatus,
79
+ upsertStableAssistantMessage: () => upsertStableAssistantMessage,
74
80
  useAgents24ChatController: () => useAgents24ChatController,
75
81
  useMessageScroller: () => import_message_scroller2.useMessageScroller,
76
82
  useMessageScrollerScrollable: () => import_message_scroller2.useMessageScrollerScrollable,
@@ -80,7 +86,79 @@ __export(index_exports, {
80
86
  module.exports = __toCommonJS(index_exports);
81
87
 
82
88
  // src/controller.ts
89
+ var import_react2 = require("react");
90
+
91
+ // src/controller-actions.ts
83
92
  var import_react = require("react");
93
+ function useControllerMessageActions(input) {
94
+ const handleCopy = (0, import_react.useCallback)((content, messageId) => {
95
+ navigator.clipboard?.writeText(content);
96
+ input.setCopiedMessageId(messageId);
97
+ setTimeout(() => input.setCopiedMessageId(null), 200);
98
+ }, [input]);
99
+ const handleLike = (0, import_react.useCallback)(async (msg) => {
100
+ const nextLiked = !input.liked[msg.id];
101
+ input.setLiked((prev) => ({ ...prev, [msg.id]: nextLiked }));
102
+ if (nextLiked) input.setDisliked((prev) => ({ ...prev, [msg.id]: false }));
103
+ }, [input]);
104
+ const handleDislike = (0, import_react.useCallback)(async (msg) => {
105
+ const nextDisliked = !input.disliked[msg.id];
106
+ input.setDisliked((prev) => ({ ...prev, [msg.id]: nextDisliked }));
107
+ if (nextDisliked) input.setLiked((prev) => ({ ...prev, [msg.id]: false }));
108
+ }, [input]);
109
+ const handleRetry = (0, import_react.useCallback)(async (msg) => {
110
+ const index = input.messagesRef.current.findIndex((message) => message.id === msg.id);
111
+ if (index <= 0) return;
112
+ const userMessage = input.messagesRef.current[index - 1];
113
+ if (userMessage.role !== "user") return;
114
+ const trimmed = input.messagesRef.current.slice(0, index);
115
+ input.setMessages(trimmed);
116
+ input.messagesRef.current = trimmed;
117
+ if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, trimmed);
118
+ await input.handleSubmit({ text: userMessage.content, files: userMessage.attachments || [] });
119
+ }, [input]);
120
+ const upsertLiveVoiceMessage = (0, import_react.useCallback)((payload) => {
121
+ const content = payload.content?.trim() ?? "";
122
+ if (!content && !payload.citations?.length && !payload.reasoningSteps?.length) return;
123
+ input.setMessages((prev) => {
124
+ const currentId = input.liveVoiceIdsRef.current[payload.role];
125
+ const index = currentId ? prev.findIndex((message) => message.id === currentId) : -1;
126
+ const nextMessage = {
127
+ id: currentId || input.createId(),
128
+ role: payload.role,
129
+ content,
130
+ createdAt: /* @__PURE__ */ new Date(),
131
+ isFinal: Boolean(payload.isFinal),
132
+ isVoice: payload.role === "user",
133
+ parts: content ? [{ id: input.createId(), type: "text", kind: "text", text: content }] : [],
134
+ citations: payload.citations,
135
+ reasoningSteps: payload.reasoningSteps
136
+ };
137
+ const next = index === -1 ? [...prev, nextMessage] : prev.map((message, itemIndex) => itemIndex === index ? { ...message, ...nextMessage } : message);
138
+ input.liveVoiceIdsRef.current[payload.role] = payload.isFinal ? void 0 : nextMessage.id;
139
+ input.messagesRef.current = next;
140
+ if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, next);
141
+ return next;
142
+ });
143
+ }, [input]);
144
+ const startNewThread = (0, import_react.useCallback)(() => {
145
+ input.detachActiveStream();
146
+ input.requestSeqRef.current += 1;
147
+ input.activeThreadIdRef.current = null;
148
+ input.loadedThreadIdRef.current = null;
149
+ input.nextBeforeTurnIndexRef.current = null;
150
+ input.hasOlderTurnsRef.current = false;
151
+ input.storage.setActiveThreadId?.(null);
152
+ input.onActiveThreadIdChange?.(null);
153
+ input.setMessages([]);
154
+ input.messagesRef.current = [];
155
+ input.setHasOlderTurns(false);
156
+ input.setIsLoadingOlder(false);
157
+ input.setContextStatus(null);
158
+ input.setLoadingHistory(false);
159
+ }, [input]);
160
+ return { handleCopy, handleDislike, handleLike, handleRetry, startNewThread, upsertLiveVoiceMessage };
161
+ }
84
162
 
85
163
  // src/context-window.ts
86
164
  var SOURCE_PRIORITY = {
@@ -203,7 +281,8 @@ function compressionFromContextWindow(contextWindow) {
203
281
 
204
282
  // src/model.ts
205
283
  var DEFAULT_THREAD_PAGE_SIZE = 5;
206
- var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
284
+ var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling", "paused"])).has(String(status || "").toLowerCase());
285
+ var isAutoAttachThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
207
286
  var createChatId = () => {
208
287
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
209
288
  return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
@@ -215,12 +294,6 @@ var titleFromMessage = (text, files = []) => {
215
294
  var threadActivityDate = (thread) => String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString());
216
295
  var assistantTextFromResponseBlocks = (blocks) => (blocks || []).filter((block) => block.kind === "assistant_text" && typeof block.text === "string").map((block) => String(block.text)).join("\n\n").trim();
217
296
  var assistantTextFromParts = (parts) => (parts || []).filter((part) => part.kind === "text").map((part) => part.text).join("\n\n").trim();
218
- var textFromFinalOutput = (value) => {
219
- if (typeof value === "string") return value;
220
- if (!value || typeof value !== "object") return "";
221
- const record = value;
222
- return String(record.message || record.text || record.answer || "");
223
- };
224
297
  var displayTextWithoutInlineAttachments = (text, hasAttachments) => {
225
298
  if (!hasAttachments) return text;
226
299
  const marker = "Attached text file (";
@@ -267,7 +340,7 @@ var responseBlocksFromTurn = (turn) => {
267
340
  var assistantTextFromEvents = (events) => {
268
341
  const assistantText = latestEventPayloadValue(events, "assistant_output_text");
269
342
  if (typeof assistantText === "string" && assistantText.trim()) return assistantText;
270
- return textFromFinalOutput(latestEventPayloadValue(events, "final_output"));
343
+ return "";
271
344
  };
272
345
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
273
346
  var optionalString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -369,8 +442,17 @@ var partsFromResponseBlocks = (blocks, fallbackText) => {
369
442
  });
370
443
  return;
371
444
  }
372
- if (block.kind === "approval_request") {
373
- parts.push({ id, type: "approval", kind: "approval", raw: block });
445
+ if (block.kind === "hitl_request") {
446
+ const hitl = asRecord(block.hitl) || block;
447
+ parts.push({
448
+ id,
449
+ type: "hitl",
450
+ kind: "hitl",
451
+ hitl,
452
+ interruptId: optionalString(block.interruptId) || optionalString(hitl.interrupt_id) || null,
453
+ hitlKind: optionalString(block.hitlKind) || optionalString(hitl.kind) || null,
454
+ raw: block
455
+ });
374
456
  return;
375
457
  }
376
458
  if (block.kind === "error") {
@@ -442,7 +524,7 @@ var turnToMessages = (turn, activeRunId) => {
442
524
  attachments
443
525
  });
444
526
  }
445
- const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events) || textFromFinalOutput(turn.final_output);
527
+ const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events);
446
528
  if (assistantText || responseBlocks.length > 0 || isRunning) {
447
529
  const parts = partsFromResponseBlocks(responseBlocks, assistantText);
448
530
  messages.push({
@@ -496,6 +578,23 @@ var activeRunIdFromThread = (thread) => {
496
578
  const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
497
579
  return lastRunId && isRunningThreadStatus(lastRunStatus) ? String(lastRunId) : null;
498
580
  };
581
+ var autoAttachRunIdFromThread = (thread) => {
582
+ const hasCamelActiveRun = Boolean(
583
+ thread && Object.prototype.hasOwnProperty.call(thread, "activeRun")
584
+ );
585
+ const hasCamelLastRunStatus = Boolean(
586
+ thread && Object.prototype.hasOwnProperty.call(thread, "lastRunStatus")
587
+ );
588
+ const hasCamelLastRunId = Boolean(
589
+ thread && Object.prototype.hasOwnProperty.call(thread, "lastRunId")
590
+ );
591
+ const activeRun = hasCamelActiveRun ? thread?.activeRun || null : thread?.active_run || null;
592
+ const lastRunStatus = hasCamelLastRunStatus ? thread?.lastRunStatus || null : thread?.last_run_status || thread?.lastRunStatus || null;
593
+ const activeRunId = activeRun?.run_id ? String(activeRun.run_id) : "";
594
+ if (activeRunId && isAutoAttachThreadStatus(activeRun?.status || lastRunStatus)) return activeRunId;
595
+ const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
596
+ return lastRunId && isAutoAttachThreadStatus(lastRunStatus) ? String(lastRunId) : null;
597
+ };
499
598
  var hasUnfinishedAssistantMessage = (messages) => Boolean(messages?.some((message) => message.role === "assistant" && message.isFinal === false));
500
599
  var hasStaleUnfinishedAssistantCache = (thread) => hasUnfinishedAssistantMessage(thread?.messages) && !activeRunIdFromThread(thread);
501
600
  var activeRunIdFromThreadDetail = (thread) => {
@@ -505,7 +604,7 @@ var activeRunIdFromThreadDetail = (thread) => {
505
604
  return runningTurn?.run_id ? String(runningTurn.run_id) : null;
506
605
  };
507
606
 
508
- // src/controller.ts
607
+ // src/controller-helpers.ts
509
608
  var threadSummaryToStored = (thread) => ({
510
609
  ...thread,
511
610
  id: String(thread.id),
@@ -514,6 +613,121 @@ var threadSummaryToStored = (thread) => ({
514
613
  messages: [],
515
614
  isHydrated: false
516
615
  });
616
+ var isThreadNotFoundError = (error) => {
617
+ if (!error || typeof error !== "object" || !("status" in error)) return false;
618
+ return Number(error.status) === 404;
619
+ };
620
+ var isAbortError = (error) => {
621
+ if (!error || typeof error !== "object") return false;
622
+ const maybe = error;
623
+ return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
624
+ };
625
+ var abortDetachedStream = (controller) => {
626
+ if (!controller || controller.signal.aborted) return;
627
+ try {
628
+ const reason = typeof DOMException !== "undefined" ? new DOMException("Chat stream detached.", "AbortError") : new Error("Chat stream detached.");
629
+ controller.abort(reason);
630
+ } catch {
631
+ }
632
+ };
633
+ var hasRunState = (thread) => Boolean(
634
+ thread.active_run || thread.activeRun || thread.last_run_id || thread.lastRunId || thread.last_run_status || thread.lastRunStatus || thread.isRunning
635
+ );
636
+ var mergeStoredThreadsForRefresh = (currentThreads, serverThreads) => {
637
+ const currentById = new Map(currentThreads.map((thread) => [thread.id, thread]));
638
+ const serverIds = new Set(serverThreads.map((thread) => thread.id));
639
+ const mergedServerThreads = serverThreads.map((serverThread) => {
640
+ const current = currentById.get(serverThread.id);
641
+ if (!current) return serverThread;
642
+ const incomingHasRunState = hasRunState(serverThread);
643
+ return {
644
+ ...current,
645
+ ...serverThread,
646
+ messages: current.messages || [],
647
+ isHydrated: current.isHydrated,
648
+ hasOlderTurns: current.hasOlderTurns,
649
+ nextBeforeTurnIndex: current.nextBeforeTurnIndex,
650
+ active_run: incomingHasRunState ? serverThread.active_run : current.active_run,
651
+ activeRun: incomingHasRunState ? serverThread.activeRun : current.activeRun,
652
+ last_run_id: incomingHasRunState ? serverThread.last_run_id : current.last_run_id,
653
+ lastRunId: incomingHasRunState ? serverThread.lastRunId : current.lastRunId,
654
+ last_run_status: incomingHasRunState ? serverThread.last_run_status : current.last_run_status,
655
+ lastRunStatus: incomingHasRunState ? serverThread.lastRunStatus : current.lastRunStatus,
656
+ lastEventSeq: incomingHasRunState ? serverThread.lastEventSeq : current.lastEventSeq,
657
+ isRunning: incomingHasRunState ? serverThread.isRunning : current.isRunning
658
+ };
659
+ });
660
+ const localOnlyThreads = currentThreads.filter((thread) => {
661
+ if (serverIds.has(thread.id)) return false;
662
+ return Boolean(thread.isHydrated || thread.messages?.length || thread.isRunning || thread.activeRun || thread.active_run);
663
+ });
664
+ return [...mergedServerThreads, ...localOnlyThreads];
665
+ };
666
+ var stableStringify = (value) => JSON.stringify(value ?? null);
667
+ var sameStoredThread = (left, right) => {
668
+ if (left === right) return true;
669
+ if (!left || !right) return false;
670
+ return stableStringify(left) === stableStringify(right);
671
+ };
672
+ var applyThreadSummaryEvent = (currentThreads, event) => {
673
+ if (event.event === "snapshot_required") return currentThreads;
674
+ if (event.event === "thread.deleted") {
675
+ const next2 = currentThreads.filter((thread) => thread.id !== event.thread_id);
676
+ return next2.length === currentThreads.length ? currentThreads : next2;
677
+ }
678
+ const incoming = threadSummaryToStored(event.thread);
679
+ const index = currentThreads.findIndex((thread) => thread.id === incoming.id);
680
+ const sortByActivity = (threads) => [...threads].sort((a, b) => {
681
+ const left = Date.parse(String(a.updated_at || a.last_activity_at || a.created_at || ""));
682
+ const right = Date.parse(String(b.updated_at || b.last_activity_at || b.created_at || ""));
683
+ return (Number.isFinite(right) ? right : 0) - (Number.isFinite(left) ? left : 0);
684
+ });
685
+ if (index === -1) return sortByActivity([...currentThreads, incoming]);
686
+ const [merged] = mergeStoredThreadsForRefresh([currentThreads[index]], [incoming]);
687
+ if (sameStoredThread(currentThreads[index], merged)) return currentThreads;
688
+ const next = [...currentThreads];
689
+ next[index] = merged;
690
+ return sortByActivity(next);
691
+ };
692
+
693
+ // src/message-lifecycle.ts
694
+ function findStableAssistantMessageIndex(messages, input) {
695
+ return messages.findIndex(
696
+ (message) => message.role === "assistant" && (input.messageId && message.id === input.messageId || Boolean(input.runId && message.runId === input.runId))
697
+ );
698
+ }
699
+ function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
700
+ const next = [...messages];
701
+ if (!next.some((message) => message.id === userMessage.id)) {
702
+ next.push(userMessage);
703
+ }
704
+ if (!next.some(
705
+ (message) => message.id === assistantMessage.id || Boolean(
706
+ assistantMessage.runId && message.role === "assistant" && message.runId === assistantMessage.runId
707
+ )
708
+ )) {
709
+ next.push(assistantMessage);
710
+ }
711
+ return next;
712
+ }
713
+ function upsertStableAssistantMessage(messages, input) {
714
+ const next = [...messages];
715
+ for (const baseMessage of input.baseMessages || []) {
716
+ const matchesTarget = baseMessage.role === "assistant" && (input.messageId && baseMessage.id === input.messageId || Boolean(input.runId && baseMessage.runId === input.runId));
717
+ if (!matchesTarget && !next.some((message) => message.id === baseMessage.id)) {
718
+ next.push(baseMessage);
719
+ }
720
+ }
721
+ const index = findStableAssistantMessageIndex(next, input);
722
+ if (index === -1) {
723
+ next.push(input.create());
724
+ return next;
725
+ }
726
+ next[index] = input.update(next[index]);
727
+ return next;
728
+ }
729
+
730
+ // src/controller.ts
517
731
  function useAgents24ChatController({
518
732
  transport,
519
733
  storage,
@@ -529,56 +743,60 @@ function useAgents24ChatController({
529
743
  }) {
530
744
  const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
531
745
  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) => {
746
+ const [messages, setMessages] = (0, import_react2.useState)(() => initialCached?.messages || []);
747
+ const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
748
+ const [isLoadingHistory, setIsLoadingHistory] = (0, import_react2.useState)(() => Boolean(activeThreadId) && !initialCached?.messages?.length);
749
+ const [isLoadingOlder, setIsLoadingOlder] = (0, import_react2.useState)(false);
750
+ const [hasOlderTurns, setHasOlderTurns] = (0, import_react2.useState)(Boolean(initialCached?.hasOlderTurns));
751
+ const [streamingContent, setStreamingContent] = (0, import_react2.useState)("");
752
+ const [streamingMessageId, setStreamingMessageId] = (0, import_react2.useState)(null);
753
+ const [contextStatus, setContextStatus] = (0, import_react2.useState)(null);
754
+ const [currentReasoning, setCurrentReasoning] = (0, import_react2.useState)([]);
755
+ const [liked, setLiked] = (0, import_react2.useState)({});
756
+ const [disliked, setDisliked] = (0, import_react2.useState)({});
757
+ const [copiedMessageId, setCopiedMessageId] = (0, import_react2.useState)(null);
758
+ const [lastThinkingDurationMs, setLastThinkingDurationMs] = (0, import_react2.useState)(null);
759
+ const [threads, setThreads] = (0, import_react2.useState)(() => storage.listThreads());
760
+ const [isRefreshingThreads, setIsRefreshingThreads] = (0, import_react2.useState)(false);
761
+ const [isSelectingThread, setIsSelectingThread] = (0, import_react2.useState)(false);
762
+ const [activeRunId, setActiveRunId] = (0, import_react2.useState)(null);
763
+ const textareaRef = (0, import_react2.useRef)(null);
764
+ const activeThreadIdRef = (0, import_react2.useRef)(activeThreadId);
765
+ const messagesRef = (0, import_react2.useRef)(messages);
766
+ const nextBeforeTurnIndexRef = (0, import_react2.useRef)(initialCached?.nextBeforeTurnIndex ?? null);
767
+ const hasOlderTurnsRef = (0, import_react2.useRef)(Boolean(initialCached?.hasOlderTurns));
768
+ const isLoadingOlderRef = (0, import_react2.useRef)(false);
769
+ const loadedThreadIdRef = (0, import_react2.useRef)(initialCached?.messages?.length ? activeThreadId : null);
770
+ const requestSeqRef = (0, import_react2.useRef)(0);
771
+ const isLoadingHistoryRef = (0, import_react2.useRef)(isLoadingHistory);
772
+ const activeRunIdRef = (0, import_react2.useRef)(null);
773
+ const reattachedRunIdRef = (0, import_react2.useRef)(null);
774
+ const abortControllerRef = (0, import_react2.useRef)(null);
775
+ const [lifecycleAbortController] = (0, import_react2.useState)(() => new AbortController());
776
+ const streamingContentRef = (0, import_react2.useRef)("");
777
+ const streamingMessageIdRef = (0, import_react2.useRef)(null);
778
+ const reasoningRef = (0, import_react2.useRef)([]);
779
+ const liveVoiceIdsRef = (0, import_react2.useRef)({});
780
+ const refreshSeqRef = (0, import_react2.useRef)(0);
781
+ const threadEventsCursorRef = (0, import_react2.useRef)(null);
782
+ const setActiveRunIdValue = (0, import_react2.useCallback)((runId) => {
565
783
  activeRunIdRef.current = runId;
566
784
  setActiveRunId(runId);
567
785
  }, []);
568
- const syncThreadsFromStorage = (0, import_react.useCallback)(() => {
786
+ const syncThreadsFromStorage = (0, import_react2.useCallback)(() => {
569
787
  setThreads(storage.listThreads());
570
788
  }, [storage]);
571
- const upsertStoredThread = (0, import_react.useCallback)(
789
+ const upsertStoredThread = (0, import_react2.useCallback)(
572
790
  (thread) => {
573
791
  storage.upsertThread(thread);
574
792
  syncThreadsFromStorage();
575
793
  },
576
794
  [storage, syncThreadsFromStorage]
577
795
  );
578
- (0, import_react.useEffect)(() => {
796
+ (0, import_react2.useEffect)(() => {
579
797
  messagesRef.current = messages;
580
798
  }, [messages]);
581
- const persistThread = (0, import_react.useCallback)(
799
+ const persistThread = (0, import_react2.useCallback)(
582
800
  (threadId, nextMessages, paging, options) => {
583
801
  const existing = storage.getThread(threadId);
584
802
  const firstUser = nextMessages.find((message) => message.role === "user");
@@ -595,7 +813,7 @@ function useAgents24ChatController({
595
813
  },
596
814
  [storage, upsertStoredThread]
597
815
  );
598
- const markThreadRunStatus = (0, import_react.useCallback)(
816
+ const markThreadRunStatus = (0, import_react2.useCallback)(
599
817
  (threadId, runId, status, lastEventSeq) => {
600
818
  const existing = storage.getThread(threadId);
601
819
  if (!existing || !runId) return;
@@ -625,18 +843,21 @@ function useAgents24ChatController({
625
843
  },
626
844
  [storage, upsertStoredThread]
627
845
  );
628
- const refresh = (0, import_react.useCallback)(async () => {
846
+ const refresh = (0, import_react2.useCallback)(async () => {
847
+ const seq = ++refreshSeqRef.current;
629
848
  setIsRefreshingThreads(true);
630
849
  try {
631
- const data = await transport.listThreads();
850
+ const data = await transport.listThreads({ signal: lifecycleAbortController.signal });
851
+ if (lifecycleAbortController.signal.aborted) return;
852
+ if (seq !== refreshSeqRef.current) return;
632
853
  const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
633
- storage.setThreads(nextThreads);
854
+ storage.setThreads(mergeStoredThreadsForRefresh(storage.listThreads(), nextThreads));
634
855
  setThreads(storage.listThreads());
635
856
  } finally {
636
- setIsRefreshingThreads(false);
857
+ if (!lifecycleAbortController.signal.aborted && seq === refreshSeqRef.current) setIsRefreshingThreads(false);
637
858
  }
638
- }, [storage, transport]);
639
- const applyThreadId = (0, import_react.useCallback)(
859
+ }, [lifecycleAbortController, storage, transport]);
860
+ const applyThreadId = (0, import_react2.useCallback)(
640
861
  (threadId, baseMessages) => {
641
862
  if (!threadId || activeThreadIdRef.current === threadId) return;
642
863
  activeThreadIdRef.current = threadId;
@@ -650,18 +871,18 @@ function useAgents24ChatController({
650
871
  streamingContentRef.current = value;
651
872
  setStreamingContent(value);
652
873
  };
653
- const setLoadingHistory = (0, import_react.useCallback)((value) => {
874
+ const setLoadingHistory = (0, import_react2.useCallback)((value) => {
654
875
  isLoadingHistoryRef.current = value;
655
876
  setIsLoadingHistory(value);
656
877
  }, []);
657
- const setReasoningSteps = (0, import_react.useCallback)((value) => {
878
+ const setReasoningSteps = (0, import_react2.useCallback)((value) => {
658
879
  reasoningRef.current = value || [];
659
880
  setCurrentReasoning(value || []);
660
881
  }, []);
661
- const detachActiveStream = (0, import_react.useCallback)(() => {
882
+ const detachActiveStream = (0, import_react2.useCallback)(() => {
662
883
  const controller = abortControllerRef.current;
663
884
  abortControllerRef.current = null;
664
- controller?.abort();
885
+ abortDetachedStream(controller);
665
886
  setActiveRunIdValue(null);
666
887
  reattachedRunIdRef.current = null;
667
888
  streamingMessageIdRef.current = null;
@@ -672,21 +893,36 @@ function useAgents24ChatController({
672
893
  setStreamingContent("");
673
894
  setCurrentReasoning([]);
674
895
  }, [setActiveRunIdValue]);
675
- const setLiveAssistantMessage = (0, import_react.useCallback)(
896
+ const clearMissingThread = (0, import_react2.useCallback)(
897
+ (threadId) => {
898
+ storage.deleteThread?.(threadId);
899
+ syncThreadsFromStorage();
900
+ if (activeThreadIdRef.current !== threadId) return;
901
+ requestSeqRef.current += 1;
902
+ detachActiveStream();
903
+ activeThreadIdRef.current = null;
904
+ loadedThreadIdRef.current = null;
905
+ nextBeforeTurnIndexRef.current = null;
906
+ hasOlderTurnsRef.current = false;
907
+ storage.setActiveThreadId?.(null);
908
+ onActiveThreadIdChange?.(null);
909
+ setMessages([]);
910
+ messagesRef.current = [];
911
+ setHasOlderTurns(false);
912
+ setIsLoadingOlder(false);
913
+ setContextStatus(null);
914
+ setLoadingHistory(false);
915
+ },
916
+ [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage, syncThreadsFromStorage]
917
+ );
918
+ const setLiveAssistantMessage = (0, import_react2.useCallback)(
676
919
  (input) => {
677
920
  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({
921
+ const next = upsertStableAssistantMessage(prev, {
922
+ baseMessages: input.baseMessages,
923
+ messageId: input.messageId,
924
+ runId: input.runId,
925
+ create: () => ({
690
926
  id: input.messageId,
691
927
  role: "assistant",
692
928
  runId: input.runId ?? null,
@@ -695,26 +931,23 @@ function useAgents24ChatController({
695
931
  reasoningSteps: input.reasoning,
696
932
  isFinal: false,
697
933
  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
- };
934
+ }),
935
+ update: (message) => ({
936
+ ...message,
937
+ runId: input.runId ?? message.runId ?? null,
938
+ content: input.content,
939
+ reasoningSteps: input.reasoning,
940
+ isFinal: false,
941
+ parts: input.parts ?? message.parts
942
+ })
943
+ });
711
944
  messagesRef.current = next;
712
945
  return next;
713
946
  });
714
947
  },
715
948
  []
716
949
  );
717
- const finalizeAssistantMessage = (0, import_react.useCallback)(
950
+ const finalizeAssistantMessage = (0, import_react2.useCallback)(
718
951
  (input) => {
719
952
  const content = input.error || input.assistantText.trim();
720
953
  if (!content) return input.baseMessages;
@@ -734,7 +967,17 @@ function useAgents24ChatController({
734
967
  reasoningSteps: mergeReasoningSteps(input.reasoning, { finalize: true }),
735
968
  thinkingDurationMs: input.thinkingDurationMs
736
969
  };
737
- const completed = existingIndex >= 0 ? input.baseMessages.map((message, index) => index === existingIndex ? assistant : message) : [...input.baseMessages, assistant];
970
+ const completed = upsertStableAssistantMessage(input.baseMessages, {
971
+ messageId: input.messageId,
972
+ runId: input.runId,
973
+ create: () => assistant,
974
+ update: (message) => ({
975
+ ...message,
976
+ ...assistant,
977
+ id: message.id,
978
+ createdAt: message.createdAt
979
+ })
980
+ });
738
981
  setMessages(completed);
739
982
  messagesRef.current = completed;
740
983
  if (input.threadId) {
@@ -758,7 +1001,7 @@ function useAgents24ChatController({
758
1001
  },
759
1002
  [createId, persistThread, storage, upsertStoredThread]
760
1003
  );
761
- const loadThread = (0, import_react.useCallback)(
1004
+ const loadThread = (0, import_react2.useCallback)(
762
1005
  async (threadId) => {
763
1006
  const seq = ++requestSeqRef.current;
764
1007
  setLoadingHistory(true);
@@ -768,7 +1011,8 @@ function useAgents24ChatController({
768
1011
  const detail = await transport.getThread({
769
1012
  threadId,
770
1013
  limit: pageSize,
771
- includeRunEvents: false
1014
+ includeRunEvents: false,
1015
+ signal: lifecycleAbortController.signal
772
1016
  });
773
1017
  if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
774
1018
  void onThreadDetailLoaded?.(detail);
@@ -789,13 +1033,20 @@ function useAgents24ChatController({
789
1033
  nextBeforeTurnIndex: paging.nextBeforeTurnIndex,
790
1034
  updated_at: threadActivityDate(detail)
791
1035
  });
1036
+ } catch (error) {
1037
+ if (isAbortError(error)) return;
1038
+ if (isThreadNotFoundError(error)) {
1039
+ clearMissingThread(threadId);
1040
+ return;
1041
+ }
1042
+ throw error;
792
1043
  } finally {
793
- if (activeThreadIdRef.current === threadId) setLoadingHistory(false);
1044
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
794
1045
  }
795
1046
  },
796
- [onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
1047
+ [clearMissingThread, lifecycleAbortController, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
797
1048
  );
798
- const loadOlderTurns = (0, import_react.useCallback)(async () => {
1049
+ const loadOlderTurns = (0, import_react2.useCallback)(async () => {
799
1050
  const threadId = activeThreadIdRef.current;
800
1051
  const beforeTurnIndex = nextBeforeTurnIndexRef.current;
801
1052
  if (!threadId || beforeTurnIndex === null || isLoadingOlderRef.current) return;
@@ -806,7 +1057,8 @@ function useAgents24ChatController({
806
1057
  threadId,
807
1058
  limit: pageSize,
808
1059
  beforeTurnIndex,
809
- includeRunEvents: false
1060
+ includeRunEvents: false,
1061
+ signal: lifecycleAbortController.signal
810
1062
  });
811
1063
  if (activeThreadIdRef.current !== threadId) return;
812
1064
  const older = threadDetailToMessages(detail);
@@ -818,12 +1070,19 @@ function useAgents24ChatController({
818
1070
  hasOlderTurnsRef.current = paging.hasOlderTurns;
819
1071
  nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
820
1072
  persistThread(threadId, next, paging);
1073
+ } catch (error) {
1074
+ if (isAbortError(error)) return;
1075
+ if (isThreadNotFoundError(error)) {
1076
+ clearMissingThread(threadId);
1077
+ return;
1078
+ }
1079
+ throw error;
821
1080
  } finally {
822
- if (activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
1081
+ if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
823
1082
  isLoadingOlderRef.current = false;
824
1083
  }
825
- }, [pageSize, persistThread, transport]);
826
- const handleStreamEvent = (0, import_react.useCallback)(
1084
+ }, [clearMissingThread, lifecycleAbortController, pageSize, persistThread, transport]);
1085
+ const handleStreamEvent = (0, import_react2.useCallback)(
827
1086
  (input) => {
828
1087
  const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
829
1088
  const payload = event.payload || {};
@@ -869,7 +1128,7 @@ function useAgents24ChatController({
869
1128
  streamThreadIdRef.current = terminalThreadId;
870
1129
  applyThreadId(terminalThreadId, baseMessages);
871
1130
  }
872
- const finalText = String(payload.assistant_output_text || textFromFinalOutput(payload.final_output) || streamingContentRef.current || "");
1131
+ const finalText = String(payload.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks || []) || streamingContentRef.current || "");
873
1132
  finalizeAssistantMessage({
874
1133
  threadId: streamThreadIdRef.current || activeThreadIdRef.current,
875
1134
  baseMessages: messagesRef.current,
@@ -884,7 +1143,7 @@ function useAgents24ChatController({
884
1143
  },
885
1144
  [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
886
1145
  );
887
- const runStream = (0, import_react.useCallback)(
1146
+ const runStream = (0, import_react2.useCallback)(
888
1147
  async (input) => {
889
1148
  const startedAt = Date.now();
890
1149
  const controller = new AbortController();
@@ -943,19 +1202,28 @@ function useAgents24ChatController({
943
1202
  );
944
1203
  }
945
1204
  try {
1205
+ let streamResult = null;
946
1206
  if (input.mode === "attach") {
947
1207
  setActiveRunIdValue(input.runId);
948
1208
  reattachedRunIdRef.current = input.runId;
949
- await transport.attachRun(
1209
+ streamResult = await transport.attachRun(
950
1210
  { runId: input.runId, signal: controller.signal },
951
1211
  (event) => handleStreamEvent({ mode: "attach", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
952
1212
  );
953
1213
  } else {
954
- await transport.streamMessage(
1214
+ streamResult = await transport.streamMessage(
955
1215
  { ...input.message, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
956
1216
  (event) => handleStreamEvent({ mode: "submit", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
957
1217
  );
958
1218
  }
1219
+ if (streamResult?.runId) setActiveRunIdValue(streamResult.runId);
1220
+ if (streamResult?.threadId) {
1221
+ streamThreadIdRef.current = streamResult.threadId;
1222
+ applyThreadId(streamResult.threadId, baseMessages);
1223
+ if (streamResult.runId) {
1224
+ markThreadRunStatus(streamResult.threadId, streamResult.runId, "running");
1225
+ }
1226
+ }
959
1227
  if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
960
1228
  finalizeAssistantMessage({
961
1229
  threadId: streamThreadIdRef.current,
@@ -969,7 +1237,7 @@ function useAgents24ChatController({
969
1237
  }
970
1238
  await refresh().catch(() => void 0);
971
1239
  } catch (error) {
972
- if (error.name !== "AbortError") {
1240
+ if (!isAbortError(error)) {
973
1241
  finalizeAssistantMessage({
974
1242
  threadId: streamThreadIdRef.current,
975
1243
  baseMessages: messagesRef.current,
@@ -998,18 +1266,22 @@ function useAgents24ChatController({
998
1266
  },
999
1267
  [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
1000
1268
  );
1001
- const handleSubmit = (0, import_react.useCallback)(
1269
+ const handleSubmit = (0, import_react2.useCallback)(
1002
1270
  async (message) => {
1003
1271
  if (!message.text.trim() && !(message.files || []).length) return;
1004
1272
  await runStream({ mode: "submit", message });
1005
1273
  },
1006
1274
  [runStream]
1007
1275
  );
1008
- const handleStop = (0, import_react.useCallback)(() => {
1276
+ const attachRun = (0, import_react2.useCallback)(async (runId, threadId) => {
1277
+ const resolvedThreadId = threadId ?? activeThreadIdRef.current;
1278
+ if (runId && resolvedThreadId) await runStream({ mode: "attach", runId, threadId: resolvedThreadId });
1279
+ }, [runStream]);
1280
+ const handleStop = (0, import_react2.useCallback)(() => {
1009
1281
  const runId = activeRunIdRef.current;
1010
1282
  const partial = streamingContentRef.current;
1011
1283
  const liveMessageId = streamingMessageIdRef.current;
1012
- abortControllerRef.current?.abort();
1284
+ abortDetachedStream(abortControllerRef.current);
1013
1285
  abortControllerRef.current = null;
1014
1286
  setActiveRunIdValue(null);
1015
1287
  streamingMessageIdRef.current = null;
@@ -1029,16 +1301,56 @@ function useAgents24ChatController({
1029
1301
  });
1030
1302
  }
1031
1303
  }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
1032
- (0, import_react.useEffect)(() => {
1033
- refresh().catch(() => {
1034
- storage.setThreads([]);
1035
- setThreads([]);
1304
+ (0, import_react2.useEffect)(() => {
1305
+ refresh().catch((error) => {
1306
+ if (!isAbortError(error) && !lifecycleAbortController.signal.aborted) {
1307
+ setThreads(storage.listThreads());
1308
+ }
1036
1309
  });
1037
- }, [refresh, storage]);
1038
- (0, import_react.useEffect)(() => {
1310
+ }, [lifecycleAbortController, refresh, storage]);
1311
+ (0, import_react2.useEffect)(() => {
1312
+ return () => {
1313
+ lifecycleAbortController.abort();
1314
+ abortDetachedStream(abortControllerRef.current);
1315
+ abortControllerRef.current = null;
1316
+ };
1317
+ }, [lifecycleAbortController]);
1318
+ (0, import_react2.useEffect)(() => {
1319
+ if (!transport.subscribeThreadEvents) return;
1320
+ let cancelled = false;
1321
+ let retryTimeout = null;
1322
+ let controller = null;
1323
+ const connect = () => {
1324
+ if (cancelled) return;
1325
+ controller = new AbortController();
1326
+ transport.subscribeThreadEvents?.(
1327
+ { cursor: threadEventsCursorRef.current, signal: controller.signal },
1328
+ async (event) => {
1329
+ if (typeof event.cursor === "number") threadEventsCursorRef.current = event.cursor;
1330
+ if (event.event === "snapshot_required") {
1331
+ await refresh().catch(() => void 0);
1332
+ return;
1333
+ }
1334
+ const next = applyThreadSummaryEvent(storage.listThreads(), event);
1335
+ storage.setThreads(next);
1336
+ setThreads(storage.listThreads());
1337
+ }
1338
+ ).catch((error) => {
1339
+ if (cancelled || isAbortError(error)) return;
1340
+ retryTimeout = setTimeout(connect, 1500);
1341
+ });
1342
+ };
1343
+ connect();
1344
+ return () => {
1345
+ cancelled = true;
1346
+ if (retryTimeout) clearTimeout(retryTimeout);
1347
+ controller?.abort();
1348
+ };
1349
+ }, [refresh, storage, transport]);
1350
+ (0, import_react2.useEffect)(() => {
1039
1351
  syncThreadsFromStorage();
1040
1352
  }, [storageKey, syncThreadsFromStorage]);
1041
- (0, import_react.useEffect)(() => {
1353
+ (0, import_react2.useEffect)(() => {
1042
1354
  const previous = activeThreadIdRef.current;
1043
1355
  activeThreadIdRef.current = activeThreadId;
1044
1356
  if (activeThreadId && previous === activeThreadId && (activeRunIdRef.current || streamingMessageIdRef.current)) {
@@ -1048,6 +1360,9 @@ function useAgents24ChatController({
1048
1360
  detachActiveStream();
1049
1361
  }
1050
1362
  if (!activeThreadId) {
1363
+ if (previous === null && loadedThreadIdRef.current === null && messagesRef.current.length === 0 && !hasOlderTurnsRef.current && !isLoadingHistoryRef.current && !isLoadingOlderRef.current && nextBeforeTurnIndexRef.current === null) {
1364
+ return;
1365
+ }
1051
1366
  requestSeqRef.current += 1;
1052
1367
  loadedThreadIdRef.current = null;
1053
1368
  setMessages([]);
@@ -1079,14 +1394,14 @@ function useAgents24ChatController({
1079
1394
  }
1080
1395
  void loadThread(activeThreadId).catch(() => setLoadingHistory(false));
1081
1396
  }, [activeThreadId, detachActiveStream, loadThread, setLoadingHistory, storage]);
1082
- (0, import_react.useEffect)(() => {
1397
+ (0, import_react2.useEffect)(() => {
1083
1398
  const threadId = activeThreadId;
1084
1399
  if (!threadId || isLoadingHistory || loadedThreadIdRef.current !== threadId || activeRunIdRef.current) return;
1085
- const runId = activeRunIdFromThread(storage.getThread(threadId));
1400
+ const runId = autoAttachRunIdFromThread(storage.getThread(threadId));
1086
1401
  if (!runId || reattachedRunIdRef.current === runId) return;
1087
1402
  void runStream({ mode: "attach", threadId, runId });
1088
1403
  }, [activeThreadId, isLoadingHistory, runStream, storage, storageKey]);
1089
- (0, import_react.useEffect)(() => {
1404
+ (0, import_react2.useEffect)(() => {
1090
1405
  const threadId = activeThreadId;
1091
1406
  if (!threadId || isLoadingHistoryRef.current || activeRunIdRef.current || streamingMessageIdRef.current) {
1092
1407
  return;
@@ -1095,87 +1410,58 @@ function useAgents24ChatController({
1095
1410
  if (!hasStaleUnfinishedAssistantCache(cached)) return;
1096
1411
  void loadThread(threadId).catch(() => setLoadingHistory(false));
1097
1412
  }, [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)(
1413
+ const {
1414
+ handleCopy,
1415
+ handleDislike,
1416
+ handleLike,
1417
+ handleRetry,
1418
+ startNewThread,
1419
+ upsertLiveVoiceMessage
1420
+ } = useControllerMessageActions({
1421
+ activeThreadIdRef,
1422
+ createId,
1423
+ detachActiveStream,
1424
+ disliked,
1425
+ handleSubmit,
1426
+ hasOlderTurnsRef,
1427
+ liked,
1428
+ liveVoiceIdsRef,
1429
+ loadedThreadIdRef,
1430
+ messagesRef,
1431
+ nextBeforeTurnIndexRef,
1432
+ onActiveThreadIdChange,
1433
+ persistThread,
1434
+ requestSeqRef,
1435
+ setContextStatus,
1436
+ setCopiedMessageId,
1437
+ setDisliked,
1438
+ setHasOlderTurns,
1439
+ setIsLoadingOlder,
1440
+ setLiked,
1441
+ setLoadingHistory,
1442
+ setMessages,
1443
+ storage
1444
+ });
1445
+ const loadThreadById = (0, import_react2.useCallback)(
1165
1446
  async (threadId) => {
1166
1447
  if (!threadId) return;
1448
+ setIsSelectingThread(true);
1167
1449
  if (activeThreadIdRef.current !== threadId) {
1168
1450
  if (abortControllerRef.current) detachActiveStream();
1169
1451
  activeThreadIdRef.current = threadId;
1170
1452
  storage.setActiveThreadId?.(threadId);
1171
1453
  onActiveThreadIdChange?.(threadId);
1172
1454
  }
1173
- await loadThread(threadId);
1455
+ try {
1456
+ await loadThread(threadId);
1457
+ } finally {
1458
+ if (activeThreadIdRef.current === threadId) setIsSelectingThread(false);
1459
+ }
1174
1460
  },
1175
1461
  [detachActiveStream, loadThread, onActiveThreadIdChange, storage]
1176
1462
  );
1177
1463
  const activeThread = activeThreadId ? storage.getThread(activeThreadId) || threads.find((thread) => thread.id === activeThreadId) || null : null;
1178
- return (0, import_react.useMemo)(() => ({
1464
+ return (0, import_react2.useMemo)(() => ({
1179
1465
  threads,
1180
1466
  activeThreadId,
1181
1467
  activeThread,
@@ -1188,6 +1474,7 @@ function useAgents24ChatController({
1188
1474
  isLoadingHistory,
1189
1475
  isLoadingOlder,
1190
1476
  isRefreshingThreads,
1477
+ isSelectingThread,
1191
1478
  hasOlderTurns,
1192
1479
  liked,
1193
1480
  disliked,
@@ -1195,6 +1482,7 @@ function useAgents24ChatController({
1195
1482
  lastThinkingDurationMs,
1196
1483
  activeRunId,
1197
1484
  handleSubmit,
1485
+ attachRun,
1198
1486
  handleStop,
1199
1487
  handleCopy,
1200
1488
  handleLike,
@@ -1212,6 +1500,7 @@ function useAgents24ChatController({
1212
1500
  activeRunId,
1213
1501
  activeThread,
1214
1502
  activeThreadId,
1503
+ attachRun,
1215
1504
  contextStatus,
1216
1505
  currentReasoning,
1217
1506
  disliked,
@@ -1226,6 +1515,7 @@ function useAgents24ChatController({
1226
1515
  isLoadingHistory,
1227
1516
  isLoadingOlder,
1228
1517
  isRefreshingThreads,
1518
+ isSelectingThread,
1229
1519
  lastThinkingDurationMs,
1230
1520
  liked,
1231
1521
  loadThreadById,
@@ -1455,6 +1745,7 @@ function LatestThreadScrollerOutline({
1455
1745
  );
1456
1746
  };
1457
1747
  updateSideRoom();
1748
+ if (typeof ResizeObserver === "undefined") return;
1458
1749
  const resizeObserver = new ResizeObserver(updateSideRoom);
1459
1750
  resizeObserver.observe(root);
1460
1751
  const content = layout?.contentElement;
@@ -1613,13 +1904,16 @@ var DefaultChatPart = ({
1613
1904
  if (part.kind === "ui-blocks") {
1614
1905
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-ui-blocks-part": true, "data-state": part.state });
1615
1906
  }
1616
- if (part.kind === "approval") {
1617
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-approval-part": true });
1907
+ if (part.kind === "hitl") {
1908
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-hitl-part": part.interruptId || "" });
1618
1909
  }
1619
1910
  if (part.kind === "error") {
1620
1911
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-error-part": true, children: part.errorText });
1621
1912
  }
1622
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-data-part": part.name });
1913
+ if (part.kind === "data") {
1914
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-agents24-data-part": part.name });
1915
+ }
1916
+ return null;
1623
1917
  };
1624
1918
  var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1625
1919
  DefaultChatPart,
@@ -1632,12 +1926,17 @@ var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ (0, impor
1632
1926
  );
1633
1927
 
1634
1928
  // src/sse.ts
1929
+ var isAbortError2 = (error) => {
1930
+ if (!error || typeof error !== "object") return false;
1931
+ const maybe = error;
1932
+ return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
1933
+ };
1635
1934
  var parseSseBlock = (block) => {
1636
1935
  const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.replace(/^data:\s?/, "")).join("\n").trim();
1637
1936
  if (!data) return null;
1638
1937
  return JSON.parse(data);
1639
1938
  };
1640
- var consumeSseResponse = async (response, onEvent) => {
1939
+ var consumeSseResponse = async (response, onEvent, options = {}) => {
1641
1940
  if (!response.ok) {
1642
1941
  let message = response.statusText || "Failed to open chat stream.";
1643
1942
  try {
@@ -1651,30 +1950,49 @@ var consumeSseResponse = async (response, onEvent) => {
1651
1950
  if (!reader) throw new Error("The chat stream did not return a readable body.");
1652
1951
  const decoder = new TextDecoder();
1653
1952
  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);
1953
+ let threadId = response.headers.get("X-Thread-ID") || null;
1954
+ let runId = response.headers.get("X-Run-ID") || null;
1955
+ let closed = false;
1956
+ const closeReader = () => {
1957
+ closed = true;
1958
+ queueMicrotask(() => {
1959
+ void reader.cancel().catch(() => {
1960
+ });
1961
+ });
1962
+ };
1963
+ if (options.signal?.aborted) {
1964
+ closeReader();
1965
+ return { threadId, runId };
1966
+ }
1967
+ options.signal?.addEventListener("abort", closeReader, { once: true });
1968
+ try {
1969
+ while (!closed) {
1970
+ const { value, done } = await reader.read();
1971
+ if (done) break;
1972
+ buffer += decoder.decode(value, { stream: true });
1973
+ let boundary = buffer.indexOf("\n\n");
1974
+ while (boundary !== -1) {
1975
+ const block = buffer.slice(0, boundary);
1976
+ buffer = buffer.slice(boundary + 2);
1977
+ boundary = buffer.indexOf("\n\n");
1978
+ const event = parseSseBlock(block);
1979
+ if (!event) continue;
1980
+ if (event.run_id) runId = event.run_id;
1981
+ const payloadThreadId = event.payload?.thread_id;
1982
+ if (payloadThreadId) threadId = String(payloadThreadId);
1983
+ await onEvent(event);
1984
+ }
1671
1985
  }
1986
+ } catch (error) {
1987
+ if (!isAbortError2(error)) throw error;
1988
+ } finally {
1989
+ options.signal?.removeEventListener("abort", closeReader);
1672
1990
  }
1673
1991
  return { threadId, runId };
1674
1992
  };
1675
1993
 
1676
1994
  // src/streaming-text.ts
1677
- var import_react2 = require("react");
1995
+ var import_react3 = require("react");
1678
1996
  var DEFAULT_COMPLETED_TEXT_CACHE_SIZE = 200;
1679
1997
  var defaultCompletedTextCache = /* @__PURE__ */ new Map();
1680
1998
  var defaultStreamingTextCache = {
@@ -1702,24 +2020,24 @@ function useStreamingText({
1702
2020
  maxCatchupChars = 20
1703
2021
  }) {
1704
2022
  const cacheAdapter = cache === false ? null : cache;
1705
- const [displayedText, setDisplayedText] = (0, import_react2.useState)(() => {
2023
+ const [displayedText, setDisplayedText] = (0, import_react3.useState)(() => {
1706
2024
  const cachedText = cacheAdapter?.get(id);
1707
2025
  return isStreaming && cachedText !== text ? "" : text;
1708
2026
  });
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)(() => {
2027
+ const targetRef = (0, import_react3.useRef)(text);
2028
+ const displayedRef = (0, import_react3.useRef)(displayedText);
2029
+ const rafRef = (0, import_react3.useRef)(null);
2030
+ const lastFrameAtRef = (0, import_react3.useRef)(null);
2031
+ const idRef = (0, import_react3.useRef)(id);
2032
+ const shouldAnimateRef = (0, import_react3.useRef)(isStreaming);
2033
+ (0, import_react3.useEffect)(() => {
1716
2034
  if (isStreaming || !text) return;
1717
2035
  cacheAdapter?.set(id, text);
1718
2036
  }, [cacheAdapter, id, isStreaming, text]);
1719
- (0, import_react2.useEffect)(() => {
2037
+ (0, import_react3.useEffect)(() => {
1720
2038
  targetRef.current = text;
1721
2039
  }, [text]);
1722
- (0, import_react2.useEffect)(() => {
2040
+ (0, import_react3.useEffect)(() => {
1723
2041
  if (idRef.current === id) return;
1724
2042
  idRef.current = id;
1725
2043
  const cachedText = cacheAdapter?.get(id);
@@ -1733,7 +2051,7 @@ function useStreamingText({
1733
2051
  }
1734
2052
  lastFrameAtRef.current = null;
1735
2053
  }, [cacheAdapter, id, isStreaming, text]);
1736
- (0, import_react2.useEffect)(() => {
2054
+ (0, import_react3.useEffect)(() => {
1737
2055
  if (typeof window === "undefined") {
1738
2056
  displayedRef.current = text;
1739
2057
  setDisplayedText(text);
@@ -1818,6 +2136,14 @@ var streamHeaders = (headers) => ({
1818
2136
  Accept: "text/event-stream",
1819
2137
  "Content-Type": "application/json"
1820
2138
  });
2139
+ var ChatTransportError = class extends Error {
2140
+ constructor(message, status) {
2141
+ super(message);
2142
+ this.name = "ChatTransportError";
2143
+ this.status = status;
2144
+ }
2145
+ };
2146
+ var transportError = (response, fallback) => new ChatTransportError(response.statusText || fallback, response.status);
1821
2147
  var createFetchChatTransport = ({
1822
2148
  routes,
1823
2149
  fetchImpl = fetch,
@@ -1831,7 +2157,7 @@ var createFetchChatTransport = ({
1831
2157
  signal: input?.signal,
1832
2158
  headers: jsonHeaders(await loadHeaders())
1833
2159
  });
1834
- if (!response.ok) throw new Error(response.statusText || "Failed to list chat threads.");
2160
+ if (!response.ok) throw transportError(response, "Failed to list chat threads.");
1835
2161
  return response.json();
1836
2162
  },
1837
2163
  async getThread(input) {
@@ -1839,7 +2165,7 @@ var createFetchChatTransport = ({
1839
2165
  signal: input.signal,
1840
2166
  headers: jsonHeaders(await loadHeaders())
1841
2167
  });
1842
- if (!response.ok) throw new Error(response.statusText || "Failed to load chat thread.");
2168
+ if (!response.ok) throw transportError(response, "Failed to load chat thread.");
1843
2169
  return response.json();
1844
2170
  },
1845
2171
  async streamMessage(input, onEvent) {
@@ -1855,7 +2181,7 @@ var createFetchChatTransport = ({
1855
2181
  }
1856
2182
  )
1857
2183
  });
1858
- return consumeSseResponse(response, onEvent);
2184
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
1859
2185
  },
1860
2186
  async attachRun(input, onEvent) {
1861
2187
  const response = await fetchImpl(routes.attachRun(input), {
@@ -1864,7 +2190,7 @@ var createFetchChatTransport = ({
1864
2190
  headers: streamHeaders(await loadHeaders()),
1865
2191
  body: JSON.stringify({})
1866
2192
  });
1867
- return consumeSseResponse(response, onEvent);
2193
+ return consumeSseResponse(response, onEvent, { signal: input.signal });
1868
2194
  },
1869
2195
  async cancelRun(input) {
1870
2196
  const response = await fetchImpl(routes.cancelRun(input), {
@@ -1872,7 +2198,7 @@ var createFetchChatTransport = ({
1872
2198
  headers: jsonHeaders(await loadHeaders()),
1873
2199
  body: JSON.stringify({ assistant_output_text: input.assistantOutputText || void 0 })
1874
2200
  });
1875
- if (!response.ok) throw new Error(response.statusText || "Failed to cancel chat run.");
2201
+ if (!response.ok) throw transportError(response, "Failed to cancel chat run.");
1876
2202
  return response.json();
1877
2203
  },
1878
2204
  async deleteThread(input) {
@@ -1881,7 +2207,50 @@ var createFetchChatTransport = ({
1881
2207
  method: "DELETE",
1882
2208
  headers: jsonHeaders(await loadHeaders())
1883
2209
  });
1884
- if (!response.ok) throw new Error(response.statusText || "Failed to delete chat thread.");
2210
+ if (!response.ok) throw transportError(response, "Failed to delete chat thread.");
2211
+ return response.json();
2212
+ },
2213
+ async subscribeThreadEvents(input, onEvent) {
2214
+ if (!routes.threadEvents) return;
2215
+ const response = await fetchImpl(routes.threadEvents({ cursor: input.cursor }), {
2216
+ method: "GET",
2217
+ signal: input.signal,
2218
+ headers: streamHeaders(await loadHeaders())
2219
+ });
2220
+ await consumeSseResponse(
2221
+ response,
2222
+ (event) => onEvent(event),
2223
+ { signal: input.signal }
2224
+ );
2225
+ },
2226
+ async resumeHitl(input) {
2227
+ if (!routes.resumeRun) throw new ChatTransportError("HITL resume is not configured.", 501);
2228
+ const response = await fetchImpl(routes.resumeRun({ runId: input.runId }), {
2229
+ method: "POST",
2230
+ headers: jsonHeaders(await loadHeaders()),
2231
+ body: JSON.stringify({
2232
+ schema_version: "agents24.hitl.resume.v1",
2233
+ interrupt_id: input.interruptId,
2234
+ decisions: input.decisions,
2235
+ client: input.client
2236
+ })
2237
+ });
2238
+ if (!response.ok) throw transportError(response, "Failed to resume chat run.");
2239
+ return response.json();
2240
+ },
2241
+ async startMcpAuth(input) {
2242
+ if (!routes.startMcpAuth) throw new ChatTransportError("MCP auth is not configured.", 501);
2243
+ const response = await fetchImpl(routes.startMcpAuth({ runId: input.runId, serverId: input.serverId }), {
2244
+ method: "POST",
2245
+ headers: jsonHeaders(await loadHeaders()),
2246
+ body: JSON.stringify({
2247
+ interrupt_id: input.interruptId || void 0,
2248
+ principal_type: input.principalType || void 0,
2249
+ principal_id: input.principalId || void 0,
2250
+ client: input.client
2251
+ })
2252
+ });
2253
+ if (!response.ok) throw transportError(response, "Failed to start MCP authorization.");
1885
2254
  return response.json();
1886
2255
  }
1887
2256
  };
@@ -1891,6 +2260,7 @@ var createFetchChatTransport = ({
1891
2260
  var import_message_scroller2 = require("@shadcn/react/message-scroller");
1892
2261
  // Annotate the CommonJS export names for ESM import in node:
1893
2262
  0 && (module.exports = {
2263
+ ChatTransportError,
1894
2264
  DEFAULT_THREAD_PAGE_SIZE,
1895
2265
  DefaultChatPart,
1896
2266
  DefaultToolPart,
@@ -1905,33 +2275,38 @@ var import_message_scroller2 = require("@shadcn/react/message-scroller");
1905
2275
  MessageScroller,
1906
2276
  activeRunIdFromThread,
1907
2277
  activeRunIdFromThreadDetail,
2278
+ appendStableStreamingTurn,
1908
2279
  assistantTextFromParts,
1909
2280
  assistantTextFromResponseBlocks,
2281
+ autoAttachRunIdFromThread,
1910
2282
  compressionFromContextWindow,
1911
2283
  consumeSseResponse,
1912
2284
  createChatId,
1913
2285
  createFetchChatTransport,
2286
+ findStableAssistantMessageIndex,
1914
2287
  getActiveStreamingTextPartId,
1915
2288
  hasStaleUnfinishedAssistantCache,
1916
2289
  hasUnfinishedAssistantMessage,
1917
2290
  isActiveStreamingTextPart,
2291
+ isAutoAttachThreadStatus,
1918
2292
  isRunningThreadStatus,
1919
2293
  latestContextWindowFromThread,
1920
2294
  mergeContextWindow,
1921
2295
  mergeContextWindowUpdate,
1922
2296
  mergeReasoningSteps,
2297
+ mergeStoredThreadsForRefresh,
1923
2298
  normalizeContextCompression,
1924
2299
  normalizeContextWindow,
1925
2300
  parseSseBlock,
1926
2301
  partsFromResponseBlocks,
1927
2302
  reasoningStepsFromParts,
1928
2303
  renderChatPart,
1929
- textFromFinalOutput,
1930
2304
  threadActivityDate,
1931
2305
  threadDetailToMessages,
1932
2306
  threadPaging,
1933
2307
  titleFromMessage,
1934
2308
  toolStateFromStatus,
2309
+ upsertStableAssistantMessage,
1935
2310
  useAgents24ChatController,
1936
2311
  useMessageScroller,
1937
2312
  useMessageScrollerScrollable,