@agents24/react 0.2.7 → 0.2.9

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
@@ -37,6 +37,24 @@ function getThreadActivity(thread) {
37
37
  };
38
38
  }
39
39
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
40
+ function computerResourceTargets(value) {
41
+ const output = record(value);
42
+ if (!output) return [];
43
+ const candidates = [
44
+ output.resource_target,
45
+ ...Array.isArray(output.resource_targets) ? output.resource_targets : [],
46
+ ...Array.isArray(output.files) ? output.files.map((file) => record(file)?.resource_target) : []
47
+ ];
48
+ const seen = /* @__PURE__ */ new Set();
49
+ return candidates.flatMap((candidate) => {
50
+ const target = record(candidate);
51
+ if (target?.type !== "computer" || typeof target.thread_id !== "string" || typeof target.computer_id !== "string" || typeof target.path !== "string" || !target.path.startsWith("/workspace") || target.kind !== "file" && target.kind !== "directory") return [];
52
+ const key = `${target.thread_id}:${target.computer_id}:${target.path}:${target.kind}`;
53
+ if (seen.has(key)) return [];
54
+ seen.add(key);
55
+ return [target];
56
+ });
57
+ }
40
58
  var text = (value) => typeof value === "string" && value.trim() ? value : null;
41
59
  var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
42
60
  var statusToToolState = (value) => {
@@ -200,6 +218,7 @@ function partsFromResponseBlocks(blocks, fallbackText = "", fallbackPartId = "as
200
218
  state: statusToToolState(block.status),
201
219
  input: tool.input,
202
220
  output: tool.output,
221
+ resourceTargets: computerResourceTargets(tool.output),
203
222
  errorText: text(tool.error || block.error),
204
223
  failure: failureView(tool.failure),
205
224
  presentation: toolPresentation(tool)
@@ -369,6 +388,24 @@ function threadDetailToMessages(detail) {
369
388
  return output;
370
389
  });
371
390
  }
391
+ function threadTimelineToMessages(items, detail) {
392
+ const feedbackByRun = new Map((detail?.turns || []).flatMap((turn) => turn.run_id && turn.feedback ? [[turn.run_id, turn.feedback]] : []));
393
+ return items.map((item) => ({
394
+ id: item.id,
395
+ role: item.role,
396
+ runId: item.run_id,
397
+ segmentIndex: item.segment_index,
398
+ content: item.text,
399
+ createdAt: item.created_at,
400
+ isFinal: item.status === "completed" || item.status === "delivered",
401
+ parts: item.role === "assistant" ? partsFromResponseBlocks(item.response_blocks, item.text, `${item.id}-text`) : item.text ? [{ id: `${item.id}-text`, type: "text", kind: "text", text: item.text }] : [],
402
+ attachments: item.role === "user" ? item.attachment_ids.map((id) => {
403
+ const descriptor = item.attachments?.find((attachment) => attachment.id === id);
404
+ return { id, filename: descriptor?.filename || "Attachment", mediaType: descriptor?.mime_type || "application/octet-stream" };
405
+ }) : void 0,
406
+ feedback: item.role === "assistant" && item.run_id ? feedbackByRun.get(item.run_id) : void 0
407
+ }));
408
+ }
372
409
  function latestThreadContext(detail) {
373
410
  let contextWindow = null;
374
411
  for (const turn of detail.turns || []) {
@@ -415,6 +452,54 @@ function useAgentClient() {
415
452
  return useAgents24Runtime().client;
416
453
  }
417
454
 
455
+ // src/runtime-event-buffer.ts
456
+ function runtimeEventFrameScheduler() {
457
+ const runtime = globalThis;
458
+ if (typeof runtime.requestAnimationFrame !== "function") return void 0;
459
+ return (callback) => {
460
+ const handle = runtime.requestAnimationFrame?.(callback);
461
+ return () => {
462
+ if (handle !== void 0) runtime.cancelAnimationFrame?.(handle);
463
+ };
464
+ };
465
+ }
466
+ function createRuntimeEventFrameBuffer(apply, scheduleFrame) {
467
+ let pending = [];
468
+ let cancelFrame = null;
469
+ const flush = () => {
470
+ cancelFrame?.();
471
+ cancelFrame = null;
472
+ if (!pending.length) return;
473
+ const events = pending;
474
+ pending = [];
475
+ apply(events);
476
+ };
477
+ return {
478
+ dispose() {
479
+ cancelFrame?.();
480
+ cancelFrame = null;
481
+ pending = [];
482
+ },
483
+ flush,
484
+ push(event) {
485
+ if (event.event !== "assistant.delta" || !scheduleFrame) {
486
+ pending.push(event);
487
+ flush();
488
+ return;
489
+ }
490
+ pending.push(event);
491
+ if (cancelFrame) return;
492
+ cancelFrame = scheduleFrame(() => {
493
+ cancelFrame = null;
494
+ if (!pending.length) return;
495
+ const events = pending;
496
+ pending = [];
497
+ apply(events);
498
+ });
499
+ }
500
+ };
501
+ }
502
+
418
503
  // src/state.ts
419
504
  import {
420
505
  compressionFromContextWindow as compressionFromContextWindow2,
@@ -478,10 +563,18 @@ function clearAgentChatBackgroundError(state) {
478
563
  return state.backgroundError ? { ...state, backgroundError: null } : state;
479
564
  }
480
565
  function reconcileRefreshedThreads(current, incoming, activeThreadId, concurrentEvents = []) {
566
+ const currentById = new Map(current.map((thread) => [thread.id, thread]));
567
+ const newest = incoming.map((thread) => preferNewestThreadSummary(currentById.get(thread.id), thread));
481
568
  const active = activeThreadId && !incoming.some((thread) => thread.id === activeThreadId) ? current.find((thread) => thread.id === activeThreadId) : null;
482
- const base = active?.isHydrated ? [active, ...incoming] : incoming;
569
+ const base = active?.isHydrated ? [active, ...newest] : newest;
483
570
  return concurrentEvents.reduce(applyThreadSummaryEvent, base);
484
571
  }
572
+ function preferNewestThreadSummary(current, incoming) {
573
+ if (!current?.updated_at || !incoming.updated_at) return incoming;
574
+ const currentTime = Date.parse(current.updated_at);
575
+ const incomingTime = Date.parse(incoming.updated_at);
576
+ return Number.isFinite(currentTime) && Number.isFinite(incomingTime) && currentTime > incomingTime ? current : incoming;
577
+ }
485
578
  function adoptStreamingThread(state, threadId) {
486
579
  const normalizedThreadId = threadId.trim();
487
580
  if (!normalizedThreadId || state.activeThreadId) return state;
@@ -507,13 +600,36 @@ function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
507
600
  ))) next.push(assistantMessage);
508
601
  return next;
509
602
  }
603
+ function preserveMountedMessageIds(canonical, current) {
604
+ return canonical.map((message) => {
605
+ const mounted = current.find((item) => item.role === message.role && (item.durableId === message.id || item.id === message.id || message.role === "assistant" && message.segmentIndex === 1 && item.runId && item.runId === message.runId && !item.durableId));
606
+ return mounted && mounted.id !== message.id ? { ...message, id: mounted.id, durableId: message.id } : message;
607
+ });
608
+ }
609
+ function reconcileThreadTimeline(current, canonical, activeRunId, streamingMessageId) {
610
+ const mounted = preserveMountedMessageIds(canonical, current);
611
+ const canonicalIds = new Set(mounted.map((message) => message.id));
612
+ const retained = current.filter((message) => !canonicalIds.has(message.id) && (message.isFinal || activeRunId && (!message.runId || message.runId === activeRunId)));
613
+ const reconciled = mounted.map((message) => {
614
+ const live = current.find((item) => item.id === message.id);
615
+ return activeRunId && live?.role === "assistant" && live.runId === activeRunId && (live.id === streamingMessageId || !live.isFinal) ? { ...message, ...live, durableId: message.durableId || live.durableId } : message;
616
+ });
617
+ return [...retained, ...reconciled];
618
+ }
619
+ function promoteThreadMessage(current, message, runId) {
620
+ const promoted = { ...message, runId, isFinal: true };
621
+ const index = current.findIndex((item) => item.id === message.id || item.durableId === message.id);
622
+ if (index < 0) return [...current, promoted];
623
+ return current.map((item, itemIndex) => itemIndex === index ? { ...item, ...promoted, id: item.id, durableId: item.id === message.id ? item.durableId : message.id } : item);
624
+ }
510
625
  function upsertStableAssistantMessage(messages, input) {
511
626
  const next = [...messages];
512
627
  for (const baseMessage of input.baseMessages || []) {
513
- const isTarget = baseMessage.role === "assistant" && (Boolean(input.messageId && baseMessage.id === input.messageId) || Boolean(input.runId && baseMessage.runId === input.runId));
628
+ const isTarget = baseMessage.role === "assistant" && (Boolean(input.messageId && baseMessage.id === input.messageId) || Boolean(!input.strictMessageId && input.runId && baseMessage.runId === input.runId));
514
629
  if (!isTarget && !next.some((message) => message.id === baseMessage.id)) next.push(baseMessage);
515
630
  }
516
- const index = next.findIndex((message) => message.role === "assistant" && (Boolean(input.messageId && message.id === input.messageId) || Boolean(input.runId && message.runId === input.runId)));
631
+ const exactIndex = next.findIndex((message) => message.role === "assistant" && Boolean(input.messageId && message.id === input.messageId));
632
+ const index = exactIndex >= 0 ? exactIndex : input.strictMessageId ? -1 : next.findIndex((message) => message.role === "assistant" && Boolean(input.runId && message.runId === input.runId));
517
633
  if (index === -1) next.push(input.create());
518
634
  else next[index] = input.update(next[index]);
519
635
  return next;
@@ -556,24 +672,41 @@ function reconcileModelTurnParts(parts, event) {
556
672
  return active ? upsertResponseBlockParts(next, [active]) : next;
557
673
  }
558
674
  function applyRuntimeEvent(state, event) {
675
+ if (event.event === "run.accepted" && typeof event.payload?.message_id === "string") {
676
+ const messageId2 = event.payload.message_id;
677
+ let optimisticIndex = -1;
678
+ for (let index = state.messages.length - 1; index >= 0; index -= 1) {
679
+ const message = state.messages[index];
680
+ if (message.role === "user" && !message.runId && !message.durableId) {
681
+ optimisticIndex = index;
682
+ break;
683
+ }
684
+ }
685
+ if (optimisticIndex >= 0) {
686
+ state = { ...state, messages: state.messages.map((message, index) => index === optimisticIndex ? { ...message, durableId: messageId2, runId: event.run_id } : message.role === "assistant" && !message.runId ? { ...message, runId: event.run_id } : message) };
687
+ }
688
+ }
559
689
  const projection = assistantProjectionFromEvent(event);
560
690
  const eventName = String(event.event || "");
561
- const runId = event.run_id || state.activeRunId;
562
- const messageId = state.streamingMessageId || `${runId || "run"}-assistant`;
691
+ const runId = state.activeRunId && event.event !== "run.accepted" ? state.activeRunId : event.run_id || state.activeRunId;
692
+ const segmentId = typeof event.payload?.segment_id === "string" ? event.payload.segment_id : null;
693
+ const distinctSegment = Boolean(segmentId && segmentId !== `${runId}:0`);
694
+ const messageId = distinctSegment ? `${runId || "run"}-assistant-${segmentId}` : state.streamingMessageId || `${runId || "run"}-assistant`;
563
695
  const terminal = eventName === "run.completed" || eventName === "run.failed" || eventName === "run.cancelled";
564
696
  const paused = eventName === "run.paused" || projection.parts.some((part) => part.kind === "hitl" && part.status === "pending");
565
697
  const suspension = eventName === "run.waiting" ? "waiting" : eventName === "run.interrupted" ? "interrupted" : null;
566
698
  const cancelling = eventName === "run.cancelling" || String(event.payload?.status || "") === "cancelling";
567
- const existing = state.messages.find((message) => message.role === "assistant" && (message.id === messageId || Boolean(runId && message.runId === runId)));
699
+ const existing = state.messages.find((message) => message.role === "assistant" && message.id === messageId) || (!distinctSegment ? state.messages.find((message) => message.role === "assistant" && Boolean(runId && message.runId === runId)) : void 0);
568
700
  const payload = event.payload || {};
569
701
  const eventThreadId = typeof payload.thread_id === "string" && payload.thread_id.trim() ? payload.thread_id.trim() : null;
570
702
  const contextApplies = (!state.activeThreadId || !eventThreadId || eventThreadId === state.activeThreadId) && (!state.activeRunId || !runId || runId === state.activeRunId);
571
- const contextWindow = contextApplies ? mergeContextWindowUpdate2(state.contextWindow, payload.context_window) : state.contextWindow;
703
+ const contextPayload = payload.context_window && typeof payload.context_window === "object" ? { ...payload.context_window, run_id: runId } : payload.context_window;
704
+ const contextWindow = contextApplies ? mergeContextWindowUpdate2(state.contextWindow, contextPayload) : state.contextWindow;
572
705
  const explicitCompression = contextApplies ? normalizeContextCompression2(payload.context_compression) : null;
573
706
  const contextCompression = contextApplies ? explicitCompression || compressionFromContextWindow2(contextWindow) || state.contextCompression : state.contextCompression;
574
707
  const hasLiveCompressionUpdate = Boolean(
575
708
  contextApplies && (explicitCompression?.active || compressionFromContextWindow2(
576
- mergeContextWindowUpdate2(null, payload.context_window)
709
+ mergeContextWindowUpdate2(null, contextPayload)
577
710
  )?.active)
578
711
  );
579
712
  const delta = eventName === "assistant.delta" && typeof payload.content === "string" ? payload.content : "";
@@ -591,6 +724,7 @@ function applyRuntimeEvent(state, event) {
591
724
  const messages = upsertStableAssistantMessage(state.messages, {
592
725
  messageId,
593
726
  runId,
727
+ strictMessageId: distinctSegment,
594
728
  create: () => ({
595
729
  id: messageId,
596
730
  role: "assistant",
@@ -663,61 +797,164 @@ function applyThreadSummaryEvent(threads, event) {
663
797
  const index = threads.findIndex((thread) => thread.id === incoming.id);
664
798
  if (index === -1) return [{ ...incoming, messages: incoming.messages || [], isHydrated: false }, ...threads];
665
799
  const next = [...threads];
666
- next[index] = { ...next[index], ...incoming, messages: next[index].messages, isHydrated: next[index].isHydrated };
800
+ next[index] = preferNewestThreadSummary(next[index], {
801
+ ...next[index],
802
+ ...incoming,
803
+ messages: next[index].messages,
804
+ isHydrated: next[index].isHydrated
805
+ });
667
806
  return next;
668
807
  }
669
808
 
670
- // src/use-agent-chat.ts
809
+ // src/thread-message-queue.ts
671
810
  import * as React2 from "react";
672
-
673
- // src/runtime-event-buffer.ts
674
- function runtimeEventFrameScheduler() {
675
- const runtime = globalThis;
676
- if (typeof runtime.requestAnimationFrame !== "function") return void 0;
677
- return (callback) => {
678
- const handle = runtime.requestAnimationFrame?.(callback);
679
- return () => {
680
- if (handle !== void 0) runtime.cancelAnimationFrame?.(handle);
681
- };
682
- };
683
- }
684
- function createRuntimeEventFrameBuffer(apply, scheduleFrame) {
685
- let pending = [];
686
- let cancelFrame = null;
687
- const flush = () => {
688
- cancelFrame?.();
689
- cancelFrame = null;
690
- if (!pending.length) return;
691
- const events = pending;
692
- pending = [];
693
- apply(events);
694
- };
695
- return {
696
- dispose() {
697
- cancelFrame?.();
698
- cancelFrame = null;
699
- pending = [];
700
- },
701
- flush,
702
- push(event) {
703
- if (event.event !== "assistant.delta" || !scheduleFrame) {
704
- pending.push(event);
705
- flush();
706
- return;
811
+ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
812
+ const [pendingMessages, setPendingMessages] = React2.useState([]);
813
+ const [messageQueue, setMessageQueue] = React2.useState(null);
814
+ const pendingRef = React2.useRef(/* @__PURE__ */ new Map());
815
+ const versionRef = React2.useRef(0);
816
+ const cursorRef = React2.useRef(0);
817
+ const promotedRef = React2.useRef(/* @__PURE__ */ new Set());
818
+ const threadRef = React2.useRef(threadId);
819
+ const transportRef = React2.useRef(transport);
820
+ const onPromotedRef = React2.useRef(onPromoted);
821
+ threadRef.current = threadId;
822
+ transportRef.current = transport;
823
+ onPromotedRef.current = onPromoted;
824
+ const hydrate = React2.useCallback((page, selectedThreadId) => {
825
+ if (threadRef.current !== selectedThreadId) return;
826
+ const pending = page.pending.filter((message) => !promotedRef.current.has(message.id)).map((message) => {
827
+ const known = pendingRef.current.get(message.id);
828
+ if (message.attachments?.length || !known?.attachments?.length) return message;
829
+ return { ...message, attachments: known.attachments.filter((attachment) => message.attachment_ids.includes(attachment.id)) };
830
+ });
831
+ for (const message of pending) pendingRef.current.set(message.id, message);
832
+ setPendingMessages(pending);
833
+ setMessageQueue(page.queue);
834
+ }, []);
835
+ const refresh = React2.useCallback(async (selectedThreadId = threadRef.current) => {
836
+ if (!selectedThreadId) return null;
837
+ const version = ++versionRef.current;
838
+ const page = await transportRef.current.list(selectedThreadId);
839
+ if (version !== versionRef.current || threadRef.current !== selectedThreadId) return null;
840
+ for (const item of page.items) {
841
+ if (item.role === "user" && item.status === "delivered" && item.run_id) {
842
+ const pending = pendingRef.current.get(item.id);
843
+ if (pending && !promotedRef.current.has(item.id)) {
844
+ promotedRef.current.add(item.id);
845
+ pendingRef.current.delete(item.id);
846
+ onPromotedRef.current({
847
+ ...pending,
848
+ ...item,
849
+ attachments: item.attachments?.length ? item.attachments : pending.attachments
850
+ }, item.run_id);
851
+ }
707
852
  }
708
- pending.push(event);
709
- if (cancelFrame) return;
710
- cancelFrame = scheduleFrame(() => {
711
- cancelFrame = null;
712
- if (!pending.length) return;
713
- const events = pending;
714
- pending = [];
715
- apply(events);
716
- });
717
853
  }
718
- };
854
+ hydrate(page, selectedThreadId);
855
+ return page;
856
+ }, [hydrate]);
857
+ const acceptNotice = React2.useCallback((notice) => {
858
+ if (notice.cursor && notice.cursor <= cursorRef.current) return false;
859
+ if (notice.cursor) cursorRef.current = notice.cursor;
860
+ const pending = pendingRef.current.get(notice.messageId);
861
+ if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(pending.id)) {
862
+ promotedRef.current.add(pending.id);
863
+ pendingRef.current.delete(pending.id);
864
+ versionRef.current += 1;
865
+ setPendingMessages((current) => current.filter((message) => message.id !== pending.id));
866
+ onPromotedRef.current(pending, notice.runId);
867
+ }
868
+ if (notice.status === "removed") pendingRef.current.delete(notice.messageId);
869
+ void refresh().catch(() => void 0);
870
+ return true;
871
+ }, [refresh]);
872
+ const submit = React2.useCallback(async (input) => {
873
+ const selectedThreadId = threadRef.current;
874
+ if (!selectedThreadId) throw new Error("No active Agent thread is available.");
875
+ const { attachments, ...submission } = input;
876
+ const result = await transportRef.current.submit({ ...submission, threadId: selectedThreadId });
877
+ if (threadRef.current !== selectedThreadId) return result;
878
+ versionRef.current += 1;
879
+ if (!result.run_id && ["queued", "steer_pending", "blocked"].includes(result.status)) {
880
+ const now = (/* @__PURE__ */ new Date()).toISOString();
881
+ const pending = {
882
+ id: result.message_id,
883
+ thread_id: selectedThreadId,
884
+ sequence: Number.MAX_SAFE_INTEGER,
885
+ run_id: null,
886
+ turn_id: null,
887
+ segment_index: null,
888
+ role: "user",
889
+ status: result.status,
890
+ requested_mode: input.mode,
891
+ text: input.input,
892
+ attachment_ids: input.attachmentIds || [],
893
+ attachments,
894
+ response_blocks: [],
895
+ revision: 1,
896
+ created_at: now,
897
+ updated_at: now
898
+ };
899
+ pendingRef.current.set(pending.id, pending);
900
+ setPendingMessages((current) => current.some((message) => message.id === pending.id) ? current : [...current, pending]);
901
+ setMessageQueue((current) => ({
902
+ pending_count: Math.max(current?.pending_count || 0, result.queue_position || 1),
903
+ paused: current?.paused || false,
904
+ reason: current?.reason || null
905
+ }));
906
+ }
907
+ void refresh().catch(() => void 0);
908
+ return result;
909
+ }, [refresh]);
910
+ const edit = React2.useCallback(async (message, input) => {
911
+ const updated = await transportRef.current.edit(message, input);
912
+ versionRef.current += 1;
913
+ pendingRef.current.set(updated.id, updated);
914
+ await refresh(message.thread_id);
915
+ return updated;
916
+ }, [refresh]);
917
+ const remove = React2.useCallback(async (message) => {
918
+ await transportRef.current.remove(message);
919
+ versionRef.current += 1;
920
+ pendingRef.current.delete(message.id);
921
+ setPendingMessages((current) => current.filter((item) => item.id !== message.id));
922
+ await refresh(message.thread_id);
923
+ }, [refresh]);
924
+ const retractForEdit = React2.useCallback(async (message) => {
925
+ await remove(message);
926
+ return { text: message.text, attachment_ids: message.attachment_ids, attachments: message.attachments };
927
+ }, [remove]);
928
+ const resume = React2.useCallback(async () => {
929
+ const selectedThreadId = threadRef.current;
930
+ if (!selectedThreadId) return null;
931
+ const result = await transportRef.current.resume(selectedThreadId);
932
+ await refresh(selectedThreadId);
933
+ return result;
934
+ }, [refresh]);
935
+ React2.useEffect(() => {
936
+ versionRef.current += 1;
937
+ cursorRef.current = 0;
938
+ promotedRef.current.clear();
939
+ pendingRef.current.clear();
940
+ setPendingMessages([]);
941
+ setMessageQueue(null);
942
+ if (threadId) void refresh(threadId).catch(() => void 0);
943
+ }, [threadId, refresh]);
944
+ React2.useEffect(() => {
945
+ if (!threadId || !pendingMessages.length || messageQueue?.paused) return;
946
+ const clock = globalThis;
947
+ const timer = clock.setInterval(() => {
948
+ void refresh(threadId).catch(() => void 0);
949
+ }, 3e3);
950
+ return () => clock.clearInterval(timer);
951
+ }, [threadId, pendingMessages.length, messageQueue?.paused, refresh]);
952
+ return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, submit, edit, remove, retractForEdit, resume };
719
953
  }
720
954
 
955
+ // src/use-agent-chat.ts
956
+ import * as React6 from "react";
957
+
721
958
  // src/thread-summary-subscription.ts
722
959
  function waitForReconnect(signal, delay) {
723
960
  return new Promise((resolve) => {
@@ -770,6 +1007,167 @@ async function subscribeToThreadSummaries(input) {
770
1007
  }
771
1008
  }
772
1009
 
1010
+ // src/use-agent-chat-thread-list.ts
1011
+ import * as React3 from "react";
1012
+ function summaryToStored(thread) {
1013
+ return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
1014
+ }
1015
+ function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation }) {
1016
+ const [isLoadingThreads, setIsLoadingThreads] = React3.useState(false);
1017
+ const [isLoadingMoreThreads, setIsLoadingMoreThreads] = React3.useState(false);
1018
+ const [threadTotal, setThreadTotal] = React3.useState();
1019
+ const refreshPromiseRef = React3.useRef(null);
1020
+ const eventsDuringRefreshRef = React3.useRef(null);
1021
+ const refreshThreads = React3.useCallback(() => {
1022
+ if (refreshPromiseRef.current) return refreshPromiseRef.current;
1023
+ clearOperationError();
1024
+ setIsLoadingThreads(true);
1025
+ eventsDuringRefreshRef.current = [];
1026
+ const request = (async () => {
1027
+ try {
1028
+ const result = await client.threads.list({ limit: threadPageSize });
1029
+ const concurrentEvents = eventsDuringRefreshRef.current || [];
1030
+ eventsDuringRefreshRef.current = null;
1031
+ setThreadTotal(result.total);
1032
+ update((current) => {
1033
+ const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
1034
+ const incoming = result.items.map((item) => {
1035
+ const existing = byId.get(item.id);
1036
+ return { ...existing, ...summaryToStored(item), messages: existing?.messages || [], isHydrated: existing?.isHydrated };
1037
+ });
1038
+ return { ...current, threads: reconcileRefreshedThreads(current.threads, incoming, current.activeThreadId, concurrentEvents) };
1039
+ });
1040
+ } catch (error) {
1041
+ if (!isAbortLike(error)) {
1042
+ failOperation("refresh_threads", error);
1043
+ throw error;
1044
+ }
1045
+ } finally {
1046
+ eventsDuringRefreshRef.current = null;
1047
+ setIsLoadingThreads(false);
1048
+ }
1049
+ })();
1050
+ refreshPromiseRef.current = request;
1051
+ void request.finally(() => {
1052
+ if (refreshPromiseRef.current === request) refreshPromiseRef.current = null;
1053
+ }).catch(() => void 0);
1054
+ return request;
1055
+ }, [clearOperationError, client, failOperation, threadPageSize, update]);
1056
+ const loadMoreThreads = React3.useCallback(async () => {
1057
+ if (isLoadingMoreThreads) return;
1058
+ const skip = stateRef.current.threads.length;
1059
+ if (threadTotal !== void 0 && skip >= threadTotal) return;
1060
+ clearOperationError();
1061
+ setIsLoadingMoreThreads(true);
1062
+ try {
1063
+ const result = await client.threads.list({ skip, limit: threadPageSize });
1064
+ setThreadTotal(result.total);
1065
+ update((current) => {
1066
+ const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
1067
+ for (const item of result.items) {
1068
+ const existing = byId.get(item.id);
1069
+ byId.set(item.id, preferNewestThreadSummary(existing, {
1070
+ ...existing,
1071
+ ...summaryToStored(item),
1072
+ messages: existing?.messages || [],
1073
+ isHydrated: existing?.isHydrated
1074
+ }));
1075
+ }
1076
+ return { ...current, threads: [...byId.values()] };
1077
+ });
1078
+ } catch (error) {
1079
+ if (!isAbortLike(error)) {
1080
+ failOperation("load_more_threads", error);
1081
+ throw error;
1082
+ }
1083
+ } finally {
1084
+ setIsLoadingMoreThreads(false);
1085
+ }
1086
+ }, [clearOperationError, client, failOperation, isLoadingMoreThreads, stateRef, threadPageSize, threadTotal, update]);
1087
+ return { isLoadingThreads, isLoadingMoreThreads, threadTotal, eventsDuringRefreshRef, refreshThreads, loadMoreThreads };
1088
+ }
1089
+
1090
+ // src/use-agent-chat-feedback.ts
1091
+ import * as React4 from "react";
1092
+ function useAgentChatFeedback({ client, update, clearOperationError, failOperation }) {
1093
+ return React4.useCallback(async (message, input) => {
1094
+ const runId = message.runId;
1095
+ if (!runId || message.role !== "assistant" || message.isFinal === false) {
1096
+ const error = new Error("Only persisted final responses can receive feedback.");
1097
+ failOperation("set_feedback", error);
1098
+ throw error;
1099
+ }
1100
+ const previous = message.feedback ?? null;
1101
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1102
+ const optimistic = input.rating ? {
1103
+ rating: input.rating,
1104
+ reason: input.rating === "dislike" ? input.reason ?? null : null,
1105
+ comment: input.rating === "dislike" ? input.comment?.trim() || null : null,
1106
+ created_at: previous?.created_at || timestamp,
1107
+ updated_at: timestamp
1108
+ } : null;
1109
+ const replaceFeedback = (current, feedback) => ({
1110
+ ...current,
1111
+ messages: current.messages.map((item) => item.id === message.id ? { ...item, feedback } : item),
1112
+ threads: current.threads.map((thread) => ({
1113
+ ...thread,
1114
+ messages: thread.messages.map((item) => item.id === message.id ? { ...item, feedback } : item)
1115
+ }))
1116
+ });
1117
+ clearOperationError();
1118
+ update((current) => replaceFeedback(current, optimistic));
1119
+ try {
1120
+ const result = await client.runs.setFeedback({ runId, ...input });
1121
+ update((current) => replaceFeedback(current, result.feedback));
1122
+ } catch (error) {
1123
+ update((current) => withAgentChatOperationError(replaceFeedback(current, previous), "set_feedback", error));
1124
+ throw error;
1125
+ }
1126
+ }, [clearOperationError, client, failOperation, update]);
1127
+ }
1128
+
1129
+ // src/use-agent-chat-queue-actions.ts
1130
+ import * as React5 from "react";
1131
+ function useAgentChatQueueActions({
1132
+ queue,
1133
+ stateRef,
1134
+ createId,
1135
+ clearOperationError,
1136
+ failOperation,
1137
+ refreshMessages,
1138
+ attachRun
1139
+ }) {
1140
+ const steer = React5.useCallback(async (text2) => {
1141
+ const threadId = stateRef.current.activeThreadId;
1142
+ if (!threadId || !text2.trim()) throw new Error("An active thread and text are required to steer.");
1143
+ clearOperationError();
1144
+ try {
1145
+ await queue.submit({ mode: "steer", input: text2.trim(), idempotencyKey: createId() });
1146
+ await refreshMessages(threadId);
1147
+ } catch (error) {
1148
+ failOperation("submit", error);
1149
+ throw error;
1150
+ }
1151
+ }, [clearOperationError, createId, failOperation, queue.submit, refreshMessages, stateRef]);
1152
+ const editPendingMessage = React5.useCallback(async (message, input) => {
1153
+ await queue.edit(message, {
1154
+ text: input.text,
1155
+ attachmentIds: input.attachmentIds ?? message.attachment_ids,
1156
+ requestedAgentId: input.requestedAgentId,
1157
+ requestedModelId: input.requestedModelId
1158
+ });
1159
+ }, [queue.edit]);
1160
+ const steerPendingMessage = React5.useCallback(async (message) => {
1161
+ await queue.edit(message, { text: message.text, attachmentIds: message.attachment_ids, mode: "steer" });
1162
+ }, [queue.edit]);
1163
+ const resumeQueue = React5.useCallback(async () => {
1164
+ if (!stateRef.current.activeThreadId) return;
1165
+ const result = await queue.resume();
1166
+ if (result?.run_id) void attachRun(result.run_id).catch(() => void 0);
1167
+ }, [attachRun, queue.resume, stateRef]);
1168
+ return { steer, editPendingMessage, steerPendingMessage, removePendingMessage: queue.remove, resumeQueue };
1169
+ }
1170
+
773
1171
  // src/initial-thread.ts
774
1172
  function resolveInitialActiveThreadId(options, storedThreadId) {
775
1173
  return Object.prototype.hasOwnProperty.call(options, "activeThreadId") ? options.activeThreadId ?? null : storedThreadId;
@@ -782,9 +1180,6 @@ function isCurrentThreadOpenRequest(input) {
782
1180
  }
783
1181
 
784
1182
  // src/use-agent-chat.ts
785
- function summaryToStored(thread) {
786
- return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
787
- }
788
1183
  function activeRunIdFromThread(thread) {
789
1184
  if (!thread) return null;
790
1185
  const activity = getThreadActivity(thread);
@@ -795,7 +1190,7 @@ function useAgentChat(options = {}) {
795
1190
  const client = options.client || runtime.client;
796
1191
  const pageSize = options.pageSize ?? 30;
797
1192
  const threadPageSize = options.threadPageSize ?? 50;
798
- const [state, setState] = React2.useState(() => ({
1193
+ const [state, setState] = React6.useState(() => ({
799
1194
  ...initialAgentChatState,
800
1195
  activeThreadId: resolveInitialActiveThreadId(
801
1196
  options,
@@ -803,25 +1198,25 @@ function useAgentChat(options = {}) {
803
1198
  ),
804
1199
  threads: runtime.storage?.loadThreads() ?? []
805
1200
  }));
806
- const [isLoadingThreads, setIsLoadingThreads] = React2.useState(false);
807
- const [isLoadingMoreThreads, setIsLoadingMoreThreads] = React2.useState(false);
808
- const [threadTotal, setThreadTotal] = React2.useState();
809
- const [isLoadingHistory, setIsLoadingHistory] = React2.useState(false);
810
- const [isLoadingOlder, setIsLoadingOlder] = React2.useState(false);
811
- const [isSubmitting, setIsSubmitting] = React2.useState(false);
812
- const streamControllerRef = React2.useRef(null);
813
- const threadOpenControllerRef = React2.useRef(null);
814
- const threadOpenGenerationRef = React2.useRef(0);
815
- const refreshThreadsPromiseRef = React2.useRef(null);
816
- const threadEventsDuringRefreshRef = React2.useRef(null);
817
- const attachRunRef = React2.useRef(async () => void 0);
818
- const stateRef = React2.useRef(state);
1201
+ const [isLoadingHistory, setIsLoadingHistory] = React6.useState(false);
1202
+ const [isLoadingOlder, setIsLoadingOlder] = React6.useState(false);
1203
+ const [isSubmitting, setIsSubmitting] = React6.useState(false);
1204
+ const streamControllerRef = React6.useRef(null);
1205
+ const threadOpenControllerRef = React6.useRef(null);
1206
+ const threadOpenGenerationRef = React6.useRef(0);
1207
+ const attachRunRef = React6.useRef(async () => void 0);
1208
+ const messageRefreshVersionRef = React6.useRef(0);
1209
+ const promotedRunHintRef = React6.useRef(null);
1210
+ const promotedAttachRef = React6.useRef(null);
1211
+ const firstSubmissionRef = React6.useRef(null);
1212
+ const firstSubmissionAdmittedRef = React6.useRef(null);
1213
+ const stateRef = React6.useRef(state);
819
1214
  stateRef.current = state;
820
- const persist = React2.useCallback((next) => {
1215
+ const persist = React6.useCallback((next) => {
821
1216
  runtime.storage?.saveThreads(next.threads);
822
1217
  runtime.storage?.saveActiveThreadId?.(next.activeThreadId);
823
1218
  }, [runtime.storage]);
824
- const update = React2.useCallback((updater) => {
1219
+ const update = React6.useCallback((updater) => {
825
1220
  setState((current) => {
826
1221
  const next = updater(current);
827
1222
  stateRef.current = next;
@@ -829,119 +1224,114 @@ function useAgentChat(options = {}) {
829
1224
  return next;
830
1225
  });
831
1226
  }, [persist]);
832
- const eventBuffer = React2.useMemo(
1227
+ const queueTransport = React6.useMemo(() => ({
1228
+ list: (threadId) => client.threads.messages.list({ threadId, limit: pageSize }),
1229
+ submit: (input) => client.threads.messages.submit(input),
1230
+ edit: (message, input) => client.threads.messages.edit({
1231
+ threadId: message.thread_id,
1232
+ messageId: message.id,
1233
+ revision: message.revision,
1234
+ input: input.text,
1235
+ attachmentIds: input.attachmentIds ?? message.attachment_ids,
1236
+ mode: input.mode,
1237
+ requestedAgentId: input.requestedAgentId,
1238
+ requestedModelId: input.requestedModelId
1239
+ }),
1240
+ remove: async (message) => {
1241
+ await client.threads.messages.remove({ threadId: message.thread_id, messageId: message.id, revision: message.revision });
1242
+ },
1243
+ resume: (threadId) => client.threads.resumeQueue({ threadId })
1244
+ }), [client, pageSize]);
1245
+ const onQueuePromotion = React6.useCallback((message, runId) => {
1246
+ if (stateRef.current.activeThreadId !== message.thread_id) return;
1247
+ promotedRunHintRef.current = runId;
1248
+ const userMessage = threadTimelineToMessages([{ ...message, run_id: runId, status: "delivered" }])[0];
1249
+ update((current) => current.activeThreadId === message.thread_id ? { ...current, messages: promoteThreadMessage(current.messages, userMessage, runId) } : current);
1250
+ if (stateRef.current.activeRunId !== runId && promotedAttachRef.current !== runId) {
1251
+ promotedAttachRef.current = runId;
1252
+ void attachRunRef.current(runId).catch(() => {
1253
+ promotedAttachRef.current = null;
1254
+ });
1255
+ }
1256
+ }, [update]);
1257
+ const queue = useThreadMessageQueue({ threadId: state.activeThreadId, transport: queueTransport, onPromoted: onQueuePromotion });
1258
+ const { pendingMessages, messageQueue } = queue;
1259
+ const eventBuffer = React6.useMemo(
833
1260
  () => createRuntimeEventFrameBuffer(
834
1261
  (events) => update((current) => applyRuntimeEvents(current, events)),
835
1262
  runtimeEventFrameScheduler()
836
1263
  ),
837
1264
  [update]
838
1265
  );
839
- const clearOperationError = React2.useCallback((operation) => {
1266
+ const clearOperationError = React6.useCallback((operation) => {
840
1267
  update((current) => clearAgentChatOperationError(current, operation));
841
1268
  }, [update]);
842
- const clearBackgroundError = React2.useCallback(() => {
1269
+ const clearBackgroundError = React6.useCallback(() => {
843
1270
  update(clearAgentChatBackgroundError);
844
1271
  }, [update]);
845
- const failOperation = React2.useCallback((operation, cause) => {
1272
+ const failOperation = React6.useCallback((operation, cause) => {
846
1273
  update((current) => withAgentChatOperationError(current, operation, cause));
847
1274
  }, [update]);
848
- const invalidateThreadOpen = React2.useCallback(() => {
1275
+ const {
1276
+ isLoadingThreads,
1277
+ isLoadingMoreThreads,
1278
+ threadTotal,
1279
+ eventsDuringRefreshRef: threadEventsDuringRefreshRef,
1280
+ refreshThreads,
1281
+ loadMoreThreads
1282
+ } = useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation });
1283
+ const invalidateThreadOpen = React6.useCallback(() => {
849
1284
  threadOpenGenerationRef.current += 1;
850
1285
  threadOpenControllerRef.current?.abort("Thread selection changed.");
851
1286
  threadOpenControllerRef.current = null;
852
1287
  setIsLoadingHistory(false);
853
1288
  }, []);
854
- const detach = React2.useCallback(() => {
1289
+ const detach = React6.useCallback(() => {
855
1290
  eventBuffer.flush();
856
1291
  streamControllerRef.current?.abort();
857
1292
  streamControllerRef.current = null;
858
1293
  update((current) => ({ ...current, streamingMessageId: null, runState: current.activeRunId ? "reconnecting" : "idle" }));
859
1294
  }, [eventBuffer, update]);
860
- React2.useEffect(() => () => {
1295
+ const refreshMessages = React6.useCallback(async (threadId, promotedRunId) => {
1296
+ const target = threadId || stateRef.current.activeThreadId;
1297
+ if (!target) return;
1298
+ const version = ++messageRefreshVersionRef.current;
1299
+ const page = await client.threads.messages.list({ threadId: target, limit: pageSize });
1300
+ if (stateRef.current.activeThreadId !== target || version !== messageRefreshVersionRef.current) return;
1301
+ queue.hydrate(page, target);
1302
+ const promoted = promotedRunId || promotedRunHintRef.current;
1303
+ if (promoted && page.items.some((message) => message.role === "user" && message.run_id === promoted)) {
1304
+ promotedRunHintRef.current = null;
1305
+ update((current) => {
1306
+ if (current.activeThreadId !== target) return current;
1307
+ return {
1308
+ ...current,
1309
+ messages: reconcileThreadTimeline(current.messages, threadTimelineToMessages(page.items), current.activeRunId, current.streamingMessageId)
1310
+ };
1311
+ });
1312
+ return;
1313
+ }
1314
+ if (stateRef.current.activeRunId) {
1315
+ const delivered = threadTimelineToMessages(page.items.filter((item) => item.role === "user" && item.run_id === stateRef.current.activeRunId && (item.segment_index ?? 0) > 0));
1316
+ if (delivered.length) update((current) => {
1317
+ if (current.activeThreadId !== target) return current;
1318
+ const additions = delivered.filter((item) => !current.messages.some((existing) => existing.id === item.id));
1319
+ return additions.length ? { ...current, messages: [...current.messages, ...additions] } : current;
1320
+ });
1321
+ }
1322
+ if (!stateRef.current.activeRunId && page.items.length) {
1323
+ update((current) => current.activeThreadId === target && !current.activeRunId ? (() => {
1324
+ const messages = reconcileThreadTimeline(current.messages, threadTimelineToMessages(page.items), null, null);
1325
+ return { ...current, messages, threads: current.threads.map((thread) => thread.id === target ? { ...thread, messages } : thread) };
1326
+ })() : current);
1327
+ }
1328
+ }, [client, pageSize, queue.hydrate, update]);
1329
+ React6.useEffect(() => () => {
861
1330
  eventBuffer.dispose();
862
1331
  streamControllerRef.current?.abort();
863
1332
  threadOpenControllerRef.current?.abort();
864
1333
  }, [eventBuffer]);
865
- const refreshThreads = React2.useCallback(() => {
866
- if (refreshThreadsPromiseRef.current) return refreshThreadsPromiseRef.current;
867
- clearOperationError();
868
- setIsLoadingThreads(true);
869
- threadEventsDuringRefreshRef.current = [];
870
- const request = (async () => {
871
- try {
872
- const result = await client.threads.list({ limit: threadPageSize });
873
- const concurrentEvents = threadEventsDuringRefreshRef.current || [];
874
- threadEventsDuringRefreshRef.current = null;
875
- setThreadTotal(result.total);
876
- update((current) => {
877
- const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
878
- const nextThreads = result.items.map((item) => {
879
- const existing = byId.get(item.id);
880
- return { ...existing, ...summaryToStored(item), messages: existing?.messages || [], isHydrated: existing?.isHydrated };
881
- });
882
- return {
883
- ...current,
884
- threads: reconcileRefreshedThreads(
885
- current.threads,
886
- nextThreads,
887
- current.activeThreadId,
888
- concurrentEvents
889
- )
890
- };
891
- });
892
- } catch (error) {
893
- if (!isAbortLike(error)) {
894
- failOperation("refresh_threads", error);
895
- throw error;
896
- }
897
- } finally {
898
- threadEventsDuringRefreshRef.current = null;
899
- setIsLoadingThreads(false);
900
- }
901
- })();
902
- refreshThreadsPromiseRef.current = request;
903
- void request.then(
904
- () => {
905
- if (refreshThreadsPromiseRef.current === request) refreshThreadsPromiseRef.current = null;
906
- },
907
- () => {
908
- if (refreshThreadsPromiseRef.current === request) refreshThreadsPromiseRef.current = null;
909
- }
910
- );
911
- return request;
912
- }, [clearOperationError, client, failOperation, threadPageSize, update]);
913
- const loadMoreThreads = React2.useCallback(async () => {
914
- if (isLoadingMoreThreads) return;
915
- const skip = stateRef.current.threads.length;
916
- if (threadTotal !== void 0 && skip >= threadTotal) return;
917
- clearOperationError();
918
- setIsLoadingMoreThreads(true);
919
- try {
920
- const result = await client.threads.list({ skip, limit: threadPageSize });
921
- setThreadTotal(result.total);
922
- update((current) => {
923
- const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
924
- for (const item of result.items) {
925
- const existing = byId.get(item.id);
926
- byId.set(item.id, {
927
- ...existing,
928
- ...summaryToStored(item),
929
- messages: existing?.messages || [],
930
- isHydrated: existing?.isHydrated
931
- });
932
- }
933
- return { ...current, threads: [...byId.values()] };
934
- });
935
- } catch (error) {
936
- if (!isAbortLike(error)) {
937
- failOperation("load_more_threads", error);
938
- throw error;
939
- }
940
- } finally {
941
- setIsLoadingMoreThreads(false);
942
- }
943
- }, [clearOperationError, client, failOperation, isLoadingMoreThreads, threadPageSize, threadTotal, update]);
944
- const openThread = React2.useCallback(async (threadId) => {
1334
+ const openThread = React6.useCallback(async (threadId) => {
945
1335
  const previousSelection = captureAgentChatThreadSelection(stateRef.current);
946
1336
  threadOpenControllerRef.current?.abort();
947
1337
  const requestController = runtime.createAbortController();
@@ -977,7 +1367,16 @@ function useAgentChat(options = {}) {
977
1367
  requestedThreadId: threadId,
978
1368
  signalAborted: requestController.signal.aborted
979
1369
  })) return;
980
- const messages = threadDetailToMessages(detail);
1370
+ const timeline = await client.threads.messages.list({ threadId, limit: pageSize, signal: requestController.signal });
1371
+ if (!isCurrentThreadOpenRequest({
1372
+ activeThreadId: stateRef.current.activeThreadId,
1373
+ currentGeneration: threadOpenGenerationRef.current,
1374
+ requestGeneration,
1375
+ requestedThreadId: threadId,
1376
+ signalAborted: requestController.signal.aborted
1377
+ })) return;
1378
+ queue.hydrate(timeline, threadId);
1379
+ const messages = timeline.items.length ? threadTimelineToMessages(timeline.items, detail) : threadDetailToMessages(detail);
981
1380
  const context = latestThreadContext(detail);
982
1381
  const stored = {
983
1382
  ...detail,
@@ -1025,15 +1424,16 @@ function useAgentChat(options = {}) {
1025
1424
  setIsLoadingHistory(false);
1026
1425
  }
1027
1426
  }
1028
- }, [clearOperationError, client, detach, failOperation, pageSize, runtime, update]);
1029
- const consumeEvent = React2.useCallback((event) => {
1427
+ }, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update]);
1428
+ const consumeEvent = React6.useCallback((event) => {
1030
1429
  const eventThreadId = typeof event.payload?.thread_id === "string" ? event.payload.thread_id.trim() : "";
1031
1430
  if (eventThreadId && !stateRef.current.activeThreadId) {
1032
1431
  update((current) => adoptStreamingThread(current, eventThreadId));
1432
+ firstSubmissionAdmittedRef.current?.();
1033
1433
  }
1034
1434
  eventBuffer.push(event);
1035
1435
  }, [eventBuffer, update]);
1036
- const attachRun = React2.useCallback(async (runId, cursor) => {
1436
+ const attachRun = React6.useCallback(async (runId, cursor) => {
1037
1437
  streamControllerRef.current?.abort();
1038
1438
  const controller = runtime.createAbortController();
1039
1439
  streamControllerRef.current = controller;
@@ -1041,7 +1441,9 @@ function useAgentChat(options = {}) {
1041
1441
  update((current) => ({ ...current, activeRunId: runId, runState: "reconnecting", error: null }));
1042
1442
  try {
1043
1443
  const result = await client.runs.attach({ runId, cursor, signal: controller.signal }, consumeEvent);
1444
+ eventBuffer.flush();
1044
1445
  update((current) => ({ ...current, activeRunId: result.detached ? runId : current.activeRunId, cursor: result.cursor ?? current.cursor, runState: result.detached ? "reconnecting" : current.runState }));
1446
+ if (!result.detached && result.threadId) await refreshMessages(result.threadId);
1045
1447
  } catch (error) {
1046
1448
  if (!isAbortLike(error)) {
1047
1449
  update((current) => ({ ...current, runState: "failed", streamingMessageId: null }));
@@ -1051,100 +1453,171 @@ function useAgentChat(options = {}) {
1051
1453
  } finally {
1052
1454
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
1053
1455
  }
1054
- }, [clearOperationError, client, consumeEvent, failOperation, runtime, update]);
1456
+ }, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update]);
1055
1457
  attachRunRef.current = attachRun;
1056
- const submit = React2.useCallback(async (input) => {
1458
+ const submit = React6.useCallback(async (input) => {
1057
1459
  if (!input.text.trim() && !input.attachmentIds?.length && !input.uploads?.length) return;
1058
- invalidateThreadOpen();
1059
- detach();
1060
- const controller = runtime.createAbortController();
1061
- streamControllerRef.current = controller;
1062
- const userId = runtime.createId();
1063
- const assistantId = runtime.createId();
1064
- const idempotencyKey = runtime.createId();
1065
- const optimisticAttachments = input.attachments?.length ? input.attachments : input.uploads?.map((upload, index) => ({
1066
- id: `${userId}-attachment-${index}`,
1067
- filename: upload.name,
1068
- mediaType: upload.mediaType,
1069
- status: "uploading"
1070
- }));
1071
- const userMessage = { id: userId, role: "user", content: input.text.trim(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), parts: input.text.trim() ? [{ id: `${userId}-text`, type: "text", kind: "text", text: input.text.trim() }] : [], attachments: optimisticAttachments };
1072
- const assistantMessage = { id: assistantId, role: "assistant", content: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), isFinal: false, parts: [] };
1073
- clearOperationError();
1074
- const hasPendingUploads = Boolean(input.uploads?.length);
1075
- update((current) => ({
1076
- ...current,
1077
- messages: appendStableStreamingTurn(current.messages, userMessage, assistantMessage),
1078
- streamingMessageId: assistantId,
1079
- runState: hasPendingUploads ? "uploading" : "streaming",
1080
- error: null
1081
- }));
1082
- setIsSubmitting(true);
1083
- let streamStarted = false;
1084
- try {
1085
- const uploaded = hasPendingUploads ? await client.attachments.uploadBatch({
1086
- uploads: input.uploads || [],
1087
- threadId: stateRef.current.activeThreadId || void 0,
1088
- requestedAgentId: input.requestedAgentId,
1089
- idempotencyKey: runtime.createId(),
1090
- signal: controller.signal
1091
- }) : [];
1092
- if (hasPendingUploads && uploaded.length !== input.uploads?.length) {
1093
- throw new Error("Attachment upload returned an incomplete batch");
1094
- }
1095
- const resolvedAttachments = uploaded.length ? uploaded.map(attachmentResultToChatAttachment) : input.attachments;
1096
- const attachmentIds = uploaded.length ? uploaded.map((attachment) => attachment.id) : input.attachmentIds;
1097
- if (uploaded.length) {
1098
- update((current) => ({
1099
- ...current,
1100
- messages: current.messages.map((message) => message.id === userId ? { ...message, attachments: resolvedAttachments } : message),
1101
- runState: "streaming"
1102
- }));
1103
- }
1104
- streamStarted = true;
1105
- const result = await client.chat.stream({ input: input.text.trim(), threadId: stateRef.current.activeThreadId || void 0, attachmentIds, requestedModelId: input.requestedModelId, requestedAgentId: input.requestedAgentId, idempotencyKey, signal: controller.signal }, consumeEvent);
1106
- eventBuffer.flush();
1107
- update((current) => {
1108
- const threadId = result.threadId || current.activeThreadId;
1109
- return {
1110
- ...current,
1111
- activeThreadId: threadId,
1112
- activeRunId: result.detached ? result.runId : current.activeRunId,
1113
- cursor: result.cursor,
1114
- runState: result.detached ? "reconnecting" : current.runState,
1115
- threads: threadId ? current.threads.map((thread) => thread.id === threadId ? { ...thread, messages: current.messages, isHydrated: true } : thread) : current.threads
1116
- };
1117
- });
1118
- if (result.threadId) await refreshThreads().catch(() => void 0);
1119
- } catch (error) {
1120
- if (isAbortLike(error) && !streamStarted) {
1121
- update((current) => current.streamingMessageId === assistantId ? {
1122
- ...current,
1123
- messages: current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
1124
- runState: "idle",
1125
- streamingMessageId: null
1126
- } : current);
1127
- } else if (!isAbortLike(error)) {
1128
- update((current) => ({
1129
- ...current,
1130
- messages: streamStarted ? current.messages : current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
1131
- runState: "failed",
1132
- streamingMessageId: null
1133
- }));
1460
+ if (firstSubmissionRef.current && !stateRef.current.activeThreadId) {
1461
+ await firstSubmissionRef.current;
1462
+ }
1463
+ const existingThreadId = stateRef.current.activeThreadId;
1464
+ if (existingThreadId) {
1465
+ clearOperationError();
1466
+ setIsSubmitting(true);
1467
+ try {
1468
+ const uploaded = input.uploads?.length ? await client.attachments.uploadBatch({
1469
+ uploads: input.uploads,
1470
+ threadId: existingThreadId,
1471
+ requestedAgentId: input.requestedAgentId,
1472
+ idempotencyKey: runtime.createId()
1473
+ }) : [];
1474
+ const result = await queue.submit({
1475
+ mode: "queue",
1476
+ input: input.text.trim(),
1477
+ attachmentIds: uploaded.length ? uploaded.map((item) => item.id) : input.attachmentIds,
1478
+ requestedAgentId: input.requestedAgentId,
1479
+ requestedModelId: input.requestedModelId,
1480
+ idempotencyKey: runtime.createId()
1481
+ });
1482
+ if (stateRef.current.activeThreadId !== existingThreadId) return;
1483
+ if (result.run_id) {
1484
+ const userMessage = {
1485
+ id: result.message_id,
1486
+ role: "user",
1487
+ runId: result.run_id,
1488
+ content: input.text.trim(),
1489
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1490
+ isFinal: true,
1491
+ parts: input.text.trim() ? [{ id: `${result.message_id}-text`, type: "text", kind: "text", text: input.text.trim() }] : [],
1492
+ attachments: input.attachments || []
1493
+ };
1494
+ update((current) => current.activeThreadId !== existingThreadId || current.messages.some((message) => message.id === userMessage.id) ? current : { ...current, messages: [...current.messages, userMessage] });
1495
+ }
1496
+ void refreshMessages(existingThreadId).catch(() => void 0);
1497
+ if (result.run_id && promotedAttachRef.current !== result.run_id && stateRef.current.activeRunId !== result.run_id) {
1498
+ void attachRunRef.current(result.run_id).catch(() => void 0);
1499
+ }
1500
+ } catch (error) {
1134
1501
  failOperation("submit", error);
1135
1502
  throw error;
1503
+ } finally {
1504
+ setIsSubmitting(false);
1136
1505
  }
1506
+ return;
1507
+ }
1508
+ const admission = new Promise((resolve) => {
1509
+ firstSubmissionAdmittedRef.current = resolve;
1510
+ });
1511
+ const stream = (async () => {
1512
+ invalidateThreadOpen();
1513
+ detach();
1514
+ const controller = runtime.createAbortController();
1515
+ streamControllerRef.current = controller;
1516
+ const userId = runtime.createId();
1517
+ const assistantId = runtime.createId();
1518
+ const idempotencyKey = runtime.createId();
1519
+ const optimisticAttachments = input.attachments?.length ? input.attachments : input.uploads?.map((upload, index) => ({
1520
+ id: `${userId}-attachment-${index}`,
1521
+ filename: upload.name,
1522
+ mediaType: upload.mediaType,
1523
+ status: "uploading"
1524
+ }));
1525
+ const userMessage = { id: userId, role: "user", content: input.text.trim(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), parts: input.text.trim() ? [{ id: `${userId}-text`, type: "text", kind: "text", text: input.text.trim() }] : [], attachments: optimisticAttachments };
1526
+ const assistantMessage = { id: assistantId, role: "assistant", content: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), isFinal: false, parts: [] };
1527
+ clearOperationError();
1528
+ const hasPendingUploads = Boolean(input.uploads?.length);
1529
+ update((current) => ({
1530
+ ...current,
1531
+ messages: appendStableStreamingTurn(current.messages, userMessage, assistantMessage),
1532
+ streamingMessageId: assistantId,
1533
+ runState: hasPendingUploads ? "uploading" : "streaming",
1534
+ error: null
1535
+ }));
1536
+ setIsSubmitting(true);
1537
+ let streamStarted = false;
1538
+ try {
1539
+ const uploaded = hasPendingUploads ? await client.attachments.uploadBatch({
1540
+ uploads: input.uploads || [],
1541
+ threadId: stateRef.current.activeThreadId || void 0,
1542
+ requestedAgentId: input.requestedAgentId,
1543
+ idempotencyKey: runtime.createId(),
1544
+ signal: controller.signal
1545
+ }) : [];
1546
+ if (hasPendingUploads && uploaded.length !== input.uploads?.length) {
1547
+ throw new Error("Attachment upload returned an incomplete batch");
1548
+ }
1549
+ const resolvedAttachments = uploaded.length ? uploaded.map(attachmentResultToChatAttachment) : input.attachments;
1550
+ const attachmentIds = uploaded.length ? uploaded.map((attachment) => attachment.id) : input.attachmentIds;
1551
+ if (uploaded.length) {
1552
+ update((current) => ({
1553
+ ...current,
1554
+ messages: current.messages.map((message) => message.id === userId ? { ...message, attachments: resolvedAttachments } : message),
1555
+ runState: "streaming"
1556
+ }));
1557
+ }
1558
+ streamStarted = true;
1559
+ const result = await client.chat.stream({ input: input.text.trim(), threadId: stateRef.current.activeThreadId || void 0, attachmentIds, requestedModelId: input.requestedModelId, requestedAgentId: input.requestedAgentId, idempotencyKey, signal: controller.signal }, consumeEvent);
1560
+ eventBuffer.flush();
1561
+ update((current) => {
1562
+ const threadId = result.threadId || current.activeThreadId;
1563
+ return {
1564
+ ...current,
1565
+ activeThreadId: threadId,
1566
+ activeRunId: result.detached ? result.runId : current.activeRunId,
1567
+ cursor: result.cursor,
1568
+ runState: result.detached ? "reconnecting" : current.runState,
1569
+ threads: threadId ? current.threads.map((thread) => thread.id === threadId ? { ...thread, messages: current.messages, isHydrated: true } : thread) : current.threads
1570
+ };
1571
+ });
1572
+ if (result.threadId) await refreshThreads().catch(() => void 0);
1573
+ if (result.threadId && !result.detached) await refreshMessages(result.threadId);
1574
+ } catch (error) {
1575
+ if (isAbortLike(error) && !streamStarted) {
1576
+ update((current) => current.streamingMessageId === assistantId ? {
1577
+ ...current,
1578
+ messages: current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
1579
+ runState: "idle",
1580
+ streamingMessageId: null
1581
+ } : current);
1582
+ } else if (!isAbortLike(error)) {
1583
+ update((current) => ({
1584
+ ...current,
1585
+ messages: streamStarted ? current.messages : current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
1586
+ runState: "failed",
1587
+ streamingMessageId: null
1588
+ }));
1589
+ failOperation("submit", error);
1590
+ throw error;
1591
+ }
1592
+ } finally {
1593
+ setIsSubmitting(false);
1594
+ if (streamControllerRef.current === controller) streamControllerRef.current = null;
1595
+ }
1596
+ })();
1597
+ const accepted = Promise.race([stream, admission]);
1598
+ firstSubmissionRef.current = accepted;
1599
+ try {
1600
+ await accepted;
1137
1601
  } finally {
1138
- setIsSubmitting(false);
1139
- if (streamControllerRef.current === controller) streamControllerRef.current = null;
1602
+ if (firstSubmissionRef.current === accepted) firstSubmissionRef.current = null;
1603
+ firstSubmissionAdmittedRef.current = null;
1140
1604
  }
1141
- }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, refreshThreads, runtime, update]);
1142
- const startNewThread = React2.useCallback(() => {
1605
+ }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update]);
1606
+ const { steer, editPendingMessage, steerPendingMessage, removePendingMessage, resumeQueue } = useAgentChatQueueActions({
1607
+ queue,
1608
+ stateRef,
1609
+ createId: runtime.createId,
1610
+ clearOperationError,
1611
+ failOperation,
1612
+ refreshMessages,
1613
+ attachRun
1614
+ });
1615
+ const startNewThread = React6.useCallback(() => {
1143
1616
  invalidateThreadOpen();
1144
1617
  detach();
1145
1618
  update((current) => ({ ...initialAgentChatState, threads: current.threads }));
1146
1619
  }, [detach, invalidateThreadOpen, update]);
1147
- const deleteThread = React2.useCallback(async (threadId) => {
1620
+ const deleteThread = React6.useCallback(async (threadId) => {
1148
1621
  if (stateRef.current.activeThreadId === threadId) invalidateThreadOpen();
1149
1622
  clearOperationError();
1150
1623
  try {
@@ -1165,7 +1638,7 @@ function useAgentChat(options = {}) {
1165
1638
  }
1166
1639
  }
1167
1640
  }, [clearOperationError, client, failOperation, invalidateThreadOpen, runtime, update]);
1168
- const cancelRun = React2.useCallback(async () => {
1641
+ const cancelRun = React6.useCallback(async () => {
1169
1642
  const runId = stateRef.current.activeRunId;
1170
1643
  if (!runId || stateRef.current.runState === "cancelling") return;
1171
1644
  const previousRunState = stateRef.current.runState;
@@ -1204,7 +1677,7 @@ function useAgentChat(options = {}) {
1204
1677
  }
1205
1678
  }
1206
1679
  }, [clearOperationError, client, detach, failOperation, runtime, update]);
1207
- const resumeHitl = React2.useCallback(async (part, response) => {
1680
+ const resumeHitl = React6.useCallback(async (part, response) => {
1208
1681
  const runId = stateRef.current.activeRunId || stateRef.current.messages.find((message) => message.parts.includes(part))?.runId;
1209
1682
  clearOperationError();
1210
1683
  if (!runId) {
@@ -1225,54 +1698,8 @@ function useAgentChat(options = {}) {
1225
1698
  throw error;
1226
1699
  }
1227
1700
  }, [attachRun, clearOperationError, client, failOperation, runtime]);
1228
- const setFeedback = React2.useCallback(async (message, input) => {
1229
- const runId = message.runId;
1230
- if (!runId || message.role !== "assistant" || message.isFinal === false) {
1231
- const error = new Error("Only persisted final responses can receive feedback.");
1232
- failOperation("set_feedback", error);
1233
- throw error;
1234
- }
1235
- const previous = message.feedback ?? null;
1236
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1237
- const optimistic = input.rating ? {
1238
- rating: input.rating,
1239
- reason: input.rating === "dislike" ? input.reason ?? null : null,
1240
- comment: input.rating === "dislike" ? input.comment?.trim() || null : null,
1241
- created_at: previous?.created_at || timestamp,
1242
- updated_at: timestamp
1243
- } : null;
1244
- const replaceFeedback = (current, feedback) => {
1245
- const messages = current.messages.map(
1246
- (item) => item.id === message.id ? { ...item, feedback } : item
1247
- );
1248
- return {
1249
- ...current,
1250
- messages,
1251
- threads: current.threads.map((thread) => ({
1252
- ...thread,
1253
- messages: thread.messages.map(
1254
- (item) => item.id === message.id ? { ...item, feedback } : item
1255
- )
1256
- }))
1257
- };
1258
- };
1259
- clearOperationError();
1260
- update((current) => replaceFeedback(current, optimistic));
1261
- try {
1262
- const result = await client.runs.setFeedback({ runId, ...input });
1263
- update((current) => replaceFeedback(current, result.feedback));
1264
- } catch (error) {
1265
- update(
1266
- (current) => withAgentChatOperationError(
1267
- replaceFeedback(current, previous),
1268
- "set_feedback",
1269
- error
1270
- )
1271
- );
1272
- throw error;
1273
- }
1274
- }, [clearOperationError, client, failOperation, update]);
1275
- const loadOlder = React2.useCallback(async () => {
1701
+ const setFeedback = useAgentChatFeedback({ client, update, clearOperationError, failOperation });
1702
+ const loadOlder = React6.useCallback(async () => {
1276
1703
  const thread = stateRef.current.threads.find((item) => item.id === stateRef.current.activeThreadId);
1277
1704
  if (!thread?.nextBeforeTurnIndex) return;
1278
1705
  clearOperationError();
@@ -1290,7 +1717,7 @@ function useAgentChat(options = {}) {
1290
1717
  setIsLoadingOlder(false);
1291
1718
  }
1292
1719
  }, [clearOperationError, client, failOperation, pageSize, update]);
1293
- const regenerate = React2.useCallback(async (message) => {
1720
+ const regenerate = React6.useCallback(async (message) => {
1294
1721
  const index = stateRef.current.messages.findIndex((item) => item.id === message.id);
1295
1722
  const user = stateRef.current.messages.slice(0, index).reverse().find((item) => item.role === "user");
1296
1723
  if (user) await submit({
@@ -1299,7 +1726,7 @@ function useAgentChat(options = {}) {
1299
1726
  attachments: user.attachments
1300
1727
  });
1301
1728
  }, [submit]);
1302
- React2.useEffect(() => {
1729
+ React6.useEffect(() => {
1303
1730
  const controller = runtime.createAbortController();
1304
1731
  clearBackgroundError();
1305
1732
  void subscribeToThreadSummaries({
@@ -1312,6 +1739,26 @@ function useAgentChat(options = {}) {
1312
1739
  ...clearAgentChatBackgroundError(current),
1313
1740
  threads: applyThreadSummaryEvent(current.threads, event)
1314
1741
  }));
1742
+ if (event.event === "thread.message.updated" && event.thread_id === stateRef.current.activeThreadId) {
1743
+ const wasQueued = queue.acceptNotice({
1744
+ messageId: event.message_id,
1745
+ status: event.status,
1746
+ runId: event.run_id,
1747
+ cursor: event.cursor
1748
+ });
1749
+ if (wasQueued) void refreshMessages(event.thread_id, event.run_id || void 0).catch(() => void 0);
1750
+ }
1751
+ if (event.event === "thread.summary.updated" && event.thread.id === stateRef.current.activeThreadId) {
1752
+ if (streamControllerRef.current && stateRef.current.messages.some((message) => message.role === "user" && !message.runId)) return;
1753
+ const promotedRunId = activeRunIdFromThread(summaryToStored(event.thread));
1754
+ void (async () => {
1755
+ await refreshMessages(String(event.thread.id), promotedRunId || void 0);
1756
+ if (promotedRunId && stateRef.current.activeRunId !== promotedRunId && stateRef.current.activeThreadId === String(event.thread.id) && promotedAttachRef.current !== promotedRunId) {
1757
+ promotedAttachRef.current = promotedRunId;
1758
+ await attachRunRef.current(promotedRunId);
1759
+ }
1760
+ })().catch(() => void 0);
1761
+ }
1315
1762
  },
1316
1763
  onError(error) {
1317
1764
  update((current) => withAgentChatBackgroundError(current, error));
@@ -1321,8 +1768,8 @@ function useAgentChat(options = {}) {
1321
1768
  }
1322
1769
  });
1323
1770
  return () => controller.abort();
1324
- }, [clearBackgroundError, client, refreshThreads, runtime.createAbortController, update]);
1325
- React2.useEffect(() => {
1771
+ }, [clearBackgroundError, client, queue.acceptNotice, refreshMessages, refreshThreads, runtime.createAbortController, update]);
1772
+ React6.useEffect(() => {
1326
1773
  const threadId = options.activeThreadId;
1327
1774
  const activeThread2 = stateRef.current.threads.find((thread) => thread.id === stateRef.current.activeThreadId);
1328
1775
  if (shouldHydrateSelectedThread(threadId, stateRef.current.activeThreadId, Boolean(activeThread2?.isHydrated))) {
@@ -1334,6 +1781,8 @@ function useAgentChat(options = {}) {
1334
1781
  return {
1335
1782
  ...state,
1336
1783
  activeThread,
1784
+ pendingMessages,
1785
+ messageQueue,
1337
1786
  hasOlderTurns: Boolean(activeThread?.hasOlderTurns),
1338
1787
  hasMoreThreads,
1339
1788
  isLoadingHistory,
@@ -1352,6 +1801,13 @@ function useAgentChat(options = {}) {
1352
1801
  openThread,
1353
1802
  refreshThreads,
1354
1803
  regenerate,
1804
+ refreshMessages,
1805
+ editPendingMessage,
1806
+ steerPendingMessage,
1807
+ removePendingMessage,
1808
+ retractPendingMessage: queue.retractForEdit,
1809
+ resumeQueue,
1810
+ steer,
1355
1811
  resumeHitl,
1356
1812
  setFeedback,
1357
1813
  startNewThread,
@@ -1360,15 +1816,15 @@ function useAgentChat(options = {}) {
1360
1816
  }
1361
1817
 
1362
1818
  // src/use-agent-resources.ts
1363
- import * as React3 from "react";
1819
+ import * as React7 from "react";
1364
1820
  function useAgentThreads(options = {}) {
1365
1821
  const { client, createAbortController } = useAgents24Runtime();
1366
- const [threads, setThreads] = React3.useState([]);
1367
- const [total, setTotal] = React3.useState();
1368
- const [isLoading, setIsLoading] = React3.useState(true);
1369
- const [error, setError] = React3.useState(null);
1370
- const controllerRef = React3.useRef(null);
1371
- const load = React3.useCallback(async () => {
1822
+ const [threads, setThreads] = React7.useState([]);
1823
+ const [total, setTotal] = React7.useState();
1824
+ const [isLoading, setIsLoading] = React7.useState(true);
1825
+ const [error, setError] = React7.useState(null);
1826
+ const controllerRef = React7.useRef(null);
1827
+ const load = React7.useCallback(async () => {
1372
1828
  controllerRef.current?.abort();
1373
1829
  const controller = createAbortController();
1374
1830
  controllerRef.current = controller;
@@ -1387,7 +1843,7 @@ function useAgentThreads(options = {}) {
1387
1843
  }
1388
1844
  }
1389
1845
  }, [client, createAbortController, options.limit, options.skip]);
1390
- React3.useEffect(() => {
1846
+ React7.useEffect(() => {
1391
1847
  void load();
1392
1848
  return () => controllerRef.current?.abort();
1393
1849
  }, [load]);
@@ -1395,11 +1851,11 @@ function useAgentThreads(options = {}) {
1395
1851
  }
1396
1852
  function useAgentThread(threadId, options = {}) {
1397
1853
  const { client, createAbortController } = useAgents24Runtime();
1398
- const [thread, setThread] = React3.useState(null);
1399
- const [isLoading, setIsLoading] = React3.useState(Boolean(threadId));
1400
- const [error, setError] = React3.useState(null);
1401
- const controllerRef = React3.useRef(null);
1402
- const load = React3.useCallback(async () => {
1854
+ const [thread, setThread] = React7.useState(null);
1855
+ const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
1856
+ const [error, setError] = React7.useState(null);
1857
+ const controllerRef = React7.useRef(null);
1858
+ const load = React7.useCallback(async () => {
1403
1859
  if (!threadId) {
1404
1860
  controllerRef.current?.abort();
1405
1861
  controllerRef.current = null;
@@ -1426,24 +1882,73 @@ function useAgentThread(threadId, options = {}) {
1426
1882
  }
1427
1883
  }
1428
1884
  }, [client, createAbortController, options.beforeTurnIndex, options.limit, threadId]);
1429
- React3.useEffect(() => {
1885
+ React7.useEffect(() => {
1430
1886
  void load();
1431
1887
  return () => controllerRef.current?.abort();
1432
1888
  }, [load]);
1433
1889
  return { error, isLoading, refresh: load, thread };
1434
1890
  }
1891
+ function useThreadComputer(threadId) {
1892
+ const { client, createAbortController } = useAgents24Runtime();
1893
+ const [computer, setComputer] = React7.useState(null);
1894
+ const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
1895
+ const [error, setError] = React7.useState(null);
1896
+ const controllerRef = React7.useRef(null);
1897
+ const refresh = React7.useCallback(async () => {
1898
+ controllerRef.current?.abort();
1899
+ if (!threadId) {
1900
+ setComputer(null);
1901
+ setIsLoading(false);
1902
+ return null;
1903
+ }
1904
+ const controller = createAbortController();
1905
+ controllerRef.current = controller;
1906
+ setIsLoading(true);
1907
+ setError(null);
1908
+ try {
1909
+ const result = await client.threads.computer.get({ threadId, signal: controller.signal });
1910
+ setComputer(result);
1911
+ return result;
1912
+ } catch (nextError) {
1913
+ if (!isAbortLike(nextError)) setError(nextError);
1914
+ return null;
1915
+ } finally {
1916
+ if (controllerRef.current === controller) {
1917
+ controllerRef.current = null;
1918
+ setIsLoading(false);
1919
+ }
1920
+ }
1921
+ }, [client, createAbortController, threadId]);
1922
+ React7.useEffect(() => {
1923
+ void refresh();
1924
+ return () => controllerRef.current?.abort();
1925
+ }, [refresh]);
1926
+ const list = React7.useCallback((input) => {
1927
+ if (!threadId) throw new Error("A thread is required to list computer files");
1928
+ return client.threads.computer.list({ threadId, ...input });
1929
+ }, [client, threadId]);
1930
+ const read = React7.useCallback((path) => {
1931
+ if (!threadId) throw new Error("A thread is required to read a computer file");
1932
+ return client.threads.computer.read({ threadId, path });
1933
+ }, [client, threadId]);
1934
+ const createContentAccess = React7.useCallback((path, download = false) => {
1935
+ if (!threadId) throw new Error("A thread is required to access a computer file");
1936
+ return client.threads.computer.createContentAccess({ threadId, path, download });
1937
+ }, [client, threadId]);
1938
+ return { computer, error, isLoading, list, read, createContentAccess, refresh };
1939
+ }
1435
1940
  function useAgentRun() {
1436
1941
  const { client, createAbortController, createId } = useAgents24Runtime();
1437
- const controllerRef = React3.useRef(null);
1438
- const [isAttached, setIsAttached] = React3.useState(false);
1439
- const [error, setError] = React3.useState(null);
1440
- React3.useEffect(() => () => controllerRef.current?.abort(), []);
1441
- const detach = React3.useCallback(() => {
1942
+ const controllerRef = React7.useRef(null);
1943
+ const [isAttached, setIsAttached] = React7.useState(false);
1944
+ const [error, setError] = React7.useState(null);
1945
+ React7.useEffect(() => () => controllerRef.current?.abort(), []);
1946
+ const detach = React7.useCallback(() => {
1442
1947
  controllerRef.current?.abort();
1443
1948
  controllerRef.current = null;
1444
1949
  setIsAttached(false);
1445
1950
  }, []);
1446
- const attach = React3.useCallback(async (runId, onEvent, cursor) => {
1951
+ const attach = React7.useCallback(async (runId, onEvent, cursor) => {
1447
1952
  detach();
1448
1953
  const controller = createAbortController();
1449
1954
  controllerRef.current = controller;
@@ -1459,7 +1964,7 @@ function useAgentRun() {
1459
1964
  setIsAttached(false);
1460
1965
  }
1461
1966
  }, [client, createAbortController, detach]);
1462
- const cancel = React3.useCallback(async (runId) => {
1967
+ const cancel = React7.useCallback(async (runId) => {
1463
1968
  const result = await client.runs.cancel({ runId, idempotencyKey: createId() });
1464
1969
  detach();
1465
1970
  return result;
@@ -1468,9 +1973,9 @@ function useAgentRun() {
1468
1973
  }
1469
1974
  function useAgentAttachments() {
1470
1975
  const { client, createId } = useAgents24Runtime();
1471
- const [uploads, setUploads] = React3.useState({});
1472
- const [isUploading, setIsUploading] = React3.useState(false);
1473
- const uploadBatch = React3.useCallback(async (input) => {
1976
+ const [uploads, setUploads] = React7.useState({});
1977
+ const [isUploading, setIsUploading] = React7.useState(false);
1978
+ const uploadBatch = React7.useCallback(async (input) => {
1474
1979
  setIsUploading(true);
1475
1980
  try {
1476
1981
  const results = await client.attachments.uploadBatch({ uploads: input.uploads, threadId: input.threadId, requestedAgentId: input.requestedAgentId, idempotencyKey: createId(), signal: input.signal });
@@ -1483,7 +1988,7 @@ function useAgentAttachments() {
1483
1988
  setIsUploading(false);
1484
1989
  }
1485
1990
  }, [client, createId]);
1486
- const createContentAccess = React3.useCallback(
1991
+ const createContentAccess = React7.useCallback(
1487
1992
  (input) => client.attachments.createContentAccess(input),
1488
1993
  [client]
1489
1994
  );
@@ -1491,9 +1996,9 @@ function useAgentAttachments() {
1491
1996
  }
1492
1997
  function useAgentHitl() {
1493
1998
  const { client, createId } = useAgents24Runtime();
1494
- const [isResolving, setIsResolving] = React3.useState(false);
1495
- const [lastResult, setLastResult] = React3.useState(null);
1496
- const resume = React3.useCallback(async (input) => {
1999
+ const [isResolving, setIsResolving] = React7.useState(false);
2000
+ const [lastResult, setLastResult] = React7.useState(null);
2001
+ const resume = React7.useCallback(async (input) => {
1497
2002
  setIsResolving(true);
1498
2003
  try {
1499
2004
  const result = await client.hitl.resume({ ...input, idempotencyKey: createId() });
@@ -1507,8 +2012,8 @@ function useAgentHitl() {
1507
2012
  }
1508
2013
  function useAgentMcp() {
1509
2014
  const { client, createId } = useAgents24Runtime();
1510
- const [isAuthorizing, setIsAuthorizing] = React3.useState(false);
1511
- const startAuthorization = React3.useCallback(async (input) => {
2015
+ const [isAuthorizing, setIsAuthorizing] = React7.useState(false);
2016
+ const startAuthorization = React7.useCallback(async (input) => {
1512
2017
  setIsAuthorizing(true);
1513
2018
  try {
1514
2019
  return await client.mcp.startAuthorization({ ...input, idempotencyKey: createId() });
@@ -1516,7 +2021,7 @@ function useAgentMcp() {
1516
2021
  setIsAuthorizing(false);
1517
2022
  }
1518
2023
  }, [client, createId]);
1519
- const redeemCallback = React3.useCallback(async (input) => client.mcp.redeemCallback({ ...input, idempotencyKey: createId() }), [client, createId]);
2024
+ const redeemCallback = React7.useCallback(async (input) => client.mcp.redeemCallback({ ...input, idempotencyKey: createId() }), [client, createId]);
1520
2025
  return { isAuthorizing, redeemCallback, startAuthorization };
1521
2026
  }
1522
2027
  export {
@@ -1532,17 +2037,25 @@ export {
1532
2037
  chatAttachmentIds,
1533
2038
  clearAgentChatBackgroundError,
1534
2039
  clearAgentChatOperationError,
2040
+ computerResourceTargets,
1535
2041
  contextCompressionPartFromEvent,
1536
2042
  createPortableId,
2043
+ createRuntimeEventFrameBuffer,
1537
2044
  getThreadActivity,
1538
2045
  initialAgentChatState,
1539
2046
  isAbortLike,
1540
2047
  latestThreadContext,
1541
2048
  modelTurnPartFromEvent,
1542
2049
  partsFromResponseBlocks,
2050
+ preferNewestThreadSummary,
2051
+ preserveMountedMessageIds,
2052
+ promoteThreadMessage,
1543
2053
  reconcileRefreshedThreads,
2054
+ reconcileThreadTimeline,
1544
2055
  restoreAgentChatThreadSelection,
2056
+ runtimeEventFrameScheduler,
1545
2057
  threadDetailToMessages,
2058
+ threadTimelineToMessages,
1546
2059
  upsertStableAssistantMessage,
1547
2060
  useAgentAttachments,
1548
2061
  useAgentChat,
@@ -1553,6 +2066,8 @@ export {
1553
2066
  useAgentThread,
1554
2067
  useAgentThreads,
1555
2068
  useAgents24Runtime,
2069
+ useThreadComputer,
2070
+ useThreadMessageQueue,
1556
2071
  withAgentChatBackgroundError,
1557
2072
  withAgentChatOperationError
1558
2073
  };