@dudousxd/nestjs-agent-react 0.14.0 → 0.16.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
@@ -1,6 +1,255 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
+ // src/client.ts
5
+ var AgentHttpError = class extends Error {
6
+ static {
7
+ __name(this, "AgentHttpError");
8
+ }
9
+ status;
10
+ method;
11
+ path;
12
+ constructor(status, method, path, statusText) {
13
+ super(`Agent request failed: ${method} ${path} \u2192 ${status} ${statusText}`), this.status = status, this.method = method, this.path = path;
14
+ this.name = "AgentHttpError";
15
+ }
16
+ };
17
+ var HEADER_RUN_ID = "x-agent-run-id";
18
+ var HEADER_THREAD_ID = "x-agent-thread-id";
19
+ var AgentClient = class {
20
+ static {
21
+ __name(this, "AgentClient");
22
+ }
23
+ options;
24
+ constructor(options = {}) {
25
+ this.options = options;
26
+ }
27
+ /** `POST /agent/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */
28
+ async openChatStream(request) {
29
+ const response = await this.fetchImpl()(`${this.baseUrl()}/agent/chat`, {
30
+ method: "POST",
31
+ headers: {
32
+ "content-type": "application/json",
33
+ accept: "text/event-stream",
34
+ ...await this.resolveHeaders(),
35
+ ...request.headers
36
+ },
37
+ body: JSON.stringify(request.body),
38
+ ...this.credentials(),
39
+ ...request.signal !== void 0 ? {
40
+ signal: request.signal
41
+ } : {}
42
+ });
43
+ if (!response.ok || !response.body) {
44
+ throw new AgentHttpError(response.status, "POST", "/agent/chat", response.statusText);
45
+ }
46
+ return streamResponse(response);
47
+ }
48
+ /**
49
+ * `GET /agent/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is
50
+ * streaming under that id (404).
51
+ */
52
+ async resumeChatStream(request) {
53
+ const path = `/agent/chat/${encodeURIComponent(request.runId)}/stream`;
54
+ const query = request.after !== void 0 && request.after > 0 ? `?after=${request.after}` : "";
55
+ const response = await this.fetchImpl()(`${this.baseUrl()}${path}${query}`, {
56
+ method: "GET",
57
+ headers: {
58
+ accept: "text/event-stream",
59
+ ...await this.resolveHeaders(),
60
+ ...request.headers
61
+ },
62
+ ...this.credentials(),
63
+ ...request.signal !== void 0 ? {
64
+ signal: request.signal
65
+ } : {}
66
+ });
67
+ if (response.status === 404) {
68
+ return null;
69
+ }
70
+ if (!response.ok || !response.body) {
71
+ throw new AgentHttpError(response.status, "GET", path, response.statusText);
72
+ }
73
+ return streamResponse(response);
74
+ }
75
+ /**
76
+ * Rate a message (`'up'`/`'down'`, optional comment) or clear its rating (`value: null`). Answers
77
+ * the stored rating.
78
+ */
79
+ setMessageFeedback(messageId, input) {
80
+ return this.request("POST", `/agent/messages/${encodeURIComponent(messageId)}/feedback`, input);
81
+ }
82
+ listThreads() {
83
+ return this.request("GET", "/agent/threads");
84
+ }
85
+ /**
86
+ * The skills this caller can invoke right now, scope-resolved — the same list, built by the same
87
+ * call, that the model is offered, so what a user can type after a `/` and what the agent can
88
+ * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to
89
+ * one conversation; omitted, the server reads it as a brand-new thread.
90
+ */
91
+ listSkills(threadId) {
92
+ const query = threadId === void 0 ? "" : `?threadId=${encodeURIComponent(threadId)}`;
93
+ return this.request("GET", `/agent/skills${query}`);
94
+ }
95
+ /**
96
+ * The tools this caller can reach through `agent` (the default agent when omitted), each with the
97
+ * server-declared `presentation` a chat narrates it by — the same list the model is offered.
98
+ * Prefer {@link useToolCatalog}, which fetches it once and shares it.
99
+ */
100
+ listTools(agent) {
101
+ const query = agent === void 0 ? "" : `?agent=${encodeURIComponent(agent)}`;
102
+ return this.request("GET", `/agent/tools${query}`);
103
+ }
104
+ getThread(id) {
105
+ return this.request("GET", `/agent/threads/${encodeURIComponent(id)}`);
106
+ }
107
+ deleteThread(id) {
108
+ return this.request("DELETE", `/agent/threads/${encodeURIComponent(id)}`);
109
+ }
110
+ forkFromMessage(threadId, messageId) {
111
+ return this.request("POST", `/agent/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`);
112
+ }
113
+ renameThread(id, title) {
114
+ return this.updateThread(id, {
115
+ title
116
+ });
117
+ }
118
+ /** General `PATCH /agent/threads/:threadId` — title and/or the thread's pinned default agent. */
119
+ updateThread(id, patch) {
120
+ return this.request("PATCH", `/agent/threads/${encodeURIComponent(id)}`, patch);
121
+ }
122
+ /**
123
+ * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —
124
+ * mirrors the backend's `POST /agent/attachments`. The returned {@link MessageAttachment} is
125
+ * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.
126
+ */
127
+ async uploadAttachment(file, options = {}) {
128
+ const formData = new FormData();
129
+ formData.append("file", file);
130
+ const response = await this.fetchImpl()(`${this.baseUrl()}/agent/attachments`, {
131
+ method: "POST",
132
+ headers: {
133
+ accept: "application/json",
134
+ ...await this.resolveHeaders()
135
+ },
136
+ body: formData,
137
+ ...this.credentials(),
138
+ ...options.signal !== void 0 ? {
139
+ signal: options.signal
140
+ } : {}
141
+ });
142
+ return this.handleResponse(response, "POST", "/agent/attachments");
143
+ }
144
+ promoteThread(id) {
145
+ return this.request("POST", `/agent/threads/${encodeURIComponent(id)}/promote`);
146
+ }
147
+ truncateFromMessage(threadId, messageId) {
148
+ return this.request("DELETE", `/agent/threads/${encodeURIComponent(threadId)}/from/${encodeURIComponent(messageId)}`);
149
+ }
150
+ /** `GET /agent/models?agent=` — the models this caller may pick, grouped by provider. */
151
+ listModels(agent) {
152
+ const query = agent === void 0 ? "" : `?agent=${encodeURIComponent(agent)}`;
153
+ return this.request("GET", `/agent/models${query}`);
154
+ }
155
+ /** `GET /agent/agents` — the registered agents, the default one flagged. */
156
+ listAgents() {
157
+ return this.request("GET", "/agent/agents");
158
+ }
159
+ getQuotaToday() {
160
+ return this.request("GET", "/agent/quota/today");
161
+ }
162
+ cancelStream(runId) {
163
+ return this.request("POST", `/agent/chat/${encodeURIComponent(runId)}/cancel`);
164
+ }
165
+ /**
166
+ * `remember` approves later calls of the same tool in the same thread; `via` names the surface
167
+ * the decision came through (the server records `'web'` when omitted).
168
+ */
169
+ approveToolCall(input) {
170
+ return this.request("POST", "/agent/tool-call/approve", input);
171
+ }
172
+ rejectToolCall(input) {
173
+ return this.request("POST", "/agent/tool-call/reject", input);
174
+ }
175
+ /**
176
+ * Settle a parked question set. `answers` is questionId → chosen option values; a question left
177
+ * out takes the pre-picked default the request carried, resolved server-side against the request
178
+ * the run already holds. Omit the whole object and the user has confirmed every pre-picked
179
+ * answer — which is the point of the surface, so it is a valid submission rather than a blank.
180
+ */
181
+ answerToolCall(input) {
182
+ return this.request("POST", "/agent/tool-call/answer", input);
183
+ }
184
+ /**
185
+ * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same
186
+ * values a confirmation would, and persists differently on purpose — only one of them is
187
+ * evidence the user chose them.
188
+ */
189
+ skipToolCall(input) {
190
+ return this.request("POST", "/agent/tool-call/skip", input);
191
+ }
192
+ fetchImpl() {
193
+ return this.options.fetch ?? globalThis.fetch;
194
+ }
195
+ baseUrl() {
196
+ return (this.options.baseUrl ?? "").replace(/\/$/, "");
197
+ }
198
+ async resolveHeaders() {
199
+ const dynamic = await this.options.getHeaders?.() ?? {};
200
+ return {
201
+ ...this.options.headers,
202
+ ...dynamic
203
+ };
204
+ }
205
+ credentials() {
206
+ return this.options.credentials !== void 0 ? {
207
+ credentials: this.options.credentials
208
+ } : {};
209
+ }
210
+ async request(method, path, body) {
211
+ const response = await this.fetchImpl()(`${this.baseUrl()}${path}`, {
212
+ method,
213
+ headers: {
214
+ accept: "application/json",
215
+ ...body !== void 0 ? {
216
+ "content-type": "application/json"
217
+ } : {},
218
+ ...await this.resolveHeaders()
219
+ },
220
+ ...body !== void 0 ? {
221
+ body: JSON.stringify(body)
222
+ } : {},
223
+ ...this.credentials()
224
+ });
225
+ return this.handleResponse(response, method, path);
226
+ }
227
+ async handleResponse(response, method, path) {
228
+ if (!response.ok) {
229
+ throw new AgentHttpError(response.status, method, path, response.statusText);
230
+ }
231
+ if (response.status === 204) return void 0;
232
+ const text = await response.text();
233
+ if (!text) return void 0;
234
+ return JSON.parse(text);
235
+ }
236
+ };
237
+ function streamResponse(response) {
238
+ const body = response.body;
239
+ const runId = response.headers?.get(HEADER_RUN_ID) ?? void 0;
240
+ const threadId = response.headers?.get(HEADER_THREAD_ID) ?? void 0;
241
+ return {
242
+ body,
243
+ ...runId ? {
244
+ runId
245
+ } : {},
246
+ ...threadId ? {
247
+ threadId
248
+ } : {}
249
+ };
250
+ }
251
+ __name(streamResponse, "streamResponse");
252
+
4
253
  // src/reasoning/timing.ts
5
254
  import { useEffect, useRef, useState } from "react";
6
255
  var REASONING_METADATA_KEY = "agent";
@@ -61,8 +310,11 @@ function formatElapsed(ms) {
61
310
  __name(formatElapsed, "formatElapsed");
62
311
 
63
312
  // src/agent-chat-transport.ts
64
- var HEADER_RUN_ID = "x-agent-run-id";
65
- var HEADER_THREAD_ID = "x-agent-thread-id";
313
+ var DEFAULT_RECONNECT = {
314
+ maxAttempts: 6,
315
+ baseDelayMs: 500,
316
+ maxDelayMs: 8e3
317
+ };
66
318
  var ASK_TOOL_NAME = "ask";
67
319
  var AgentChatTransport = class {
68
320
  static {
@@ -79,8 +331,26 @@ var AgentChatTransport = class {
79
331
  * several entries under each id — which React renders as duplicate keys.
80
332
  */
81
333
  attemptLive = false;
334
+ backend;
82
335
  constructor(options = {}) {
83
336
  this.options = options;
337
+ this.backend = options.backend ?? new AgentClient({
338
+ ...options.baseUrl !== void 0 ? {
339
+ baseUrl: options.baseUrl
340
+ } : {},
341
+ ...options.headers !== void 0 ? {
342
+ headers: options.headers
343
+ } : {},
344
+ ...options.getHeaders !== void 0 ? {
345
+ getHeaders: options.getHeaders
346
+ } : {},
347
+ ...options.credentials !== void 0 ? {
348
+ credentials: options.credentials
349
+ } : {},
350
+ ...options.fetch !== void 0 ? {
351
+ fetch: options.fetch
352
+ } : {}
353
+ });
84
354
  }
85
355
  /** Run id of the most recent stream — HITL approve/reject target this. */
86
356
  get runId() {
@@ -113,26 +383,25 @@ var AgentChatTransport = class {
113
383
  ...options.body ?? {},
114
384
  message
115
385
  };
116
- const response = await this.fetchImpl()(`${this.baseUrl()}/agent/chat`, {
117
- method: "POST",
118
- headers: {
119
- "content-type": "application/json",
120
- accept: "text/event-stream",
121
- ...await this.resolveHeaders(options.headers)
122
- },
123
- body: JSON.stringify(body),
124
- ...this.options.credentials !== void 0 ? {
125
- credentials: this.options.credentials
386
+ const headers = headerRecord(options.headers);
387
+ const response = await this.backend.openChatStream({
388
+ body,
389
+ ...headers !== void 0 ? {
390
+ headers
391
+ } : {},
392
+ ...options.abortSignal ? {
393
+ signal: options.abortSignal
394
+ } : {}
395
+ });
396
+ this.captureHeaderMeta(response);
397
+ return this.toChunkStream(response, {
398
+ ...headers !== void 0 ? {
399
+ headers
126
400
  } : {},
127
401
  ...options.abortSignal ? {
128
402
  signal: options.abortSignal
129
403
  } : {}
130
404
  });
131
- if (!response.ok || !response.body) {
132
- throw new Error(`Agent chat request failed: ${response.status} ${response.statusText}`);
133
- }
134
- this.captureHeaderMeta(response.headers);
135
- return this.toChunkStream(response.body);
136
405
  } catch (error) {
137
406
  this.attemptLive = false;
138
407
  throw error;
@@ -145,51 +414,41 @@ var AgentChatTransport = class {
145
414
  if (runId === void 0) return null;
146
415
  this.attemptLive = true;
147
416
  try {
148
- const response = await this.fetchImpl()(`${this.baseUrl()}/agent/chat/${encodeURIComponent(runId)}/stream`, {
149
- method: "GET",
150
- headers: {
151
- accept: "text/event-stream",
152
- ...await this.resolveHeaders(options.headers)
153
- },
154
- ...this.options.credentials !== void 0 ? {
155
- credentials: this.options.credentials
417
+ const headers = headerRecord(options.headers);
418
+ const response = await this.backend.resumeChatStream({
419
+ runId,
420
+ ...headers !== void 0 ? {
421
+ headers
156
422
  } : {}
157
423
  });
158
- if (response.status === 404) {
424
+ if (response === null) {
159
425
  this.attemptLive = false;
160
426
  return null;
161
427
  }
162
- if (!response.ok || !response.body) {
163
- throw new Error(`Agent stream reconnect failed: ${response.status} ${response.statusText}`);
164
- }
165
- this.captureHeaderMeta(response.headers);
166
- return this.toChunkStream(response.body);
428
+ this.captureHeaderMeta(response);
429
+ return this.toChunkStream(response, {
430
+ runId,
431
+ ...headers !== void 0 ? {
432
+ headers
433
+ } : {}
434
+ });
167
435
  } catch (error) {
168
436
  this.attemptLive = false;
169
437
  throw error;
170
438
  }
171
439
  }
172
- fetchImpl() {
173
- return this.options.fetch ?? globalThis.fetch;
174
- }
175
- baseUrl() {
176
- return (this.options.baseUrl ?? "").replace(/\/$/, "");
440
+ captureHeaderMeta(response) {
441
+ if (response.runId) this.currentRunId = response.runId;
442
+ if (response.threadId) this.currentThreadId = response.threadId;
177
443
  }
178
- async resolveHeaders(perRequest) {
179
- const dynamic = await this.options.getHeaders?.() ?? {};
180
- const extra = perRequest instanceof Headers ? Object.fromEntries(perRequest.entries()) : perRequest ?? {};
444
+ reconnectPolicy() {
445
+ const configured = this.options.reconnect;
446
+ if (configured === false) return null;
181
447
  return {
182
- ...this.options.headers,
183
- ...dynamic,
184
- ...extra
448
+ ...DEFAULT_RECONNECT,
449
+ ...configured
185
450
  };
186
451
  }
187
- captureHeaderMeta(headers) {
188
- const runId = headers.get(HEADER_RUN_ID);
189
- const threadId = headers.get(HEADER_THREAD_ID);
190
- if (runId) this.currentRunId = runId;
191
- if (threadId) this.currentThreadId = threadId;
192
- }
193
452
  recordMeta(meta) {
194
453
  this.currentRunId = meta.runId;
195
454
  this.currentThreadId = meta.threadId;
@@ -208,10 +467,66 @@ var AgentChatTransport = class {
208
467
  * call plus its tool execution; text and reasoning open lazily and close at the step boundary, so
209
468
  * tool-call cards (input streaming → output) render live between the prose.
210
469
  */
211
- toChunkStream(source) {
212
- const reader = source.getReader();
213
- const decoder = new TextDecoder();
470
+ toChunkStream(source, context) {
471
+ let reader = source.body.getReader();
472
+ let decoder = new TextDecoder();
214
473
  let buffer = "";
474
+ let streamRunId = source.runId ?? context.runId;
475
+ let lastSeq;
476
+ let cancelled = false;
477
+ const backend = this.backend;
478
+ const policy = this.reconnectPolicy();
479
+ const notify = /* @__PURE__ */ __name((state) => this.options.onConnectionChange?.(state), "notify");
480
+ const aborted = /* @__PURE__ */ __name(() => cancelled || context.signal?.aborted === true, "aborted");
481
+ const canResume = /* @__PURE__ */ __name(() => policy !== null && lastSeq !== void 0 && streamRunId !== void 0 && !aborted(), "canResume");
482
+ const resume = /* @__PURE__ */ __name(async () => {
483
+ const runId = streamRunId;
484
+ const after = lastSeq ?? 0;
485
+ const { maxAttempts, baseDelayMs, maxDelayMs } = policy;
486
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
487
+ notify({
488
+ status: "reconnecting",
489
+ runId,
490
+ attempt,
491
+ after
492
+ });
493
+ await delay(Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1)), context.signal);
494
+ if (aborted()) return "aborted";
495
+ try {
496
+ const next = await backend.resumeChatStream({
497
+ runId,
498
+ after,
499
+ ...context.headers !== void 0 ? {
500
+ headers: context.headers
501
+ } : {},
502
+ ...context.signal !== void 0 ? {
503
+ signal: context.signal
504
+ } : {}
505
+ });
506
+ if (next === null) {
507
+ notify({
508
+ status: "gone",
509
+ runId
510
+ });
511
+ return "gone";
512
+ }
513
+ reader = next.body.getReader();
514
+ decoder = new TextDecoder();
515
+ buffer = "";
516
+ notify({
517
+ status: "live"
518
+ });
519
+ return "resumed";
520
+ } catch {
521
+ if (aborted()) return "aborted";
522
+ }
523
+ }
524
+ notify({
525
+ status: "failed",
526
+ runId
527
+ });
528
+ return "failed";
529
+ }, "resume");
215
530
  let started = false;
216
531
  let stepOpen = false;
217
532
  let stepIndex = 0;
@@ -222,7 +537,10 @@ var AgentChatTransport = class {
222
537
  let reasoningSeq = 0;
223
538
  const announced = /* @__PURE__ */ new Set();
224
539
  const toolMetadata = /* @__PURE__ */ new Map();
225
- const record = /* @__PURE__ */ __name((meta) => this.recordMeta(meta), "record");
540
+ const record = /* @__PURE__ */ __name((meta) => {
541
+ streamRunId = meta.runId;
542
+ this.recordMeta(meta);
543
+ }, "record");
226
544
  const endAttempt = /* @__PURE__ */ __name(() => {
227
545
  this.attemptLive = false;
228
546
  }, "endAttempt");
@@ -231,7 +549,12 @@ var AgentChatTransport = class {
231
549
  function ensureStarted() {
232
550
  if (started) return;
233
551
  started = true;
234
- controller.enqueue({
552
+ controller.enqueue(streamRunId !== void 0 ? {
553
+ type: "start",
554
+ messageMetadata: {
555
+ runId: streamRunId
556
+ }
557
+ } : {
235
558
  type: "start"
236
559
  });
237
560
  }
@@ -487,33 +810,67 @@ var AgentChatTransport = class {
487
810
  endAttempt();
488
811
  }
489
812
  __name(finish, "finish");
813
+ function fail(errorText, start = true) {
814
+ if (start) ensureStarted();
815
+ controller.enqueue({
816
+ type: "error",
817
+ errorText
818
+ });
819
+ controller.close();
820
+ endAttempt();
821
+ }
822
+ __name(fail, "fail");
823
+ async function recover(error) {
824
+ if (!canResume()) {
825
+ if (error === void 0) {
826
+ finish();
827
+ } else {
828
+ fail(error instanceof Error ? error.message : "Agent stream error", false);
829
+ }
830
+ return false;
831
+ }
832
+ const outcome = await resume();
833
+ if (outcome === "resumed") return true;
834
+ if (outcome === "failed") {
835
+ fail("Lost the connection to the agent stream");
836
+ } else {
837
+ finish();
838
+ }
839
+ return false;
840
+ }
841
+ __name(recover, "recover");
490
842
  try {
491
843
  while (true) {
492
- const { done, value } = await reader.read();
493
- if (done) {
494
- finish();
844
+ let result;
845
+ try {
846
+ result = await reader.read();
847
+ } catch (error) {
848
+ if (await recover(error)) continue;
849
+ return;
850
+ }
851
+ if (result.done) {
852
+ if (await recover(void 0)) continue;
495
853
  return;
496
854
  }
497
- buffer += decoder.decode(value, {
855
+ buffer += decoder.decode(result.value, {
498
856
  stream: true
499
857
  });
500
858
  let separator = buffer.indexOf("\n\n");
501
859
  while (separator !== -1) {
502
860
  const rawEvent = buffer.slice(0, separator);
503
861
  buffer = buffer.slice(separator + 2);
862
+ separator = buffer.indexOf("\n\n");
504
863
  const frame = parseSseFrame(rawEvent);
864
+ if (frame.id !== void 0) {
865
+ if (lastSeq !== void 0 && frame.id <= lastSeq) continue;
866
+ lastSeq = frame.id;
867
+ }
505
868
  if (frame.event === "done") {
506
869
  finish();
507
870
  return;
508
871
  }
509
872
  if (frame.event === "error") {
510
- ensureStarted();
511
- controller.enqueue({
512
- type: "error",
513
- errorText: parseErrorText(frame.data)
514
- });
515
- controller.close();
516
- endAttempt();
873
+ fail(parseErrorText(frame.data));
517
874
  return;
518
875
  }
519
876
  if (frame.event === "meta") {
@@ -523,19 +880,14 @@ var AgentChatTransport = class {
523
880
  const event = parseEvent(frame.data);
524
881
  if (event) emit(event);
525
882
  }
526
- separator = buffer.indexOf("\n\n");
527
883
  }
528
884
  }
529
885
  } catch (error) {
530
- controller.enqueue({
531
- type: "error",
532
- errorText: error instanceof Error ? error.message : "Agent stream error"
533
- });
534
- controller.close();
535
- endAttempt();
886
+ fail(error instanceof Error ? error.message : "Agent stream error", false);
536
887
  }
537
888
  },
538
889
  cancel() {
890
+ cancelled = true;
539
891
  void reader.cancel();
540
892
  endAttempt();
541
893
  }
@@ -544,17 +896,22 @@ var AgentChatTransport = class {
544
896
  };
545
897
  function parseSseFrame(raw) {
546
898
  let event;
899
+ let id;
547
900
  const dataLines = [];
548
901
  for (const line of raw.split("\n")) {
549
902
  if (line.startsWith("event:")) {
550
903
  event = line.slice("event:".length).trim();
904
+ } else if (line.startsWith("id:")) {
905
+ const parsed = Number(line.slice("id:".length).trim());
906
+ if (Number.isSafeInteger(parsed) && parsed > 0) id = parsed;
551
907
  } else if (line.startsWith("data:")) {
552
908
  dataLines.push(line.slice("data:".length).trimStart());
553
909
  }
554
910
  }
555
911
  return {
556
912
  event,
557
- data: dataLines.length > 0 ? dataLines.join("\n") : void 0
913
+ data: dataLines.length > 0 ? dataLines.join("\n") : void 0,
914
+ id
558
915
  };
559
916
  }
560
917
  __name(parseSseFrame, "parseSseFrame");
@@ -595,15 +952,382 @@ function parseEvent(data) {
595
952
  }
596
953
  return null;
597
954
  }
598
- __name(parseEvent, "parseEvent");
599
- function extractText(message) {
600
- let text = "";
601
- for (const part of message.parts ?? []) {
602
- if (part.type === "text") text += part.text;
955
+ __name(parseEvent, "parseEvent");
956
+ function extractText(message) {
957
+ let text = "";
958
+ for (const part of message.parts ?? []) {
959
+ if (part.type === "text") text += part.text;
960
+ }
961
+ return text;
962
+ }
963
+ __name(extractText, "extractText");
964
+ function headerRecord(headers) {
965
+ if (headers === void 0) return void 0;
966
+ return headers instanceof Headers ? Object.fromEntries(headers.entries()) : headers;
967
+ }
968
+ __name(headerRecord, "headerRecord");
969
+ function delay(ms, signal) {
970
+ if (ms <= 0 || signal?.aborted) return Promise.resolve();
971
+ return new Promise((resolve) => {
972
+ const timer = setTimeout(done, ms);
973
+ function done() {
974
+ clearTimeout(timer);
975
+ signal?.removeEventListener("abort", done);
976
+ resolve();
977
+ }
978
+ __name(done, "done");
979
+ signal?.addEventListener("abort", done, {
980
+ once: true
981
+ });
982
+ });
983
+ }
984
+ __name(delay, "delay");
985
+
986
+ // src/backend.ts
987
+ var AgentBackendUnsupportedError = class extends Error {
988
+ static {
989
+ __name(this, "AgentBackendUnsupportedError");
990
+ }
991
+ method;
992
+ constructor(method) {
993
+ super(`The agent backend does not implement ${method}()`), this.method = method;
994
+ this.name = "AgentBackendUnsupportedError";
995
+ }
996
+ };
997
+ function requireBackendMethod(backend, name) {
998
+ const method = backend[name];
999
+ if (typeof method !== "function") {
1000
+ throw new AgentBackendUnsupportedError(name);
1001
+ }
1002
+ return method.bind(backend);
1003
+ }
1004
+ __name(requireBackendMethod, "requireBackendMethod");
1005
+
1006
+ // src/catalog/use-agents.ts
1007
+ import { useMemo } from "react";
1008
+
1009
+ // src/catalog/use-resource.ts
1010
+ import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
1011
+ function useResource(load2, key, enabled) {
1012
+ const [data, setData] = useState2(void 0);
1013
+ const [isLoading, setIsLoading] = useState2(enabled);
1014
+ const [error, setError] = useState2(null);
1015
+ const loadRef = useRef2(load2);
1016
+ loadRef.current = load2;
1017
+ const generation = useRef2(0);
1018
+ const refresh = useCallback(async () => {
1019
+ const mine = ++generation.current;
1020
+ setIsLoading(true);
1021
+ try {
1022
+ const value = await loadRef.current();
1023
+ if (mine !== generation.current) return;
1024
+ setData(value);
1025
+ setError(null);
1026
+ } catch (caught) {
1027
+ if (mine !== generation.current) return;
1028
+ setError(caught instanceof Error ? caught : new Error(String(caught)));
1029
+ } finally {
1030
+ if (mine === generation.current) setIsLoading(false);
1031
+ }
1032
+ }, []);
1033
+ useEffect2(() => {
1034
+ if (!enabled) return;
1035
+ void refresh();
1036
+ }, [
1037
+ enabled,
1038
+ key,
1039
+ refresh
1040
+ ]);
1041
+ return {
1042
+ data,
1043
+ isLoading,
1044
+ error,
1045
+ refresh
1046
+ };
1047
+ }
1048
+ __name(useResource, "useResource");
1049
+
1050
+ // src/catalog/use-agents.ts
1051
+ function useAgents(options) {
1052
+ const { backend, enabled = true } = options;
1053
+ const { data, isLoading, error, refresh } = useResource(() => requireBackendMethod(backend, "listAgents")(), "", enabled);
1054
+ return useMemo(() => {
1055
+ const agents = data ?? [];
1056
+ return {
1057
+ agents,
1058
+ defaultAgent: agents.find((agent) => agent.isDefault === true)?.name ?? null,
1059
+ isLoading,
1060
+ error,
1061
+ refresh
1062
+ };
1063
+ }, [
1064
+ data,
1065
+ isLoading,
1066
+ error,
1067
+ refresh
1068
+ ]);
1069
+ }
1070
+ __name(useAgents, "useAgents");
1071
+
1072
+ // src/catalog/use-models.ts
1073
+ import { useMemo as useMemo2 } from "react";
1074
+ function useModels(options) {
1075
+ const { backend, agent, enabled = true } = options;
1076
+ const { data, isLoading, error, refresh } = useResource(() => requireBackendMethod(backend, "listModels")(agent), `${agent ?? ""}`, enabled);
1077
+ return useMemo2(() => {
1078
+ const providers = data?.providers ?? [];
1079
+ const models = providers.flatMap((provider) => provider.models.map((model) => ({
1080
+ ...model,
1081
+ providerId: provider.id,
1082
+ providerLabel: provider.label
1083
+ })));
1084
+ const byId = new Map(models.map((model) => [
1085
+ model.id,
1086
+ model
1087
+ ]));
1088
+ return {
1089
+ providers,
1090
+ models,
1091
+ defaultModel: data?.default ?? null,
1092
+ find: /* @__PURE__ */ __name((id) => id == null ? void 0 : byId.get(id), "find"),
1093
+ isLoading,
1094
+ error,
1095
+ refresh
1096
+ };
1097
+ }, [
1098
+ data,
1099
+ isLoading,
1100
+ error,
1101
+ refresh
1102
+ ]);
1103
+ }
1104
+ __name(useModels, "useModels");
1105
+
1106
+ // src/feedback/use-message-feedback.ts
1107
+ import { useCallback as useCallback2, useRef as useRef3, useState as useState3 } from "react";
1108
+ function useMessageFeedback(options) {
1109
+ const latest = useRef3(options);
1110
+ latest.current = options;
1111
+ const [local, setLocal] = useState3(/* @__PURE__ */ new Map());
1112
+ const [pending, setPending] = useState3(/* @__PURE__ */ new Set());
1113
+ const [error, setError] = useState3(null);
1114
+ const localRef = useRef3(local);
1115
+ localRef.current = local;
1116
+ const resolved = useRef3(/* @__PURE__ */ new Map());
1117
+ const feedbackOf = useCallback2((message) => {
1118
+ const mine = local.get(message.id);
1119
+ if (mine !== void 0) return mine.feedback;
1120
+ return metadataOf(message).feedback ?? null;
1121
+ }, [
1122
+ local
1123
+ ]);
1124
+ const isPending = useCallback2((message) => pending.has(message.id), [
1125
+ pending
1126
+ ]);
1127
+ const storedIdFor = useCallback2(async (message) => {
1128
+ const runId = metadataOf(message).runId;
1129
+ if (runId === void 0) return message.id;
1130
+ const known = resolved.current.get(message.id);
1131
+ if (known !== void 0) return known;
1132
+ const { backend, threadId } = latest.current;
1133
+ const thread = typeof threadId === "function" ? threadId() : threadId;
1134
+ if (thread === void 0) {
1135
+ throw new Error("useMessageFeedback: a streamed message needs `threadId` to be rated");
1136
+ }
1137
+ const detail = await backend.getThread(thread);
1138
+ const row = [
1139
+ ...detail.messages
1140
+ ].reverse().find((candidate) => candidate.role === "assistant" && candidate.runId === runId);
1141
+ if (row === void 0) {
1142
+ throw new Error(`useMessageFeedback: run ${runId} has no persisted answer yet`);
1143
+ }
1144
+ resolved.current.set(message.id, row.id);
1145
+ return row.id;
1146
+ }, []);
1147
+ const rate = useCallback2(async (message, value, comment) => {
1148
+ const setFeedback = requireBackendMethod(latest.current.backend, "setMessageFeedback");
1149
+ const had = localRef.current.get(message.id);
1150
+ const optimistic = value === null ? null : {
1151
+ value,
1152
+ ...comment !== void 0 && comment.trim().length > 0 ? {
1153
+ comment: comment.trim()
1154
+ } : {},
1155
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1156
+ };
1157
+ setLocal((current) => new Map(current).set(message.id, {
1158
+ feedback: optimistic
1159
+ }));
1160
+ setPending((current) => new Set(current).add(message.id));
1161
+ try {
1162
+ const storedId = await storedIdFor(message);
1163
+ const { feedback } = await setFeedback(storedId, {
1164
+ value,
1165
+ ...comment !== void 0 ? {
1166
+ comment
1167
+ } : {}
1168
+ });
1169
+ setLocal((current) => new Map(current).set(message.id, {
1170
+ feedback
1171
+ }));
1172
+ setError(null);
1173
+ } catch (caught) {
1174
+ setLocal((current) => {
1175
+ const next = new Map(current);
1176
+ if (had === void 0) next.delete(message.id);
1177
+ else next.set(message.id, had);
1178
+ return next;
1179
+ });
1180
+ const failure = caught instanceof Error ? caught : new Error(String(caught));
1181
+ setError(failure);
1182
+ throw failure;
1183
+ } finally {
1184
+ setPending((current) => {
1185
+ const next = new Set(current);
1186
+ next.delete(message.id);
1187
+ return next;
1188
+ });
1189
+ }
1190
+ }, [
1191
+ storedIdFor
1192
+ ]);
1193
+ const toggle = useCallback2((message, value) => {
1194
+ const mine = localRef.current.get(message.id);
1195
+ const current = mine !== void 0 ? mine.feedback : metadataOf(message).feedback ?? null;
1196
+ return rate(message, current?.value === value ? null : value);
1197
+ }, [
1198
+ rate
1199
+ ]);
1200
+ return {
1201
+ feedbackOf,
1202
+ isPending,
1203
+ rate,
1204
+ toggle,
1205
+ error
1206
+ };
1207
+ }
1208
+ __name(useMessageFeedback, "useMessageFeedback");
1209
+ function metadataOf(message) {
1210
+ const metadata = message.metadata;
1211
+ return metadata !== null && typeof metadata === "object" ? metadata : {};
1212
+ }
1213
+ __name(metadataOf, "metadataOf");
1214
+
1215
+ // src/threads/threads-events.ts
1216
+ var listeners = /* @__PURE__ */ new WeakMap();
1217
+ function notifyThreads(backend, event) {
1218
+ for (const listener of listeners.get(backend) ?? []) {
1219
+ listener(event);
603
1220
  }
604
- return text;
605
1221
  }
606
- __name(extractText, "extractText");
1222
+ __name(notifyThreads, "notifyThreads");
1223
+ function onThreadsEvent(backend, listener) {
1224
+ let set = listeners.get(backend);
1225
+ if (set === void 0) {
1226
+ set = /* @__PURE__ */ new Set();
1227
+ listeners.set(backend, set);
1228
+ }
1229
+ set.add(listener);
1230
+ return () => {
1231
+ set.delete(listener);
1232
+ };
1233
+ }
1234
+ __name(onThreadsEvent, "onThreadsEvent");
1235
+
1236
+ // src/threads/use-threads.ts
1237
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef4, useState as useState4 } from "react";
1238
+ function useThreads(options) {
1239
+ const { backend, enabled = true } = options;
1240
+ const [threads, setThreads] = useState4([]);
1241
+ const [isLoading, setIsLoading] = useState4(enabled);
1242
+ const [error, setError] = useState4(null);
1243
+ const threadsRef = useRef4(threads);
1244
+ threadsRef.current = threads;
1245
+ const generation = useRef4(0);
1246
+ const refresh = useCallback3(async () => {
1247
+ const mine = ++generation.current;
1248
+ setIsLoading(true);
1249
+ try {
1250
+ const list = await backend.listThreads();
1251
+ if (mine !== generation.current) return;
1252
+ setThreads(list);
1253
+ setError(null);
1254
+ } catch (caught) {
1255
+ if (mine !== generation.current) return;
1256
+ setError(caught instanceof Error ? caught : new Error(String(caught)));
1257
+ } finally {
1258
+ if (mine === generation.current) setIsLoading(false);
1259
+ }
1260
+ }, [
1261
+ backend
1262
+ ]);
1263
+ useEffect3(() => {
1264
+ if (!enabled) return;
1265
+ void refresh();
1266
+ return onThreadsEvent(backend, (event) => {
1267
+ if (event.type === "title") {
1268
+ setThreads((current) => current.map((thread) => thread.id === event.threadId ? {
1269
+ ...thread,
1270
+ title: event.title
1271
+ } : thread));
1272
+ } else if (event.type === "removed") {
1273
+ setThreads((current) => current.filter((thread) => thread.id !== event.threadId));
1274
+ } else {
1275
+ void refresh();
1276
+ }
1277
+ });
1278
+ }, [
1279
+ backend,
1280
+ enabled,
1281
+ refresh
1282
+ ]);
1283
+ const rename = useCallback3(async (id, title) => {
1284
+ const before = threadsRef.current;
1285
+ setThreads((current) => current.map((thread) => thread.id === id ? {
1286
+ ...thread,
1287
+ title
1288
+ } : thread));
1289
+ try {
1290
+ await backend.updateThread(id, {
1291
+ title
1292
+ });
1293
+ notifyThreads(backend, {
1294
+ type: "title",
1295
+ threadId: id,
1296
+ title
1297
+ });
1298
+ } catch (caught) {
1299
+ setThreads(before);
1300
+ throw caught;
1301
+ }
1302
+ }, [
1303
+ backend
1304
+ ]);
1305
+ const remove = useCallback3(async (id) => {
1306
+ const before = threadsRef.current;
1307
+ setThreads((current) => current.filter((thread) => thread.id !== id));
1308
+ try {
1309
+ await backend.deleteThread(id);
1310
+ notifyThreads(backend, {
1311
+ type: "removed",
1312
+ threadId: id
1313
+ });
1314
+ } catch (caught) {
1315
+ setThreads(before);
1316
+ throw caught;
1317
+ }
1318
+ }, [
1319
+ backend
1320
+ ]);
1321
+ return {
1322
+ threads,
1323
+ isLoading,
1324
+ error,
1325
+ refresh,
1326
+ rename,
1327
+ remove
1328
+ };
1329
+ }
1330
+ __name(useThreads, "useThreads");
607
1331
 
608
1332
  // src/background-runs.ts
609
1333
  function asReceipt(output) {
@@ -652,170 +1376,11 @@ function backgroundRunsFromThread(messages) {
652
1376
  }
653
1377
  __name(backgroundRunsFromThread, "backgroundRunsFromThread");
654
1378
 
655
- // src/client.ts
656
- var AgentHttpError = class extends Error {
657
- static {
658
- __name(this, "AgentHttpError");
659
- }
660
- status;
661
- method;
662
- path;
663
- constructor(status, method, path, statusText) {
664
- super(`Agent request failed: ${method} ${path} \u2192 ${status} ${statusText}`), this.status = status, this.method = method, this.path = path;
665
- this.name = "AgentHttpError";
666
- }
667
- };
668
- var AgentClient = class {
669
- static {
670
- __name(this, "AgentClient");
671
- }
672
- options;
673
- constructor(options = {}) {
674
- this.options = options;
675
- }
676
- listThreads() {
677
- return this.request("GET", "/agent/threads");
678
- }
679
- /**
680
- * The skills this caller can invoke right now, scope-resolved — the same list, built by the same
681
- * call, that the model is offered, so what a user can type after a `/` and what the agent can
682
- * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to
683
- * one conversation; omitted, the server reads it as a brand-new thread.
684
- */
685
- listSkills(threadId) {
686
- const query = threadId === void 0 ? "" : `?threadId=${encodeURIComponent(threadId)}`;
687
- return this.request("GET", `/agent/skills${query}`);
688
- }
689
- /**
690
- * The tools this caller can reach through `agent` (the default agent when omitted), each with the
691
- * server-declared `presentation` a chat narrates it by — the same list the model is offered.
692
- * Prefer {@link useToolCatalog}, which fetches it once and shares it.
693
- */
694
- listTools(agent) {
695
- const query = agent === void 0 ? "" : `?agent=${encodeURIComponent(agent)}`;
696
- return this.request("GET", `/agent/tools${query}`);
697
- }
698
- getThread(id) {
699
- return this.request("GET", `/agent/threads/${encodeURIComponent(id)}`);
700
- }
701
- deleteThread(id) {
702
- return this.request("DELETE", `/agent/threads/${encodeURIComponent(id)}`);
703
- }
704
- forkFromMessage(threadId, messageId) {
705
- return this.request("POST", `/agent/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`);
706
- }
707
- renameThread(id, title) {
708
- return this.updateThread(id, {
709
- title
710
- });
711
- }
712
- /** General `PATCH /agent/threads/:threadId` — title and/or the thread's pinned default agent. */
713
- updateThread(id, patch) {
714
- return this.request("PATCH", `/agent/threads/${encodeURIComponent(id)}`, patch);
715
- }
716
- /**
717
- * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —
718
- * mirrors the backend's `POST /agent/attachments`. The returned {@link MessageAttachment} is
719
- * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.
720
- */
721
- async uploadAttachment(file) {
722
- const fetchImpl = this.options.fetch ?? globalThis.fetch;
723
- const baseUrl = (this.options.baseUrl ?? "").replace(/\/$/, "");
724
- const dynamic = await this.options.getHeaders?.() ?? {};
725
- const formData = new FormData();
726
- formData.append("file", file);
727
- const response = await fetchImpl(`${baseUrl}/agent/attachments`, {
728
- method: "POST",
729
- headers: {
730
- accept: "application/json",
731
- ...this.options.headers,
732
- ...dynamic
733
- },
734
- body: formData,
735
- ...this.options.credentials !== void 0 ? {
736
- credentials: this.options.credentials
737
- } : {}
738
- });
739
- return this.handleResponse(response, "POST", "/agent/attachments");
740
- }
741
- promoteThread(id) {
742
- return this.request("POST", `/agent/threads/${encodeURIComponent(id)}/promote`);
743
- }
744
- truncateFromMessage(threadId, messageId) {
745
- return this.request("DELETE", `/agent/threads/${encodeURIComponent(threadId)}/from/${encodeURIComponent(messageId)}`);
746
- }
747
- getQuotaToday() {
748
- return this.request("GET", "/agent/quota/today");
749
- }
750
- cancelStream(runId) {
751
- return this.request("POST", `/agent/chat/${encodeURIComponent(runId)}/cancel`);
752
- }
753
- /**
754
- * `remember` approves later calls of the same tool in the same thread; `via` names the surface
755
- * the decision came through (the server records `'web'` when omitted).
756
- */
757
- approveToolCall(input) {
758
- return this.request("POST", "/agent/tool-call/approve", input);
759
- }
760
- rejectToolCall(input) {
761
- return this.request("POST", "/agent/tool-call/reject", input);
762
- }
763
- /**
764
- * Settle a parked question set. `answers` is questionId → chosen option values; a question left
765
- * out takes the pre-picked default the request carried, resolved server-side against the request
766
- * the run already holds. Omit the whole object and the user has confirmed every pre-picked
767
- * answer — which is the point of the surface, so it is a valid submission rather than a blank.
768
- */
769
- answerToolCall(input) {
770
- return this.request("POST", "/agent/tool-call/answer", input);
771
- }
772
- /**
773
- * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same
774
- * values a confirmation would, and persists differently on purpose — only one of them is
775
- * evidence the user chose them.
776
- */
777
- skipToolCall(input) {
778
- return this.request("POST", "/agent/tool-call/skip", input);
779
- }
780
- async request(method, path, body) {
781
- const fetchImpl = this.options.fetch ?? globalThis.fetch;
782
- const baseUrl = (this.options.baseUrl ?? "").replace(/\/$/, "");
783
- const dynamic = await this.options.getHeaders?.() ?? {};
784
- const response = await fetchImpl(`${baseUrl}${path}`, {
785
- method,
786
- headers: {
787
- accept: "application/json",
788
- ...body !== void 0 ? {
789
- "content-type": "application/json"
790
- } : {},
791
- ...this.options.headers,
792
- ...dynamic
793
- },
794
- ...body !== void 0 ? {
795
- body: JSON.stringify(body)
796
- } : {},
797
- ...this.options.credentials !== void 0 ? {
798
- credentials: this.options.credentials
799
- } : {}
800
- });
801
- return this.handleResponse(response, method, path);
802
- }
803
- async handleResponse(response, method, path) {
804
- if (!response.ok) {
805
- throw new AgentHttpError(response.status, method, path, response.statusText);
806
- }
807
- if (response.status === 204) return void 0;
808
- const text = await response.text();
809
- if (!text) return void 0;
810
- return JSON.parse(text);
811
- }
812
- };
813
-
814
1379
  // src/components/chat-input.tsx
815
- import React2, { useEffect as useEffect3, useState as useState3 } from "react";
1380
+ import React2, { useEffect as useEffect5, useState as useState6 } from "react";
816
1381
 
817
1382
  // src/components/use-speech-recognition.ts
818
- import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
1383
+ import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef5, useState as useState5 } from "react";
819
1384
  function getRecognitionCtor() {
820
1385
  if (typeof window === "undefined") return null;
821
1386
  const scope = window;
@@ -823,15 +1388,15 @@ function getRecognitionCtor() {
823
1388
  }
824
1389
  __name(getRecognitionCtor, "getRecognitionCtor");
825
1390
  function useSpeechRecognition({ lang = "en-US", onFinalTranscript, onInterimTranscript }) {
826
- const recognitionRef = useRef2(null);
827
- const [isListening, setIsListening] = useState2(false);
828
- const ctor = useMemo(() => getRecognitionCtor(), []);
1391
+ const recognitionRef = useRef5(null);
1392
+ const [isListening, setIsListening] = useState5(false);
1393
+ const ctor = useMemo3(() => getRecognitionCtor(), []);
829
1394
  const isSupported = ctor !== null;
830
- const finalRef = useRef2(onFinalTranscript);
831
- const interimRef = useRef2(onInterimTranscript);
1395
+ const finalRef = useRef5(onFinalTranscript);
1396
+ const interimRef = useRef5(onInterimTranscript);
832
1397
  finalRef.current = onFinalTranscript;
833
1398
  interimRef.current = onInterimTranscript;
834
- useEffect2(() => {
1399
+ useEffect4(() => {
835
1400
  if (!ctor) return;
836
1401
  const recognition = new ctor();
837
1402
  recognition.lang = lang;
@@ -870,7 +1435,7 @@ function useSpeechRecognition({ lang = "en-US", onFinalTranscript, onInterimTran
870
1435
  ctor,
871
1436
  lang
872
1437
  ]);
873
- const start = useCallback(() => {
1438
+ const start = useCallback4(() => {
874
1439
  const recognition = recognitionRef.current;
875
1440
  if (!recognition) return;
876
1441
  try {
@@ -880,7 +1445,7 @@ function useSpeechRecognition({ lang = "en-US", onFinalTranscript, onInterimTran
880
1445
  console.warn("SpeechRecognition start failed", error);
881
1446
  }
882
1447
  }, []);
883
- const stop = useCallback(() => {
1448
+ const stop = useCallback4(() => {
884
1449
  const recognition = recognitionRef.current;
885
1450
  if (!recognition) return;
886
1451
  try {
@@ -899,8 +1464,8 @@ __name(useSpeechRecognition, "useSpeechRecognition");
899
1464
 
900
1465
  // src/components/chat-input.tsx
901
1466
  function ChatInput({ onSubmit, disabled, placeholder, rows = 2, enableSpeech = true, speechLang = "en-US", sendLabel = "Send", onStop, isStreaming = false, stopLabel = "Stop generating", className, textareaClassName, sendButtonClassName, micButtonClassName, stopButtonClassName, renderMic }) {
902
- const [value, setValue] = useState3("");
903
- const [interim, setInterim] = useState3("");
1467
+ const [value, setValue] = useState6("");
1468
+ const [interim, setInterim] = useState6("");
904
1469
  const speech = useSpeechRecognition({
905
1470
  lang: speechLang,
906
1471
  onFinalTranscript: /* @__PURE__ */ __name((text) => {
@@ -911,7 +1476,7 @@ function ChatInput({ onSubmit, disabled, placeholder, rows = 2, enableSpeech = t
911
1476
  }, "onFinalTranscript"),
912
1477
  onInterimTranscript: /* @__PURE__ */ __name((text) => setInterim(text), "onInterimTranscript")
913
1478
  });
914
- useEffect3(() => {
1479
+ useEffect5(() => {
915
1480
  if (!speech.isListening) setInterim("");
916
1481
  }, [
917
1482
  speech.isListening
@@ -978,7 +1543,7 @@ __name(ChatInput, "ChatInput");
978
1543
  import React3 from "react";
979
1544
 
980
1545
  // src/transcript/use-chat-transcript.ts
981
- import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef4, useState as useState5 } from "react";
1546
+ import { useCallback as useCallback6, useEffect as useEffect7, useRef as useRef7, useState as useState8 } from "react";
982
1547
 
983
1548
  // src/transcript/model.ts
984
1549
  import { readElicitationInput, validateElicitationAnswer as validateElicitationAnswer2 } from "@dudousxd/nestjs-agent-core";
@@ -1955,11 +2520,11 @@ function formatCostUsd(cost) {
1955
2520
  __name(formatCostUsd, "formatCostUsd");
1956
2521
 
1957
2522
  // src/transcript/use-stick-to-bottom.ts
1958
- import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
2523
+ import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef6, useState as useState7 } from "react";
1959
2524
  function useStickToBottom({ contentKey, threshold = 32 }) {
1960
- const containerRef = useRef3(null);
1961
- const [isAtBottom, setIsAtBottom] = useState4(true);
1962
- const measure = useCallback2(() => {
2525
+ const containerRef = useRef6(null);
2526
+ const [isAtBottom, setIsAtBottom] = useState7(true);
2527
+ const measure = useCallback5(() => {
1963
2528
  const element = containerRef.current;
1964
2529
  if (!element) {
1965
2530
  return;
@@ -1969,7 +2534,7 @@ function useStickToBottom({ contentKey, threshold = 32 }) {
1969
2534
  }, [
1970
2535
  threshold
1971
2536
  ]);
1972
- const scrollToBottom = useCallback2(() => {
2537
+ const scrollToBottom = useCallback5(() => {
1973
2538
  const element = containerRef.current;
1974
2539
  if (!element) {
1975
2540
  return;
@@ -1977,7 +2542,7 @@ function useStickToBottom({ contentKey, threshold = 32 }) {
1977
2542
  element.scrollTop = element.scrollHeight;
1978
2543
  setIsAtBottom(true);
1979
2544
  }, []);
1980
- const setContainer = useCallback2((element) => {
2545
+ const setContainer = useCallback5((element) => {
1981
2546
  containerRef.current = element;
1982
2547
  if (element) {
1983
2548
  measure();
@@ -1985,7 +2550,7 @@ function useStickToBottom({ contentKey, threshold = 32 }) {
1985
2550
  }, [
1986
2551
  measure
1987
2552
  ]);
1988
- useEffect4(() => {
2553
+ useEffect6(() => {
1989
2554
  if (isAtBottom) {
1990
2555
  scrollToBottom();
1991
2556
  }
@@ -1994,7 +2559,7 @@ function useStickToBottom({ contentKey, threshold = 32 }) {
1994
2559
  isAtBottom,
1995
2560
  scrollToBottom
1996
2561
  ]);
1997
- const getContainerProps = useCallback2(() => ({
2562
+ const getContainerProps = useCallback5(() => ({
1998
2563
  ref: setContainer,
1999
2564
  onScroll: measure
2000
2565
  }), [
@@ -2016,14 +2581,14 @@ var DEFAULT_LOAD_EARLIER_STEP = 50;
2016
2581
  var DEFAULT_COPY_RESET_MS = 1500;
2017
2582
  function useChatTranscript(options) {
2018
2583
  const { messages, status, initialVisibleCount = DEFAULT_VISIBLE_COUNT, loadEarlierStep = DEFAULT_LOAD_EARLIER_STEP, followUps, stickToBottomThreshold } = options;
2019
- const latest = useRef4(options);
2584
+ const latest = useRef7(options);
2020
2585
  latest.current = options;
2021
- const [visibleCount, setVisibleCount] = useState5(initialVisibleCount);
2022
- const [stopRequested, setStopRequested] = useState5(false);
2023
- const isStreaming = status === "streaming";
2586
+ const [visibleCount, setVisibleCount] = useState8(initialVisibleCount);
2587
+ const [stopRequested, setStopRequested] = useState8(false);
2588
+ const isStreaming = status === "streaming" || status === "reconnecting";
2024
2589
  const isBusy = status === "submitted" || isStreaming;
2025
2590
  const isEmpty = messages.length === 0;
2026
- useEffect5(() => {
2591
+ useEffect7(() => {
2027
2592
  if (!isBusy) {
2028
2593
  setStopRequested(false);
2029
2594
  }
@@ -2040,16 +2605,16 @@ function useChatTranscript(options) {
2040
2605
  streamingMessageId,
2041
2606
  options
2042
2607
  });
2043
- const stop = useCallback3(() => {
2608
+ const stop = useCallback6(() => {
2044
2609
  const current = latest.current;
2045
- if (current.status !== "submitted" && current.status !== "streaming") {
2610
+ if (current.status !== "submitted" && current.status !== "streaming" && current.status !== "reconnecting") {
2046
2611
  return;
2047
2612
  }
2048
2613
  setStopRequested(true);
2049
2614
  void current.onStop?.();
2050
2615
  }, []);
2051
- const loadEarlier = useCallback3(() => setVisibleCount((count) => count + (latest.current.loadEarlierStep ?? DEFAULT_LOAD_EARLIER_STEP)), []);
2052
- const showAll = useCallback3(() => setVisibleCount(latest.current.messages.length), []);
2616
+ const loadEarlier = useCallback6(() => setVisibleCount((count) => count + (latest.current.loadEarlierStep ?? DEFAULT_LOAD_EARLIER_STEP)), []);
2617
+ const showAll = useCallback6(() => setVisibleCount(latest.current.messages.length), []);
2053
2618
  const scroll = useStickToBottom({
2054
2619
  contentKey: contentSignature(messages),
2055
2620
  ...stickToBottomThreshold !== void 0 ? {
@@ -2085,7 +2650,7 @@ function useChatTranscript(options) {
2085
2650
  __name(useChatTranscript, "useChatTranscript");
2086
2651
  function useTranscriptItem(options) {
2087
2652
  const { message, isStreaming = false } = options;
2088
- const messagesRef = useRef4([
2653
+ const messagesRef = useRef7([
2089
2654
  message
2090
2655
  ]);
2091
2656
  if (messagesRef.current[0] !== message) {
@@ -2103,13 +2668,13 @@ function useTranscriptItem(options) {
2103
2668
  }
2104
2669
  __name(useTranscriptItem, "useTranscriptItem");
2105
2670
  function useTranscriptItems({ messages, visibleStart, streamingMessageId, options }) {
2106
- const [drafts, setDrafts] = useState5(() => /* @__PURE__ */ new Map());
2107
- const [copied, setCopied] = useState5(() => /* @__PURE__ */ new Set());
2108
- const [openReasoning, setOpenReasoning] = useState5(() => /* @__PURE__ */ new Map());
2109
- const [picks, setPicks] = useState5(() => /* @__PURE__ */ new Map());
2110
- const [settling, setSettling] = useState5(() => /* @__PURE__ */ new Map());
2111
- const [settleErrors, setSettleErrors] = useState5(() => /* @__PURE__ */ new Map());
2112
- const latest = useRef4({
2671
+ const [drafts, setDrafts] = useState8(() => /* @__PURE__ */ new Map());
2672
+ const [copied, setCopied] = useState8(() => /* @__PURE__ */ new Set());
2673
+ const [openReasoning, setOpenReasoning] = useState8(() => /* @__PURE__ */ new Map());
2674
+ const [picks, setPicks] = useState8(() => /* @__PURE__ */ new Map());
2675
+ const [settling, setSettling] = useState8(() => /* @__PURE__ */ new Map());
2676
+ const [settleErrors, setSettleErrors] = useState8(() => /* @__PURE__ */ new Map());
2677
+ const latest = useRef7({
2113
2678
  options,
2114
2679
  drafts,
2115
2680
  picks,
@@ -2121,23 +2686,23 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
2121
2686
  picks,
2122
2687
  messages
2123
2688
  };
2124
- const copyTimers = useRef4(/* @__PURE__ */ new Map());
2125
- useEffect5(() => () => {
2689
+ const copyTimers = useRef7(/* @__PURE__ */ new Map());
2690
+ useEffect7(() => () => {
2126
2691
  for (const timer of copyTimers.current.values()) {
2127
2692
  clearTimeout(timer);
2128
2693
  }
2129
2694
  copyTimers.current.clear();
2130
2695
  }, []);
2131
- const callbackCache = useRef4(/* @__PURE__ */ new Map());
2132
- const reasoningToggleCache = useRef4(/* @__PURE__ */ new Map());
2133
- const toggleReasoning = useCallback3((key, open) => {
2696
+ const callbackCache = useRef7(/* @__PURE__ */ new Map());
2697
+ const reasoningToggleCache = useRef7(/* @__PURE__ */ new Map());
2698
+ const toggleReasoning = useCallback6((key, open) => {
2134
2699
  setOpenReasoning((current) => {
2135
2700
  const next = new Map(current);
2136
2701
  next.set(key, open ?? !(current.get(key) ?? false));
2137
2702
  return next;
2138
2703
  });
2139
2704
  }, []);
2140
- const stableToggle = useCallback3((key) => {
2705
+ const stableToggle = useCallback6((key) => {
2141
2706
  let cached = reasoningToggleCache.current.get(key);
2142
2707
  if (!cached) {
2143
2708
  cached = /* @__PURE__ */ __name((open) => toggleReasoning(key, open), "cached");
@@ -2147,10 +2712,10 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
2147
2712
  }, [
2148
2713
  toggleReasoning
2149
2714
  ]);
2150
- const pick = useCallback3((toolCallId, questionId, values) => {
2715
+ const pick = useCallback6((toolCallId, questionId, values) => {
2151
2716
  setPicks((current) => new Map(current).set(pickKey(toolCallId, questionId), values));
2152
2717
  }, []);
2153
- const settle = useCallback3((toolCallId, action, send) => {
2718
+ const settle = useCallback6((toolCallId, action, send) => {
2154
2719
  setSettleErrors((current) => {
2155
2720
  if (!current.has(toolCallId)) {
2156
2721
  return current;
@@ -2174,7 +2739,7 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
2174
2739
  failed(error);
2175
2740
  }
2176
2741
  }, []);
2177
- const answer = useCallback3((toolCallId) => {
2742
+ const answer = useCallback6((toolCallId) => {
2178
2743
  const current = latest.current;
2179
2744
  const onAnswer = current.options.onAnswer;
2180
2745
  if (!onAnswer) {
@@ -2184,7 +2749,7 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
2184
2749
  }, [
2185
2750
  settle
2186
2751
  ]);
2187
- const skip = useCallback3((toolCallId) => {
2752
+ const skip = useCallback6((toolCallId) => {
2188
2753
  const onSkip = latest.current.options.onSkip;
2189
2754
  if (onSkip) {
2190
2755
  settle(toolCallId, "skip", () => onSkip(toolCallId));
@@ -2192,7 +2757,7 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
2192
2757
  }, [
2193
2758
  settle
2194
2759
  ]);
2195
- const approve = useCallback3((toolCallId, options2) => {
2760
+ const approve = useCallback6((toolCallId, options2) => {
2196
2761
  const onApprove = latest.current.options.onApprove;
2197
2762
  if (onApprove) {
2198
2763
  settle(toolCallId, "approve", () => options2 === void 0 ? onApprove(toolCallId) : onApprove(toolCallId, options2));
@@ -2200,7 +2765,7 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
2200
2765
  }, [
2201
2766
  settle
2202
2767
  ]);
2203
- const reject = useCallback3((toolCallId) => {
2768
+ const reject = useCallback6((toolCallId) => {
2204
2769
  const onReject = latest.current.options.onReject;
2205
2770
  if (onReject) {
2206
2771
  settle(toolCallId, "reject", () => onReject(toolCallId));
@@ -2208,7 +2773,7 @@ function useTranscriptItems({ messages, visibleStart, streamingMessageId, option
2208
2773
  }, [
2209
2774
  settle
2210
2775
  ]);
2211
- const callbacksFor = useCallback3((id) => {
2776
+ const callbacksFor = useCallback6((id) => {
2212
2777
  const cached = callbackCache.current.get(id);
2213
2778
  if (cached) {
2214
2779
  return cached;
@@ -2831,7 +3396,11 @@ function createSkillsSource(options) {
2831
3396
  if (cached) {
2832
3397
  return cached;
2833
3398
  }
2834
- const pending = options.client.listSkills(threadId).then((entries) => entries.map(toItem)).catch((error) => {
3399
+ const listSkills = options.client.listSkills;
3400
+ if (listSkills === void 0) {
3401
+ return Promise.reject(new AgentBackendUnsupportedError("listSkills"));
3402
+ }
3403
+ const pending = listSkills.call(options.client, threadId).then((entries) => entries.map(toItem)).catch((error) => {
2835
3404
  byThread.delete(key);
2836
3405
  throw error;
2837
3406
  });
@@ -2861,35 +3430,35 @@ function toItem(entry) {
2861
3430
  __name(toItem, "toItem");
2862
3431
 
2863
3432
  // src/composer/use-composer-autocomplete.ts
2864
- import { useCallback as useCallback4, useEffect as useEffect6, useId, useLayoutEffect, useMemo as useMemo2, useRef as useRef5, useState as useState6 } from "react";
3433
+ import { useCallback as useCallback7, useEffect as useEffect8, useId, useLayoutEffect, useMemo as useMemo4, useRef as useRef8, useState as useState9 } from "react";
2865
3434
  function useComposerAutocomplete(options) {
2866
3435
  const { value, sources, acceptOnTab = true } = options;
2867
- const latest = useRef5(options);
3436
+ const latest = useRef8(options);
2868
3437
  latest.current = options;
2869
- const inputRef = useRef5(null);
2870
- const [caret, setCaret] = useState6(value.length);
2871
- const [items, setItems] = useState6([]);
2872
- const [isLoading, setLoading] = useState6(false);
2873
- const [error, setError] = useState6(null);
2874
- const [activeIndex, setActiveIndex] = useState6(0);
2875
- const [dismissedIndex, setDismissedIndex] = useState6(null);
3438
+ const inputRef = useRef8(null);
3439
+ const [caret, setCaret] = useState9(value.length);
3440
+ const [items, setItems] = useState9([]);
3441
+ const [isLoading, setLoading] = useState9(false);
3442
+ const [error, setError] = useState9(null);
3443
+ const [activeIndex, setActiveIndex] = useState9(0);
3444
+ const [dismissedIndex, setDismissedIndex] = useState9(null);
2876
3445
  const safeCaret = Math.min(caret, value.length);
2877
- const match = useMemo2(() => findActiveTrigger(value, safeCaret, sources), [
3446
+ const match = useMemo4(() => findActiveTrigger(value, safeCaret, sources), [
2878
3447
  value,
2879
3448
  safeCaret,
2880
3449
  sources
2881
3450
  ]);
2882
3451
  const isActive = match !== null && dismissedIndex !== match.index;
2883
- const matchRef = useRef5(match);
3452
+ const matchRef = useRef8(match);
2884
3453
  matchRef.current = match;
2885
- const caretRef = useRef5(safeCaret);
3454
+ const caretRef = useRef8(safeCaret);
2886
3455
  caretRef.current = safeCaret;
2887
- const itemsRef = useRef5(items);
3456
+ const itemsRef = useRef8(items);
2888
3457
  itemsRef.current = items;
2889
- const activeIndexRef = useRef5(activeIndex);
3458
+ const activeIndexRef = useRef8(activeIndex);
2890
3459
  activeIndexRef.current = activeIndex;
2891
3460
  const requestKey = isActive && match ? `${match.source.id}\0${match.query}` : null;
2892
- useEffect6(() => {
3461
+ useEffect8(() => {
2893
3462
  const current = matchRef.current;
2894
3463
  if (requestKey === null || current === null) {
2895
3464
  setItems([]);
@@ -2939,23 +3508,23 @@ function useComposerAutocomplete(options) {
2939
3508
  const activeItem = isOpen ? items[activeIndex] ?? null : null;
2940
3509
  const reactId = useId();
2941
3510
  const listboxId = options.id ?? `agent-autocomplete-${reactId}`;
2942
- const optionId = useCallback4((index) => `${listboxId}-option-${index}`, [
3511
+ const optionId = useCallback7((index) => `${listboxId}-option-${index}`, [
2943
3512
  listboxId
2944
3513
  ]);
2945
- const highlight = useCallback4((index) => {
3514
+ const highlight = useCallback7((index) => {
2946
3515
  const total = itemsRef.current.length;
2947
3516
  if (total === 0) return;
2948
3517
  setActiveIndex((index % total + total) % total);
2949
3518
  }, []);
2950
- const moveHighlight = useCallback4((delta) => highlight(activeIndexRef.current + delta), [
3519
+ const moveHighlight = useCallback7((delta) => highlight(activeIndexRef.current + delta), [
2951
3520
  highlight
2952
3521
  ]);
2953
- const dismiss = useCallback4(() => {
3522
+ const dismiss = useCallback7(() => {
2954
3523
  const current = matchRef.current;
2955
3524
  if (current) setDismissedIndex(current.index);
2956
3525
  }, []);
2957
- const pendingCaret = useRef5(null);
2958
- const accept = useCallback4((item) => {
3526
+ const pendingCaret = useRef8(null);
3527
+ const accept = useCallback7((item) => {
2959
3528
  const current = matchRef.current;
2960
3529
  if (!current) return;
2961
3530
  const chosen = item ?? itemsRef.current[activeIndexRef.current];
@@ -2972,10 +3541,10 @@ function useComposerAutocomplete(options) {
2972
3541
  pendingCaret.current = null;
2973
3542
  inputRef.current?.setSelectionRange(position, position);
2974
3543
  });
2975
- const readCaret = useCallback4((element) => {
3544
+ const readCaret = useCallback7((element) => {
2976
3545
  setCaret(element.selectionStart ?? element.value.length);
2977
3546
  }, []);
2978
- const onKeyDown = useCallback4((event) => {
3547
+ const onKeyDown = useCallback7((event) => {
2979
3548
  if (!isOpen) return;
2980
3549
  const hasItems = itemsRef.current.length > 0;
2981
3550
  switch (event.key) {
@@ -3013,23 +3582,23 @@ function useComposerAutocomplete(options) {
3013
3582
  dismiss,
3014
3583
  moveHighlight
3015
3584
  ]);
3016
- const onChange = useCallback4((event) => {
3585
+ const onChange = useCallback7((event) => {
3017
3586
  readCaret(event.target);
3018
3587
  const next = findActiveTrigger(event.target.value, event.target.selectionStart ?? event.target.value.length, latest.current.sources);
3019
3588
  setDismissedIndex((current) => current !== null && next?.index === current ? current : null);
3020
3589
  }, [
3021
3590
  readCaret
3022
3591
  ]);
3023
- const onSelect = useCallback4((event) => readCaret(event.currentTarget), [
3592
+ const onSelect = useCallback7((event) => readCaret(event.currentTarget), [
3024
3593
  readCaret
3025
3594
  ]);
3026
- const onClick = useCallback4((event) => readCaret(event.currentTarget), [
3595
+ const onClick = useCallback7((event) => readCaret(event.currentTarget), [
3027
3596
  readCaret
3028
3597
  ]);
3029
- const setInput = useCallback4((element) => {
3598
+ const setInput = useCallback7((element) => {
3030
3599
  inputRef.current = element;
3031
3600
  }, []);
3032
- const getInputProps = useCallback4(() => ({
3601
+ const getInputProps = useCallback7(() => ({
3033
3602
  ref: setInput,
3034
3603
  onChange,
3035
3604
  onKeyDown,
@@ -3057,7 +3626,7 @@ function useComposerAutocomplete(options) {
3057
3626
  activeIndex,
3058
3627
  optionId
3059
3628
  ]);
3060
- const getListboxProps = useCallback4(() => ({
3629
+ const getListboxProps = useCallback7(() => ({
3061
3630
  id: listboxId,
3062
3631
  role: "listbox",
3063
3632
  "aria-label": match?.source.label ?? match?.source.id ?? "Suggestions"
@@ -3065,7 +3634,7 @@ function useComposerAutocomplete(options) {
3065
3634
  listboxId,
3066
3635
  match
3067
3636
  ]);
3068
- const getOptionProps = useCallback4((index) => ({
3637
+ const getOptionProps = useCallback7((index) => ({
3069
3638
  id: optionId(index),
3070
3639
  role: "option",
3071
3640
  "aria-selected": index === activeIndex
@@ -3099,7 +3668,7 @@ function isPromise(value) {
3099
3668
  __name(isPromise, "isPromise");
3100
3669
 
3101
3670
  // src/presentation/use-tool-catalog.ts
3102
- import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo3, useState as useState7 } from "react";
3671
+ import { useCallback as useCallback8, useEffect as useEffect9, useMemo as useMemo5, useState as useState10 } from "react";
3103
3672
  var shared = /* @__PURE__ */ new WeakMap();
3104
3673
  function load(client, agent) {
3105
3674
  let byAgent = shared.get(client);
@@ -3110,6 +3679,9 @@ function load(client, agent) {
3110
3679
  const key = agent ?? "";
3111
3680
  const cached = byAgent.get(key);
3112
3681
  if (cached !== void 0) return cached;
3682
+ if (client.listTools === void 0) {
3683
+ return Promise.reject(new AgentBackendUnsupportedError("listTools"));
3684
+ }
3113
3685
  const request = client.listTools(agent).catch((error) => {
3114
3686
  byAgent.delete(key);
3115
3687
  throw error;
@@ -3120,11 +3692,11 @@ function load(client, agent) {
3120
3692
  __name(load, "load");
3121
3693
  function useToolCatalog(options) {
3122
3694
  const { client, agent, enabled = true } = options;
3123
- const [entries, setEntries] = useState7([]);
3124
- const [isLoading, setLoading] = useState7(enabled);
3125
- const [error, setError] = useState7(null);
3126
- const [generation, setGeneration] = useState7(0);
3127
- useEffect7(() => {
3695
+ const [entries, setEntries] = useState10([]);
3696
+ const [isLoading, setLoading] = useState10(enabled);
3697
+ const [error, setError] = useState10(null);
3698
+ const [generation, setGeneration] = useState10(0);
3699
+ useEffect9(() => {
3128
3700
  if (!enabled) {
3129
3701
  setLoading(false);
3130
3702
  return;
@@ -3150,14 +3722,14 @@ function useToolCatalog(options) {
3150
3722
  enabled,
3151
3723
  generation
3152
3724
  ]);
3153
- const refresh = useCallback5(() => {
3725
+ const refresh = useCallback8(() => {
3154
3726
  shared.get(client)?.delete(agent ?? "");
3155
3727
  setGeneration((current) => current + 1);
3156
3728
  }, [
3157
3729
  client,
3158
3730
  agent
3159
3731
  ]);
3160
- const catalog = useMemo3(() => toolCatalogFrom(entries), [
3732
+ const catalog = useMemo5(() => toolCatalogFrom(entries), [
3161
3733
  entries
3162
3734
  ]);
3163
3735
  return {
@@ -3171,7 +3743,7 @@ function useToolCatalog(options) {
3171
3743
  __name(useToolCatalog, "useToolCatalog");
3172
3744
 
3173
3745
  // src/approvals/countdown.ts
3174
- import { useEffect as useEffect8, useRef as useRef6, useState as useState8 } from "react";
3746
+ import { useEffect as useEffect10, useRef as useRef9, useState as useState11 } from "react";
3175
3747
  function approvalCountdown(expiresAt, now = Date.now()) {
3176
3748
  const deadline = expiresAt == null ? Number.NaN : Date.parse(expiresAt);
3177
3749
  if (!Number.isFinite(deadline)) {
@@ -3189,10 +3761,10 @@ function approvalCountdown(expiresAt, now = Date.now()) {
3189
3761
  __name(approvalCountdown, "approvalCountdown");
3190
3762
  function useApprovalCountdown(expiresAt, options = {}) {
3191
3763
  const { intervalMs = 1e3 } = options;
3192
- const nowRef = useRef6(options.now ?? Date.now);
3764
+ const nowRef = useRef9(options.now ?? Date.now);
3193
3765
  nowRef.current = options.now ?? Date.now;
3194
- const [state, setState] = useState8(() => approvalCountdown(expiresAt, nowRef.current()));
3195
- useEffect8(() => {
3766
+ const [state, setState] = useState11(() => approvalCountdown(expiresAt, nowRef.current()));
3767
+ useEffect10(() => {
3196
3768
  const read = /* @__PURE__ */ __name(() => approvalCountdown(expiresAt, nowRef.current()), "read");
3197
3769
  const first = read();
3198
3770
  setState(first);
@@ -3358,7 +3930,12 @@ function storedMessageToUiMessage(message) {
3358
3930
  return {
3359
3931
  id: message.id,
3360
3932
  role: message.role,
3361
- parts
3933
+ parts,
3934
+ ...message.feedback !== void 0 ? {
3935
+ metadata: {
3936
+ feedback: message.feedback
3937
+ }
3938
+ } : {}
3362
3939
  };
3363
3940
  }
3364
3941
  __name(storedMessageToUiMessage, "storedMessageToUiMessage");
@@ -3394,14 +3971,21 @@ function mergeTurn(rows) {
3394
3971
  }
3395
3972
  const parts = rows.flatMap((row) => storedMessageToUiMessage(row).parts);
3396
3973
  const usage = sumUsage(rows);
3974
+ const feedback = lastRow.feedback;
3975
+ const metadata = {
3976
+ ...usage !== void 0 ? {
3977
+ usage
3978
+ } : {},
3979
+ ...feedback !== void 0 ? {
3980
+ feedback
3981
+ } : {}
3982
+ };
3397
3983
  return {
3398
3984
  id: lastRow.id,
3399
3985
  role: lastRow.role,
3400
3986
  parts,
3401
- ...usage !== void 0 ? {
3402
- metadata: {
3403
- usage
3404
- }
3987
+ ...Object.keys(metadata).length > 0 ? {
3988
+ metadata
3405
3989
  } : {}
3406
3990
  };
3407
3991
  }
@@ -3422,20 +4006,21 @@ __name(sumUsage, "sumUsage");
3422
4006
 
3423
4007
  // src/use-agent-chat.ts
3424
4008
  import { useChat } from "@ai-sdk/react";
3425
- import { useCallback as useCallback6, useEffect as useEffect9, useMemo as useMemo4, useRef as useRef7, useState as useState9 } from "react";
4009
+ import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMemo6, useRef as useRef10, useState as useState12 } from "react";
3426
4010
  function useAgentChat(options) {
3427
- const latest = useRef7(options);
4011
+ const latest = useRef10(options);
3428
4012
  latest.current = options;
3429
- const [runId, setRunId] = useState9(options.resumeRunId);
3430
- const runIdRef = useRef7(runId);
4013
+ const [runId, setRunId] = useState12(options.resumeRunId);
4014
+ const runIdRef = useRef10(runId);
3431
4015
  runIdRef.current = runId;
3432
- const settlingRunIdRef = useRef7(void 0);
3433
- const autoResumeRunIdRef = useRef7(void 0);
3434
- const [autoResume, setAutoResume] = useState9(false);
3435
- const [activeRunId, setActiveRunId] = useState9(null);
3436
- const regenerateNext = useRef7(false);
3437
- const createdThreadId = useRef7(void 0);
3438
- const client = useMemo4(() => {
4016
+ const settlingRunIdRef = useRef10(void 0);
4017
+ const autoResumeRunIdRef = useRef10(void 0);
4018
+ const [autoResume, setAutoResume] = useState12(false);
4019
+ const [activeRunId, setActiveRunId] = useState12(null);
4020
+ const regenerateNext = useRef10(false);
4021
+ const createdThreadId = useRef10(void 0);
4022
+ const client = useMemo6(() => {
4023
+ if (options.backend) return options.backend;
3439
4024
  if (options.client) return options.client;
3440
4025
  return new AgentClient({
3441
4026
  ...options.baseUrl !== void 0 ? {
@@ -3450,7 +4035,11 @@ function useAgentChat(options) {
3450
4035
  getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders")
3451
4036
  });
3452
4037
  }, []);
3453
- useEffect9(() => {
4038
+ const [connection, setConnection] = useState12({
4039
+ status: "live"
4040
+ });
4041
+ const resyncAfterSettle = useRef10(false);
4042
+ useEffect11(() => {
3454
4043
  if (!options.resume || options.threadId === void 0) return;
3455
4044
  let cancelled = false;
3456
4045
  const threadId = options.threadId;
@@ -3470,30 +4059,33 @@ function useAgentChat(options) {
3470
4059
  options.resume,
3471
4060
  options.threadId
3472
4061
  ]);
3473
- const transport = useMemo4(() => {
4062
+ const transport = useMemo6(() => {
3474
4063
  function onMeta(meta) {
3475
4064
  settlingRunIdRef.current = meta.runId;
3476
4065
  setRunId(meta.runId);
3477
4066
  if (latest.current.threadId === void 0 && meta.threadId && createdThreadId.current !== meta.threadId) {
3478
4067
  createdThreadId.current = meta.threadId;
3479
4068
  latest.current.onThreadCreated?.(meta.threadId);
4069
+ notifyThreads(client, {
4070
+ type: "changed"
4071
+ });
3480
4072
  }
3481
4073
  }
3482
4074
  __name(onMeta, "onMeta");
3483
4075
  return new AgentChatTransport({
3484
- ...options.baseUrl !== void 0 ? {
3485
- baseUrl: options.baseUrl
3486
- } : {},
3487
- ...options.credentials !== void 0 ? {
3488
- credentials: options.credentials
3489
- } : {},
3490
- ...options.fetch !== void 0 ? {
3491
- fetch: options.fetch
4076
+ backend: client,
4077
+ ...options.reconnect !== void 0 ? {
4078
+ reconnect: options.reconnect
3492
4079
  } : {},
4080
+ onConnectionChange: /* @__PURE__ */ __name((state) => {
4081
+ if (state.status === "gone") resyncAfterSettle.current = true;
4082
+ setConnection(state.status === "gone" ? {
4083
+ status: "live"
4084
+ } : state);
4085
+ }, "onConnectionChange"),
3493
4086
  ...options.agent !== void 0 ? {
3494
4087
  agent: options.agent
3495
4088
  } : {},
3496
- getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders"),
3497
4089
  getBody: /* @__PURE__ */ __name(() => {
3498
4090
  const current = latest.current;
3499
4091
  const pageContext = current.getPageContext?.() ?? null;
@@ -3504,6 +4096,9 @@ function useAgentChat(options) {
3504
4096
  ...threadId !== void 0 ? {
3505
4097
  threadId
3506
4098
  } : {},
4099
+ ...current.model !== void 0 ? {
4100
+ model: current.model
4101
+ } : {},
3507
4102
  ...pageContext ? {
3508
4103
  pageContext
3509
4104
  } : {},
@@ -3519,7 +4114,7 @@ function useAgentChat(options) {
3519
4114
  }, "onAttemptStart")
3520
4115
  });
3521
4116
  }, []);
3522
- const backgroundRef = useRef7(() => Promise.resolve());
4117
+ const backgroundRef = useRef10(() => Promise.resolve());
3523
4118
  const chat = useChat({
3524
4119
  transport,
3525
4120
  resume: options.resumeRunId !== void 0 || autoResume,
@@ -3533,7 +4128,15 @@ function useAgentChat(options) {
3533
4128
  latest.current.onData?.(part);
3534
4129
  if (part.type === "data-title") {
3535
4130
  const title = part.data?.title;
3536
- if (typeof title === "string") latest.current.onTitle?.(title);
4131
+ if (typeof title === "string") {
4132
+ latest.current.onTitle?.(title);
4133
+ const threadId = latest.current.threadId ?? createdThreadId.current;
4134
+ if (threadId !== void 0) notifyThreads(client, {
4135
+ type: "title",
4136
+ threadId,
4137
+ title
4138
+ });
4139
+ }
3537
4140
  }
3538
4141
  }, "onData"),
3539
4142
  onFinish: /* @__PURE__ */ __name(({ isError }) => {
@@ -3547,16 +4150,36 @@ function useAgentChat(options) {
3547
4150
  runId: settledRunId,
3548
4151
  status: isError ? "failed" : "completed"
3549
4152
  });
4153
+ notifyThreads(client, {
4154
+ type: "changed"
4155
+ });
4156
+ }
4157
+ setConnection({
4158
+ status: "live"
4159
+ });
4160
+ if (resyncAfterSettle.current) {
4161
+ resyncAfterSettle.current = false;
4162
+ void resyncFromThread();
3550
4163
  }
3551
4164
  }, "onFinish")
3552
4165
  });
3553
- const chatRef = useRef7(chat);
4166
+ const chatRef = useRef10(chat);
3554
4167
  chatRef.current = chat;
3555
- const turnInFlight = useRef7(false);
3556
- const isTurnInFlight = useCallback6(() => turnInFlight.current || transport.isAttemptLive, [
4168
+ async function resyncFromThread() {
4169
+ const threadId = latest.current.threadId ?? createdThreadId.current;
4170
+ if (threadId === void 0) return;
4171
+ try {
4172
+ const thread = await client.getThread(threadId);
4173
+ chatRef.current.setMessages(storedThreadToUiMessages(thread.messages));
4174
+ } catch {
4175
+ }
4176
+ }
4177
+ __name(resyncFromThread, "resyncFromThread");
4178
+ const turnInFlight = useRef10(false);
4179
+ const isTurnInFlight = useCallback9(() => turnInFlight.current || transport.isAttemptLive, [
3557
4180
  transport
3558
4181
  ]);
3559
- const sendMessage = useCallback6(async (...args) => {
4182
+ const sendMessage = useCallback9(async (...args) => {
3560
4183
  if (isTurnInFlight()) return;
3561
4184
  turnInFlight.current = true;
3562
4185
  try {
@@ -3567,15 +4190,15 @@ function useAgentChat(options) {
3567
4190
  }, [
3568
4191
  isTurnInFlight
3569
4192
  ]);
3570
- const addToolResult = useCallback6(({ tool, toolCallId, output }) => chatRef.current.addToolResult({
4193
+ const addToolResult = useCallback9(({ tool, toolCallId, output }) => chatRef.current.addToolResult({
3571
4194
  tool,
3572
4195
  toolCallId,
3573
4196
  output
3574
4197
  }), []);
3575
- const [threads, setThreads] = useState9([]);
3576
- const [quota, setQuota] = useState9(null);
3577
- const [backgroundRuns, setBackgroundRuns] = useState9([]);
3578
- const refreshBackground = useCallback6(async () => {
4198
+ const [threads, setThreads] = useState12([]);
4199
+ const [quota, setQuota] = useState12(null);
4200
+ const [backgroundRuns, setBackgroundRuns] = useState12([]);
4201
+ const refreshBackground = useCallback9(async () => {
3579
4202
  const threadId = latest.current.threadId ?? createdThreadId.current;
3580
4203
  if (threadId === void 0) {
3581
4204
  return;
@@ -3600,7 +4223,7 @@ function useAgentChat(options) {
3600
4223
  ]);
3601
4224
  backgroundRef.current = refreshBackground;
3602
4225
  const isWorking = backgroundRuns.some((run) => run.status === "running");
3603
- useEffect9(() => {
4226
+ useEffect11(() => {
3604
4227
  if (options.background !== true || options.threadId === void 0) {
3605
4228
  return;
3606
4229
  }
@@ -3610,7 +4233,7 @@ function useAgentChat(options) {
3610
4233
  options.threadId,
3611
4234
  refreshBackground
3612
4235
  ]);
3613
- useEffect9(() => {
4236
+ useEffect11(() => {
3614
4237
  const every = latest.current.backgroundPollMs ?? 5e3;
3615
4238
  if (latest.current.background !== true || !isWorking || every <= 0) {
3616
4239
  return;
@@ -3621,27 +4244,29 @@ function useAgentChat(options) {
3621
4244
  isWorking,
3622
4245
  refreshBackground
3623
4246
  ]);
3624
- const loadThreads = useCallback6(async () => {
4247
+ const loadThreads = useCallback9(async () => {
3625
4248
  const list = await client.listThreads();
3626
4249
  setThreads(list);
3627
4250
  return list;
3628
4251
  }, [
3629
4252
  client
3630
4253
  ]);
3631
- const loadThread = useCallback6((id) => client.getThread(id), [
4254
+ const loadThread = useCallback9((id) => client.getThread(id), [
3632
4255
  client
3633
4256
  ]);
3634
- const deleteThread = useCallback6(async (id) => {
4257
+ const deleteThread = useCallback9(async (id) => {
3635
4258
  await client.deleteThread(id);
3636
4259
  setThreads((current) => current.filter((thread) => thread.id !== id));
3637
4260
  }, [
3638
4261
  client
3639
4262
  ]);
3640
- const forkThread = useCallback6((threadId, messageId) => client.forkFromMessage(threadId, messageId), [
4263
+ const forkThread = useCallback9((threadId, messageId) => requireBackendMethod(client, "forkFromMessage")(threadId, messageId), [
3641
4264
  client
3642
4265
  ]);
3643
- const renameThread = useCallback6(async (id, title) => {
3644
- await client.renameThread(id, title);
4266
+ const renameThread = useCallback9(async (id, title) => {
4267
+ await client.updateThread(id, {
4268
+ title
4269
+ });
3645
4270
  setThreads((current) => current.map((thread) => thread.id === id ? {
3646
4271
  ...thread,
3647
4272
  title
@@ -3649,24 +4274,24 @@ function useAgentChat(options) {
3649
4274
  }, [
3650
4275
  client
3651
4276
  ]);
3652
- const promoteThread = useCallback6(async (id) => {
3653
- await client.promoteThread(id);
4277
+ const promoteThread = useCallback9(async (id) => {
4278
+ await requireBackendMethod(client, "promoteThread")(id);
3654
4279
  await loadThreads();
3655
4280
  }, [
3656
4281
  client,
3657
4282
  loadThreads
3658
4283
  ]);
3659
- const truncateFromMessage = useCallback6((threadId, messageId) => client.truncateFromMessage(threadId, messageId).then(() => void 0), [
4284
+ const truncateFromMessage = useCallback9((threadId, messageId) => requireBackendMethod(client, "truncateFromMessage")(threadId, messageId).then(() => void 0), [
3660
4285
  client
3661
4286
  ]);
3662
- const loadQuota = useCallback6(async () => {
3663
- const today = await client.getQuotaToday();
4287
+ const loadQuota = useCallback9(async () => {
4288
+ const today = await requireBackendMethod(client, "getQuotaToday")();
3664
4289
  setQuota(today);
3665
4290
  return today;
3666
4291
  }, [
3667
4292
  client
3668
4293
  ]);
3669
- const cancel = useCallback6(async () => {
4294
+ const cancel = useCallback9(async () => {
3670
4295
  chatRef.current.stop();
3671
4296
  const activeRunId2 = runIdRef.current;
3672
4297
  if (activeRunId2) {
@@ -3678,8 +4303,8 @@ function useAgentChat(options) {
3678
4303
  }, [
3679
4304
  client
3680
4305
  ]);
3681
- const approve = useCallback6(async ({ toolCallId, remember, via }) => {
3682
- await client.approveToolCall({
4306
+ const approve = useCallback9(async ({ toolCallId, remember, via }) => {
4307
+ await requireBackendMethod(client, "approveToolCall")({
3683
4308
  toolCallId,
3684
4309
  ...remember === true ? {
3685
4310
  remember: true
@@ -3691,8 +4316,8 @@ function useAgentChat(options) {
3691
4316
  }, [
3692
4317
  client
3693
4318
  ]);
3694
- const reject = useCallback6(async ({ toolCallId, reason, via }) => {
3695
- await client.rejectToolCall({
4319
+ const reject = useCallback9(async ({ toolCallId, reason, via }) => {
4320
+ await requireBackendMethod(client, "rejectToolCall")({
3696
4321
  toolCallId,
3697
4322
  ...reason !== void 0 ? {
3698
4323
  reason
@@ -3704,8 +4329,8 @@ function useAgentChat(options) {
3704
4329
  }, [
3705
4330
  client
3706
4331
  ]);
3707
- const answer = useCallback6(async ({ toolCallId, answers }) => {
3708
- await client.answerToolCall({
4332
+ const answer = useCallback9(async ({ toolCallId, answers }) => {
4333
+ await requireBackendMethod(client, "answerToolCall")({
3709
4334
  toolCallId,
3710
4335
  ...answers !== void 0 ? {
3711
4336
  answers
@@ -3714,14 +4339,14 @@ function useAgentChat(options) {
3714
4339
  }, [
3715
4340
  client
3716
4341
  ]);
3717
- const skip = useCallback6(async ({ toolCallId }) => {
3718
- await client.skipToolCall({
4342
+ const skip = useCallback9(async ({ toolCallId }) => {
4343
+ await requireBackendMethod(client, "skipToolCall")({
3719
4344
  toolCallId
3720
4345
  });
3721
4346
  }, [
3722
4347
  client
3723
4348
  ]);
3724
- const regenerate = useCallback6(() => {
4349
+ const regenerate = useCallback9(() => {
3725
4350
  if (isTurnInFlight()) return;
3726
4351
  regenerateNext.current = true;
3727
4352
  turnInFlight.current = true;
@@ -3736,8 +4361,27 @@ function useAgentChat(options) {
3736
4361
  isWorking,
3737
4362
  refresh: refreshBackground
3738
4363
  };
4364
+ const setThreadModel = useCallback9(async (model) => {
4365
+ const threadId = latest.current.threadId ?? createdThreadId.current;
4366
+ if (threadId === void 0) {
4367
+ throw new Error("setThreadModel: this chat has no thread yet");
4368
+ }
4369
+ await client.updateThread(threadId, {
4370
+ model
4371
+ });
4372
+ notifyThreads(client, {
4373
+ type: "changed"
4374
+ });
4375
+ }, [
4376
+ client
4377
+ ]);
4378
+ const getThreadId = useCallback9(() => latest.current.threadId ?? createdThreadId.current, []);
4379
+ const status = connection.status === "reconnecting" ? "reconnecting" : chat.status;
3739
4380
  return {
3740
4381
  ...chat,
4382
+ status,
4383
+ /** Where the live stream stands — see {@link StreamConnectionState}. */
4384
+ connection,
3741
4385
  sendMessage,
3742
4386
  addToolResult,
3743
4387
  /** Sub-agents this conversation started and did not wait for. */
@@ -3745,6 +4389,15 @@ function useAgentChat(options) {
3745
4389
  runId,
3746
4390
  /** The `resume`-fetched thread's active run id, or `null` once resolved with none. */
3747
4391
  activeRunId,
4392
+ /**
4393
+ * The thread this chat is on right now: the `threadId` option, else the one the backend created
4394
+ * on the first send. A getter, because the created id is learned mid-stream.
4395
+ */
4396
+ getThreadId,
4397
+ setThreadModel,
4398
+ /** The backend this chat talks to — pass it to `useThreads`, `useMessageFeedback`, … */
4399
+ backend: client,
4400
+ /** Same object as {@link backend}; kept for callers that read it under its old name. */
3748
4401
  client,
3749
4402
  threads,
3750
4403
  loadThreads,
@@ -3774,6 +4427,7 @@ async function mergeHeaders(options) {
3774
4427
  }
3775
4428
  __name(mergeHeaders, "mergeHeaders");
3776
4429
  export {
4430
+ AgentBackendUnsupportedError,
3777
4431
  AgentChatTransport,
3778
4432
  AgentClient,
3779
4433
  AgentHttpError,
@@ -3803,22 +4457,29 @@ export {
3803
4457
  inferResultView,
3804
4458
  isActionCall,
3805
4459
  loadStoredSpeechLang,
4460
+ notifyThreads,
4461
+ onThreadsEvent,
3806
4462
  persistSpeechLang,
3807
4463
  phraseFor,
3808
4464
  readPath,
3809
4465
  readReasoningMs,
4466
+ requireBackendMethod,
3810
4467
  resolveResultView,
3811
4468
  storedMessageToUiMessage,
3812
4469
  storedThreadToUiMessages,
3813
4470
  toolCallState,
3814
4471
  toolCatalogFrom,
3815
4472
  useAgentChat,
4473
+ useAgents,
3816
4474
  useApprovalCountdown,
3817
4475
  useChatTranscript,
3818
4476
  useComposerAutocomplete,
3819
4477
  useElapsed,
4478
+ useMessageFeedback,
4479
+ useModels,
3820
4480
  useSpeechRecognition,
3821
4481
  useStickToBottom,
4482
+ useThreads,
3822
4483
  useToolCatalog,
3823
4484
  useTranscriptItem,
3824
4485
  validateElicitationAnswer as validateAnswer,