@coral-ai/chat 0.3.1 → 0.4.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.js CHANGED
@@ -1,15 +1,16 @@
1
1
  // src/Chat.tsx
2
+ import { useLayoutEffect as useLayoutEffect3, useRef as useRef7, useState as useState7 } from "react";
2
3
  import {
3
- Caption1,
4
- InteractionTag,
5
- InteractionTagPrimary,
6
- makeStyles as makeStyles8,
4
+ Caption1 as Caption14,
5
+ Title3,
6
+ makeStyles as makeStyles10,
7
7
  motionTokens
8
8
  } from "@fluentui/react-components";
9
+ import { useCoralConfig } from "@coral-ai/react";
9
10
 
10
11
  // src/Chat.Canvas.tsx
11
12
  import { useState, useEffect, useLayoutEffect, useRef, useCallback } from "react";
12
- import { Button, makeStyles, tokens } from "@fluentui/react-components";
13
+ import { Button as Button2, makeStyles as makeStyles2, tokens as tokens2 } from "@fluentui/react-components";
13
14
  import { ArrowDown } from "@coral-ai/bundleicons";
14
15
 
15
16
  // src/Chat.Context.tsx
@@ -19,9 +20,154 @@ function useChatContext() {
19
20
  return useContext(ChatContext);
20
21
  }
21
22
 
22
- // src/Chat.Canvas.tsx
23
+ // src/Chat.Error.tsx
24
+ import { Body1, Button, makeStyles, tokens } from "@fluentui/react-components";
25
+ import {
26
+ ErrorCircle20Regular,
27
+ Clock20Regular,
28
+ LockClosed20Regular,
29
+ PlugDisconnected20Regular,
30
+ Warning20Regular
31
+ } from "@fluentui/react-icons";
23
32
  import { jsx, jsxs } from "react/jsx-runtime";
33
+ var ERROR_CATEGORIES = [
34
+ "auth_billing",
35
+ "rate_limit",
36
+ "overloaded",
37
+ "bad_input",
38
+ "not_found",
39
+ "server_error",
40
+ "unknown"
41
+ ];
42
+ function isNormalizedChatError(err) {
43
+ return !!err && typeof err === "object" && typeof err.message === "string" && ERROR_CATEGORIES.includes(err.category);
44
+ }
45
+ function normalizeChatError(err) {
46
+ if (isNormalizedChatError(err)) return err;
47
+ const anyErr = err;
48
+ const rawMessage = String(
49
+ anyErr?.message ?? anyErr?.error?.message ?? err ?? ""
50
+ ).toLowerCase();
51
+ const status = Number(anyErr?.status ?? anyErr?.statusCode ?? 0);
52
+ const errorType = String(
53
+ anyErr?.error?.type ?? anyErr?.type ?? anyErr?.code ?? ""
54
+ );
55
+ const errorCode = String(
56
+ anyErr?.error?.code ?? anyErr?.code ?? ""
57
+ );
58
+ if (rawMessage.includes("credit") || rawMessage.includes("billing") || rawMessage.includes("insufficient_quota") || errorCode === "insufficient_quota" || errorType === "PERMISSION_DENIED") {
59
+ return {
60
+ category: "auth_billing",
61
+ message: "API credits exhausted. Add credits in your provider's billing dashboard."
62
+ };
63
+ }
64
+ if (errorType === "rate_limit_error" || rawMessage.includes("rate limit") || rawMessage.includes("too many requests")) {
65
+ return {
66
+ category: "rate_limit",
67
+ message: "You're being rate limited. Wait a moment and try again."
68
+ };
69
+ }
70
+ if (status === 529 || status === 503 || errorType === "overloaded_error" || errorType === "RESOURCE_EXHAUSTED" || rawMessage.includes("overload") || rawMessage.includes("capacity")) {
71
+ return {
72
+ category: "overloaded",
73
+ message: "The AI service is overloaded right now. Try again in a few seconds."
74
+ };
75
+ }
76
+ if (status === 404 || errorType === "NOT_FOUND" || rawMessage.includes("model_not_found") || rawMessage.includes("not found")) {
77
+ return {
78
+ category: "not_found",
79
+ message: "Model not found. Check the model name in your configuration."
80
+ };
81
+ }
82
+ if (status === 400 || errorType === "invalid_request_error" || errorType === "INVALID_ARGUMENT") {
83
+ return {
84
+ category: "bad_input",
85
+ message: "The request was invalid. Check your message or API configuration."
86
+ };
87
+ }
88
+ if (status >= 500 || errorType === "api_error" || errorType === "INTERNAL") {
89
+ return {
90
+ category: "server_error",
91
+ message: "The AI service hit an internal error. Try again shortly."
92
+ };
93
+ }
94
+ return {
95
+ category: "unknown",
96
+ message: "Something went wrong. Check your connection and try again."
97
+ };
98
+ }
24
99
  var useStyles = makeStyles({
100
+ root: {
101
+ display: "flex",
102
+ alignItems: "center",
103
+ justifyContent: "space-between",
104
+ gap: "8px",
105
+ padding: "12px",
106
+ borderRadius: tokens.borderRadiusXLarge,
107
+ backgroundColor: tokens.colorNeutralBackground3,
108
+ border: `1px solid ${tokens.colorNeutralStroke2}`,
109
+ color: tokens.colorNeutralForeground3,
110
+ width: "100%",
111
+ boxSizing: "border-box",
112
+ margin: "8px 0"
113
+ },
114
+ left: {
115
+ display: "flex",
116
+ alignItems: "center",
117
+ gap: "8px",
118
+ minWidth: 0
119
+ },
120
+ message: {
121
+ fontFamily: "var(--fontFamilyMonospace)"
122
+ }
123
+ });
124
+ var categoryIcon = {
125
+ auth_billing: /* @__PURE__ */ jsx(LockClosed20Regular, {}),
126
+ rate_limit: /* @__PURE__ */ jsx(Clock20Regular, {}),
127
+ overloaded: /* @__PURE__ */ jsx(PlugDisconnected20Regular, {}),
128
+ bad_input: /* @__PURE__ */ jsx(Warning20Regular, {}),
129
+ not_found: /* @__PURE__ */ jsx(ErrorCircle20Regular, {}),
130
+ server_error: /* @__PURE__ */ jsx(ErrorCircle20Regular, {}),
131
+ unknown: /* @__PURE__ */ jsx(Warning20Regular, {})
132
+ };
133
+ var ChatError = ({ error, onRetry }) => {
134
+ const styles = useStyles();
135
+ const { category, message } = normalizeChatError(error);
136
+ return /* @__PURE__ */ jsxs("div", { className: styles.root, children: [
137
+ /* @__PURE__ */ jsxs("span", { className: styles.left, children: [
138
+ categoryIcon[category],
139
+ /* @__PURE__ */ jsx(Body1, { className: styles.message, children: message })
140
+ ] }),
141
+ onRetry && /* @__PURE__ */ jsx(
142
+ Button,
143
+ {
144
+ size: "small",
145
+ appearance: "primary",
146
+ onClick: onRetry,
147
+ children: "Retry"
148
+ }
149
+ )
150
+ ] });
151
+ };
152
+ var Chat_Error_default = ChatError;
153
+
154
+ // src/fileToBase64.ts
155
+ function fileToBase64(file) {
156
+ return new Promise((resolve, reject) => {
157
+ const reader = new FileReader();
158
+ reader.onload = () => {
159
+ const dataUrl = reader.result;
160
+ const comma = dataUrl.indexOf(",");
161
+ resolve(comma === -1 ? dataUrl : dataUrl.slice(comma + 1));
162
+ };
163
+ reader.onerror = () => reject(reader.error ?? new Error("FileReader failed"));
164
+ reader.readAsDataURL(file);
165
+ });
166
+ }
167
+
168
+ // src/Chat.Canvas.tsx
169
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
170
+ var useStyles2 = makeStyles2({
25
171
  root: {
26
172
  width: "100%",
27
173
  display: "flex",
@@ -36,7 +182,7 @@ var useStyles = makeStyles({
36
182
  transform: "translateX(-50%)",
37
183
  zIndex: 1e3,
38
184
  cursor: "pointer",
39
- boxShadow: tokens.shadow8,
185
+ boxShadow: tokens2.shadow8,
40
186
  marginBottom: "48px",
41
187
  backgroundColor: "var(--colorNeutralBackgroundAlpha2)",
42
188
  backdropFilter: "blur(40px) saturate(150%)"
@@ -51,9 +197,54 @@ function isRateLimitError(err) {
51
197
  const msg = String(e?.message ?? "").toLowerCase();
52
198
  return status === 429 || msg.includes("rate limit") || msg.includes("too many requests");
53
199
  }
200
+ async function* wrapWithStatusChunk(taskType, reply) {
201
+ yield { type: "status", name: taskType };
202
+ if (reply) yield reply;
203
+ }
204
+ async function* parseStream(res) {
205
+ const reader = res.body?.getReader();
206
+ if (!reader) return;
207
+ const decoder = new TextDecoder();
208
+ let buffer = "";
209
+ while (true) {
210
+ const { value, done } = await reader.read();
211
+ if (done) break;
212
+ buffer += decoder.decode(value, { stream: true });
213
+ const lines = buffer.split("\n");
214
+ buffer = lines.pop() ?? "";
215
+ for (const raw of lines) {
216
+ const line = raw.startsWith("data:") ? raw.slice(5).trim() : raw.trim();
217
+ if (!line || line === "[DONE]") continue;
218
+ try {
219
+ const parsed = JSON.parse(line);
220
+ if (typeof parsed === "string") {
221
+ yield parsed;
222
+ } else if (parsed && typeof parsed === "object" && "type" in parsed) {
223
+ yield parsed;
224
+ } else if (parsed && typeof parsed === "object" && typeof parsed.reply === "string") {
225
+ if (typeof parsed.type === "string") {
226
+ yield { type: "status", name: parsed.type };
227
+ }
228
+ yield parsed.reply;
229
+ }
230
+ } catch {
231
+ yield line;
232
+ }
233
+ }
234
+ }
235
+ if (buffer.trim()) {
236
+ try {
237
+ const parsed = JSON.parse(buffer);
238
+ if (typeof parsed === "string") yield parsed;
239
+ else if (parsed && typeof parsed === "object" && "type" in parsed) yield parsed;
240
+ } catch {
241
+ yield buffer;
242
+ }
243
+ }
244
+ }
54
245
  var ChatCanvas = ({
55
- agent,
56
- agents,
246
+ api,
247
+ allowedAttachments,
57
248
  conversationId,
58
249
  initialMessages,
59
250
  systemPrompt,
@@ -66,20 +257,67 @@ var ChatCanvas = ({
66
257
  onStop,
67
258
  children
68
259
  }) => {
69
- const styles = useStyles();
70
- const agentKey = agent ?? "default";
71
- const resolvedHandler = onSendMessage ?? agents?.[agentKey]?.handler ?? agents?.["default"]?.handler;
72
- const [messages, setMessages] = useState(initialMessages ?? []);
260
+ const styles = useStyles2();
261
+ const apiHandler = api ? async function(input2, history, files) {
262
+ const attachments2 = await Promise.all(
263
+ files.map(async (file) => ({
264
+ name: file.name,
265
+ mediaType: file.type,
266
+ data: await fileToBase64(file)
267
+ }))
268
+ );
269
+ const res = await fetch(api, {
270
+ method: "POST",
271
+ headers: { "Content-Type": "application/json" },
272
+ body: JSON.stringify({
273
+ text: input2,
274
+ history,
275
+ attachments: attachments2
276
+ })
277
+ });
278
+ if (!res.ok) {
279
+ const text = await res.text().catch(() => "");
280
+ throw Object.assign(
281
+ new Error(`Chat API request failed (${res.status}): ${text || res.statusText}`),
282
+ { status: res.status }
283
+ );
284
+ }
285
+ const contentType = res.headers.get("content-type") ?? "";
286
+ if (contentType.includes("text/event-stream") || contentType.includes("application/x-ndjson")) {
287
+ return parseStream(res);
288
+ }
289
+ const data = await res.json();
290
+ if (data && typeof data === "object") {
291
+ if (typeof data.type === "string") {
292
+ return wrapWithStatusChunk(data.type, String(data.reply ?? ""));
293
+ }
294
+ if (typeof data.reply === "string") return data.reply;
295
+ }
296
+ return typeof data === "string" ? data : "";
297
+ } : void 0;
298
+ const resolvedHandler = onSendMessage ?? apiHandler;
299
+ const [messages, setMessages] = useState(
300
+ () => (initialMessages ?? []).map((m) => m.id ? m : { ...m, id: crypto.randomUUID() })
301
+ );
73
302
  const [input, setInput] = useState("");
74
303
  const [isTyping, setIsTyping] = useState(false);
75
304
  const [isRetrying, setIsRetrying] = useState(false);
76
- const [chatError, setChatError] = useState(null);
77
305
  const [showScrollButton, setShowScrollButton] = useState(false);
78
306
  const [inputHeight, setInputHeightState] = useState(0);
79
307
  const [isInterrupted, setIsInterrupted] = useState(false);
80
308
  const [streamingReasoning, setStreamingReasoning] = useState("");
81
- const [streamingAgentName, setStreamingAgentName] = useState();
309
+ const [streamingStatus, setStreamingStatus] = useState();
82
310
  const [attachments, setAttachments] = useState([]);
311
+ const [awaitingFreeformApproval, setAwaitingFreeformApproval] = useState(null);
312
+ const pendingApproval = (() => {
313
+ for (let i = messages.length - 1; i >= 0; i--) {
314
+ const m = messages[i];
315
+ if (m.role !== "assistant") continue;
316
+ if (m.approval && !m.approval.answer) return m.approval;
317
+ if (m.approval && m.approval.answer) return null;
318
+ }
319
+ return null;
320
+ })();
83
321
  const reasoningRef = useRef("");
84
322
  const lastUserInputRef = useRef("");
85
323
  const hasEverSentRef = useRef(false);
@@ -92,7 +330,9 @@ var ChatCanvas = ({
92
330
  useEffect(() => {
93
331
  setMessages((current) => {
94
332
  if ((initialMessages?.length ?? 0) > 0 || current.length === 0) {
95
- return initialMessages ?? [];
333
+ return (initialMessages ?? []).map(
334
+ (m) => m.id ? m : { ...m, id: crypto.randomUUID() }
335
+ );
96
336
  }
97
337
  return current;
98
338
  });
@@ -159,40 +399,18 @@ var ChatCanvas = ({
159
399
  const removeAttachment = (id) => {
160
400
  setAttachments((prev) => prev.filter((a) => a.id !== id));
161
401
  };
162
- const sendMessage = async (overrideInput) => {
163
- const textToSend = overrideInput ?? input;
164
- if (!textToSend.trim()) return;
165
- hasEverSentRef.current = true;
402
+ const runHandler = async (textToSend, updatedMessages, snapshotAttachments) => {
166
403
  const sessionAtSend = conversationId;
167
- const baseMessages = overrideInput ? messages.filter((_, i) => i < messages.length - 2) : messages;
168
- const snapshotAttachments = attachments;
169
- const attachmentMeta = snapshotAttachments.map((a) => ({
170
- name: a.file.name,
171
- type: a.file.type,
172
- preview: a.preview
173
- }));
174
- const updatedMessages = [
175
- ...baseMessages,
176
- {
177
- id: crypto.randomUUID(),
178
- role: "user",
179
- content: textToSend.replace(/^[\n\r]+|[\n\r]+$/g, ""),
180
- ...attachmentMeta.length > 0 ? { metadata: { attachments: attachmentMeta } } : {}
181
- }
182
- ];
183
404
  lastUserInputRef.current = textToSend;
184
405
  userScrolledUpRef.current = false;
185
406
  setMessages(updatedMessages);
186
407
  onMessagesChange?.(updatedMessages, sessionAtSend);
187
- if (!overrideInput) setInput("");
188
- setAttachments([]);
189
408
  setIsTyping(true);
190
409
  setIsInterrupted(false);
191
- setChatError(null);
192
410
  shouldStopRef.current = false;
193
411
  reasoningRef.current = "";
194
412
  setStreamingReasoning("");
195
- setStreamingAgentName(void 0);
413
+ setStreamingStatus(void 0);
196
414
  const historyForAI = systemPrompt ? [{ role: "system", content: systemPrompt }, ...updatedMessages] : updatedMessages;
197
415
  const MAX_RETRIES = 3;
198
416
  try {
@@ -208,6 +426,8 @@ var ChatCanvas = ({
208
426
  let assistantContent = "";
209
427
  let rafId = null;
210
428
  let pendingContent = "";
429
+ let streamedApproval;
430
+ let streamedTodos;
211
431
  for await (const chunk of response) {
212
432
  if (shouldStopRef.current) break;
213
433
  if (sessionAtSend !== void 0 && currentConversationIdRef.current !== sessionAtSend) break;
@@ -217,9 +437,13 @@ var ChatCanvas = ({
217
437
  if (rafId === null) {
218
438
  rafId = requestAnimationFrame(() => {
219
439
  const content = pendingContent;
220
- const nextMsg = { id: assistantId, role: "assistant", content };
440
+ let nextMsg = { id: assistantId, role: "assistant", content };
221
441
  setMessages((prev) => {
222
442
  const updated = [...prev];
443
+ const last = updated[updated.length - 1];
444
+ if (last && last.id === assistantId) {
445
+ nextMsg = { ...last, content };
446
+ }
223
447
  updated[updated.length - 1] = nextMsg;
224
448
  return updated;
225
449
  });
@@ -230,8 +454,28 @@ var ChatCanvas = ({
230
454
  } else if (chunk.type === "reasoning") {
231
455
  reasoningRef.current += chunk.content;
232
456
  setStreamingReasoning(reasoningRef.current);
233
- } else if (chunk.type === "agent") {
234
- setStreamingAgentName(chunk.name);
457
+ } else if (chunk.type === "status") {
458
+ setStreamingStatus(chunk.name);
459
+ } else if (chunk.type === "approval") {
460
+ streamedApproval = chunk.approval;
461
+ setMessages((prev) => {
462
+ const updated = [...prev];
463
+ const last = updated[updated.length - 1];
464
+ if (last && last.id === assistantId) {
465
+ updated[updated.length - 1] = { ...last, approval: chunk.approval };
466
+ }
467
+ return updated;
468
+ });
469
+ } else if (chunk.type === "todos") {
470
+ streamedTodos = chunk.todos;
471
+ setMessages((prev) => {
472
+ const updated = [...prev];
473
+ const last = updated[updated.length - 1];
474
+ if (last && last.id === assistantId) {
475
+ updated[updated.length - 1] = { ...last, todos: chunk.todos };
476
+ }
477
+ return updated;
478
+ });
235
479
  }
236
480
  }
237
481
  if (rafId !== null) cancelAnimationFrame(rafId);
@@ -243,7 +487,9 @@ var ChatCanvas = ({
243
487
  id: assistantId,
244
488
  role: "assistant",
245
489
  content: assistantContent,
246
- ...reasoningRef.current ? { metadata: { reasoning: reasoningRef.current } } : {}
490
+ ...reasoningRef.current ? { metadata: { reasoning: reasoningRef.current } } : {},
491
+ ...streamedApproval ? { approval: streamedApproval } : {},
492
+ ...streamedTodos ? { todos: streamedTodos } : {}
247
493
  };
248
494
  const finalMessages = [...updatedMessages, finalMsg];
249
495
  setMessages(finalMessages);
@@ -262,7 +508,8 @@ var ChatCanvas = ({
262
508
  role: "assistant",
263
509
  content: response.content,
264
510
  type: response.type,
265
- metadata: response.metadata
511
+ metadata: response.metadata,
512
+ todos: response.todos
266
513
  };
267
514
  const finalMessages = [...updatedMessages, assistantMsg];
268
515
  setMessages(finalMessages);
@@ -282,17 +529,148 @@ var ChatCanvas = ({
282
529
  }
283
530
  } catch (err) {
284
531
  onError?.(err);
285
- setChatError(err);
532
+ if (sessionAtSend === void 0 || currentConversationIdRef.current === sessionAtSend) {
533
+ const erroredMessages = [
534
+ ...updatedMessages,
535
+ {
536
+ id: crypto.randomUUID(),
537
+ role: "assistant",
538
+ content: "",
539
+ error: normalizeChatError(err)
540
+ }
541
+ ];
542
+ setMessages(erroredMessages);
543
+ onMessagesChange?.(erroredMessages, sessionAtSend);
544
+ }
286
545
  } finally {
287
546
  setIsRetrying(false);
288
547
  setIsTyping(false);
289
548
  }
290
549
  };
291
- return /* @__PURE__ */ jsx(
550
+ const resolveApproval = (approvalId, optionId) => {
551
+ const target = messages.find(
552
+ (m) => m.role === "assistant" && m.approval?.id === approvalId
553
+ );
554
+ if (!target || !target.approval) return;
555
+ if (target.approval.answer) return;
556
+ const option = target.approval.options.find((o) => o.id === optionId);
557
+ if (!option) return;
558
+ if (option.freeform) {
559
+ setAwaitingFreeformApproval({ approvalId, optionId });
560
+ return;
561
+ }
562
+ const updated = messages.map(
563
+ (m) => m.role === "assistant" && m.approval?.id === approvalId ? { ...m, approval: { ...m.approval, answer: { optionId } } } : m
564
+ );
565
+ const replyText = option.label;
566
+ const userMsg = {
567
+ id: crypto.randomUUID(),
568
+ role: "user",
569
+ type: "approval_response",
570
+ content: replyText,
571
+ metadata: { approvalId, optionId, label: option.label }
572
+ };
573
+ const next = [...updated, userMsg];
574
+ void runHandler(replyText, next, []);
575
+ };
576
+ const sendMessage = async (overrideInput) => {
577
+ const textToSend = overrideInput ?? input;
578
+ if (!textToSend.trim()) return;
579
+ if (awaitingFreeformApproval) {
580
+ const { approvalId, optionId } = awaitingFreeformApproval;
581
+ const target = messages.find(
582
+ (m) => m.role === "assistant" && m.approval?.id === approvalId
583
+ );
584
+ if (target && target.approval) {
585
+ const option = target.approval.options.find((o) => o.id === optionId);
586
+ const updated = messages.map(
587
+ (m) => m.role === "assistant" && m.approval?.id === approvalId ? { ...m, approval: { ...m.approval, answer: { optionId, text: textToSend } } } : m
588
+ );
589
+ const userMsg = {
590
+ id: crypto.randomUUID(),
591
+ role: "user",
592
+ type: "approval_response",
593
+ content: textToSend.replace(/^[\n\r]+|[\n\r]+$/g, ""),
594
+ metadata: { approvalId, optionId, label: option?.label, text: textToSend }
595
+ };
596
+ const next = [...updated, userMsg];
597
+ if (!overrideInput) setInput("");
598
+ setAwaitingFreeformApproval(null);
599
+ hasEverSentRef.current = true;
600
+ await runHandler(textToSend, next, []);
601
+ return;
602
+ }
603
+ setAwaitingFreeformApproval(null);
604
+ }
605
+ hasEverSentRef.current = true;
606
+ const lastMessage = messages[messages.length - 1];
607
+ const baseMessages = overrideInput ? messages.filter((_, i) => i < messages.length - 2) : lastMessage?.role === "assistant" && lastMessage.error ? messages.slice(0, -1) : messages;
608
+ const snapshotAttachments = attachments;
609
+ const attachmentMeta = snapshotAttachments.map((a) => ({
610
+ name: a.file.name,
611
+ type: a.file.type,
612
+ preview: a.preview
613
+ }));
614
+ const updatedMessages = [
615
+ ...baseMessages,
616
+ {
617
+ id: crypto.randomUUID(),
618
+ role: "user",
619
+ content: textToSend.replace(/^[\n\r]+|[\n\r]+$/g, ""),
620
+ ...attachmentMeta.length > 0 ? { metadata: { attachments: attachmentMeta } } : {}
621
+ }
622
+ ];
623
+ if (!overrideInput) setInput("");
624
+ setAttachments([]);
625
+ await runHandler(textToSend, updatedMessages, snapshotAttachments);
626
+ };
627
+ const resolveRegenerateTarget = (messageId) => {
628
+ if (!messageId) {
629
+ for (let i = messages.length - 1; i >= 0; i--) {
630
+ if (messages[i].role === "user") return i;
631
+ }
632
+ return -1;
633
+ }
634
+ const idx = messages.findIndex((m) => m.id === messageId);
635
+ if (idx === -1) return -1;
636
+ if (messages[idx].role === "user") return idx;
637
+ if (messages[idx].role === "assistant") {
638
+ for (let i = idx - 1; i >= 0; i--) {
639
+ if (messages[i].role === "user") return i;
640
+ }
641
+ }
642
+ return -1;
643
+ };
644
+ const regenerate = async (messageId) => {
645
+ const userIdx = resolveRegenerateTarget(messageId);
646
+ if (userIdx === -1) return;
647
+ const userMsg = messages[userIdx];
648
+ const baseMessages = messages.slice(0, userIdx + 1);
649
+ await runHandler(userMsg.content, baseMessages, []);
650
+ };
651
+ const editMessage = async (messageId, newContent) => {
652
+ const idx = messages.findIndex((m) => m.id === messageId);
653
+ if (idx === -1) return;
654
+ if (messages[idx].role !== "user") return;
655
+ const trimmed = newContent.replace(/^[\n\r]+|[\n\r]+$/g, "");
656
+ if (!trimmed.trim()) return;
657
+ const editedMessage = {
658
+ ...messages[idx],
659
+ content: trimmed
660
+ };
661
+ const baseMessages = [
662
+ ...messages.slice(0, idx),
663
+ editedMessage
664
+ ];
665
+ await runHandler(trimmed, baseMessages, []);
666
+ };
667
+ return /* @__PURE__ */ jsx2(
292
668
  ChatContext.Provider,
293
669
  {
294
670
  value: {
295
671
  sendMessage,
672
+ regenerate,
673
+ editMessage,
296
674
  scrollToBottom,
297
675
  handleCopy,
298
676
  messages,
@@ -303,9 +681,8 @@ var ChatCanvas = ({
303
681
  isRetrying,
304
682
  isInterrupted,
305
683
  streamingReasoning,
306
- streamingAgentName,
684
+ streamingStatus,
307
685
  thinkingStatus,
308
- chatError,
309
686
  showScrollButton,
310
687
  setInputHeight: setInputHeightState,
311
688
  messagesContainerRef,
@@ -317,12 +694,16 @@ var ChatCanvas = ({
317
694
  onStop,
318
695
  attachments,
319
696
  addAttachments,
320
- removeAttachment
697
+ removeAttachment,
698
+ allowedAttachments,
699
+ pendingApproval,
700
+ awaitingFreeformApproval,
701
+ resolveApproval
321
702
  },
322
- children: /* @__PURE__ */ jsxs("div", { className: styles.root, children: [
703
+ children: /* @__PURE__ */ jsxs2("div", { className: styles.root, children: [
323
704
  children,
324
- showScrollButton && /* @__PURE__ */ jsx(
325
- Button,
705
+ showScrollButton && /* @__PURE__ */ jsx2(
706
+ Button2,
326
707
  {
327
708
  onClick: scrollToBottom,
328
709
  className: styles.scrollToBottom,
@@ -330,10 +711,10 @@ var ChatCanvas = ({
330
711
  style: {
331
712
  bottom: inputHeight + 16,
332
713
  backgroundColor: "transparent",
333
- border: `1px solid ${tokens.colorNeutralStroke3}`,
714
+ border: `1px solid ${tokens2.colorNeutralStroke3}`,
334
715
  backdropFilter: "saturate(180%) blur(16px)"
335
716
  },
336
- icon: /* @__PURE__ */ jsx(ArrowDown, { style: { fontSize: "16px" } })
717
+ icon: /* @__PURE__ */ jsx2(ArrowDown, { style: { fontSize: "16px" } })
337
718
  }
338
719
  )
339
720
  ] })
@@ -343,22 +724,34 @@ var ChatCanvas = ({
343
724
  var Chat_Canvas_default = ChatCanvas;
344
725
 
345
726
  // src/Chat.Messages.tsx
346
- import React4, { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
347
- import { Body1 as Body14, Button as Button4, makeStyles as makeStyles6, tokens as tokens6 } from "@fluentui/react-components";
348
- import { Copy, Heart } from "@coral-ai/bundleicons";
349
- import { CoralMarkdown } from "@coral-ai/markdown";
727
+ import React5, { useEffect as useEffect4, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState5 } from "react";
728
+ import {
729
+ Body1 as Body15,
730
+ Button as Button4,
731
+ Caption1 as Caption12,
732
+ makeStyles as makeStyles7,
733
+ tokens as tokens7
734
+ } from "@fluentui/react-components";
735
+ import {
736
+ ArrowClockwise,
737
+ Copy,
738
+ Edit as EditIcon,
739
+ Info
740
+ } from "@coral-ai/bundleicons";
741
+ import {
742
+ CoralMarkdown
743
+ } from "@coral-ai/markdown";
350
744
 
351
745
  // src/Chat.ThinkingIndicator.tsx
352
746
  import { useState as useState2, useEffect as useEffect2, useRef as useRef2 } from "react";
353
- import { makeStyles as makeStyles2, tokens as tokens2 } from "@fluentui/react-components";
354
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
747
+ import { makeStyles as makeStyles3, Spinner, tokens as tokens3 } from "@fluentui/react-components";
748
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
355
749
  var thinkingMessages = [
356
750
  "Generating",
357
751
  "Thinking",
358
752
  "Optimizing",
359
753
  "Processing",
360
754
  "Analyzing",
361
- "Reasoning",
362
755
  "Drafting",
363
756
  "Evaluating",
364
757
  "Computing",
@@ -377,45 +770,13 @@ var retryingMessages = [
377
770
  ];
378
771
  var TYPING_SPEED = 80;
379
772
  var HOLD_DURATION = 1300;
380
- var SPINNER_BG = "#000000";
381
- var useStyles2 = makeStyles2({
773
+ var useStyles3 = makeStyles3({
382
774
  root: {
383
775
  display: "flex",
384
776
  alignItems: "center",
385
777
  gap: "8px",
386
778
  width: "100%",
387
- color: tokens2.colorNeutralForeground3
388
- // fontSize: "14px",
389
- },
390
- icon: {
391
- width: "28px",
392
- height: "28px",
393
- flexShrink: 0,
394
- border: `3px solid ${SPINNER_BG}`,
395
- boxSizing: "border-box",
396
- backgroundColor: "${SPINNER_BG}",
397
- opacity: 0.8,
398
- backgroundImage: [
399
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
400
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
401
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
402
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
403
- `radial-gradient(farthest-side, ${tokens2.colorNeutralForeground1} 98%, ${SPINNER_BG})`,
404
- `linear-gradient(${tokens2.colorNeutralForeground1} 0 0)`,
405
- `linear-gradient(${tokens2.colorNeutralForeground1} 0 0)`
406
- ].join(", "),
407
- backgroundSize: "7px 7px, 7px 7px, 7px 7px, 7px 7px, 7px 7px, 100% 3px, 3px 100%",
408
- backgroundPosition: "50%, 50%, 50%, 50%, 50%, 50%, 50%",
409
- backgroundRepeat: "no-repeat",
410
- filter: "blur(2px) contrast(10)",
411
- animationName: {
412
- "100%": {
413
- backgroundPosition: "50% -7px, -7px 50%, 22px 50%, 50% 22px, 50%, 50%, 50%"
414
- }
415
- },
416
- animationDuration: "0.8s",
417
- animationIterationCount: "infinite",
418
- animationTimingFunction: "ease"
779
+ color: tokens3.colorNeutralForeground3
419
780
  },
420
781
  text: {
421
782
  // fontFamily: tokens.fontFamilyBase,
@@ -428,7 +789,7 @@ var useStyles2 = makeStyles2({
428
789
  display: "inline-block",
429
790
  width: "6px",
430
791
  height: "1em",
431
- backgroundColor: tokens2.colorNeutralForeground3,
792
+ backgroundColor: tokens3.colorNeutralForeground3,
432
793
  marginLeft: "1px",
433
794
  verticalAlign: "text-bottom",
434
795
  animationName: {
@@ -440,14 +801,14 @@ var useStyles2 = makeStyles2({
440
801
  animationIterationCount: "infinite"
441
802
  }
442
803
  });
443
- var ThinkingIndicator = ({ isRetrying = false, status }) => {
444
- const styles = useStyles2();
804
+ var ThinkingIndicator = ({ isRetrying = false, status, messages }) => {
805
+ const styles = useStyles3();
445
806
  const [displayText, setDisplayText] = useState2("");
446
807
  const timerRef = useRef2(void 0);
447
808
  const msgIdxRef = useRef2(0);
448
809
  const charIdxRef = useRef2(0);
449
810
  useEffect2(() => {
450
- const pool = isRetrying ? retryingMessages : thinkingMessages;
811
+ const pool = isRetrying ? retryingMessages : messages && messages.length > 0 ? messages : thinkingMessages;
451
812
  msgIdxRef.current = 0;
452
813
  charIdxRef.current = 0;
453
814
  const tick = () => {
@@ -466,32 +827,58 @@ var ThinkingIndicator = ({ isRetrying = false, status }) => {
466
827
  };
467
828
  tick();
468
829
  return () => clearTimeout(timerRef.current);
469
- }, [isRetrying]);
470
- return /* @__PURE__ */ jsxs2("div", { className: styles.root, children: [
471
- /* @__PURE__ */ jsx2("div", { className: styles.icon }),
472
- /* @__PURE__ */ jsxs2("span", { className: styles.text, children: [
830
+ }, [isRetrying, messages]);
831
+ return /* @__PURE__ */ jsxs3("div", { className: styles.root, children: [
832
+ /* @__PURE__ */ jsx3(Spinner, { size: "tiny" }),
833
+ /* @__PURE__ */ jsxs3("span", { className: styles.text, children: [
473
834
  status ?? displayText,
474
- /* @__PURE__ */ jsx2("span", { className: styles.cursor })
835
+ /* @__PURE__ */ jsx3("span", { className: styles.cursor })
475
836
  ] })
476
837
  ] });
477
838
  };
478
839
  var Chat_ThinkingIndicator_default = ThinkingIndicator;
479
840
 
841
+ // src/Chat.TaskMessages.ts
842
+ var DEFAULT_TASK_MESSAGES = {
843
+ text: [
844
+ "Generating",
845
+ "Thinking",
846
+ "Optimizing",
847
+ "Processing",
848
+ "Analyzing",
849
+ "Drafting",
850
+ "Evaluating",
851
+ "Computing",
852
+ "Synthesizing",
853
+ "Reviewing",
854
+ "Refining"
855
+ ],
856
+ photo: ["Looking", "Examining", "Reading image", "Analyzing visuals"],
857
+ file: ["Reading document", "Parsing", "Reviewing", "Scanning"],
858
+ voice: ["Listening", "Transcribing", "Processing audio"]
859
+ };
860
+ function resolveTaskMessages(name) {
861
+ if (name && name in DEFAULT_TASK_MESSAGES) {
862
+ return DEFAULT_TASK_MESSAGES[name];
863
+ }
864
+ return DEFAULT_TASK_MESSAGES.text;
865
+ }
866
+
480
867
  // src/Chat.Callout.tsx
481
- import { Body1, Button as Button2, makeStyles as makeStyles3, mergeClasses, tokens as tokens3 } from "@fluentui/react-components";
868
+ import { Body1 as Body12, Button as Button3, makeStyles as makeStyles4, mergeClasses, tokens as tokens4 } from "@fluentui/react-components";
482
869
  import { ChatWarning20Regular } from "@fluentui/react-icons";
483
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
484
- var useStyles3 = makeStyles3({
870
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
871
+ var useStyles4 = makeStyles4({
485
872
  root: {
486
873
  display: "inline-flex",
487
874
  alignItems: "center",
488
875
  justifyContent: "space-between",
489
876
  gap: "8px",
490
877
  padding: "12px 12px",
491
- borderRadius: tokens3.borderRadiusXLarge,
492
- backgroundColor: tokens3.colorNeutralBackground3,
493
- border: `1px solid ${tokens3.colorNeutralStroke2}`,
494
- color: tokens3.colorNeutralForeground3,
878
+ borderRadius: tokens4.borderRadiusXLarge,
879
+ backgroundColor: tokens4.colorNeutralBackground3,
880
+ border: `1px solid ${tokens4.colorNeutralStroke2}`,
881
+ color: tokens4.colorNeutralForeground3,
495
882
  width: "100%",
496
883
  boxSizing: "border-box",
497
884
  margin: "24px 0"
@@ -509,143 +896,25 @@ var useStyles3 = makeStyles3({
509
896
  }
510
897
  });
511
898
  var ChatCallout = ({ message, onRetry, className }) => {
512
- const styles = useStyles3();
513
- return /* @__PURE__ */ jsxs3("div", { className: mergeClasses(styles.root, className), children: [
514
- /* @__PURE__ */ jsxs3("span", { className: styles.left, children: [
515
- /* @__PURE__ */ jsx3(ChatWarning20Regular, {}),
516
- /* @__PURE__ */ jsx3(Body1, { className: styles.label, children: message })
517
- ] }),
518
- onRetry && /* @__PURE__ */ jsx3(
519
- Button2,
520
- {
521
- onClick: onRetry,
522
- className: styles.retryButton,
523
- size: "small",
524
- appearance: "primary",
525
- children: "Retry"
526
- }
527
- )
528
- ] });
529
- };
530
- var Chat_Callout_default = ChatCallout;
531
-
532
- // src/Chat.Error.tsx
533
- import { Body1 as Body12, Button as Button3, makeStyles as makeStyles4, tokens as tokens4 } from "@fluentui/react-components";
534
- import {
535
- ErrorCircle20Regular,
536
- Clock20Regular,
537
- LockClosed20Regular,
538
- PlugDisconnected20Regular,
539
- Warning20Regular
540
- } from "@fluentui/react-icons";
541
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
542
- function normalizeChatError(err) {
543
- const anyErr = err;
544
- const rawMessage = String(
545
- anyErr?.message ?? anyErr?.error?.message ?? err ?? ""
546
- ).toLowerCase();
547
- const status = Number(anyErr?.status ?? anyErr?.statusCode ?? 0);
548
- const errorType = String(
549
- anyErr?.error?.type ?? anyErr?.type ?? anyErr?.code ?? ""
550
- );
551
- const errorCode = String(
552
- anyErr?.error?.code ?? anyErr?.code ?? ""
553
- );
554
- if (rawMessage.includes("credit") || rawMessage.includes("billing") || rawMessage.includes("insufficient_quota") || errorCode === "insufficient_quota" || errorType === "PERMISSION_DENIED") {
555
- return {
556
- category: "auth_billing",
557
- message: "API credits exhausted. Add credits in your provider's billing dashboard."
558
- };
559
- }
560
- if (errorType === "rate_limit_error" || rawMessage.includes("rate limit") || rawMessage.includes("too many requests")) {
561
- return {
562
- category: "rate_limit",
563
- message: "You're being rate limited. Wait a moment and try again."
564
- };
565
- }
566
- if (status === 529 || status === 503 || errorType === "overloaded_error" || errorType === "RESOURCE_EXHAUSTED" || rawMessage.includes("overload") || rawMessage.includes("capacity")) {
567
- return {
568
- category: "overloaded",
569
- message: "The AI service is overloaded right now. Try again in a few seconds."
570
- };
571
- }
572
- if (status === 404 || errorType === "NOT_FOUND" || rawMessage.includes("model_not_found") || rawMessage.includes("not found")) {
573
- return {
574
- category: "not_found",
575
- message: "Model not found. Check the model name in your configuration."
576
- };
577
- }
578
- if (status === 400 || errorType === "invalid_request_error" || errorType === "INVALID_ARGUMENT") {
579
- return {
580
- category: "bad_input",
581
- message: "The request was invalid. Check your message or API configuration."
582
- };
583
- }
584
- if (status >= 500 || errorType === "api_error" || errorType === "INTERNAL") {
585
- return {
586
- category: "server_error",
587
- message: "The AI service hit an internal error. Try again shortly."
588
- };
589
- }
590
- return {
591
- category: "unknown",
592
- message: "Something went wrong. Check your connection and try again."
593
- };
594
- }
595
- var useStyles4 = makeStyles4({
596
- root: {
597
- display: "flex",
598
- alignItems: "center",
599
- justifyContent: "space-between",
600
- gap: "8px",
601
- padding: "12px",
602
- borderRadius: tokens4.borderRadiusXLarge,
603
- backgroundColor: tokens4.colorNeutralBackground3,
604
- border: `1px solid ${tokens4.colorNeutralStroke2}`,
605
- color: tokens4.colorNeutralForeground3,
606
- width: "100%",
607
- boxSizing: "border-box",
608
- margin: "8px 0"
609
- },
610
- left: {
611
- display: "flex",
612
- alignItems: "center",
613
- gap: "8px",
614
- minWidth: 0
615
- },
616
- message: {
617
- fontFamily: "var(--fontFamilyMonospace)"
618
- }
619
- });
620
- var categoryIcon = {
621
- auth_billing: /* @__PURE__ */ jsx4(LockClosed20Regular, {}),
622
- rate_limit: /* @__PURE__ */ jsx4(Clock20Regular, {}),
623
- overloaded: /* @__PURE__ */ jsx4(PlugDisconnected20Regular, {}),
624
- bad_input: /* @__PURE__ */ jsx4(Warning20Regular, {}),
625
- not_found: /* @__PURE__ */ jsx4(ErrorCircle20Regular, {}),
626
- server_error: /* @__PURE__ */ jsx4(ErrorCircle20Regular, {}),
627
- unknown: /* @__PURE__ */ jsx4(Warning20Regular, {})
628
- };
629
- var ChatError = ({ error, onRetry }) => {
630
899
  const styles = useStyles4();
631
- const { category, message } = normalizeChatError(error);
632
- return /* @__PURE__ */ jsxs4("div", { className: styles.root, children: [
900
+ return /* @__PURE__ */ jsxs4("div", { className: mergeClasses(styles.root, className), children: [
633
901
  /* @__PURE__ */ jsxs4("span", { className: styles.left, children: [
634
- categoryIcon[category],
635
- /* @__PURE__ */ jsx4(Body12, { className: styles.message, children: message })
902
+ /* @__PURE__ */ jsx4(ChatWarning20Regular, {}),
903
+ /* @__PURE__ */ jsx4(Body12, { className: styles.label, children: message })
636
904
  ] }),
637
905
  onRetry && /* @__PURE__ */ jsx4(
638
906
  Button3,
639
907
  {
908
+ onClick: onRetry,
909
+ className: styles.retryButton,
640
910
  size: "small",
641
911
  appearance: "primary",
642
- onClick: onRetry,
643
912
  children: "Retry"
644
913
  }
645
914
  )
646
915
  ] });
647
916
  };
648
- var Chat_Error_default = ChatError;
917
+ var Chat_Callout_default = ChatCallout;
649
918
 
650
919
  // src/Chat.ReasoningBlock.tsx
651
920
  import { useState as useState3, useEffect as useEffect3, useRef as useRef3 } from "react";
@@ -766,10 +1035,168 @@ var ReasoningBlock = ({
766
1035
  };
767
1036
  var Chat_ReasoningBlock_default = ReasoningBlock;
768
1037
 
769
- // src/Chat.Messages.tsx
770
- import { CheckmarkCircle20Filled } from "@fluentui/react-icons";
1038
+ // src/Chat.TodoList.tsx
1039
+ import { useState as useState4 } from "react";
1040
+ import {
1041
+ Body1 as Body14,
1042
+ Caption1,
1043
+ makeStyles as makeStyles6,
1044
+ Spinner as Spinner2,
1045
+ tokens as tokens6
1046
+ } from "@fluentui/react-components";
1047
+ import {
1048
+ CheckmarkCircle16Filled,
1049
+ ChevronRight16Regular as ChevronRight16Regular2,
1050
+ Circle16Regular
1051
+ } from "@fluentui/react-icons";
771
1052
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
772
1053
  var useStyles6 = makeStyles6({
1054
+ root: {
1055
+ boxSizing: "border-box",
1056
+ display: "flex",
1057
+ flexDirection: "column",
1058
+ margin: "12px 0",
1059
+ borderRadius: "var(--borderRadiusXLarge)",
1060
+ backgroundColor: "var(--colorNeutralBackgroundAlpha2)",
1061
+ border: `1px solid ${tokens6.colorNeutralStroke2}`,
1062
+ overflow: "hidden"
1063
+ },
1064
+ header: {
1065
+ display: "flex",
1066
+ alignItems: "center",
1067
+ gap: "8px",
1068
+ padding: "10px 14px",
1069
+ cursor: "pointer",
1070
+ background: "none",
1071
+ border: "none",
1072
+ width: "100%",
1073
+ textAlign: "left"
1074
+ },
1075
+ label: {
1076
+ color: tokens6.colorNeutralForeground2,
1077
+ flexShrink: 0
1078
+ },
1079
+ count: {
1080
+ color: tokens6.colorNeutralForeground3,
1081
+ marginLeft: "auto",
1082
+ flexShrink: 0
1083
+ },
1084
+ chevron: {
1085
+ color: tokens6.colorNeutralForeground3,
1086
+ flexShrink: 0,
1087
+ transition: "transform 0.2s ease"
1088
+ },
1089
+ chevronExpanded: {
1090
+ transform: "rotate(90deg)"
1091
+ },
1092
+ list: {
1093
+ display: "flex",
1094
+ flexDirection: "column",
1095
+ gap: "8px",
1096
+ padding: "2px 14px 14px 14px"
1097
+ },
1098
+ item: {
1099
+ display: "flex",
1100
+ alignItems: "flex-start",
1101
+ gap: "8px"
1102
+ },
1103
+ iconCell: {
1104
+ display: "flex",
1105
+ alignItems: "center",
1106
+ justifyContent: "center",
1107
+ width: "16px",
1108
+ height: "20px",
1109
+ flexShrink: 0
1110
+ },
1111
+ pendingIcon: {
1112
+ color: tokens6.colorNeutralForeground4
1113
+ },
1114
+ doneIcon: {
1115
+ color: tokens6.colorStatusSuccessForeground1
1116
+ },
1117
+ text: {
1118
+ minWidth: 0,
1119
+ wordBreak: "break-word"
1120
+ },
1121
+ pendingText: {
1122
+ color: tokens6.colorNeutralForeground3
1123
+ },
1124
+ activeText: {
1125
+ color: tokens6.colorNeutralForeground1,
1126
+ fontWeight: tokens6.fontWeightSemibold
1127
+ },
1128
+ doneText: {
1129
+ color: tokens6.colorNeutralForeground3,
1130
+ textDecorationLine: "line-through"
1131
+ }
1132
+ });
1133
+ var ChatTodoList = ({
1134
+ todos,
1135
+ className,
1136
+ style
1137
+ }) => {
1138
+ const styles = useStyles6();
1139
+ const total = todos.length;
1140
+ const completed = todos.filter((t) => t.status === "completed").length;
1141
+ const [expanded, setExpanded] = useState4(true);
1142
+ if (total === 0) return null;
1143
+ return /* @__PURE__ */ jsxs6(
1144
+ "div",
1145
+ {
1146
+ className: `${styles.root}${className ? ` ${className}` : ""}`,
1147
+ style,
1148
+ role: "group",
1149
+ "aria-label": `Task plan, ${completed} of ${total} done`,
1150
+ children: [
1151
+ /* @__PURE__ */ jsxs6(
1152
+ "button",
1153
+ {
1154
+ type: "button",
1155
+ className: styles.header,
1156
+ onClick: () => setExpanded((e) => !e),
1157
+ "aria-expanded": expanded,
1158
+ children: [
1159
+ /* @__PURE__ */ jsx6(
1160
+ ChevronRight16Regular2,
1161
+ {
1162
+ className: `${styles.chevron}${expanded ? ` ${styles.chevronExpanded}` : ""}`
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsx6(Body14, { className: styles.label, children: "Tasks" }),
1166
+ /* @__PURE__ */ jsxs6(Caption1, { className: styles.count, children: [
1167
+ completed,
1168
+ "/",
1169
+ total
1170
+ ] })
1171
+ ]
1172
+ }
1173
+ ),
1174
+ expanded && /* @__PURE__ */ jsx6("div", { className: styles.list, role: "list", children: todos.map((todo, index) => {
1175
+ const text = todo.status === "in_progress" ? todo.activeForm ?? todo.content : todo.content;
1176
+ const textClass = todo.status === "completed" ? styles.doneText : todo.status === "in_progress" ? styles.activeText : styles.pendingText;
1177
+ return /* @__PURE__ */ jsxs6(
1178
+ "div",
1179
+ {
1180
+ className: styles.item,
1181
+ role: "listitem",
1182
+ children: [
1183
+ /* @__PURE__ */ jsx6("span", { className: styles.iconCell, children: todo.status === "completed" ? /* @__PURE__ */ jsx6(CheckmarkCircle16Filled, { className: styles.doneIcon }) : todo.status === "in_progress" ? /* @__PURE__ */ jsx6(Spinner2, { size: "extra-tiny" }) : /* @__PURE__ */ jsx6(Circle16Regular, { className: styles.pendingIcon }) }),
1184
+ /* @__PURE__ */ jsx6(Body14, { className: `${styles.text} ${textClass}`, children: text })
1185
+ ]
1186
+ },
1187
+ todo.id ?? `todo-${index}`
1188
+ );
1189
+ }) })
1190
+ ]
1191
+ }
1192
+ );
1193
+ };
1194
+ var Chat_TodoList_default = ChatTodoList;
1195
+
1196
+ // src/Chat.Messages.tsx
1197
+ import { CheckmarkCircle20Filled } from "@fluentui/react-icons";
1198
+ import { Fragment, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1199
+ var useStyles7 = makeStyles7({
773
1200
  messages: {
774
1201
  flex: "1",
775
1202
  overflowY: "auto",
@@ -790,43 +1217,119 @@ var useStyles6 = makeStyles6({
790
1217
  padding: "0 16px",
791
1218
  boxSizing: "border-box"
792
1219
  },
793
- messageUser: {
1220
+ // ── User message column: holds attachments + bubble + hover-only footer ──
1221
+ userRow: {
794
1222
  alignSelf: "flex-end",
1223
+ display: "flex",
1224
+ flexDirection: "column",
1225
+ alignItems: "flex-end",
795
1226
  maxWidth: "80%",
796
- backgroundColor: tokens6.colorNeutralBackground2,
797
- color: tokens6.colorNeutralForeground2,
798
- border: `1px solid ${tokens6.colorNeutralStroke2}`,
1227
+ "&:hover .coral-user-footer, &:focus-within .coral-user-footer": {
1228
+ opacity: 1
1229
+ }
1230
+ },
1231
+ userBubble: {
1232
+ backgroundColor: tokens7.colorNeutralBackground2,
1233
+ color: tokens7.colorNeutralForeground2,
1234
+ border: `1px solid ${tokens7.colorNeutralStroke2}`,
799
1235
  padding: "8px 12px",
800
- borderRadius: tokens6.borderRadiusXLarge,
1236
+ borderRadius: tokens7.borderRadiusXLarge,
801
1237
  wordBreak: "break-word",
802
1238
  boxSizing: "border-box"
803
1239
  },
1240
+ userFooter: {
1241
+ display: "flex",
1242
+ justifyContent: "flex-end",
1243
+ marginTop: "6px",
1244
+ marginRight: "-10px",
1245
+ opacity: 0,
1246
+ transition: "opacity 120ms ease-in-out"
1247
+ },
1248
+ userActions: {
1249
+ display: "flex"
1250
+ },
1251
+ // ── Inline edit mode ──
1252
+ userEditWrap: {
1253
+ display: "flex",
1254
+ flexDirection: "column",
1255
+ alignItems: "stretch",
1256
+ width: "min(640px, 80vw)",
1257
+ gap: "8px"
1258
+ },
1259
+ userEditTextarea: {
1260
+ backgroundColor: tokens7.colorNeutralBackground2,
1261
+ color: tokens7.colorNeutralForeground2,
1262
+ border: `1px solid ${tokens7.colorNeutralStroke1}`,
1263
+ padding: "8px 12px",
1264
+ borderRadius: tokens7.borderRadiusLarge,
1265
+ fontFamily: tokens7.fontFamilyBase,
1266
+ fontSize: tokens7.fontSizeBase300,
1267
+ lineHeight: tokens7.lineHeightBase300,
1268
+ resize: "none",
1269
+ outline: "none",
1270
+ width: "100%",
1271
+ boxSizing: "border-box",
1272
+ minHeight: "40px",
1273
+ "&:focus": {
1274
+ border: `1px solid ${tokens7.colorNeutralStroke3}`
1275
+ }
1276
+ },
1277
+ userEditFooter: {
1278
+ display: "flex",
1279
+ marginTop: "8px",
1280
+ // marginLeft: "12px",
1281
+ color: `${tokens7.colorNeutralForeground3}`,
1282
+ gap: "12px",
1283
+ alignItems: "center"
1284
+ },
1285
+ userEditActions: {
1286
+ display: "flex",
1287
+ justifyContent: "flex-end",
1288
+ gap: "8px"
1289
+ },
1290
+ // ── Assistant message ──
804
1291
  messageAssistant: {
805
1292
  alignSelf: "flex-start",
806
1293
  width: "100%",
807
- color: tokens6.colorNeutralForeground2,
1294
+ color: tokens7.colorNeutralForeground2,
808
1295
  wordBreak: "break-word",
809
- boxSizing: "border-box"
1296
+ boxSizing: "border-box",
1297
+ "&:hover .coral-assistant-footer, &:focus-within .coral-assistant-footer": {
1298
+ opacity: 1
1299
+ }
810
1300
  },
811
1301
  assistantFooter: {
812
1302
  display: "flex",
813
1303
  justifyContent: "space-between",
814
1304
  marginTop: "6px",
815
- marginLeft: "-10px"
1305
+ marginLeft: "-10px",
1306
+ // Hover-only by default; the latest message overrides this to stay visible.
1307
+ opacity: 0,
1308
+ transition: "opacity 120ms ease-in-out"
816
1309
  },
817
1310
  assistantActions: {
818
1311
  display: "flex"
1312
+ },
1313
+ copiedBadge: {
1314
+ display: "inline-flex",
1315
+ alignItems: "center",
1316
+ justifyContent: "center",
1317
+ width: "32px",
1318
+ height: "32px",
1319
+ color: tokens7.colorPaletteSeafoamForeground2
1320
+ },
1321
+ footerIconButton: {
1322
+ color: tokens7.colorNeutralForeground3
819
1323
  }
820
1324
  });
821
1325
  var ChatMessages = ({
822
1326
  components,
823
- onFeedback,
824
1327
  onCopy,
825
1328
  onNavigate,
826
1329
  fontSize,
827
1330
  fontFamily
828
1331
  }) => {
829
- const styles = useStyles6();
1332
+ const styles = useStyles7();
830
1333
  const ctx = useChatContext();
831
1334
  const {
832
1335
  messages,
@@ -835,35 +1338,204 @@ var ChatMessages = ({
835
1338
  isRetrying,
836
1339
  isInterrupted,
837
1340
  streamingReasoning,
838
- streamingAgentName,
1341
+ streamingStatus,
839
1342
  thinkingStatus,
840
- chatError,
841
1343
  sendMessage,
1344
+ regenerate,
1345
+ editMessage,
842
1346
  handleCopy,
843
1347
  lastUserInputRef,
844
1348
  registerMessagesContainer,
845
1349
  bottomRef
846
1350
  } = ctx;
847
1351
  const containerRef = useRef4(null);
848
- const [copiedIndex, setCopiedIndex] = useState4(null);
1352
+ const [surface, setSurface] = useState5(null);
1353
+ useLayoutEffect2(() => {
1354
+ const el = containerRef.current?.closest("[data-coral-surface]");
1355
+ setSurface(el?.getAttribute("data-coral-surface") ?? null);
1356
+ }, []);
1357
+ const isSidecar = surface === "panel-right";
1358
+ const [copiedKey, setCopiedKey] = useState5(null);
849
1359
  const copiedTimerRef = useRef4(null);
1360
+ const [editingId, setEditingId] = useState5(null);
1361
+ const [editDraft, setEditDraft] = useState5("");
1362
+ const editTextareaRef = useRef4(null);
850
1363
  useEffect4(() => {
851
1364
  registerMessagesContainer(containerRef.current);
852
1365
  return () => registerMessagesContainer(null);
853
1366
  }, []);
854
- const onCopyMerged = (content, index) => {
1367
+ useLayoutEffect2(() => {
1368
+ if (editingId && editTextareaRef.current) {
1369
+ const ta = editTextareaRef.current;
1370
+ ta.focus();
1371
+ ta.setSelectionRange(ta.value.length, ta.value.length);
1372
+ ta.style.height = "auto";
1373
+ ta.style.height = `${ta.scrollHeight}px`;
1374
+ }
1375
+ }, [editingId]);
1376
+ const startEdit = (msg) => {
1377
+ if (!msg.id) return;
1378
+ setEditingId(msg.id);
1379
+ setEditDraft(msg.content);
1380
+ };
1381
+ const cancelEdit = () => {
1382
+ setEditingId(null);
1383
+ setEditDraft("");
1384
+ };
1385
+ const saveEdit = async () => {
1386
+ if (!editingId) return;
1387
+ const trimmed = editDraft.trim();
1388
+ if (!trimmed) return;
1389
+ const id = editingId;
1390
+ setEditingId(null);
1391
+ setEditDraft("");
1392
+ await editMessage(id, trimmed);
1393
+ };
1394
+ const handleEditTextareaInput = (e) => {
1395
+ setEditDraft(e.target.value);
1396
+ const ta = e.target;
1397
+ ta.style.height = "auto";
1398
+ ta.style.height = `${ta.scrollHeight}px`;
1399
+ };
1400
+ const handleEditKeyDown = (e) => {
1401
+ if (e.key === "Escape") {
1402
+ e.preventDefault();
1403
+ cancelEdit();
1404
+ } else if (e.key === "Enter" && !e.shiftKey) {
1405
+ e.preventDefault();
1406
+ void saveEdit();
1407
+ }
1408
+ };
1409
+ const onCopyMerged = (content, key) => {
855
1410
  handleCopy(content);
856
1411
  onCopy?.(content);
857
1412
  if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
858
- setCopiedIndex(index);
859
- copiedTimerRef.current = setTimeout(() => setCopiedIndex(null), 1500);
1413
+ setCopiedKey(key);
1414
+ copiedTimerRef.current = setTimeout(() => setCopiedKey(null), 1500);
1415
+ };
1416
+ const renderUserMessage = (msg, index) => {
1417
+ const key = msg.id ?? `idx-${index}`;
1418
+ const isEditing = !!msg.id && editingId === msg.id;
1419
+ const attachments = Array.isArray(msg.metadata?.attachments) ? msg.metadata.attachments : null;
1420
+ return /* @__PURE__ */ jsxs7("div", { className: styles.userRow, children: [
1421
+ attachments && /* @__PURE__ */ jsx7(
1422
+ "div",
1423
+ {
1424
+ style: {
1425
+ display: "flex",
1426
+ gap: "6px",
1427
+ flexWrap: "wrap",
1428
+ marginBottom: "6px"
1429
+ },
1430
+ children: attachments.map(
1431
+ (a, i) => a.type.startsWith("image/") && a.preview ? /* @__PURE__ */ jsx7(
1432
+ "img",
1433
+ {
1434
+ src: a.preview,
1435
+ alt: a.name,
1436
+ style: {
1437
+ width: 64,
1438
+ height: 64,
1439
+ borderRadius: tokens7.borderRadiusMedium,
1440
+ objectFit: "cover"
1441
+ }
1442
+ },
1443
+ i
1444
+ ) : /* @__PURE__ */ jsxs7(
1445
+ "span",
1446
+ {
1447
+ style: {
1448
+ fontSize: 12,
1449
+ padding: "2px 8px",
1450
+ borderRadius: tokens7.borderRadiusSmall,
1451
+ backgroundColor: tokens7.colorNeutralBackground3,
1452
+ color: tokens7.colorNeutralForeground2
1453
+ },
1454
+ children: [
1455
+ "\u{1F4C4} ",
1456
+ a.name
1457
+ ]
1458
+ },
1459
+ i
1460
+ )
1461
+ )
1462
+ }
1463
+ ),
1464
+ isEditing ? /* @__PURE__ */ jsxs7("div", { className: styles.userEditWrap, children: [
1465
+ /* @__PURE__ */ jsx7(
1466
+ "textarea",
1467
+ {
1468
+ ref: editTextareaRef,
1469
+ className: styles.userEditTextarea,
1470
+ value: editDraft,
1471
+ onChange: handleEditTextareaInput,
1472
+ onKeyDown: handleEditKeyDown,
1473
+ rows: 1
1474
+ }
1475
+ ),
1476
+ /* @__PURE__ */ jsxs7("div", { className: styles.userEditFooter, children: [
1477
+ /* @__PURE__ */ jsx7(
1478
+ Info,
1479
+ {
1480
+ style: {
1481
+ marginRight: "6px",
1482
+ color: `${tokens7.colorNeutralForeground3}`,
1483
+ fontSize: "20px",
1484
+ boxSizing: "border-box",
1485
+ flexShrink: 0
1486
+ }
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ jsx7(Caption12, { children: "Editing this message will discard the current response and regenerate from here." }),
1490
+ /* @__PURE__ */ jsxs7("div", { className: styles.userEditActions, children: [
1491
+ /* @__PURE__ */ jsx7(Button4, { appearance: "subtle", onClick: cancelEdit, children: "Cancel" }),
1492
+ /* @__PURE__ */ jsx7(
1493
+ Button4,
1494
+ {
1495
+ appearance: "primary",
1496
+ onClick: saveEdit,
1497
+ disabled: !editDraft.trim(),
1498
+ children: "Save"
1499
+ }
1500
+ )
1501
+ ] })
1502
+ ] })
1503
+ ] }) : /* @__PURE__ */ jsxs7(Fragment, { children: [
1504
+ /* @__PURE__ */ jsx7("div", { className: styles.userBubble, children: /* @__PURE__ */ jsx7(Body15, { children: msg.content.split("\n").map((line, i, arr) => /* @__PURE__ */ jsxs7(React5.Fragment, { children: [
1505
+ line,
1506
+ i < arr.length - 1 && /* @__PURE__ */ jsx7("br", {})
1507
+ ] }, i)) }) }),
1508
+ msg.id && /* @__PURE__ */ jsx7("div", { className: `${styles.userFooter} coral-user-footer`, children: /* @__PURE__ */ jsxs7("div", { className: styles.userActions, children: [
1509
+ copiedKey === `user-${key}` ? /* @__PURE__ */ jsx7("span", { className: styles.copiedBadge, children: /* @__PURE__ */ jsx7(CheckmarkCircle20Filled, {}) }) : /* @__PURE__ */ jsx7(
1510
+ Button4,
1511
+ {
1512
+ onClick: () => onCopyMerged(msg.content, `user-${key}`),
1513
+ title: "Copy message",
1514
+ appearance: "subtle",
1515
+ className: styles.footerIconButton,
1516
+ icon: /* @__PURE__ */ jsx7(Copy, {})
1517
+ }
1518
+ ),
1519
+ /* @__PURE__ */ jsx7(
1520
+ Button4,
1521
+ {
1522
+ onClick: () => startEdit(msg),
1523
+ title: "Edit message",
1524
+ appearance: "subtle",
1525
+ className: styles.footerIconButton,
1526
+ icon: /* @__PURE__ */ jsx7(EditIcon, {})
1527
+ }
1528
+ )
1529
+ ] }) })
1530
+ ] })
1531
+ ] }, key);
860
1532
  };
861
1533
  const renderAssistantMessage = (msg, index) => {
862
1534
  const isLatest = index === messages.length - 1;
863
1535
  const reasoning = isLatest && isTyping ? streamingReasoning : msg.metadata?.reasoning;
864
1536
  if (msg.type && components?.[msg.type]) {
865
1537
  const CustomRenderer = components[msg.type];
866
- return /* @__PURE__ */ jsx6(
1538
+ return /* @__PURE__ */ jsx7(
867
1539
  CustomRenderer,
868
1540
  {
869
1541
  message: msg,
@@ -878,131 +1550,197 @@ var ChatMessages = ({
878
1550
  }
879
1551
  );
880
1552
  }
881
- return /* @__PURE__ */ jsxs6("div", { children: [
882
- reasoning && /* @__PURE__ */ jsx6(
1553
+ const key = msg.id ?? `idx-${index}`;
1554
+ return /* @__PURE__ */ jsxs7("div", { children: [
1555
+ reasoning && /* @__PURE__ */ jsx7(
883
1556
  Chat_ReasoningBlock_default,
884
1557
  {
885
1558
  content: reasoning,
886
1559
  isStreaming: isLatest && isTyping && !msg.content
887
1560
  }
888
1561
  ),
889
- /* @__PURE__ */ jsx6(CoralMarkdown, { onNavigate, fontSize, fontFamily, children: msg.content }),
890
- isInterrupted && !isTyping && index === messages.length - 1 && /* @__PURE__ */ jsx6(
1562
+ msg.todos && msg.todos.length > 0 && /* @__PURE__ */ jsx7(Chat_TodoList_default, { todos: msg.todos }),
1563
+ /* @__PURE__ */ jsx7(
1564
+ CoralMarkdown,
1565
+ {
1566
+ onNavigate,
1567
+ fontSize,
1568
+ fontFamily,
1569
+ children: msg.content
1570
+ }
1571
+ ),
1572
+ msg.error && /* @__PURE__ */ jsx7(
1573
+ Chat_Error_default,
1574
+ {
1575
+ error: msg.error,
1576
+ onRetry: () => {
1577
+ const userMsg = messages[index - 1];
1578
+ if (userMsg?.role === "user") sendMessage(userMsg.content);
1579
+ }
1580
+ }
1581
+ ),
1582
+ isInterrupted && !isTyping && isLatest && /* @__PURE__ */ jsx7(
891
1583
  Chat_Callout_default,
892
1584
  {
893
1585
  message: "Response was interrupted",
894
1586
  onRetry: () => sendMessage(lastUserInputRef.current)
895
1587
  }
896
1588
  ),
897
- !(isTyping && isLatest) && /* @__PURE__ */ jsx6("div", { className: `${styles.assistantFooter} coral-assistant-footer`, children: /* @__PURE__ */ jsxs6("div", { className: styles.assistantActions, children: [
898
- copiedIndex === index ? /* @__PURE__ */ jsx6(
899
- "span",
900
- {
901
- style: {
902
- display: "inline-flex",
903
- alignItems: "center",
904
- justifyContent: "center",
905
- width: "32px",
906
- height: "32px",
907
- color: tokens6.colorPaletteSeafoamForeground2
908
- },
909
- children: /* @__PURE__ */ jsx6(CheckmarkCircle20Filled, {})
1589
+ !(isTyping && isLatest) && msg.content.trim() && /* @__PURE__ */ jsx7(
1590
+ "div",
1591
+ {
1592
+ className: `${styles.assistantFooter} coral-assistant-footer`,
1593
+ style: isLatest ? { opacity: 1 } : void 0,
1594
+ children: /* @__PURE__ */ jsxs7("div", { className: styles.assistantActions, children: [
1595
+ copiedKey === `assistant-${key}` ? /* @__PURE__ */ jsx7("span", { className: styles.copiedBadge, children: /* @__PURE__ */ jsx7(CheckmarkCircle20Filled, {}) }) : /* @__PURE__ */ jsx7(
1596
+ Button4,
1597
+ {
1598
+ onClick: () => onCopyMerged(msg.content, `assistant-${key}`),
1599
+ title: "Copy response",
1600
+ appearance: "subtle",
1601
+ className: styles.footerIconButton,
1602
+ icon: /* @__PURE__ */ jsx7(Copy, {})
1603
+ }
1604
+ ),
1605
+ isLatest && /* @__PURE__ */ jsx7(
1606
+ Button4,
1607
+ {
1608
+ onClick: () => regenerate(msg.id),
1609
+ title: "Regenerate response",
1610
+ appearance: "subtle",
1611
+ className: styles.footerIconButton,
1612
+ icon: /* @__PURE__ */ jsx7(ArrowClockwise, {})
1613
+ }
1614
+ )
1615
+ ] })
1616
+ }
1617
+ )
1618
+ ] });
1619
+ };
1620
+ return /* @__PURE__ */ jsx7("div", { className: styles.messages, ref: containerRef, children: /* @__PURE__ */ jsxs7(
1621
+ "div",
1622
+ {
1623
+ className: styles.messageWrapper,
1624
+ style: isSidecar ? { padding: "0 8px" } : void 0,
1625
+ children: [
1626
+ messages.map((msg, index) => {
1627
+ if (msg.role === "user") {
1628
+ return renderUserMessage(msg, index);
910
1629
  }
911
- ) : /* @__PURE__ */ jsx6(
912
- Button4,
1630
+ const key = msg.id ?? `idx-${index}`;
1631
+ const isLatest = index === messages.length - 1;
1632
+ return /* @__PURE__ */ jsx7(
1633
+ "div",
1634
+ {
1635
+ className: `${styles.messageAssistant} coral-message-assistant${isLatest ? " is-latest" : ""}`,
1636
+ children: renderAssistantMessage(msg, index)
1637
+ },
1638
+ key
1639
+ );
1640
+ }),
1641
+ isTyping && /* @__PURE__ */ jsx7(
1642
+ Chat_ThinkingIndicator_default,
913
1643
  {
914
- onClick: () => onCopyMerged(msg.content, index),
915
- title: "Copy Response",
916
- appearance: "subtle",
917
- style: { color: tokens6.colorNeutralForeground3 },
918
- icon: /* @__PURE__ */ jsx6(Copy, {})
1644
+ isRetrying,
1645
+ status: thinkingStatus,
1646
+ messages: resolveTaskMessages(streamingStatus)
919
1647
  }
920
1648
  ),
921
- /* @__PURE__ */ jsx6(
922
- Button4,
923
- {
924
- onClick: () => onFeedback?.(index, "up"),
925
- title: "Like",
926
- appearance: "subtle",
927
- style: { color: tokens6.colorNeutralForeground3 },
928
- icon: /* @__PURE__ */ jsx6(Heart, {})
929
- }
930
- )
931
- ] }) })
932
- ] });
933
- };
934
- return /* @__PURE__ */ jsx6("div", { className: styles.messages, ref: containerRef, children: /* @__PURE__ */ jsxs6("div", { className: styles.messageWrapper, children: [
935
- messages.map((msg, index) => /* @__PURE__ */ jsxs6(
936
- "div",
937
- {
938
- className: msg.role === "user" ? styles.messageUser : `${styles.messageAssistant} coral-message-assistant${index === messages.length - 1 ? " is-latest" : ""}`,
939
- children: [
940
- msg.role === "user" && Array.isArray(msg.metadata?.attachments) && /* @__PURE__ */ jsx6("div", { style: { display: "flex", gap: "6px", flexWrap: "wrap", marginBottom: "6px" }, children: msg.metadata.attachments.map(
941
- (a, i) => a.type.startsWith("image/") && a.preview ? /* @__PURE__ */ jsx6(
942
- "img",
943
- {
944
- src: a.preview,
945
- alt: a.name,
946
- style: { width: 64, height: 64, borderRadius: "var(--borderRadiusMedium)", objectFit: "cover" }
947
- },
948
- i
949
- ) : /* @__PURE__ */ jsxs6(
950
- "span",
951
- {
952
- style: {
953
- fontSize: 12,
954
- padding: "2px 8px",
955
- borderRadius: "var(--borderRadiusSmall)",
956
- backgroundColor: tokens6.colorNeutralBackground3,
957
- color: tokens6.colorNeutralForeground2
958
- },
959
- children: [
960
- "\u{1F4C4} ",
961
- a.name
962
- ]
963
- },
964
- i
965
- )
966
- ) }),
967
- /* @__PURE__ */ jsx6(Body14, { children: msg.role === "user" ? msg.content.split("\n").map((line, i, arr) => /* @__PURE__ */ jsxs6(React4.Fragment, { children: [
968
- line,
969
- i < arr.length - 1 && /* @__PURE__ */ jsx6("br", {})
970
- ] }, i)) : renderAssistantMessage(msg, index) })
971
- ]
972
- },
973
- msg.id ?? index
974
- )),
975
- chatError !== null && !isTyping && /* @__PURE__ */ jsx6(
976
- Chat_Error_default,
977
- {
978
- error: chatError,
979
- onRetry: () => sendMessage(lastUserInputRef.current)
980
- }
981
- ),
982
- isTyping && /* @__PURE__ */ jsx6(
983
- Chat_ThinkingIndicator_default,
984
- {
985
- isRetrying,
986
- status: streamingAgentName ? `${streamingAgentName}...` : thinkingStatus
987
- }
988
- ),
989
- /* @__PURE__ */ jsx6("div", { ref: bottomRef })
990
- ] }) });
1649
+ /* @__PURE__ */ jsx7("div", { ref: bottomRef })
1650
+ ]
1651
+ }
1652
+ ) });
991
1653
  };
992
1654
  var Chat_Messages_default = ChatMessages;
993
1655
 
994
1656
  // src/Chat.Input.tsx
995
1657
  import {
996
- useRef as useRef5,
997
- useState as useState5,
1658
+ useRef as useRef6,
1659
+ useState as useState6,
998
1660
  useEffect as useEffect5,
999
1661
  forwardRef,
1000
1662
  useImperativeHandle
1001
1663
  } from "react";
1002
- import { Body1 as Body15, Button as Button5, Tag, makeStyles as makeStyles7, tokens as tokens7 } from "@fluentui/react-components";
1664
+ import { Body1 as Body16, Button as Button6, Tag, makeStyles as makeStyles8, tokens as tokens8 } from "@fluentui/react-components";
1003
1665
  import { Dismiss, Send, Stop } from "@coral-ai/bundleicons";
1004
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1005
- var useStyles7 = makeStyles7({
1666
+
1667
+ // src/Chat.AttachButton.tsx
1668
+ import { useRef as useRef5 } from "react";
1669
+ import {
1670
+ Button as Button5,
1671
+ Menu,
1672
+ MenuTrigger,
1673
+ MenuPopover,
1674
+ MenuList,
1675
+ MenuItem
1676
+ } from "@fluentui/react-components";
1677
+ import { Add, DocumentAdd, ImageAdd } from "@coral-ai/bundleicons";
1678
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1679
+ var ChatAttachButton = ({
1680
+ types = ["files", "photos"]
1681
+ }) => {
1682
+ const { addAttachments } = useChatContext();
1683
+ const fileRef = useRef5(null);
1684
+ const photoRef = useRef5(null);
1685
+ const showFiles = types.includes("files");
1686
+ const showPhotos = types.includes("photos");
1687
+ return /* @__PURE__ */ jsxs8(Fragment2, { children: [
1688
+ showFiles && /* @__PURE__ */ jsx8(
1689
+ "input",
1690
+ {
1691
+ ref: fileRef,
1692
+ type: "file",
1693
+ hidden: true,
1694
+ multiple: true,
1695
+ onChange: (e) => {
1696
+ if (e.target.files) addAttachments([...e.target.files]);
1697
+ e.target.value = "";
1698
+ }
1699
+ }
1700
+ ),
1701
+ showPhotos && /* @__PURE__ */ jsx8(
1702
+ "input",
1703
+ {
1704
+ ref: photoRef,
1705
+ type: "file",
1706
+ hidden: true,
1707
+ multiple: true,
1708
+ accept: "image/*",
1709
+ onChange: (e) => {
1710
+ if (e.target.files) addAttachments([...e.target.files]);
1711
+ e.target.value = "";
1712
+ }
1713
+ }
1714
+ ),
1715
+ /* @__PURE__ */ jsxs8(Menu, { children: [
1716
+ /* @__PURE__ */ jsx8(MenuTrigger, { children: /* @__PURE__ */ jsx8(Button5, { appearance: "subtle", icon: /* @__PURE__ */ jsx8(Add, {}) }) }),
1717
+ /* @__PURE__ */ jsx8(MenuPopover, { children: /* @__PURE__ */ jsx8(MenuList, { children: types.map(
1718
+ (t) => t === "files" ? /* @__PURE__ */ jsx8(
1719
+ MenuItem,
1720
+ {
1721
+ icon: /* @__PURE__ */ jsx8(DocumentAdd, {}),
1722
+ onClick: () => fileRef.current?.click(),
1723
+ children: "Add files"
1724
+ },
1725
+ "files"
1726
+ ) : /* @__PURE__ */ jsx8(
1727
+ MenuItem,
1728
+ {
1729
+ icon: /* @__PURE__ */ jsx8(ImageAdd, {}),
1730
+ onClick: () => photoRef.current?.click(),
1731
+ children: "Add photos"
1732
+ },
1733
+ "photos"
1734
+ )
1735
+ ) }) })
1736
+ ] })
1737
+ ] });
1738
+ };
1739
+ var Chat_AttachButton_default = ChatAttachButton;
1740
+
1741
+ // src/Chat.Input.tsx
1742
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1743
+ var useStyles8 = makeStyles8({
1006
1744
  wrapper: {
1007
1745
  width: "100%",
1008
1746
  margin: "0 auto"
@@ -1022,7 +1760,7 @@ var useStyles7 = makeStyles7({
1022
1760
  overflow: "hidden"
1023
1761
  },
1024
1762
  cardFocused: {
1025
- boxShadow: tokens7.shadow28
1763
+ boxShadow: tokens8.shadow28
1026
1764
  },
1027
1765
  cardDisabled: {
1028
1766
  opacity: "0.3",
@@ -1041,7 +1779,7 @@ var useStyles7 = makeStyles7({
1041
1779
  height: "128px",
1042
1780
  borderRadius: "var(--borderRadiusMedium)",
1043
1781
  overflow: "hidden",
1044
- border: `1px solid ${tokens7.colorNeutralStroke2}`,
1782
+ border: `1px solid ${tokens8.colorNeutralStroke2}`,
1045
1783
  backgroundColor: "var(--colorNeutralBackground3)",
1046
1784
  display: "flex",
1047
1785
  flexDirection: "column"
@@ -1127,18 +1865,20 @@ var ChatInput = forwardRef(
1127
1865
  actionsStart,
1128
1866
  actionsEnd,
1129
1867
  attachments: attachmentsProp,
1130
- removeAttachment: removeAttachmentProp
1868
+ removeAttachment: removeAttachmentProp,
1869
+ allowedAttachments: allowedAttachmentsProp
1131
1870
  }, ref) => {
1132
- const styles = useStyles7();
1133
- const [isFocused, setIsFocused] = useState5(false);
1134
- const textareaRef = useRef5(null);
1135
- const containerRef = useRef5(null);
1871
+ const styles = useStyles8();
1872
+ const [isFocused, setIsFocused] = useState6(false);
1873
+ const textareaRef = useRef6(null);
1874
+ const containerRef = useRef6(null);
1136
1875
  useImperativeHandle(ref, () => textareaRef.current);
1137
1876
  const ctx = useChatContext();
1138
1877
  const isInsideCanvas = ctx.setInput !== void 0;
1139
1878
  const attachments = isInsideCanvas ? ctx.attachments : attachmentsProp ?? [];
1140
1879
  const removeAttachment = isInsideCanvas ? ctx.removeAttachment : removeAttachmentProp ?? (() => {
1141
1880
  });
1881
+ const allowedAttachments = allowedAttachmentsProp ?? (isInsideCanvas ? ctx.allowedAttachments : void 0);
1142
1882
  const value = valueProp ?? (isInsideCanvas ? ctx.input : "");
1143
1883
  const onChange = onChangeProp ?? (isInsideCanvas ? ctx.setInput : () => {
1144
1884
  });
@@ -1173,53 +1913,53 @@ var ChatInput = forwardRef(
1173
1913
  observer.observe(el);
1174
1914
  return () => observer.disconnect();
1175
1915
  }, []);
1176
- const sendStopButton = isInsideCanvas ? ctx.isTyping ? /* @__PURE__ */ jsx7(
1177
- Button5,
1916
+ const sendStopButton = isInsideCanvas ? ctx.isTyping ? /* @__PURE__ */ jsx9(
1917
+ Button6,
1178
1918
  {
1179
1919
  appearance: "transparent",
1180
1920
  onClick: () => {
1181
1921
  ctx.shouldStopRef.current = true;
1182
1922
  ctx.onStop?.();
1183
1923
  },
1184
- icon: /* @__PURE__ */ jsx7(Stop, {}),
1924
+ icon: /* @__PURE__ */ jsx9(Stop, {}),
1185
1925
  style: { cursor: "default" }
1186
1926
  }
1187
- ) : /* @__PURE__ */ jsx7(
1188
- Button5,
1927
+ ) : /* @__PURE__ */ jsx9(
1928
+ Button6,
1189
1929
  {
1190
1930
  appearance: "transparent",
1191
1931
  onClick: () => ctx.sendMessage(),
1192
- icon: /* @__PURE__ */ jsx7(Send, {}),
1932
+ icon: /* @__PURE__ */ jsx9(Send, {}),
1193
1933
  disabled: !value.trim(),
1194
1934
  style: { cursor: "default" }
1195
1935
  }
1196
1936
  ) : null;
1197
- return /* @__PURE__ */ jsx7("div", { ref: containerRef, className: styles.wrapper, style, children: /* @__PURE__ */ jsxs7(
1937
+ return /* @__PURE__ */ jsx9("div", { ref: containerRef, className: styles.wrapper, style, children: /* @__PURE__ */ jsxs9(
1198
1938
  "div",
1199
1939
  {
1200
1940
  className: `${styles.card}${isFocused ? ` ${styles.cardFocused}` : ""}${disabled ? ` ${styles.cardDisabled}` : ""}`,
1201
1941
  style: {
1202
- border: isFocused ? `1px solid ${tokens7.colorNeutralStroke1}` : `1px solid ${tokens7.colorNeutralStroke2}`
1942
+ border: isFocused ? `1px solid ${tokens8.colorNeutralStroke1}` : `1px solid ${tokens8.colorNeutralStroke2}`
1203
1943
  },
1204
1944
  children: [
1205
- attachments.length > 0 && /* @__PURE__ */ jsx7("div", { className: styles.attachmentsRow, children: attachments.map((a) => {
1945
+ attachments.length > 0 && /* @__PURE__ */ jsx9("div", { className: styles.attachmentsRow, children: attachments.map((a) => {
1206
1946
  const ext = a.file.name.split(".").pop()?.toUpperCase() ?? "FILE";
1207
- return /* @__PURE__ */ jsxs7("div", { className: styles.attachmentItem, children: [
1208
- a.preview ? /* @__PURE__ */ jsx7(
1947
+ return /* @__PURE__ */ jsxs9("div", { className: styles.attachmentItem, children: [
1948
+ a.preview ? /* @__PURE__ */ jsx9(
1209
1949
  "img",
1210
1950
  {
1211
1951
  src: a.preview,
1212
1952
  alt: a.file.name,
1213
1953
  className: styles.attachmentImage
1214
1954
  }
1215
- ) : /* @__PURE__ */ jsxs7("div", { className: styles.attachmentFile, children: [
1216
- /* @__PURE__ */ jsx7(Body15, { className: styles.attachmentFileName, children: a.file.name }),
1217
- /* @__PURE__ */ jsx7(Tag, { appearance: "outline", size: "small", children: ext })
1955
+ ) : /* @__PURE__ */ jsxs9("div", { className: styles.attachmentFile, children: [
1956
+ /* @__PURE__ */ jsx9(Body16, { className: styles.attachmentFileName, children: a.file.name }),
1957
+ /* @__PURE__ */ jsx9(Tag, { appearance: "outline", size: "small", children: ext })
1218
1958
  ] }),
1219
- /* @__PURE__ */ jsx7(
1220
- Button5,
1959
+ /* @__PURE__ */ jsx9(
1960
+ Button6,
1221
1961
  {
1222
- icon: /* @__PURE__ */ jsx7(Dismiss, {}),
1962
+ icon: /* @__PURE__ */ jsx9(Dismiss, {}),
1223
1963
  size: "small",
1224
1964
  onClick: () => removeAttachment(a.id),
1225
1965
  className: styles.attachmentDismiss
@@ -1227,7 +1967,7 @@ var ChatInput = forwardRef(
1227
1967
  )
1228
1968
  ] }, a.id);
1229
1969
  }) }),
1230
- /* @__PURE__ */ jsx7(
1970
+ /* @__PURE__ */ jsx9(
1231
1971
  "textarea",
1232
1972
  {
1233
1973
  disabled,
@@ -1247,14 +1987,17 @@ var ChatInput = forwardRef(
1247
1987
  className: styles.textarea
1248
1988
  }
1249
1989
  ),
1250
- /* @__PURE__ */ jsxs7("div", { className: styles.toolbar, children: [
1251
- /* @__PURE__ */ jsx7("div", { className: styles.toolbarSide, children: actionsStart }),
1252
- /* @__PURE__ */ jsxs7("div", { className: styles.toolbarSide, children: [
1990
+ /* @__PURE__ */ jsxs9("div", { className: styles.toolbar, children: [
1991
+ /* @__PURE__ */ jsxs9("div", { className: styles.toolbarSide, children: [
1992
+ allowedAttachments && allowedAttachments.length > 0 && /* @__PURE__ */ jsx9(Chat_AttachButton_default, { types: allowedAttachments }),
1993
+ actionsStart
1994
+ ] }),
1995
+ /* @__PURE__ */ jsxs9("div", { className: styles.toolbarSide, children: [
1253
1996
  actionsEnd,
1254
1997
  sendStopButton
1255
1998
  ] })
1256
1999
  ] }),
1257
- /* @__PURE__ */ jsx7(
2000
+ /* @__PURE__ */ jsx9(
1258
2001
  "span",
1259
2002
  {
1260
2003
  className: `${styles.focusBar} ${isFocused ? styles.focusBarVisible : styles.focusBarHidden}`
@@ -1267,9 +2010,112 @@ var ChatInput = forwardRef(
1267
2010
  );
1268
2011
  var Chat_Input_default = ChatInput;
1269
2012
 
2013
+ // src/Chat.Approval.tsx
2014
+ import { Body1 as Body17, Body1Strong, Button as Button7, Caption1 as Caption13, makeStyles as makeStyles9, tokens as tokens9 } from "@fluentui/react-components";
2015
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2016
+ var useStyles9 = makeStyles9({
2017
+ root: {
2018
+ width: "100%",
2019
+ boxSizing: "border-box",
2020
+ display: "flex",
2021
+ flexDirection: "column",
2022
+ gap: "24px",
2023
+ padding: "12px 14px",
2024
+ marginBottom: "8px",
2025
+ borderRadius: "var(--borderRadiusXLarge)",
2026
+ backgroundColor: "var(--colorNeutralBackgroundAlpha2)",
2027
+ border: `1px solid ${tokens9.colorNeutralStroke2}`
2028
+ },
2029
+ header: {
2030
+ display: "flex",
2031
+ flexDirection: "column",
2032
+ gap: "2px"
2033
+ },
2034
+ description: {
2035
+ color: tokens9.colorNeutralForeground3
2036
+ },
2037
+ optionsRow: {
2038
+ display: "flex",
2039
+ flexWrap: "wrap",
2040
+ gap: "6px"
2041
+ },
2042
+ optionsColumn: {
2043
+ display: "flex",
2044
+ flexDirection: "column",
2045
+ gap: "6px"
2046
+ },
2047
+ optionStacked: {
2048
+ justifyContent: "flex-start"
2049
+ },
2050
+ hint: {
2051
+ color: tokens9.colorNeutralForeground3
2052
+ // fontStyle: "italic",
2053
+ }
2054
+ });
2055
+ function shouldStackVertically(options) {
2056
+ if (options.length > 3) return true;
2057
+ const totalLength = options.reduce((sum, o) => sum + o.label.length, 0);
2058
+ if (totalLength > 40) return true;
2059
+ if (options.some((o) => !!o.description)) return true;
2060
+ return false;
2061
+ }
2062
+ var ChatApproval = ({
2063
+ approval: approvalProp,
2064
+ onSelect: onSelectProp,
2065
+ disabled = false,
2066
+ className,
2067
+ style
2068
+ }) => {
2069
+ const styles = useStyles9();
2070
+ const ctx = useChatContext();
2071
+ const approval = approvalProp ?? ctx.pendingApproval;
2072
+ if (!approval) return null;
2073
+ const onSelect = onSelectProp ?? ((optionId) => ctx.resolveApproval(approval.id, optionId));
2074
+ const answeredId = approval.answer?.optionId;
2075
+ const isAnswered = answeredId !== void 0;
2076
+ const awaitingFreeform = ctx.awaitingFreeformApproval?.approvalId === approval.id ? ctx.awaitingFreeformApproval : null;
2077
+ const stack = shouldStackVertically(approval.options);
2078
+ return /* @__PURE__ */ jsxs10(
2079
+ "div",
2080
+ {
2081
+ className: `${styles.root}${className ? ` ${className}` : ""}`,
2082
+ style,
2083
+ role: "group",
2084
+ "aria-label": approval.question,
2085
+ children: [
2086
+ /* @__PURE__ */ jsxs10("div", { className: styles.header, children: [
2087
+ /* @__PURE__ */ jsx10(Body1Strong, { children: approval.question }),
2088
+ approval.description && /* @__PURE__ */ jsx10(Body17, { className: styles.description, children: approval.description })
2089
+ ] }),
2090
+ /* @__PURE__ */ jsx10("div", { className: stack ? styles.optionsColumn : styles.optionsRow, children: approval.options.map((opt) => {
2091
+ const selected = answeredId === opt.id || awaitingFreeform?.optionId === opt.id;
2092
+ const baseAppearance = opt.appearance ?? (opt.freeform ? "subtle" : "outline");
2093
+ const appearance = selected ? "primary" : baseAppearance;
2094
+ return /* @__PURE__ */ jsx10(
2095
+ Button7,
2096
+ {
2097
+ appearance,
2098
+ disabled: disabled || isAnswered,
2099
+ onClick: () => onSelect(opt.id),
2100
+ className: stack ? styles.optionStacked : void 0,
2101
+ style: {
2102
+ height: "40px"
2103
+ },
2104
+ children: opt.label
2105
+ },
2106
+ opt.id
2107
+ );
2108
+ }) }),
2109
+ awaitingFreeform && !isAnswered && /* @__PURE__ */ jsx10(Caption13, { className: styles.hint, children: "Type your reply in the message box below." })
2110
+ ]
2111
+ }
2112
+ );
2113
+ };
2114
+ var Chat_Approval_default = ChatApproval;
2115
+
1270
2116
  // src/Chat.tsx
1271
- import { Fragment, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1272
- var useStyles8 = makeStyles8({
2117
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2118
+ var useStyles10 = makeStyles10({
1273
2119
  emptySlot: {
1274
2120
  width: "100%",
1275
2121
  maxWidth: "768px",
@@ -1299,49 +2145,61 @@ var useStyles8 = makeStyles8({
1299
2145
  padding: "0 16px 16px 16px",
1300
2146
  boxSizing: "border-box"
1301
2147
  },
1302
- starterRow: {
2148
+ suggestionsRow: {
1303
2149
  display: "flex",
1304
- flexWrap: "wrap",
1305
- justifyContent: "center",
1306
- gap: "6px",
1307
2150
  width: "100%",
1308
2151
  maxWidth: "768px",
1309
2152
  margin: "0 auto",
1310
- padding: "0 16px",
2153
+ padding: "0 16px 16px 16px",
1311
2154
  boxSizing: "border-box",
1312
- overflow: "hidden"
2155
+ justifyContent: "center"
1313
2156
  }
1314
2157
  });
2158
+ var remapFontSizeForSurface = (size, surface) => {
2159
+ if (surface === "panel-right" && (size === void 0 || size === "default")) {
2160
+ return "small";
2161
+ }
2162
+ return size;
2163
+ };
1315
2164
  var ChatLayout = ({
1316
- emptyState,
1317
- promptStarters,
2165
+ welcomeMessage,
2166
+ promptSuggestions,
1318
2167
  surfaceMotion,
1319
2168
  placeholder,
1320
2169
  disabled,
1321
2170
  actionsStart,
1322
2171
  actionsEnd,
1323
2172
  components,
1324
- onFeedback,
1325
2173
  onCopy,
1326
2174
  onNavigate,
1327
2175
  fontSize,
1328
2176
  fontFamily
1329
2177
  }) => {
1330
- const styles = useStyles8();
1331
- const { messages, isTyping, sendMessage } = useChatContext();
2178
+ const welcomeIsFill = welcomeMessage !== void 0 && typeof welcomeMessage !== "string";
2179
+ const welcomeNode = typeof welcomeMessage === "string" ? /* @__PURE__ */ jsx11(Title3, { children: welcomeMessage }) : welcomeMessage;
2180
+ const styles = useStyles10();
2181
+ const surfaceProbeRef = useRef7(null);
2182
+ const [surface, setSurface] = useState7(null);
2183
+ useLayoutEffect3(() => {
2184
+ const el = surfaceProbeRef.current?.closest("[data-coral-surface]");
2185
+ setSurface(el?.getAttribute("data-coral-surface") ?? null);
2186
+ }, []);
2187
+ const effectiveFontSize = remapFontSizeForSurface(fontSize, surface);
2188
+ const { messages, isTyping, pendingApproval, awaitingFreeformApproval } = useChatContext();
1332
2189
  const empty = messages.every((m) => m.role === "system") && !isTyping;
2190
+ const approvalLocksInput = pendingApproval !== null && awaitingFreeformApproval === null;
2191
+ const inputDisabled = disabled || approvalLocksInput;
1333
2192
  const motionEnabled = surfaceMotion !== null;
1334
2193
  const curve = motionTokens.curveDecelerateMax;
1335
2194
  const spacerTransition = motionEnabled ? `flex-grow 300ms ${curve}` : "none";
1336
2195
  const gapTransition = motionEnabled ? `height 300ms ${curve}` : "none";
1337
- const starterTransition = motionEnabled ? `opacity 300ms ${curve}, max-height 300ms ${curve}, padding-bottom 300ms ${curve}` : "none";
1338
- const hasStarters = !!promptStarters && promptStarters.length > 0;
1339
- return /* @__PURE__ */ jsxs8(Fragment, { children: [
1340
- /* @__PURE__ */ jsx8(
2196
+ return /* @__PURE__ */ jsxs11(Fragment3, { children: [
2197
+ /* @__PURE__ */ jsx11(
1341
2198
  "div",
1342
2199
  {
2200
+ ref: surfaceProbeRef,
1343
2201
  style: {
1344
- flexGrow: empty ? 1 : 0,
2202
+ flexGrow: empty && !welcomeIsFill ? 1 : 0,
1345
2203
  flexShrink: 1,
1346
2204
  flexBasis: 0,
1347
2205
  minHeight: 0,
@@ -1349,67 +2207,56 @@ var ChatLayout = ({
1349
2207
  }
1350
2208
  }
1351
2209
  ),
1352
- /* @__PURE__ */ jsx8(
2210
+ /* @__PURE__ */ jsx11(
1353
2211
  "div",
1354
2212
  {
1355
2213
  className: styles.contentArea,
1356
2214
  style: {
1357
- flex: empty ? "none" : 1,
1358
- overflow: empty ? "visible" : "hidden"
2215
+ flex: empty && !welcomeIsFill ? "none" : 1,
2216
+ overflow: empty && !welcomeIsFill ? "visible" : "hidden"
1359
2217
  },
1360
- children: empty ? /* @__PURE__ */ jsx8("div", { className: styles.emptySlot, children: emptyState }) : /* @__PURE__ */ jsx8(
2218
+ children: empty ? /* @__PURE__ */ jsx11(
2219
+ "div",
2220
+ {
2221
+ className: styles.emptySlot,
2222
+ style: welcomeIsFill ? {
2223
+ flex: 1,
2224
+ justifyContent: "center",
2225
+ alignContent: "center"
2226
+ } : void 0,
2227
+ children: welcomeNode
2228
+ }
2229
+ ) : /* @__PURE__ */ jsx11(
1361
2230
  Chat_Messages_default,
1362
2231
  {
1363
2232
  components,
1364
- onFeedback,
1365
2233
  onCopy,
1366
2234
  onNavigate,
1367
- fontSize,
2235
+ fontSize: effectiveFontSize,
1368
2236
  fontFamily
1369
2237
  }
1370
2238
  )
1371
2239
  }
1372
2240
  ),
1373
- /* @__PURE__ */ jsx8("div", { style: { height: empty ? 24 : 0, transition: gapTransition } }),
1374
- /* @__PURE__ */ jsx8("div", { className: styles.inputRow, children: /* @__PURE__ */ jsx8("div", { className: styles.inputInner, children: /* @__PURE__ */ jsx8(
1375
- Chat_Input_default,
1376
- {
1377
- placeholder,
1378
- disabled,
1379
- actionsStart,
1380
- actionsEnd
1381
- }
1382
- ) }) }),
1383
- hasStarters && /* @__PURE__ */ jsx8(
1384
- "div",
1385
- {
1386
- className: styles.starterRow,
1387
- style: {
1388
- opacity: empty ? 1 : 0,
1389
- maxHeight: empty ? 160 : 0,
1390
- paddingBottom: empty ? 16 : 0,
1391
- pointerEvents: empty ? "auto" : "none",
1392
- transition: starterTransition,
1393
- gap: "8px"
1394
- },
1395
- "aria-hidden": !empty,
1396
- children: promptStarters.map((p) => /* @__PURE__ */ jsx8(
1397
- InteractionTag,
1398
- {
1399
- appearance: "outline",
1400
- onClick: () => sendMessage(p),
1401
- tabIndex: empty ? 0 : -1,
1402
- children: /* @__PURE__ */ jsx8(InteractionTagPrimary, { children: p })
1403
- },
1404
- p
1405
- ))
1406
- }
1407
- ),
1408
- /* @__PURE__ */ jsx8(
2241
+ /* @__PURE__ */ jsx11("div", { style: { height: empty ? 24 : 0, transition: gapTransition } }),
2242
+ /* @__PURE__ */ jsx11("div", { className: styles.inputRow, children: /* @__PURE__ */ jsxs11("div", { className: styles.inputInner, style: { flexDirection: "column" }, children: [
2243
+ pendingApproval && /* @__PURE__ */ jsx11(Chat_Approval_default, {}),
2244
+ /* @__PURE__ */ jsx11(
2245
+ Chat_Input_default,
2246
+ {
2247
+ placeholder: approvalLocksInput ? "Choose an option above to continue..." : placeholder,
2248
+ disabled: inputDisabled,
2249
+ actionsStart,
2250
+ actionsEnd
2251
+ }
2252
+ )
2253
+ ] }) }),
2254
+ empty && promptSuggestions && /* @__PURE__ */ jsx11("div", { className: styles.suggestionsRow, children: promptSuggestions }),
2255
+ /* @__PURE__ */ jsx11(
1409
2256
  "div",
1410
2257
  {
1411
2258
  style: {
1412
- flexGrow: empty ? 1 : 0,
2259
+ flexGrow: empty && !welcomeIsFill ? 1 : 0,
1413
2260
  flexShrink: 1,
1414
2261
  flexBasis: 0,
1415
2262
  minHeight: 0,
@@ -1417,8 +2264,8 @@ var ChatLayout = ({
1417
2264
  }
1418
2265
  }
1419
2266
  ),
1420
- /* @__PURE__ */ jsx8(
1421
- Caption1,
2267
+ /* @__PURE__ */ jsx11(
2268
+ Caption14,
1422
2269
  {
1423
2270
  style: {
1424
2271
  display: "flex",
@@ -1433,8 +2280,8 @@ var ChatLayout = ({
1433
2280
  ] });
1434
2281
  };
1435
2282
  var Chat = ({
1436
- agent,
1437
- agents,
2283
+ api,
2284
+ allowedAttachments,
1438
2285
  conversationId,
1439
2286
  initialMessages,
1440
2287
  systemPrompt,
@@ -1445,11 +2292,10 @@ var Chat = ({
1445
2292
  autoSendMessage,
1446
2293
  thinkingStatus,
1447
2294
  onStop,
1448
- emptyState,
1449
- promptStarters,
2295
+ welcomeMessage,
2296
+ promptSuggestions,
1450
2297
  surfaceMotion,
1451
2298
  components,
1452
- onFeedback,
1453
2299
  onCopy,
1454
2300
  onNavigate,
1455
2301
  placeholder,
@@ -1459,11 +2305,14 @@ var Chat = ({
1459
2305
  fontSize,
1460
2306
  fontFamily
1461
2307
  }) => {
1462
- return /* @__PURE__ */ jsx8(
2308
+ const chatConfig = useCoralConfig()?.chat;
2309
+ const resolvedApi = api ?? chatConfig?.api;
2310
+ const resolvedAllowedAttachments = allowedAttachments ?? chatConfig?.attachments;
2311
+ return /* @__PURE__ */ jsx11(
1463
2312
  Chat_Canvas_default,
1464
2313
  {
1465
- agent,
1466
- agents,
2314
+ api: resolvedApi,
2315
+ allowedAttachments: resolvedAllowedAttachments,
1467
2316
  conversationId,
1468
2317
  initialMessages,
1469
2318
  systemPrompt,
@@ -1474,18 +2323,17 @@ var Chat = ({
1474
2323
  autoSendMessage,
1475
2324
  thinkingStatus,
1476
2325
  onStop,
1477
- children: /* @__PURE__ */ jsx8(
2326
+ children: /* @__PURE__ */ jsx11(
1478
2327
  ChatLayout,
1479
2328
  {
1480
- emptyState,
1481
- promptStarters,
2329
+ welcomeMessage,
2330
+ promptSuggestions,
1482
2331
  surfaceMotion,
1483
2332
  placeholder,
1484
2333
  disabled,
1485
2334
  actionsStart,
1486
2335
  actionsEnd,
1487
2336
  components,
1488
- onFeedback,
1489
2337
  onCopy,
1490
2338
  onNavigate,
1491
2339
  fontSize,
@@ -1499,11 +2347,18 @@ var Chat_default = Chat;
1499
2347
 
1500
2348
  // src/createHandler.ts
1501
2349
  function createHandler({ endpoint, headers }) {
1502
- return async function* (input, history, context) {
2350
+ return async function* (input, history, files) {
2351
+ const attachments = await Promise.all(
2352
+ files.map(async (file) => ({
2353
+ name: file.name,
2354
+ mediaType: file.type,
2355
+ data: await fileToBase64(file)
2356
+ }))
2357
+ );
1503
2358
  const res = await fetch(endpoint, {
1504
2359
  method: "POST",
1505
2360
  headers: { "Content-Type": "application/json", ...headers },
1506
- body: JSON.stringify({ input, history, context })
2361
+ body: JSON.stringify({ input, history, attachments })
1507
2362
  });
1508
2363
  if (!res.ok || !res.body) {
1509
2364
  throw Object.assign(new Error(`${res.status} ${res.statusText}`), {
@@ -1534,20 +2389,407 @@ function createHandler({ endpoint, headers }) {
1534
2389
  };
1535
2390
  }
1536
2391
 
2392
+ // src/Chat.Agent.ts
2393
+ var UPDATE_TODOS_TOOL = {
2394
+ name: "update_todos",
2395
+ description: "Display or update the multi-step task plan shown to the user as an inline checklist. Call this when you begin a task that takes 3 or more distinct steps, and again every time a step changes status. Always send the COMPLETE list of todos \u2014 it replaces the previous one. Keep exactly one item 'in_progress' at a time.",
2396
+ parameters: {
2397
+ type: "object",
2398
+ properties: {
2399
+ todos: {
2400
+ type: "array",
2401
+ description: "The full task list, in order.",
2402
+ items: {
2403
+ type: "object",
2404
+ properties: {
2405
+ content: {
2406
+ type: "string",
2407
+ description: "Imperative title, e.g. 'Optimize images'."
2408
+ },
2409
+ activeForm: {
2410
+ type: "string",
2411
+ description: "Present-tense title shown while this step is in progress, e.g. 'Optimizing images'."
2412
+ },
2413
+ status: {
2414
+ type: "string",
2415
+ enum: ["pending", "in_progress", "completed"]
2416
+ }
2417
+ },
2418
+ required: ["content", "status"]
2419
+ }
2420
+ }
2421
+ },
2422
+ required: ["todos"]
2423
+ }
2424
+ };
2425
+ var REQUEST_APPROVAL_TOOL = {
2426
+ name: "request_approval",
2427
+ description: "Pause and ask the user to approve an action or choose between options before continuing. The conversation halts until the user responds \u2014 their choice comes back as this tool's result. Use this before any consequential or irreversible action (deleting, sending, purchasing, etc).",
2428
+ parameters: {
2429
+ type: "object",
2430
+ properties: {
2431
+ question: {
2432
+ type: "string",
2433
+ description: "The question or action to confirm."
2434
+ },
2435
+ description: {
2436
+ type: "string",
2437
+ description: "Optional extra context shown below the question."
2438
+ },
2439
+ options: {
2440
+ type: "array",
2441
+ description: "The choices offered to the user.",
2442
+ items: {
2443
+ type: "object",
2444
+ properties: {
2445
+ id: { type: "string" },
2446
+ label: { type: "string" },
2447
+ description: { type: "string" },
2448
+ freeform: {
2449
+ type: "boolean",
2450
+ description: "If true, choosing this option lets the user type a free-text reply instead of a canned choice."
2451
+ }
2452
+ },
2453
+ required: ["id", "label"]
2454
+ }
2455
+ }
2456
+ },
2457
+ required: ["question", "options"]
2458
+ }
2459
+ };
2460
+ function createMemoryStore() {
2461
+ const map = /* @__PURE__ */ new Map();
2462
+ return {
2463
+ get: (id) => map.get(id),
2464
+ set: (id, snap) => {
2465
+ map.set(id, snap);
2466
+ },
2467
+ delete: (id) => {
2468
+ map.delete(id);
2469
+ }
2470
+ };
2471
+ }
2472
+ function historyToAgentMessages(history) {
2473
+ const out = [];
2474
+ for (const m of history) {
2475
+ if (m.role === "user") out.push({ role: "user", content: m.content });
2476
+ else if (m.role === "assistant") out.push({ role: "assistant", content: m.content });
2477
+ }
2478
+ return out;
2479
+ }
2480
+ function extractTodos(input) {
2481
+ const todos = input?.todos;
2482
+ if (!Array.isArray(todos)) return [];
2483
+ return todos.map((t, i) => {
2484
+ const o = t ?? {};
2485
+ return {
2486
+ id: typeof o.id === "string" ? o.id : `todo-${i}`,
2487
+ content: String(o.content ?? ""),
2488
+ activeForm: typeof o.activeForm === "string" ? o.activeForm : void 0,
2489
+ status: o.status === "in_progress" || o.status === "completed" ? o.status : "pending"
2490
+ };
2491
+ });
2492
+ }
2493
+ function buildApproval(tc) {
2494
+ const input = tc.input ?? {};
2495
+ const rawOptions = Array.isArray(input.options) ? input.options : [];
2496
+ const options = rawOptions.map((o, i) => {
2497
+ const obj = o ?? {};
2498
+ return {
2499
+ id: typeof obj.id === "string" ? obj.id : `opt-${i}`,
2500
+ label: String(obj.label ?? `Option ${i + 1}`),
2501
+ description: typeof obj.description === "string" ? obj.description : void 0,
2502
+ freeform: obj.freeform === true ? true : void 0
2503
+ };
2504
+ });
2505
+ return {
2506
+ // The tool-call id is the approval id — Coral round-trips it as
2507
+ // metadata.approvalId, which is the session-store key on resume.
2508
+ id: tc.id,
2509
+ question: String(input.question ?? "Approve?"),
2510
+ description: typeof input.description === "string" ? input.description : void 0,
2511
+ options
2512
+ };
2513
+ }
2514
+ function createAgentHandler(options) {
2515
+ const { callModel, system, tools = [], maxSteps = 8 } = options;
2516
+ const store = options.sessionStore ?? createMemoryStore();
2517
+ const customByName = new Map(tools.map((t) => [t.spec.name, t]));
2518
+ const toolSpecs = [
2519
+ UPDATE_TODOS_TOOL,
2520
+ REQUEST_APPROVAL_TOOL,
2521
+ ...tools.map((t) => t.spec)
2522
+ ];
2523
+ return async function* (_input, history, attachments) {
2524
+ const sysFromHistory = history.filter((m) => m.role === "system").map((m) => m.content);
2525
+ const effectiveSystem = [system, ...sysFromHistory].filter(Boolean).join("\n\n") || void 0;
2526
+ const last = history[history.length - 1];
2527
+ let messages;
2528
+ let firstCallAttachments;
2529
+ const resumeId = last?.role === "user" && last.type === "approval_response" && typeof last.metadata?.approvalId === "string" ? last.metadata.approvalId : void 0;
2530
+ const snapshot = resumeId ? await store.get(resumeId) : void 0;
2531
+ if (resumeId && snapshot) {
2532
+ await store.delete(resumeId);
2533
+ const meta = last.metadata;
2534
+ const answer = typeof meta.text === "string" && meta.text || typeof meta.label === "string" && meta.label || last.content;
2535
+ messages = [
2536
+ ...snapshot.messages,
2537
+ {
2538
+ role: "tool",
2539
+ toolCallId: snapshot.pendingToolCallId,
2540
+ content: `The user chose: ${answer}`
2541
+ }
2542
+ ];
2543
+ } else {
2544
+ messages = historyToAgentMessages(history);
2545
+ firstCallAttachments = attachments.length > 0 ? attachments : void 0;
2546
+ }
2547
+ for (let step = 0; step < maxSteps; step++) {
2548
+ const events = callModel({
2549
+ system: effectiveSystem,
2550
+ messages,
2551
+ tools: toolSpecs,
2552
+ attachments: step === 0 ? firstCallAttachments : void 0
2553
+ });
2554
+ let assistantText = "";
2555
+ let assistantRaw;
2556
+ const toolCalls = [];
2557
+ for await (const ev of events) {
2558
+ if (ev.type === "text") {
2559
+ assistantText += ev.delta;
2560
+ yield ev.delta;
2561
+ } else if (ev.type === "reasoning") {
2562
+ yield { type: "reasoning", content: ev.delta };
2563
+ } else if (ev.type === "tool_call") {
2564
+ toolCalls.push(ev.toolCall);
2565
+ } else if (ev.type === "raw") {
2566
+ assistantRaw = ev.raw;
2567
+ }
2568
+ }
2569
+ messages.push({
2570
+ role: "assistant",
2571
+ content: assistantText,
2572
+ ...toolCalls.length > 0 ? { toolCalls } : {},
2573
+ ...assistantRaw !== void 0 ? { raw: assistantRaw } : {}
2574
+ });
2575
+ if (toolCalls.length === 0) return;
2576
+ for (const tc of toolCalls) {
2577
+ if (tc.name === UPDATE_TODOS_TOOL.name) {
2578
+ yield { type: "todos", todos: extractTodos(tc.input) };
2579
+ messages.push({
2580
+ role: "tool",
2581
+ toolCallId: tc.id,
2582
+ content: "Task list updated and shown to the user."
2583
+ });
2584
+ } else if (tc.name === REQUEST_APPROVAL_TOOL.name) {
2585
+ yield { type: "approval", approval: buildApproval(tc) };
2586
+ await store.set(tc.id, { messages, pendingToolCallId: tc.id });
2587
+ return;
2588
+ } else {
2589
+ const tool = customByName.get(tc.name);
2590
+ let content;
2591
+ if (!tool) {
2592
+ content = `Error: unknown tool "${tc.name}".`;
2593
+ } else {
2594
+ try {
2595
+ content = (await tool.execute(tc.input)).content;
2596
+ } catch (err) {
2597
+ content = `Error: ${err instanceof Error ? err.message : String(err)}`;
2598
+ }
2599
+ }
2600
+ messages.push({ role: "tool", toolCallId: tc.id, content });
2601
+ }
2602
+ }
2603
+ }
2604
+ yield "\n\n_(Reached the tool-step limit for this turn.)_";
2605
+ };
2606
+ }
2607
+
2608
+ // src/Chat.Threads.tsx
2609
+ import * as React9 from "react";
2610
+ import { jsx as jsx12 } from "react/jsx-runtime";
2611
+ var ChatThreadsContext = React9.createContext(
2612
+ null
2613
+ );
2614
+ function useChatThreads() {
2615
+ const value = React9.useContext(ChatThreadsContext);
2616
+ if (!value) {
2617
+ throw new Error(
2618
+ "useChatThreads must be used inside <ChatThreadsProvider>."
2619
+ );
2620
+ }
2621
+ return value;
2622
+ }
2623
+ function createMemoryThreadsAdapter(initialThreads = []) {
2624
+ let threads = [...initialThreads];
2625
+ return {
2626
+ list: () => threads,
2627
+ get: (id) => threads.find((t) => t.id === id),
2628
+ create: (initial) => {
2629
+ const thread = {
2630
+ id: initial?.id ?? crypto.randomUUID(),
2631
+ title: initial?.title ?? "New chat",
2632
+ messages: initial?.messages ?? [],
2633
+ createdAt: Date.now(),
2634
+ updatedAt: Date.now()
2635
+ };
2636
+ threads = [thread, ...threads];
2637
+ return thread;
2638
+ },
2639
+ save: (thread) => {
2640
+ const idx = threads.findIndex((t) => t.id === thread.id);
2641
+ const next = { ...thread, updatedAt: Date.now() };
2642
+ threads = idx >= 0 ? threads.map((t, i) => i === idx ? next : t) : [next, ...threads];
2643
+ },
2644
+ rename: (id, newTitle) => {
2645
+ threads = threads.map(
2646
+ (t) => t.id === id ? { ...t, title: newTitle, updatedAt: Date.now() } : t
2647
+ );
2648
+ },
2649
+ remove: (id) => {
2650
+ threads = threads.filter((t) => t.id !== id);
2651
+ }
2652
+ };
2653
+ }
2654
+ function ChatThreadsProvider({
2655
+ adapter: providedAdapter,
2656
+ activeThreadId: controlledId,
2657
+ defaultActiveThreadId,
2658
+ onActiveThreadChange,
2659
+ children
2660
+ }) {
2661
+ const adapter = React9.useMemo(
2662
+ () => providedAdapter ?? createMemoryThreadsAdapter(),
2663
+ [providedAdapter]
2664
+ );
2665
+ const [threads, setThreads] = React9.useState([]);
2666
+ const [uncontrolledId, setUncontrolledId] = React9.useState(
2667
+ defaultActiveThreadId
2668
+ );
2669
+ const [isLoading, setIsLoading] = React9.useState(true);
2670
+ const isControlled = controlledId !== void 0;
2671
+ const activeThreadId = isControlled ? controlledId : uncontrolledId;
2672
+ const setActiveThreadId = React9.useCallback(
2673
+ (id) => {
2674
+ if (!isControlled) setUncontrolledId(id);
2675
+ onActiveThreadChange?.(id);
2676
+ },
2677
+ [isControlled, onActiveThreadChange]
2678
+ );
2679
+ React9.useEffect(() => {
2680
+ let cancelled = false;
2681
+ (async () => {
2682
+ setIsLoading(true);
2683
+ const result = await adapter.list();
2684
+ if (cancelled) return;
2685
+ setThreads(result);
2686
+ setIsLoading(false);
2687
+ })();
2688
+ return () => {
2689
+ cancelled = true;
2690
+ };
2691
+ }, [adapter]);
2692
+ const createThread = React9.useCallback(
2693
+ async (initial) => {
2694
+ const thread = await adapter.create(initial);
2695
+ setThreads((prev) => [thread, ...prev.filter((t) => t.id !== thread.id)]);
2696
+ return thread;
2697
+ },
2698
+ [adapter]
2699
+ );
2700
+ const renameThread = React9.useCallback(
2701
+ async (id, newTitle) => {
2702
+ await adapter.rename(id, newTitle);
2703
+ setThreads(
2704
+ (prev) => prev.map(
2705
+ (t) => t.id === id ? { ...t, title: newTitle, updatedAt: Date.now() } : t
2706
+ )
2707
+ );
2708
+ },
2709
+ [adapter]
2710
+ );
2711
+ const removeThread = React9.useCallback(
2712
+ async (id) => {
2713
+ await adapter.remove(id);
2714
+ setThreads((prev) => prev.filter((t) => t.id !== id));
2715
+ if (activeThreadId === id) setActiveThreadId(void 0);
2716
+ },
2717
+ [adapter, activeThreadId, setActiveThreadId]
2718
+ );
2719
+ const threadsRef = React9.useRef(threads);
2720
+ threadsRef.current = threads;
2721
+ const updateThreadMessages = React9.useCallback(
2722
+ (id, messages) => {
2723
+ const existing = threadsRef.current.find((t) => t.id === id);
2724
+ if (!existing) return;
2725
+ const next = {
2726
+ ...existing,
2727
+ messages,
2728
+ updatedAt: Date.now()
2729
+ };
2730
+ setThreads((prev) => prev.map((t) => t.id === id ? next : t));
2731
+ void adapter.save(next);
2732
+ },
2733
+ [adapter]
2734
+ );
2735
+ const activeThread = React9.useMemo(
2736
+ () => threads.find((t) => t.id === activeThreadId),
2737
+ [threads, activeThreadId]
2738
+ );
2739
+ const value = React9.useMemo(
2740
+ () => ({
2741
+ threads,
2742
+ activeThreadId,
2743
+ activeThread,
2744
+ setActiveThreadId,
2745
+ createThread,
2746
+ renameThread,
2747
+ removeThread,
2748
+ updateThreadMessages,
2749
+ isLoading
2750
+ }),
2751
+ [
2752
+ threads,
2753
+ activeThreadId,
2754
+ activeThread,
2755
+ setActiveThreadId,
2756
+ createThread,
2757
+ renameThread,
2758
+ removeThread,
2759
+ updateThreadMessages,
2760
+ isLoading
2761
+ ]
2762
+ );
2763
+ return /* @__PURE__ */ jsx12(ChatThreadsContext.Provider, { value, children });
2764
+ }
2765
+
1537
2766
  // src/index.ts
1538
2767
  var Chat2 = Object.assign(Chat_default, {
1539
2768
  Canvas: Chat_Canvas_default,
1540
2769
  Messages: Chat_Messages_default,
1541
2770
  Input: Chat_Input_default,
1542
2771
  Error: Chat_Error_default,
1543
- Callout: Chat_Callout_default
2772
+ Callout: Chat_Callout_default,
2773
+ Approval: Chat_Approval_default,
2774
+ TodoList: Chat_TodoList_default,
2775
+ AttachButton: Chat_AttachButton_default,
2776
+ ThinkingIndicator: Chat_ThinkingIndicator_default
1544
2777
  });
1545
2778
  var normalizeChatError2 = normalizeChatError;
1546
2779
  var createHandler2 = createHandler;
1547
2780
  export {
1548
2781
  Chat2 as Chat,
2782
+ ChatThreadsProvider,
2783
+ DEFAULT_TASK_MESSAGES,
2784
+ REQUEST_APPROVAL_TOOL,
2785
+ UPDATE_TODOS_TOOL,
2786
+ createAgentHandler,
1549
2787
  createHandler2 as createHandler,
2788
+ createMemoryThreadsAdapter,
2789
+ fileToBase64,
1550
2790
  normalizeChatError2 as normalizeChatError,
1551
- useChatContext
2791
+ resolveTaskMessages,
2792
+ useChatContext,
2793
+ useChatThreads
1552
2794
  };
1553
2795
  //# sourceMappingURL=index.js.map