@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.cjs CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var src_exports = {};
33
33
  __export(src_exports, {
34
+ ALL_AGENTS: () => import_nestjs_agent_core4.ALL_AGENTS,
34
35
  AgentBackendUnsupportedError: () => AgentBackendUnsupportedError,
35
36
  AgentChatTransport: () => AgentChatTransport,
36
37
  AgentClient: () => AgentClient,
@@ -213,12 +214,48 @@ var AgentClient = class {
213
214
  signal: request.signal
214
215
  } : {}
215
216
  });
217
+ if (response.status === 202) {
218
+ return {
219
+ body: emptyStream(),
220
+ queued: await response.json()
221
+ };
222
+ }
216
223
  if (!response.ok || !response.body) {
217
224
  throw await this.failure(response, "POST", `${this.agentPath()}/chat`);
218
225
  }
219
226
  return streamResponse(response);
220
227
  }
221
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
+ /**
222
259
  * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is
223
260
  * streaming under that id (404).
224
261
  */
@@ -484,6 +521,14 @@ var AgentClient = class {
484
521
  return JSON.parse(text);
485
522
  }
486
523
  };
524
+ function emptyStream() {
525
+ return new ReadableStream({
526
+ start(controller) {
527
+ controller.close();
528
+ }
529
+ });
530
+ }
531
+ __name(emptyStream, "emptyStream");
487
532
  function streamResponse(response) {
488
533
  const body = response.body;
489
534
  const runId = response.headers?.get(HEADER_RUN_ID) ?? void 0;
@@ -646,6 +691,10 @@ var AgentChatTransport = class {
646
691
  signal: options.abortSignal
647
692
  } : {}
648
693
  });
694
+ if (response.queued !== void 0) {
695
+ this.attemptLive = false;
696
+ return queuedChunkStream(response.queued);
697
+ }
649
698
  this.captureHeaderMeta(response);
650
699
  return this.toChunkStream(response, {
651
700
  ...headers !== void 0 ? {
@@ -676,6 +725,7 @@ var AgentChatTransport = class {
676
725
  });
677
726
  if (response === null) {
678
727
  this.attemptLive = false;
728
+ this.options.onResumeGone?.(runId);
679
729
  return null;
680
730
  }
681
731
  this.captureHeaderMeta(response);
@@ -1053,6 +1103,9 @@ var AgentChatTransport = class {
1053
1103
  break;
1054
1104
  case "title":
1055
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":
1056
1109
  forwardAsData(event, true);
1057
1110
  break;
1058
1111
  default:
@@ -1155,6 +1208,22 @@ var AgentChatTransport = class {
1155
1208
  });
1156
1209
  }
1157
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");
1158
1227
  function parseSseFrame(raw) {
1159
1228
  let event;
1160
1229
  let id;
@@ -3869,6 +3938,7 @@ function useChatTranscript(options) {
3869
3938
  } : {}
3870
3939
  });
3871
3940
  const showTypingIndicator = isBusy && lastMessage?.role !== "assistant";
3941
+ const queued = useQueuedItems(options.queue ?? null, options.readOnly === true);
3872
3942
  return {
3873
3943
  items,
3874
3944
  status,
@@ -3888,13 +3958,56 @@ function useChatTranscript(options) {
3888
3958
  },
3889
3959
  scroll,
3890
3960
  stop: {
3891
- available: isBusy && options.onStop !== void 0,
3961
+ available: isBusy && options.readOnly !== true && options.onStop !== void 0,
3892
3962
  isStopping: stopRequested,
3893
3963
  stop
3894
- }
3964
+ },
3965
+ queued,
3966
+ queuePaused: options.queue?.paused ?? null
3895
3967
  };
3896
3968
  }
3897
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");
3898
4011
  function useTranscriptItem(options) {
3899
4012
  const { message, isStreaming = false } = options;
3900
4013
  const messagesRef = (0, import_react17.useRef)([
@@ -4158,11 +4271,12 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
4158
4271
  ...options.toolCatalog !== void 0 ? {
4159
4272
  toolCatalog: options.toolCatalog
4160
4273
  } : {},
4161
- ...handlers.onAnswer !== void 0 ? {
4274
+ // Read-only still lifts question sets into their block, so the outcome shows.
4275
+ ...handlers.onAnswer !== void 0 || options.readOnly === true && options.onAnswer !== null ? {
4162
4276
  elicitation: {
4163
4277
  picked: /* @__PURE__ */ __name((toolCallId, questionId) => picks.get(pickKey(toolCallId, questionId)), "picked"),
4164
4278
  pick,
4165
- canAnswer: true,
4279
+ canAnswer: handlers.onAnswer !== void 0,
4166
4280
  canSkip: handlers.onSkip !== void 0,
4167
4281
  answer,
4168
4282
  skip,
@@ -4190,7 +4304,7 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
4190
4304
  copy: callbacks.copy
4191
4305
  },
4192
4306
  edit: {
4193
- available: isUser && options.editable === true && options.onEditSubmit !== void 0,
4307
+ available: isUser && options.readOnly !== true && options.editable === true && options.onEditSubmit !== void 0,
4194
4308
  isEditing,
4195
4309
  draft: draft ?? text,
4196
4310
  canSave: (draft ?? "").trim().length > 0,
@@ -4206,11 +4320,11 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
4206
4320
  }), "getTextareaProps")
4207
4321
  },
4208
4322
  fork: {
4209
- available: options.onFork !== void 0,
4323
+ available: options.readOnly !== true && options.onFork !== void 0,
4210
4324
  run: callbacks.fork
4211
4325
  },
4212
4326
  regenerate: {
4213
- available: isLastAssistant && options.regeneratable === true && options.onRegenerate !== void 0,
4327
+ available: isLastAssistant && options.readOnly !== true && options.regeneratable === true && options.onRegenerate !== void 0,
4214
4328
  run: callbacks.regenerate
4215
4329
  }
4216
4330
  });
@@ -4219,7 +4333,7 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
4219
4333
  }
4220
4334
  __name(useTranscriptItems, "useTranscriptItems");
4221
4335
  function settleHandlers(options, backend) {
4222
- const pick = /* @__PURE__ */ __name((own, fallback2) => own === null ? void 0 : own ?? fallback2, "pick");
4336
+ const pick = /* @__PURE__ */ __name((own, fallback2) => options.readOnly === true || own === null ? void 0 : own ?? fallback2, "pick");
4223
4337
  return {
4224
4338
  onAnswer: pick(options.onAnswer, backend.answerToolCall ? async (input) => {
4225
4339
  await backend.answerToolCall?.(input);
@@ -4512,10 +4626,13 @@ function joinClasses(...entries) {
4512
4626
  __name(joinClasses, "joinClasses");
4513
4627
 
4514
4628
  // src/components/message-list.tsx
4515
- 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 }) {
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 }) {
4516
4630
  const transcript = useChatTranscript({
4517
4631
  messages,
4518
4632
  status,
4633
+ ...readOnly !== void 0 ? {
4634
+ readOnly
4635
+ } : {},
4519
4636
  ...onFork ? {
4520
4637
  onFork
4521
4638
  } : {},
@@ -4539,9 +4656,16 @@ function MessageList({ messages, status, renderToolPart, renderToolGroup, render
4539
4656
  } : {},
4540
4657
  ...followUps !== void 0 ? {
4541
4658
  followUps
4659
+ } : {},
4660
+ ...queue !== void 0 ? {
4661
+ queue: {
4662
+ items: queue.items,
4663
+ paused: queue.paused,
4664
+ remove: queue.remove
4665
+ }
4542
4666
  } : {}
4543
4667
  });
4544
- if (transcript.showEmptyState) {
4668
+ if (transcript.showEmptyState && transcript.queued.length === 0) {
4545
4669
  return /* @__PURE__ */ React.createElement("div", {
4546
4670
  className: classNames?.empty
4547
4671
  }, emptyState);
@@ -4585,7 +4709,23 @@ function MessageList({ messages, status, renderToolPart, renderToolGroup, render
4585
4709
  ...getMeta ? {
4586
4710
  meta: getMeta(item.message)
4587
4711
  } : {}
4588
- })), 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", {
4589
4729
  className: classNames?.typing
4590
4730
  }, typingIndicator ?? "Thinking\u2026") : null, error && onRetry ? /* @__PURE__ */ React.createElement("div", {
4591
4731
  className: classNames?.error
@@ -4980,6 +5120,7 @@ function isPromise(value) {
4980
5120
  __name(isPromise, "isPromise");
4981
5121
 
4982
5122
  // src/presentation/use-tool-catalog.ts
5123
+ var import_nestjs_agent_core4 = require("@dudousxd/nestjs-agent-core");
4983
5124
  var import_react20 = require("react");
4984
5125
  var shared2 = /* @__PURE__ */ new WeakMap();
4985
5126
  function load2(client, agent) {
@@ -5099,7 +5240,7 @@ function useApprovalCountdown(expiresAt, options = {}) {
5099
5240
  __name(useApprovalCountdown, "useApprovalCountdown");
5100
5241
 
5101
5242
  // src/stored-message-to-ui-message.ts
5102
- var import_nestjs_agent_core4 = require("@dudousxd/nestjs-agent-core");
5243
+ var import_nestjs_agent_core5 = require("@dudousxd/nestjs-agent-core");
5103
5244
  function isRefusal(result) {
5104
5245
  if (result.denied === true) {
5105
5246
  return true;
@@ -5112,7 +5253,7 @@ function refusalReason(result) {
5112
5253
  const { output } = result;
5113
5254
  if (output !== null && typeof output === "object" && "reason" in output) {
5114
5255
  const reason = output.reason;
5115
- return typeof reason === "string" && reason !== import_nestjs_agent_core4.DEFAULT_REFUSAL_REASON ? reason : void 0;
5256
+ return typeof reason === "string" && reason !== import_nestjs_agent_core5.DEFAULT_REFUSAL_REASON ? reason : void 0;
5116
5257
  }
5117
5258
  return void 0;
5118
5259
  }
@@ -5353,6 +5494,77 @@ __name(sumUsage, "sumUsage");
5353
5494
  // src/use-agent-chat.ts
5354
5495
  var import_react22 = require("@ai-sdk/react");
5355
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");
5356
5568
  var localChatSequence = 0;
5357
5569
  function localChatId() {
5358
5570
  localChatSequence += 1;
@@ -5382,7 +5594,7 @@ function useAgentChat(options = {}) {
5382
5594
  const settlingRunIdRef = (0, import_react23.useRef)(void 0);
5383
5595
  const autoResumeRunIdRef = (0, import_react23.useRef)(void 0);
5384
5596
  const [activeRunId, setActiveRunId] = (0, import_react23.useState)(null);
5385
- const [isLoadingHistory, setIsLoadingHistory] = (0, import_react23.useState)(false);
5597
+ const [historyFor, setHistoryFor] = (0, import_react23.useState)(void 0);
5386
5598
  const [historyError, setHistoryError] = (0, import_react23.useState)(null);
5387
5599
  const [threadModel, setThreadModel] = (0, import_react23.useState)(null);
5388
5600
  const [pickedModel, setPickedModel] = (0, import_react23.useState)(void 0);
@@ -5392,6 +5604,41 @@ function useAgentChat(options = {}) {
5392
5604
  const pendingPin = (0, import_react23.useRef)(void 0);
5393
5605
  const regenerateNext = (0, import_react23.useRef)(false);
5394
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
+ ]);
5395
5642
  const [connection, setConnection] = (0, import_react23.useState)({
5396
5643
  status: "live"
5397
5644
  });
@@ -5400,6 +5647,11 @@ function useAgentChat(options = {}) {
5400
5647
  function onMeta(meta) {
5401
5648
  settlingRunIdRef.current = meta.runId;
5402
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
+ }
5403
5655
  if (latest.current.threadId === void 0 && meta.threadId && createdThreadId.current !== meta.threadId) {
5404
5656
  createdThreadId.current = meta.threadId;
5405
5657
  const pin = pendingPin.current;
@@ -5431,27 +5683,10 @@ function useAgentChat(options = {}) {
5431
5683
  agent: options.agent
5432
5684
  } : {},
5433
5685
  getBody: /* @__PURE__ */ __name(() => {
5434
- const current = latest.current;
5435
- const pageContext = current.getPageContext?.() ?? null;
5436
5686
  const regenerate2 = regenerateNext.current;
5437
5687
  regenerateNext.current = false;
5438
- const threadId = currentThreadId();
5439
- const model = current.model ?? (lockedRef.current ? void 0 : pickedModelRef.current);
5440
5688
  return {
5441
- // Read per send: a host that switches agents before the first message (a picker on a
5442
- // new chat) sends the one picked now, not the one this chat mounted with.
5443
- ...current.agent !== void 0 ? {
5444
- agent: current.agent
5445
- } : {},
5446
- ...threadId !== void 0 ? {
5447
- threadId
5448
- } : {},
5449
- ...model !== void 0 ? {
5450
- model
5451
- } : {},
5452
- ...pageContext ? {
5453
- pageContext
5454
- } : {},
5689
+ ...turnBody(),
5455
5690
  ...regenerate2 ? {
5456
5691
  regenerate: true
5457
5692
  } : {}
@@ -5461,7 +5696,11 @@ function useAgentChat(options = {}) {
5461
5696
  onMeta,
5462
5697
  onAttemptStart: /* @__PURE__ */ __name(() => {
5463
5698
  settlingRunIdRef.current = void 0;
5464
- }, "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")
5465
5704
  });
5466
5705
  }, []);
5467
5706
  const backgroundRef = (0, import_react23.useRef)(() => Promise.resolve());
@@ -5474,6 +5713,9 @@ function useAgentChat(options = {}) {
5474
5713
  } : {},
5475
5714
  onData: /* @__PURE__ */ __name((part) => {
5476
5715
  latest.current.onData?.(part);
5716
+ if (part.type === "data-queue") {
5717
+ onQueueData(part.data);
5718
+ }
5477
5719
  if (part.type === "data-title") {
5478
5720
  const title = part.data?.title;
5479
5721
  if (typeof title === "string") {
@@ -5505,6 +5747,20 @@ function useAgentChat(options = {}) {
5505
5747
  setConnection({
5506
5748
  status: "live"
5507
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
+ }
5508
5764
  if (resyncAfterSettle.current) {
5509
5765
  resyncAfterSettle.current = false;
5510
5766
  void resyncFromThread();
@@ -5513,6 +5769,46 @@ function useAgentChat(options = {}) {
5513
5769
  });
5514
5770
  const chatRef = (0, import_react23.useRef)(chat);
5515
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");
5516
5812
  async function resyncFromThread() {
5517
5813
  const threadId = currentThreadId();
5518
5814
  if (threadId === void 0) return;
@@ -5535,6 +5831,11 @@ function useAgentChat(options = {}) {
5535
5831
  setHistoryError(null);
5536
5832
  autoResumeRunIdRef.current = void 0;
5537
5833
  pendingPin.current = void 0;
5834
+ applyQueue(EMPTY_QUEUE);
5835
+ setPendingSends([]);
5836
+ setQueueError(null);
5837
+ handoff.current = void 0;
5838
+ queuedSend.current = void 0;
5538
5839
  }, [
5539
5840
  chatId
5540
5841
  ]);
@@ -5547,7 +5848,6 @@ function useAgentChat(options = {}) {
5547
5848
  const wantResume = current.resume !== false && current.resumeRunId === void 0;
5548
5849
  if (!wantHistory && !wantResume) return;
5549
5850
  let cancelled = false;
5550
- if (wantHistory) setIsLoadingHistory(true);
5551
5851
  client.getThread(threadId).then((thread) => {
5552
5852
  if (cancelled || thread == null) return;
5553
5853
  const active = thread.activeRunId ?? null;
@@ -5555,6 +5855,11 @@ function useAgentChat(options = {}) {
5555
5855
  setActiveRunId(active);
5556
5856
  setThreadModel(thread.model ?? null);
5557
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
+ }
5558
5863
  const resuming = wantResume && active !== null;
5559
5864
  if (wantHistory && chatRef.current.messages.length === 0) {
5560
5865
  const rows = resuming ? stored.filter((message) => !(message.role === "assistant" && message.runId === active)) : stored;
@@ -5568,17 +5873,17 @@ function useAgentChat(options = {}) {
5568
5873
  if (cancelled) return;
5569
5874
  setHistoryError(error instanceof Error ? error : new Error(String(error)));
5570
5875
  }).finally(() => {
5571
- if (!cancelled) setIsLoadingHistory(false);
5876
+ if (!cancelled) setHistoryFor(threadId);
5572
5877
  });
5573
5878
  return () => {
5574
5879
  cancelled = true;
5575
- setIsLoadingHistory(false);
5576
5880
  };
5577
5881
  }, [
5578
5882
  client,
5579
5883
  chatId,
5580
5884
  options.threadId
5581
5885
  ]);
5886
+ const isLoadingHistory = options.threadId !== void 0 && options.threadId !== createdThreadId.current && options.history !== false && options.initialMessages === void 0 && historyFor !== options.threadId;
5582
5887
  const turnInFlight = (0, import_react23.useRef)(false);
5583
5888
  const isTurnInFlight = (0, import_react23.useCallback)(() => turnInFlight.current || transport.isAttemptLive, [
5584
5889
  transport
@@ -5590,10 +5895,98 @@ function useAgentChat(options = {}) {
5590
5895
  const blocked = options.blocked !== void 0 ? options.blocked : quota.blocked;
5591
5896
  const blockedRef = (0, import_react23.useRef)(blocked);
5592
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
+ ]);
5593
5977
  const sendMessage = (0, import_react23.useCallback)(async (...args) => {
5594
5978
  const block = blockedRef.current;
5595
5979
  if (block != null) throw new QuotaBlockedError(block);
5596
- 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
+ }
5597
5990
  turnInFlight.current = true;
5598
5991
  try {
5599
5992
  await chatRef.current.sendMessage(...args);
@@ -5601,8 +5994,122 @@ function useAgentChat(options = {}) {
5601
5994
  turnInFlight.current = false;
5602
5995
  }
5603
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,
5604
6085
  isTurnInFlight
5605
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
+ };
5606
6113
  const addToolResult = (0, import_react23.useCallback)(({ tool, toolCallId, output }) => chatRef.current.addToolResult({
5607
6114
  tool,
5608
6115
  toolCallId,
@@ -5843,12 +6350,14 @@ function useAgentChat(options = {}) {
5843
6350
  };
5844
6351
  const status = connection.status === "reconnecting" ? "reconnecting" : chat.status;
5845
6352
  const isBusy = status === "submitted" || status === "streaming" || status === "reconnecting";
6353
+ const isBusyRef = (0, import_react23.useRef)(isBusy);
6354
+ isBusyRef.current = isBusy;
5846
6355
  const files = useAttachments({
5847
6356
  ...options.composer,
5848
6357
  backend: client
5849
6358
  });
5850
6359
  const [text, setText] = (0, import_react23.useState)("");
5851
- 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;
5852
6361
  const composerRef = (0, import_react23.useRef)({
5853
6362
  text,
5854
6363
  files,
@@ -5879,6 +6388,17 @@ function useAgentChat(options = {}) {
5879
6388
  const draft = current.text.trim();
5880
6389
  setText("");
5881
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
+ }
5882
6402
  await sendMessage(files2.length > 0 ? {
5883
6403
  text: draft,
5884
6404
  files: files2
@@ -5890,7 +6410,9 @@ function useAgentChat(options = {}) {
5890
6410
  }
5891
6411
  } : void 0);
5892
6412
  }, [
5893
- sendMessage
6413
+ sendMessage,
6414
+ enqueue,
6415
+ isTurnInFlight
5894
6416
  ]);
5895
6417
  const composer = {
5896
6418
  text,
@@ -5925,6 +6447,11 @@ function useAgentChat(options = {}) {
5925
6447
  ...ownCatalog === void 0 ? {
5926
6448
  toolCatalog: tools.catalog
5927
6449
  } : {},
6450
+ queue: {
6451
+ items: queue.items,
6452
+ paused: queue.paused,
6453
+ remove: removeQueued
6454
+ },
5928
6455
  ...options.transcript
5929
6456
  });
5930
6457
  const background = {
@@ -5959,6 +6486,11 @@ function useAgentChat(options = {}) {
5959
6486
  transcript,
5960
6487
  /** A headless composer: draft, files, send gate, `submit()`. */
5961
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,
5962
6494
  /** The model picker: `list`, `selected`, `select(id)`, `pinToThread(id)`. */
5963
6495
  models,
5964
6496
  /** The caller's budget (`GET <base>/quota`); `quota.blocked` gates sends. */
@@ -5989,6 +6521,7 @@ var QuotaBlockedError = class extends Error {
5989
6521
  };
5990
6522
  // Annotate the CommonJS export names for ESM import in node:
5991
6523
  0 && (module.exports = {
6524
+ ALL_AGENTS,
5992
6525
  AgentBackendUnsupportedError,
5993
6526
  AgentChatTransport,
5994
6527
  AgentClient,