@dudousxd/nestjs-agent-react 0.25.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,
@@ -3785,13 +3854,56 @@ function useChatTranscript(options) {
3785
3854
  },
3786
3855
  scroll,
3787
3856
  stop: {
3788
- available: isBusy && options.onStop !== void 0,
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([
@@ -4055,11 +4167,12 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
4055
4167
  ...options.toolCatalog !== void 0 ? {
4056
4168
  toolCatalog: options.toolCatalog
4057
4169
  } : {},
4058
- ...handlers.onAnswer !== void 0 ? {
4170
+ // Read-only still lifts question sets into their block, so the outcome shows.
4171
+ ...handlers.onAnswer !== void 0 || options.readOnly === true && options.onAnswer !== null ? {
4059
4172
  elicitation: {
4060
4173
  picked: /* @__PURE__ */ __name((toolCallId, questionId) => picks.get(pickKey(toolCallId, questionId)), "picked"),
4061
4174
  pick,
4062
- canAnswer: true,
4175
+ canAnswer: handlers.onAnswer !== void 0,
4063
4176
  canSkip: handlers.onSkip !== void 0,
4064
4177
  answer,
4065
4178
  skip,
@@ -4087,7 +4200,7 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
4087
4200
  copy: callbacks.copy
4088
4201
  },
4089
4202
  edit: {
4090
- available: isUser && options.editable === true && options.onEditSubmit !== void 0,
4203
+ available: isUser && options.readOnly !== true && options.editable === true && options.onEditSubmit !== void 0,
4091
4204
  isEditing,
4092
4205
  draft: draft ?? text,
4093
4206
  canSave: (draft ?? "").trim().length > 0,
@@ -4103,11 +4216,11 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
4103
4216
  }), "getTextareaProps")
4104
4217
  },
4105
4218
  fork: {
4106
- available: options.onFork !== void 0,
4219
+ available: options.readOnly !== true && options.onFork !== void 0,
4107
4220
  run: callbacks.fork
4108
4221
  },
4109
4222
  regenerate: {
4110
- available: isLastAssistant && options.regeneratable === true && options.onRegenerate !== void 0,
4223
+ available: isLastAssistant && options.readOnly !== true && options.regeneratable === true && options.onRegenerate !== void 0,
4111
4224
  run: callbacks.regenerate
4112
4225
  }
4113
4226
  });
@@ -4116,7 +4229,7 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
4116
4229
  }
4117
4230
  __name(useTranscriptItems, "useTranscriptItems");
4118
4231
  function settleHandlers(options, backend) {
4119
- const pick = /* @__PURE__ */ __name((own, fallback2) => own === null ? void 0 : own ?? fallback2, "pick");
4232
+ const pick = /* @__PURE__ */ __name((own, fallback2) => options.readOnly === true || own === null ? void 0 : own ?? fallback2, "pick");
4120
4233
  return {
4121
4234
  onAnswer: pick(options.onAnswer, backend.answerToolCall ? async (input) => {
4122
4235
  await backend.answerToolCall?.(input);
@@ -4409,10 +4522,13 @@ function joinClasses(...entries) {
4409
4522
  __name(joinClasses, "joinClasses");
4410
4523
 
4411
4524
  // src/components/message-list.tsx
4412
- function MessageList({ messages, status, renderToolPart, renderToolGroup, renderText, renderReasoning, renderFiles, renderUi, reasoningLabel, getMeta, 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 }) {
4413
4526
  const transcript = useChatTranscript({
4414
4527
  messages,
4415
4528
  status,
4529
+ ...readOnly !== void 0 ? {
4530
+ readOnly
4531
+ } : {},
4416
4532
  ...onFork ? {
4417
4533
  onFork
4418
4534
  } : {},
@@ -4436,9 +4552,16 @@ function MessageList({ messages, status, renderToolPart, renderToolGroup, render
4436
4552
  } : {},
4437
4553
  ...followUps !== void 0 ? {
4438
4554
  followUps
4555
+ } : {},
4556
+ ...queue !== void 0 ? {
4557
+ queue: {
4558
+ items: queue.items,
4559
+ paused: queue.paused,
4560
+ remove: queue.remove
4561
+ }
4439
4562
  } : {}
4440
4563
  });
4441
- if (transcript.showEmptyState) {
4564
+ if (transcript.showEmptyState && transcript.queued.length === 0) {
4442
4565
  return /* @__PURE__ */ React.createElement("div", {
4443
4566
  className: classNames?.empty
4444
4567
  }, emptyState);
@@ -4482,7 +4605,23 @@ function MessageList({ messages, status, renderToolPart, renderToolGroup, render
4482
4605
  ...getMeta ? {
4483
4606
  meta: getMeta(item.message)
4484
4607
  } : {}
4485
- })), 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", {
4486
4625
  className: classNames?.typing
4487
4626
  }, typingIndicator ?? "Thinking\u2026") : null, error && onRetry ? /* @__PURE__ */ React.createElement("div", {
4488
4627
  className: classNames?.error
@@ -4877,6 +5016,7 @@ function isPromise(value) {
4877
5016
  __name(isPromise, "isPromise");
4878
5017
 
4879
5018
  // src/presentation/use-tool-catalog.ts
5019
+ import { ALL_AGENTS } from "@dudousxd/nestjs-agent-core";
4880
5020
  import { useCallback as useCallback10, useEffect as useEffect13, useMemo as useMemo10, useState as useState13 } from "react";
4881
5021
  var shared2 = /* @__PURE__ */ new WeakMap();
4882
5022
  function load2(client, agent) {
@@ -5250,6 +5390,77 @@ __name(sumUsage, "sumUsage");
5250
5390
  // src/use-agent-chat.ts
5251
5391
  import { useChat } from "@ai-sdk/react";
5252
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");
5253
5464
  var localChatSequence = 0;
5254
5465
  function localChatId() {
5255
5466
  localChatSequence += 1;
@@ -5279,7 +5490,7 @@ function useAgentChat(options = {}) {
5279
5490
  const settlingRunIdRef = useRef12(void 0);
5280
5491
  const autoResumeRunIdRef = useRef12(void 0);
5281
5492
  const [activeRunId, setActiveRunId] = useState15(null);
5282
- const [isLoadingHistory, setIsLoadingHistory] = useState15(false);
5493
+ const [historyFor, setHistoryFor] = useState15(void 0);
5283
5494
  const [historyError, setHistoryError] = useState15(null);
5284
5495
  const [threadModel, setThreadModel] = useState15(null);
5285
5496
  const [pickedModel, setPickedModel] = useState15(void 0);
@@ -5289,6 +5500,41 @@ function useAgentChat(options = {}) {
5289
5500
  const pendingPin = useRef12(void 0);
5290
5501
  const regenerateNext = useRef12(false);
5291
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
+ ]);
5292
5538
  const [connection, setConnection] = useState15({
5293
5539
  status: "live"
5294
5540
  });
@@ -5297,6 +5543,11 @@ function useAgentChat(options = {}) {
5297
5543
  function onMeta(meta) {
5298
5544
  settlingRunIdRef.current = meta.runId;
5299
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
+ }
5300
5551
  if (latest.current.threadId === void 0 && meta.threadId && createdThreadId.current !== meta.threadId) {
5301
5552
  createdThreadId.current = meta.threadId;
5302
5553
  const pin = pendingPin.current;
@@ -5328,27 +5579,10 @@ function useAgentChat(options = {}) {
5328
5579
  agent: options.agent
5329
5580
  } : {},
5330
5581
  getBody: /* @__PURE__ */ __name(() => {
5331
- const current = latest.current;
5332
- const pageContext = current.getPageContext?.() ?? null;
5333
5582
  const regenerate2 = regenerateNext.current;
5334
5583
  regenerateNext.current = false;
5335
- const threadId = currentThreadId();
5336
- const model = current.model ?? (lockedRef.current ? void 0 : pickedModelRef.current);
5337
5584
  return {
5338
- // Read per send: a host that switches agents before the first message (a picker on a
5339
- // new chat) sends the one picked now, not the one this chat mounted with.
5340
- ...current.agent !== void 0 ? {
5341
- agent: current.agent
5342
- } : {},
5343
- ...threadId !== void 0 ? {
5344
- threadId
5345
- } : {},
5346
- ...model !== void 0 ? {
5347
- model
5348
- } : {},
5349
- ...pageContext ? {
5350
- pageContext
5351
- } : {},
5585
+ ...turnBody(),
5352
5586
  ...regenerate2 ? {
5353
5587
  regenerate: true
5354
5588
  } : {}
@@ -5358,7 +5592,11 @@ function useAgentChat(options = {}) {
5358
5592
  onMeta,
5359
5593
  onAttemptStart: /* @__PURE__ */ __name(() => {
5360
5594
  settlingRunIdRef.current = void 0;
5361
- }, "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")
5362
5600
  });
5363
5601
  }, []);
5364
5602
  const backgroundRef = useRef12(() => Promise.resolve());
@@ -5371,6 +5609,9 @@ function useAgentChat(options = {}) {
5371
5609
  } : {},
5372
5610
  onData: /* @__PURE__ */ __name((part) => {
5373
5611
  latest.current.onData?.(part);
5612
+ if (part.type === "data-queue") {
5613
+ onQueueData(part.data);
5614
+ }
5374
5615
  if (part.type === "data-title") {
5375
5616
  const title = part.data?.title;
5376
5617
  if (typeof title === "string") {
@@ -5402,6 +5643,20 @@ function useAgentChat(options = {}) {
5402
5643
  setConnection({
5403
5644
  status: "live"
5404
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
+ }
5405
5660
  if (resyncAfterSettle.current) {
5406
5661
  resyncAfterSettle.current = false;
5407
5662
  void resyncFromThread();
@@ -5410,6 +5665,46 @@ function useAgentChat(options = {}) {
5410
5665
  });
5411
5666
  const chatRef = useRef12(chat);
5412
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");
5413
5708
  async function resyncFromThread() {
5414
5709
  const threadId = currentThreadId();
5415
5710
  if (threadId === void 0) return;
@@ -5432,6 +5727,11 @@ function useAgentChat(options = {}) {
5432
5727
  setHistoryError(null);
5433
5728
  autoResumeRunIdRef.current = void 0;
5434
5729
  pendingPin.current = void 0;
5730
+ applyQueue(EMPTY_QUEUE);
5731
+ setPendingSends([]);
5732
+ setQueueError(null);
5733
+ handoff.current = void 0;
5734
+ queuedSend.current = void 0;
5435
5735
  }, [
5436
5736
  chatId
5437
5737
  ]);
@@ -5444,7 +5744,6 @@ function useAgentChat(options = {}) {
5444
5744
  const wantResume = current.resume !== false && current.resumeRunId === void 0;
5445
5745
  if (!wantHistory && !wantResume) return;
5446
5746
  let cancelled = false;
5447
- if (wantHistory) setIsLoadingHistory(true);
5448
5747
  client.getThread(threadId).then((thread) => {
5449
5748
  if (cancelled || thread == null) return;
5450
5749
  const active = thread.activeRunId ?? null;
@@ -5452,6 +5751,11 @@ function useAgentChat(options = {}) {
5452
5751
  setActiveRunId(active);
5453
5752
  setThreadModel(thread.model ?? null);
5454
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
+ }
5455
5759
  const resuming = wantResume && active !== null;
5456
5760
  if (wantHistory && chatRef.current.messages.length === 0) {
5457
5761
  const rows = resuming ? stored.filter((message) => !(message.role === "assistant" && message.runId === active)) : stored;
@@ -5465,17 +5769,17 @@ function useAgentChat(options = {}) {
5465
5769
  if (cancelled) return;
5466
5770
  setHistoryError(error instanceof Error ? error : new Error(String(error)));
5467
5771
  }).finally(() => {
5468
- if (!cancelled) setIsLoadingHistory(false);
5772
+ if (!cancelled) setHistoryFor(threadId);
5469
5773
  });
5470
5774
  return () => {
5471
5775
  cancelled = true;
5472
- setIsLoadingHistory(false);
5473
5776
  };
5474
5777
  }, [
5475
5778
  client,
5476
5779
  chatId,
5477
5780
  options.threadId
5478
5781
  ]);
5782
+ const isLoadingHistory = options.threadId !== void 0 && options.threadId !== createdThreadId.current && options.history !== false && options.initialMessages === void 0 && historyFor !== options.threadId;
5479
5783
  const turnInFlight = useRef12(false);
5480
5784
  const isTurnInFlight = useCallback11(() => turnInFlight.current || transport.isAttemptLive, [
5481
5785
  transport
@@ -5487,10 +5791,98 @@ function useAgentChat(options = {}) {
5487
5791
  const blocked = options.blocked !== void 0 ? options.blocked : quota.blocked;
5488
5792
  const blockedRef = useRef12(blocked);
5489
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
+ ]);
5490
5873
  const sendMessage = useCallback11(async (...args) => {
5491
5874
  const block = blockedRef.current;
5492
5875
  if (block != null) throw new QuotaBlockedError(block);
5493
- 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
+ }
5494
5886
  turnInFlight.current = true;
5495
5887
  try {
5496
5888
  await chatRef.current.sendMessage(...args);
@@ -5498,8 +5890,122 @@ function useAgentChat(options = {}) {
5498
5890
  turnInFlight.current = false;
5499
5891
  }
5500
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,
5501
5981
  isTurnInFlight
5502
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
+ };
5503
6009
  const addToolResult = useCallback11(({ tool, toolCallId, output }) => chatRef.current.addToolResult({
5504
6010
  tool,
5505
6011
  toolCallId,
@@ -5740,12 +6246,14 @@ function useAgentChat(options = {}) {
5740
6246
  };
5741
6247
  const status = connection.status === "reconnecting" ? "reconnecting" : chat.status;
5742
6248
  const isBusy = status === "submitted" || status === "streaming" || status === "reconnecting";
6249
+ const isBusyRef = useRef12(isBusy);
6250
+ isBusyRef.current = isBusy;
5743
6251
  const files = useAttachments({
5744
6252
  ...options.composer,
5745
6253
  backend: client
5746
6254
  });
5747
6255
  const [text, setText] = useState15("");
5748
- 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;
5749
6257
  const composerRef = useRef12({
5750
6258
  text,
5751
6259
  files,
@@ -5776,6 +6284,17 @@ function useAgentChat(options = {}) {
5776
6284
  const draft = current.text.trim();
5777
6285
  setText("");
5778
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
+ }
5779
6298
  await sendMessage(files2.length > 0 ? {
5780
6299
  text: draft,
5781
6300
  files: files2
@@ -5787,7 +6306,9 @@ function useAgentChat(options = {}) {
5787
6306
  }
5788
6307
  } : void 0);
5789
6308
  }, [
5790
- sendMessage
6309
+ sendMessage,
6310
+ enqueue,
6311
+ isTurnInFlight
5791
6312
  ]);
5792
6313
  const composer = {
5793
6314
  text,
@@ -5822,6 +6343,11 @@ function useAgentChat(options = {}) {
5822
6343
  ...ownCatalog === void 0 ? {
5823
6344
  toolCatalog: tools.catalog
5824
6345
  } : {},
6346
+ queue: {
6347
+ items: queue.items,
6348
+ paused: queue.paused,
6349
+ remove: removeQueued
6350
+ },
5825
6351
  ...options.transcript
5826
6352
  });
5827
6353
  const background = {
@@ -5856,6 +6382,11 @@ function useAgentChat(options = {}) {
5856
6382
  transcript,
5857
6383
  /** A headless composer: draft, files, send gate, `submit()`. */
5858
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,
5859
6390
  /** The model picker: `list`, `selected`, `select(id)`, `pinToThread(id)`. */
5860
6391
  models,
5861
6392
  /** The caller's budget (`GET <base>/quota`); `quota.blocked` gates sends. */
@@ -5885,6 +6416,7 @@ var QuotaBlockedError = class extends Error {
5885
6416
  }
5886
6417
  };
5887
6418
  export {
6419
+ ALL_AGENTS,
5888
6420
  AgentBackendUnsupportedError,
5889
6421
  AgentChatTransport,
5890
6422
  AgentClient,