@nextclaw/agent-chat-ui 0.2.2 → 0.2.4

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.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { RefObject } from 'react';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
4
 
4
5
  type ChatTexts = {
5
6
  slashLoadingLabel: string;
@@ -128,6 +129,7 @@ type ChatInputBarProps = {
128
129
  placeholder: string;
129
130
  disabled: boolean;
130
131
  onNodesChange: (nodes: ChatComposerNode[]) => void;
132
+ onFilesAdd?: (files: File[]) => Promise<void> | void;
131
133
  onSlashQueryChange?: (query: string | null) => void;
132
134
  };
133
135
  slashMenu: Pick<ChatSlashMenuProps, 'isLoading' | 'items' | 'texts'>;
@@ -155,6 +157,14 @@ type ChatMessagePartViewModel = {
155
157
  } | {
156
158
  type: 'tool-card';
157
159
  card: ChatToolPartViewModel;
160
+ } | {
161
+ type: 'file';
162
+ file: {
163
+ label: string;
164
+ mimeType: string;
165
+ dataUrl?: string;
166
+ isImage: boolean;
167
+ };
158
168
  } | {
159
169
  type: 'unknown';
160
170
  label: string;
@@ -182,7 +192,15 @@ type ChatMessageListProps = {
182
192
  className?: string;
183
193
  };
184
194
 
185
- declare function ChatInputBar(props: ChatInputBarProps): react_jsx_runtime.JSX.Element;
195
+ type ChatInputBarHandle = {
196
+ insertFileToken: (tokenKey: string, label: string) => void;
197
+ insertFileTokens: (tokens: Array<{
198
+ tokenKey: string;
199
+ label: string;
200
+ }>) => void;
201
+ focusComposer: () => void;
202
+ };
203
+ declare const ChatInputBar: react.ForwardRefExoticComponent<ChatInputBarProps & react.RefAttributes<ChatInputBarHandle>>;
186
204
 
187
205
  declare function ChatMessageList(props: ChatMessageListProps): react_jsx_runtime.JSX.Element;
188
206
 
@@ -246,4 +264,4 @@ declare function resolveChatComposerSlashTrigger(nodes: ChatComposerNode[], sele
246
264
  end: number;
247
265
  } | null;
248
266
 
249
- export { type ChatComposerNode, type ChatComposerSelection, type ChatComposerTextNode, type ChatComposerTokenKind, type ChatComposerTokenNode, type ChatInlineHint, ChatInputBar, type ChatInputBarActionsProps, type ChatInputBarProps, type ChatInputBarToolbarProps, ChatMessageList, type ChatMessageListProps, type ChatMessagePartViewModel, type ChatMessageRole, type ChatMessageTexts, type ChatMessageViewModel, type ChatSelectedItem, type ChatSkillPickerOption, type ChatSkillPickerProps, type ChatSlashItem, type ChatSlashMenuProps, type ChatTexts, type ChatToolPartViewModel, type ChatToolbarAccessory, type ChatToolbarAccessoryIcon, type ChatToolbarIcon, type ChatToolbarSelect, type ChatToolbarSelectOption, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
267
+ export { type ChatComposerNode, type ChatComposerSelection, type ChatComposerTextNode, type ChatComposerTokenKind, type ChatComposerTokenNode, type ChatInlineHint, ChatInputBar, type ChatInputBarActionsProps, type ChatInputBarHandle, type ChatInputBarProps, type ChatInputBarToolbarProps, ChatMessageList, type ChatMessageListProps, type ChatMessagePartViewModel, type ChatMessageRole, type ChatMessageTexts, type ChatMessageViewModel, type ChatSelectedItem, type ChatSkillPickerOption, type ChatSkillPickerProps, type ChatSlashItem, type ChatSlashMenuProps, type ChatTexts, type ChatToolPartViewModel, type ChatToolbarAccessory, type ChatToolbarAccessoryIcon, type ChatToolbarIcon, type ChatToolbarSelect, type ChatToolbarSelectOption, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/chat/ui/chat-input-bar/chat-input-bar.tsx
2
- import { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
2
+ import { forwardRef as forwardRef7, useEffect as useEffect4, useImperativeHandle as useImperativeHandle2, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
3
3
 
4
4
  // src/components/chat/ui/chat-input-bar/chat-slash-menu.tsx
5
5
  import { useMemo, useRef as useRef2 } from "react";
@@ -1082,22 +1082,14 @@ var ChatComposerController = class {
1082
1082
  this.selection = { start: nextOffset, end: nextOffset };
1083
1083
  return this.getSnapshot();
1084
1084
  };
1085
+ this.insertFileToken = (tokenKey, label) => {
1086
+ return this.insertToken("file", tokenKey, label);
1087
+ };
1085
1088
  this.insertSkillToken = (tokenKey, label) => {
1086
1089
  if (this.getSelectedSkillKeys().includes(tokenKey)) {
1087
1090
  return this.getSnapshot();
1088
1091
  }
1089
- const trigger = this.getSlashTrigger();
1090
- const documentLength = this.getDocumentLength();
1091
- const replaceStart = trigger?.start ?? this.selection?.start ?? documentLength;
1092
- const replaceEnd = trigger?.end ?? this.selection?.end ?? replaceStart;
1093
- this.nodes = replaceChatComposerRange(
1094
- this.nodes,
1095
- replaceStart,
1096
- replaceEnd,
1097
- [createChatComposerTokenNode({ tokenKind: "skill", tokenKey, label })]
1098
- );
1099
- this.selection = { start: replaceStart + 1, end: replaceStart + 1 };
1100
- return this.getSnapshot();
1092
+ return this.insertToken("skill", tokenKey, label, this.getSlashTrigger());
1101
1093
  };
1102
1094
  this.syncSelectedSkills = (nextKeys, options) => {
1103
1095
  const selectedSkillKeys = this.getSelectedSkillKeys();
@@ -1150,6 +1142,19 @@ var ChatComposerController = class {
1150
1142
  this.getSelectedSkillKeys = () => {
1151
1143
  return extractChatComposerTokenKeys(this.nodes, "skill");
1152
1144
  };
1145
+ this.insertToken = (tokenKind, tokenKey, label, trigger = null) => {
1146
+ const documentLength = this.getDocumentLength();
1147
+ const replaceStart = trigger?.start ?? this.selection?.start ?? documentLength;
1148
+ const replaceEnd = trigger?.end ?? this.selection?.end ?? replaceStart;
1149
+ this.nodes = replaceChatComposerRange(
1150
+ this.nodes,
1151
+ replaceStart,
1152
+ replaceEnd,
1153
+ [createChatComposerTokenNode({ tokenKind, tokenKey, label })]
1154
+ );
1155
+ this.selection = { start: replaceStart + 1, end: replaceStart + 1 };
1156
+ return this.getSnapshot();
1157
+ };
1153
1158
  this.getSlashTrigger = () => {
1154
1159
  return resolveChatComposerSlashTrigger(this.nodes, this.selection);
1155
1160
  };
@@ -1246,32 +1251,76 @@ var ChatComposerSurfaceRenderer = class {
1246
1251
  element.dataset.composerTokenKind = node.tokenKind;
1247
1252
  element.dataset.composerTokenKey = node.tokenKey;
1248
1253
  element.dataset.composerLabel = node.label;
1249
- element.className = [
1254
+ element.title = node.label;
1255
+ element.className = this.buildTokenClassName(node.tokenKind, isSelected);
1256
+ element.append(this.createTokenIcon(node.tokenKind));
1257
+ const label = document.createElement("span");
1258
+ label.className = node.tokenKind === "file" ? "min-w-0 flex-1 truncate text-[12px] font-medium text-slate-800" : "truncate";
1259
+ label.textContent = node.label;
1260
+ element.append(label);
1261
+ if (node.tokenKind === "file") {
1262
+ const badge = document.createElement("span");
1263
+ badge.className = [
1264
+ "hidden",
1265
+ "shrink-0",
1266
+ "rounded-md",
1267
+ "border",
1268
+ "border-sky-100",
1269
+ "bg-sky-50",
1270
+ "px-1.5",
1271
+ "py-0.5",
1272
+ "text-[9px]",
1273
+ "font-semibold",
1274
+ "uppercase",
1275
+ "tracking-[0.12em]",
1276
+ "text-sky-700",
1277
+ "sm:inline-flex"
1278
+ ].join(" ");
1279
+ badge.textContent = this.resolveFileBadgeLabel(node.label);
1280
+ element.append(badge);
1281
+ }
1282
+ return element;
1283
+ };
1284
+ this.buildTokenClassName = (tokenKind, isSelected) => {
1285
+ if (tokenKind === "file") {
1286
+ return [
1287
+ "mx-[2px]",
1288
+ "inline-flex",
1289
+ "h-8",
1290
+ "max-w-[min(100%,19rem)]",
1291
+ "items-center",
1292
+ "gap-2",
1293
+ "rounded-xl",
1294
+ "border",
1295
+ "px-2",
1296
+ "pr-2.5",
1297
+ "align-baseline",
1298
+ "shadow-[0_1px_2px_rgba(15,23,42,0.06)]",
1299
+ "transition-[border-color,background-color,box-shadow,color]",
1300
+ "duration-150",
1301
+ isSelected ? "border-sky-300 bg-sky-50 text-slate-900 shadow-[0_0_0_3px_rgba(14,165,233,0.14)]" : "border-slate-200 bg-[linear-gradient(180deg,rgba(255,255,255,1),rgba(248,250,252,0.98))] text-slate-700"
1302
+ ].join(" ");
1303
+ }
1304
+ return [
1250
1305
  "mx-[2px]",
1251
1306
  "inline-flex",
1252
- "h-6",
1307
+ "h-7",
1253
1308
  "max-w-full",
1254
1309
  "items-center",
1255
- "gap-1",
1256
- "rounded-md",
1310
+ "gap-1.5",
1311
+ "rounded-lg",
1257
1312
  "border",
1258
- "px-1.5",
1313
+ "px-2",
1259
1314
  "align-baseline",
1260
1315
  "text-[11px]",
1261
1316
  "font-medium",
1262
1317
  "transition",
1263
1318
  isSelected ? "border-primary/30 bg-primary/18 text-primary" : "border-primary/12 bg-primary/8 text-primary"
1264
1319
  ].join(" ");
1265
- element.append(this.createTokenIcon(node.tokenKind));
1266
- const label = document.createElement("span");
1267
- label.className = "truncate";
1268
- label.textContent = node.label;
1269
- element.append(label);
1270
- return element;
1271
1320
  };
1272
1321
  this.createTokenIcon = (tokenKind) => {
1273
1322
  const wrapper = document.createElement("span");
1274
- wrapper.className = "inline-flex h-3 w-3 shrink-0 items-center justify-center text-primary/70";
1323
+ wrapper.className = tokenKind === "file" ? "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-lg bg-sky-100 text-sky-700" : "inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center text-primary/70";
1275
1324
  wrapper.append(tokenKind === "file" ? this.createFileIcon() : this.createSkillIcon());
1276
1325
  return wrapper;
1277
1326
  };
@@ -1285,12 +1334,15 @@ var ChatComposerSurfaceRenderer = class {
1285
1334
  };
1286
1335
  this.createFileIcon = () => {
1287
1336
  return this.createSvgIcon([
1288
- { tag: "path", attrs: { d: "M5.25 2.75h4.5L13 6v7.25A1.75 1.75 0 0 1 11.25 15h-6.5A1.75 1.75 0 0 1 3 13.25v-8.75A1.75 1.75 0 0 1 4.75 2.75Z" } },
1289
- { tag: "path", attrs: { d: "M9.75 2.75V6H13" } },
1290
- { tag: "path", attrs: { d: "M5.75 8.75h4.5" } },
1291
- { tag: "path", attrs: { d: "M5.75 10.75h4.5" } }
1337
+ { tag: "path", attrs: { d: "M3.25 4.25A1.5 1.5 0 0 1 4.75 2.75h6.5a1.5 1.5 0 0 1 1.5 1.5v7.5a1.5 1.5 0 0 1-1.5 1.5h-6.5a1.5 1.5 0 0 1-1.5-1.5v-7.5Z" } },
1338
+ { tag: "path", attrs: { d: "m4.75 10 2.25-2.5 1.75 1.75 1.25-1.25 2 2" } },
1339
+ { tag: "path", attrs: { d: "M9.75 6.25h.01" } }
1292
1340
  ]);
1293
1341
  };
1342
+ this.resolveFileBadgeLabel = (label) => {
1343
+ const match = /\.([a-z0-9]+)$/i.exec(label.trim());
1344
+ return match?.[1]?.slice(0, 4).toUpperCase() || "IMG";
1345
+ };
1294
1346
  this.createSvgIcon = (children) => {
1295
1347
  const svg = document.createElementNS(SVG_NAMESPACE, "svg");
1296
1348
  svg.setAttribute("viewBox", "0 0 16 16");
@@ -1443,7 +1495,13 @@ var ChatComposerViewController = class {
1443
1495
  }
1444
1496
  };
1445
1497
  this.handlePaste = (params) => {
1446
- const { event, commitSnapshot } = params;
1498
+ const { event, onFilesAdd, commitSnapshot } = params;
1499
+ const files = Array.from(event.clipboardData.files ?? []);
1500
+ if (files.length > 0 && onFilesAdd) {
1501
+ event.preventDefault();
1502
+ void onFilesAdd(files);
1503
+ return;
1504
+ }
1447
1505
  const text = event.clipboardData.getData("text/plain");
1448
1506
  if (!text) {
1449
1507
  return;
@@ -1452,8 +1510,8 @@ var ChatComposerViewController = class {
1452
1510
  commitSnapshot(this.controller.insertText(text));
1453
1511
  };
1454
1512
  this.handleBlur = (params) => {
1455
- const { setSelectedRange, onSlashQueryChange, onSlashOpenChange } = params;
1456
- setSelectedRange(null);
1513
+ const { clearSelectedRange, onSlashQueryChange, onSlashOpenChange } = params;
1514
+ clearSelectedRange();
1457
1515
  onSlashQueryChange?.(null);
1458
1516
  onSlashOpenChange(false);
1459
1517
  };
@@ -1497,6 +1555,23 @@ var ChatComposerRuntime = class {
1497
1555
  insertSlashItem: (item) => {
1498
1556
  this.viewController.insertSlashItem(item, this.commitSnapshot);
1499
1557
  },
1558
+ insertFileToken: (tokenKey, label) => {
1559
+ this.commitSnapshot(this.controller.insertFileToken(tokenKey, label));
1560
+ this.focusComposerSoon();
1561
+ },
1562
+ insertFileTokens: (tokens) => {
1563
+ let nextSnapshot = null;
1564
+ for (const token of tokens) {
1565
+ nextSnapshot = this.controller.insertFileToken(token.tokenKey, token.label);
1566
+ }
1567
+ if (nextSnapshot) {
1568
+ this.commitSnapshot(nextSnapshot);
1569
+ this.focusComposerSoon();
1570
+ }
1571
+ },
1572
+ focusComposer: () => {
1573
+ this.focusComposer();
1574
+ },
1500
1575
  syncSelectedSkills: (nextKeys, options) => {
1501
1576
  this.viewController.syncSelectedSkills(nextKeys, options, this.commitSnapshot);
1502
1577
  }
@@ -1558,6 +1633,12 @@ var ChatComposerRuntime = class {
1558
1633
  };
1559
1634
  this.handleKeyDown = (event) => {
1560
1635
  const config = this.requireConfig();
1636
+ if (this.rootElement && !this.isComposing) {
1637
+ const nextSnapshot = this.viewController.syncSelectionFromRoot(this.rootElement);
1638
+ this.selection = nextSnapshot.selection;
1639
+ this.selectedRange = nextSnapshot.selection;
1640
+ this.snapshot = nextSnapshot;
1641
+ }
1561
1642
  const activeSlashItem = config.slashItems[config.activeSlashIndex] ?? null;
1562
1643
  this.viewController.handleKeyDown({
1563
1644
  event,
@@ -1575,6 +1656,7 @@ var ChatComposerRuntime = class {
1575
1656
  this.handlePaste = (event) => {
1576
1657
  this.viewController.handlePaste({
1577
1658
  event,
1659
+ onFilesAdd: this.requireConfig().onFilesAdd,
1578
1660
  commitSnapshot: this.commitSnapshot
1579
1661
  });
1580
1662
  };
@@ -1582,7 +1664,7 @@ var ChatComposerRuntime = class {
1582
1664
  const config = this.requireConfig();
1583
1665
  this.isComposing = false;
1584
1666
  this.viewController.handleBlur({
1585
- setSelectedRange: this.setSelectedRange,
1667
+ clearSelectedRange: this.clearSelectedRange,
1586
1668
  onSlashQueryChange: config.onSlashQueryChange,
1587
1669
  onSlashOpenChange: config.onSlashOpenChange
1588
1670
  });
@@ -1592,6 +1674,10 @@ var ChatComposerRuntime = class {
1592
1674
  this.selection = selection;
1593
1675
  this.requestRender();
1594
1676
  };
1677
+ this.clearSelectedRange = () => {
1678
+ this.selectedRange = null;
1679
+ this.requestRender();
1680
+ };
1595
1681
  this.commitSnapshot = (nextSnapshot) => {
1596
1682
  const config = this.requireConfig();
1597
1683
  this.selection = nextSnapshot.selection;
@@ -1605,12 +1691,27 @@ var ChatComposerRuntime = class {
1605
1691
  };
1606
1692
  this.syncSlashState = (nextSnapshot) => {
1607
1693
  const config = this.requireConfig();
1694
+ config.onSlashTriggerChange?.(nextSnapshot.slashTrigger);
1608
1695
  config.onSlashQueryChange?.(nextSnapshot.slashTrigger?.query ?? null);
1609
1696
  config.onSlashOpenChange(nextSnapshot.slashTrigger !== null);
1610
1697
  };
1611
1698
  this.requestRender = () => {
1612
1699
  this.requireConfig().requestRender();
1613
1700
  };
1701
+ this.focusComposerSoon = () => {
1702
+ if (typeof requestAnimationFrame === "function") {
1703
+ requestAnimationFrame(() => this.focusComposer());
1704
+ return;
1705
+ }
1706
+ this.focusComposer();
1707
+ };
1708
+ this.focusComposer = () => {
1709
+ if (!this.rootElement) {
1710
+ return;
1711
+ }
1712
+ this.rootElement.focus();
1713
+ this.viewController.restoreSelectionIfFocused(this.rootElement, this.selection);
1714
+ };
1614
1715
  this.requireConfig = () => {
1615
1716
  if (!this.config) {
1616
1717
  throw new Error("ChatComposerRuntime is not configured.");
@@ -1630,7 +1731,9 @@ var ChatInputBarTokenizedComposer = forwardRef6(function ChatInputBarTokenizedCo
1630
1731
  slashItems,
1631
1732
  actions,
1632
1733
  onNodesChange,
1734
+ onFilesAdd,
1633
1735
  onSlashQueryChange,
1736
+ onSlashTriggerChange,
1634
1737
  onSlashOpenChange,
1635
1738
  onSlashActiveIndexChange,
1636
1739
  activeSlashIndex
@@ -1655,7 +1758,9 @@ var ChatInputBarTokenizedComposer = forwardRef6(function ChatInputBarTokenizedCo
1655
1758
  slashItems,
1656
1759
  actions,
1657
1760
  onNodesChange,
1761
+ onFilesAdd,
1658
1762
  onSlashQueryChange,
1763
+ onSlashTriggerChange,
1659
1764
  onSlashOpenChange,
1660
1765
  onSlashActiveIndexChange,
1661
1766
  activeSlashIndex,
@@ -1718,11 +1823,13 @@ function InputBarHint({ hint }) {
1718
1823
  ) : null
1719
1824
  ] }) });
1720
1825
  }
1721
- function ChatInputBar(props) {
1826
+ var ChatInputBar = forwardRef7(function ChatInputBar2(props, ref) {
1722
1827
  const composerRef = useRef4(null);
1723
1828
  const [slashQuery, setSlashQuery] = useState4(null);
1724
1829
  const [activeSlashIndex, setActiveSlashIndex] = useState4(0);
1725
- const isSlashPanelOpen = slashQuery !== null;
1830
+ const [activeSlashTriggerStart, setActiveSlashTriggerStart] = useState4(null);
1831
+ const [dismissedSlashTriggerStart, setDismissedSlashTriggerStart] = useState4(null);
1832
+ const isSlashPanelOpen = activeSlashTriggerStart !== null && dismissedSlashTriggerStart !== activeSlashTriggerStart;
1726
1833
  const activeSlashItem = props.slashMenu.items[activeSlashIndex] ?? null;
1727
1834
  useEffect4(() => {
1728
1835
  setActiveSlashIndex((current) => {
@@ -1737,6 +1844,11 @@ function ChatInputBar(props) {
1737
1844
  setActiveSlashIndex(0);
1738
1845
  }
1739
1846
  }, [slashQuery]);
1847
+ useEffect4(() => {
1848
+ if (activeSlashTriggerStart === null && dismissedSlashTriggerStart !== null) {
1849
+ setDismissedSlashTriggerStart(null);
1850
+ }
1851
+ }, [activeSlashTriggerStart, dismissedSlashTriggerStart]);
1740
1852
  const toolbar = useMemo3(() => {
1741
1853
  if (!props.toolbar.skillPicker) {
1742
1854
  return props.toolbar;
@@ -1751,6 +1863,11 @@ function ChatInputBar(props) {
1751
1863
  }
1752
1864
  };
1753
1865
  }, [props.toolbar]);
1866
+ useImperativeHandle2(ref, () => ({
1867
+ insertFileToken: (tokenKey, label) => composerRef.current?.insertFileToken(tokenKey, label),
1868
+ insertFileTokens: (tokens) => composerRef.current?.insertFileTokens(tokens),
1869
+ focusComposer: () => composerRef.current?.focusComposer()
1870
+ }), []);
1754
1871
  return /* @__PURE__ */ jsx11("div", { className: "border-t border-gray-200/80 bg-white p-4", children: /* @__PURE__ */ jsx11("div", { className: "mx-auto w-full max-w-[min(1120px,100%)]", children: /* @__PURE__ */ jsxs6("div", { className: "overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-card", children: [
1755
1872
  /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
1756
1873
  /* @__PURE__ */ jsx11(
@@ -1764,13 +1881,17 @@ function ChatInputBar(props) {
1764
1881
  actions: props.toolbar.actions,
1765
1882
  activeSlashIndex,
1766
1883
  onNodesChange: props.composer.onNodesChange,
1884
+ onFilesAdd: props.composer.onFilesAdd,
1767
1885
  onSlashQueryChange: (query) => {
1768
1886
  setSlashQuery(query);
1769
1887
  props.composer.onSlashQueryChange?.(query);
1770
1888
  },
1889
+ onSlashTriggerChange: (trigger) => {
1890
+ setActiveSlashTriggerStart(trigger?.start ?? null);
1891
+ },
1771
1892
  onSlashOpenChange: (open) => {
1772
- if (!open) {
1773
- setSlashQuery(null);
1893
+ if (!open && activeSlashTriggerStart !== null) {
1894
+ setDismissedSlashTriggerStart(activeSlashTriggerStart);
1774
1895
  }
1775
1896
  },
1776
1897
  onSlashActiveIndexChange: setActiveSlashIndex
@@ -1786,11 +1907,12 @@ function ChatInputBar(props) {
1786
1907
  activeItem: activeSlashItem,
1787
1908
  texts: props.slashMenu.texts,
1788
1909
  onSelectItem: (item) => {
1910
+ setDismissedSlashTriggerStart(null);
1789
1911
  composerRef.current?.insertSlashItem(item);
1790
1912
  },
1791
1913
  onOpenChange: (open) => {
1792
- if (!open) {
1793
- setSlashQuery(null);
1914
+ if (!open && activeSlashTriggerStart !== null) {
1915
+ setDismissedSlashTriggerStart(activeSlashTriggerStart);
1794
1916
  }
1795
1917
  },
1796
1918
  onSetActiveIndex: setActiveSlashIndex
@@ -1800,7 +1922,8 @@ function ChatInputBar(props) {
1800
1922
  /* @__PURE__ */ jsx11(InputBarHint, { hint: props.hint }),
1801
1923
  /* @__PURE__ */ jsx11(ChatInputBarToolbar, { ...toolbar })
1802
1924
  ] }) }) });
1803
- }
1925
+ });
1926
+ ChatInputBar.displayName = "ChatInputBar";
1804
1927
 
1805
1928
  // src/components/chat/ui/chat-message-list/chat-message-avatar.tsx
1806
1929
  import { Bot, User, Wrench } from "lucide-react";
@@ -2077,12 +2200,27 @@ function ChatMessageMarkdown(props) {
2077
2200
  return /* @__PURE__ */ jsx14("div", { className: cn("chat-markdown", isUser ? "chat-markdown-user" : "chat-markdown-assistant"), children: /* @__PURE__ */ jsx14(ReactMarkdown, { skipHtml: true, remarkPlugins: [remarkGfm], components: markdownComponents, children: trimMarkdown(props.text) }) });
2078
2201
  }
2079
2202
 
2080
- // src/components/chat/ui/chat-message-list/chat-reasoning-block.tsx
2203
+ // src/components/chat/ui/chat-message-list/chat-message-file.tsx
2081
2204
  import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
2205
+ function ChatMessageFile({ file }) {
2206
+ if (file.isImage && file.dataUrl) {
2207
+ return /* @__PURE__ */ jsxs8("figure", { className: "overflow-hidden rounded-2xl border border-black/8 bg-black/6", children: [
2208
+ /* @__PURE__ */ jsx15("img", { src: file.dataUrl, alt: file.label, className: "block max-h-80 w-full object-contain" }),
2209
+ /* @__PURE__ */ jsx15("figcaption", { className: "border-t border-black/8 px-3 py-2 text-xs opacity-80", children: file.label })
2210
+ ] });
2211
+ }
2212
+ return /* @__PURE__ */ jsxs8("div", { className: "rounded-2xl border border-black/8 bg-black/6 px-3 py-2 text-sm", children: [
2213
+ /* @__PURE__ */ jsx15("div", { className: "font-medium", children: file.label }),
2214
+ /* @__PURE__ */ jsx15("div", { className: "text-xs opacity-75", children: file.mimeType })
2215
+ ] });
2216
+ }
2217
+
2218
+ // src/components/chat/ui/chat-message-list/chat-reasoning-block.tsx
2219
+ import { jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
2082
2220
  function ChatReasoningBlock(props) {
2083
- return /* @__PURE__ */ jsxs8("details", { className: "mt-3", open: true, children: [
2084
- /* @__PURE__ */ jsx15("summary", { className: cn("cursor-pointer text-xs", props.isUser ? "text-primary-100" : "text-gray-500"), children: props.label }),
2085
- /* @__PURE__ */ jsx15(
2221
+ return /* @__PURE__ */ jsxs9("details", { className: "mt-3", open: true, children: [
2222
+ /* @__PURE__ */ jsx16("summary", { className: cn("cursor-pointer text-xs", props.isUser ? "text-primary-100" : "text-gray-500"), children: props.label }),
2223
+ /* @__PURE__ */ jsx16(
2086
2224
  "pre",
2087
2225
  {
2088
2226
  className: cn(
@@ -2097,87 +2235,90 @@ function ChatReasoningBlock(props) {
2097
2235
 
2098
2236
  // src/components/chat/ui/chat-message-list/chat-tool-card.tsx
2099
2237
  import { Clock3, FileSearch, Globe, Search as Search2, SendHorizontal, Terminal, Wrench as Wrench2 } from "lucide-react";
2100
- import { jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
2238
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
2101
2239
  var TOOL_OUTPUT_PREVIEW_MAX = 220;
2102
2240
  function renderToolIcon(toolName) {
2103
2241
  const lowered = toolName.toLowerCase();
2104
2242
  if (lowered.includes("exec") || lowered.includes("shell") || lowered.includes("command")) {
2105
- return /* @__PURE__ */ jsx16(Terminal, { className: "h-3.5 w-3.5" });
2243
+ return /* @__PURE__ */ jsx17(Terminal, { className: "h-3.5 w-3.5" });
2106
2244
  }
2107
2245
  if (lowered.includes("search")) {
2108
- return /* @__PURE__ */ jsx16(Search2, { className: "h-3.5 w-3.5" });
2246
+ return /* @__PURE__ */ jsx17(Search2, { className: "h-3.5 w-3.5" });
2109
2247
  }
2110
2248
  if (lowered.includes("fetch") || lowered.includes("http") || lowered.includes("web")) {
2111
- return /* @__PURE__ */ jsx16(Globe, { className: "h-3.5 w-3.5" });
2249
+ return /* @__PURE__ */ jsx17(Globe, { className: "h-3.5 w-3.5" });
2112
2250
  }
2113
2251
  if (lowered.includes("read") || lowered.includes("file")) {
2114
- return /* @__PURE__ */ jsx16(FileSearch, { className: "h-3.5 w-3.5" });
2252
+ return /* @__PURE__ */ jsx17(FileSearch, { className: "h-3.5 w-3.5" });
2115
2253
  }
2116
2254
  if (lowered.includes("message") || lowered.includes("send")) {
2117
- return /* @__PURE__ */ jsx16(SendHorizontal, { className: "h-3.5 w-3.5" });
2255
+ return /* @__PURE__ */ jsx17(SendHorizontal, { className: "h-3.5 w-3.5" });
2118
2256
  }
2119
2257
  if (lowered.includes("cron") || lowered.includes("schedule")) {
2120
- return /* @__PURE__ */ jsx16(Clock3, { className: "h-3.5 w-3.5" });
2258
+ return /* @__PURE__ */ jsx17(Clock3, { className: "h-3.5 w-3.5" });
2121
2259
  }
2122
- return /* @__PURE__ */ jsx16(Wrench2, { className: "h-3.5 w-3.5" });
2260
+ return /* @__PURE__ */ jsx17(Wrench2, { className: "h-3.5 w-3.5" });
2123
2261
  }
2124
2262
  function ChatToolCard({ card }) {
2125
2263
  const output = card.output?.trim() ?? "";
2126
2264
  const showDetails = output.length > TOOL_OUTPUT_PREVIEW_MAX || output.includes("\n");
2127
2265
  const preview = showDetails ? `${output.slice(0, TOOL_OUTPUT_PREVIEW_MAX)}...` : output;
2128
2266
  const showOutputSection = card.kind === "result" || card.hasResult;
2129
- return /* @__PURE__ */ jsxs9("div", { className: "rounded-xl border border-amber-200/80 bg-amber-50/60 px-3 py-2.5", children: [
2130
- /* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap items-center gap-2 text-xs font-semibold text-amber-800", children: [
2267
+ return /* @__PURE__ */ jsxs10("div", { className: "rounded-xl border border-amber-200/80 bg-amber-50/60 px-3 py-2.5", children: [
2268
+ /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-center gap-2 text-xs font-semibold text-amber-800", children: [
2131
2269
  renderToolIcon(card.toolName),
2132
- /* @__PURE__ */ jsx16("span", { children: card.titleLabel }),
2133
- /* @__PURE__ */ jsx16("span", { className: "font-mono text-[11px] text-amber-900/80", children: card.toolName })
2270
+ /* @__PURE__ */ jsx17("span", { children: card.titleLabel }),
2271
+ /* @__PURE__ */ jsx17("span", { className: "font-mono text-[11px] text-amber-900/80", children: card.toolName })
2134
2272
  ] }),
2135
- card.summary ? /* @__PURE__ */ jsx16("div", { className: "mt-1 break-words font-mono text-[11px] text-amber-800/90", children: card.summary }) : null,
2136
- showOutputSection ? /* @__PURE__ */ jsx16("div", { className: "mt-2", children: !output ? /* @__PURE__ */ jsx16("div", { className: "text-[11px] text-amber-700/80", children: card.emptyLabel }) : showDetails ? /* @__PURE__ */ jsxs9("details", { className: "group", children: [
2137
- /* @__PURE__ */ jsx16("summary", { className: "cursor-pointer text-[11px] text-amber-700", children: card.outputLabel }),
2138
- /* @__PURE__ */ jsx16("pre", { className: "mt-2 whitespace-pre-wrap break-words rounded-lg border border-amber-200 bg-amber-100/40 p-2 text-[11px] text-amber-900", children: output })
2139
- ] }) : /* @__PURE__ */ jsx16("pre", { className: "rounded-lg border border-amber-200 bg-amber-100/40 p-2 text-[11px] whitespace-pre-wrap break-words text-amber-900", children: preview }) }) : null
2273
+ card.summary ? /* @__PURE__ */ jsx17("div", { className: "mt-1 break-words font-mono text-[11px] text-amber-800/90", children: card.summary }) : null,
2274
+ showOutputSection ? /* @__PURE__ */ jsx17("div", { className: "mt-2", children: !output ? /* @__PURE__ */ jsx17("div", { className: "text-[11px] text-amber-700/80", children: card.emptyLabel }) : showDetails ? /* @__PURE__ */ jsxs10("details", { className: "group", children: [
2275
+ /* @__PURE__ */ jsx17("summary", { className: "cursor-pointer text-[11px] text-amber-700", children: card.outputLabel }),
2276
+ /* @__PURE__ */ jsx17("pre", { className: "mt-2 whitespace-pre-wrap break-words rounded-lg border border-amber-200 bg-amber-100/40 p-2 text-[11px] text-amber-900", children: output })
2277
+ ] }) : /* @__PURE__ */ jsx17("pre", { className: "rounded-lg border border-amber-200 bg-amber-100/40 p-2 text-[11px] whitespace-pre-wrap break-words text-amber-900", children: preview }) }) : null
2140
2278
  ] });
2141
2279
  }
2142
2280
 
2143
2281
  // src/components/chat/ui/chat-message-list/chat-unknown-part.tsx
2144
- import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
2282
+ import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
2145
2283
  function ChatUnknownPart(props) {
2146
- return /* @__PURE__ */ jsxs10("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-2 text-xs text-gray-600", children: [
2147
- /* @__PURE__ */ jsxs10("div", { className: "font-semibold text-gray-700", children: [
2284
+ return /* @__PURE__ */ jsxs11("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-2 text-xs text-gray-600", children: [
2285
+ /* @__PURE__ */ jsxs11("div", { className: "font-semibold text-gray-700", children: [
2148
2286
  props.label,
2149
2287
  ": ",
2150
2288
  props.rawType
2151
2289
  ] }),
2152
- props.text ? /* @__PURE__ */ jsx17("pre", { className: "mt-1 whitespace-pre-wrap break-words text-[11px] text-gray-500", children: props.text }) : null
2290
+ props.text ? /* @__PURE__ */ jsx18("pre", { className: "mt-1 whitespace-pre-wrap break-words text-[11px] text-gray-500", children: props.text }) : null
2153
2291
  ] });
2154
2292
  }
2155
2293
 
2156
2294
  // src/components/chat/ui/chat-message-list/chat-message.tsx
2157
- import { jsx as jsx18 } from "react/jsx-runtime";
2295
+ import { jsx as jsx19 } from "react/jsx-runtime";
2158
2296
  function ChatMessage(props) {
2159
2297
  const { message, texts } = props;
2160
2298
  const { role } = message;
2161
2299
  const isUser = role === "user";
2162
- return /* @__PURE__ */ jsx18(
2300
+ return /* @__PURE__ */ jsx19(
2163
2301
  "div",
2164
2302
  {
2165
2303
  className: cn(
2166
2304
  "inline-block w-fit max-w-full rounded-2xl border px-4 py-3 shadow-sm",
2167
2305
  isUser ? "border-primary bg-primary text-white" : role === "assistant" ? "border-gray-200 bg-white text-gray-900" : "border-orange-200/80 bg-orange-50/70 text-gray-900"
2168
2306
  ),
2169
- children: /* @__PURE__ */ jsx18("div", { className: "space-y-2", children: message.parts.map((part, index) => {
2307
+ children: /* @__PURE__ */ jsx19("div", { className: "space-y-2", children: message.parts.map((part, index) => {
2170
2308
  if (part.type === "markdown") {
2171
- return /* @__PURE__ */ jsx18(ChatMessageMarkdown, { text: part.text, role, texts }, `markdown-${index}`);
2309
+ return /* @__PURE__ */ jsx19(ChatMessageMarkdown, { text: part.text, role, texts }, `markdown-${index}`);
2172
2310
  }
2173
2311
  if (part.type === "reasoning") {
2174
- return /* @__PURE__ */ jsx18(ChatReasoningBlock, { label: part.label, text: part.text, isUser }, `reasoning-${index}`);
2312
+ return /* @__PURE__ */ jsx19(ChatReasoningBlock, { label: part.label, text: part.text, isUser }, `reasoning-${index}`);
2175
2313
  }
2176
2314
  if (part.type === "tool-card") {
2177
- return /* @__PURE__ */ jsx18("div", { className: "mt-0.5", children: /* @__PURE__ */ jsx18(ChatToolCard, { card: part.card }) }, `tool-${index}`);
2315
+ return /* @__PURE__ */ jsx19("div", { className: "mt-0.5", children: /* @__PURE__ */ jsx19(ChatToolCard, { card: part.card }) }, `tool-${index}`);
2316
+ }
2317
+ if (part.type === "file") {
2318
+ return /* @__PURE__ */ jsx19(ChatMessageFile, { file: part.file }, `file-${index}`);
2178
2319
  }
2179
2320
  if (part.type === "unknown") {
2180
- return /* @__PURE__ */ jsx18(ChatUnknownPart, { label: part.label, rawType: part.rawType, text: part.text }, `unknown-${index}`);
2321
+ return /* @__PURE__ */ jsx19(ChatUnknownPart, { label: part.label, rawType: part.rawType, text: part.text }, `unknown-${index}`);
2181
2322
  }
2182
2323
  return null;
2183
2324
  }) })
@@ -2186,9 +2327,9 @@ function ChatMessage(props) {
2186
2327
  }
2187
2328
 
2188
2329
  // src/components/chat/ui/chat-message-list/chat-message-meta.tsx
2189
- import { jsxs as jsxs11 } from "react/jsx-runtime";
2330
+ import { jsxs as jsxs12 } from "react/jsx-runtime";
2190
2331
  function ChatMessageMeta(props) {
2191
- return /* @__PURE__ */ jsxs11(
2332
+ return /* @__PURE__ */ jsxs12(
2192
2333
  "div",
2193
2334
  {
2194
2335
  className: cn(
@@ -2205,7 +2346,7 @@ function ChatMessageMeta(props) {
2205
2346
  }
2206
2347
 
2207
2348
  // src/components/chat/ui/chat-message-list/chat-message-list.tsx
2208
- import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
2349
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
2209
2350
  var INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
2210
2351
  function hasRenderableText(value) {
2211
2352
  const trimmed = value.trim();
@@ -2227,21 +2368,21 @@ function ChatMessageList(props) {
2227
2368
  const hasRenderableAssistantDraft = visibleMessages.some(
2228
2369
  (message) => message.role === "assistant" && (message.status === "streaming" || message.status === "pending")
2229
2370
  );
2230
- return /* @__PURE__ */ jsxs12("div", { className: cn("space-y-5", props.className), children: [
2371
+ return /* @__PURE__ */ jsxs13("div", { className: cn("space-y-5", props.className), children: [
2231
2372
  visibleMessages.map((message) => {
2232
2373
  const isUser = message.role === "user";
2233
- return /* @__PURE__ */ jsxs12("div", { className: cn("flex gap-3", isUser ? "justify-end" : "justify-start"), children: [
2234
- !isUser ? /* @__PURE__ */ jsx19(ChatMessageAvatar, { role: message.role }) : null,
2235
- /* @__PURE__ */ jsxs12("div", { className: cn("w-fit max-w-[92%] space-y-2", isUser && "flex flex-col items-end"), children: [
2236
- /* @__PURE__ */ jsx19(ChatMessage, { message, texts: props.texts }),
2237
- /* @__PURE__ */ jsx19(ChatMessageMeta, { roleLabel: message.roleLabel, timestampLabel: message.timestampLabel, isUser })
2374
+ return /* @__PURE__ */ jsxs13("div", { className: cn("flex gap-3", isUser ? "justify-end" : "justify-start"), children: [
2375
+ !isUser ? /* @__PURE__ */ jsx20(ChatMessageAvatar, { role: message.role }) : null,
2376
+ /* @__PURE__ */ jsxs13("div", { className: cn("w-fit max-w-[92%] space-y-2", isUser && "flex flex-col items-end"), children: [
2377
+ /* @__PURE__ */ jsx20(ChatMessage, { message, texts: props.texts }),
2378
+ /* @__PURE__ */ jsx20(ChatMessageMeta, { roleLabel: message.roleLabel, timestampLabel: message.timestampLabel, isUser })
2238
2379
  ] }),
2239
- isUser ? /* @__PURE__ */ jsx19(ChatMessageAvatar, { role: message.role }) : null
2380
+ isUser ? /* @__PURE__ */ jsx20(ChatMessageAvatar, { role: message.role }) : null
2240
2381
  ] }, message.id);
2241
2382
  }),
2242
- props.isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs12("div", { className: "flex justify-start gap-3", children: [
2243
- /* @__PURE__ */ jsx19(ChatMessageAvatar, { role: "assistant" }),
2244
- /* @__PURE__ */ jsx19("div", { className: "rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm", children: props.texts.typingLabel })
2383
+ props.isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs13("div", { className: "flex justify-start gap-3", children: [
2384
+ /* @__PURE__ */ jsx20(ChatMessageAvatar, { role: "assistant" }),
2385
+ /* @__PURE__ */ jsx20("div", { className: "rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm", children: props.texts.typingLabel })
2245
2386
  ] }) : null
2246
2387
  ] });
2247
2388
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/agent-chat-ui",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "description": "Reusable Nextclaw agent chat UI primitives and default skin.",
6
6
  "type": "module",