@agents24/react 0.2.6 → 0.2.8

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
@@ -29,7 +29,7 @@ function getThreadActivity(thread) {
29
29
  return {
30
30
  status,
31
31
  runId: activeRun?.run_id || thread.last_run_id || null,
32
- isSpinning: Boolean(activeRun),
32
+ isSpinning: Boolean(activeRun) && !["waiting", "interrupted"].includes(status || ""),
33
33
  isPaused: status === "paused",
34
34
  isFailed: status === "failed",
35
35
  isCompleted: status === "completed",
@@ -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,21 @@ 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) => ({ id, filename: "Attachment" })) : void 0,
403
+ feedback: item.role === "assistant" && item.run_id ? feedbackByRun.get(item.run_id) : void 0
404
+ }));
405
+ }
372
406
  function latestThreadContext(detail) {
373
407
  let contextWindow = null;
374
408
  for (const turn of detail.turns || []) {
@@ -477,12 +511,18 @@ function withAgentChatBackgroundError(state, cause) {
477
511
  function clearAgentChatBackgroundError(state) {
478
512
  return state.backgroundError ? { ...state, backgroundError: null } : state;
479
513
  }
480
- function reconcileRefreshedThreads(current, incoming, activeThreadId) {
481
- if (!activeThreadId || incoming.some((thread) => thread.id === activeThreadId)) {
482
- return incoming;
483
- }
484
- const active = current.find((thread) => thread.id === activeThreadId);
485
- return active?.isHydrated ? [active, ...incoming] : incoming;
514
+ function reconcileRefreshedThreads(current, incoming, activeThreadId, concurrentEvents = []) {
515
+ const currentById = new Map(current.map((thread) => [thread.id, thread]));
516
+ const newest = incoming.map((thread) => preferNewestThreadSummary(currentById.get(thread.id), thread));
517
+ const active = activeThreadId && !incoming.some((thread) => thread.id === activeThreadId) ? current.find((thread) => thread.id === activeThreadId) : null;
518
+ const base = active?.isHydrated ? [active, ...newest] : newest;
519
+ return concurrentEvents.reduce(applyThreadSummaryEvent, base);
520
+ }
521
+ function preferNewestThreadSummary(current, incoming) {
522
+ if (!current?.updated_at || !incoming.updated_at) return incoming;
523
+ const currentTime = Date.parse(current.updated_at);
524
+ const incomingTime = Date.parse(incoming.updated_at);
525
+ return Number.isFinite(currentTime) && Number.isFinite(incomingTime) && currentTime > incomingTime ? current : incoming;
486
526
  }
487
527
  function adoptStreamingThread(state, threadId) {
488
528
  const normalizedThreadId = threadId.trim();
@@ -509,13 +549,20 @@ function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
509
549
  ))) next.push(assistantMessage);
510
550
  return next;
511
551
  }
552
+ function preserveMountedMessageIds(canonical, current) {
553
+ return canonical.map((message) => {
554
+ 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));
555
+ return mounted && mounted.id !== message.id ? { ...message, id: mounted.id, durableId: message.id } : message;
556
+ });
557
+ }
512
558
  function upsertStableAssistantMessage(messages, input) {
513
559
  const next = [...messages];
514
560
  for (const baseMessage of input.baseMessages || []) {
515
- const isTarget = baseMessage.role === "assistant" && (Boolean(input.messageId && baseMessage.id === input.messageId) || Boolean(input.runId && baseMessage.runId === input.runId));
561
+ const isTarget = baseMessage.role === "assistant" && (Boolean(input.messageId && baseMessage.id === input.messageId) || Boolean(!input.strictMessageId && input.runId && baseMessage.runId === input.runId));
516
562
  if (!isTarget && !next.some((message) => message.id === baseMessage.id)) next.push(baseMessage);
517
563
  }
518
- const index = next.findIndex((message) => message.role === "assistant" && (Boolean(input.messageId && message.id === input.messageId) || Boolean(input.runId && message.runId === input.runId)));
564
+ const exactIndex = next.findIndex((message) => message.role === "assistant" && Boolean(input.messageId && message.id === input.messageId));
565
+ const index = exactIndex >= 0 ? exactIndex : input.strictMessageId ? -1 : next.findIndex((message) => message.role === "assistant" && Boolean(input.runId && message.runId === input.runId));
519
566
  if (index === -1) next.push(input.create());
520
567
  else next[index] = input.update(next[index]);
521
568
  return next;
@@ -561,11 +608,14 @@ function applyRuntimeEvent(state, event) {
561
608
  const projection = assistantProjectionFromEvent(event);
562
609
  const eventName = String(event.event || "");
563
610
  const runId = event.run_id || state.activeRunId;
564
- const messageId = state.streamingMessageId || `${runId || "run"}-assistant`;
611
+ const segmentId = typeof event.payload?.segment_id === "string" ? event.payload.segment_id : null;
612
+ const distinctSegment = Boolean(segmentId && segmentId !== `${runId}:0`);
613
+ const messageId = distinctSegment ? `${runId || "run"}-assistant-${segmentId}` : state.streamingMessageId || `${runId || "run"}-assistant`;
565
614
  const terminal = eventName === "run.completed" || eventName === "run.failed" || eventName === "run.cancelled";
566
615
  const paused = eventName === "run.paused" || projection.parts.some((part) => part.kind === "hitl" && part.status === "pending");
616
+ const suspension = eventName === "run.waiting" ? "waiting" : eventName === "run.interrupted" ? "interrupted" : null;
567
617
  const cancelling = eventName === "run.cancelling" || String(event.payload?.status || "") === "cancelling";
568
- const existing = state.messages.find((message) => message.role === "assistant" && (message.id === messageId || Boolean(runId && message.runId === runId)));
618
+ 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);
569
619
  const payload = event.payload || {};
570
620
  const eventThreadId = typeof payload.thread_id === "string" && payload.thread_id.trim() ? payload.thread_id.trim() : null;
571
621
  const contextApplies = (!state.activeThreadId || !eventThreadId || eventThreadId === state.activeThreadId) && (!state.activeRunId || !runId || runId === state.activeRunId);
@@ -592,6 +642,7 @@ function applyRuntimeEvent(state, event) {
592
642
  const messages = upsertStableAssistantMessage(state.messages, {
593
643
  messageId,
594
644
  runId,
645
+ strictMessageId: distinctSegment,
595
646
  create: () => ({
596
647
  id: messageId,
597
648
  role: "assistant",
@@ -637,14 +688,26 @@ function applyRuntimeEvent(state, event) {
637
688
  contextWindow,
638
689
  contextCompression,
639
690
  contextCompressionRevision: hasLiveCompressionUpdate ? state.contextCompressionRevision + 1 : state.contextCompressionRevision,
640
- runState: paused ? "paused" : cancelling ? "cancelling" : terminal ? eventName === "run.completed" ? "completed" : eventName === "run.cancelled" ? "cancelled" : "failed" : "streaming",
641
- streamingMessageId: terminal || paused ? null : messageId
691
+ runState: suspension || (paused ? "paused" : cancelling ? "cancelling" : terminal ? eventName === "run.completed" ? "completed" : eventName === "run.cancelled" ? "cancelled" : "failed" : "streaming"),
692
+ streamingMessageId: terminal || paused || suspension ? null : messageId
642
693
  };
643
694
  }
644
695
  function applyRuntimeEvents(state, events) {
645
696
  return events.reduce(applyRuntimeEvent, state);
646
697
  }
647
698
  function applyThreadSummaryEvent(threads, event) {
699
+ if (String(event.event) === "thread.task.updated") {
700
+ const value = event;
701
+ const handle = value.task?.handle;
702
+ if (typeof handle !== "string") return threads;
703
+ return threads.map((thread) => thread.id === value.thread_id ? {
704
+ ...thread,
705
+ taskNotifications: [
706
+ ...(thread.taskNotifications || []).filter((task) => task.handle !== handle),
707
+ { ...value.task, handle }
708
+ ].slice(-1e3)
709
+ } : thread);
710
+ }
648
711
  if (event.event === "snapshot_required") return threads;
649
712
  if (event.event === "thread.deleted") return threads.filter((thread) => thread.id !== event.thread_id);
650
713
  if (event.event !== "thread.summary.updated") return threads;
@@ -652,12 +715,153 @@ function applyThreadSummaryEvent(threads, event) {
652
715
  const index = threads.findIndex((thread) => thread.id === incoming.id);
653
716
  if (index === -1) return [{ ...incoming, messages: incoming.messages || [], isHydrated: false }, ...threads];
654
717
  const next = [...threads];
655
- next[index] = { ...next[index], ...incoming, messages: next[index].messages, isHydrated: next[index].isHydrated };
718
+ next[index] = preferNewestThreadSummary(next[index], {
719
+ ...next[index],
720
+ ...incoming,
721
+ messages: next[index].messages,
722
+ isHydrated: next[index].isHydrated
723
+ });
656
724
  return next;
657
725
  }
658
726
 
659
- // src/use-agent-chat.ts
727
+ // src/thread-message-queue.ts
660
728
  import * as React2 from "react";
729
+ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
730
+ const [pendingMessages, setPendingMessages] = React2.useState([]);
731
+ const [messageQueue, setMessageQueue] = React2.useState(null);
732
+ const pendingRef = React2.useRef(/* @__PURE__ */ new Map());
733
+ const versionRef = React2.useRef(0);
734
+ const cursorRef = React2.useRef(0);
735
+ const promotedRef = React2.useRef(/* @__PURE__ */ new Set());
736
+ const threadRef = React2.useRef(threadId);
737
+ const transportRef = React2.useRef(transport);
738
+ const onPromotedRef = React2.useRef(onPromoted);
739
+ threadRef.current = threadId;
740
+ transportRef.current = transport;
741
+ onPromotedRef.current = onPromoted;
742
+ const hydrate = React2.useCallback((page, selectedThreadId) => {
743
+ if (threadRef.current !== selectedThreadId) return;
744
+ const pending = page.pending.filter((message) => !promotedRef.current.has(message.id));
745
+ for (const message of pending) pendingRef.current.set(message.id, message);
746
+ setPendingMessages(pending);
747
+ setMessageQueue(page.queue);
748
+ }, []);
749
+ const refresh = React2.useCallback(async (selectedThreadId = threadRef.current) => {
750
+ if (!selectedThreadId) return null;
751
+ const version = ++versionRef.current;
752
+ const page = await transportRef.current.list(selectedThreadId);
753
+ if (version !== versionRef.current || threadRef.current !== selectedThreadId) return null;
754
+ for (const item of page.items) {
755
+ if (item.role === "user" && item.status === "delivered" && item.run_id) {
756
+ const pending = pendingRef.current.get(item.id);
757
+ if (pending && !promotedRef.current.has(item.id)) {
758
+ promotedRef.current.add(item.id);
759
+ pendingRef.current.delete(item.id);
760
+ onPromotedRef.current(pending, item.run_id);
761
+ }
762
+ }
763
+ }
764
+ hydrate(page, selectedThreadId);
765
+ return page;
766
+ }, [hydrate]);
767
+ const acceptNotice = React2.useCallback((notice) => {
768
+ if (notice.cursor && notice.cursor <= cursorRef.current) return false;
769
+ if (notice.cursor) cursorRef.current = notice.cursor;
770
+ const pending = pendingRef.current.get(notice.messageId);
771
+ if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(pending.id)) {
772
+ promotedRef.current.add(pending.id);
773
+ pendingRef.current.delete(pending.id);
774
+ versionRef.current += 1;
775
+ setPendingMessages((current) => current.filter((message) => message.id !== pending.id));
776
+ onPromotedRef.current(pending, notice.runId);
777
+ }
778
+ if (notice.status === "removed") pendingRef.current.delete(notice.messageId);
779
+ void refresh().catch(() => void 0);
780
+ return true;
781
+ }, [refresh]);
782
+ const submit = React2.useCallback(async (input) => {
783
+ const selectedThreadId = threadRef.current;
784
+ if (!selectedThreadId) throw new Error("No active Agent thread is available.");
785
+ const result = await transportRef.current.submit({ ...input, threadId: selectedThreadId });
786
+ if (threadRef.current !== selectedThreadId) return result;
787
+ versionRef.current += 1;
788
+ if (!result.run_id && ["queued", "steer_pending", "blocked"].includes(result.status)) {
789
+ const now = (/* @__PURE__ */ new Date()).toISOString();
790
+ const pending = {
791
+ id: result.message_id,
792
+ thread_id: selectedThreadId,
793
+ sequence: Number.MAX_SAFE_INTEGER,
794
+ run_id: null,
795
+ turn_id: null,
796
+ segment_index: null,
797
+ role: "user",
798
+ status: result.status,
799
+ requested_mode: input.mode,
800
+ text: input.input,
801
+ attachment_ids: input.attachmentIds || [],
802
+ response_blocks: [],
803
+ revision: 1,
804
+ created_at: now,
805
+ updated_at: now
806
+ };
807
+ pendingRef.current.set(pending.id, pending);
808
+ setPendingMessages((current) => current.some((message) => message.id === pending.id) ? current : [...current, pending]);
809
+ setMessageQueue((current) => ({
810
+ pending_count: Math.max(current?.pending_count || 0, result.queue_position || 1),
811
+ paused: current?.paused || false,
812
+ reason: current?.reason || null
813
+ }));
814
+ }
815
+ void refresh().catch(() => void 0);
816
+ return result;
817
+ }, [refresh]);
818
+ const edit = React2.useCallback(async (message, input) => {
819
+ const updated = await transportRef.current.edit(message, input);
820
+ versionRef.current += 1;
821
+ pendingRef.current.set(updated.id, updated);
822
+ await refresh(message.thread_id);
823
+ return updated;
824
+ }, [refresh]);
825
+ const remove = React2.useCallback(async (message) => {
826
+ await transportRef.current.remove(message);
827
+ versionRef.current += 1;
828
+ pendingRef.current.delete(message.id);
829
+ setPendingMessages((current) => current.filter((item) => item.id !== message.id));
830
+ await refresh(message.thread_id);
831
+ }, [refresh]);
832
+ const retractForEdit = React2.useCallback(async (message) => {
833
+ await remove(message);
834
+ return { text: message.text, attachment_ids: message.attachment_ids, attachments: message.attachments };
835
+ }, [remove]);
836
+ const resume = React2.useCallback(async () => {
837
+ const selectedThreadId = threadRef.current;
838
+ if (!selectedThreadId) return null;
839
+ const result = await transportRef.current.resume(selectedThreadId);
840
+ await refresh(selectedThreadId);
841
+ return result;
842
+ }, [refresh]);
843
+ React2.useEffect(() => {
844
+ versionRef.current += 1;
845
+ cursorRef.current = 0;
846
+ promotedRef.current.clear();
847
+ pendingRef.current.clear();
848
+ setPendingMessages([]);
849
+ setMessageQueue(null);
850
+ if (threadId) void refresh(threadId).catch(() => void 0);
851
+ }, [threadId, refresh]);
852
+ React2.useEffect(() => {
853
+ if (!threadId || !pendingMessages.length || messageQueue?.paused) return;
854
+ const clock = globalThis;
855
+ const timer = clock.setInterval(() => {
856
+ void refresh(threadId).catch(() => void 0);
857
+ }, 3e3);
858
+ return () => clock.clearInterval(timer);
859
+ }, [threadId, pendingMessages.length, messageQueue?.paused, refresh]);
860
+ return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, submit, edit, remove, retractForEdit, resume };
861
+ }
862
+
863
+ // src/use-agent-chat.ts
864
+ import * as React6 from "react";
661
865
 
662
866
  // src/runtime-event-buffer.ts
663
867
  function runtimeEventFrameScheduler() {
@@ -707,6 +911,219 @@ function createRuntimeEventFrameBuffer(apply, scheduleFrame) {
707
911
  };
708
912
  }
709
913
 
914
+ // src/thread-summary-subscription.ts
915
+ function waitForReconnect(signal, delay) {
916
+ return new Promise((resolve) => {
917
+ if (signal.aborted) return resolve();
918
+ const clock = globalThis;
919
+ const finish = () => {
920
+ clock.clearTimeout(timer);
921
+ signal.removeEventListener("abort", finish);
922
+ resolve();
923
+ };
924
+ const timer = clock.setTimeout(finish, delay);
925
+ signal.addEventListener("abort", finish, { once: true });
926
+ });
927
+ }
928
+ async function subscribeToThreadSummaries(input) {
929
+ let cursor;
930
+ let failures = 0;
931
+ while (!input.signal.aborted) {
932
+ let refreshedFromSnapshot = false;
933
+ try {
934
+ await input.client.threads.events({ cursor, signal: input.signal }, async (event) => {
935
+ if (event.event === "snapshot_required") {
936
+ await input.refresh();
937
+ cursor = void 0;
938
+ refreshedFromSnapshot = true;
939
+ return;
940
+ }
941
+ cursor = event.cursor;
942
+ failures = 0;
943
+ await input.onEvent(event);
944
+ });
945
+ if (input.signal.aborted) break;
946
+ if (!refreshedFromSnapshot) await input.refresh();
947
+ input.onRecovered();
948
+ failures = 0;
949
+ } catch (error) {
950
+ if (input.signal.aborted || isAbortLike(error)) break;
951
+ input.onError(error);
952
+ failures += 1;
953
+ try {
954
+ await input.refresh();
955
+ input.onRecovered();
956
+ } catch (refreshError) {
957
+ if (!input.signal.aborted && !isAbortLike(refreshError)) input.onError(refreshError);
958
+ }
959
+ }
960
+ if (!input.signal.aborted) {
961
+ await waitForReconnect(input.signal, Math.min(3e4, 1e3 * 2 ** Math.min(failures, 5)));
962
+ }
963
+ }
964
+ }
965
+
966
+ // src/use-agent-chat-thread-list.ts
967
+ import * as React3 from "react";
968
+ function summaryToStored(thread) {
969
+ return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
970
+ }
971
+ function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation }) {
972
+ const [isLoadingThreads, setIsLoadingThreads] = React3.useState(false);
973
+ const [isLoadingMoreThreads, setIsLoadingMoreThreads] = React3.useState(false);
974
+ const [threadTotal, setThreadTotal] = React3.useState();
975
+ const refreshPromiseRef = React3.useRef(null);
976
+ const eventsDuringRefreshRef = React3.useRef(null);
977
+ const refreshThreads = React3.useCallback(() => {
978
+ if (refreshPromiseRef.current) return refreshPromiseRef.current;
979
+ clearOperationError();
980
+ setIsLoadingThreads(true);
981
+ eventsDuringRefreshRef.current = [];
982
+ const request = (async () => {
983
+ try {
984
+ const result = await client.threads.list({ limit: threadPageSize });
985
+ const concurrentEvents = eventsDuringRefreshRef.current || [];
986
+ eventsDuringRefreshRef.current = null;
987
+ setThreadTotal(result.total);
988
+ update((current) => {
989
+ const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
990
+ const incoming = result.items.map((item) => {
991
+ const existing = byId.get(item.id);
992
+ return { ...existing, ...summaryToStored(item), messages: existing?.messages || [], isHydrated: existing?.isHydrated };
993
+ });
994
+ return { ...current, threads: reconcileRefreshedThreads(current.threads, incoming, current.activeThreadId, concurrentEvents) };
995
+ });
996
+ } catch (error) {
997
+ if (!isAbortLike(error)) {
998
+ failOperation("refresh_threads", error);
999
+ throw error;
1000
+ }
1001
+ } finally {
1002
+ eventsDuringRefreshRef.current = null;
1003
+ setIsLoadingThreads(false);
1004
+ }
1005
+ })();
1006
+ refreshPromiseRef.current = request;
1007
+ void request.finally(() => {
1008
+ if (refreshPromiseRef.current === request) refreshPromiseRef.current = null;
1009
+ }).catch(() => void 0);
1010
+ return request;
1011
+ }, [clearOperationError, client, failOperation, threadPageSize, update]);
1012
+ const loadMoreThreads = React3.useCallback(async () => {
1013
+ if (isLoadingMoreThreads) return;
1014
+ const skip = stateRef.current.threads.length;
1015
+ if (threadTotal !== void 0 && skip >= threadTotal) return;
1016
+ clearOperationError();
1017
+ setIsLoadingMoreThreads(true);
1018
+ try {
1019
+ const result = await client.threads.list({ skip, limit: threadPageSize });
1020
+ setThreadTotal(result.total);
1021
+ update((current) => {
1022
+ const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
1023
+ for (const item of result.items) {
1024
+ const existing = byId.get(item.id);
1025
+ byId.set(item.id, preferNewestThreadSummary(existing, {
1026
+ ...existing,
1027
+ ...summaryToStored(item),
1028
+ messages: existing?.messages || [],
1029
+ isHydrated: existing?.isHydrated
1030
+ }));
1031
+ }
1032
+ return { ...current, threads: [...byId.values()] };
1033
+ });
1034
+ } catch (error) {
1035
+ if (!isAbortLike(error)) {
1036
+ failOperation("load_more_threads", error);
1037
+ throw error;
1038
+ }
1039
+ } finally {
1040
+ setIsLoadingMoreThreads(false);
1041
+ }
1042
+ }, [clearOperationError, client, failOperation, isLoadingMoreThreads, stateRef, threadPageSize, threadTotal, update]);
1043
+ return { isLoadingThreads, isLoadingMoreThreads, threadTotal, eventsDuringRefreshRef, refreshThreads, loadMoreThreads };
1044
+ }
1045
+
1046
+ // src/use-agent-chat-feedback.ts
1047
+ import * as React4 from "react";
1048
+ function useAgentChatFeedback({ client, update, clearOperationError, failOperation }) {
1049
+ return React4.useCallback(async (message, input) => {
1050
+ const runId = message.runId;
1051
+ if (!runId || message.role !== "assistant" || message.isFinal === false) {
1052
+ const error = new Error("Only persisted final responses can receive feedback.");
1053
+ failOperation("set_feedback", error);
1054
+ throw error;
1055
+ }
1056
+ const previous = message.feedback ?? null;
1057
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1058
+ const optimistic = input.rating ? {
1059
+ rating: input.rating,
1060
+ reason: input.rating === "dislike" ? input.reason ?? null : null,
1061
+ comment: input.rating === "dislike" ? input.comment?.trim() || null : null,
1062
+ created_at: previous?.created_at || timestamp,
1063
+ updated_at: timestamp
1064
+ } : null;
1065
+ const replaceFeedback = (current, feedback) => ({
1066
+ ...current,
1067
+ messages: current.messages.map((item) => item.id === message.id ? { ...item, feedback } : item),
1068
+ threads: current.threads.map((thread) => ({
1069
+ ...thread,
1070
+ messages: thread.messages.map((item) => item.id === message.id ? { ...item, feedback } : item)
1071
+ }))
1072
+ });
1073
+ clearOperationError();
1074
+ update((current) => replaceFeedback(current, optimistic));
1075
+ try {
1076
+ const result = await client.runs.setFeedback({ runId, ...input });
1077
+ update((current) => replaceFeedback(current, result.feedback));
1078
+ } catch (error) {
1079
+ update((current) => withAgentChatOperationError(replaceFeedback(current, previous), "set_feedback", error));
1080
+ throw error;
1081
+ }
1082
+ }, [clearOperationError, client, failOperation, update]);
1083
+ }
1084
+
1085
+ // src/use-agent-chat-queue-actions.ts
1086
+ import * as React5 from "react";
1087
+ function useAgentChatQueueActions({
1088
+ queue,
1089
+ stateRef,
1090
+ createId,
1091
+ clearOperationError,
1092
+ failOperation,
1093
+ refreshMessages,
1094
+ attachRun
1095
+ }) {
1096
+ const steer = React5.useCallback(async (text2) => {
1097
+ const threadId = stateRef.current.activeThreadId;
1098
+ if (!threadId || !text2.trim()) throw new Error("An active thread and text are required to steer.");
1099
+ clearOperationError();
1100
+ try {
1101
+ await queue.submit({ mode: "steer", input: text2.trim(), idempotencyKey: createId() });
1102
+ await refreshMessages(threadId);
1103
+ } catch (error) {
1104
+ failOperation("submit", error);
1105
+ throw error;
1106
+ }
1107
+ }, [clearOperationError, createId, failOperation, queue.submit, refreshMessages, stateRef]);
1108
+ const editPendingMessage = React5.useCallback(async (message, input) => {
1109
+ await queue.edit(message, {
1110
+ text: input.text,
1111
+ attachmentIds: input.attachmentIds ?? message.attachment_ids,
1112
+ requestedAgentId: input.requestedAgentId,
1113
+ requestedModelId: input.requestedModelId
1114
+ });
1115
+ }, [queue.edit]);
1116
+ const steerPendingMessage = React5.useCallback(async (message) => {
1117
+ await queue.edit(message, { text: message.text, attachmentIds: message.attachment_ids, mode: "steer" });
1118
+ }, [queue.edit]);
1119
+ const resumeQueue = React5.useCallback(async () => {
1120
+ if (!stateRef.current.activeThreadId) return;
1121
+ const result = await queue.resume();
1122
+ if (result?.run_id) void attachRun(result.run_id).catch(() => void 0);
1123
+ }, [attachRun, queue.resume, stateRef]);
1124
+ return { steer, editPendingMessage, steerPendingMessage, removePendingMessage: queue.remove, resumeQueue };
1125
+ }
1126
+
710
1127
  // src/initial-thread.ts
711
1128
  function resolveInitialActiveThreadId(options, storedThreadId) {
712
1129
  return Object.prototype.hasOwnProperty.call(options, "activeThreadId") ? options.activeThreadId ?? null : storedThreadId;
@@ -719,9 +1136,6 @@ function isCurrentThreadOpenRequest(input) {
719
1136
  }
720
1137
 
721
1138
  // src/use-agent-chat.ts
722
- function summaryToStored(thread) {
723
- return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
724
- }
725
1139
  function activeRunIdFromThread(thread) {
726
1140
  if (!thread) return null;
727
1141
  const activity = getThreadActivity(thread);
@@ -732,7 +1146,7 @@ function useAgentChat(options = {}) {
732
1146
  const client = options.client || runtime.client;
733
1147
  const pageSize = options.pageSize ?? 30;
734
1148
  const threadPageSize = options.threadPageSize ?? 50;
735
- const [state, setState] = React2.useState(() => ({
1149
+ const [state, setState] = React6.useState(() => ({
736
1150
  ...initialAgentChatState,
737
1151
  activeThreadId: resolveInitialActiveThreadId(
738
1152
  options,
@@ -740,24 +1154,25 @@ function useAgentChat(options = {}) {
740
1154
  ),
741
1155
  threads: runtime.storage?.loadThreads() ?? []
742
1156
  }));
743
- const [isLoadingThreads, setIsLoadingThreads] = React2.useState(false);
744
- const [isLoadingMoreThreads, setIsLoadingMoreThreads] = React2.useState(false);
745
- const [threadTotal, setThreadTotal] = React2.useState();
746
- const [isLoadingHistory, setIsLoadingHistory] = React2.useState(false);
747
- const [isLoadingOlder, setIsLoadingOlder] = React2.useState(false);
748
- const [isSubmitting, setIsSubmitting] = React2.useState(false);
749
- const streamControllerRef = React2.useRef(null);
750
- const threadOpenControllerRef = React2.useRef(null);
751
- const threadOpenGenerationRef = React2.useRef(0);
752
- const refreshThreadsPromiseRef = React2.useRef(null);
753
- const attachRunRef = React2.useRef(async () => void 0);
754
- const stateRef = React2.useRef(state);
1157
+ const [isLoadingHistory, setIsLoadingHistory] = React6.useState(false);
1158
+ const [isLoadingOlder, setIsLoadingOlder] = React6.useState(false);
1159
+ const [isSubmitting, setIsSubmitting] = React6.useState(false);
1160
+ const streamControllerRef = React6.useRef(null);
1161
+ const threadOpenControllerRef = React6.useRef(null);
1162
+ const threadOpenGenerationRef = React6.useRef(0);
1163
+ const attachRunRef = React6.useRef(async () => void 0);
1164
+ const messageRefreshVersionRef = React6.useRef(0);
1165
+ const promotedRunHintRef = React6.useRef(null);
1166
+ const promotedAttachRef = React6.useRef(null);
1167
+ const firstSubmissionRef = React6.useRef(null);
1168
+ const firstSubmissionAdmittedRef = React6.useRef(null);
1169
+ const stateRef = React6.useRef(state);
755
1170
  stateRef.current = state;
756
- const persist = React2.useCallback((next) => {
1171
+ const persist = React6.useCallback((next) => {
757
1172
  runtime.storage?.saveThreads(next.threads);
758
1173
  runtime.storage?.saveActiveThreadId?.(next.activeThreadId);
759
1174
  }, [runtime.storage]);
760
- const update = React2.useCallback((updater) => {
1175
+ const update = React6.useCallback((updater) => {
761
1176
  setState((current) => {
762
1177
  const next = updater(current);
763
1178
  stateRef.current = next;
@@ -765,116 +1180,121 @@ function useAgentChat(options = {}) {
765
1180
  return next;
766
1181
  });
767
1182
  }, [persist]);
768
- const eventBuffer = React2.useMemo(
1183
+ const queueTransport = React6.useMemo(() => ({
1184
+ list: (threadId) => client.threads.messages.list({ threadId, limit: pageSize }),
1185
+ submit: (input) => client.threads.messages.submit(input),
1186
+ edit: (message, input) => client.threads.messages.edit({
1187
+ threadId: message.thread_id,
1188
+ messageId: message.id,
1189
+ revision: message.revision,
1190
+ input: input.text,
1191
+ attachmentIds: input.attachmentIds ?? message.attachment_ids,
1192
+ mode: input.mode,
1193
+ requestedAgentId: input.requestedAgentId,
1194
+ requestedModelId: input.requestedModelId
1195
+ }),
1196
+ remove: async (message) => {
1197
+ await client.threads.messages.remove({ threadId: message.thread_id, messageId: message.id, revision: message.revision });
1198
+ },
1199
+ resume: (threadId) => client.threads.resumeQueue({ threadId })
1200
+ }), [client, pageSize]);
1201
+ const onQueuePromotion = React6.useCallback((message, runId) => {
1202
+ if (stateRef.current.activeThreadId !== message.thread_id) return;
1203
+ promotedRunHintRef.current = runId;
1204
+ const userMessage = threadTimelineToMessages([{ ...message, run_id: runId, status: "delivered" }])[0];
1205
+ update((current) => current.activeThreadId === message.thread_id && !current.messages.some((item) => item.id === userMessage.id) ? { ...current, messages: [...current.messages, userMessage] } : current);
1206
+ if (stateRef.current.activeRunId !== runId && promotedAttachRef.current !== runId) {
1207
+ promotedAttachRef.current = runId;
1208
+ void attachRunRef.current(runId).catch(() => {
1209
+ promotedAttachRef.current = null;
1210
+ });
1211
+ }
1212
+ }, [update]);
1213
+ const queue = useThreadMessageQueue({ threadId: state.activeThreadId, transport: queueTransport, onPromoted: onQueuePromotion });
1214
+ const { pendingMessages, messageQueue } = queue;
1215
+ const eventBuffer = React6.useMemo(
769
1216
  () => createRuntimeEventFrameBuffer(
770
1217
  (events) => update((current) => applyRuntimeEvents(current, events)),
771
1218
  runtimeEventFrameScheduler()
772
1219
  ),
773
1220
  [update]
774
1221
  );
775
- const clearOperationError = React2.useCallback((operation) => {
1222
+ const clearOperationError = React6.useCallback((operation) => {
776
1223
  update((current) => clearAgentChatOperationError(current, operation));
777
1224
  }, [update]);
778
- const clearBackgroundError = React2.useCallback(() => {
1225
+ const clearBackgroundError = React6.useCallback(() => {
779
1226
  update(clearAgentChatBackgroundError);
780
1227
  }, [update]);
781
- const failOperation = React2.useCallback((operation, cause) => {
1228
+ const failOperation = React6.useCallback((operation, cause) => {
782
1229
  update((current) => withAgentChatOperationError(current, operation, cause));
783
1230
  }, [update]);
784
- const invalidateThreadOpen = React2.useCallback(() => {
1231
+ const {
1232
+ isLoadingThreads,
1233
+ isLoadingMoreThreads,
1234
+ threadTotal,
1235
+ eventsDuringRefreshRef: threadEventsDuringRefreshRef,
1236
+ refreshThreads,
1237
+ loadMoreThreads
1238
+ } = useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation });
1239
+ const invalidateThreadOpen = React6.useCallback(() => {
785
1240
  threadOpenGenerationRef.current += 1;
786
1241
  threadOpenControllerRef.current?.abort("Thread selection changed.");
787
1242
  threadOpenControllerRef.current = null;
788
1243
  setIsLoadingHistory(false);
789
1244
  }, []);
790
- const detach = React2.useCallback(() => {
1245
+ const detach = React6.useCallback(() => {
791
1246
  eventBuffer.flush();
792
1247
  streamControllerRef.current?.abort();
793
1248
  streamControllerRef.current = null;
794
1249
  update((current) => ({ ...current, streamingMessageId: null, runState: current.activeRunId ? "reconnecting" : "idle" }));
795
1250
  }, [eventBuffer, update]);
796
- React2.useEffect(() => () => {
1251
+ const refreshMessages = React6.useCallback(async (threadId, promotedRunId) => {
1252
+ const target = threadId || stateRef.current.activeThreadId;
1253
+ if (!target) return;
1254
+ const version = ++messageRefreshVersionRef.current;
1255
+ const page = await client.threads.messages.list({ threadId: target, limit: pageSize });
1256
+ if (stateRef.current.activeThreadId !== target || version !== messageRefreshVersionRef.current) return;
1257
+ queue.hydrate(page, target);
1258
+ const promoted = promotedRunId || promotedRunHintRef.current;
1259
+ if (promoted && page.items.some((message) => message.role === "user" && message.run_id === promoted)) {
1260
+ promotedRunHintRef.current = null;
1261
+ update((current) => {
1262
+ if (current.activeThreadId !== target) return current;
1263
+ const canonical = preserveMountedMessageIds(threadTimelineToMessages(page.items), current.messages);
1264
+ const canonicalIds = new Set(canonical.map((message) => message.id));
1265
+ const older = current.messages.filter((message) => !canonicalIds.has(message.id) && message.id !== current.streamingMessageId);
1266
+ const live = current.activeRunId === promoted ? current.messages.filter((message) => message.id === current.streamingMessageId) : [];
1267
+ return {
1268
+ ...current,
1269
+ messages: [...older, ...canonical, ...live.filter((message) => !canonicalIds.has(message.id))],
1270
+ streamingMessageId: live.length ? current.streamingMessageId : null
1271
+ };
1272
+ });
1273
+ return;
1274
+ }
1275
+ if (stateRef.current.activeRunId) {
1276
+ const delivered = threadTimelineToMessages(page.items.filter((item) => item.role === "user" && item.run_id === stateRef.current.activeRunId && (item.segment_index ?? 0) > 0));
1277
+ if (delivered.length) update((current) => {
1278
+ if (current.activeThreadId !== target) return current;
1279
+ const additions = delivered.filter((item) => !current.messages.some((existing) => existing.id === item.id));
1280
+ return additions.length ? { ...current, messages: [...current.messages, ...additions] } : current;
1281
+ });
1282
+ }
1283
+ if (!stateRef.current.activeRunId && page.items.length) {
1284
+ update((current) => current.activeThreadId === target && !current.activeRunId ? (() => {
1285
+ const messages = preserveMountedMessageIds(threadTimelineToMessages(page.items), current.messages);
1286
+ return { ...current, messages, threads: current.threads.map((thread) => thread.id === target ? { ...thread, messages } : thread) };
1287
+ })() : current);
1288
+ }
1289
+ }, [client, pageSize, queue.hydrate, update]);
1290
+ React6.useEffect(() => () => {
797
1291
  eventBuffer.dispose();
798
1292
  streamControllerRef.current?.abort();
799
1293
  threadOpenControllerRef.current?.abort();
800
1294
  }, [eventBuffer]);
801
- const refreshThreads = React2.useCallback(() => {
802
- if (refreshThreadsPromiseRef.current) return refreshThreadsPromiseRef.current;
803
- clearOperationError();
804
- setIsLoadingThreads(true);
805
- const request = (async () => {
806
- try {
807
- const result = await client.threads.list({ limit: threadPageSize });
808
- setThreadTotal(result.total);
809
- update((current) => {
810
- const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
811
- const nextThreads = result.items.map((item) => {
812
- const existing = byId.get(item.id);
813
- return { ...existing, ...summaryToStored(item), messages: existing?.messages || [], isHydrated: existing?.isHydrated };
814
- });
815
- return {
816
- ...current,
817
- threads: reconcileRefreshedThreads(
818
- current.threads,
819
- nextThreads,
820
- current.activeThreadId
821
- )
822
- };
823
- });
824
- } catch (error) {
825
- if (!isAbortLike(error)) {
826
- failOperation("refresh_threads", error);
827
- throw error;
828
- }
829
- } finally {
830
- setIsLoadingThreads(false);
831
- }
832
- })();
833
- refreshThreadsPromiseRef.current = request;
834
- void request.then(
835
- () => {
836
- if (refreshThreadsPromiseRef.current === request) refreshThreadsPromiseRef.current = null;
837
- },
838
- () => {
839
- if (refreshThreadsPromiseRef.current === request) refreshThreadsPromiseRef.current = null;
840
- }
841
- );
842
- return request;
843
- }, [clearOperationError, client, failOperation, threadPageSize, update]);
844
- const loadMoreThreads = React2.useCallback(async () => {
845
- if (isLoadingMoreThreads) return;
846
- const skip = stateRef.current.threads.length;
847
- if (threadTotal !== void 0 && skip >= threadTotal) return;
848
- clearOperationError();
849
- setIsLoadingMoreThreads(true);
850
- try {
851
- const result = await client.threads.list({ skip, limit: threadPageSize });
852
- setThreadTotal(result.total);
853
- update((current) => {
854
- const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
855
- for (const item of result.items) {
856
- const existing = byId.get(item.id);
857
- byId.set(item.id, {
858
- ...existing,
859
- ...summaryToStored(item),
860
- messages: existing?.messages || [],
861
- isHydrated: existing?.isHydrated
862
- });
863
- }
864
- return { ...current, threads: [...byId.values()] };
865
- });
866
- } catch (error) {
867
- if (!isAbortLike(error)) {
868
- failOperation("load_more_threads", error);
869
- throw error;
870
- }
871
- } finally {
872
- setIsLoadingMoreThreads(false);
873
- }
874
- }, [clearOperationError, client, failOperation, isLoadingMoreThreads, threadPageSize, threadTotal, update]);
875
- const openThread = React2.useCallback(async (threadId) => {
1295
+ const openThread = React6.useCallback(async (threadId) => {
876
1296
  const previousSelection = captureAgentChatThreadSelection(stateRef.current);
877
- threadOpenControllerRef.current?.abort("A newer thread was selected.");
1297
+ threadOpenControllerRef.current?.abort();
878
1298
  const requestController = runtime.createAbortController();
879
1299
  threadOpenControllerRef.current = requestController;
880
1300
  const requestGeneration = threadOpenGenerationRef.current + 1;
@@ -908,7 +1328,16 @@ function useAgentChat(options = {}) {
908
1328
  requestedThreadId: threadId,
909
1329
  signalAborted: requestController.signal.aborted
910
1330
  })) return;
911
- const messages = threadDetailToMessages(detail);
1331
+ const timeline = await client.threads.messages.list({ threadId, limit: pageSize, signal: requestController.signal });
1332
+ if (!isCurrentThreadOpenRequest({
1333
+ activeThreadId: stateRef.current.activeThreadId,
1334
+ currentGeneration: threadOpenGenerationRef.current,
1335
+ requestGeneration,
1336
+ requestedThreadId: threadId,
1337
+ signalAborted: requestController.signal.aborted
1338
+ })) return;
1339
+ queue.hydrate(timeline, threadId);
1340
+ const messages = timeline.items.length ? threadTimelineToMessages(timeline.items, detail) : threadDetailToMessages(detail);
912
1341
  const context = latestThreadContext(detail);
913
1342
  const stored = {
914
1343
  ...detail,
@@ -942,7 +1371,7 @@ function useAgentChat(options = {}) {
942
1371
  signalAborted: requestController.signal.aborted
943
1372
  })) await attachRunRef.current(activeRunId).catch(() => void 0);
944
1373
  } catch (error) {
945
- if (!isAbortLike(error) && requestGeneration === threadOpenGenerationRef.current) {
1374
+ if (!requestController.signal.aborted && !isAbortLike(error) && requestGeneration === threadOpenGenerationRef.current) {
946
1375
  failOperation("open_thread", error);
947
1376
  update((current) => restoreAgentChatThreadSelection(current, previousSelection));
948
1377
  if (previousSelection.activeRunId) {
@@ -956,15 +1385,23 @@ function useAgentChat(options = {}) {
956
1385
  setIsLoadingHistory(false);
957
1386
  }
958
1387
  }
959
- }, [clearOperationError, client, detach, failOperation, pageSize, runtime, update]);
960
- const consumeEvent = React2.useCallback((event) => {
1388
+ }, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update]);
1389
+ const consumeEvent = React6.useCallback((event) => {
1390
+ if (event.event === "run.accepted" && !stateRef.current.activeThreadId && typeof event.payload?.message_id === "string") {
1391
+ const durableId = event.payload.message_id;
1392
+ update((current) => ({
1393
+ ...current,
1394
+ messages: current.messages.map((message) => message.role === "user" && !message.runId && !message.durableId ? { ...message, durableId, runId: event.run_id } : message.role === "assistant" && !message.runId ? { ...message, runId: event.run_id } : message)
1395
+ }));
1396
+ }
961
1397
  const eventThreadId = typeof event.payload?.thread_id === "string" ? event.payload.thread_id.trim() : "";
962
1398
  if (eventThreadId && !stateRef.current.activeThreadId) {
963
1399
  update((current) => adoptStreamingThread(current, eventThreadId));
1400
+ firstSubmissionAdmittedRef.current?.();
964
1401
  }
965
1402
  eventBuffer.push(event);
966
1403
  }, [eventBuffer, update]);
967
- const attachRun = React2.useCallback(async (runId, cursor) => {
1404
+ const attachRun = React6.useCallback(async (runId, cursor) => {
968
1405
  streamControllerRef.current?.abort();
969
1406
  const controller = runtime.createAbortController();
970
1407
  streamControllerRef.current = controller;
@@ -972,7 +1409,9 @@ function useAgentChat(options = {}) {
972
1409
  update((current) => ({ ...current, activeRunId: runId, runState: "reconnecting", error: null }));
973
1410
  try {
974
1411
  const result = await client.runs.attach({ runId, cursor, signal: controller.signal }, consumeEvent);
1412
+ eventBuffer.flush();
975
1413
  update((current) => ({ ...current, activeRunId: result.detached ? runId : current.activeRunId, cursor: result.cursor ?? current.cursor, runState: result.detached ? "reconnecting" : current.runState }));
1414
+ if (!result.detached && result.threadId) await refreshMessages(result.threadId);
976
1415
  } catch (error) {
977
1416
  if (!isAbortLike(error)) {
978
1417
  update((current) => ({ ...current, runState: "failed", streamingMessageId: null }));
@@ -982,100 +1421,171 @@ function useAgentChat(options = {}) {
982
1421
  } finally {
983
1422
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
984
1423
  }
985
- }, [clearOperationError, client, consumeEvent, failOperation, runtime, update]);
1424
+ }, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update]);
986
1425
  attachRunRef.current = attachRun;
987
- const submit = React2.useCallback(async (input) => {
1426
+ const submit = React6.useCallback(async (input) => {
988
1427
  if (!input.text.trim() && !input.attachmentIds?.length && !input.uploads?.length) return;
989
- invalidateThreadOpen();
990
- detach();
991
- const controller = runtime.createAbortController();
992
- streamControllerRef.current = controller;
993
- const userId = runtime.createId();
994
- const assistantId = runtime.createId();
995
- const idempotencyKey = runtime.createId();
996
- const optimisticAttachments = input.attachments?.length ? input.attachments : input.uploads?.map((upload, index) => ({
997
- id: `${userId}-attachment-${index}`,
998
- filename: upload.name,
999
- mediaType: upload.mediaType,
1000
- status: "uploading"
1001
- }));
1002
- 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 };
1003
- const assistantMessage = { id: assistantId, role: "assistant", content: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), isFinal: false, parts: [] };
1004
- clearOperationError();
1005
- const hasPendingUploads = Boolean(input.uploads?.length);
1006
- update((current) => ({
1007
- ...current,
1008
- messages: appendStableStreamingTurn(current.messages, userMessage, assistantMessage),
1009
- streamingMessageId: assistantId,
1010
- runState: hasPendingUploads ? "uploading" : "streaming",
1011
- error: null
1012
- }));
1013
- setIsSubmitting(true);
1014
- let streamStarted = false;
1015
- try {
1016
- const uploaded = hasPendingUploads ? await client.attachments.uploadBatch({
1017
- uploads: input.uploads || [],
1018
- threadId: stateRef.current.activeThreadId || void 0,
1019
- requestedAgentId: input.requestedAgentId,
1020
- idempotencyKey: runtime.createId(),
1021
- signal: controller.signal
1022
- }) : [];
1023
- if (hasPendingUploads && uploaded.length !== input.uploads?.length) {
1024
- throw new Error("Attachment upload returned an incomplete batch");
1025
- }
1026
- const resolvedAttachments = uploaded.length ? uploaded.map(attachmentResultToChatAttachment) : input.attachments;
1027
- const attachmentIds = uploaded.length ? uploaded.map((attachment) => attachment.id) : input.attachmentIds;
1028
- if (uploaded.length) {
1029
- update((current) => ({
1030
- ...current,
1031
- messages: current.messages.map((message) => message.id === userId ? { ...message, attachments: resolvedAttachments } : message),
1032
- runState: "streaming"
1033
- }));
1034
- }
1035
- streamStarted = true;
1036
- 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);
1037
- eventBuffer.flush();
1038
- update((current) => {
1039
- const threadId = result.threadId || current.activeThreadId;
1040
- return {
1041
- ...current,
1042
- activeThreadId: threadId,
1043
- activeRunId: result.detached ? result.runId : current.activeRunId,
1044
- cursor: result.cursor,
1045
- runState: result.detached ? "reconnecting" : current.runState,
1046
- threads: threadId ? current.threads.map((thread) => thread.id === threadId ? { ...thread, messages: current.messages, isHydrated: true } : thread) : current.threads
1047
- };
1048
- });
1049
- if (result.threadId) await refreshThreads().catch(() => void 0);
1050
- } catch (error) {
1051
- if (isAbortLike(error) && !streamStarted) {
1052
- update((current) => current.streamingMessageId === assistantId ? {
1053
- ...current,
1054
- messages: current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
1055
- runState: "idle",
1056
- streamingMessageId: null
1057
- } : current);
1058
- } else if (!isAbortLike(error)) {
1059
- update((current) => ({
1060
- ...current,
1061
- messages: streamStarted ? current.messages : current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
1062
- runState: "failed",
1063
- streamingMessageId: null
1064
- }));
1428
+ if (firstSubmissionRef.current && !stateRef.current.activeThreadId) {
1429
+ await firstSubmissionRef.current;
1430
+ }
1431
+ const existingThreadId = stateRef.current.activeThreadId;
1432
+ if (existingThreadId) {
1433
+ clearOperationError();
1434
+ setIsSubmitting(true);
1435
+ try {
1436
+ const uploaded = input.uploads?.length ? await client.attachments.uploadBatch({
1437
+ uploads: input.uploads,
1438
+ threadId: existingThreadId,
1439
+ requestedAgentId: input.requestedAgentId,
1440
+ idempotencyKey: runtime.createId()
1441
+ }) : [];
1442
+ const result = await queue.submit({
1443
+ mode: "queue",
1444
+ input: input.text.trim(),
1445
+ attachmentIds: uploaded.length ? uploaded.map((item) => item.id) : input.attachmentIds,
1446
+ requestedAgentId: input.requestedAgentId,
1447
+ requestedModelId: input.requestedModelId,
1448
+ idempotencyKey: runtime.createId()
1449
+ });
1450
+ if (stateRef.current.activeThreadId !== existingThreadId) return;
1451
+ if (result.run_id) {
1452
+ const userMessage = {
1453
+ id: result.message_id,
1454
+ role: "user",
1455
+ runId: result.run_id,
1456
+ content: input.text.trim(),
1457
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1458
+ isFinal: true,
1459
+ parts: input.text.trim() ? [{ id: `${result.message_id}-text`, type: "text", kind: "text", text: input.text.trim() }] : [],
1460
+ attachments: input.attachments || []
1461
+ };
1462
+ update((current) => current.activeThreadId !== existingThreadId || current.messages.some((message) => message.id === userMessage.id) ? current : { ...current, messages: [...current.messages, userMessage] });
1463
+ }
1464
+ void refreshMessages(existingThreadId).catch(() => void 0);
1465
+ if (result.run_id && promotedAttachRef.current !== result.run_id && stateRef.current.activeRunId !== result.run_id) {
1466
+ void attachRunRef.current(result.run_id).catch(() => void 0);
1467
+ }
1468
+ } catch (error) {
1065
1469
  failOperation("submit", error);
1066
1470
  throw error;
1471
+ } finally {
1472
+ setIsSubmitting(false);
1473
+ }
1474
+ return;
1475
+ }
1476
+ const admission = new Promise((resolve) => {
1477
+ firstSubmissionAdmittedRef.current = resolve;
1478
+ });
1479
+ const stream = (async () => {
1480
+ invalidateThreadOpen();
1481
+ detach();
1482
+ const controller = runtime.createAbortController();
1483
+ streamControllerRef.current = controller;
1484
+ const userId = runtime.createId();
1485
+ const assistantId = runtime.createId();
1486
+ const idempotencyKey = runtime.createId();
1487
+ const optimisticAttachments = input.attachments?.length ? input.attachments : input.uploads?.map((upload, index) => ({
1488
+ id: `${userId}-attachment-${index}`,
1489
+ filename: upload.name,
1490
+ mediaType: upload.mediaType,
1491
+ status: "uploading"
1492
+ }));
1493
+ 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 };
1494
+ const assistantMessage = { id: assistantId, role: "assistant", content: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), isFinal: false, parts: [] };
1495
+ clearOperationError();
1496
+ const hasPendingUploads = Boolean(input.uploads?.length);
1497
+ update((current) => ({
1498
+ ...current,
1499
+ messages: appendStableStreamingTurn(current.messages, userMessage, assistantMessage),
1500
+ streamingMessageId: assistantId,
1501
+ runState: hasPendingUploads ? "uploading" : "streaming",
1502
+ error: null
1503
+ }));
1504
+ setIsSubmitting(true);
1505
+ let streamStarted = false;
1506
+ try {
1507
+ const uploaded = hasPendingUploads ? await client.attachments.uploadBatch({
1508
+ uploads: input.uploads || [],
1509
+ threadId: stateRef.current.activeThreadId || void 0,
1510
+ requestedAgentId: input.requestedAgentId,
1511
+ idempotencyKey: runtime.createId(),
1512
+ signal: controller.signal
1513
+ }) : [];
1514
+ if (hasPendingUploads && uploaded.length !== input.uploads?.length) {
1515
+ throw new Error("Attachment upload returned an incomplete batch");
1516
+ }
1517
+ const resolvedAttachments = uploaded.length ? uploaded.map(attachmentResultToChatAttachment) : input.attachments;
1518
+ const attachmentIds = uploaded.length ? uploaded.map((attachment) => attachment.id) : input.attachmentIds;
1519
+ if (uploaded.length) {
1520
+ update((current) => ({
1521
+ ...current,
1522
+ messages: current.messages.map((message) => message.id === userId ? { ...message, attachments: resolvedAttachments } : message),
1523
+ runState: "streaming"
1524
+ }));
1525
+ }
1526
+ streamStarted = true;
1527
+ 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);
1528
+ eventBuffer.flush();
1529
+ update((current) => {
1530
+ const threadId = result.threadId || current.activeThreadId;
1531
+ return {
1532
+ ...current,
1533
+ activeThreadId: threadId,
1534
+ activeRunId: result.detached ? result.runId : current.activeRunId,
1535
+ cursor: result.cursor,
1536
+ runState: result.detached ? "reconnecting" : current.runState,
1537
+ threads: threadId ? current.threads.map((thread) => thread.id === threadId ? { ...thread, messages: current.messages, isHydrated: true } : thread) : current.threads
1538
+ };
1539
+ });
1540
+ if (result.threadId) await refreshThreads().catch(() => void 0);
1541
+ if (result.threadId && !result.detached) await refreshMessages(result.threadId);
1542
+ } catch (error) {
1543
+ if (isAbortLike(error) && !streamStarted) {
1544
+ update((current) => current.streamingMessageId === assistantId ? {
1545
+ ...current,
1546
+ messages: current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
1547
+ runState: "idle",
1548
+ streamingMessageId: null
1549
+ } : current);
1550
+ } else if (!isAbortLike(error)) {
1551
+ update((current) => ({
1552
+ ...current,
1553
+ messages: streamStarted ? current.messages : current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
1554
+ runState: "failed",
1555
+ streamingMessageId: null
1556
+ }));
1557
+ failOperation("submit", error);
1558
+ throw error;
1559
+ }
1560
+ } finally {
1561
+ setIsSubmitting(false);
1562
+ if (streamControllerRef.current === controller) streamControllerRef.current = null;
1067
1563
  }
1564
+ })();
1565
+ const accepted = Promise.race([stream, admission]);
1566
+ firstSubmissionRef.current = accepted;
1567
+ try {
1568
+ await accepted;
1068
1569
  } finally {
1069
- setIsSubmitting(false);
1070
- if (streamControllerRef.current === controller) streamControllerRef.current = null;
1570
+ if (firstSubmissionRef.current === accepted) firstSubmissionRef.current = null;
1571
+ firstSubmissionAdmittedRef.current = null;
1071
1572
  }
1072
- }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, refreshThreads, runtime, update]);
1073
- const startNewThread = React2.useCallback(() => {
1573
+ }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update]);
1574
+ const { steer, editPendingMessage, steerPendingMessage, removePendingMessage, resumeQueue } = useAgentChatQueueActions({
1575
+ queue,
1576
+ stateRef,
1577
+ createId: runtime.createId,
1578
+ clearOperationError,
1579
+ failOperation,
1580
+ refreshMessages,
1581
+ attachRun
1582
+ });
1583
+ const startNewThread = React6.useCallback(() => {
1074
1584
  invalidateThreadOpen();
1075
1585
  detach();
1076
1586
  update((current) => ({ ...initialAgentChatState, threads: current.threads }));
1077
1587
  }, [detach, invalidateThreadOpen, update]);
1078
- const deleteThread = React2.useCallback(async (threadId) => {
1588
+ const deleteThread = React6.useCallback(async (threadId) => {
1079
1589
  if (stateRef.current.activeThreadId === threadId) invalidateThreadOpen();
1080
1590
  clearOperationError();
1081
1591
  try {
@@ -1096,7 +1606,7 @@ function useAgentChat(options = {}) {
1096
1606
  }
1097
1607
  }
1098
1608
  }, [clearOperationError, client, failOperation, invalidateThreadOpen, runtime, update]);
1099
- const cancelRun = React2.useCallback(async () => {
1609
+ const cancelRun = React6.useCallback(async () => {
1100
1610
  const runId = stateRef.current.activeRunId;
1101
1611
  if (!runId || stateRef.current.runState === "cancelling") return;
1102
1612
  const previousRunState = stateRef.current.runState;
@@ -1135,7 +1645,7 @@ function useAgentChat(options = {}) {
1135
1645
  }
1136
1646
  }
1137
1647
  }, [clearOperationError, client, detach, failOperation, runtime, update]);
1138
- const resumeHitl = React2.useCallback(async (part, response) => {
1648
+ const resumeHitl = React6.useCallback(async (part, response) => {
1139
1649
  const runId = stateRef.current.activeRunId || stateRef.current.messages.find((message) => message.parts.includes(part))?.runId;
1140
1650
  clearOperationError();
1141
1651
  if (!runId) {
@@ -1156,54 +1666,8 @@ function useAgentChat(options = {}) {
1156
1666
  throw error;
1157
1667
  }
1158
1668
  }, [attachRun, clearOperationError, client, failOperation, runtime]);
1159
- const setFeedback = React2.useCallback(async (message, input) => {
1160
- const runId = message.runId;
1161
- if (!runId || message.role !== "assistant" || message.isFinal === false) {
1162
- const error = new Error("Only persisted final responses can receive feedback.");
1163
- failOperation("set_feedback", error);
1164
- throw error;
1165
- }
1166
- const previous = message.feedback ?? null;
1167
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1168
- const optimistic = input.rating ? {
1169
- rating: input.rating,
1170
- reason: input.rating === "dislike" ? input.reason ?? null : null,
1171
- comment: input.rating === "dislike" ? input.comment?.trim() || null : null,
1172
- created_at: previous?.created_at || timestamp,
1173
- updated_at: timestamp
1174
- } : null;
1175
- const replaceFeedback = (current, feedback) => {
1176
- const messages = current.messages.map(
1177
- (item) => item.id === message.id ? { ...item, feedback } : item
1178
- );
1179
- return {
1180
- ...current,
1181
- messages,
1182
- threads: current.threads.map((thread) => ({
1183
- ...thread,
1184
- messages: thread.messages.map(
1185
- (item) => item.id === message.id ? { ...item, feedback } : item
1186
- )
1187
- }))
1188
- };
1189
- };
1190
- clearOperationError();
1191
- update((current) => replaceFeedback(current, optimistic));
1192
- try {
1193
- const result = await client.runs.setFeedback({ runId, ...input });
1194
- update((current) => replaceFeedback(current, result.feedback));
1195
- } catch (error) {
1196
- update(
1197
- (current) => withAgentChatOperationError(
1198
- replaceFeedback(current, previous),
1199
- "set_feedback",
1200
- error
1201
- )
1202
- );
1203
- throw error;
1204
- }
1205
- }, [clearOperationError, client, failOperation, update]);
1206
- const loadOlder = React2.useCallback(async () => {
1669
+ const setFeedback = useAgentChatFeedback({ client, update, clearOperationError, failOperation });
1670
+ const loadOlder = React6.useCallback(async () => {
1207
1671
  const thread = stateRef.current.threads.find((item) => item.id === stateRef.current.activeThreadId);
1208
1672
  if (!thread?.nextBeforeTurnIndex) return;
1209
1673
  clearOperationError();
@@ -1221,7 +1685,7 @@ function useAgentChat(options = {}) {
1221
1685
  setIsLoadingOlder(false);
1222
1686
  }
1223
1687
  }, [clearOperationError, client, failOperation, pageSize, update]);
1224
- const regenerate = React2.useCallback(async (message) => {
1688
+ const regenerate = React6.useCallback(async (message) => {
1225
1689
  const index = stateRef.current.messages.findIndex((item) => item.id === message.id);
1226
1690
  const user = stateRef.current.messages.slice(0, index).reverse().find((item) => item.role === "user");
1227
1691
  if (user) await submit({
@@ -1230,25 +1694,50 @@ function useAgentChat(options = {}) {
1230
1694
  attachments: user.attachments
1231
1695
  });
1232
1696
  }, [submit]);
1233
- React2.useEffect(() => {
1697
+ React6.useEffect(() => {
1234
1698
  const controller = runtime.createAbortController();
1235
1699
  clearBackgroundError();
1236
- void client.threads.events({ signal: controller.signal }, async (event) => {
1237
- if (event.event === "snapshot_required") {
1700
+ void subscribeToThreadSummaries({
1701
+ client,
1702
+ signal: controller.signal,
1703
+ refresh: refreshThreads,
1704
+ onEvent(event) {
1705
+ threadEventsDuringRefreshRef.current?.push(event);
1706
+ update((current) => ({
1707
+ ...clearAgentChatBackgroundError(current),
1708
+ threads: applyThreadSummaryEvent(current.threads, event)
1709
+ }));
1710
+ if (event.event === "thread.message.updated" && event.thread_id === stateRef.current.activeThreadId) {
1711
+ const wasQueued = queue.acceptNotice({
1712
+ messageId: event.message_id,
1713
+ status: event.status,
1714
+ runId: event.run_id,
1715
+ cursor: event.cursor
1716
+ });
1717
+ if (wasQueued) void refreshMessages(event.thread_id, event.run_id || void 0).catch(() => void 0);
1718
+ }
1719
+ if (event.event === "thread.summary.updated" && event.thread.id === stateRef.current.activeThreadId) {
1720
+ if (streamControllerRef.current && stateRef.current.messages.some((message) => message.role === "user" && !message.runId)) return;
1721
+ const promotedRunId = activeRunIdFromThread(summaryToStored(event.thread));
1722
+ void (async () => {
1723
+ await refreshMessages(String(event.thread.id), promotedRunId || void 0);
1724
+ if (promotedRunId && stateRef.current.activeRunId !== promotedRunId && stateRef.current.activeThreadId === String(event.thread.id) && promotedAttachRef.current !== promotedRunId) {
1725
+ promotedAttachRef.current = promotedRunId;
1726
+ await attachRunRef.current(promotedRunId);
1727
+ }
1728
+ })().catch(() => void 0);
1729
+ }
1730
+ },
1731
+ onError(error) {
1732
+ update((current) => withAgentChatBackgroundError(current, error));
1733
+ },
1734
+ onRecovered() {
1238
1735
  clearBackgroundError();
1239
- await refreshThreads();
1240
- return;
1241
1736
  }
1242
- update((current) => ({
1243
- ...clearAgentChatBackgroundError(current),
1244
- threads: applyThreadSummaryEvent(current.threads, event)
1245
- }));
1246
- }).catch((error) => {
1247
- if (!isAbortLike(error)) update((current) => withAgentChatBackgroundError(current, error));
1248
1737
  });
1249
1738
  return () => controller.abort();
1250
- }, [clearBackgroundError, client, refreshThreads, runtime.createAbortController, update]);
1251
- React2.useEffect(() => {
1739
+ }, [clearBackgroundError, client, queue.acceptNotice, refreshMessages, refreshThreads, runtime.createAbortController, update]);
1740
+ React6.useEffect(() => {
1252
1741
  const threadId = options.activeThreadId;
1253
1742
  const activeThread2 = stateRef.current.threads.find((thread) => thread.id === stateRef.current.activeThreadId);
1254
1743
  if (shouldHydrateSelectedThread(threadId, stateRef.current.activeThreadId, Boolean(activeThread2?.isHydrated))) {
@@ -1260,6 +1749,8 @@ function useAgentChat(options = {}) {
1260
1749
  return {
1261
1750
  ...state,
1262
1751
  activeThread,
1752
+ pendingMessages,
1753
+ messageQueue,
1263
1754
  hasOlderTurns: Boolean(activeThread?.hasOlderTurns),
1264
1755
  hasMoreThreads,
1265
1756
  isLoadingHistory,
@@ -1278,6 +1769,13 @@ function useAgentChat(options = {}) {
1278
1769
  openThread,
1279
1770
  refreshThreads,
1280
1771
  regenerate,
1772
+ refreshMessages,
1773
+ editPendingMessage,
1774
+ steerPendingMessage,
1775
+ removePendingMessage,
1776
+ retractPendingMessage: queue.retractForEdit,
1777
+ resumeQueue,
1778
+ steer,
1281
1779
  resumeHitl,
1282
1780
  setFeedback,
1283
1781
  startNewThread,
@@ -1286,15 +1784,15 @@ function useAgentChat(options = {}) {
1286
1784
  }
1287
1785
 
1288
1786
  // src/use-agent-resources.ts
1289
- import * as React3 from "react";
1787
+ import * as React7 from "react";
1290
1788
  function useAgentThreads(options = {}) {
1291
1789
  const { client, createAbortController } = useAgents24Runtime();
1292
- const [threads, setThreads] = React3.useState([]);
1293
- const [total, setTotal] = React3.useState();
1294
- const [isLoading, setIsLoading] = React3.useState(true);
1295
- const [error, setError] = React3.useState(null);
1296
- const controllerRef = React3.useRef(null);
1297
- const load = React3.useCallback(async () => {
1790
+ const [threads, setThreads] = React7.useState([]);
1791
+ const [total, setTotal] = React7.useState();
1792
+ const [isLoading, setIsLoading] = React7.useState(true);
1793
+ const [error, setError] = React7.useState(null);
1794
+ const controllerRef = React7.useRef(null);
1795
+ const load = React7.useCallback(async () => {
1298
1796
  controllerRef.current?.abort();
1299
1797
  const controller = createAbortController();
1300
1798
  controllerRef.current = controller;
@@ -1313,7 +1811,7 @@ function useAgentThreads(options = {}) {
1313
1811
  }
1314
1812
  }
1315
1813
  }, [client, createAbortController, options.limit, options.skip]);
1316
- React3.useEffect(() => {
1814
+ React7.useEffect(() => {
1317
1815
  void load();
1318
1816
  return () => controllerRef.current?.abort();
1319
1817
  }, [load]);
@@ -1321,11 +1819,11 @@ function useAgentThreads(options = {}) {
1321
1819
  }
1322
1820
  function useAgentThread(threadId, options = {}) {
1323
1821
  const { client, createAbortController } = useAgents24Runtime();
1324
- const [thread, setThread] = React3.useState(null);
1325
- const [isLoading, setIsLoading] = React3.useState(Boolean(threadId));
1326
- const [error, setError] = React3.useState(null);
1327
- const controllerRef = React3.useRef(null);
1328
- const load = React3.useCallback(async () => {
1822
+ const [thread, setThread] = React7.useState(null);
1823
+ const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
1824
+ const [error, setError] = React7.useState(null);
1825
+ const controllerRef = React7.useRef(null);
1826
+ const load = React7.useCallback(async () => {
1329
1827
  if (!threadId) {
1330
1828
  controllerRef.current?.abort();
1331
1829
  controllerRef.current = null;
@@ -1352,24 +1850,73 @@ function useAgentThread(threadId, options = {}) {
1352
1850
  }
1353
1851
  }
1354
1852
  }, [client, createAbortController, options.beforeTurnIndex, options.limit, threadId]);
1355
- React3.useEffect(() => {
1853
+ React7.useEffect(() => {
1356
1854
  void load();
1357
1855
  return () => controllerRef.current?.abort();
1358
1856
  }, [load]);
1359
1857
  return { error, isLoading, refresh: load, thread };
1360
1858
  }
1859
+ function useThreadComputer(threadId) {
1860
+ const { client, createAbortController } = useAgents24Runtime();
1861
+ const [computer, setComputer] = React7.useState(null);
1862
+ const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
1863
+ const [error, setError] = React7.useState(null);
1864
+ const controllerRef = React7.useRef(null);
1865
+ const refresh = React7.useCallback(async () => {
1866
+ controllerRef.current?.abort();
1867
+ if (!threadId) {
1868
+ setComputer(null);
1869
+ setIsLoading(false);
1870
+ return null;
1871
+ }
1872
+ const controller = createAbortController();
1873
+ controllerRef.current = controller;
1874
+ setIsLoading(true);
1875
+ setError(null);
1876
+ try {
1877
+ const result = await client.threads.computer.get({ threadId, signal: controller.signal });
1878
+ setComputer(result);
1879
+ return result;
1880
+ } catch (nextError) {
1881
+ if (!isAbortLike(nextError)) setError(nextError);
1882
+ return null;
1883
+ } finally {
1884
+ if (controllerRef.current === controller) {
1885
+ controllerRef.current = null;
1886
+ setIsLoading(false);
1887
+ }
1888
+ }
1889
+ }, [client, createAbortController, threadId]);
1890
+ React7.useEffect(() => {
1891
+ void refresh();
1892
+ return () => controllerRef.current?.abort();
1893
+ }, [refresh]);
1894
+ const list = React7.useCallback((input) => {
1895
+ if (!threadId) throw new Error("A thread is required to list computer files");
1896
+ return client.threads.computer.list({ threadId, ...input });
1897
+ }, [client, threadId]);
1898
+ const read = React7.useCallback((path) => {
1899
+ if (!threadId) throw new Error("A thread is required to read a computer file");
1900
+ return client.threads.computer.read({ threadId, path });
1901
+ }, [client, threadId]);
1902
+ const createContentAccess = React7.useCallback((path, download = false) => {
1903
+ if (!threadId) throw new Error("A thread is required to access a computer file");
1904
+ return client.threads.computer.createContentAccess({ threadId, path, download });
1905
+ }, [client, threadId]);
1906
+ return { computer, error, isLoading, list, read, createContentAccess, refresh };
1907
+ }
1361
1908
  function useAgentRun() {
1362
1909
  const { client, createAbortController, createId } = useAgents24Runtime();
1363
- const controllerRef = React3.useRef(null);
1364
- const [isAttached, setIsAttached] = React3.useState(false);
1365
- const [error, setError] = React3.useState(null);
1366
- React3.useEffect(() => () => controllerRef.current?.abort(), []);
1367
- const detach = React3.useCallback(() => {
1910
+ const controllerRef = React7.useRef(null);
1911
+ const [isAttached, setIsAttached] = React7.useState(false);
1912
+ const [error, setError] = React7.useState(null);
1913
+ React7.useEffect(() => () => controllerRef.current?.abort(), []);
1914
+ const detach = React7.useCallback(() => {
1368
1915
  controllerRef.current?.abort();
1369
1916
  controllerRef.current = null;
1370
1917
  setIsAttached(false);
1371
1918
  }, []);
1372
- const attach = React3.useCallback(async (runId, onEvent, cursor) => {
1919
+ const attach = React7.useCallback(async (runId, onEvent, cursor) => {
1373
1920
  detach();
1374
1921
  const controller = createAbortController();
1375
1922
  controllerRef.current = controller;
@@ -1385,7 +1932,7 @@ function useAgentRun() {
1385
1932
  setIsAttached(false);
1386
1933
  }
1387
1934
  }, [client, createAbortController, detach]);
1388
- const cancel = React3.useCallback(async (runId) => {
1935
+ const cancel = React7.useCallback(async (runId) => {
1389
1936
  const result = await client.runs.cancel({ runId, idempotencyKey: createId() });
1390
1937
  detach();
1391
1938
  return result;
@@ -1394,9 +1941,9 @@ function useAgentRun() {
1394
1941
  }
1395
1942
  function useAgentAttachments() {
1396
1943
  const { client, createId } = useAgents24Runtime();
1397
- const [uploads, setUploads] = React3.useState({});
1398
- const [isUploading, setIsUploading] = React3.useState(false);
1399
- const uploadBatch = React3.useCallback(async (input) => {
1944
+ const [uploads, setUploads] = React7.useState({});
1945
+ const [isUploading, setIsUploading] = React7.useState(false);
1946
+ const uploadBatch = React7.useCallback(async (input) => {
1400
1947
  setIsUploading(true);
1401
1948
  try {
1402
1949
  const results = await client.attachments.uploadBatch({ uploads: input.uploads, threadId: input.threadId, requestedAgentId: input.requestedAgentId, idempotencyKey: createId(), signal: input.signal });
@@ -1409,7 +1956,7 @@ function useAgentAttachments() {
1409
1956
  setIsUploading(false);
1410
1957
  }
1411
1958
  }, [client, createId]);
1412
- const createContentAccess = React3.useCallback(
1959
+ const createContentAccess = React7.useCallback(
1413
1960
  (input) => client.attachments.createContentAccess(input),
1414
1961
  [client]
1415
1962
  );
@@ -1417,9 +1964,9 @@ function useAgentAttachments() {
1417
1964
  }
1418
1965
  function useAgentHitl() {
1419
1966
  const { client, createId } = useAgents24Runtime();
1420
- const [isResolving, setIsResolving] = React3.useState(false);
1421
- const [lastResult, setLastResult] = React3.useState(null);
1422
- const resume = React3.useCallback(async (input) => {
1967
+ const [isResolving, setIsResolving] = React7.useState(false);
1968
+ const [lastResult, setLastResult] = React7.useState(null);
1969
+ const resume = React7.useCallback(async (input) => {
1423
1970
  setIsResolving(true);
1424
1971
  try {
1425
1972
  const result = await client.hitl.resume({ ...input, idempotencyKey: createId() });
@@ -1433,8 +1980,8 @@ function useAgentHitl() {
1433
1980
  }
1434
1981
  function useAgentMcp() {
1435
1982
  const { client, createId } = useAgents24Runtime();
1436
- const [isAuthorizing, setIsAuthorizing] = React3.useState(false);
1437
- const startAuthorization = React3.useCallback(async (input) => {
1983
+ const [isAuthorizing, setIsAuthorizing] = React7.useState(false);
1984
+ const startAuthorization = React7.useCallback(async (input) => {
1438
1985
  setIsAuthorizing(true);
1439
1986
  try {
1440
1987
  return await client.mcp.startAuthorization({ ...input, idempotencyKey: createId() });
@@ -1442,7 +1989,7 @@ function useAgentMcp() {
1442
1989
  setIsAuthorizing(false);
1443
1990
  }
1444
1991
  }, [client, createId]);
1445
- const redeemCallback = React3.useCallback(async (input) => client.mcp.redeemCallback({ ...input, idempotencyKey: createId() }), [client, createId]);
1992
+ const redeemCallback = React7.useCallback(async (input) => client.mcp.redeemCallback({ ...input, idempotencyKey: createId() }), [client, createId]);
1446
1993
  return { isAuthorizing, redeemCallback, startAuthorization };
1447
1994
  }
1448
1995
  export {
@@ -1458,6 +2005,7 @@ export {
1458
2005
  chatAttachmentIds,
1459
2006
  clearAgentChatBackgroundError,
1460
2007
  clearAgentChatOperationError,
2008
+ computerResourceTargets,
1461
2009
  contextCompressionPartFromEvent,
1462
2010
  createPortableId,
1463
2011
  getThreadActivity,
@@ -1466,9 +2014,12 @@ export {
1466
2014
  latestThreadContext,
1467
2015
  modelTurnPartFromEvent,
1468
2016
  partsFromResponseBlocks,
2017
+ preferNewestThreadSummary,
2018
+ preserveMountedMessageIds,
1469
2019
  reconcileRefreshedThreads,
1470
2020
  restoreAgentChatThreadSelection,
1471
2021
  threadDetailToMessages,
2022
+ threadTimelineToMessages,
1472
2023
  upsertStableAssistantMessage,
1473
2024
  useAgentAttachments,
1474
2025
  useAgentChat,
@@ -1479,6 +2030,8 @@ export {
1479
2030
  useAgentThread,
1480
2031
  useAgentThreads,
1481
2032
  useAgents24Runtime,
2033
+ useThreadComputer,
2034
+ useThreadMessageQueue,
1482
2035
  withAgentChatBackgroundError,
1483
2036
  withAgentChatOperationError
1484
2037
  };