@dudousxd/nestjs-agent-react 0.5.5 → 0.6.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
@@ -4,6 +4,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
4
4
  // src/agent-chat-transport.ts
5
5
  var HEADER_RUN_ID = "x-agent-run-id";
6
6
  var HEADER_THREAD_ID = "x-agent-thread-id";
7
+ var ASK_TOOL_NAME = "ask";
7
8
  var AgentChatTransport = class {
8
9
  static {
9
10
  __name(this, "AgentChatTransport");
@@ -11,6 +12,14 @@ var AgentChatTransport = class {
11
12
  options;
12
13
  currentRunId;
13
14
  currentThreadId;
15
+ /**
16
+ * One attempt at a time. The AI SDK keeps a single in-flight response per chat and decides
17
+ * whether its streamed message REPLACES the list's last entry or is appended by comparing that
18
+ * response's message id against the last entry alone. Two attempts writing into one chat
19
+ * therefore append alternating copies of each other's message, and the list ends up holding
20
+ * several entries under each id — which React renders as duplicate keys.
21
+ */
22
+ attemptLive = false;
14
23
  constructor(options = {}) {
15
24
  this.options = options;
16
25
  }
@@ -22,59 +31,84 @@ var AgentChatTransport = class {
22
31
  get threadId() {
23
32
  return this.currentThreadId;
24
33
  }
34
+ /**
35
+ * True from the moment a send or resume attempt starts until its chunk stream terminates. A
36
+ * caller that can refuse a turn BEFORE the SDK commits to one — `useAgentChat` wrapping
37
+ * `sendMessage` — reads this; the transport itself can only refuse a resume, which is the one
38
+ * attempt the SDK lets a transport decline.
39
+ */
40
+ get isAttemptLive() {
41
+ return this.attemptLive;
42
+ }
25
43
  async sendMessages(options) {
26
44
  this.options.onAttemptStart?.();
27
- const lastMessage = options.messages.at(-1);
28
- const message = lastMessage ? extractText(lastMessage) : "";
29
- const body = {
30
- ...this.options.agent !== void 0 ? {
31
- agent: this.options.agent
32
- } : {},
33
- ...this.options.getBody?.() ?? {},
34
- ...options.body ?? {},
35
- message
36
- };
37
- const response = await this.fetchImpl()(`${this.baseUrl()}/agent/chat`, {
38
- method: "POST",
39
- headers: {
40
- "content-type": "application/json",
41
- accept: "text/event-stream",
42
- ...await this.resolveHeaders(options.headers)
43
- },
44
- body: JSON.stringify(body),
45
- ...this.options.credentials !== void 0 ? {
46
- credentials: this.options.credentials
47
- } : {},
48
- ...options.abortSignal ? {
49
- signal: options.abortSignal
50
- } : {}
51
- });
52
- if (!response.ok || !response.body) {
53
- throw new Error(`Agent chat request failed: ${response.status} ${response.statusText}`);
45
+ this.attemptLive = true;
46
+ try {
47
+ const lastMessage = options.messages.at(-1);
48
+ const message = lastMessage ? extractText(lastMessage) : "";
49
+ const body = {
50
+ ...this.options.agent !== void 0 ? {
51
+ agent: this.options.agent
52
+ } : {},
53
+ ...this.options.getBody?.() ?? {},
54
+ ...options.body ?? {},
55
+ message
56
+ };
57
+ const response = await this.fetchImpl()(`${this.baseUrl()}/agent/chat`, {
58
+ method: "POST",
59
+ headers: {
60
+ "content-type": "application/json",
61
+ accept: "text/event-stream",
62
+ ...await this.resolveHeaders(options.headers)
63
+ },
64
+ body: JSON.stringify(body),
65
+ ...this.options.credentials !== void 0 ? {
66
+ credentials: this.options.credentials
67
+ } : {},
68
+ ...options.abortSignal ? {
69
+ signal: options.abortSignal
70
+ } : {}
71
+ });
72
+ if (!response.ok || !response.body) {
73
+ throw new Error(`Agent chat request failed: ${response.status} ${response.statusText}`);
74
+ }
75
+ this.captureHeaderMeta(response.headers);
76
+ return this.toChunkStream(response.body);
77
+ } catch (error) {
78
+ this.attemptLive = false;
79
+ throw error;
54
80
  }
55
- this.captureHeaderMeta(response.headers);
56
- return this.toChunkStream(response.body);
57
81
  }
58
82
  async reconnectToStream(options) {
83
+ if (this.attemptLive) return null;
59
84
  this.options.onAttemptStart?.();
60
85
  const runId = this.options.getResumeRunId?.();
61
86
  if (runId === void 0) return null;
62
- const response = await this.fetchImpl()(`${this.baseUrl()}/agent/chat/${encodeURIComponent(runId)}/stream`, {
63
- method: "GET",
64
- headers: {
65
- accept: "text/event-stream",
66
- ...await this.resolveHeaders(options.headers)
67
- },
68
- ...this.options.credentials !== void 0 ? {
69
- credentials: this.options.credentials
70
- } : {}
71
- });
72
- if (response.status === 404) return null;
73
- if (!response.ok || !response.body) {
74
- throw new Error(`Agent stream reconnect failed: ${response.status} ${response.statusText}`);
87
+ this.attemptLive = true;
88
+ try {
89
+ const response = await this.fetchImpl()(`${this.baseUrl()}/agent/chat/${encodeURIComponent(runId)}/stream`, {
90
+ method: "GET",
91
+ headers: {
92
+ accept: "text/event-stream",
93
+ ...await this.resolveHeaders(options.headers)
94
+ },
95
+ ...this.options.credentials !== void 0 ? {
96
+ credentials: this.options.credentials
97
+ } : {}
98
+ });
99
+ if (response.status === 404) {
100
+ this.attemptLive = false;
101
+ return null;
102
+ }
103
+ if (!response.ok || !response.body) {
104
+ throw new Error(`Agent stream reconnect failed: ${response.status} ${response.statusText}`);
105
+ }
106
+ this.captureHeaderMeta(response.headers);
107
+ return this.toChunkStream(response.body);
108
+ } catch (error) {
109
+ this.attemptLive = false;
110
+ throw error;
75
111
  }
76
- this.captureHeaderMeta(response.headers);
77
- return this.toChunkStream(response.body);
78
112
  }
79
113
  fetchImpl() {
80
114
  return this.options.fetch ?? globalThis.fetch;
@@ -123,7 +157,11 @@ var AgentChatTransport = class {
123
157
  let stepIndex = 0;
124
158
  let textId = null;
125
159
  let reasoningId = null;
160
+ const announced = /* @__PURE__ */ new Set();
126
161
  const record = /* @__PURE__ */ __name((meta) => this.recordMeta(meta), "record");
162
+ const endAttempt = /* @__PURE__ */ __name(() => {
163
+ this.attemptLive = false;
164
+ }, "endAttempt");
127
165
  return new ReadableStream({
128
166
  async pull(controller) {
129
167
  function ensureStarted() {
@@ -220,6 +258,7 @@ var AgentChatTransport = class {
220
258
  break;
221
259
  case "tool-input-start":
222
260
  ensureStep();
261
+ announced.add(event.id);
223
262
  controller.enqueue({
224
263
  type: "tool-input-start",
225
264
  toolCallId: event.id,
@@ -244,6 +283,7 @@ var AgentChatTransport = class {
244
283
  break;
245
284
  case "tool-input-available":
246
285
  ensureStep();
286
+ announced.add(event.id);
247
287
  controller.enqueue({
248
288
  type: "tool-input-available",
249
289
  toolCallId: event.id,
@@ -256,6 +296,23 @@ var AgentChatTransport = class {
256
296
  } : {}
257
297
  });
258
298
  break;
299
+ case "elicitation":
300
+ ensureStep();
301
+ if (!announced.has(event.id)) {
302
+ announced.add(event.id);
303
+ controller.enqueue({
304
+ type: "tool-input-available",
305
+ toolCallId: event.id,
306
+ toolName: ASK_TOOL_NAME,
307
+ input: {
308
+ ...event.request.preamble !== void 0 ? {
309
+ preamble: event.request.preamble
310
+ } : {},
311
+ questions: event.request.questions
312
+ }
313
+ });
314
+ }
315
+ break;
259
316
  case "tool-output":
260
317
  ensureStep();
261
318
  controller.enqueue({
@@ -284,6 +341,7 @@ var AgentChatTransport = class {
284
341
  type: "finish"
285
342
  });
286
343
  controller.close();
344
+ endAttempt();
287
345
  }
288
346
  __name(finish, "finish");
289
347
  try {
@@ -312,6 +370,7 @@ var AgentChatTransport = class {
312
370
  errorText: parseErrorText(frame.data)
313
371
  });
314
372
  controller.close();
373
+ endAttempt();
315
374
  return;
316
375
  }
317
376
  if (frame.event === "meta") {
@@ -330,10 +389,12 @@ var AgentChatTransport = class {
330
389
  errorText: error instanceof Error ? error.message : "Agent stream error"
331
390
  });
332
391
  controller.close();
392
+ endAttempt();
333
393
  }
334
394
  },
335
395
  cancel() {
336
396
  void reader.cancel();
397
+ endAttempt();
337
398
  }
338
399
  });
339
400
  }
@@ -401,6 +462,53 @@ function extractText(message) {
401
462
  }
402
463
  __name(extractText, "extractText");
403
464
 
465
+ // src/background-runs.ts
466
+ function asReceipt(output) {
467
+ if (typeof output !== "object" || output === null) {
468
+ return null;
469
+ }
470
+ const candidate = output;
471
+ if (candidate.detached !== true || candidate.status !== "started" || typeof candidate.agent !== "string" || typeof candidate.runId !== "string") {
472
+ return null;
473
+ }
474
+ return {
475
+ detached: true,
476
+ status: "started",
477
+ agent: candidate.agent,
478
+ runId: candidate.runId
479
+ };
480
+ }
481
+ __name(asReceipt, "asReceipt");
482
+ function backgroundRunsFromThread(messages) {
483
+ const byRun = /* @__PURE__ */ new Map();
484
+ for (const message of messages) {
485
+ for (const result of message.toolResults ?? []) {
486
+ const receipt = asReceipt(result.output);
487
+ if (receipt === null || byRun.has(receipt.runId)) {
488
+ continue;
489
+ }
490
+ byRun.set(receipt.runId, {
491
+ runId: receipt.runId,
492
+ agent: receipt.agent,
493
+ toolCallId: result.id,
494
+ toolName: result.name,
495
+ status: "running"
496
+ });
497
+ }
498
+ }
499
+ for (const message of messages) {
500
+ const run = message.runId === void 0 ? void 0 : byRun.get(message.runId);
501
+ if (run !== void 0) {
502
+ run.status = "delivered";
503
+ run.message = message;
504
+ }
505
+ }
506
+ return [
507
+ ...byRun.values()
508
+ ];
509
+ }
510
+ __name(backgroundRunsFromThread, "backgroundRunsFromThread");
511
+
404
512
  // src/client.ts
405
513
  var AgentHttpError = class extends Error {
406
514
  static {
@@ -425,6 +533,16 @@ var AgentClient = class {
425
533
  listThreads() {
426
534
  return this.request("GET", "/agent/threads");
427
535
  }
536
+ /**
537
+ * The skills this caller can invoke right now, scope-resolved — the same list, built by the same
538
+ * call, that the model is offered, so what a user can type after a `/` and what the agent can
539
+ * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to
540
+ * one conversation; omitted, the server reads it as a brand-new thread.
541
+ */
542
+ listSkills(threadId) {
543
+ const query = threadId === void 0 ? "" : `?threadId=${encodeURIComponent(threadId)}`;
544
+ return this.request("GET", `/agent/skills${query}`);
545
+ }
428
546
  getThread(id) {
429
547
  return this.request("GET", `/agent/threads/${encodeURIComponent(id)}`);
430
548
  }
@@ -486,6 +604,23 @@ var AgentClient = class {
486
604
  rejectToolCall(input) {
487
605
  return this.request("POST", "/agent/tool-call/reject", input);
488
606
  }
607
+ /**
608
+ * Settle a parked question set. `answers` is questionId → chosen option values; a question left
609
+ * out takes the pre-picked default the request carried, resolved server-side against the request
610
+ * the run already holds. Omit the whole object and the user has confirmed every pre-picked
611
+ * answer — which is the point of the surface, so it is a valid submission rather than a blank.
612
+ */
613
+ answerToolCall(input) {
614
+ return this.request("POST", "/agent/tool-call/answer", input);
615
+ }
616
+ /**
617
+ * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same
618
+ * values a confirmation would, and persists differently on purpose — only one of them is
619
+ * evidence the user chose them.
620
+ */
621
+ skipToolCall(input) {
622
+ return this.request("POST", "/agent/tool-call/skip", input);
623
+ }
489
624
  async request(method, path, body) {
490
625
  const fetchImpl = this.options.fetch ?? globalThis.fetch;
491
626
  const baseUrl = (this.options.baseUrl ?? "").replace(/\/$/, "");
@@ -520,414 +655,76 @@ var AgentClient = class {
520
655
  }
521
656
  };
522
657
 
523
- // src/stored-message-to-ui-message.ts
524
- function storedMessageToUiMessage(message) {
525
- const parts = [];
526
- if (message.content) {
527
- parts.push({
528
- type: "text",
529
- text: message.content
530
- });
531
- }
532
- for (const attachment of message.attachments ?? []) {
533
- parts.push({
534
- type: "file",
535
- mediaType: attachment.contentType,
536
- filename: attachment.name,
537
- url: attachment.url
538
- });
539
- }
540
- for (const call of message.toolCalls ?? []) {
541
- const result = message.toolResults?.find((candidate) => candidate.id === call.id);
542
- const toolKind = call.kind;
543
- parts.push({
544
- type: `tool-${call.name}`,
545
- toolCallId: call.id,
546
- ...toolKind !== void 0 ? {
547
- toolMetadata: {
548
- toolKind
549
- }
550
- } : {},
551
- ...result !== void 0 ? {
552
- state: "output-available",
553
- input: call.input,
554
- output: result.output
555
- } : {
556
- state: "input-available",
557
- input: call.input
558
- }
559
- });
560
- }
561
- return {
562
- id: message.id,
563
- role: message.role,
564
- parts
565
- };
566
- }
567
- __name(storedMessageToUiMessage, "storedMessageToUiMessage");
568
-
569
- // src/stored-thread-to-ui-messages.ts
570
- function storedThreadToUiMessages(messages) {
571
- const turns = [];
572
- for (const message of messages) {
573
- const currentTurn = turns.at(-1);
574
- const previousRow = currentTurn?.at(-1);
575
- if (message.role === "assistant" && previousRow?.role === "assistant" && currentTurn) {
576
- currentTurn.push(message);
577
- } else {
578
- turns.push([
579
- message
580
- ]);
581
- }
582
- }
583
- return turns.map(mergeTurn);
584
- }
585
- __name(storedThreadToUiMessages, "storedThreadToUiMessages");
586
- function mergeTurn(rows) {
587
- const lastRow = rows.at(-1);
588
- if (lastRow === void 0) {
589
- throw new Error("storedThreadToUiMessages: encountered an empty turn");
590
- }
591
- if (rows.length === 1) {
592
- return storedMessageToUiMessage(lastRow);
593
- }
594
- const parts = rows.flatMap((row) => storedMessageToUiMessage(row).parts);
595
- const usage = sumUsage(rows);
596
- return {
597
- id: lastRow.id,
598
- role: lastRow.role,
599
- parts,
600
- ...usage !== void 0 ? {
601
- metadata: {
602
- usage
603
- }
604
- } : {}
605
- };
606
- }
607
- __name(mergeTurn, "mergeTurn");
608
- function sumUsage(rows) {
609
- const usages = rows.map((row) => row.usage).filter((usage) => usage !== void 0);
610
- if (usages.length === 0) {
611
- return void 0;
612
- }
613
- const allCostsUnknown = usages.every((usage) => usage.costUsd === null || usage.costUsd === void 0);
614
- return {
615
- inputTokens: usages.reduce((sum, usage) => sum + usage.inputTokens, 0),
616
- outputTokens: usages.reduce((sum, usage) => sum + usage.outputTokens, 0),
617
- costUsd: allCostsUnknown ? null : usages.reduce((sum, usage) => sum + (usage.costUsd ?? 0), 0)
618
- };
619
- }
620
- __name(sumUsage, "sumUsage");
658
+ // src/components/chat-input.tsx
659
+ import React2, { useEffect as useEffect2, useState as useState2 } from "react";
621
660
 
622
- // src/use-agent-chat.ts
623
- import { useChat } from "@ai-sdk/react";
661
+ // src/components/use-speech-recognition.ts
624
662
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
625
- function useAgentChat(options) {
626
- const latest = useRef(options);
627
- latest.current = options;
628
- const [runId, setRunId] = useState(options.resumeRunId);
629
- const runIdRef = useRef(runId);
630
- runIdRef.current = runId;
631
- const settlingRunIdRef = useRef(void 0);
632
- const autoResumeRunIdRef = useRef(void 0);
633
- const [autoResume, setAutoResume] = useState(false);
634
- const [activeRunId, setActiveRunId] = useState(null);
635
- const regenerateNext = useRef(false);
636
- const createdThreadId = useRef(void 0);
637
- const client = useMemo(() => {
638
- if (options.client) return options.client;
639
- return new AgentClient({
640
- ...options.baseUrl !== void 0 ? {
641
- baseUrl: options.baseUrl
642
- } : {},
643
- ...options.credentials !== void 0 ? {
644
- credentials: options.credentials
645
- } : {},
646
- ...options.fetch !== void 0 ? {
647
- fetch: options.fetch
648
- } : {},
649
- getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders")
650
- });
651
- }, []);
663
+ function getRecognitionCtor() {
664
+ if (typeof window === "undefined") return null;
665
+ const scope = window;
666
+ return scope.SpeechRecognition ?? scope.webkitSpeechRecognition ?? null;
667
+ }
668
+ __name(getRecognitionCtor, "getRecognitionCtor");
669
+ function useSpeechRecognition({ lang = "en-US", onFinalTranscript, onInterimTranscript }) {
670
+ const recognitionRef = useRef(null);
671
+ const [isListening, setIsListening] = useState(false);
672
+ const ctor = useMemo(() => getRecognitionCtor(), []);
673
+ const isSupported = ctor !== null;
674
+ const finalRef = useRef(onFinalTranscript);
675
+ const interimRef = useRef(onInterimTranscript);
676
+ finalRef.current = onFinalTranscript;
677
+ interimRef.current = onInterimTranscript;
652
678
  useEffect(() => {
653
- if (!options.resume || options.threadId === void 0) return;
654
- let cancelled = false;
655
- const threadId = options.threadId;
656
- void client.getThread(threadId).then((thread) => {
657
- if (cancelled) return;
658
- setActiveRunId(thread.activeRunId ?? null);
659
- if (thread.activeRunId != null) {
660
- autoResumeRunIdRef.current = thread.activeRunId;
661
- setAutoResume(true);
679
+ if (!ctor) return;
680
+ const recognition = new ctor();
681
+ recognition.lang = lang;
682
+ recognition.continuous = false;
683
+ recognition.interimResults = true;
684
+ recognition.onresult = (event) => {
685
+ let interim = "";
686
+ for (let i = event.resultIndex; i < event.results.length; i++) {
687
+ const result = event.results[i];
688
+ if (!result) continue;
689
+ const text = result[0]?.transcript ?? "";
690
+ if (result.isFinal) {
691
+ finalRef.current(text);
692
+ } else {
693
+ interim += text;
694
+ }
662
695
  }
663
- });
696
+ if (interim && interimRef.current) interimRef.current(interim);
697
+ };
698
+ recognition.onerror = (event) => {
699
+ if (event.error !== "no-speech" && event.error !== "aborted") {
700
+ console.warn("SpeechRecognition error", event.error);
701
+ }
702
+ setIsListening(false);
703
+ };
704
+ recognition.onend = () => setIsListening(false);
705
+ recognitionRef.current = recognition;
664
706
  return () => {
665
- cancelled = true;
707
+ try {
708
+ recognition.stop();
709
+ } catch {
710
+ }
711
+ recognitionRef.current = null;
666
712
  };
667
713
  }, [
668
- client,
669
- options.resume,
670
- options.threadId
714
+ ctor,
715
+ lang
671
716
  ]);
672
- const transport = useMemo(() => {
673
- function onMeta(meta) {
674
- settlingRunIdRef.current = meta.runId;
675
- setRunId(meta.runId);
676
- if (latest.current.threadId === void 0 && meta.threadId && createdThreadId.current !== meta.threadId) {
677
- createdThreadId.current = meta.threadId;
678
- latest.current.onThreadCreated?.(meta.threadId);
679
- }
680
- }
681
- __name(onMeta, "onMeta");
682
- return new AgentChatTransport({
683
- ...options.baseUrl !== void 0 ? {
684
- baseUrl: options.baseUrl
685
- } : {},
686
- ...options.credentials !== void 0 ? {
687
- credentials: options.credentials
688
- } : {},
689
- ...options.fetch !== void 0 ? {
690
- fetch: options.fetch
691
- } : {},
692
- ...options.agent !== void 0 ? {
693
- agent: options.agent
694
- } : {},
695
- getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders"),
696
- getBody: /* @__PURE__ */ __name(() => {
697
- const current = latest.current;
698
- const pageContext = current.getPageContext?.() ?? null;
699
- const regenerate2 = regenerateNext.current;
700
- regenerateNext.current = false;
701
- const threadId = current.threadId ?? createdThreadId.current;
702
- return {
703
- ...threadId !== void 0 ? {
704
- threadId
705
- } : {},
706
- ...pageContext ? {
707
- pageContext
708
- } : {},
709
- ...regenerate2 ? {
710
- regenerate: true
711
- } : {}
712
- };
713
- }, "getBody"),
714
- getResumeRunId: /* @__PURE__ */ __name(() => latest.current.resumeRunId ?? autoResumeRunIdRef.current, "getResumeRunId"),
715
- onMeta,
716
- onAttemptStart: /* @__PURE__ */ __name(() => {
717
- settlingRunIdRef.current = void 0;
718
- }, "onAttemptStart")
719
- });
720
- }, []);
721
- const chat = useChat({
722
- transport,
723
- resume: options.resumeRunId !== void 0 || autoResume,
724
- ...options.threadId !== void 0 ? {
725
- id: options.threadId
726
- } : {},
727
- ...options.initialMessages !== void 0 ? {
728
- messages: options.initialMessages
729
- } : {},
730
- onFinish: /* @__PURE__ */ __name(({ isError }) => {
731
- latest.current.onFinish?.();
732
- const settledRunId = settlingRunIdRef.current;
733
- if (settledRunId !== void 0) {
734
- latest.current.onRunSettled?.({
735
- runId: settledRunId,
736
- status: isError ? "failed" : "completed"
737
- });
738
- }
739
- }, "onFinish")
740
- });
741
- const chatRef = useRef(chat);
742
- chatRef.current = chat;
743
- const addToolResult = useCallback(({ tool, toolCallId, output }) => chatRef.current.addToolResult({
744
- tool,
745
- toolCallId,
746
- output
747
- }), []);
748
- const [threads, setThreads] = useState([]);
749
- const [quota, setQuota] = useState(null);
750
- const loadThreads = useCallback(async () => {
751
- const list = await client.listThreads();
752
- setThreads(list);
753
- return list;
754
- }, [
755
- client
756
- ]);
757
- const loadThread = useCallback((id) => client.getThread(id), [
758
- client
759
- ]);
760
- const deleteThread = useCallback(async (id) => {
761
- await client.deleteThread(id);
762
- setThreads((current) => current.filter((thread) => thread.id !== id));
763
- }, [
764
- client
765
- ]);
766
- const forkThread = useCallback((threadId, messageId) => client.forkFromMessage(threadId, messageId), [
767
- client
768
- ]);
769
- const renameThread = useCallback(async (id, title) => {
770
- await client.renameThread(id, title);
771
- setThreads((current) => current.map((thread) => thread.id === id ? {
772
- ...thread,
773
- title
774
- } : thread));
775
- }, [
776
- client
777
- ]);
778
- const promoteThread = useCallback(async (id) => {
779
- await client.promoteThread(id);
780
- await loadThreads();
781
- }, [
782
- client,
783
- loadThreads
784
- ]);
785
- const truncateFromMessage = useCallback((threadId, messageId) => client.truncateFromMessage(threadId, messageId).then(() => void 0), [
786
- client
787
- ]);
788
- const loadQuota = useCallback(async () => {
789
- const today = await client.getQuotaToday();
790
- setQuota(today);
791
- return today;
792
- }, [
793
- client
794
- ]);
795
- const cancel = useCallback(async () => {
796
- chatRef.current.stop();
797
- const activeRunId2 = runIdRef.current;
798
- if (activeRunId2) {
799
- try {
800
- await client.cancelStream(activeRunId2);
801
- } catch {
802
- }
803
- }
804
- }, [
805
- client
806
- ]);
807
- const approve = useCallback(async ({ toolCallId }) => {
808
- await client.approveToolCall({
809
- toolCallId
810
- });
811
- }, [
812
- client
813
- ]);
814
- const reject = useCallback(async ({ toolCallId, reason }) => {
815
- await client.rejectToolCall({
816
- toolCallId,
817
- ...reason !== void 0 ? {
818
- reason
819
- } : {}
820
- });
821
- }, [
822
- client
823
- ]);
824
- const regenerate = useCallback(() => {
825
- regenerateNext.current = true;
826
- void chatRef.current.regenerate();
827
- }, []);
828
- return {
829
- ...chat,
830
- addToolResult,
831
- runId,
832
- /** The `resume`-fetched thread's active run id, or `null` once resolved with none. */
833
- activeRunId,
834
- client,
835
- threads,
836
- loadThreads,
837
- loadThread,
838
- deleteThread,
839
- forkThread,
840
- renameThread,
841
- promoteThread,
842
- truncateFromMessage,
843
- quota,
844
- loadQuota,
845
- cancel,
846
- approve,
847
- reject,
848
- regenerate
849
- };
850
- }
851
- __name(useAgentChat, "useAgentChat");
852
- async function mergeHeaders(options) {
853
- const dynamic = await options.getHeaders?.() ?? {};
854
- return {
855
- ...options.headers,
856
- ...dynamic
857
- };
858
- }
859
- __name(mergeHeaders, "mergeHeaders");
860
-
861
- // src/components/chat-input.tsx
862
- import React, { useEffect as useEffect3, useState as useState3 } from "react";
863
-
864
- // src/components/use-speech-recognition.ts
865
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
866
- function getRecognitionCtor() {
867
- if (typeof window === "undefined") return null;
868
- const scope = window;
869
- return scope.SpeechRecognition ?? scope.webkitSpeechRecognition ?? null;
870
- }
871
- __name(getRecognitionCtor, "getRecognitionCtor");
872
- function useSpeechRecognition({ lang = "en-US", onFinalTranscript, onInterimTranscript }) {
873
- const recognitionRef = useRef2(null);
874
- const [isListening, setIsListening] = useState2(false);
875
- const ctor = useMemo2(() => getRecognitionCtor(), []);
876
- const isSupported = ctor !== null;
877
- const finalRef = useRef2(onFinalTranscript);
878
- const interimRef = useRef2(onInterimTranscript);
879
- finalRef.current = onFinalTranscript;
880
- interimRef.current = onInterimTranscript;
881
- useEffect2(() => {
882
- if (!ctor) return;
883
- const recognition = new ctor();
884
- recognition.lang = lang;
885
- recognition.continuous = false;
886
- recognition.interimResults = true;
887
- recognition.onresult = (event) => {
888
- let interim = "";
889
- for (let i = event.resultIndex; i < event.results.length; i++) {
890
- const result = event.results[i];
891
- if (!result) continue;
892
- const text = result[0]?.transcript ?? "";
893
- if (result.isFinal) {
894
- finalRef.current(text);
895
- } else {
896
- interim += text;
897
- }
898
- }
899
- if (interim && interimRef.current) interimRef.current(interim);
900
- };
901
- recognition.onerror = (event) => {
902
- if (event.error !== "no-speech" && event.error !== "aborted") {
903
- console.warn("SpeechRecognition error", event.error);
904
- }
905
- setIsListening(false);
906
- };
907
- recognition.onend = () => setIsListening(false);
908
- recognitionRef.current = recognition;
909
- return () => {
910
- try {
911
- recognition.stop();
912
- } catch {
913
- }
914
- recognitionRef.current = null;
915
- };
916
- }, [
917
- ctor,
918
- lang
919
- ]);
920
- const start = useCallback2(() => {
921
- const recognition = recognitionRef.current;
922
- if (!recognition) return;
923
- try {
924
- recognition.start();
925
- setIsListening(true);
926
- } catch (error) {
927
- console.warn("SpeechRecognition start failed", error);
717
+ const start = useCallback(() => {
718
+ const recognition = recognitionRef.current;
719
+ if (!recognition) return;
720
+ try {
721
+ recognition.start();
722
+ setIsListening(true);
723
+ } catch (error) {
724
+ console.warn("SpeechRecognition start failed", error);
928
725
  }
929
726
  }, []);
930
- const stop = useCallback2(() => {
727
+ const stop = useCallback(() => {
931
728
  const recognition = recognitionRef.current;
932
729
  if (!recognition) return;
933
730
  try {
@@ -945,9 +742,9 @@ function useSpeechRecognition({ lang = "en-US", onFinalTranscript, onInterimTran
945
742
  __name(useSpeechRecognition, "useSpeechRecognition");
946
743
 
947
744
  // src/components/chat-input.tsx
948
- function ChatInput({ onSubmit, disabled, placeholder, rows = 2, enableSpeech = true, speechLang = "en-US", sendLabel = "Send", className, textareaClassName, sendButtonClassName, micButtonClassName, renderMic }) {
949
- const [value, setValue] = useState3("");
950
- const [interim, setInterim] = useState3("");
745
+ 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 }) {
746
+ const [value, setValue] = useState2("");
747
+ const [interim, setInterim] = useState2("");
951
748
  const speech = useSpeechRecognition({
952
749
  lang: speechLang,
953
750
  onFinalTranscript: /* @__PURE__ */ __name((text) => {
@@ -958,7 +755,7 @@ function ChatInput({ onSubmit, disabled, placeholder, rows = 2, enableSpeech = t
958
755
  }, "onFinalTranscript"),
959
756
  onInterimTranscript: /* @__PURE__ */ __name((text) => setInterim(text), "onInterimTranscript")
960
757
  });
961
- useEffect3(() => {
758
+ useEffect2(() => {
962
759
  if (!speech.isListening) setInterim("");
963
760
  }, [
964
761
  speech.isListening
@@ -985,9 +782,10 @@ function ChatInput({ onSubmit, disabled, placeholder, rows = 2, enableSpeech = t
985
782
  __name(toggleMic, "toggleMic");
986
783
  const displayValue = speech.isListening && interim ? `${value}${value && !value.endsWith(" ") ? " " : ""}${interim}` : value;
987
784
  const showMic = enableSpeech && speech.isSupported;
988
- return /* @__PURE__ */ React.createElement("div", {
785
+ const showStop = isStreaming && onStop !== void 0;
786
+ return /* @__PURE__ */ React2.createElement("div", {
989
787
  className
990
- }, /* @__PURE__ */ React.createElement("textarea", {
788
+ }, /* @__PURE__ */ React2.createElement("textarea", {
991
789
  className: textareaClassName,
992
790
  rows,
993
791
  value: displayValue,
@@ -998,12 +796,17 @@ function ChatInput({ onSubmit, disabled, placeholder, rows = 2, enableSpeech = t
998
796
  }, "onChange"),
999
797
  onKeyDown: handleKeyDown,
1000
798
  placeholder: placeholder ?? "Ask something (Enter to send, Shift+Enter for newline)"
1001
- }), /* @__PURE__ */ React.createElement("button", {
799
+ }), /* @__PURE__ */ React2.createElement("button", {
1002
800
  type: "button",
1003
801
  className: sendButtonClassName,
1004
802
  onClick: submit,
1005
803
  disabled: disabled || !value.trim()
1006
- }, sendLabel), showMic ? /* @__PURE__ */ React.createElement("button", {
804
+ }, sendLabel), showStop ? /* @__PURE__ */ React2.createElement("button", {
805
+ type: "button",
806
+ className: stopButtonClassName,
807
+ onClick: /* @__PURE__ */ __name(() => void onStop(), "onClick"),
808
+ "aria-label": "Stop generating"
809
+ }, stopLabel) : null, showMic ? /* @__PURE__ */ React2.createElement("button", {
1007
810
  type: "button",
1008
811
  className: micButtonClassName,
1009
812
  onClick: toggleMic,
@@ -1016,223 +819,393 @@ function ChatInput({ onSubmit, disabled, placeholder, rows = 2, enableSpeech = t
1016
819
  __name(ChatInput, "ChatInput");
1017
820
 
1018
821
  // src/components/message-item.tsx
1019
- import { isTextUIPart, isToolUIPart } from "ai";
1020
- import React2, { useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
1021
- function MessageItem({ message, renderToolPart, renderToolGroup, renderText, classNames, isStreaming = false, editable, onEditSubmit, regeneratable, onRegenerate, onFork, usage, createdAt }) {
1022
- const isUser = message.role === "user";
1023
- const parts = message.parts ?? [];
1024
- const copyableText = extractText2(parts);
1025
- const actionRow = /* @__PURE__ */ React2.createElement("div", {
1026
- className: classNames?.actions,
1027
- "data-actions-for": message.role
1028
- }, copyableText ? /* @__PURE__ */ React2.createElement(CopyButton, {
1029
- text: copyableText,
1030
- className: classNames?.actionButton
1031
- }) : null, onFork ? /* @__PURE__ */ React2.createElement("button", {
1032
- type: "button",
1033
- onClick: /* @__PURE__ */ __name(() => void onFork(message.id), "onClick"),
1034
- className: classNames?.actionButton,
1035
- title: "Fork thread from this message"
1036
- }, "Fork") : null, !isUser && regeneratable && onRegenerate ? /* @__PURE__ */ React2.createElement("button", {
1037
- type: "button",
1038
- onClick: /* @__PURE__ */ __name(() => void onRegenerate(), "onClick"),
1039
- className: classNames?.actionButton,
1040
- title: "Regenerate response"
1041
- }, "Regenerate") : null, !isUser && usage ? /* @__PURE__ */ React2.createElement(UsageInline, {
1042
- usage,
1043
- className: classNames?.usage
1044
- }) : null, createdAt ? /* @__PURE__ */ React2.createElement(TimestampInline, {
1045
- createdAt,
1046
- className: classNames?.timestamp
1047
- }) : null);
1048
- if (isUser && editable && onEditSubmit) {
1049
- return /* @__PURE__ */ React2.createElement(EditableUserBubble, {
1050
- message,
1051
- onSubmit: onEditSubmit,
1052
- renderText,
1053
- classNames,
1054
- actions: actionRow
1055
- });
1056
- }
1057
- const className = [
1058
- classNames?.root,
1059
- classNames?.byRole?.[message.role]
1060
- ].filter((entry) => entry).join(" ");
1061
- return /* @__PURE__ */ React2.createElement("div", {
1062
- className: className === "" ? void 0 : className,
1063
- "data-role": message.role,
1064
- "data-streaming": isStreaming ? "true" : void 0
1065
- }, /* @__PURE__ */ React2.createElement("div", null, renderParts(message, parts, {
1066
- renderText,
1067
- renderToolPart,
1068
- renderToolGroup,
1069
- classNames,
1070
- isStreaming
1071
- })), actionRow);
1072
- }
1073
- __name(MessageItem, "MessageItem");
1074
- function renderParts(message, parts, ctx) {
822
+ import React3 from "react";
823
+
824
+ // src/transcript/use-chat-transcript.ts
825
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
826
+
827
+ // src/transcript/model.ts
828
+ import { getToolName, isFileUIPart, isReasoningUIPart, isTextUIPart, isToolUIPart } from "ai";
829
+ function buildTranscriptBlocks(message, options) {
1075
830
  const blocks = [];
1076
831
  let toolBuffer = [];
1077
- let toolGroupCounter = 0;
832
+ let retrievalBuffer = [];
833
+ let fileBuffer = [];
834
+ let toolCounter = 0;
1078
835
  let textCounter = 0;
836
+ let fileCounter = 0;
837
+ let reasoningCounter = 0;
838
+ let sourcesCounter = 0;
839
+ let elicitationCounter = 0;
1079
840
  function flushTools() {
1080
841
  if (toolBuffer.length === 0) {
1081
842
  return;
1082
843
  }
1083
- const key = `${message.id}-tools-${toolGroupCounter++}`;
1084
- if (ctx.renderToolGroup) {
1085
- blocks.push(/* @__PURE__ */ React2.createElement(React2.Fragment, {
1086
- key
1087
- }, ctx.renderToolGroup(toolBuffer, key)));
1088
- } else if (ctx.renderToolPart) {
1089
- for (const part of toolBuffer) {
1090
- const partKey = `${message.id}-${part.toolCallId}`;
1091
- blocks.push(/* @__PURE__ */ React2.createElement(React2.Fragment, {
1092
- key: partKey
1093
- }, ctx.renderToolPart(part, partKey)));
1094
- }
1095
- }
844
+ blocks.push({
845
+ kind: "tools",
846
+ key: `${message.id}-tools-${toolCounter++}`,
847
+ parts: toolBuffer,
848
+ calls: toolBuffer.map((part) => buildToolCall(part, options.approval))
849
+ });
1096
850
  toolBuffer = [];
1097
851
  }
1098
852
  __name(flushTools, "flushTools");
1099
- for (const part of parts) {
853
+ function flushSources() {
854
+ if (retrievalBuffer.length === 0) {
855
+ return;
856
+ }
857
+ blocks.push(buildSourcesBlock(`${message.id}-sources-${sourcesCounter++}`, retrievalBuffer));
858
+ retrievalBuffer = [];
859
+ }
860
+ __name(flushSources, "flushSources");
861
+ function flushFiles() {
862
+ if (fileBuffer.length === 0) {
863
+ return;
864
+ }
865
+ blocks.push({
866
+ kind: "files",
867
+ key: `${message.id}-files-${fileCounter++}`,
868
+ files: fileBuffer.map((part) => ({
869
+ url: part.url,
870
+ mediaType: part.mediaType,
871
+ filename: part.filename ?? null,
872
+ isImage: part.mediaType.startsWith("image/")
873
+ }))
874
+ });
875
+ fileBuffer = [];
876
+ }
877
+ __name(flushFiles, "flushFiles");
878
+ function flushAll() {
879
+ flushTools();
880
+ flushSources();
881
+ flushFiles();
882
+ }
883
+ __name(flushAll, "flushAll");
884
+ for (const part of message.parts ?? []) {
1100
885
  if (isToolUIPart(part)) {
886
+ const elicitation = options.elicitation;
887
+ if (elicitation !== void 0) {
888
+ const request = readElicitationRequest(part);
889
+ if (request !== null) {
890
+ flushAll();
891
+ blocks.push(buildElicitationBlock(`${message.id}-elicitation-${elicitationCounter++}`, part, request, elicitation));
892
+ continue;
893
+ }
894
+ }
895
+ if (options.sources === true && readPassages(part) !== null) {
896
+ flushTools();
897
+ retrievalBuffer.push(part);
898
+ continue;
899
+ }
900
+ flushSources();
901
+ flushFiles();
1101
902
  toolBuffer.push(part);
1102
903
  continue;
1103
904
  }
1104
- flushTools();
905
+ if (isFileUIPart(part)) {
906
+ flushTools();
907
+ flushSources();
908
+ fileBuffer.push(part);
909
+ continue;
910
+ }
911
+ flushAll();
1105
912
  if (isTextUIPart(part)) {
1106
- const key = `${message.id}-text-${textCounter++}`;
1107
- blocks.push(/* @__PURE__ */ React2.createElement("div", {
913
+ blocks.push({
914
+ kind: "text",
915
+ key: `${message.id}-text-${textCounter++}`,
916
+ text: part.text,
917
+ isStreaming: part.state === "streaming"
918
+ });
919
+ continue;
920
+ }
921
+ if (isReasoningUIPart(part)) {
922
+ const key = `${message.id}-reasoning-${reasoningCounter++}`;
923
+ const isStreaming = part.state === "streaming";
924
+ blocks.push({
925
+ kind: "reasoning",
1108
926
  key,
1109
- className: ctx.classNames?.text
1110
- }, ctx.renderText ? ctx.renderText(part.text, {
1111
- isStreaming: ctx.isStreaming
1112
- }) : part.text));
927
+ text: part.text,
928
+ isStreaming,
929
+ isOpen: options.isReasoningOpen(key, isStreaming),
930
+ toggle: /* @__PURE__ */ __name((open) => options.toggleReasoning(key, open), "toggle")
931
+ });
1113
932
  }
1114
933
  }
1115
- flushTools();
934
+ flushAll();
1116
935
  return blocks;
1117
936
  }
1118
- __name(renderParts, "renderParts");
1119
- function EditableUserBubble({ message, onSubmit, renderText, classNames, actions }) {
1120
- const initialText = extractText2(message.parts ?? []);
1121
- const [isEditing, setIsEditing] = useState4(false);
1122
- const [draft, setDraft] = useState4(initialText);
1123
- const textareaRef = useRef3(null);
1124
- useEffect4(() => {
1125
- if (isEditing && textareaRef.current) {
1126
- const el = textareaRef.current;
1127
- el.focus();
1128
- el.setSelectionRange(el.value.length, el.value.length);
1129
- }
1130
- }, [
1131
- isEditing
1132
- ]);
1133
- async function handleSave() {
1134
- const next = draft.trim();
1135
- if (!next) {
1136
- return;
937
+ __name(buildTranscriptBlocks, "buildTranscriptBlocks");
938
+ function buildSourcesBlock(key, parts) {
939
+ const byId = /* @__PURE__ */ new Map();
940
+ let query = null;
941
+ let passageCount = 0;
942
+ for (const part of parts) {
943
+ query ??= readQuery(part);
944
+ for (const passage of readPassages(part) ?? []) {
945
+ passageCount++;
946
+ const id = passage.source ?? passage.id;
947
+ const existing = byId.get(id);
948
+ if (existing) {
949
+ existing.passageCount++;
950
+ existing.topScore = Math.max(existing.topScore, passage.score);
951
+ existing.passages.push(passage);
952
+ continue;
953
+ }
954
+ byId.set(id, {
955
+ id,
956
+ label: passage.source ?? passage.id,
957
+ passageCount: 1,
958
+ topScore: passage.score,
959
+ passages: [
960
+ passage
961
+ ]
962
+ });
1137
963
  }
1138
- setIsEditing(false);
1139
- await onSubmit(next);
1140
- }
1141
- __name(handleSave, "handleSave");
1142
- if (!isEditing) {
1143
- const rootClass = [
1144
- classNames?.root,
1145
- classNames?.byRole?.user
1146
- ].filter((entry) => entry).join(" ");
1147
- return /* @__PURE__ */ React2.createElement("div", {
1148
- className: rootClass === "" ? void 0 : rootClass,
1149
- "data-role": "user"
1150
- }, /* @__PURE__ */ React2.createElement("div", {
1151
- className: classNames?.text
1152
- }, renderText ? renderText(initialText, {
1153
- isStreaming: false
1154
- }) : initialText), /* @__PURE__ */ React2.createElement("button", {
1155
- type: "button",
1156
- onClick: /* @__PURE__ */ __name(() => {
1157
- setDraft(initialText);
1158
- setIsEditing(true);
1159
- }, "onClick"),
1160
- className: classNames?.actionButton,
1161
- title: "Edit & resubmit"
1162
- }, "Edit"), actions);
1163
964
  }
1164
- return /* @__PURE__ */ React2.createElement("div", {
1165
- "data-editing": "true"
1166
- }, /* @__PURE__ */ React2.createElement("textarea", {
1167
- ref: textareaRef,
1168
- value: draft,
1169
- onChange: /* @__PURE__ */ __name((event) => setDraft(event.target.value), "onChange"),
1170
- onKeyDown: /* @__PURE__ */ __name((event) => {
1171
- if (event.key === "Enter" && !event.shiftKey) {
1172
- event.preventDefault();
1173
- void handleSave();
1174
- }
1175
- if (event.key === "Escape") {
1176
- event.preventDefault();
1177
- setIsEditing(false);
965
+ return {
966
+ kind: "sources",
967
+ key,
968
+ query,
969
+ sources: [
970
+ ...byId.values()
971
+ ],
972
+ passageCount
973
+ };
974
+ }
975
+ __name(buildSourcesBlock, "buildSourcesBlock");
976
+ function isSettled(state) {
977
+ return state === "output-available" || state === "output-error" || state === "output-denied";
978
+ }
979
+ __name(isSettled, "isSettled");
980
+ function toolKind(part) {
981
+ const metadata = part.toolMetadata;
982
+ return isRecord(metadata) && typeof metadata.toolKind === "string" ? metadata.toolKind : void 0;
983
+ }
984
+ __name(toolKind, "toolKind");
985
+ function isAwaitingApproval(part) {
986
+ if (part.state === "approval-requested") {
987
+ return true;
988
+ }
989
+ if (part.state !== "input-available") {
990
+ return false;
991
+ }
992
+ return toolKind(part) === "action" && readElicitationRequest(part) === null;
993
+ }
994
+ __name(isAwaitingApproval, "isAwaitingApproval");
995
+ function buildToolCall(part, options) {
996
+ const toolCallId = part.toolCallId;
997
+ const awaiting = isAwaitingApproval(part);
998
+ const isSubmitting = awaiting && options?.isSubmitting(toolCallId) === true;
999
+ return {
1000
+ part,
1001
+ toolCallId,
1002
+ name: getToolName(part),
1003
+ isAwaitingApproval: awaiting,
1004
+ approve: {
1005
+ available: awaiting && options?.canApprove === true,
1006
+ isSubmitting,
1007
+ run: /* @__PURE__ */ __name(() => options?.approve(toolCallId), "run")
1008
+ },
1009
+ reject: {
1010
+ available: awaiting && options?.canReject === true,
1011
+ isSubmitting,
1012
+ run: /* @__PURE__ */ __name(() => options?.reject(toolCallId), "run")
1013
+ },
1014
+ error: options?.errorOf(toolCallId) ?? null
1015
+ };
1016
+ }
1017
+ __name(buildToolCall, "buildToolCall");
1018
+ function readElicitationRequest(part) {
1019
+ const input = part.input;
1020
+ if (!isRecord(input) || !Array.isArray(input.questions) || input.questions.length === 0) {
1021
+ return null;
1022
+ }
1023
+ const questions = [];
1024
+ for (const candidate of input.questions) {
1025
+ if (!isRecord(candidate) || typeof candidate.id !== "string" || typeof candidate.prompt !== "string" || !Array.isArray(candidate.options) || candidate.options.length === 0) {
1026
+ return null;
1027
+ }
1028
+ const options = [];
1029
+ for (const option of candidate.options) {
1030
+ if (!isRecord(option) || typeof option.value !== "string" || typeof option.label !== "string") {
1031
+ return null;
1178
1032
  }
1179
- }, "onKeyDown"),
1180
- rows: Math.min(8, draft.split("\n").length + 1),
1181
- className: classNames?.editTextarea
1182
- }), /* @__PURE__ */ React2.createElement("div", {
1183
- className: classNames?.editActions
1184
- }, /* @__PURE__ */ React2.createElement("button", {
1185
- type: "button",
1186
- onClick: /* @__PURE__ */ __name(() => setIsEditing(false), "onClick")
1187
- }, "Cancel"), /* @__PURE__ */ React2.createElement("button", {
1188
- type: "button",
1189
- onClick: /* @__PURE__ */ __name(() => void handleSave(), "onClick"),
1190
- disabled: !draft.trim()
1191
- }, "Resend")));
1033
+ options.push({
1034
+ value: option.value,
1035
+ label: option.label,
1036
+ hotkey: typeof option.hotkey === "string" ? option.hotkey : null
1037
+ });
1038
+ }
1039
+ questions.push({
1040
+ id: candidate.id,
1041
+ prompt: candidate.prompt,
1042
+ multiple: candidate.multiple === true,
1043
+ defaults: Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [],
1044
+ options
1045
+ });
1046
+ }
1047
+ return {
1048
+ preamble: typeof input.preamble === "string" ? input.preamble : null,
1049
+ questions
1050
+ };
1192
1051
  }
1193
- __name(EditableUserBubble, "EditableUserBubble");
1194
- function CopyButton({ text, className }) {
1195
- const [copied, setCopied] = useState4(false);
1196
- async function handleCopy() {
1197
- try {
1198
- await navigator.clipboard.writeText(text);
1199
- setCopied(true);
1200
- setTimeout(() => setCopied(false), 1500);
1201
- } catch {
1052
+ __name(readElicitationRequest, "readElicitationRequest");
1053
+ function readElicitationOutcome(part) {
1054
+ const output = part.output;
1055
+ if (!isRecord(output) || !isRecord(output.answers) || typeof output.skipped !== "boolean") {
1056
+ return null;
1057
+ }
1058
+ const answers = {};
1059
+ for (const [id, values] of Object.entries(output.answers)) {
1060
+ answers[id] = Array.isArray(values) ? values.filter((value) => typeof value === "string") : [];
1061
+ }
1062
+ return {
1063
+ answers,
1064
+ skipped: output.skipped,
1065
+ defaulted: Array.isArray(output.defaulted) ? output.defaulted.filter((id) => typeof id === "string") : [],
1066
+ summary: typeof output.summary === "string" ? output.summary : null
1067
+ };
1068
+ }
1069
+ __name(readElicitationOutcome, "readElicitationOutcome");
1070
+ function buildElicitationBlock(key, part, request, options) {
1071
+ const toolCallId = part.toolCallId;
1072
+ const isPending = !isSettled(part.state);
1073
+ const outcome = isPending ? null : readElicitationOutcome(part);
1074
+ const questions = request.questions.map((question, index) => {
1075
+ const picked = isPending ? options.picked(toolCallId, question.id) : void 0;
1076
+ const selected = picked ?? outcome?.answers[question.id] ?? question.defaults;
1077
+ const pick = /* @__PURE__ */ __name((values) => options.pick(toolCallId, question.id, values), "pick");
1078
+ return {
1079
+ id: question.id,
1080
+ prompt: question.prompt,
1081
+ multiple: question.multiple,
1082
+ position: index + 1,
1083
+ selected,
1084
+ isPristine: isPending ? picked === void 0 : outcome?.defaulted.includes(question.id) ?? true,
1085
+ options: question.options.map((option) => ({
1086
+ value: option.value,
1087
+ label: option.label,
1088
+ hotkey: option.hotkey,
1089
+ isSelected: selected.includes(option.value),
1090
+ isDefault: question.defaults.includes(option.value),
1091
+ select: /* @__PURE__ */ __name(() => {
1092
+ if (!isPending) {
1093
+ return;
1094
+ }
1095
+ if (!question.multiple) {
1096
+ pick([
1097
+ option.value
1098
+ ]);
1099
+ return;
1100
+ }
1101
+ pick(selected.includes(option.value) ? selected.filter((value) => value !== option.value) : [
1102
+ ...selected,
1103
+ option.value
1104
+ ]);
1105
+ }, "select")
1106
+ }))
1107
+ };
1108
+ });
1109
+ const isSubmitting = isPending && options.isSubmitting(toolCallId);
1110
+ return {
1111
+ kind: "elicitation",
1112
+ key,
1113
+ toolCallId,
1114
+ preamble: request.preamble,
1115
+ questions,
1116
+ questionCount: questions.length,
1117
+ isPending,
1118
+ outcome,
1119
+ error: options.errorOf(toolCallId),
1120
+ answer: {
1121
+ available: isPending && options.canAnswer,
1122
+ isSubmitting,
1123
+ run: /* @__PURE__ */ __name(() => options.answer(toolCallId), "run")
1124
+ },
1125
+ skip: {
1126
+ available: isPending && options.canSkip,
1127
+ isSubmitting,
1128
+ run: /* @__PURE__ */ __name(() => options.skip(toolCallId), "run")
1202
1129
  }
1130
+ };
1131
+ }
1132
+ __name(buildElicitationBlock, "buildElicitationBlock");
1133
+ function isRecord(value) {
1134
+ return typeof value === "object" && value !== null;
1135
+ }
1136
+ __name(isRecord, "isRecord");
1137
+ function readQuery(part) {
1138
+ const input = part.input;
1139
+ if (!isRecord(input) || typeof input.query !== "string") {
1140
+ return null;
1203
1141
  }
1204
- __name(handleCopy, "handleCopy");
1205
- return /* @__PURE__ */ React2.createElement("button", {
1206
- type: "button",
1207
- onClick: /* @__PURE__ */ __name(() => void handleCopy(), "onClick"),
1208
- className,
1209
- title: copied ? "Copied!" : "Copy message text"
1210
- }, copied ? "Copied" : "Copy");
1142
+ return input.query;
1211
1143
  }
1212
- __name(CopyButton, "CopyButton");
1213
- function UsageInline({ usage, className }) {
1214
- const total = usage.inputTokens + usage.outputTokens;
1215
- return /* @__PURE__ */ React2.createElement("span", {
1216
- className,
1217
- title: `Input ${usage.inputTokens.toLocaleString()} \xB7 Output ${usage.outputTokens.toLocaleString()} tokens`
1218
- }, formatCostUsd(usage.costUsd), " \xB7 ", formatTokensShort(total));
1144
+ __name(readQuery, "readQuery");
1145
+ function readPassages(part) {
1146
+ const output = part.output;
1147
+ if (!isRecord(output) || !Array.isArray(output.passages) || output.passages.length === 0) {
1148
+ return null;
1149
+ }
1150
+ const passages = [];
1151
+ for (const candidate of output.passages) {
1152
+ if (!isRecord(candidate) || typeof candidate.id !== "string" || typeof candidate.text !== "string") {
1153
+ return null;
1154
+ }
1155
+ passages.push({
1156
+ id: candidate.id,
1157
+ text: candidate.text,
1158
+ score: typeof candidate.score === "number" ? candidate.score : 0,
1159
+ ...typeof candidate.source === "string" ? {
1160
+ source: candidate.source
1161
+ } : {},
1162
+ ...isRecord(candidate.metadata) ? {
1163
+ metadata: candidate.metadata
1164
+ } : {}
1165
+ });
1166
+ }
1167
+ return passages;
1219
1168
  }
1220
- __name(UsageInline, "UsageInline");
1221
- function TimestampInline({ createdAt, className }) {
1222
- const parsed = new Date(createdAt);
1223
- if (Number.isNaN(parsed.getTime())) {
1169
+ __name(readPassages, "readPassages");
1170
+ function extractMessageText(parts) {
1171
+ const out = [];
1172
+ for (const part of parts ?? []) {
1173
+ if (isTextUIPart(part)) {
1174
+ out.push(part.text);
1175
+ }
1176
+ }
1177
+ return out.join("\n\n").trim();
1178
+ }
1179
+ __name(extractMessageText, "extractMessageText");
1180
+ function describeUsage(usage) {
1181
+ const totalTokens = usage.inputTokens + usage.outputTokens;
1182
+ return {
1183
+ ...usage,
1184
+ totalTokens,
1185
+ costLabel: formatCostUsd(usage.costUsd),
1186
+ tokensLabel: formatTokensShort(totalTokens)
1187
+ };
1188
+ }
1189
+ __name(describeUsage, "describeUsage");
1190
+ function describeTimestamp(createdAt) {
1191
+ if (createdAt == null) {
1224
1192
  return null;
1225
1193
  }
1226
- const absolute = parsed.toLocaleString(void 0, {
1227
- dateStyle: "medium",
1228
- timeStyle: "short"
1229
- });
1230
- return /* @__PURE__ */ React2.createElement("span", {
1231
- className,
1232
- title: absolute
1233
- }, formatRelativeTime(parsed));
1194
+ const date = new Date(createdAt);
1195
+ if (Number.isNaN(date.getTime())) {
1196
+ return null;
1197
+ }
1198
+ return {
1199
+ iso: createdAt,
1200
+ date,
1201
+ relative: formatRelativeTime(date),
1202
+ absolute: date.toLocaleString(void 0, {
1203
+ dateStyle: "medium",
1204
+ timeStyle: "short"
1205
+ })
1206
+ };
1234
1207
  }
1235
- __name(TimestampInline, "TimestampInline");
1208
+ __name(describeTimestamp, "describeTimestamp");
1236
1209
  var RELATIVE_TIME = new Intl.RelativeTimeFormat(void 0, {
1237
1210
  numeric: "auto",
1238
1211
  style: "narrow"
@@ -1271,6 +1244,9 @@ function formatTokensShort(n) {
1271
1244
  }
1272
1245
  __name(formatTokensShort, "formatTokensShort");
1273
1246
  function formatCostUsd(cost) {
1247
+ if (cost === null) {
1248
+ return "\u2014";
1249
+ }
1274
1250
  if (cost === 0) {
1275
1251
  return "$0";
1276
1252
  }
@@ -1283,142 +1259,799 @@ function formatCostUsd(cost) {
1283
1259
  return `$${cost.toFixed(2)}`;
1284
1260
  }
1285
1261
  __name(formatCostUsd, "formatCostUsd");
1286
- function extractText2(parts) {
1287
- const out = [];
1288
- for (const part of parts) {
1289
- if (isTextUIPart(part)) {
1290
- out.push(part.text);
1262
+
1263
+ // src/transcript/use-stick-to-bottom.ts
1264
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
1265
+ function useStickToBottom({ contentKey, threshold = 32 }) {
1266
+ const containerRef = useRef2(null);
1267
+ const [isAtBottom, setIsAtBottom] = useState3(true);
1268
+ const measure = useCallback2(() => {
1269
+ const element = containerRef.current;
1270
+ if (!element) {
1271
+ return;
1291
1272
  }
1292
- }
1293
- return out.join("\n\n").trim();
1273
+ const distance = element.scrollHeight - element.scrollTop - element.clientHeight;
1274
+ setIsAtBottom(distance <= threshold);
1275
+ }, [
1276
+ threshold
1277
+ ]);
1278
+ const scrollToBottom = useCallback2(() => {
1279
+ const element = containerRef.current;
1280
+ if (!element) {
1281
+ return;
1282
+ }
1283
+ element.scrollTop = element.scrollHeight;
1284
+ setIsAtBottom(true);
1285
+ }, []);
1286
+ const setContainer = useCallback2((element) => {
1287
+ containerRef.current = element;
1288
+ if (element) {
1289
+ measure();
1290
+ }
1291
+ }, [
1292
+ measure
1293
+ ]);
1294
+ useEffect3(() => {
1295
+ if (isAtBottom) {
1296
+ scrollToBottom();
1297
+ }
1298
+ }, [
1299
+ contentKey,
1300
+ isAtBottom,
1301
+ scrollToBottom
1302
+ ]);
1303
+ const getContainerProps = useCallback2(() => ({
1304
+ ref: setContainer,
1305
+ onScroll: measure
1306
+ }), [
1307
+ setContainer,
1308
+ measure
1309
+ ]);
1310
+ return {
1311
+ isAtBottom,
1312
+ showJumpToLatest: !isAtBottom,
1313
+ scrollToBottom,
1314
+ getContainerProps
1315
+ };
1294
1316
  }
1295
- __name(extractText2, "extractText");
1317
+ __name(useStickToBottom, "useStickToBottom");
1296
1318
 
1297
- // src/components/message-list.tsx
1298
- import React3, { useState as useState5 } from "react";
1319
+ // src/transcript/use-chat-transcript.ts
1299
1320
  var DEFAULT_VISIBLE_COUNT = 50;
1300
- var LOAD_EARLIER_STEP = 50;
1301
- function MessageList({ messages, status, renderToolPart, renderToolGroup, renderText, onFork, editable, onEditSubmit, regeneratable, onRegenerate, getUsage, getCreatedAt, emptyState, typingIndicator, followUps, onFollowUpClick, error, onRetry, classNames }) {
1302
- const [visibleCount, setVisibleCount] = useState5(DEFAULT_VISIBLE_COUNT);
1303
- if (messages.length === 0 && status === "ready") {
1304
- return /* @__PURE__ */ React3.createElement("div", {
1305
- className: classNames?.empty
1306
- }, emptyState);
1307
- }
1321
+ var DEFAULT_LOAD_EARLIER_STEP = 50;
1322
+ var DEFAULT_COPY_RESET_MS = 1500;
1323
+ function useChatTranscript(options) {
1324
+ const { messages, status, initialVisibleCount = DEFAULT_VISIBLE_COUNT, loadEarlierStep = DEFAULT_LOAD_EARLIER_STEP, followUps, stickToBottomThreshold } = options;
1325
+ const latest = useRef3(options);
1326
+ latest.current = options;
1327
+ const [visibleCount, setVisibleCount] = useState4(initialVisibleCount);
1328
+ const [stopRequested, setStopRequested] = useState4(false);
1329
+ const isStreaming = status === "streaming";
1330
+ const isBusy = status === "submitted" || isStreaming;
1331
+ const isEmpty = messages.length === 0;
1332
+ useEffect4(() => {
1333
+ if (!isBusy) {
1334
+ setStopRequested(false);
1335
+ }
1336
+ }, [
1337
+ isBusy
1338
+ ]);
1308
1339
  const lastMessage = messages.at(-1);
1309
- const showTypingBubble = (status === "submitted" || status === "streaming") && lastMessage?.role !== "assistant";
1310
- const showFollowUps = status === "ready" && !showTypingBubble && lastMessage?.role === "assistant" && onFollowUpClick !== void 0 && !!followUps && followUps.length > 0;
1340
+ const lastAssistant = findLastAssistant(messages);
1341
+ const streamingMessageId = isStreaming ? lastAssistant?.id ?? null : null;
1311
1342
  const visibleStart = Math.max(0, messages.length - visibleCount);
1312
- const visibleMessages = messages.slice(visibleStart);
1313
- const hiddenCount = visibleStart;
1314
- let lastAssistantIndex = -1;
1315
- for (let i = messages.length - 1; i >= 0; i--) {
1316
- if (messages[i]?.role === "assistant") {
1317
- lastAssistantIndex = i;
1318
- break;
1343
+ const items = useTranscriptItems({
1344
+ messages,
1345
+ visibleStart,
1346
+ streamingMessageId,
1347
+ options
1348
+ });
1349
+ const stop = useCallback3(() => {
1350
+ const current = latest.current;
1351
+ if (current.status !== "submitted" && current.status !== "streaming") {
1352
+ return;
1353
+ }
1354
+ setStopRequested(true);
1355
+ void current.onStop?.();
1356
+ }, []);
1357
+ const loadEarlier = useCallback3(() => setVisibleCount((count) => count + (latest.current.loadEarlierStep ?? DEFAULT_LOAD_EARLIER_STEP)), []);
1358
+ const showAll = useCallback3(() => setVisibleCount(latest.current.messages.length), []);
1359
+ const scroll = useStickToBottom({
1360
+ contentKey: contentSignature(messages),
1361
+ ...stickToBottomThreshold !== void 0 ? {
1362
+ threshold: stickToBottomThreshold
1363
+ } : {}
1364
+ });
1365
+ const showTypingIndicator = isBusy && lastMessage?.role !== "assistant";
1366
+ return {
1367
+ items,
1368
+ status,
1369
+ isEmpty,
1370
+ isBusy,
1371
+ isStreaming,
1372
+ showEmptyState: isEmpty && status === "ready",
1373
+ showTypingIndicator,
1374
+ showFollowUps: status === "ready" && !showTypingIndicator && lastMessage?.role === "assistant" && !!followUps && followUps.length > 0,
1375
+ lastAssistantId: lastAssistant?.id ?? null,
1376
+ window: {
1377
+ visibleCount,
1378
+ hiddenCount: visibleStart,
1379
+ canLoadEarlier: visibleStart > 0,
1380
+ loadEarlier,
1381
+ showAll
1382
+ },
1383
+ scroll,
1384
+ stop: {
1385
+ available: isBusy && options.onStop !== void 0,
1386
+ isStopping: stopRequested,
1387
+ stop
1319
1388
  }
1389
+ };
1390
+ }
1391
+ __name(useChatTranscript, "useChatTranscript");
1392
+ function useTranscriptItem(options) {
1393
+ const { message, isStreaming = false } = options;
1394
+ const messagesRef = useRef3([
1395
+ message
1396
+ ]);
1397
+ if (messagesRef.current[0] !== message) {
1398
+ messagesRef.current = [
1399
+ message
1400
+ ];
1320
1401
  }
1321
- return /* @__PURE__ */ React3.createElement("div", {
1322
- className: classNames?.root
1323
- }, hiddenCount > 0 ? /* @__PURE__ */ React3.createElement("button", {
1324
- type: "button",
1325
- className: classNames?.loadEarlier,
1326
- onClick: /* @__PURE__ */ __name(() => setVisibleCount((count) => count + LOAD_EARLIER_STEP), "onClick")
1327
- }, "Load earlier (", hiddenCount, " more)") : null, visibleMessages.map((message, index) => {
1328
- const absoluteIndex = visibleStart + index;
1329
- const isLastAssistant = absoluteIndex === lastAssistantIndex;
1330
- const usage = getUsage?.(message) ?? null;
1331
- const createdAt = getCreatedAt?.(message) ?? null;
1332
- return /* @__PURE__ */ React3.createElement(MessageItem, {
1333
- key: message.id,
1402
+ const items = useTranscriptItems({
1403
+ messages: messagesRef.current,
1404
+ visibleStart: 0,
1405
+ streamingMessageId: isStreaming ? message.id : null,
1406
+ options
1407
+ });
1408
+ return items[0];
1409
+ }
1410
+ __name(useTranscriptItem, "useTranscriptItem");
1411
+ function useTranscriptItems({ messages, visibleStart, streamingMessageId, options }) {
1412
+ const [drafts, setDrafts] = useState4(() => /* @__PURE__ */ new Map());
1413
+ const [copied, setCopied] = useState4(() => /* @__PURE__ */ new Set());
1414
+ const [openReasoning, setOpenReasoning] = useState4(() => /* @__PURE__ */ new Map());
1415
+ const [picks, setPicks] = useState4(() => /* @__PURE__ */ new Map());
1416
+ const [settling, setSettling] = useState4(() => /* @__PURE__ */ new Set());
1417
+ const [settleErrors, setSettleErrors] = useState4(() => /* @__PURE__ */ new Map());
1418
+ const latest = useRef3({
1419
+ options,
1420
+ drafts,
1421
+ picks,
1422
+ messages
1423
+ });
1424
+ latest.current = {
1425
+ options,
1426
+ drafts,
1427
+ picks,
1428
+ messages
1429
+ };
1430
+ const copyTimers = useRef3(/* @__PURE__ */ new Map());
1431
+ useEffect4(() => () => {
1432
+ for (const timer of copyTimers.current.values()) {
1433
+ clearTimeout(timer);
1434
+ }
1435
+ copyTimers.current.clear();
1436
+ }, []);
1437
+ const callbackCache = useRef3(/* @__PURE__ */ new Map());
1438
+ const reasoningToggleCache = useRef3(/* @__PURE__ */ new Map());
1439
+ const toggleReasoning = useCallback3((key, open) => {
1440
+ setOpenReasoning((current) => {
1441
+ const next = new Map(current);
1442
+ next.set(key, open ?? !(current.get(key) ?? false));
1443
+ return next;
1444
+ });
1445
+ }, []);
1446
+ const stableToggle = useCallback3((key) => {
1447
+ let cached = reasoningToggleCache.current.get(key);
1448
+ if (!cached) {
1449
+ cached = /* @__PURE__ */ __name((open) => toggleReasoning(key, open), "cached");
1450
+ reasoningToggleCache.current.set(key, cached);
1451
+ }
1452
+ return cached;
1453
+ }, [
1454
+ toggleReasoning
1455
+ ]);
1456
+ const pick = useCallback3((toolCallId, questionId, values) => {
1457
+ setPicks((current) => new Map(current).set(pickKey(toolCallId, questionId), values));
1458
+ }, []);
1459
+ const settle = useCallback3((toolCallId, send) => {
1460
+ setSettleErrors((current) => {
1461
+ if (!current.has(toolCallId)) {
1462
+ return current;
1463
+ }
1464
+ const next = new Map(current);
1465
+ next.delete(toolCallId);
1466
+ return next;
1467
+ });
1468
+ setSettling((current) => new Set(current).add(toolCallId));
1469
+ const failed = /* @__PURE__ */ __name((error) => {
1470
+ setSettling((current) => {
1471
+ const next = new Set(current);
1472
+ next.delete(toolCallId);
1473
+ return next;
1474
+ });
1475
+ setSettleErrors((current) => new Map(current).set(toolCallId, error instanceof Error ? error.message : "Could not settle this"));
1476
+ }, "failed");
1477
+ try {
1478
+ void Promise.resolve(send()).catch(failed);
1479
+ } catch (error) {
1480
+ failed(error);
1481
+ }
1482
+ }, []);
1483
+ const answer = useCallback3((toolCallId) => {
1484
+ const current = latest.current;
1485
+ const onAnswer = current.options.onAnswer;
1486
+ if (!onAnswer) {
1487
+ return;
1488
+ }
1489
+ settle(toolCallId, () => onAnswer(toolCallId, answersFor(current.picks, toolCallId)));
1490
+ }, [
1491
+ settle
1492
+ ]);
1493
+ const skip = useCallback3((toolCallId) => {
1494
+ const onSkip = latest.current.options.onSkip;
1495
+ if (onSkip) {
1496
+ settle(toolCallId, () => onSkip(toolCallId));
1497
+ }
1498
+ }, [
1499
+ settle
1500
+ ]);
1501
+ const approve = useCallback3((toolCallId) => {
1502
+ const onApprove = latest.current.options.onApprove;
1503
+ if (onApprove) {
1504
+ settle(toolCallId, () => onApprove(toolCallId));
1505
+ }
1506
+ }, [
1507
+ settle
1508
+ ]);
1509
+ const reject = useCallback3((toolCallId) => {
1510
+ const onReject = latest.current.options.onReject;
1511
+ if (onReject) {
1512
+ settle(toolCallId, () => onReject(toolCallId));
1513
+ }
1514
+ }, [
1515
+ settle
1516
+ ]);
1517
+ const callbacksFor = useCallback3((id) => {
1518
+ const cached = callbackCache.current.get(id);
1519
+ if (cached) {
1520
+ return cached;
1521
+ }
1522
+ const textFor = /* @__PURE__ */ __name(() => extractMessageText(latest.current.messages.find((message) => message.id === id)?.parts), "textFor");
1523
+ const created = {
1524
+ copy: /* @__PURE__ */ __name(() => {
1525
+ const text = textFor();
1526
+ if (!text) {
1527
+ return;
1528
+ }
1529
+ const write = latest.current.options.writeClipboard ?? ((value) => navigator.clipboard.writeText(value));
1530
+ void Promise.resolve().then(() => write(text)).then(() => {
1531
+ setCopied((current) => new Set(current).add(id));
1532
+ const existing = copyTimers.current.get(id);
1533
+ if (existing) {
1534
+ clearTimeout(existing);
1535
+ }
1536
+ copyTimers.current.set(id, setTimeout(() => {
1537
+ copyTimers.current.delete(id);
1538
+ setCopied((current) => {
1539
+ const next = new Set(current);
1540
+ next.delete(id);
1541
+ return next;
1542
+ });
1543
+ }, latest.current.options.copyResetMs ?? DEFAULT_COPY_RESET_MS));
1544
+ }).catch(() => {
1545
+ });
1546
+ }, "copy"),
1547
+ startEdit: /* @__PURE__ */ __name(() => {
1548
+ const text = textFor();
1549
+ setDrafts((current) => new Map(current).set(id, text));
1550
+ }, "startEdit"),
1551
+ cancelEdit: /* @__PURE__ */ __name(() => {
1552
+ setDrafts((current) => {
1553
+ const next = new Map(current);
1554
+ next.delete(id);
1555
+ return next;
1556
+ });
1557
+ }, "cancelEdit"),
1558
+ setDraft: /* @__PURE__ */ __name((value) => {
1559
+ setDrafts((current) => new Map(current).set(id, value));
1560
+ }, "setDraft"),
1561
+ save: /* @__PURE__ */ __name(() => {
1562
+ const next = (latest.current.drafts.get(id) ?? "").trim();
1563
+ if (!next) {
1564
+ return;
1565
+ }
1566
+ created.cancelEdit();
1567
+ void latest.current.options.onEditSubmit?.(id, next);
1568
+ }, "save"),
1569
+ fork: /* @__PURE__ */ __name(() => {
1570
+ void latest.current.options.onFork?.(id);
1571
+ }, "fork"),
1572
+ regenerate: /* @__PURE__ */ __name(() => {
1573
+ void latest.current.options.onRegenerate?.(id);
1574
+ }, "regenerate"),
1575
+ textareaRef: /* @__PURE__ */ __name((element) => {
1576
+ if (!element) {
1577
+ return;
1578
+ }
1579
+ element.focus();
1580
+ element.setSelectionRange(element.value.length, element.value.length);
1581
+ }, "textareaRef"),
1582
+ onTextareaChange: /* @__PURE__ */ __name((event) => {
1583
+ created.setDraft(event.target.value);
1584
+ }, "onTextareaChange"),
1585
+ onTextareaKeyDown: /* @__PURE__ */ __name((event) => {
1586
+ if (event.key === "Enter" && !event.shiftKey) {
1587
+ event.preventDefault();
1588
+ created.save();
1589
+ }
1590
+ if (event.key === "Escape") {
1591
+ event.preventDefault();
1592
+ created.cancelEdit();
1593
+ }
1594
+ }, "onTextareaKeyDown")
1595
+ };
1596
+ callbackCache.current.set(id, created);
1597
+ return created;
1598
+ }, []);
1599
+ const lastAssistantId = findLastAssistant(messages)?.id ?? null;
1600
+ const items = [];
1601
+ for (let index = visibleStart; index < messages.length; index++) {
1602
+ const message = messages[index];
1603
+ if (!message) {
1604
+ continue;
1605
+ }
1606
+ const callbacks = callbacksFor(message.id);
1607
+ const draft = drafts.get(message.id);
1608
+ const isEditing = draft !== void 0;
1609
+ const text = extractMessageText(message.parts);
1610
+ const usage = options.getUsage?.(message) ?? null;
1611
+ const isUser = message.role === "user";
1612
+ const isAssistant = message.role === "assistant";
1613
+ const isLastAssistant = isAssistant && message.id === lastAssistantId;
1614
+ items.push({
1615
+ id: message.id,
1334
1616
  message,
1335
- ...renderToolPart ? {
1336
- renderToolPart
1337
- } : {},
1338
- ...renderToolGroup ? {
1339
- renderToolGroup
1340
- } : {},
1341
- ...renderText ? {
1342
- renderText
1343
- } : {},
1344
- ...classNames?.message ? {
1345
- classNames: classNames.message
1346
- } : {},
1347
- ...onFork ? {
1348
- onFork
1349
- } : {},
1350
- ...editable ? {
1351
- editable
1352
- } : {},
1353
- ...onEditSubmit ? {
1354
- onEditSubmit: /* @__PURE__ */ __name((text) => onEditSubmit(message.id, text), "onEditSubmit")
1355
- } : {},
1356
- ...regeneratable && isLastAssistant ? {
1357
- regeneratable
1358
- } : {},
1359
- ...onRegenerate ? {
1360
- onRegenerate: /* @__PURE__ */ __name(() => onRegenerate(message.id), "onRegenerate")
1361
- } : {},
1362
- ...usage !== null ? {
1363
- usage
1364
- } : {},
1365
- ...createdAt !== null ? {
1366
- createdAt
1367
- } : {},
1368
- isStreaming: status === "streaming" && isLastAssistant
1617
+ role: message.role,
1618
+ index,
1619
+ isUser,
1620
+ isAssistant,
1621
+ isLastAssistant,
1622
+ isStreaming: message.id === streamingMessageId,
1623
+ blocks: buildTranscriptBlocks(message, {
1624
+ isReasoningOpen: /* @__PURE__ */ __name((key, isStreamingRun) => openReasoning.get(key) ?? isStreamingRun, "isReasoningOpen"),
1625
+ toggleReasoning: /* @__PURE__ */ __name((key, open) => stableToggle(key)(open), "toggleReasoning"),
1626
+ ...options.sources !== void 0 ? {
1627
+ sources: options.sources
1628
+ } : {},
1629
+ ...options.onAnswer !== void 0 ? {
1630
+ elicitation: {
1631
+ picked: /* @__PURE__ */ __name((toolCallId, questionId) => picks.get(pickKey(toolCallId, questionId)), "picked"),
1632
+ pick,
1633
+ canAnswer: true,
1634
+ canSkip: options.onSkip !== void 0,
1635
+ answer,
1636
+ skip,
1637
+ isSubmitting: /* @__PURE__ */ __name((toolCallId) => settling.has(toolCallId), "isSubmitting"),
1638
+ errorOf: /* @__PURE__ */ __name((toolCallId) => settleErrors.get(toolCallId) ?? null, "errorOf")
1639
+ }
1640
+ } : {},
1641
+ ...options.onApprove !== void 0 || options.onReject !== void 0 ? {
1642
+ approval: {
1643
+ canApprove: options.onApprove !== void 0,
1644
+ canReject: options.onReject !== void 0,
1645
+ approve,
1646
+ reject,
1647
+ isSubmitting: /* @__PURE__ */ __name((toolCallId) => settling.has(toolCallId), "isSubmitting"),
1648
+ errorOf: /* @__PURE__ */ __name((toolCallId) => settleErrors.get(toolCallId) ?? null, "errorOf")
1649
+ }
1650
+ } : {}
1651
+ }),
1652
+ text,
1653
+ usage: usage ? describeUsage(usage) : null,
1654
+ timestamp: describeTimestamp(options.getCreatedAt?.(message)),
1655
+ copy: {
1656
+ available: text.length > 0,
1657
+ copied: copied.has(message.id),
1658
+ copy: callbacks.copy
1659
+ },
1660
+ edit: {
1661
+ available: isUser && options.editable === true && options.onEditSubmit !== void 0,
1662
+ isEditing,
1663
+ draft: draft ?? text,
1664
+ canSave: (draft ?? "").trim().length > 0,
1665
+ start: callbacks.startEdit,
1666
+ cancel: callbacks.cancelEdit,
1667
+ setDraft: callbacks.setDraft,
1668
+ save: callbacks.save,
1669
+ getTextareaProps: /* @__PURE__ */ __name(() => ({
1670
+ ref: callbacks.textareaRef,
1671
+ value: draft ?? text,
1672
+ onChange: callbacks.onTextareaChange,
1673
+ onKeyDown: callbacks.onTextareaKeyDown
1674
+ }), "getTextareaProps")
1675
+ },
1676
+ fork: {
1677
+ available: options.onFork !== void 0,
1678
+ run: callbacks.fork
1679
+ },
1680
+ regenerate: {
1681
+ available: isLastAssistant && options.regeneratable === true && options.onRegenerate !== void 0,
1682
+ run: callbacks.regenerate
1683
+ }
1369
1684
  });
1370
- }), showTypingBubble ? /* @__PURE__ */ React3.createElement("div", {
1371
- className: classNames?.typing
1372
- }, typingIndicator ?? "Thinking\u2026") : null, error && onRetry ? /* @__PURE__ */ React3.createElement("div", {
1373
- className: classNames?.error
1374
- }, /* @__PURE__ */ React3.createElement("span", null, error.message || "The assistant didn't respond."), /* @__PURE__ */ React3.createElement("button", {
1375
- type: "button",
1376
- className: classNames?.errorRetry,
1377
- onClick: /* @__PURE__ */ __name(() => void onRetry(), "onClick")
1378
- }, "Retry")) : null, showFollowUps ? /* @__PURE__ */ React3.createElement("div", {
1379
- className: classNames?.followUps
1380
- }, followUps.map((text) => /* @__PURE__ */ React3.createElement("button", {
1381
- key: text,
1382
- type: "button",
1383
- className: classNames?.followUpChip,
1384
- onClick: /* @__PURE__ */ __name(() => onFollowUpClick(text), "onClick")
1385
- }, text))) : null);
1685
+ }
1686
+ return items;
1386
1687
  }
1387
- __name(MessageList, "MessageList");
1388
-
1389
- // src/components/speech-languages.ts
1390
- var DEFAULT_SPEECH_LANG = "en-US";
1391
- var SPEECH_LANGUAGES = [
1392
- {
1393
- tag: "en-US",
1394
- label: "English (US)"
1395
- },
1396
- {
1397
- tag: "en-GB",
1398
- label: "English (UK)"
1399
- },
1400
- {
1401
- tag: "pt-BR",
1402
- label: "Portuguese (Brazil)"
1403
- },
1404
- {
1405
- tag: "es-ES",
1406
- label: "Spanish (Spain)"
1688
+ __name(useTranscriptItems, "useTranscriptItems");
1689
+ function pickKey(toolCallId, questionId) {
1690
+ return `${toolCallId}|${questionId}`;
1691
+ }
1692
+ __name(pickKey, "pickKey");
1693
+ function answersFor(picks, toolCallId) {
1694
+ const prefix = `${toolCallId}|`;
1695
+ const answers = {};
1696
+ for (const [key, values] of picks) {
1697
+ if (key.startsWith(prefix)) {
1698
+ answers[key.slice(prefix.length)] = values;
1699
+ }
1407
1700
  }
1408
- ];
1409
- var STORAGE_KEY = "nestjs-agent-chat-speech-lang";
1410
- function loadStoredSpeechLang() {
1411
- if (typeof window === "undefined") return DEFAULT_SPEECH_LANG;
1412
- try {
1413
- const raw = window.localStorage.getItem(STORAGE_KEY);
1414
- if (raw && SPEECH_LANGUAGES.some((language) => language.tag === raw)) {
1415
- return raw;
1701
+ return answers;
1702
+ }
1703
+ __name(answersFor, "answersFor");
1704
+ function findLastAssistant(messages) {
1705
+ for (let index = messages.length - 1; index >= 0; index--) {
1706
+ const message = messages[index];
1707
+ if (message?.role === "assistant") {
1708
+ return message;
1416
1709
  }
1417
- } catch {
1418
1710
  }
1419
- return DEFAULT_SPEECH_LANG;
1711
+ return void 0;
1420
1712
  }
1421
- __name(loadStoredSpeechLang, "loadStoredSpeechLang");
1713
+ __name(findLastAssistant, "findLastAssistant");
1714
+ function contentSignature(messages) {
1715
+ const last = messages.at(-1);
1716
+ if (!last) {
1717
+ return "0";
1718
+ }
1719
+ let length = 0;
1720
+ for (const part of last.parts ?? []) {
1721
+ if ("text" in part && typeof part.text === "string") {
1722
+ length += part.text.length;
1723
+ }
1724
+ }
1725
+ return `${messages.length}|${last.id}|${last.parts?.length ?? 0}|${length}`;
1726
+ }
1727
+ __name(contentSignature, "contentSignature");
1728
+
1729
+ // src/components/message-item.tsx
1730
+ function MessageItem({ message, isStreaming = false, editable, onEditSubmit, regeneratable, onRegenerate, onFork, usage, createdAt, ...slots }) {
1731
+ const item = useTranscriptItem({
1732
+ message,
1733
+ isStreaming,
1734
+ ...editable !== void 0 ? {
1735
+ editable
1736
+ } : {},
1737
+ ...onEditSubmit ? {
1738
+ onEditSubmit: /* @__PURE__ */ __name((_id, next) => onEditSubmit(next), "onEditSubmit")
1739
+ } : {},
1740
+ ...onFork ? {
1741
+ onFork
1742
+ } : {},
1743
+ ...regeneratable !== void 0 ? {
1744
+ regeneratable
1745
+ } : {},
1746
+ ...onRegenerate ? {
1747
+ onRegenerate: /* @__PURE__ */ __name(() => onRegenerate(), "onRegenerate")
1748
+ } : {},
1749
+ ...usage != null ? {
1750
+ getUsage: /* @__PURE__ */ __name(() => usage, "getUsage")
1751
+ } : {},
1752
+ ...createdAt != null ? {
1753
+ getCreatedAt: /* @__PURE__ */ __name(() => createdAt, "getCreatedAt")
1754
+ } : {}
1755
+ });
1756
+ return /* @__PURE__ */ React3.createElement(MessageItemView, {
1757
+ item,
1758
+ ...slots
1759
+ });
1760
+ }
1761
+ __name(MessageItem, "MessageItem");
1762
+ function MessageItemView({ item, ...slots }) {
1763
+ const { classNames } = slots;
1764
+ const actionRow = /* @__PURE__ */ React3.createElement("div", {
1765
+ className: classNames?.actions,
1766
+ "data-actions-for": item.role
1767
+ }, item.copy.available ? /* @__PURE__ */ React3.createElement("button", {
1768
+ type: "button",
1769
+ onClick: item.copy.copy,
1770
+ className: classNames?.actionButton,
1771
+ title: item.copy.copied ? "Copied!" : "Copy message text"
1772
+ }, item.copy.copied ? "Copied" : "Copy") : null, item.fork.available ? /* @__PURE__ */ React3.createElement("button", {
1773
+ type: "button",
1774
+ onClick: item.fork.run,
1775
+ className: classNames?.actionButton,
1776
+ title: "Fork thread from this message"
1777
+ }, "Fork") : null, item.regenerate.available ? /* @__PURE__ */ React3.createElement("button", {
1778
+ type: "button",
1779
+ onClick: item.regenerate.run,
1780
+ className: classNames?.actionButton,
1781
+ title: "Regenerate response"
1782
+ }, "Regenerate") : null, slots.meta, !item.isUser && item.usage ? /* @__PURE__ */ React3.createElement("span", {
1783
+ className: classNames?.usage,
1784
+ title: `Input ${item.usage.inputTokens.toLocaleString()} \xB7 Output ${item.usage.outputTokens.toLocaleString()} tokens`
1785
+ }, item.usage.costLabel, " \xB7 ", item.usage.tokensLabel) : null, item.timestamp ? /* @__PURE__ */ React3.createElement("span", {
1786
+ className: classNames?.timestamp,
1787
+ title: item.timestamp.absolute
1788
+ }, item.timestamp.relative) : null);
1789
+ if (item.edit.available) {
1790
+ return /* @__PURE__ */ React3.createElement(EditableUserBubble, {
1791
+ item,
1792
+ slots,
1793
+ actions: actionRow
1794
+ });
1795
+ }
1796
+ const className = joinClasses(classNames?.root, classNames?.byRole?.[item.role]);
1797
+ return /* @__PURE__ */ React3.createElement("div", {
1798
+ className,
1799
+ "data-role": item.role,
1800
+ "data-streaming": item.isStreaming ? "true" : void 0
1801
+ }, /* @__PURE__ */ React3.createElement("div", null, renderBlocks(item, slots)), actionRow);
1802
+ }
1803
+ __name(MessageItemView, "MessageItemView");
1804
+ function renderBlocks(item, slots) {
1805
+ const nodes = [];
1806
+ for (const block of item.blocks) {
1807
+ if (block.kind === "text") {
1808
+ nodes.push(/* @__PURE__ */ React3.createElement("div", {
1809
+ key: block.key,
1810
+ className: slots.classNames?.text
1811
+ }, slots.renderText ? slots.renderText(block.text, {
1812
+ isStreaming: item.isStreaming
1813
+ }) : block.text));
1814
+ continue;
1815
+ }
1816
+ if (block.kind === "reasoning") {
1817
+ nodes.push(renderReasoningBlock(block, slots));
1818
+ continue;
1819
+ }
1820
+ if (block.kind === "files") {
1821
+ nodes.push(renderFilesBlock(block, slots));
1822
+ continue;
1823
+ }
1824
+ if (block.kind === "tools") {
1825
+ nodes.push(...renderToolBlock(item, block, slots));
1826
+ }
1827
+ }
1828
+ return nodes;
1829
+ }
1830
+ __name(renderBlocks, "renderBlocks");
1831
+ function renderFilesBlock(block, slots) {
1832
+ return /* @__PURE__ */ React3.createElement("div", {
1833
+ key: block.key,
1834
+ className: slots.classNames?.files,
1835
+ "data-files": "true"
1836
+ }, slots.renderFiles ? slots.renderFiles(block.files) : block.files.map((file) => /* @__PURE__ */ React3.createElement("a", {
1837
+ key: file.url,
1838
+ href: file.url,
1839
+ target: "_blank",
1840
+ rel: "noreferrer",
1841
+ className: slots.classNames?.file,
1842
+ title: file.filename ?? file.mediaType
1843
+ }, file.isImage ? /* @__PURE__ */ React3.createElement("img", {
1844
+ src: file.url,
1845
+ alt: file.filename ?? "image",
1846
+ className: slots.classNames?.fileImage
1847
+ }) : file.filename ?? file.mediaType)));
1848
+ }
1849
+ __name(renderFilesBlock, "renderFilesBlock");
1850
+ function renderReasoningBlock(block, slots) {
1851
+ return /* @__PURE__ */ React3.createElement("div", {
1852
+ key: block.key,
1853
+ className: slots.classNames?.reasoning,
1854
+ "data-reasoning": "true",
1855
+ "data-streaming": block.isStreaming ? "true" : void 0
1856
+ }, /* @__PURE__ */ React3.createElement("button", {
1857
+ type: "button",
1858
+ className: slots.classNames?.reasoningToggle,
1859
+ onClick: /* @__PURE__ */ __name(() => block.toggle(), "onClick"),
1860
+ "aria-expanded": block.isOpen
1861
+ }, slots.reasoningLabel ?? "Reasoning"), block.isOpen ? /* @__PURE__ */ React3.createElement("div", {
1862
+ className: slots.classNames?.reasoningText
1863
+ }, slots.renderReasoning ? slots.renderReasoning(block.text, {
1864
+ isStreaming: block.isStreaming,
1865
+ isOpen: block.isOpen
1866
+ }) : block.text) : null);
1867
+ }
1868
+ __name(renderReasoningBlock, "renderReasoningBlock");
1869
+ function renderToolBlock(item, block, slots) {
1870
+ if (slots.renderToolGroup) {
1871
+ return [
1872
+ /* @__PURE__ */ React3.createElement(React3.Fragment, {
1873
+ key: block.key
1874
+ }, slots.renderToolGroup(block.parts, block.key))
1875
+ ];
1876
+ }
1877
+ if (!slots.renderToolPart) {
1878
+ return [];
1879
+ }
1880
+ const renderToolPart = slots.renderToolPart;
1881
+ return block.parts.map((part) => {
1882
+ const partKey = `${item.id}-${part.toolCallId}`;
1883
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, {
1884
+ key: partKey
1885
+ }, renderToolPart(part, partKey));
1886
+ });
1887
+ }
1888
+ __name(renderToolBlock, "renderToolBlock");
1889
+ function EditableUserBubble({ item, slots, actions }) {
1890
+ const { classNames } = slots;
1891
+ const { edit } = item;
1892
+ if (!edit.isEditing) {
1893
+ return /* @__PURE__ */ React3.createElement("div", {
1894
+ className: joinClasses(classNames?.root, classNames?.byRole?.user),
1895
+ "data-role": "user"
1896
+ }, /* @__PURE__ */ React3.createElement("div", {
1897
+ className: classNames?.text
1898
+ }, slots.renderText ? slots.renderText(item.text, {
1899
+ isStreaming: false
1900
+ }) : item.text), /* @__PURE__ */ React3.createElement("button", {
1901
+ type: "button",
1902
+ onClick: edit.start,
1903
+ className: classNames?.actionButton,
1904
+ title: "Edit & resubmit"
1905
+ }, "Edit"), actions);
1906
+ }
1907
+ const textareaProps = edit.getTextareaProps();
1908
+ return /* @__PURE__ */ React3.createElement("div", {
1909
+ "data-editing": "true"
1910
+ }, /* @__PURE__ */ React3.createElement("textarea", {
1911
+ ...textareaProps,
1912
+ rows: Math.min(8, edit.draft.split("\n").length + 1),
1913
+ className: classNames?.editTextarea
1914
+ }), /* @__PURE__ */ React3.createElement("div", {
1915
+ className: classNames?.editActions
1916
+ }, /* @__PURE__ */ React3.createElement("button", {
1917
+ type: "button",
1918
+ onClick: edit.cancel
1919
+ }, "Cancel"), /* @__PURE__ */ React3.createElement("button", {
1920
+ type: "button",
1921
+ onClick: edit.save,
1922
+ disabled: !edit.canSave
1923
+ }, "Resend")));
1924
+ }
1925
+ __name(EditableUserBubble, "EditableUserBubble");
1926
+ function joinClasses(...entries) {
1927
+ const joined = entries.filter((entry) => entry).join(" ");
1928
+ return joined === "" ? void 0 : joined;
1929
+ }
1930
+ __name(joinClasses, "joinClasses");
1931
+
1932
+ // src/components/message-list.tsx
1933
+ function MessageList({ messages, status, renderToolPart, renderToolGroup, renderText, renderReasoning, renderFiles, reasoningLabel, getMeta, onFork, editable, onEditSubmit, regeneratable, onRegenerate, getUsage, getCreatedAt, emptyState, typingIndicator, followUps, onFollowUpClick, error, onRetry, classNames }) {
1934
+ const transcript = useChatTranscript({
1935
+ messages,
1936
+ status,
1937
+ ...onFork ? {
1938
+ onFork
1939
+ } : {},
1940
+ ...editable !== void 0 ? {
1941
+ editable
1942
+ } : {},
1943
+ ...onEditSubmit ? {
1944
+ onEditSubmit
1945
+ } : {},
1946
+ ...regeneratable !== void 0 ? {
1947
+ regeneratable
1948
+ } : {},
1949
+ ...onRegenerate ? {
1950
+ onRegenerate
1951
+ } : {},
1952
+ ...getUsage ? {
1953
+ getUsage
1954
+ } : {},
1955
+ ...getCreatedAt ? {
1956
+ getCreatedAt
1957
+ } : {},
1958
+ ...followUps !== void 0 ? {
1959
+ followUps
1960
+ } : {}
1961
+ });
1962
+ if (transcript.showEmptyState) {
1963
+ return /* @__PURE__ */ React.createElement("div", {
1964
+ className: classNames?.empty
1965
+ }, emptyState);
1966
+ }
1967
+ const slots = {
1968
+ ...renderToolPart ? {
1969
+ renderToolPart
1970
+ } : {},
1971
+ ...renderToolGroup ? {
1972
+ renderToolGroup
1973
+ } : {},
1974
+ ...renderText ? {
1975
+ renderText
1976
+ } : {},
1977
+ ...renderReasoning ? {
1978
+ renderReasoning
1979
+ } : {},
1980
+ ...renderFiles ? {
1981
+ renderFiles
1982
+ } : {},
1983
+ ...reasoningLabel !== void 0 ? {
1984
+ reasoningLabel
1985
+ } : {},
1986
+ ...classNames?.message ? {
1987
+ classNames: classNames.message
1988
+ } : {}
1989
+ };
1990
+ return /* @__PURE__ */ React.createElement("div", {
1991
+ className: classNames?.root
1992
+ }, transcript.window.canLoadEarlier ? /* @__PURE__ */ React.createElement("button", {
1993
+ type: "button",
1994
+ className: classNames?.loadEarlier,
1995
+ onClick: transcript.window.loadEarlier
1996
+ }, "Load earlier (", transcript.window.hiddenCount, " more)") : null, transcript.items.map((item) => /* @__PURE__ */ React.createElement(MessageItemView, {
1997
+ key: item.id,
1998
+ item,
1999
+ ...slots,
2000
+ ...getMeta ? {
2001
+ meta: getMeta(item.message)
2002
+ } : {}
2003
+ })), transcript.showTypingIndicator ? /* @__PURE__ */ React.createElement("div", {
2004
+ className: classNames?.typing
2005
+ }, typingIndicator ?? "Thinking\u2026") : null, error && onRetry ? /* @__PURE__ */ React.createElement("div", {
2006
+ className: classNames?.error
2007
+ }, /* @__PURE__ */ React.createElement("span", null, error.message || "The assistant didn't respond."), /* @__PURE__ */ React.createElement("button", {
2008
+ type: "button",
2009
+ className: classNames?.errorRetry,
2010
+ onClick: /* @__PURE__ */ __name(() => void onRetry(), "onClick")
2011
+ }, "Retry")) : null, transcript.showFollowUps && onFollowUpClick && followUps ? /* @__PURE__ */ React.createElement("div", {
2012
+ className: classNames?.followUps
2013
+ }, followUps.map((text) => /* @__PURE__ */ React.createElement("button", {
2014
+ key: text,
2015
+ type: "button",
2016
+ className: classNames?.followUpChip,
2017
+ onClick: /* @__PURE__ */ __name(() => onFollowUpClick(text), "onClick")
2018
+ }, text))) : null);
2019
+ }
2020
+ __name(MessageList, "MessageList");
2021
+
2022
+ // src/components/speech-languages.ts
2023
+ var DEFAULT_SPEECH_LANG = "en-US";
2024
+ var SPEECH_LANGUAGES = [
2025
+ {
2026
+ tag: "en-US",
2027
+ label: "English (US)"
2028
+ },
2029
+ {
2030
+ tag: "en-GB",
2031
+ label: "English (UK)"
2032
+ },
2033
+ {
2034
+ tag: "pt-BR",
2035
+ label: "Portuguese (Brazil)"
2036
+ },
2037
+ {
2038
+ tag: "es-ES",
2039
+ label: "Spanish (Spain)"
2040
+ }
2041
+ ];
2042
+ var STORAGE_KEY = "nestjs-agent-chat-speech-lang";
2043
+ function loadStoredSpeechLang() {
2044
+ if (typeof window === "undefined") return DEFAULT_SPEECH_LANG;
2045
+ try {
2046
+ const raw = window.localStorage.getItem(STORAGE_KEY);
2047
+ if (raw && SPEECH_LANGUAGES.some((language) => language.tag === raw)) {
2048
+ return raw;
2049
+ }
2050
+ } catch {
2051
+ }
2052
+ return DEFAULT_SPEECH_LANG;
2053
+ }
2054
+ __name(loadStoredSpeechLang, "loadStoredSpeechLang");
1422
2055
  function persistSpeechLang(tag) {
1423
2056
  if (typeof window === "undefined") return;
1424
2057
  try {
@@ -1427,6 +2060,776 @@ function persistSpeechLang(tag) {
1427
2060
  }
1428
2061
  }
1429
2062
  __name(persistSpeechLang, "persistSpeechLang");
2063
+
2064
+ // src/composer/model.ts
2065
+ var WHITESPACE = /\s/;
2066
+ function findActiveTrigger(text, caret, sources) {
2067
+ const end = Math.max(0, Math.min(caret, text.length));
2068
+ for (let index = end - 1; index >= 0; index--) {
2069
+ const char = text[index];
2070
+ if (char === void 0 || WHITESPACE.test(char)) {
2071
+ return null;
2072
+ }
2073
+ const source = sources.find((candidate) => candidate.trigger === char);
2074
+ if (source && isAllowedAt(text, index, source.position)) {
2075
+ return {
2076
+ source,
2077
+ index,
2078
+ query: text.slice(index + 1, end)
2079
+ };
2080
+ }
2081
+ }
2082
+ return null;
2083
+ }
2084
+ __name(findActiveTrigger, "findActiveTrigger");
2085
+ function isAllowedAt(text, index, position) {
2086
+ if (index === 0) {
2087
+ return true;
2088
+ }
2089
+ if (position === "start") {
2090
+ return false;
2091
+ }
2092
+ const previous = text[index - 1];
2093
+ return previous !== void 0 && WHITESPACE.test(previous);
2094
+ }
2095
+ __name(isAllowedAt, "isAllowedAt");
2096
+ function filterAutocompleteItems(items, query) {
2097
+ const needle = query.trim().toLowerCase();
2098
+ return items.filter((item) => item.label.toLowerCase().includes(needle) || (item.description?.toLowerCase().includes(needle) ?? false));
2099
+ }
2100
+ __name(filterAutocompleteItems, "filterAutocompleteItems");
2101
+ function applyCompletion(text, caret, match, item) {
2102
+ const end = Math.max(match.index, Math.min(caret, text.length));
2103
+ const inserted = `${match.source.trigger}${item.value ?? item.label}${match.source.insertSuffix ?? " "}`;
2104
+ return {
2105
+ text: text.slice(0, match.index) + inserted + text.slice(end),
2106
+ caret: match.index + inserted.length
2107
+ };
2108
+ }
2109
+ __name(applyCompletion, "applyCompletion");
2110
+
2111
+ // src/composer/skills-source.ts
2112
+ function createSkillsSource(options) {
2113
+ const byThread = /* @__PURE__ */ new Map();
2114
+ return {
2115
+ id: "skills",
2116
+ label: options.label ?? "Skills",
2117
+ trigger: options.trigger ?? "/",
2118
+ position: "start",
2119
+ getItems: /* @__PURE__ */ __name(() => {
2120
+ const threadId = options.getThreadId?.();
2121
+ const key = threadId ?? "";
2122
+ const cached = byThread.get(key);
2123
+ if (cached) {
2124
+ return cached;
2125
+ }
2126
+ const pending = options.client.listSkills(threadId).then((entries) => entries.map(toItem)).catch((error) => {
2127
+ byThread.delete(key);
2128
+ throw error;
2129
+ });
2130
+ byThread.set(key, pending);
2131
+ return pending;
2132
+ }, "getItems")
2133
+ };
2134
+ }
2135
+ __name(createSkillsSource, "createSkillsSource");
2136
+ function toItem(entry) {
2137
+ const data = {
2138
+ scope: entry.scope,
2139
+ ...entry.shadows !== void 0 ? {
2140
+ shadows: entry.shadows
2141
+ } : {}
2142
+ };
2143
+ return {
2144
+ id: entry.name,
2145
+ label: entry.name,
2146
+ description: entry.description,
2147
+ // `shadows` is present only on a conflict, so saying "overrides" is never noise: without it a
2148
+ // user cannot tell "there is no org default" from "there is one and mine wins".
2149
+ hint: entry.shadows === void 0 ? entry.scope : `${entry.scope} \xB7 overrides ${entry.shadows.join(", ")}`,
2150
+ data
2151
+ };
2152
+ }
2153
+ __name(toItem, "toItem");
2154
+
2155
+ // src/composer/use-composer-autocomplete.ts
2156
+ import { useCallback as useCallback4, useEffect as useEffect5, useId, useLayoutEffect, useMemo as useMemo2, useRef as useRef4, useState as useState5 } from "react";
2157
+ function useComposerAutocomplete(options) {
2158
+ const { value, sources, acceptOnTab = true } = options;
2159
+ const latest = useRef4(options);
2160
+ latest.current = options;
2161
+ const inputRef = useRef4(null);
2162
+ const [caret, setCaret] = useState5(value.length);
2163
+ const [items, setItems] = useState5([]);
2164
+ const [isLoading, setLoading] = useState5(false);
2165
+ const [error, setError] = useState5(null);
2166
+ const [activeIndex, setActiveIndex] = useState5(0);
2167
+ const [dismissedIndex, setDismissedIndex] = useState5(null);
2168
+ const safeCaret = Math.min(caret, value.length);
2169
+ const match = useMemo2(() => findActiveTrigger(value, safeCaret, sources), [
2170
+ value,
2171
+ safeCaret,
2172
+ sources
2173
+ ]);
2174
+ const isActive = match !== null && dismissedIndex !== match.index;
2175
+ const matchRef = useRef4(match);
2176
+ matchRef.current = match;
2177
+ const caretRef = useRef4(safeCaret);
2178
+ caretRef.current = safeCaret;
2179
+ const itemsRef = useRef4(items);
2180
+ itemsRef.current = items;
2181
+ const activeIndexRef = useRef4(activeIndex);
2182
+ activeIndexRef.current = activeIndex;
2183
+ const requestKey = isActive && match ? `${match.source.id}\0${match.query}` : null;
2184
+ useEffect5(() => {
2185
+ const current = matchRef.current;
2186
+ if (requestKey === null || current === null) {
2187
+ setItems([]);
2188
+ setLoading(false);
2189
+ setError(null);
2190
+ return;
2191
+ }
2192
+ const { source, query } = current;
2193
+ const controller = new AbortController();
2194
+ let superseded = false;
2195
+ const settle = /* @__PURE__ */ __name((next) => {
2196
+ if (superseded) return;
2197
+ setItems((source.filter ?? filterAutocompleteItems)(next, query));
2198
+ setActiveIndex(0);
2199
+ setLoading(false);
2200
+ setError(null);
2201
+ }, "settle");
2202
+ const failed = /* @__PURE__ */ __name((cause) => {
2203
+ if (superseded) return;
2204
+ setItems([]);
2205
+ setLoading(false);
2206
+ setError(cause instanceof Error ? cause.message : "Could not load suggestions");
2207
+ latest.current.onError?.(cause, source);
2208
+ }, "failed");
2209
+ let result;
2210
+ try {
2211
+ result = source.getItems(query, controller.signal);
2212
+ } catch (cause) {
2213
+ failed(cause);
2214
+ return;
2215
+ }
2216
+ if (!isPromise(result)) {
2217
+ settle(result);
2218
+ return;
2219
+ }
2220
+ setLoading(true);
2221
+ setError(null);
2222
+ result.then(settle, failed);
2223
+ return () => {
2224
+ superseded = true;
2225
+ controller.abort();
2226
+ };
2227
+ }, [
2228
+ requestKey
2229
+ ]);
2230
+ const isOpen = isActive;
2231
+ const activeItem = isOpen ? items[activeIndex] ?? null : null;
2232
+ const reactId = useId();
2233
+ const listboxId = options.id ?? `agent-autocomplete-${reactId}`;
2234
+ const optionId = useCallback4((index) => `${listboxId}-option-${index}`, [
2235
+ listboxId
2236
+ ]);
2237
+ const highlight = useCallback4((index) => {
2238
+ const total = itemsRef.current.length;
2239
+ if (total === 0) return;
2240
+ setActiveIndex((index % total + total) % total);
2241
+ }, []);
2242
+ const moveHighlight = useCallback4((delta) => highlight(activeIndexRef.current + delta), [
2243
+ highlight
2244
+ ]);
2245
+ const dismiss = useCallback4(() => {
2246
+ const current = matchRef.current;
2247
+ if (current) setDismissedIndex(current.index);
2248
+ }, []);
2249
+ const pendingCaret = useRef4(null);
2250
+ const accept = useCallback4((item) => {
2251
+ const current = matchRef.current;
2252
+ if (!current) return;
2253
+ const chosen = item ?? itemsRef.current[activeIndexRef.current];
2254
+ if (!chosen) return;
2255
+ const edit = applyCompletion(latest.current.value, caretRef.current, current, chosen);
2256
+ latest.current.onValueChange(edit.text);
2257
+ setCaret(edit.caret);
2258
+ setDismissedIndex(null);
2259
+ pendingCaret.current = edit.caret;
2260
+ }, []);
2261
+ useLayoutEffect(() => {
2262
+ const position = pendingCaret.current;
2263
+ if (position === null) return;
2264
+ pendingCaret.current = null;
2265
+ inputRef.current?.setSelectionRange(position, position);
2266
+ });
2267
+ const readCaret = useCallback4((element) => {
2268
+ setCaret(element.selectionStart ?? element.value.length);
2269
+ }, []);
2270
+ const onKeyDown = useCallback4((event) => {
2271
+ if (!isOpen) return;
2272
+ const hasItems = itemsRef.current.length > 0;
2273
+ switch (event.key) {
2274
+ case "ArrowDown":
2275
+ if (!hasItems) return;
2276
+ event.preventDefault();
2277
+ moveHighlight(1);
2278
+ return;
2279
+ case "ArrowUp":
2280
+ if (!hasItems) return;
2281
+ event.preventDefault();
2282
+ moveHighlight(-1);
2283
+ return;
2284
+ case "Enter":
2285
+ if (!hasItems) return;
2286
+ event.preventDefault();
2287
+ accept();
2288
+ return;
2289
+ case "Tab":
2290
+ if (!acceptOnTab || event.shiftKey || !hasItems) return;
2291
+ event.preventDefault();
2292
+ accept();
2293
+ return;
2294
+ case "Escape":
2295
+ event.preventDefault();
2296
+ dismiss();
2297
+ return;
2298
+ default:
2299
+ return;
2300
+ }
2301
+ }, [
2302
+ isOpen,
2303
+ acceptOnTab,
2304
+ accept,
2305
+ dismiss,
2306
+ moveHighlight
2307
+ ]);
2308
+ const onChange = useCallback4((event) => {
2309
+ readCaret(event.target);
2310
+ const next = findActiveTrigger(event.target.value, event.target.selectionStart ?? event.target.value.length, latest.current.sources);
2311
+ setDismissedIndex((current) => current !== null && next?.index === current ? current : null);
2312
+ }, [
2313
+ readCaret
2314
+ ]);
2315
+ const onSelect = useCallback4((event) => readCaret(event.currentTarget), [
2316
+ readCaret
2317
+ ]);
2318
+ const onClick = useCallback4((event) => readCaret(event.currentTarget), [
2319
+ readCaret
2320
+ ]);
2321
+ const setInput = useCallback4((element) => {
2322
+ inputRef.current = element;
2323
+ }, []);
2324
+ const getInputProps = useCallback4(() => ({
2325
+ ref: setInput,
2326
+ onChange,
2327
+ onKeyDown,
2328
+ onSelect,
2329
+ onClick,
2330
+ role: "combobox",
2331
+ "aria-expanded": isOpen,
2332
+ "aria-autocomplete": "list",
2333
+ "aria-haspopup": "listbox",
2334
+ ...isOpen ? {
2335
+ "aria-controls": listboxId
2336
+ } : {},
2337
+ ...activeItem ? {
2338
+ "aria-activedescendant": optionId(activeIndex)
2339
+ } : {}
2340
+ }), [
2341
+ setInput,
2342
+ onChange,
2343
+ onKeyDown,
2344
+ onSelect,
2345
+ onClick,
2346
+ isOpen,
2347
+ listboxId,
2348
+ activeItem,
2349
+ activeIndex,
2350
+ optionId
2351
+ ]);
2352
+ const getListboxProps = useCallback4(() => ({
2353
+ id: listboxId,
2354
+ role: "listbox",
2355
+ "aria-label": match?.source.label ?? match?.source.id ?? "Suggestions"
2356
+ }), [
2357
+ listboxId,
2358
+ match
2359
+ ]);
2360
+ const getOptionProps = useCallback4((index) => ({
2361
+ id: optionId(index),
2362
+ role: "option",
2363
+ "aria-selected": index === activeIndex
2364
+ }), [
2365
+ optionId,
2366
+ activeIndex
2367
+ ]);
2368
+ return {
2369
+ isOpen,
2370
+ source: isOpen && match ? match.source : null,
2371
+ trigger: isOpen && match ? match.source.trigger : null,
2372
+ query: match?.query ?? "",
2373
+ items,
2374
+ activeIndex,
2375
+ activeItem,
2376
+ isLoading,
2377
+ error,
2378
+ highlight,
2379
+ moveHighlight,
2380
+ accept,
2381
+ dismiss,
2382
+ getInputProps,
2383
+ getListboxProps,
2384
+ getOptionProps
2385
+ };
2386
+ }
2387
+ __name(useComposerAutocomplete, "useComposerAutocomplete");
2388
+ function isPromise(value) {
2389
+ return typeof value.then === "function";
2390
+ }
2391
+ __name(isPromise, "isPromise");
2392
+
2393
+ // src/stored-message-to-ui-message.ts
2394
+ function storedMessageToUiMessage(message) {
2395
+ const parts = [];
2396
+ if (message.content) {
2397
+ parts.push({
2398
+ type: "text",
2399
+ text: message.content
2400
+ });
2401
+ }
2402
+ for (const attachment of message.attachments ?? []) {
2403
+ parts.push({
2404
+ type: "file",
2405
+ mediaType: attachment.contentType,
2406
+ filename: attachment.name,
2407
+ url: attachment.url
2408
+ });
2409
+ }
2410
+ for (const call of message.toolCalls ?? []) {
2411
+ const result = message.toolResults?.find((candidate) => candidate.id === call.id);
2412
+ const toolKind2 = call.kind;
2413
+ parts.push({
2414
+ type: `tool-${call.name}`,
2415
+ toolCallId: call.id,
2416
+ ...toolKind2 !== void 0 ? {
2417
+ toolMetadata: {
2418
+ toolKind: toolKind2
2419
+ }
2420
+ } : {},
2421
+ ...result !== void 0 ? {
2422
+ state: "output-available",
2423
+ input: call.input,
2424
+ output: result.output
2425
+ } : {
2426
+ state: "input-available",
2427
+ input: call.input
2428
+ }
2429
+ });
2430
+ }
2431
+ return {
2432
+ id: message.id,
2433
+ role: message.role,
2434
+ parts
2435
+ };
2436
+ }
2437
+ __name(storedMessageToUiMessage, "storedMessageToUiMessage");
2438
+
2439
+ // src/stored-thread-to-ui-messages.ts
2440
+ function storedThreadToUiMessages(messages) {
2441
+ const turns = [];
2442
+ for (const message of messages) {
2443
+ const currentTurn = turns.at(-1);
2444
+ const previousRow = currentTurn?.at(-1);
2445
+ if (message.role === "assistant" && previousRow?.role === "assistant" && currentTurn && sameRun(previousRow, message)) {
2446
+ currentTurn.push(message);
2447
+ } else {
2448
+ turns.push([
2449
+ message
2450
+ ]);
2451
+ }
2452
+ }
2453
+ return turns.map(mergeTurn);
2454
+ }
2455
+ __name(storedThreadToUiMessages, "storedThreadToUiMessages");
2456
+ function sameRun(previous, next) {
2457
+ return previous.runId === void 0 || next.runId === void 0 || previous.runId === next.runId;
2458
+ }
2459
+ __name(sameRun, "sameRun");
2460
+ function mergeTurn(rows) {
2461
+ const lastRow = rows.at(-1);
2462
+ if (lastRow === void 0) {
2463
+ throw new Error("storedThreadToUiMessages: encountered an empty turn");
2464
+ }
2465
+ if (rows.length === 1) {
2466
+ return storedMessageToUiMessage(lastRow);
2467
+ }
2468
+ const parts = rows.flatMap((row) => storedMessageToUiMessage(row).parts);
2469
+ const usage = sumUsage(rows);
2470
+ return {
2471
+ id: lastRow.id,
2472
+ role: lastRow.role,
2473
+ parts,
2474
+ ...usage !== void 0 ? {
2475
+ metadata: {
2476
+ usage
2477
+ }
2478
+ } : {}
2479
+ };
2480
+ }
2481
+ __name(mergeTurn, "mergeTurn");
2482
+ function sumUsage(rows) {
2483
+ const usages = rows.map((row) => row.usage).filter((usage) => usage !== void 0);
2484
+ if (usages.length === 0) {
2485
+ return void 0;
2486
+ }
2487
+ const allCostsUnknown = usages.every((usage) => usage.costUsd === null || usage.costUsd === void 0);
2488
+ return {
2489
+ inputTokens: usages.reduce((sum, usage) => sum + usage.inputTokens, 0),
2490
+ outputTokens: usages.reduce((sum, usage) => sum + usage.outputTokens, 0),
2491
+ costUsd: allCostsUnknown ? null : usages.reduce((sum, usage) => sum + (usage.costUsd ?? 0), 0)
2492
+ };
2493
+ }
2494
+ __name(sumUsage, "sumUsage");
2495
+
2496
+ // src/use-agent-chat.ts
2497
+ import { useChat } from "@ai-sdk/react";
2498
+ import { useCallback as useCallback5, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef5, useState as useState6 } from "react";
2499
+ function useAgentChat(options) {
2500
+ const latest = useRef5(options);
2501
+ latest.current = options;
2502
+ const [runId, setRunId] = useState6(options.resumeRunId);
2503
+ const runIdRef = useRef5(runId);
2504
+ runIdRef.current = runId;
2505
+ const settlingRunIdRef = useRef5(void 0);
2506
+ const autoResumeRunIdRef = useRef5(void 0);
2507
+ const [autoResume, setAutoResume] = useState6(false);
2508
+ const [activeRunId, setActiveRunId] = useState6(null);
2509
+ const regenerateNext = useRef5(false);
2510
+ const createdThreadId = useRef5(void 0);
2511
+ const client = useMemo3(() => {
2512
+ if (options.client) return options.client;
2513
+ return new AgentClient({
2514
+ ...options.baseUrl !== void 0 ? {
2515
+ baseUrl: options.baseUrl
2516
+ } : {},
2517
+ ...options.credentials !== void 0 ? {
2518
+ credentials: options.credentials
2519
+ } : {},
2520
+ ...options.fetch !== void 0 ? {
2521
+ fetch: options.fetch
2522
+ } : {},
2523
+ getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders")
2524
+ });
2525
+ }, []);
2526
+ useEffect6(() => {
2527
+ if (!options.resume || options.threadId === void 0) return;
2528
+ let cancelled = false;
2529
+ const threadId = options.threadId;
2530
+ void client.getThread(threadId).then((thread) => {
2531
+ if (cancelled) return;
2532
+ setActiveRunId(thread.activeRunId ?? null);
2533
+ if (thread.activeRunId != null) {
2534
+ autoResumeRunIdRef.current = thread.activeRunId;
2535
+ setAutoResume(true);
2536
+ }
2537
+ });
2538
+ return () => {
2539
+ cancelled = true;
2540
+ };
2541
+ }, [
2542
+ client,
2543
+ options.resume,
2544
+ options.threadId
2545
+ ]);
2546
+ const transport = useMemo3(() => {
2547
+ function onMeta(meta) {
2548
+ settlingRunIdRef.current = meta.runId;
2549
+ setRunId(meta.runId);
2550
+ if (latest.current.threadId === void 0 && meta.threadId && createdThreadId.current !== meta.threadId) {
2551
+ createdThreadId.current = meta.threadId;
2552
+ latest.current.onThreadCreated?.(meta.threadId);
2553
+ }
2554
+ }
2555
+ __name(onMeta, "onMeta");
2556
+ return new AgentChatTransport({
2557
+ ...options.baseUrl !== void 0 ? {
2558
+ baseUrl: options.baseUrl
2559
+ } : {},
2560
+ ...options.credentials !== void 0 ? {
2561
+ credentials: options.credentials
2562
+ } : {},
2563
+ ...options.fetch !== void 0 ? {
2564
+ fetch: options.fetch
2565
+ } : {},
2566
+ ...options.agent !== void 0 ? {
2567
+ agent: options.agent
2568
+ } : {},
2569
+ getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders"),
2570
+ getBody: /* @__PURE__ */ __name(() => {
2571
+ const current = latest.current;
2572
+ const pageContext = current.getPageContext?.() ?? null;
2573
+ const regenerate2 = regenerateNext.current;
2574
+ regenerateNext.current = false;
2575
+ const threadId = current.threadId ?? createdThreadId.current;
2576
+ return {
2577
+ ...threadId !== void 0 ? {
2578
+ threadId
2579
+ } : {},
2580
+ ...pageContext ? {
2581
+ pageContext
2582
+ } : {},
2583
+ ...regenerate2 ? {
2584
+ regenerate: true
2585
+ } : {}
2586
+ };
2587
+ }, "getBody"),
2588
+ getResumeRunId: /* @__PURE__ */ __name(() => latest.current.resumeRunId ?? autoResumeRunIdRef.current, "getResumeRunId"),
2589
+ onMeta,
2590
+ onAttemptStart: /* @__PURE__ */ __name(() => {
2591
+ settlingRunIdRef.current = void 0;
2592
+ }, "onAttemptStart")
2593
+ });
2594
+ }, []);
2595
+ const backgroundRef = useRef5(() => Promise.resolve());
2596
+ const chat = useChat({
2597
+ transport,
2598
+ resume: options.resumeRunId !== void 0 || autoResume,
2599
+ ...options.threadId !== void 0 ? {
2600
+ id: options.threadId
2601
+ } : {},
2602
+ ...options.initialMessages !== void 0 ? {
2603
+ messages: options.initialMessages
2604
+ } : {},
2605
+ onFinish: /* @__PURE__ */ __name(({ isError }) => {
2606
+ latest.current.onFinish?.();
2607
+ if (latest.current.background === true) {
2608
+ void backgroundRef.current();
2609
+ }
2610
+ const settledRunId = settlingRunIdRef.current;
2611
+ if (settledRunId !== void 0) {
2612
+ latest.current.onRunSettled?.({
2613
+ runId: settledRunId,
2614
+ status: isError ? "failed" : "completed"
2615
+ });
2616
+ }
2617
+ }, "onFinish")
2618
+ });
2619
+ const chatRef = useRef5(chat);
2620
+ chatRef.current = chat;
2621
+ const turnInFlight = useRef5(false);
2622
+ const isTurnInFlight = useCallback5(() => turnInFlight.current || transport.isAttemptLive, [
2623
+ transport
2624
+ ]);
2625
+ const sendMessage = useCallback5(async (...args) => {
2626
+ if (isTurnInFlight()) return;
2627
+ turnInFlight.current = true;
2628
+ try {
2629
+ await chatRef.current.sendMessage(...args);
2630
+ } finally {
2631
+ turnInFlight.current = false;
2632
+ }
2633
+ }, [
2634
+ isTurnInFlight
2635
+ ]);
2636
+ const addToolResult = useCallback5(({ tool, toolCallId, output }) => chatRef.current.addToolResult({
2637
+ tool,
2638
+ toolCallId,
2639
+ output
2640
+ }), []);
2641
+ const [threads, setThreads] = useState6([]);
2642
+ const [quota, setQuota] = useState6(null);
2643
+ const [backgroundRuns, setBackgroundRuns] = useState6([]);
2644
+ const refreshBackground = useCallback5(async () => {
2645
+ const threadId = latest.current.threadId ?? createdThreadId.current;
2646
+ if (threadId === void 0) {
2647
+ return;
2648
+ }
2649
+ const thread = await client.getThread(threadId);
2650
+ const runs = backgroundRunsFromThread(thread.messages);
2651
+ setBackgroundRuns(runs);
2652
+ const landed = runs.map((run) => run.message).filter((message) => message !== void 0);
2653
+ if (landed.length === 0) {
2654
+ return;
2655
+ }
2656
+ chatRef.current.setMessages((current) => {
2657
+ const known = new Set(current.map((message) => message.id));
2658
+ const missing = landed.filter((message) => !known.has(message.id));
2659
+ return missing.length === 0 ? current : [
2660
+ ...current,
2661
+ ...missing.map((message) => storedMessageToUiMessage(message))
2662
+ ];
2663
+ });
2664
+ }, [
2665
+ client
2666
+ ]);
2667
+ backgroundRef.current = refreshBackground;
2668
+ const isWorking = backgroundRuns.some((run) => run.status === "running");
2669
+ useEffect6(() => {
2670
+ if (options.background !== true || options.threadId === void 0) {
2671
+ return;
2672
+ }
2673
+ void refreshBackground();
2674
+ }, [
2675
+ options.background,
2676
+ options.threadId,
2677
+ refreshBackground
2678
+ ]);
2679
+ useEffect6(() => {
2680
+ const every = latest.current.backgroundPollMs ?? 5e3;
2681
+ if (latest.current.background !== true || !isWorking || every <= 0) {
2682
+ return;
2683
+ }
2684
+ const timer = setInterval(() => void refreshBackground(), every);
2685
+ return () => clearInterval(timer);
2686
+ }, [
2687
+ isWorking,
2688
+ refreshBackground
2689
+ ]);
2690
+ const loadThreads = useCallback5(async () => {
2691
+ const list = await client.listThreads();
2692
+ setThreads(list);
2693
+ return list;
2694
+ }, [
2695
+ client
2696
+ ]);
2697
+ const loadThread = useCallback5((id) => client.getThread(id), [
2698
+ client
2699
+ ]);
2700
+ const deleteThread = useCallback5(async (id) => {
2701
+ await client.deleteThread(id);
2702
+ setThreads((current) => current.filter((thread) => thread.id !== id));
2703
+ }, [
2704
+ client
2705
+ ]);
2706
+ const forkThread = useCallback5((threadId, messageId) => client.forkFromMessage(threadId, messageId), [
2707
+ client
2708
+ ]);
2709
+ const renameThread = useCallback5(async (id, title) => {
2710
+ await client.renameThread(id, title);
2711
+ setThreads((current) => current.map((thread) => thread.id === id ? {
2712
+ ...thread,
2713
+ title
2714
+ } : thread));
2715
+ }, [
2716
+ client
2717
+ ]);
2718
+ const promoteThread = useCallback5(async (id) => {
2719
+ await client.promoteThread(id);
2720
+ await loadThreads();
2721
+ }, [
2722
+ client,
2723
+ loadThreads
2724
+ ]);
2725
+ const truncateFromMessage = useCallback5((threadId, messageId) => client.truncateFromMessage(threadId, messageId).then(() => void 0), [
2726
+ client
2727
+ ]);
2728
+ const loadQuota = useCallback5(async () => {
2729
+ const today = await client.getQuotaToday();
2730
+ setQuota(today);
2731
+ return today;
2732
+ }, [
2733
+ client
2734
+ ]);
2735
+ const cancel = useCallback5(async () => {
2736
+ chatRef.current.stop();
2737
+ const activeRunId2 = runIdRef.current;
2738
+ if (activeRunId2) {
2739
+ try {
2740
+ await client.cancelStream(activeRunId2);
2741
+ } catch {
2742
+ }
2743
+ }
2744
+ }, [
2745
+ client
2746
+ ]);
2747
+ const approve = useCallback5(async ({ toolCallId }) => {
2748
+ await client.approveToolCall({
2749
+ toolCallId
2750
+ });
2751
+ }, [
2752
+ client
2753
+ ]);
2754
+ const reject = useCallback5(async ({ toolCallId, reason }) => {
2755
+ await client.rejectToolCall({
2756
+ toolCallId,
2757
+ ...reason !== void 0 ? {
2758
+ reason
2759
+ } : {}
2760
+ });
2761
+ }, [
2762
+ client
2763
+ ]);
2764
+ const answer = useCallback5(async ({ toolCallId, answers }) => {
2765
+ await client.answerToolCall({
2766
+ toolCallId,
2767
+ ...answers !== void 0 ? {
2768
+ answers
2769
+ } : {}
2770
+ });
2771
+ }, [
2772
+ client
2773
+ ]);
2774
+ const skip = useCallback5(async ({ toolCallId }) => {
2775
+ await client.skipToolCall({
2776
+ toolCallId
2777
+ });
2778
+ }, [
2779
+ client
2780
+ ]);
2781
+ const regenerate = useCallback5(() => {
2782
+ if (isTurnInFlight()) return;
2783
+ regenerateNext.current = true;
2784
+ turnInFlight.current = true;
2785
+ void chatRef.current.regenerate().finally(() => {
2786
+ turnInFlight.current = false;
2787
+ });
2788
+ }, [
2789
+ isTurnInFlight
2790
+ ]);
2791
+ const background = {
2792
+ runs: backgroundRuns,
2793
+ isWorking,
2794
+ refresh: refreshBackground
2795
+ };
2796
+ return {
2797
+ ...chat,
2798
+ sendMessage,
2799
+ addToolResult,
2800
+ /** Sub-agents this conversation started and did not wait for. */
2801
+ background,
2802
+ runId,
2803
+ /** The `resume`-fetched thread's active run id, or `null` once resolved with none. */
2804
+ activeRunId,
2805
+ client,
2806
+ threads,
2807
+ loadThreads,
2808
+ loadThread,
2809
+ deleteThread,
2810
+ forkThread,
2811
+ renameThread,
2812
+ promoteThread,
2813
+ truncateFromMessage,
2814
+ quota,
2815
+ loadQuota,
2816
+ cancel,
2817
+ approve,
2818
+ reject,
2819
+ answer,
2820
+ skip,
2821
+ regenerate
2822
+ };
2823
+ }
2824
+ __name(useAgentChat, "useAgentChat");
2825
+ async function mergeHeaders(options) {
2826
+ const dynamic = await options.getHeaders?.() ?? {};
2827
+ return {
2828
+ ...options.headers,
2829
+ ...dynamic
2830
+ };
2831
+ }
2832
+ __name(mergeHeaders, "mergeHeaders");
1430
2833
  export {
1431
2834
  AgentChatTransport,
1432
2835
  AgentClient,
@@ -1434,14 +2837,28 @@ export {
1434
2837
  ChatInput,
1435
2838
  DEFAULT_SPEECH_LANG,
1436
2839
  MessageItem,
2840
+ MessageItemView,
1437
2841
  MessageList,
1438
2842
  SPEECH_LANGUAGES,
2843
+ applyCompletion,
2844
+ backgroundRunsFromThread,
2845
+ buildTranscriptBlocks,
2846
+ createSkillsSource,
2847
+ describeTimestamp,
2848
+ describeUsage,
2849
+ extractMessageText,
2850
+ filterAutocompleteItems,
2851
+ findActiveTrigger,
1439
2852
  formatRelativeTime,
1440
2853
  loadStoredSpeechLang,
1441
2854
  persistSpeechLang,
1442
2855
  storedMessageToUiMessage,
1443
2856
  storedThreadToUiMessages,
1444
2857
  useAgentChat,
1445
- useSpeechRecognition
2858
+ useChatTranscript,
2859
+ useComposerAutocomplete,
2860
+ useSpeechRecognition,
2861
+ useStickToBottom,
2862
+ useTranscriptItem
1446
2863
  };
1447
2864
  //# sourceMappingURL=index.js.map