@dudousxd/nestjs-agent-react 0.5.5 → 0.6.1

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