@dudousxd/nestjs-agent-react 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -110,12 +110,48 @@ var AgentClient = class {
110
110
  signal: request.signal
111
111
  } : {}
112
112
  });
113
+ if (response.status === 202) {
114
+ return {
115
+ body: emptyStream(),
116
+ queued: await response.json()
117
+ };
118
+ }
113
119
  if (!response.ok || !response.body) {
114
120
  throw await this.failure(response, "POST", `${this.agentPath()}/chat`);
115
121
  }
116
122
  return streamResponse(response);
117
123
  }
118
124
  /**
125
+ * `POST <path>/chat` for a message that should wait in the thread's queue — the body carries
126
+ * `mode: 'queue'` (or `'interrupt'`), which the server always answers with `202` JSON.
127
+ */
128
+ enqueueMessage(request) {
129
+ return this.request("POST", "/chat", {
130
+ mode: "queue",
131
+ ...request.body
132
+ });
133
+ }
134
+ /** `GET <path>/threads/:id/queue` — the thread's waiting messages, and whether it drains. */
135
+ getQueue(threadId) {
136
+ return this.request("GET", `/threads/${encodeURIComponent(threadId)}/queue`);
137
+ }
138
+ /** `PATCH <path>/queue/:messageId` — change a waiting message's text/attachments, or move it. */
139
+ updateQueuedMessage(messageId, update) {
140
+ return this.request("PATCH", `/queue/${encodeURIComponent(messageId)}`, update);
141
+ }
142
+ /** `DELETE <path>/queue/:messageId`. */
143
+ removeQueuedMessage(messageId) {
144
+ return this.request("DELETE", `/queue/${encodeURIComponent(messageId)}`);
145
+ }
146
+ /** `DELETE <path>/threads/:id/queue` — drop every waiting message. */
147
+ clearQueue(threadId) {
148
+ return this.request("DELETE", `/threads/${encodeURIComponent(threadId)}/queue`);
149
+ }
150
+ /** `POST <path>/threads/:id/queue/resume` — lift a pause; `runId` when the head started. */
151
+ resumeQueue(threadId) {
152
+ return this.request("POST", `/threads/${encodeURIComponent(threadId)}/queue/resume`);
153
+ }
154
+ /**
119
155
  * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is
120
156
  * streaming under that id (404).
121
157
  */
@@ -381,6 +417,14 @@ var AgentClient = class {
381
417
  return JSON.parse(text);
382
418
  }
383
419
  };
420
+ function emptyStream() {
421
+ return new ReadableStream({
422
+ start(controller) {
423
+ controller.close();
424
+ }
425
+ });
426
+ }
427
+ __name(emptyStream, "emptyStream");
384
428
  function streamResponse(response) {
385
429
  const body = response.body;
386
430
  const runId = response.headers?.get(HEADER_RUN_ID) ?? void 0;
@@ -543,6 +587,10 @@ var AgentChatTransport = class {
543
587
  signal: options.abortSignal
544
588
  } : {}
545
589
  });
590
+ if (response.queued !== void 0) {
591
+ this.attemptLive = false;
592
+ return queuedChunkStream(response.queued);
593
+ }
546
594
  this.captureHeaderMeta(response);
547
595
  return this.toChunkStream(response, {
548
596
  ...headers !== void 0 ? {
@@ -573,6 +621,7 @@ var AgentChatTransport = class {
573
621
  });
574
622
  if (response === null) {
575
623
  this.attemptLive = false;
624
+ this.options.onResumeGone?.(runId);
576
625
  return null;
577
626
  }
578
627
  this.captureHeaderMeta(response);
@@ -950,6 +999,9 @@ var AgentChatTransport = class {
950
999
  break;
951
1000
  case "title":
952
1001
  case "cancelled":
1002
+ // The thread's queue changed, or this run handed the thread to the next queued message
1003
+ // (`started`). Thread-level, like the title.
1004
+ case "queue":
953
1005
  forwardAsData(event, true);
954
1006
  break;
955
1007
  default:
@@ -1052,6 +1104,22 @@ var AgentChatTransport = class {
1052
1104
  });
1053
1105
  }
1054
1106
  };
1107
+ function queuedChunkStream(queued) {
1108
+ return new ReadableStream({
1109
+ start(controller) {
1110
+ controller.enqueue({
1111
+ type: "data-queue",
1112
+ data: {
1113
+ queue: queued.queue,
1114
+ queuedSend: queued
1115
+ },
1116
+ transient: true
1117
+ });
1118
+ controller.close();
1119
+ }
1120
+ });
1121
+ }
1122
+ __name(queuedChunkStream, "queuedChunkStream");
1055
1123
  function parseSseFrame(raw) {
1056
1124
  let event;
1057
1125
  let id;
@@ -3766,6 +3834,7 @@ function useChatTranscript(options) {
3766
3834
  } : {}
3767
3835
  });
3768
3836
  const showTypingIndicator = isBusy && lastMessage?.role !== "assistant";
3837
+ const queued = useQueuedItems(options.queue ?? null, options.readOnly === true);
3769
3838
  return {
3770
3839
  items,
3771
3840
  status,
@@ -3788,10 +3857,53 @@ function useChatTranscript(options) {
3788
3857
  available: isBusy && options.readOnly !== true && options.onStop !== void 0,
3789
3858
  isStopping: stopRequested,
3790
3859
  stop
3791
- }
3860
+ },
3861
+ queued,
3862
+ queuePaused: options.queue?.paused ?? null
3792
3863
  };
3793
3864
  }
3794
3865
  __name(useChatTranscript, "useChatTranscript");
3866
+ function useQueuedItems(queue, readOnly) {
3867
+ const latestQueue = useRef9(queue);
3868
+ latestQueue.current = queue;
3869
+ const removers = useRef9(/* @__PURE__ */ new Map());
3870
+ const removerFor = useCallback8((id) => {
3871
+ let remover = removers.current.get(id);
3872
+ if (remover === void 0) {
3873
+ remover = /* @__PURE__ */ __name(() => {
3874
+ void latestQueue.current?.remove?.(id);
3875
+ }, "remover");
3876
+ removers.current.set(id, remover);
3877
+ }
3878
+ return remover;
3879
+ }, []);
3880
+ if (queue === null || queue === void 0) {
3881
+ return [];
3882
+ }
3883
+ return queue.items.map((message, position) => {
3884
+ const state = message.state === "sending" ? "sending" : queue.paused !== null ? "paused" : "queued";
3885
+ return {
3886
+ id: message.id,
3887
+ role: "user",
3888
+ text: message.text,
3889
+ files: message.attachments.map((attachment) => ({
3890
+ url: attachment.url,
3891
+ mediaType: attachment.contentType,
3892
+ filename: attachment.name,
3893
+ isImage: attachment.contentType.startsWith("image/")
3894
+ })),
3895
+ state,
3896
+ position,
3897
+ isNext: position === 0,
3898
+ interrupt: message.interrupt,
3899
+ remove: {
3900
+ available: !readOnly && state !== "sending" && queue.remove !== void 0,
3901
+ run: removerFor(message.id)
3902
+ }
3903
+ };
3904
+ });
3905
+ }
3906
+ __name(useQueuedItems, "useQueuedItems");
3795
3907
  function useTranscriptItem(options) {
3796
3908
  const { message, isStreaming = false } = options;
3797
3909
  const messagesRef = useRef9([
@@ -4410,7 +4522,7 @@ function joinClasses(...entries) {
4410
4522
  __name(joinClasses, "joinClasses");
4411
4523
 
4412
4524
  // src/components/message-list.tsx
4413
- function MessageList({ messages, status, renderToolPart, renderToolGroup, renderText, renderReasoning, renderFiles, renderUi, reasoningLabel, getMeta, readOnly, onFork, editable, onEditSubmit, regeneratable, onRegenerate, getUsage, getCreatedAt, emptyState, typingIndicator, followUps, onFollowUpClick, error, onRetry, classNames }) {
4525
+ function MessageList({ messages, status, renderToolPart, renderToolGroup, renderText, renderReasoning, renderFiles, renderUi, reasoningLabel, getMeta, readOnly, onFork, editable, onEditSubmit, regeneratable, onRegenerate, getUsage, getCreatedAt, emptyState, typingIndicator, followUps, onFollowUpClick, error, onRetry, queue, classNames }) {
4414
4526
  const transcript = useChatTranscript({
4415
4527
  messages,
4416
4528
  status,
@@ -4440,9 +4552,16 @@ function MessageList({ messages, status, renderToolPart, renderToolGroup, render
4440
4552
  } : {},
4441
4553
  ...followUps !== void 0 ? {
4442
4554
  followUps
4555
+ } : {},
4556
+ ...queue !== void 0 ? {
4557
+ queue: {
4558
+ items: queue.items,
4559
+ paused: queue.paused,
4560
+ remove: queue.remove
4561
+ }
4443
4562
  } : {}
4444
4563
  });
4445
- if (transcript.showEmptyState) {
4564
+ if (transcript.showEmptyState && transcript.queued.length === 0) {
4446
4565
  return /* @__PURE__ */ React.createElement("div", {
4447
4566
  className: classNames?.empty
4448
4567
  }, emptyState);
@@ -4486,7 +4605,23 @@ function MessageList({ messages, status, renderToolPart, renderToolGroup, render
4486
4605
  ...getMeta ? {
4487
4606
  meta: getMeta(item.message)
4488
4607
  } : {}
4489
- })), transcript.showTypingIndicator ? /* @__PURE__ */ React.createElement("div", {
4608
+ })), transcript.queued.length > 0 ? /* @__PURE__ */ React.createElement("div", {
4609
+ className: classNames?.queue
4610
+ }, transcript.queued.map((item) => /* @__PURE__ */ React.createElement("div", {
4611
+ key: item.id,
4612
+ className: classNames?.queuedItem,
4613
+ "data-state": item.state
4614
+ }, /* @__PURE__ */ React.createElement("span", null, item.text), item.remove.available ? /* @__PURE__ */ React.createElement("button", {
4615
+ type: "button",
4616
+ className: classNames?.queuedRemove,
4617
+ onClick: item.remove.run
4618
+ }, "Remove") : null)), transcript.queuePaused !== null && queue !== void 0 ? /* @__PURE__ */ React.createElement("div", {
4619
+ className: classNames?.queuePaused
4620
+ }, /* @__PURE__ */ React.createElement("span", null, "Queue paused"), /* @__PURE__ */ React.createElement("button", {
4621
+ type: "button",
4622
+ className: classNames?.queueResume,
4623
+ onClick: /* @__PURE__ */ __name(() => void queue.resume(), "onClick")
4624
+ }, "Resume")) : null) : null, transcript.showTypingIndicator ? /* @__PURE__ */ React.createElement("div", {
4490
4625
  className: classNames?.typing
4491
4626
  }, typingIndicator ?? "Thinking\u2026") : null, error && onRetry ? /* @__PURE__ */ React.createElement("div", {
4492
4627
  className: classNames?.error
@@ -5255,6 +5390,77 @@ __name(sumUsage, "sumUsage");
5255
5390
  // src/use-agent-chat.ts
5256
5391
  import { useChat } from "@ai-sdk/react";
5257
5392
  import { useCallback as useCallback11, useEffect as useEffect15, useMemo as useMemo11, useRef as useRef12, useState as useState15 } from "react";
5393
+ var EMPTY_QUEUE = {
5394
+ items: [],
5395
+ paused: null
5396
+ };
5397
+ var localQueueSequence = 0;
5398
+ function queuedUserMessage(turn) {
5399
+ return {
5400
+ id: turn.messageId,
5401
+ role: "user",
5402
+ metadata: {
5403
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5404
+ },
5405
+ parts: [
5406
+ ...turn.attachments.map((attachment) => ({
5407
+ type: "file",
5408
+ mediaType: attachment.contentType,
5409
+ filename: attachment.name,
5410
+ url: attachment.url,
5411
+ providerMetadata: {
5412
+ agent: {
5413
+ mediaId: attachment.mediaId
5414
+ }
5415
+ }
5416
+ })),
5417
+ ...turn.text.length > 0 ? [
5418
+ {
5419
+ type: "text",
5420
+ text: turn.text
5421
+ }
5422
+ ] : []
5423
+ ]
5424
+ };
5425
+ }
5426
+ __name(queuedUserMessage, "queuedUserMessage");
5427
+ function draftOf(message) {
5428
+ if (typeof message !== "object" || message === null) {
5429
+ return {
5430
+ text: "",
5431
+ attachments: []
5432
+ };
5433
+ }
5434
+ const draft = message;
5435
+ let text = typeof draft.text === "string" ? draft.text : "";
5436
+ if (text === "" && Array.isArray(draft.parts)) {
5437
+ text = draft.parts.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("");
5438
+ }
5439
+ const files = Array.isArray(draft.files) ? draft.files : Array.isArray(draft.parts) ? draft.parts.filter((part) => part.type === "file") : [];
5440
+ const attachments = files.flatMap((file) => {
5441
+ const mediaId = file.providerMetadata?.agent?.mediaId;
5442
+ return typeof mediaId === "string" ? [
5443
+ {
5444
+ mediaId,
5445
+ url: String(file.url ?? ""),
5446
+ contentType: String(file.mediaType ?? "application/octet-stream"),
5447
+ name: String(file.filename ?? mediaId)
5448
+ }
5449
+ ] : [];
5450
+ });
5451
+ return {
5452
+ text,
5453
+ attachments
5454
+ };
5455
+ }
5456
+ __name(draftOf, "draftOf");
5457
+ function lastUserIndex(messages) {
5458
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
5459
+ if (messages[index]?.role === "user") return index;
5460
+ }
5461
+ return -1;
5462
+ }
5463
+ __name(lastUserIndex, "lastUserIndex");
5258
5464
  var localChatSequence = 0;
5259
5465
  function localChatId() {
5260
5466
  localChatSequence += 1;
@@ -5294,6 +5500,41 @@ function useAgentChat(options = {}) {
5294
5500
  const pendingPin = useRef12(void 0);
5295
5501
  const regenerateNext = useRef12(false);
5296
5502
  const currentThreadId = useCallback11(() => latest.current.threadId ?? createdThreadId.current, []);
5503
+ const [queueState, setQueueState] = useState15(EMPTY_QUEUE);
5504
+ const queueStateRef = useRef12(queueState);
5505
+ const applyQueue = useCallback11((next) => {
5506
+ queueStateRef.current = next;
5507
+ setQueueState(next);
5508
+ }, []);
5509
+ const [pendingSends, setPendingSends] = useState15([]);
5510
+ const [queueError, setQueueError] = useState15(null);
5511
+ const handoff = useRef12(void 0);
5512
+ const queuedSend = useRef12(void 0);
5513
+ const threadWaiters = useRef12([]);
5514
+ const turnBody = useCallback11(() => {
5515
+ const current = latest.current;
5516
+ const pageContext = current.getPageContext?.() ?? null;
5517
+ const threadId = currentThreadId();
5518
+ const model = current.model ?? (lockedRef.current ? void 0 : pickedModelRef.current);
5519
+ return {
5520
+ // Read per send: a host that switches agents before the first message (a picker on a new
5521
+ // chat) sends the one picked now, not the one this chat mounted with.
5522
+ ...current.agent !== void 0 ? {
5523
+ agent: current.agent
5524
+ } : {},
5525
+ ...threadId !== void 0 ? {
5526
+ threadId
5527
+ } : {},
5528
+ ...model !== void 0 ? {
5529
+ model
5530
+ } : {},
5531
+ ...pageContext ? {
5532
+ pageContext
5533
+ } : {}
5534
+ };
5535
+ }, [
5536
+ currentThreadId
5537
+ ]);
5297
5538
  const [connection, setConnection] = useState15({
5298
5539
  status: "live"
5299
5540
  });
@@ -5302,6 +5543,11 @@ function useAgentChat(options = {}) {
5302
5543
  function onMeta(meta) {
5303
5544
  settlingRunIdRef.current = meta.runId;
5304
5545
  setRunId(meta.runId);
5546
+ if (meta.threadId) {
5547
+ const waiting = threadWaiters.current;
5548
+ threadWaiters.current = [];
5549
+ for (const resolve of waiting) resolve(meta.threadId);
5550
+ }
5305
5551
  if (latest.current.threadId === void 0 && meta.threadId && createdThreadId.current !== meta.threadId) {
5306
5552
  createdThreadId.current = meta.threadId;
5307
5553
  const pin = pendingPin.current;
@@ -5333,27 +5579,10 @@ function useAgentChat(options = {}) {
5333
5579
  agent: options.agent
5334
5580
  } : {},
5335
5581
  getBody: /* @__PURE__ */ __name(() => {
5336
- const current = latest.current;
5337
- const pageContext = current.getPageContext?.() ?? null;
5338
5582
  const regenerate2 = regenerateNext.current;
5339
5583
  regenerateNext.current = false;
5340
- const threadId = currentThreadId();
5341
- const model = current.model ?? (lockedRef.current ? void 0 : pickedModelRef.current);
5342
5584
  return {
5343
- // Read per send: a host that switches agents before the first message (a picker on a
5344
- // new chat) sends the one picked now, not the one this chat mounted with.
5345
- ...current.agent !== void 0 ? {
5346
- agent: current.agent
5347
- } : {},
5348
- ...threadId !== void 0 ? {
5349
- threadId
5350
- } : {},
5351
- ...model !== void 0 ? {
5352
- model
5353
- } : {},
5354
- ...pageContext ? {
5355
- pageContext
5356
- } : {},
5585
+ ...turnBody(),
5357
5586
  ...regenerate2 ? {
5358
5587
  regenerate: true
5359
5588
  } : {}
@@ -5363,7 +5592,11 @@ function useAgentChat(options = {}) {
5363
5592
  onMeta,
5364
5593
  onAttemptStart: /* @__PURE__ */ __name(() => {
5365
5594
  settlingRunIdRef.current = void 0;
5366
- }, "onAttemptStart")
5595
+ }, "onAttemptStart"),
5596
+ // A queued turn this chat went to attach to had already finished: read what it wrote.
5597
+ onResumeGone: /* @__PURE__ */ __name(() => {
5598
+ void resyncFromThread();
5599
+ }, "onResumeGone")
5367
5600
  });
5368
5601
  }, []);
5369
5602
  const backgroundRef = useRef12(() => Promise.resolve());
@@ -5376,6 +5609,9 @@ function useAgentChat(options = {}) {
5376
5609
  } : {},
5377
5610
  onData: /* @__PURE__ */ __name((part) => {
5378
5611
  latest.current.onData?.(part);
5612
+ if (part.type === "data-queue") {
5613
+ onQueueData(part.data);
5614
+ }
5379
5615
  if (part.type === "data-title") {
5380
5616
  const title = part.data?.title;
5381
5617
  if (typeof title === "string") {
@@ -5407,6 +5643,20 @@ function useAgentChat(options = {}) {
5407
5643
  setConnection({
5408
5644
  status: "live"
5409
5645
  });
5646
+ const queuedInstead = queuedSend.current;
5647
+ if (queuedInstead !== void 0) {
5648
+ queuedSend.current = void 0;
5649
+ chatRef.current.setMessages((current) => {
5650
+ const last = lastUserIndex(current);
5651
+ return last === -1 ? current : current.slice(0, last);
5652
+ });
5653
+ }
5654
+ const next = handoff.current;
5655
+ handoff.current = void 0;
5656
+ if (next !== void 0) {
5657
+ attachTo(next);
5658
+ return;
5659
+ }
5410
5660
  if (resyncAfterSettle.current) {
5411
5661
  resyncAfterSettle.current = false;
5412
5662
  void resyncFromThread();
@@ -5415,6 +5665,46 @@ function useAgentChat(options = {}) {
5415
5665
  });
5416
5666
  const chatRef = useRef12(chat);
5417
5667
  chatRef.current = chat;
5668
+ function onQueueData(data) {
5669
+ const frame = data ?? {};
5670
+ const before = queueStateRef.current;
5671
+ if (frame.queue !== void 0) applyQueue(frame.queue);
5672
+ const started = frame.started;
5673
+ if (started !== void 0 && started.runId !== settlingRunIdRef.current) {
5674
+ const waiting = before.items.find((item) => item.id === started.messageId);
5675
+ handoff.current = {
5676
+ ...started,
5677
+ text: waiting?.content ?? "",
5678
+ attachments: waiting?.attachments ?? []
5679
+ };
5680
+ }
5681
+ if (frame.queuedSend !== void 0) {
5682
+ queuedSend.current = frame.queuedSend;
5683
+ if (frame.queuedSend.runId !== void 0) {
5684
+ handoff.current = {
5685
+ messageId: frame.queuedSend.messageId,
5686
+ runId: frame.queuedSend.runId,
5687
+ ...draftOf(chatRef.current.messages[lastUserIndex(chatRef.current.messages)])
5688
+ };
5689
+ }
5690
+ }
5691
+ }
5692
+ __name(onQueueData, "onQueueData");
5693
+ function attachTo(turn) {
5694
+ setTimeout(() => {
5695
+ if (turn.text.length > 0 || turn.attachments.length > 0) {
5696
+ chatRef.current.setMessages((current) => current.some((message) => message.id === turn.messageId) ? current : [
5697
+ ...current,
5698
+ queuedUserMessage(turn)
5699
+ ]);
5700
+ } else {
5701
+ resyncAfterSettle.current = true;
5702
+ }
5703
+ autoResumeRunIdRef.current = turn.runId;
5704
+ void chatRef.current.resumeStream();
5705
+ }, 0);
5706
+ }
5707
+ __name(attachTo, "attachTo");
5418
5708
  async function resyncFromThread() {
5419
5709
  const threadId = currentThreadId();
5420
5710
  if (threadId === void 0) return;
@@ -5437,6 +5727,11 @@ function useAgentChat(options = {}) {
5437
5727
  setHistoryError(null);
5438
5728
  autoResumeRunIdRef.current = void 0;
5439
5729
  pendingPin.current = void 0;
5730
+ applyQueue(EMPTY_QUEUE);
5731
+ setPendingSends([]);
5732
+ setQueueError(null);
5733
+ handoff.current = void 0;
5734
+ queuedSend.current = void 0;
5440
5735
  }, [
5441
5736
  chatId
5442
5737
  ]);
@@ -5456,6 +5751,11 @@ function useAgentChat(options = {}) {
5456
5751
  setActiveRunId(active);
5457
5752
  setThreadModel(thread.model ?? null);
5458
5753
  setHistoryError(null);
5754
+ const queue2 = thread.queue ?? EMPTY_QUEUE;
5755
+ applyQueue(queue2);
5756
+ if (queue2.items.length > 0 && queue2.paused === null && active === null && typeof client.resumeQueue === "function") {
5757
+ void resumeQueueRef.current().catch(() => void 0);
5758
+ }
5459
5759
  const resuming = wantResume && active !== null;
5460
5760
  if (wantHistory && chatRef.current.messages.length === 0) {
5461
5761
  const rows = resuming ? stored.filter((message) => !(message.role === "assistant" && message.runId === active)) : stored;
@@ -5491,10 +5791,98 @@ function useAgentChat(options = {}) {
5491
5791
  const blocked = options.blocked !== void 0 ? options.blocked : quota.blocked;
5492
5792
  const blockedRef = useRef12(blocked);
5493
5793
  blockedRef.current = blocked;
5794
+ const whileRunning = typeof client.enqueueMessage === "function" ? options.whileRunning ?? "queue" : "block";
5795
+ const whileRunningRef = useRef12(whileRunning);
5796
+ whileRunningRef.current = whileRunning;
5797
+ const threadForQueue = useCallback11(async () => {
5798
+ const known = currentThreadId();
5799
+ if (known !== void 0) return known;
5800
+ if (!isTurnInFlight()) throw new Error("queue: this chat has no thread yet");
5801
+ return new Promise((resolve) => threadWaiters.current.push(resolve));
5802
+ }, [
5803
+ currentThreadId,
5804
+ isTurnInFlight
5805
+ ]);
5806
+ const queueCall = useCallback11(async (run) => {
5807
+ try {
5808
+ const result = await run();
5809
+ setQueueError(null);
5810
+ return result;
5811
+ } catch (error) {
5812
+ setQueueError(error instanceof Error ? error : new Error(String(error)));
5813
+ throw error;
5814
+ }
5815
+ }, []);
5816
+ const enqueue = useCallback11(async (text2, { attachments = [], mode = "queue" } = {}) => {
5817
+ const block = blockedRef.current;
5818
+ if (block != null) throw new QuotaBlockedError(block);
5819
+ const send = client.enqueueMessage;
5820
+ if (typeof send !== "function") throw new AgentBackendUnsupportedError("enqueueMessage");
5821
+ localQueueSequence += 1;
5822
+ const localId = `local-queued-${localQueueSequence}`;
5823
+ setPendingSends((current) => [
5824
+ ...current,
5825
+ {
5826
+ id: localId,
5827
+ text: text2,
5828
+ attachments,
5829
+ state: "sending",
5830
+ interrupt: mode === "interrupt",
5831
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5832
+ }
5833
+ ]);
5834
+ try {
5835
+ await queueCall(async () => {
5836
+ const threadId = await threadForQueue();
5837
+ const result = await send.call(client, {
5838
+ body: {
5839
+ ...turnBody(),
5840
+ threadId,
5841
+ message: text2,
5842
+ mode,
5843
+ ...attachments.length > 0 ? {
5844
+ attachments: attachments.map(({ mediaId }) => ({
5845
+ mediaId
5846
+ }))
5847
+ } : {}
5848
+ }
5849
+ });
5850
+ applyQueue(result.queue);
5851
+ if (result.runId !== void 0) {
5852
+ const turn = {
5853
+ messageId: result.messageId,
5854
+ runId: result.runId,
5855
+ text: text2,
5856
+ attachments
5857
+ };
5858
+ if (isTurnInFlight()) handoff.current = turn;
5859
+ else attachTo(turn);
5860
+ }
5861
+ });
5862
+ } finally {
5863
+ setPendingSends((current) => current.filter((item) => item.id !== localId));
5864
+ }
5865
+ }, [
5866
+ client,
5867
+ queueCall,
5868
+ threadForQueue,
5869
+ turnBody,
5870
+ applyQueue,
5871
+ isTurnInFlight
5872
+ ]);
5494
5873
  const sendMessage = useCallback11(async (...args) => {
5495
5874
  const block = blockedRef.current;
5496
5875
  if (block != null) throw new QuotaBlockedError(block);
5497
- if (isTurnInFlight()) return;
5876
+ if (isTurnInFlight()) {
5877
+ const mode = whileRunningRef.current;
5878
+ if (mode === "block") return;
5879
+ const draft = draftOf(args[0]);
5880
+ await enqueue(draft.text, {
5881
+ attachments: draft.attachments,
5882
+ mode
5883
+ });
5884
+ return;
5885
+ }
5498
5886
  turnInFlight.current = true;
5499
5887
  try {
5500
5888
  await chatRef.current.sendMessage(...args);
@@ -5502,8 +5890,122 @@ function useAgentChat(options = {}) {
5502
5890
  turnInFlight.current = false;
5503
5891
  }
5504
5892
  }, [
5893
+ isTurnInFlight,
5894
+ enqueue
5895
+ ]);
5896
+ const removeQueued = useCallback11(async (id) => {
5897
+ const previous = queueStateRef.current;
5898
+ applyQueue({
5899
+ ...previous,
5900
+ items: previous.items.filter((item) => item.id !== id)
5901
+ });
5902
+ try {
5903
+ applyQueue(await queueCall(() => requireBackendMethod(client, "removeQueuedMessage")(id)));
5904
+ } catch (error) {
5905
+ applyQueue(previous);
5906
+ throw error;
5907
+ }
5908
+ }, [
5909
+ client,
5910
+ queueCall,
5911
+ applyQueue
5912
+ ]);
5913
+ const editQueued = useCallback11(async (id, text2) => {
5914
+ applyQueue(await queueCall(() => requireBackendMethod(client, "updateQueuedMessage")(id, {
5915
+ message: text2
5916
+ })));
5917
+ }, [
5918
+ client,
5919
+ queueCall,
5920
+ applyQueue
5921
+ ]);
5922
+ const moveQueued = useCallback11(async (id, index) => {
5923
+ const previous = queueStateRef.current;
5924
+ const moving = previous.items.find((item) => item.id === id);
5925
+ if (moving !== void 0) {
5926
+ const rest = previous.items.filter((item) => item.id !== id);
5927
+ rest.splice(Math.max(0, Math.min(rest.length, index)), 0, moving);
5928
+ applyQueue({
5929
+ ...previous,
5930
+ items: rest
5931
+ });
5932
+ }
5933
+ try {
5934
+ applyQueue(await queueCall(() => requireBackendMethod(client, "updateQueuedMessage")(id, {
5935
+ position: index
5936
+ })));
5937
+ } catch (error) {
5938
+ applyQueue(previous);
5939
+ throw error;
5940
+ }
5941
+ }, [
5942
+ client,
5943
+ queueCall,
5944
+ applyQueue
5945
+ ]);
5946
+ const clearQueue = useCallback11(async () => {
5947
+ const threadId = currentThreadId();
5948
+ if (threadId === void 0) return;
5949
+ applyQueue(await queueCall(() => requireBackendMethod(client, "clearQueue")(threadId)));
5950
+ }, [
5951
+ client,
5952
+ queueCall,
5953
+ currentThreadId,
5954
+ applyQueue
5955
+ ]);
5956
+ const resumeQueue = useCallback11(async () => {
5957
+ const threadId = currentThreadId();
5958
+ if (threadId === void 0) return;
5959
+ const before = queueStateRef.current;
5960
+ const result = await queueCall(() => requireBackendMethod(client, "resumeQueue")(threadId));
5961
+ applyQueue({
5962
+ items: result.items,
5963
+ paused: result.paused
5964
+ });
5965
+ if (result.runId !== void 0) {
5966
+ const head = before.items.find((item) => item.id === result.runId) ?? before.items[0];
5967
+ const turn = {
5968
+ messageId: result.runId,
5969
+ runId: result.runId,
5970
+ text: head?.content ?? "",
5971
+ attachments: head?.attachments ?? []
5972
+ };
5973
+ if (isTurnInFlight()) handoff.current = turn;
5974
+ else attachTo(turn);
5975
+ }
5976
+ }, [
5977
+ client,
5978
+ queueCall,
5979
+ currentThreadId,
5980
+ applyQueue,
5505
5981
  isTurnInFlight
5506
5982
  ]);
5983
+ const resumeQueueRef = useRef12(resumeQueue);
5984
+ resumeQueueRef.current = resumeQueue;
5985
+ const queuedItems = [
5986
+ ...pendingSends.filter((item) => item.interrupt),
5987
+ ...queueState.items.map((item) => ({
5988
+ id: item.id,
5989
+ text: item.content,
5990
+ attachments: item.attachments ?? [],
5991
+ state: "queued",
5992
+ interrupt: item.interrupt === true,
5993
+ createdAt: item.createdAt
5994
+ })),
5995
+ ...pendingSends.filter((item) => !item.interrupt)
5996
+ ];
5997
+ const queue = {
5998
+ items: queuedItems,
5999
+ paused: queueState.paused,
6000
+ isSupported: typeof client.enqueueMessage === "function",
6001
+ add: enqueue,
6002
+ remove: removeQueued,
6003
+ edit: editQueued,
6004
+ move: moveQueued,
6005
+ clear: clearQueue,
6006
+ resume: resumeQueue,
6007
+ error: queueError
6008
+ };
5507
6009
  const addToolResult = useCallback11(({ tool, toolCallId, output }) => chatRef.current.addToolResult({
5508
6010
  tool,
5509
6011
  toolCallId,
@@ -5744,12 +6246,14 @@ function useAgentChat(options = {}) {
5744
6246
  };
5745
6247
  const status = connection.status === "reconnecting" ? "reconnecting" : chat.status;
5746
6248
  const isBusy = status === "submitted" || status === "streaming" || status === "reconnecting";
6249
+ const isBusyRef = useRef12(isBusy);
6250
+ isBusyRef.current = isBusy;
5747
6251
  const files = useAttachments({
5748
6252
  ...options.composer,
5749
6253
  backend: client
5750
6254
  });
5751
6255
  const [text, setText] = useState15("");
5752
- const blockedBy = blocked != null ? "quota" : files.isUploading ? "uploading" : isBusy ? "busy" : text.trim().length === 0 && files.refs.length === 0 ? "empty" : null;
6256
+ const blockedBy = blocked != null ? "quota" : files.isUploading ? "uploading" : isBusy && whileRunning === "block" ? "busy" : text.trim().length === 0 && files.refs.length === 0 ? "empty" : null;
5753
6257
  const composerRef = useRef12({
5754
6258
  text,
5755
6259
  files,
@@ -5780,6 +6284,17 @@ function useAgentChat(options = {}) {
5780
6284
  const draft = current.text.trim();
5781
6285
  setText("");
5782
6286
  current.files.clear();
6287
+ const mode = whileRunningRef.current;
6288
+ if (mode !== "block" && (isTurnInFlight() || isBusyRef.current)) {
6289
+ const attachments = current.files.items.flatMap((item) => item.status === "ready" && item.attachment !== void 0 ? [
6290
+ item.attachment
6291
+ ] : []);
6292
+ await enqueue(draft, {
6293
+ attachments,
6294
+ mode
6295
+ });
6296
+ return;
6297
+ }
5783
6298
  await sendMessage(files2.length > 0 ? {
5784
6299
  text: draft,
5785
6300
  files: files2
@@ -5791,7 +6306,9 @@ function useAgentChat(options = {}) {
5791
6306
  }
5792
6307
  } : void 0);
5793
6308
  }, [
5794
- sendMessage
6309
+ sendMessage,
6310
+ enqueue,
6311
+ isTurnInFlight
5795
6312
  ]);
5796
6313
  const composer = {
5797
6314
  text,
@@ -5826,6 +6343,11 @@ function useAgentChat(options = {}) {
5826
6343
  ...ownCatalog === void 0 ? {
5827
6344
  toolCatalog: tools.catalog
5828
6345
  } : {},
6346
+ queue: {
6347
+ items: queue.items,
6348
+ paused: queue.paused,
6349
+ remove: removeQueued
6350
+ },
5829
6351
  ...options.transcript
5830
6352
  });
5831
6353
  const background = {
@@ -5860,6 +6382,11 @@ function useAgentChat(options = {}) {
5860
6382
  transcript,
5861
6383
  /** A headless composer: draft, files, send gate, `submit()`. */
5862
6384
  composer,
6385
+ /**
6386
+ * Messages sent while a turn was running, waiting their turn (server-side): list, edit, move,
6387
+ * remove, clear, and resume a paused queue.
6388
+ */
6389
+ queue,
5863
6390
  /** The model picker: `list`, `selected`, `select(id)`, `pinToThread(id)`. */
5864
6391
  models,
5865
6392
  /** The caller's budget (`GET <base>/quota`); `quota.blocked` gates sends. */