@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.cjs CHANGED
@@ -214,12 +214,48 @@ var AgentClient = class {
214
214
  signal: request.signal
215
215
  } : {}
216
216
  });
217
+ if (response.status === 202) {
218
+ return {
219
+ body: emptyStream(),
220
+ queued: await response.json()
221
+ };
222
+ }
217
223
  if (!response.ok || !response.body) {
218
224
  throw await this.failure(response, "POST", `${this.agentPath()}/chat`);
219
225
  }
220
226
  return streamResponse(response);
221
227
  }
222
228
  /**
229
+ * `POST <path>/chat` for a message that should wait in the thread's queue — the body carries
230
+ * `mode: 'queue'` (or `'interrupt'`), which the server always answers with `202` JSON.
231
+ */
232
+ enqueueMessage(request) {
233
+ return this.request("POST", "/chat", {
234
+ mode: "queue",
235
+ ...request.body
236
+ });
237
+ }
238
+ /** `GET <path>/threads/:id/queue` — the thread's waiting messages, and whether it drains. */
239
+ getQueue(threadId) {
240
+ return this.request("GET", `/threads/${encodeURIComponent(threadId)}/queue`);
241
+ }
242
+ /** `PATCH <path>/queue/:messageId` — change a waiting message's text/attachments, or move it. */
243
+ updateQueuedMessage(messageId, update) {
244
+ return this.request("PATCH", `/queue/${encodeURIComponent(messageId)}`, update);
245
+ }
246
+ /** `DELETE <path>/queue/:messageId`. */
247
+ removeQueuedMessage(messageId) {
248
+ return this.request("DELETE", `/queue/${encodeURIComponent(messageId)}`);
249
+ }
250
+ /** `DELETE <path>/threads/:id/queue` — drop every waiting message. */
251
+ clearQueue(threadId) {
252
+ return this.request("DELETE", `/threads/${encodeURIComponent(threadId)}/queue`);
253
+ }
254
+ /** `POST <path>/threads/:id/queue/resume` — lift a pause; `runId` when the head started. */
255
+ resumeQueue(threadId) {
256
+ return this.request("POST", `/threads/${encodeURIComponent(threadId)}/queue/resume`);
257
+ }
258
+ /**
223
259
  * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is
224
260
  * streaming under that id (404).
225
261
  */
@@ -485,6 +521,14 @@ var AgentClient = class {
485
521
  return JSON.parse(text);
486
522
  }
487
523
  };
524
+ function emptyStream() {
525
+ return new ReadableStream({
526
+ start(controller) {
527
+ controller.close();
528
+ }
529
+ });
530
+ }
531
+ __name(emptyStream, "emptyStream");
488
532
  function streamResponse(response) {
489
533
  const body = response.body;
490
534
  const runId = response.headers?.get(HEADER_RUN_ID) ?? void 0;
@@ -647,6 +691,10 @@ var AgentChatTransport = class {
647
691
  signal: options.abortSignal
648
692
  } : {}
649
693
  });
694
+ if (response.queued !== void 0) {
695
+ this.attemptLive = false;
696
+ return queuedChunkStream(response.queued);
697
+ }
650
698
  this.captureHeaderMeta(response);
651
699
  return this.toChunkStream(response, {
652
700
  ...headers !== void 0 ? {
@@ -677,6 +725,7 @@ var AgentChatTransport = class {
677
725
  });
678
726
  if (response === null) {
679
727
  this.attemptLive = false;
728
+ this.options.onResumeGone?.(runId);
680
729
  return null;
681
730
  }
682
731
  this.captureHeaderMeta(response);
@@ -1054,6 +1103,9 @@ var AgentChatTransport = class {
1054
1103
  break;
1055
1104
  case "title":
1056
1105
  case "cancelled":
1106
+ // The thread's queue changed, or this run handed the thread to the next queued message
1107
+ // (`started`). Thread-level, like the title.
1108
+ case "queue":
1057
1109
  forwardAsData(event, true);
1058
1110
  break;
1059
1111
  default:
@@ -1156,6 +1208,22 @@ var AgentChatTransport = class {
1156
1208
  });
1157
1209
  }
1158
1210
  };
1211
+ function queuedChunkStream(queued) {
1212
+ return new ReadableStream({
1213
+ start(controller) {
1214
+ controller.enqueue({
1215
+ type: "data-queue",
1216
+ data: {
1217
+ queue: queued.queue,
1218
+ queuedSend: queued
1219
+ },
1220
+ transient: true
1221
+ });
1222
+ controller.close();
1223
+ }
1224
+ });
1225
+ }
1226
+ __name(queuedChunkStream, "queuedChunkStream");
1159
1227
  function parseSseFrame(raw) {
1160
1228
  let event;
1161
1229
  let id;
@@ -3870,6 +3938,7 @@ function useChatTranscript(options) {
3870
3938
  } : {}
3871
3939
  });
3872
3940
  const showTypingIndicator = isBusy && lastMessage?.role !== "assistant";
3941
+ const queued = useQueuedItems(options.queue ?? null, options.readOnly === true);
3873
3942
  return {
3874
3943
  items,
3875
3944
  status,
@@ -3892,10 +3961,53 @@ function useChatTranscript(options) {
3892
3961
  available: isBusy && options.readOnly !== true && options.onStop !== void 0,
3893
3962
  isStopping: stopRequested,
3894
3963
  stop
3895
- }
3964
+ },
3965
+ queued,
3966
+ queuePaused: options.queue?.paused ?? null
3896
3967
  };
3897
3968
  }
3898
3969
  __name(useChatTranscript, "useChatTranscript");
3970
+ function useQueuedItems(queue, readOnly) {
3971
+ const latestQueue = (0, import_react17.useRef)(queue);
3972
+ latestQueue.current = queue;
3973
+ const removers = (0, import_react17.useRef)(/* @__PURE__ */ new Map());
3974
+ const removerFor = (0, import_react17.useCallback)((id) => {
3975
+ let remover = removers.current.get(id);
3976
+ if (remover === void 0) {
3977
+ remover = /* @__PURE__ */ __name(() => {
3978
+ void latestQueue.current?.remove?.(id);
3979
+ }, "remover");
3980
+ removers.current.set(id, remover);
3981
+ }
3982
+ return remover;
3983
+ }, []);
3984
+ if (queue === null || queue === void 0) {
3985
+ return [];
3986
+ }
3987
+ return queue.items.map((message, position) => {
3988
+ const state = message.state === "sending" ? "sending" : queue.paused !== null ? "paused" : "queued";
3989
+ return {
3990
+ id: message.id,
3991
+ role: "user",
3992
+ text: message.text,
3993
+ files: message.attachments.map((attachment) => ({
3994
+ url: attachment.url,
3995
+ mediaType: attachment.contentType,
3996
+ filename: attachment.name,
3997
+ isImage: attachment.contentType.startsWith("image/")
3998
+ })),
3999
+ state,
4000
+ position,
4001
+ isNext: position === 0,
4002
+ interrupt: message.interrupt,
4003
+ remove: {
4004
+ available: !readOnly && state !== "sending" && queue.remove !== void 0,
4005
+ run: removerFor(message.id)
4006
+ }
4007
+ };
4008
+ });
4009
+ }
4010
+ __name(useQueuedItems, "useQueuedItems");
3899
4011
  function useTranscriptItem(options) {
3900
4012
  const { message, isStreaming = false } = options;
3901
4013
  const messagesRef = (0, import_react17.useRef)([
@@ -4514,7 +4626,7 @@ function joinClasses(...entries) {
4514
4626
  __name(joinClasses, "joinClasses");
4515
4627
 
4516
4628
  // src/components/message-list.tsx
4517
- 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 }) {
4629
+ 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 }) {
4518
4630
  const transcript = useChatTranscript({
4519
4631
  messages,
4520
4632
  status,
@@ -4544,9 +4656,16 @@ function MessageList({ messages, status, renderToolPart, renderToolGroup, render
4544
4656
  } : {},
4545
4657
  ...followUps !== void 0 ? {
4546
4658
  followUps
4659
+ } : {},
4660
+ ...queue !== void 0 ? {
4661
+ queue: {
4662
+ items: queue.items,
4663
+ paused: queue.paused,
4664
+ remove: queue.remove
4665
+ }
4547
4666
  } : {}
4548
4667
  });
4549
- if (transcript.showEmptyState) {
4668
+ if (transcript.showEmptyState && transcript.queued.length === 0) {
4550
4669
  return /* @__PURE__ */ React.createElement("div", {
4551
4670
  className: classNames?.empty
4552
4671
  }, emptyState);
@@ -4590,7 +4709,23 @@ function MessageList({ messages, status, renderToolPart, renderToolGroup, render
4590
4709
  ...getMeta ? {
4591
4710
  meta: getMeta(item.message)
4592
4711
  } : {}
4593
- })), transcript.showTypingIndicator ? /* @__PURE__ */ React.createElement("div", {
4712
+ })), transcript.queued.length > 0 ? /* @__PURE__ */ React.createElement("div", {
4713
+ className: classNames?.queue
4714
+ }, transcript.queued.map((item) => /* @__PURE__ */ React.createElement("div", {
4715
+ key: item.id,
4716
+ className: classNames?.queuedItem,
4717
+ "data-state": item.state
4718
+ }, /* @__PURE__ */ React.createElement("span", null, item.text), item.remove.available ? /* @__PURE__ */ React.createElement("button", {
4719
+ type: "button",
4720
+ className: classNames?.queuedRemove,
4721
+ onClick: item.remove.run
4722
+ }, "Remove") : null)), transcript.queuePaused !== null && queue !== void 0 ? /* @__PURE__ */ React.createElement("div", {
4723
+ className: classNames?.queuePaused
4724
+ }, /* @__PURE__ */ React.createElement("span", null, "Queue paused"), /* @__PURE__ */ React.createElement("button", {
4725
+ type: "button",
4726
+ className: classNames?.queueResume,
4727
+ onClick: /* @__PURE__ */ __name(() => void queue.resume(), "onClick")
4728
+ }, "Resume")) : null) : null, transcript.showTypingIndicator ? /* @__PURE__ */ React.createElement("div", {
4594
4729
  className: classNames?.typing
4595
4730
  }, typingIndicator ?? "Thinking\u2026") : null, error && onRetry ? /* @__PURE__ */ React.createElement("div", {
4596
4731
  className: classNames?.error
@@ -5359,6 +5494,77 @@ __name(sumUsage, "sumUsage");
5359
5494
  // src/use-agent-chat.ts
5360
5495
  var import_react22 = require("@ai-sdk/react");
5361
5496
  var import_react23 = require("react");
5497
+ var EMPTY_QUEUE = {
5498
+ items: [],
5499
+ paused: null
5500
+ };
5501
+ var localQueueSequence = 0;
5502
+ function queuedUserMessage(turn) {
5503
+ return {
5504
+ id: turn.messageId,
5505
+ role: "user",
5506
+ metadata: {
5507
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5508
+ },
5509
+ parts: [
5510
+ ...turn.attachments.map((attachment) => ({
5511
+ type: "file",
5512
+ mediaType: attachment.contentType,
5513
+ filename: attachment.name,
5514
+ url: attachment.url,
5515
+ providerMetadata: {
5516
+ agent: {
5517
+ mediaId: attachment.mediaId
5518
+ }
5519
+ }
5520
+ })),
5521
+ ...turn.text.length > 0 ? [
5522
+ {
5523
+ type: "text",
5524
+ text: turn.text
5525
+ }
5526
+ ] : []
5527
+ ]
5528
+ };
5529
+ }
5530
+ __name(queuedUserMessage, "queuedUserMessage");
5531
+ function draftOf(message) {
5532
+ if (typeof message !== "object" || message === null) {
5533
+ return {
5534
+ text: "",
5535
+ attachments: []
5536
+ };
5537
+ }
5538
+ const draft = message;
5539
+ let text = typeof draft.text === "string" ? draft.text : "";
5540
+ if (text === "" && Array.isArray(draft.parts)) {
5541
+ text = draft.parts.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("");
5542
+ }
5543
+ const files = Array.isArray(draft.files) ? draft.files : Array.isArray(draft.parts) ? draft.parts.filter((part) => part.type === "file") : [];
5544
+ const attachments = files.flatMap((file) => {
5545
+ const mediaId = file.providerMetadata?.agent?.mediaId;
5546
+ return typeof mediaId === "string" ? [
5547
+ {
5548
+ mediaId,
5549
+ url: String(file.url ?? ""),
5550
+ contentType: String(file.mediaType ?? "application/octet-stream"),
5551
+ name: String(file.filename ?? mediaId)
5552
+ }
5553
+ ] : [];
5554
+ });
5555
+ return {
5556
+ text,
5557
+ attachments
5558
+ };
5559
+ }
5560
+ __name(draftOf, "draftOf");
5561
+ function lastUserIndex(messages) {
5562
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
5563
+ if (messages[index]?.role === "user") return index;
5564
+ }
5565
+ return -1;
5566
+ }
5567
+ __name(lastUserIndex, "lastUserIndex");
5362
5568
  var localChatSequence = 0;
5363
5569
  function localChatId() {
5364
5570
  localChatSequence += 1;
@@ -5398,6 +5604,41 @@ function useAgentChat(options = {}) {
5398
5604
  const pendingPin = (0, import_react23.useRef)(void 0);
5399
5605
  const regenerateNext = (0, import_react23.useRef)(false);
5400
5606
  const currentThreadId = (0, import_react23.useCallback)(() => latest.current.threadId ?? createdThreadId.current, []);
5607
+ const [queueState, setQueueState] = (0, import_react23.useState)(EMPTY_QUEUE);
5608
+ const queueStateRef = (0, import_react23.useRef)(queueState);
5609
+ const applyQueue = (0, import_react23.useCallback)((next) => {
5610
+ queueStateRef.current = next;
5611
+ setQueueState(next);
5612
+ }, []);
5613
+ const [pendingSends, setPendingSends] = (0, import_react23.useState)([]);
5614
+ const [queueError, setQueueError] = (0, import_react23.useState)(null);
5615
+ const handoff = (0, import_react23.useRef)(void 0);
5616
+ const queuedSend = (0, import_react23.useRef)(void 0);
5617
+ const threadWaiters = (0, import_react23.useRef)([]);
5618
+ const turnBody = (0, import_react23.useCallback)(() => {
5619
+ const current = latest.current;
5620
+ const pageContext = current.getPageContext?.() ?? null;
5621
+ const threadId = currentThreadId();
5622
+ const model = current.model ?? (lockedRef.current ? void 0 : pickedModelRef.current);
5623
+ return {
5624
+ // Read per send: a host that switches agents before the first message (a picker on a new
5625
+ // chat) sends the one picked now, not the one this chat mounted with.
5626
+ ...current.agent !== void 0 ? {
5627
+ agent: current.agent
5628
+ } : {},
5629
+ ...threadId !== void 0 ? {
5630
+ threadId
5631
+ } : {},
5632
+ ...model !== void 0 ? {
5633
+ model
5634
+ } : {},
5635
+ ...pageContext ? {
5636
+ pageContext
5637
+ } : {}
5638
+ };
5639
+ }, [
5640
+ currentThreadId
5641
+ ]);
5401
5642
  const [connection, setConnection] = (0, import_react23.useState)({
5402
5643
  status: "live"
5403
5644
  });
@@ -5406,6 +5647,11 @@ function useAgentChat(options = {}) {
5406
5647
  function onMeta(meta) {
5407
5648
  settlingRunIdRef.current = meta.runId;
5408
5649
  setRunId(meta.runId);
5650
+ if (meta.threadId) {
5651
+ const waiting = threadWaiters.current;
5652
+ threadWaiters.current = [];
5653
+ for (const resolve of waiting) resolve(meta.threadId);
5654
+ }
5409
5655
  if (latest.current.threadId === void 0 && meta.threadId && createdThreadId.current !== meta.threadId) {
5410
5656
  createdThreadId.current = meta.threadId;
5411
5657
  const pin = pendingPin.current;
@@ -5437,27 +5683,10 @@ function useAgentChat(options = {}) {
5437
5683
  agent: options.agent
5438
5684
  } : {},
5439
5685
  getBody: /* @__PURE__ */ __name(() => {
5440
- const current = latest.current;
5441
- const pageContext = current.getPageContext?.() ?? null;
5442
5686
  const regenerate2 = regenerateNext.current;
5443
5687
  regenerateNext.current = false;
5444
- const threadId = currentThreadId();
5445
- const model = current.model ?? (lockedRef.current ? void 0 : pickedModelRef.current);
5446
5688
  return {
5447
- // Read per send: a host that switches agents before the first message (a picker on a
5448
- // new chat) sends the one picked now, not the one this chat mounted with.
5449
- ...current.agent !== void 0 ? {
5450
- agent: current.agent
5451
- } : {},
5452
- ...threadId !== void 0 ? {
5453
- threadId
5454
- } : {},
5455
- ...model !== void 0 ? {
5456
- model
5457
- } : {},
5458
- ...pageContext ? {
5459
- pageContext
5460
- } : {},
5689
+ ...turnBody(),
5461
5690
  ...regenerate2 ? {
5462
5691
  regenerate: true
5463
5692
  } : {}
@@ -5467,7 +5696,11 @@ function useAgentChat(options = {}) {
5467
5696
  onMeta,
5468
5697
  onAttemptStart: /* @__PURE__ */ __name(() => {
5469
5698
  settlingRunIdRef.current = void 0;
5470
- }, "onAttemptStart")
5699
+ }, "onAttemptStart"),
5700
+ // A queued turn this chat went to attach to had already finished: read what it wrote.
5701
+ onResumeGone: /* @__PURE__ */ __name(() => {
5702
+ void resyncFromThread();
5703
+ }, "onResumeGone")
5471
5704
  });
5472
5705
  }, []);
5473
5706
  const backgroundRef = (0, import_react23.useRef)(() => Promise.resolve());
@@ -5480,6 +5713,9 @@ function useAgentChat(options = {}) {
5480
5713
  } : {},
5481
5714
  onData: /* @__PURE__ */ __name((part) => {
5482
5715
  latest.current.onData?.(part);
5716
+ if (part.type === "data-queue") {
5717
+ onQueueData(part.data);
5718
+ }
5483
5719
  if (part.type === "data-title") {
5484
5720
  const title = part.data?.title;
5485
5721
  if (typeof title === "string") {
@@ -5511,6 +5747,20 @@ function useAgentChat(options = {}) {
5511
5747
  setConnection({
5512
5748
  status: "live"
5513
5749
  });
5750
+ const queuedInstead = queuedSend.current;
5751
+ if (queuedInstead !== void 0) {
5752
+ queuedSend.current = void 0;
5753
+ chatRef.current.setMessages((current) => {
5754
+ const last = lastUserIndex(current);
5755
+ return last === -1 ? current : current.slice(0, last);
5756
+ });
5757
+ }
5758
+ const next = handoff.current;
5759
+ handoff.current = void 0;
5760
+ if (next !== void 0) {
5761
+ attachTo(next);
5762
+ return;
5763
+ }
5514
5764
  if (resyncAfterSettle.current) {
5515
5765
  resyncAfterSettle.current = false;
5516
5766
  void resyncFromThread();
@@ -5519,6 +5769,46 @@ function useAgentChat(options = {}) {
5519
5769
  });
5520
5770
  const chatRef = (0, import_react23.useRef)(chat);
5521
5771
  chatRef.current = chat;
5772
+ function onQueueData(data) {
5773
+ const frame = data ?? {};
5774
+ const before = queueStateRef.current;
5775
+ if (frame.queue !== void 0) applyQueue(frame.queue);
5776
+ const started = frame.started;
5777
+ if (started !== void 0 && started.runId !== settlingRunIdRef.current) {
5778
+ const waiting = before.items.find((item) => item.id === started.messageId);
5779
+ handoff.current = {
5780
+ ...started,
5781
+ text: waiting?.content ?? "",
5782
+ attachments: waiting?.attachments ?? []
5783
+ };
5784
+ }
5785
+ if (frame.queuedSend !== void 0) {
5786
+ queuedSend.current = frame.queuedSend;
5787
+ if (frame.queuedSend.runId !== void 0) {
5788
+ handoff.current = {
5789
+ messageId: frame.queuedSend.messageId,
5790
+ runId: frame.queuedSend.runId,
5791
+ ...draftOf(chatRef.current.messages[lastUserIndex(chatRef.current.messages)])
5792
+ };
5793
+ }
5794
+ }
5795
+ }
5796
+ __name(onQueueData, "onQueueData");
5797
+ function attachTo(turn) {
5798
+ setTimeout(() => {
5799
+ if (turn.text.length > 0 || turn.attachments.length > 0) {
5800
+ chatRef.current.setMessages((current) => current.some((message) => message.id === turn.messageId) ? current : [
5801
+ ...current,
5802
+ queuedUserMessage(turn)
5803
+ ]);
5804
+ } else {
5805
+ resyncAfterSettle.current = true;
5806
+ }
5807
+ autoResumeRunIdRef.current = turn.runId;
5808
+ void chatRef.current.resumeStream();
5809
+ }, 0);
5810
+ }
5811
+ __name(attachTo, "attachTo");
5522
5812
  async function resyncFromThread() {
5523
5813
  const threadId = currentThreadId();
5524
5814
  if (threadId === void 0) return;
@@ -5541,6 +5831,11 @@ function useAgentChat(options = {}) {
5541
5831
  setHistoryError(null);
5542
5832
  autoResumeRunIdRef.current = void 0;
5543
5833
  pendingPin.current = void 0;
5834
+ applyQueue(EMPTY_QUEUE);
5835
+ setPendingSends([]);
5836
+ setQueueError(null);
5837
+ handoff.current = void 0;
5838
+ queuedSend.current = void 0;
5544
5839
  }, [
5545
5840
  chatId
5546
5841
  ]);
@@ -5560,6 +5855,11 @@ function useAgentChat(options = {}) {
5560
5855
  setActiveRunId(active);
5561
5856
  setThreadModel(thread.model ?? null);
5562
5857
  setHistoryError(null);
5858
+ const queue2 = thread.queue ?? EMPTY_QUEUE;
5859
+ applyQueue(queue2);
5860
+ if (queue2.items.length > 0 && queue2.paused === null && active === null && typeof client.resumeQueue === "function") {
5861
+ void resumeQueueRef.current().catch(() => void 0);
5862
+ }
5563
5863
  const resuming = wantResume && active !== null;
5564
5864
  if (wantHistory && chatRef.current.messages.length === 0) {
5565
5865
  const rows = resuming ? stored.filter((message) => !(message.role === "assistant" && message.runId === active)) : stored;
@@ -5595,10 +5895,98 @@ function useAgentChat(options = {}) {
5595
5895
  const blocked = options.blocked !== void 0 ? options.blocked : quota.blocked;
5596
5896
  const blockedRef = (0, import_react23.useRef)(blocked);
5597
5897
  blockedRef.current = blocked;
5898
+ const whileRunning = typeof client.enqueueMessage === "function" ? options.whileRunning ?? "queue" : "block";
5899
+ const whileRunningRef = (0, import_react23.useRef)(whileRunning);
5900
+ whileRunningRef.current = whileRunning;
5901
+ const threadForQueue = (0, import_react23.useCallback)(async () => {
5902
+ const known = currentThreadId();
5903
+ if (known !== void 0) return known;
5904
+ if (!isTurnInFlight()) throw new Error("queue: this chat has no thread yet");
5905
+ return new Promise((resolve) => threadWaiters.current.push(resolve));
5906
+ }, [
5907
+ currentThreadId,
5908
+ isTurnInFlight
5909
+ ]);
5910
+ const queueCall = (0, import_react23.useCallback)(async (run) => {
5911
+ try {
5912
+ const result = await run();
5913
+ setQueueError(null);
5914
+ return result;
5915
+ } catch (error) {
5916
+ setQueueError(error instanceof Error ? error : new Error(String(error)));
5917
+ throw error;
5918
+ }
5919
+ }, []);
5920
+ const enqueue = (0, import_react23.useCallback)(async (text2, { attachments = [], mode = "queue" } = {}) => {
5921
+ const block = blockedRef.current;
5922
+ if (block != null) throw new QuotaBlockedError(block);
5923
+ const send = client.enqueueMessage;
5924
+ if (typeof send !== "function") throw new AgentBackendUnsupportedError("enqueueMessage");
5925
+ localQueueSequence += 1;
5926
+ const localId = `local-queued-${localQueueSequence}`;
5927
+ setPendingSends((current) => [
5928
+ ...current,
5929
+ {
5930
+ id: localId,
5931
+ text: text2,
5932
+ attachments,
5933
+ state: "sending",
5934
+ interrupt: mode === "interrupt",
5935
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
5936
+ }
5937
+ ]);
5938
+ try {
5939
+ await queueCall(async () => {
5940
+ const threadId = await threadForQueue();
5941
+ const result = await send.call(client, {
5942
+ body: {
5943
+ ...turnBody(),
5944
+ threadId,
5945
+ message: text2,
5946
+ mode,
5947
+ ...attachments.length > 0 ? {
5948
+ attachments: attachments.map(({ mediaId }) => ({
5949
+ mediaId
5950
+ }))
5951
+ } : {}
5952
+ }
5953
+ });
5954
+ applyQueue(result.queue);
5955
+ if (result.runId !== void 0) {
5956
+ const turn = {
5957
+ messageId: result.messageId,
5958
+ runId: result.runId,
5959
+ text: text2,
5960
+ attachments
5961
+ };
5962
+ if (isTurnInFlight()) handoff.current = turn;
5963
+ else attachTo(turn);
5964
+ }
5965
+ });
5966
+ } finally {
5967
+ setPendingSends((current) => current.filter((item) => item.id !== localId));
5968
+ }
5969
+ }, [
5970
+ client,
5971
+ queueCall,
5972
+ threadForQueue,
5973
+ turnBody,
5974
+ applyQueue,
5975
+ isTurnInFlight
5976
+ ]);
5598
5977
  const sendMessage = (0, import_react23.useCallback)(async (...args) => {
5599
5978
  const block = blockedRef.current;
5600
5979
  if (block != null) throw new QuotaBlockedError(block);
5601
- if (isTurnInFlight()) return;
5980
+ if (isTurnInFlight()) {
5981
+ const mode = whileRunningRef.current;
5982
+ if (mode === "block") return;
5983
+ const draft = draftOf(args[0]);
5984
+ await enqueue(draft.text, {
5985
+ attachments: draft.attachments,
5986
+ mode
5987
+ });
5988
+ return;
5989
+ }
5602
5990
  turnInFlight.current = true;
5603
5991
  try {
5604
5992
  await chatRef.current.sendMessage(...args);
@@ -5606,8 +5994,122 @@ function useAgentChat(options = {}) {
5606
5994
  turnInFlight.current = false;
5607
5995
  }
5608
5996
  }, [
5997
+ isTurnInFlight,
5998
+ enqueue
5999
+ ]);
6000
+ const removeQueued = (0, import_react23.useCallback)(async (id) => {
6001
+ const previous = queueStateRef.current;
6002
+ applyQueue({
6003
+ ...previous,
6004
+ items: previous.items.filter((item) => item.id !== id)
6005
+ });
6006
+ try {
6007
+ applyQueue(await queueCall(() => requireBackendMethod(client, "removeQueuedMessage")(id)));
6008
+ } catch (error) {
6009
+ applyQueue(previous);
6010
+ throw error;
6011
+ }
6012
+ }, [
6013
+ client,
6014
+ queueCall,
6015
+ applyQueue
6016
+ ]);
6017
+ const editQueued = (0, import_react23.useCallback)(async (id, text2) => {
6018
+ applyQueue(await queueCall(() => requireBackendMethod(client, "updateQueuedMessage")(id, {
6019
+ message: text2
6020
+ })));
6021
+ }, [
6022
+ client,
6023
+ queueCall,
6024
+ applyQueue
6025
+ ]);
6026
+ const moveQueued = (0, import_react23.useCallback)(async (id, index) => {
6027
+ const previous = queueStateRef.current;
6028
+ const moving = previous.items.find((item) => item.id === id);
6029
+ if (moving !== void 0) {
6030
+ const rest = previous.items.filter((item) => item.id !== id);
6031
+ rest.splice(Math.max(0, Math.min(rest.length, index)), 0, moving);
6032
+ applyQueue({
6033
+ ...previous,
6034
+ items: rest
6035
+ });
6036
+ }
6037
+ try {
6038
+ applyQueue(await queueCall(() => requireBackendMethod(client, "updateQueuedMessage")(id, {
6039
+ position: index
6040
+ })));
6041
+ } catch (error) {
6042
+ applyQueue(previous);
6043
+ throw error;
6044
+ }
6045
+ }, [
6046
+ client,
6047
+ queueCall,
6048
+ applyQueue
6049
+ ]);
6050
+ const clearQueue = (0, import_react23.useCallback)(async () => {
6051
+ const threadId = currentThreadId();
6052
+ if (threadId === void 0) return;
6053
+ applyQueue(await queueCall(() => requireBackendMethod(client, "clearQueue")(threadId)));
6054
+ }, [
6055
+ client,
6056
+ queueCall,
6057
+ currentThreadId,
6058
+ applyQueue
6059
+ ]);
6060
+ const resumeQueue = (0, import_react23.useCallback)(async () => {
6061
+ const threadId = currentThreadId();
6062
+ if (threadId === void 0) return;
6063
+ const before = queueStateRef.current;
6064
+ const result = await queueCall(() => requireBackendMethod(client, "resumeQueue")(threadId));
6065
+ applyQueue({
6066
+ items: result.items,
6067
+ paused: result.paused
6068
+ });
6069
+ if (result.runId !== void 0) {
6070
+ const head = before.items.find((item) => item.id === result.runId) ?? before.items[0];
6071
+ const turn = {
6072
+ messageId: result.runId,
6073
+ runId: result.runId,
6074
+ text: head?.content ?? "",
6075
+ attachments: head?.attachments ?? []
6076
+ };
6077
+ if (isTurnInFlight()) handoff.current = turn;
6078
+ else attachTo(turn);
6079
+ }
6080
+ }, [
6081
+ client,
6082
+ queueCall,
6083
+ currentThreadId,
6084
+ applyQueue,
5609
6085
  isTurnInFlight
5610
6086
  ]);
6087
+ const resumeQueueRef = (0, import_react23.useRef)(resumeQueue);
6088
+ resumeQueueRef.current = resumeQueue;
6089
+ const queuedItems = [
6090
+ ...pendingSends.filter((item) => item.interrupt),
6091
+ ...queueState.items.map((item) => ({
6092
+ id: item.id,
6093
+ text: item.content,
6094
+ attachments: item.attachments ?? [],
6095
+ state: "queued",
6096
+ interrupt: item.interrupt === true,
6097
+ createdAt: item.createdAt
6098
+ })),
6099
+ ...pendingSends.filter((item) => !item.interrupt)
6100
+ ];
6101
+ const queue = {
6102
+ items: queuedItems,
6103
+ paused: queueState.paused,
6104
+ isSupported: typeof client.enqueueMessage === "function",
6105
+ add: enqueue,
6106
+ remove: removeQueued,
6107
+ edit: editQueued,
6108
+ move: moveQueued,
6109
+ clear: clearQueue,
6110
+ resume: resumeQueue,
6111
+ error: queueError
6112
+ };
5611
6113
  const addToolResult = (0, import_react23.useCallback)(({ tool, toolCallId, output }) => chatRef.current.addToolResult({
5612
6114
  tool,
5613
6115
  toolCallId,
@@ -5848,12 +6350,14 @@ function useAgentChat(options = {}) {
5848
6350
  };
5849
6351
  const status = connection.status === "reconnecting" ? "reconnecting" : chat.status;
5850
6352
  const isBusy = status === "submitted" || status === "streaming" || status === "reconnecting";
6353
+ const isBusyRef = (0, import_react23.useRef)(isBusy);
6354
+ isBusyRef.current = isBusy;
5851
6355
  const files = useAttachments({
5852
6356
  ...options.composer,
5853
6357
  backend: client
5854
6358
  });
5855
6359
  const [text, setText] = (0, import_react23.useState)("");
5856
- const blockedBy = blocked != null ? "quota" : files.isUploading ? "uploading" : isBusy ? "busy" : text.trim().length === 0 && files.refs.length === 0 ? "empty" : null;
6360
+ const blockedBy = blocked != null ? "quota" : files.isUploading ? "uploading" : isBusy && whileRunning === "block" ? "busy" : text.trim().length === 0 && files.refs.length === 0 ? "empty" : null;
5857
6361
  const composerRef = (0, import_react23.useRef)({
5858
6362
  text,
5859
6363
  files,
@@ -5884,6 +6388,17 @@ function useAgentChat(options = {}) {
5884
6388
  const draft = current.text.trim();
5885
6389
  setText("");
5886
6390
  current.files.clear();
6391
+ const mode = whileRunningRef.current;
6392
+ if (mode !== "block" && (isTurnInFlight() || isBusyRef.current)) {
6393
+ const attachments = current.files.items.flatMap((item) => item.status === "ready" && item.attachment !== void 0 ? [
6394
+ item.attachment
6395
+ ] : []);
6396
+ await enqueue(draft, {
6397
+ attachments,
6398
+ mode
6399
+ });
6400
+ return;
6401
+ }
5887
6402
  await sendMessage(files2.length > 0 ? {
5888
6403
  text: draft,
5889
6404
  files: files2
@@ -5895,7 +6410,9 @@ function useAgentChat(options = {}) {
5895
6410
  }
5896
6411
  } : void 0);
5897
6412
  }, [
5898
- sendMessage
6413
+ sendMessage,
6414
+ enqueue,
6415
+ isTurnInFlight
5899
6416
  ]);
5900
6417
  const composer = {
5901
6418
  text,
@@ -5930,6 +6447,11 @@ function useAgentChat(options = {}) {
5930
6447
  ...ownCatalog === void 0 ? {
5931
6448
  toolCatalog: tools.catalog
5932
6449
  } : {},
6450
+ queue: {
6451
+ items: queue.items,
6452
+ paused: queue.paused,
6453
+ remove: removeQueued
6454
+ },
5933
6455
  ...options.transcript
5934
6456
  });
5935
6457
  const background = {
@@ -5964,6 +6486,11 @@ function useAgentChat(options = {}) {
5964
6486
  transcript,
5965
6487
  /** A headless composer: draft, files, send gate, `submit()`. */
5966
6488
  composer,
6489
+ /**
6490
+ * Messages sent while a turn was running, waiting their turn (server-side): list, edit, move,
6491
+ * remove, clear, and resume a paused queue.
6492
+ */
6493
+ queue,
5967
6494
  /** The model picker: `list`, `selected`, `select(id)`, `pinToThread(id)`. */
5968
6495
  models,
5969
6496
  /** The caller's budget (`GET <base>/quota`); `quota.blocked` gates sends. */