@cntyclub/agent-react 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import { Button, cn, ChatMessage, ChatMessageBubble, Markdown, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Tooltip, TooltipTrigger, TooltipContent, Chat, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, useMediaQuery, TooltipProvider, Fab, DataTablePaged } from '@cntyclub/ui-react';
2
- import { FileTextIcon, XIcon, PaperclipIcon, ArrowUpIcon, Maximize2Icon, ListChecksIcon, ChevronDownIcon, CheckIcon, ShieldAlertIcon, CheckCheckIcon, UploadCloudIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, Trash2Icon, SparklesIcon, DownloadIcon, CodeIcon } from 'lucide-react';
3
- import * as React7 from 'react';
1
+ import { Button, cn, Tooltip, TooltipTrigger, TooltipContent, ChatMessage, ChatMessageBubble, Markdown, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Chat, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, useMediaQuery, TooltipProvider, DataTablePaged } from '@cntyclub/ui-react';
2
+ import { FileTextIcon, XIcon, PaperclipIcon, ArrowUpIcon, CheckIcon, CopyIcon, Maximize2Icon, ListChecksIcon, ChevronDownIcon, ShieldAlertIcon, CheckCheckIcon, UploadCloudIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, Trash2Icon, SparklesIcon, DownloadIcon, CodeIcon } from 'lucide-react';
3
+ import * as React8 from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+ import { motion, AnimatePresence } from 'motion/react';
5
6
 
6
7
  // src/api-client.ts
7
8
  var AgentApiError = class extends Error {
@@ -230,18 +231,18 @@ function AgentComposer({
230
231
  placeholder = "Ask anything\u2026",
231
232
  value
232
233
  }) {
233
- const textareaRef = React7.useRef(null);
234
- const inputRef = React7.useRef(null);
234
+ const textareaRef = React8.useRef(null);
235
+ const inputRef = React8.useRef(null);
235
236
  const canSubmit = !disabled && !busy && (value.trim().length > 0 || files.length > 0);
236
- const submit = React7.useCallback(() => {
237
+ const submit = React8.useCallback(() => {
237
238
  if (!canSubmit) return;
238
239
  onSubmit(value.trim());
239
240
  textareaRef.current?.focus();
240
241
  }, [canSubmit, onSubmit, value]);
241
- React7.useEffect(() => {
242
+ React8.useEffect(() => {
242
243
  if (!disabled) textareaRef.current?.focus();
243
244
  }, [disabled]);
244
- React7.useEffect(() => {
245
+ React8.useEffect(() => {
245
246
  const textarea = textareaRef.current;
246
247
  if (!textarea) return;
247
248
  textarea.style.height = "auto";
@@ -423,15 +424,15 @@ function groupMessages(messages) {
423
424
  return items;
424
425
  }
425
426
  function ChatScroller({ children, className, ...props }) {
426
- const viewportRef = React7.useRef(null);
427
- const pinnedRef = React7.useRef(true);
428
- const handleScroll = React7.useCallback(() => {
427
+ const viewportRef = React8.useRef(null);
428
+ const pinnedRef = React8.useRef(true);
429
+ const handleScroll = React8.useCallback(() => {
429
430
  const viewport = viewportRef.current;
430
431
  if (!viewport) return;
431
432
  const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
432
433
  pinnedRef.current = distanceFromBottom < 64;
433
434
  }, []);
434
- React7.useEffect(() => {
435
+ React8.useEffect(() => {
435
436
  const viewport = viewportRef.current;
436
437
  if (!viewport || !pinnedRef.current) return;
437
438
  viewport.scrollTop = viewport.scrollHeight;
@@ -448,6 +449,45 @@ function ChatScroller({ children, className, ...props }) {
448
449
  }
449
450
  );
450
451
  }
452
+ function CopyButton({ className, text }) {
453
+ const [copied, setCopied] = React8.useState(false);
454
+ const timerRef = React8.useRef(null);
455
+ React8.useEffect(() => {
456
+ return () => {
457
+ if (timerRef.current) clearTimeout(timerRef.current);
458
+ };
459
+ }, []);
460
+ const copy = React8.useCallback(async () => {
461
+ try {
462
+ await navigator.clipboard.writeText(text);
463
+ } catch {
464
+ return;
465
+ }
466
+ setCopied(true);
467
+ if (timerRef.current) clearTimeout(timerRef.current);
468
+ timerRef.current = setTimeout(() => setCopied(false), 1500);
469
+ }, [text]);
470
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
471
+ /* @__PURE__ */ jsx(
472
+ TooltipTrigger,
473
+ {
474
+ render: /* @__PURE__ */ jsx(
475
+ Button,
476
+ {
477
+ "aria-label": copied ? "Copied" : "Copy message",
478
+ className: cn("text-muted-foreground", className),
479
+ onClick: copy,
480
+ size: "icon-xs",
481
+ type: "button",
482
+ variant: "ghost",
483
+ children: copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-success" }) : /* @__PURE__ */ jsx(CopyIcon, {})
484
+ }
485
+ )
486
+ }
487
+ ),
488
+ /* @__PURE__ */ jsx(TooltipContent, { children: copied ? "Copied!" : "Copy" })
489
+ ] });
490
+ }
451
491
  function MessageItem({ message }) {
452
492
  if (message.role === "user") {
453
493
  const attachments = message.attachments ?? [];
@@ -468,7 +508,19 @@ function MessageItem({ message }) {
468
508
  }
469
509
  if (message.role === "assistant") {
470
510
  if (!message.content || !message.content.trim()) return null;
471
- return /* @__PURE__ */ jsx(ChatMessage, { from: "assistant", children: /* @__PURE__ */ jsx(ChatMessageBubble, { from: "assistant", children: /* @__PURE__ */ jsx(Markdown, { children: message.content }) }) });
511
+ return /* @__PURE__ */ jsxs(ChatMessage, { className: "group", from: "assistant", children: [
512
+ /* @__PURE__ */ jsx(ChatMessageBubble, { from: "assistant", children: /* @__PURE__ */ jsx(Markdown, { children: message.content }) }),
513
+ /* @__PURE__ */ jsx(
514
+ "div",
515
+ {
516
+ className: cn(
517
+ "mt-0.5 flex opacity-0 transition-opacity",
518
+ "group-hover:opacity-100 focus-within:opacity-100"
519
+ ),
520
+ children: /* @__PURE__ */ jsx(CopyButton, { text: message.content })
521
+ }
522
+ )
523
+ ] });
472
524
  }
473
525
  return null;
474
526
  }
@@ -482,8 +534,8 @@ var DEFAULT_PHRASES = [
482
534
  ];
483
535
  function ThinkingIndicator({ className, intervalMs = 1800, phrases }) {
484
536
  const words = phrases && phrases.length ? phrases : DEFAULT_PHRASES;
485
- const [index, setIndex] = React7.useState(0);
486
- React7.useEffect(() => {
537
+ const [index, setIndex] = React8.useState(0);
538
+ React8.useEffect(() => {
487
539
  setIndex(0);
488
540
  const timer = setInterval(() => {
489
541
  setIndex((current) => (current + 1) % words.length);
@@ -745,7 +797,7 @@ function DownloadButton({ block, size = "sm" }) {
745
797
  );
746
798
  }
747
799
  function DocumentViewer({ block, onClose }) {
748
- React7.useEffect(() => {
800
+ React8.useEffect(() => {
749
801
  const onKey = (event) => event.key === "Escape" && onClose();
750
802
  window.addEventListener("keydown", onKey);
751
803
  return () => window.removeEventListener("keydown", onKey);
@@ -770,7 +822,7 @@ function DocumentViewer({ block, onClose }) {
770
822
  ] });
771
823
  }
772
824
  function DocumentCard({ block }) {
773
- const [open, setOpen] = React7.useState(false);
825
+ const [open, setOpen] = React8.useState(false);
774
826
  const previewLines = (block.preview || "").split("\n").slice(0, 6).join("\n");
775
827
  return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-full overflow-hidden rounded-xl border border-border bg-card shadow-xs", children: [
776
828
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-border/72 border-b px-3 py-2", children: [
@@ -892,7 +944,7 @@ function StepDetail({
892
944
  ] });
893
945
  }
894
946
  function ToolActivityGroup({ describeToolCall, steps }) {
895
- const [open, setOpen] = React7.useState(false);
947
+ const [open, setOpen] = React8.useState(false);
896
948
  if (steps.length === 0) return null;
897
949
  const last = steps[steps.length - 1];
898
950
  const anyRunning = steps.some((step) => step.status === "running");
@@ -991,23 +1043,24 @@ function AgentPanel({
991
1043
  chat,
992
1044
  className,
993
1045
  config,
1046
+ newMessageAnchorId,
994
1047
  onAlwaysApprove,
995
1048
  onClose,
996
1049
  onToggleAgentMode
997
1050
  }) {
998
- const [draft, setDraft] = React7.useState("");
999
- const [view, setView] = React7.useState("chat");
1000
- const [files, setFiles] = React7.useState([]);
1001
- const [uploadError, setUploadError] = React7.useState(null);
1002
- const [dragging, setDragging] = React7.useState(false);
1003
- const dragDepth = React7.useRef(0);
1051
+ const [draft, setDraft] = React8.useState("");
1052
+ const [view, setView] = React8.useState("chat");
1053
+ const [files, setFiles] = React8.useState([]);
1054
+ const [uploadError, setUploadError] = React8.useState(null);
1055
+ const [dragging, setDragging] = React8.useState(false);
1056
+ const dragDepth = React8.useRef(0);
1004
1057
  const agentName = config.agentName ?? chat.agentInfo?.name ?? "Assistant";
1005
1058
  const uploadEnabled = fileUploadEnabled(config);
1006
1059
  const suggestions = config.suggestions ?? [
1007
1060
  "What can you help me with?",
1008
1061
  "Give me a quick summary of my data."
1009
1062
  ];
1010
- const addFiles = React7.useCallback(
1063
+ const addFiles = React8.useCallback(
1011
1064
  (incoming) => {
1012
1065
  const accepted = [];
1013
1066
  let rejection = null;
@@ -1024,10 +1077,10 @@ function AgentPanel({
1024
1077
  },
1025
1078
  [config]
1026
1079
  );
1027
- const removeFile = React7.useCallback((index) => {
1080
+ const removeFile = React8.useCallback((index) => {
1028
1081
  setFiles((current) => current.filter((_, i) => i !== index));
1029
1082
  }, []);
1030
- const handleSubmit = React7.useCallback(
1083
+ const handleSubmit = React8.useCallback(
1031
1084
  (text) => {
1032
1085
  const staged = files;
1033
1086
  setDraft("");
@@ -1037,7 +1090,7 @@ function AgentPanel({
1037
1090
  },
1038
1091
  [chat, files]
1039
1092
  );
1040
- const onDragEnter = React7.useCallback(
1093
+ const onDragEnter = React8.useCallback(
1041
1094
  (event) => {
1042
1095
  if (!uploadEnabled || !event.dataTransfer?.types?.includes("Files")) return;
1043
1096
  dragDepth.current += 1;
@@ -1045,12 +1098,12 @@ function AgentPanel({
1045
1098
  },
1046
1099
  [uploadEnabled]
1047
1100
  );
1048
- const onDragLeave = React7.useCallback((event) => {
1101
+ const onDragLeave = React8.useCallback((event) => {
1049
1102
  if (!event.dataTransfer?.types?.includes("Files")) return;
1050
1103
  dragDepth.current = Math.max(0, dragDepth.current - 1);
1051
1104
  if (dragDepth.current === 0) setDragging(false);
1052
1105
  }, []);
1053
- const onDrop = React7.useCallback(
1106
+ const onDrop = React8.useCallback(
1054
1107
  (event) => {
1055
1108
  if (!uploadEnabled) return;
1056
1109
  event.preventDefault();
@@ -1061,7 +1114,7 @@ function AgentPanel({
1061
1114
  },
1062
1115
  [addFiles, uploadEnabled]
1063
1116
  );
1064
- const openHistory = React7.useCallback(() => {
1117
+ const openHistory = React8.useCallback(() => {
1065
1118
  setView("history");
1066
1119
  void chat.refreshConversations();
1067
1120
  }, [chat]);
@@ -1211,9 +1264,30 @@ function AgentPanel({
1211
1264
  suggestion
1212
1265
  )) })
1213
1266
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1214
- groupMessages(chat.messages).map(
1215
- (item) => item.kind === "tool-activity" ? /* @__PURE__ */ jsx(ChatMessage, { from: "assistant", children: /* @__PURE__ */ jsx(ToolActivityGroup, { describeToolCall: config.describeToolCall, steps: item.steps }) }, item.key) : /* @__PURE__ */ jsx(MessageItem, { message: item.message }, item.key)
1216
- ),
1267
+ (() => {
1268
+ const grouped = groupMessages(chat.messages);
1269
+ const anchorKey = newMessageAnchorId ? grouped.find(
1270
+ (it) => it.kind !== "tool-activity" && it.message.id === newMessageAnchorId
1271
+ )?.key : void 0;
1272
+ return grouped.map((item) => /* @__PURE__ */ jsxs(React8.Fragment, { children: [
1273
+ item.key === anchorKey ? /* @__PURE__ */ jsx(NewMessagesDivider, {}) : null,
1274
+ /* @__PURE__ */ jsx(
1275
+ motion.div,
1276
+ {
1277
+ initial: { opacity: 0, y: 8 },
1278
+ animate: { opacity: 1, y: 0 },
1279
+ transition: { duration: 0.18, ease: "easeOut" },
1280
+ children: item.kind === "tool-activity" ? /* @__PURE__ */ jsx(ChatMessage, { from: "assistant", children: /* @__PURE__ */ jsx(
1281
+ ToolActivityGroup,
1282
+ {
1283
+ describeToolCall: config.describeToolCall,
1284
+ steps: item.steps
1285
+ }
1286
+ ) }) : /* @__PURE__ */ jsx(MessageItem, { message: item.message })
1287
+ }
1288
+ )
1289
+ ] }, item.key));
1290
+ })(),
1217
1291
  chat.pendingApprovals.map((approval) => /* @__PURE__ */ jsx(
1218
1292
  ToolApprovalCard,
1219
1293
  {
@@ -1263,6 +1337,13 @@ function AgentPanel({
1263
1337
  }
1264
1338
  );
1265
1339
  }
1340
+ function NewMessagesDivider() {
1341
+ return /* @__PURE__ */ jsxs("div", { className: "my-1 flex items-center gap-2", "aria-label": "New messages", children: [
1342
+ /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" }),
1343
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground", children: "New messages" }),
1344
+ /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" })
1345
+ ] });
1346
+ }
1266
1347
  var OPTIMISTIC_ID_PREFIX = "optimistic-";
1267
1348
  var STREAMING_ID_PREFIX = "streaming-";
1268
1349
  var optimisticCounter = 0;
@@ -1284,16 +1365,16 @@ function buildOptimisticUserMessage(text, files) {
1284
1365
  };
1285
1366
  }
1286
1367
  function useAgentChat(config) {
1287
- const client = React7.useMemo(() => new AgentApiClient(config), [config]);
1288
- const [agentInfo, setAgentInfo] = React7.useState(null);
1289
- const [conversationId, setConversationId] = React7.useState(null);
1290
- const [conversations, setConversations] = React7.useState([]);
1291
- const [messages, setMessages] = React7.useState([]);
1292
- const [pendingApprovals, setPendingApprovals] = React7.useState([]);
1293
- const [sending, setSending] = React7.useState(false);
1294
- const [resolvingApprovalId, setResolvingApprovalId] = React7.useState(null);
1295
- const [error, setError] = React7.useState(null);
1296
- React7.useEffect(() => {
1368
+ const client = React8.useMemo(() => new AgentApiClient(config), [config]);
1369
+ const [agentInfo, setAgentInfo] = React8.useState(null);
1370
+ const [conversationId, setConversationId] = React8.useState(null);
1371
+ const [conversations, setConversations] = React8.useState([]);
1372
+ const [messages, setMessages] = React8.useState([]);
1373
+ const [pendingApprovals, setPendingApprovals] = React8.useState([]);
1374
+ const [sending, setSending] = React8.useState(false);
1375
+ const [resolvingApprovalId, setResolvingApprovalId] = React8.useState(null);
1376
+ const [error, setError] = React8.useState(null);
1377
+ React8.useEffect(() => {
1297
1378
  let cancelled = false;
1298
1379
  client.getConfig().then((info) => {
1299
1380
  if (!cancelled) setAgentInfo(info);
@@ -1303,11 +1384,11 @@ function useAgentChat(config) {
1303
1384
  cancelled = true;
1304
1385
  };
1305
1386
  }, [client]);
1306
- const handleFailure = React7.useCallback((err) => {
1387
+ const handleFailure = React8.useCallback((err) => {
1307
1388
  const message = err instanceof AgentApiError ? err.message : "Something went wrong. Please try again.";
1308
1389
  setError(message);
1309
1390
  }, []);
1310
- const applyResponse = React7.useCallback(
1391
+ const applyResponse = React8.useCallback(
1311
1392
  (response) => {
1312
1393
  if (response.status === "error" && !response.messages?.length) {
1313
1394
  setError(response.message ?? "The assistant could not answer.");
@@ -1329,7 +1410,7 @@ function useAgentChat(config) {
1329
1410
  },
1330
1411
  []
1331
1412
  );
1332
- const handleStreamEvent = React7.useCallback(
1413
+ const handleStreamEvent = React8.useCallback(
1333
1414
  (event, ctx) => {
1334
1415
  switch (event.type) {
1335
1416
  case "conversation":
@@ -1394,7 +1475,7 @@ function useAgentChat(config) {
1394
1475
  []
1395
1476
  );
1396
1477
  const streamingEnabled = config.streaming !== false && typeof ReadableStream !== "undefined";
1397
- const send = React7.useCallback(
1478
+ const send = React8.useCallback(
1398
1479
  async (text, files) => {
1399
1480
  if (sending) return;
1400
1481
  if (!text.trim() && !(files && files.length)) return;
@@ -1425,7 +1506,7 @@ function useAgentChat(config) {
1425
1506
  },
1426
1507
  [applyResponse, client, conversationId, handleFailure, handleStreamEvent, sending, streamingEnabled]
1427
1508
  );
1428
- const resolveApproval = React7.useCallback(
1509
+ const resolveApproval = React8.useCallback(
1429
1510
  async (callId, approve) => {
1430
1511
  if (resolvingApprovalId) return;
1431
1512
  setResolvingApprovalId(callId);
@@ -1454,21 +1535,23 @@ function useAgentChat(config) {
1454
1535
  },
1455
1536
  [applyResponse, client, handleFailure, handleStreamEvent, resolvingApprovalId, streamingEnabled]
1456
1537
  );
1457
- const newChat = React7.useCallback(() => {
1538
+ const newChat = React8.useCallback(() => {
1458
1539
  setConversationId(null);
1459
1540
  setMessages([]);
1460
1541
  setPendingApprovals([]);
1461
1542
  setError(null);
1462
1543
  }, []);
1463
- const refreshConversations = React7.useCallback(async () => {
1544
+ const refreshConversations = React8.useCallback(async () => {
1464
1545
  try {
1465
1546
  const { conversations: list } = await client.listConversations();
1466
1547
  setConversations(list);
1548
+ return list;
1467
1549
  } catch (err) {
1468
1550
  handleFailure(err);
1551
+ return [];
1469
1552
  }
1470
1553
  }, [client, handleFailure]);
1471
- const openConversation = React7.useCallback(
1554
+ const openConversation = React8.useCallback(
1472
1555
  async (id) => {
1473
1556
  setError(null);
1474
1557
  try {
@@ -1482,7 +1565,7 @@ function useAgentChat(config) {
1482
1565
  },
1483
1566
  [client, handleFailure]
1484
1567
  );
1485
- const deleteConversation = React7.useCallback(
1568
+ const deleteConversation = React8.useCallback(
1486
1569
  async (id) => {
1487
1570
  try {
1488
1571
  await client.deleteConversation(id);
@@ -1494,7 +1577,7 @@ function useAgentChat(config) {
1494
1577
  },
1495
1578
  [client, conversationId, handleFailure, newChat]
1496
1579
  );
1497
- const clearError = React7.useCallback(() => setError(null), []);
1580
+ const clearError = React8.useCallback(() => setError(null), []);
1498
1581
  return {
1499
1582
  agentInfo,
1500
1583
  clearError,
@@ -1529,11 +1612,11 @@ function readStored(clientId) {
1529
1612
  }
1530
1613
  }
1531
1614
  function useAlwaysApprove(clientId) {
1532
- const [approved, setApproved] = React7.useState(() => /* @__PURE__ */ new Set());
1533
- React7.useEffect(() => {
1615
+ const [approved, setApproved] = React8.useState(() => /* @__PURE__ */ new Set());
1616
+ React8.useEffect(() => {
1534
1617
  setApproved(readStored(clientId));
1535
1618
  }, [clientId]);
1536
- const persist = React7.useCallback(
1619
+ const persist = React8.useCallback(
1537
1620
  (next) => {
1538
1621
  setApproved(next);
1539
1622
  if (typeof window === "undefined") return;
@@ -1544,8 +1627,8 @@ function useAlwaysApprove(clientId) {
1544
1627
  },
1545
1628
  [clientId]
1546
1629
  );
1547
- const isAlwaysApproved = React7.useCallback((toolName) => approved.has(toolName), [approved]);
1548
- const setAlwaysApprove = React7.useCallback(
1630
+ const isAlwaysApproved = React8.useCallback((toolName) => approved.has(toolName), [approved]);
1631
+ const setAlwaysApprove = React8.useCallback(
1549
1632
  (toolName) => {
1550
1633
  if (approved.has(toolName)) return;
1551
1634
  const next = new Set(approved);
@@ -1554,7 +1637,7 @@ function useAlwaysApprove(clientId) {
1554
1637
  },
1555
1638
  [approved, persist]
1556
1639
  );
1557
- const clearAlwaysApprove = React7.useCallback(
1640
+ const clearAlwaysApprove = React8.useCallback(
1558
1641
  (toolName) => {
1559
1642
  if (!approved.has(toolName)) return;
1560
1643
  const next = new Set(approved);
@@ -1565,46 +1648,110 @@ function useAlwaysApprove(clientId) {
1565
1648
  );
1566
1649
  return { clearAlwaysApprove, isAlwaysApproved, setAlwaysApprove };
1567
1650
  }
1651
+ function isUnreadable(message) {
1652
+ if (message.id.startsWith("streaming-")) return false;
1653
+ return message.role === "assistant" || message.role === "tool";
1654
+ }
1568
1655
  function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1569
- const [uncontrolledOpen, setUncontrolledOpen] = React7.useState(false);
1656
+ const behavior = config.behavior ?? {};
1657
+ const position = config.appearance?.position ?? "bottom-right";
1658
+ const isControlled = controlledOpen !== void 0;
1659
+ const storageKey2 = `cc-agent-open:${config.clientId}`;
1660
+ const [uncontrolledOpen, setUncontrolledOpen] = React8.useState(false);
1570
1661
  const open = controlledOpen ?? uncontrolledOpen;
1571
- const setOpen = React7.useCallback(
1662
+ const persist = React8.useCallback(
1663
+ (next) => {
1664
+ if (isControlled || !behavior.persistOpenState) return;
1665
+ try {
1666
+ window.localStorage.setItem(storageKey2, next ? "1" : "0");
1667
+ } catch {
1668
+ }
1669
+ },
1670
+ [behavior.persistOpenState, isControlled, storageKey2]
1671
+ );
1672
+ const setOpen = React8.useCallback(
1572
1673
  (next) => {
1573
1674
  setUncontrolledOpen(next);
1675
+ persist(next);
1574
1676
  onOpenChange?.(next);
1575
1677
  },
1576
- [onOpenChange]
1678
+ [onOpenChange, persist]
1577
1679
  );
1578
- const [agentMode, setAgentMode] = React7.useState(false);
1579
- const isDesktop = useMediaQuery("(min-width: 640px)", {
1580
- defaultSSRValue: true,
1581
- ssr: true
1582
- });
1680
+ const [agentMode, setAgentMode] = React8.useState(false);
1681
+ const isDesktop = useMediaQuery("(min-width: 640px)", { defaultSSRValue: true, ssr: true });
1583
1682
  const fullscreen = !isDesktop || agentMode;
1584
1683
  const chat = useAgentChat(config);
1585
1684
  const alwaysApprove = useAlwaysApprove(config.clientId);
1586
- const { pendingApprovals, resolveApproval, resolvingApprovalId } = chat;
1685
+ const { messages, pendingApprovals, resolveApproval, resolvingApprovalId, sending } = chat;
1686
+ const bootRef = React8.useRef(false);
1687
+ React8.useEffect(() => {
1688
+ if (isControlled || bootRef.current) return;
1689
+ bootRef.current = true;
1690
+ let cancelled = false;
1691
+ void (async () => {
1692
+ if (behavior.openLastConversation !== false) {
1693
+ const list = await chat.refreshConversations();
1694
+ if (cancelled) return;
1695
+ if (list.length) await chat.openConversation(list[0].id);
1696
+ if (cancelled) return;
1697
+ }
1698
+ let shouldOpen = behavior.initialOpen ?? false;
1699
+ if (behavior.persistOpenState) {
1700
+ try {
1701
+ const stored = window.localStorage.getItem(storageKey2);
1702
+ if (stored !== null) shouldOpen = stored === "1";
1703
+ } catch {
1704
+ }
1705
+ }
1706
+ if (shouldOpen) {
1707
+ setUncontrolledOpen(true);
1708
+ onOpenChange?.(true);
1709
+ }
1710
+ })();
1711
+ return () => {
1712
+ cancelled = true;
1713
+ };
1714
+ }, []);
1715
+ const seenCountRef = React8.useRef(0);
1716
+ const [anchorId, setAnchorId] = React8.useState(null);
1717
+ React8.useEffect(() => {
1718
+ if (open) {
1719
+ if (messages.length > seenCountRef.current) {
1720
+ const firstUnread = messages[seenCountRef.current];
1721
+ setAnchorId(firstUnread && isUnreadable(firstUnread) ? firstUnread.id : null);
1722
+ }
1723
+ seenCountRef.current = messages.length;
1724
+ }
1725
+ }, [open, messages]);
1726
+ const unreadCount = open ? 0 : messages.slice(seenCountRef.current).filter(isUnreadable).length;
1727
+ const workLabel = React8.useMemo(() => {
1728
+ if (!sending) return null;
1729
+ const last = messages[messages.length - 1];
1730
+ if (last?.role === "tool") return "Calling tools\u2026";
1731
+ if (last?.role === "assistant" && (last.tool_calls?.length ?? 0) > 0) return "Calling tools\u2026";
1732
+ return "Thinking\u2026";
1733
+ }, [sending, messages]);
1734
+ const approvalWaiting = pendingApprovals.length > 0;
1587
1735
  const { isAlwaysApproved } = alwaysApprove;
1588
- React7.useEffect(() => {
1589
- if (!open || resolvingApprovalId) return;
1736
+ React8.useEffect(() => {
1737
+ if (resolvingApprovalId) return;
1590
1738
  const target = pendingApprovals.find((approval) => isAlwaysApproved(approval.tool_name));
1591
1739
  if (target) void resolveApproval(target.id, true);
1592
- }, [open, pendingApprovals, resolvingApprovalId, isAlwaysApproved, resolveApproval]);
1593
- const firedWriteIds = React7.useRef(/* @__PURE__ */ new Set());
1740
+ }, [pendingApprovals, resolvingApprovalId, isAlwaysApproved, resolveApproval]);
1741
+ const firedWriteIds = React8.useRef(/* @__PURE__ */ new Set());
1594
1742
  const { onWriteSuccess } = config;
1595
- React7.useEffect(() => {
1743
+ React8.useEffect(() => {
1596
1744
  if (!onWriteSuccess) return;
1597
- for (const message of chat.messages) {
1745
+ for (const message of messages) {
1598
1746
  if (message.role !== "tool" || !message.is_write || message.tool_status !== "success") continue;
1599
1747
  if (firedWriteIds.current.has(message.id)) continue;
1600
1748
  firedWriteIds.current.add(message.id);
1601
- const args = findToolArguments(chat.messages, message);
1749
+ const args = findToolArguments(messages, message);
1602
1750
  onWriteSuccess({ arguments: args, toolName: message.tool_name ?? "" });
1603
1751
  }
1604
- }, [chat.messages, onWriteSuccess]);
1605
- const lastNavigatedMessageId = React7.useRef(null);
1606
- const { messages } = chat;
1607
- React7.useEffect(() => {
1752
+ }, [messages, onWriteSuccess]);
1753
+ const lastNavigatedMessageId = React8.useRef(null);
1754
+ React8.useEffect(() => {
1608
1755
  if (!open || fullscreen || !config.navigate || !config.pages?.length) return;
1609
1756
  const toolMessages = messages.filter((message) => message.role === "tool");
1610
1757
  const latest = toolMessages[toolMessages.length - 1];
@@ -1616,69 +1763,137 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1616
1763
  const path = mapping.buildRoute ? mapping.buildRoute(args) : mapping.route;
1617
1764
  if (path) config.navigate(path);
1618
1765
  }, [config, fullscreen, messages, open]);
1619
- const closePanel = React7.useCallback(() => {
1766
+ const closePanel = React8.useCallback(() => {
1620
1767
  setOpen(false);
1621
1768
  setAgentMode(false);
1622
1769
  }, [setOpen]);
1770
+ const transformOrigin = position === "bottom-left" ? "bottom left" : "bottom right";
1771
+ const launcherHidden = open || config.appearance?.hideLauncher;
1623
1772
  return /* @__PURE__ */ jsxs(TooltipProvider, { children: [
1624
- !open && !config.appearance?.hideLauncher ? /* @__PURE__ */ jsx(
1625
- Fab,
1773
+ /* @__PURE__ */ jsx(AnimatePresence, { children: !launcherHidden ? /* @__PURE__ */ jsxs(
1774
+ motion.div,
1626
1775
  {
1627
- "aria-label": "Open assistant",
1628
- onClick: () => setOpen(true),
1629
- position: config.appearance?.position ?? "bottom-right",
1630
- children: /* @__PURE__ */ jsx(SparklesIcon, {})
1631
- }
1632
- ) : null,
1633
- open ? fullscreen ? /* @__PURE__ */ jsx(
1634
- "div",
1776
+ initial: { opacity: 0, scale: 0.6 },
1777
+ animate: { opacity: 1, scale: 1 },
1778
+ exit: { opacity: 0, scale: 0.6 },
1779
+ transition: { type: "spring", stiffness: 300, damping: 24 },
1780
+ style: { transformOrigin },
1781
+ className: cn(
1782
+ "fixed z-50 flex flex-col items-end gap-2",
1783
+ position === "bottom-left" ? "bottom-4 left-4 items-start sm:bottom-6 sm:left-6" : "right-4 bottom-4 sm:right-6 sm:bottom-6"
1784
+ ),
1785
+ children: [
1786
+ /* @__PURE__ */ jsx(AnimatePresence, { mode: "popLayout", children: approvalWaiting ? /* @__PURE__ */ jsx(StatusChip, { tone: "warning", pulse: true, children: "Approval needed" }, "approval") : workLabel ? /* @__PURE__ */ jsx(StatusChip, { tone: "muted", children: workLabel }, workLabel) : null }),
1787
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1788
+ /* @__PURE__ */ jsx(
1789
+ "button",
1790
+ {
1791
+ type: "button",
1792
+ "aria-label": "Open assistant",
1793
+ onClick: () => setOpen(true),
1794
+ className: "flex size-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg outline-none transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
1795
+ children: /* @__PURE__ */ jsx(SparklesIcon, { className: "size-6" })
1796
+ }
1797
+ ),
1798
+ /* @__PURE__ */ jsx(AnimatePresence, { children: unreadCount > 0 ? /* @__PURE__ */ jsx(
1799
+ motion.span,
1800
+ {
1801
+ initial: { scale: 0 },
1802
+ animate: { scale: 1 },
1803
+ exit: { scale: 0 },
1804
+ transition: { type: "spring", stiffness: 500, damping: 20 },
1805
+ className: "absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-destructive px-1 text-xs font-semibold text-destructive-foreground shadow",
1806
+ "aria-label": `${unreadCount} new message${unreadCount === 1 ? "" : "s"}`,
1807
+ children: unreadCount > 9 ? "9+" : unreadCount
1808
+ },
1809
+ "badge"
1810
+ ) : null })
1811
+ ] })
1812
+ ]
1813
+ },
1814
+ "agent-launcher"
1815
+ ) : null }),
1816
+ /* @__PURE__ */ jsx(AnimatePresence, { children: open ? /* @__PURE__ */ jsx(
1817
+ motion.div,
1635
1818
  {
1636
- "aria-modal": "true",
1637
- className: "fixed inset-0 z-50 flex flex-col bg-background",
1638
- role: "dialog",
1639
- children: /* @__PURE__ */ jsx(
1640
- AgentPanel,
1641
- {
1642
- agentMode,
1643
- canToggleAgentMode: isDesktop,
1644
- chat,
1645
- className: "flex-1",
1646
- config,
1647
- onAlwaysApprove: alwaysApprove.setAlwaysApprove,
1648
- onClose: closePanel,
1649
- onToggleAgentMode: () => setAgentMode((value) => !value)
1819
+ layout: true,
1820
+ initial: { opacity: 0, scale: 0.9 },
1821
+ animate: { opacity: 1, scale: 1 },
1822
+ exit: { opacity: 0, scale: 0.9 },
1823
+ transition: { type: "spring", stiffness: 260, damping: 28 },
1824
+ style: {
1825
+ transformOrigin,
1826
+ ...fullscreen ? {} : {
1827
+ height: `min(${config.appearance?.panelHeight ?? 640}px, calc(100dvh - 3rem))`,
1828
+ width: `min(${config.appearance?.panelWidth ?? 400}px, calc(100vw - 2rem))`
1650
1829
  }
1651
- )
1652
- }
1653
- ) : /* @__PURE__ */ jsx(
1654
- "div",
1655
- {
1830
+ },
1656
1831
  className: cn(
1657
- "fixed z-50 flex flex-col overflow-hidden rounded-2xl border border-border shadow-2xl",
1658
- (config.appearance?.position ?? "bottom-right") === "bottom-left" ? "bottom-4 left-4 sm:bottom-6 sm:left-6" : "right-4 bottom-4 sm:right-6 sm:bottom-6"
1832
+ "fixed z-50 flex flex-col overflow-hidden bg-background",
1833
+ fullscreen ? "inset-0" : cn(
1834
+ "rounded-2xl border border-border shadow-2xl",
1835
+ position === "bottom-left" ? "bottom-4 left-4 sm:bottom-6 sm:left-6" : "right-4 bottom-4 sm:right-6 sm:bottom-6"
1836
+ )
1659
1837
  ),
1660
1838
  role: "dialog",
1661
- style: {
1662
- height: `min(${config.appearance?.panelHeight ?? 640}px, calc(100dvh - 3rem))`,
1663
- width: `min(${config.appearance?.panelWidth ?? 400}px, calc(100vw - 2rem))`
1664
- },
1839
+ "aria-modal": fullscreen ? "true" : void 0,
1665
1840
  children: /* @__PURE__ */ jsx(
1666
1841
  AgentPanel,
1667
1842
  {
1668
- agentMode: false,
1669
- canToggleAgentMode: true,
1843
+ agentMode: fullscreen && agentMode,
1844
+ canToggleAgentMode: isDesktop,
1670
1845
  chat,
1671
1846
  className: "flex-1",
1672
1847
  config,
1848
+ newMessageAnchorId: anchorId,
1673
1849
  onAlwaysApprove: alwaysApprove.setAlwaysApprove,
1674
1850
  onClose: closePanel,
1675
- onToggleAgentMode: () => setAgentMode(true)
1851
+ onToggleAgentMode: () => setAgentMode((value) => !value)
1676
1852
  }
1677
1853
  )
1678
- }
1679
- ) : null
1854
+ },
1855
+ "agent-panel"
1856
+ ) : null })
1680
1857
  ] });
1681
1858
  }
1859
+ function StatusChip({
1860
+ children,
1861
+ tone,
1862
+ pulse
1863
+ }) {
1864
+ return /* @__PURE__ */ jsxs(
1865
+ motion.div,
1866
+ {
1867
+ layout: true,
1868
+ initial: { opacity: 0, y: 6, scale: 0.9 },
1869
+ animate: { opacity: 1, y: 0, scale: 1 },
1870
+ exit: { opacity: 0, y: 6, scale: 0.9 },
1871
+ transition: { duration: 0.18 },
1872
+ className: cn(
1873
+ "flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium shadow-sm",
1874
+ tone === "warning" ? "border-warning bg-warning-surface text-warning-foreground" : "border-border bg-card text-secondary-foreground"
1875
+ ),
1876
+ children: [
1877
+ pulse ? /* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-warning-foreground" }) : /* @__PURE__ */ jsxs("span", { className: "flex gap-0.5", children: [
1878
+ /* @__PURE__ */ jsx(Dot, { delay: 0 }),
1879
+ /* @__PURE__ */ jsx(Dot, { delay: 0.15 }),
1880
+ /* @__PURE__ */ jsx(Dot, { delay: 0.3 })
1881
+ ] }),
1882
+ children
1883
+ ]
1884
+ }
1885
+ );
1886
+ }
1887
+ function Dot({ delay }) {
1888
+ return /* @__PURE__ */ jsx(
1889
+ motion.span,
1890
+ {
1891
+ className: "size-1.5 rounded-full bg-current opacity-60",
1892
+ animate: { opacity: [0.2, 1, 0.2] },
1893
+ transition: { duration: 1, repeat: Infinity, delay }
1894
+ }
1895
+ );
1896
+ }
1682
1897
  function findToolArguments(messages, toolMessage) {
1683
1898
  if (!toolMessage.tool_call_id) return {};
1684
1899
  for (let index = messages.length - 1; index >= 0; index -= 1) {
@@ -1695,6 +1910,6 @@ function defineAgentConfig(config) {
1695
1910
  return config;
1696
1911
  }
1697
1912
 
1698
- export { AgentApiClient, AgentApiError, AgentComposer, AgentPanel, AgentWidget, ChatScroller, DEFAULT_ACCEPT, DocumentCard, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove, validateFile };
1913
+ export { AgentApiClient, AgentApiError, AgentComposer, AgentPanel, AgentWidget, ChatScroller, CopyButton, DEFAULT_ACCEPT, DocumentCard, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove, validateFile };
1699
1914
  //# sourceMappingURL=index.js.map
1700
1915
  //# sourceMappingURL=index.js.map