@nextclaw/agent-chat-ui 0.3.16 → 0.3.18

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
@@ -146,6 +146,7 @@ type ChatSlashMenuProps = {
146
146
  texts: Pick<ChatTexts, "slashLoadingLabel" | "slashSectionLabel" | "slashEmptyLabel" | "slashHintLabel" | "slashSkillHintLabel">;
147
147
  onSelectItem: (item: ChatSlashItem) => void;
148
148
  onOpenChange: (open: boolean) => void;
149
+ onDetailsPointerDown?: () => void;
149
150
  onSetActiveIndex: (index: number) => void;
150
151
  };
151
152
  type ChatInputBarProps = {
@@ -306,6 +307,7 @@ type ChatInputBarHandle = {
306
307
  label: string;
307
308
  }>) => void;
308
309
  focusComposer: () => void;
310
+ focusComposerAtEnd: () => void;
309
311
  };
310
312
  declare const ChatInputBar: _$react.ForwardRefExoticComponent<ChatInputBarProps & _$react.RefAttributes<ChatInputBarHandle>>;
311
313
  //#endregion
package/dist/index.js CHANGED
@@ -208,7 +208,7 @@ function ChatSlashMenu(props) {
208
208
  const { Popover, PopoverAnchor, PopoverContent } = ChatUiPrimitives;
209
209
  const { elementRef: anchorRef, width: panelWidth } = useElementWidth();
210
210
  const listRef = useRef(null);
211
- const { isOpen, isLoading, items, activeIndex, activeItem, texts, onSelectItem, onOpenChange, onSetActiveIndex } = props;
211
+ const { isOpen, isLoading, items, activeIndex, activeItem, texts, onSelectItem, onOpenChange, onDetailsPointerDown, onSetActiveIndex } = props;
212
212
  const resolvedWidth = useMemo(() => {
213
213
  if (!panelWidth) return;
214
214
  return Math.min(panelWidth > SLASH_PANEL_DESKTOP_MIN_WIDTH ? panelWidth * SLASH_PANEL_DESKTOP_SHRINK_RATIO : panelWidth, SLASH_PANEL_MAX_WIDTH);
@@ -276,6 +276,7 @@ function ChatSlashMenu(props) {
276
276
  })] })
277
277
  }), /* @__PURE__ */ jsx("div", {
278
278
  className: "min-w-0 p-2.5",
279
+ onPointerDown: onDetailsPointerDown,
279
280
  children: activeItem ? /* @__PURE__ */ jsxs("div", {
280
281
  className: "space-y-3",
281
282
  children: [
@@ -630,13 +631,11 @@ function ChatInputBarSkillPicker(props) {
630
631
  visibleIndex += 1;
631
632
  const isSelected = selectedSet.has(option.key);
632
633
  const isActive = index === activeIndex;
633
- return /* @__PURE__ */ jsxs("button", {
634
- type: "button",
634
+ return /* @__PURE__ */ jsxs("div", {
635
635
  id: `${listId}-option-${index}`,
636
636
  role: "option",
637
637
  "aria-selected": isSelected,
638
638
  "data-skill-index": index,
639
- onClick: () => onToggleOption(option.key),
640
639
  onMouseEnter: () => setActiveIndex(index),
641
640
  className: `flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors ${isActive ? "bg-gray-50" : "hover:bg-gray-50"}`,
642
641
  children: [
@@ -645,7 +644,7 @@ function ChatInputBarSkillPicker(props) {
645
644
  children: /* @__PURE__ */ jsx(Puzzle, { className: "h-4 w-4" })
646
645
  }),
647
646
  /* @__PURE__ */ jsxs("div", {
648
- className: "min-w-0 flex-1",
647
+ className: "min-w-0 flex-1 select-text",
649
648
  children: [/* @__PURE__ */ jsxs("div", {
650
649
  className: "flex items-center gap-1.5",
651
650
  children: [/* @__PURE__ */ jsx("span", {
@@ -662,7 +661,10 @@ function ChatInputBarSkillPicker(props) {
662
661
  }),
663
662
  /* @__PURE__ */ jsx("div", {
664
663
  className: "ml-3 shrink-0",
665
- children: /* @__PURE__ */ jsx("span", {
664
+ children: /* @__PURE__ */ jsx("button", {
665
+ type: "button",
666
+ "aria-label": `${isSelected ? "Remove" : "Add"} ${option.label}`,
667
+ onClick: () => onToggleOption(option.key),
666
668
  className: isSelected ? "inline-flex h-5 w-5 items-center justify-center rounded-full bg-primary text-white" : "inline-flex h-5 w-5 items-center justify-center rounded-full border border-gray-300 bg-white",
667
669
  children: isSelected ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : null
668
670
  })
@@ -1510,6 +1512,9 @@ var LexicalComposerHandleOwner = class {
1510
1512
  this.focusComposer = () => {
1511
1513
  this.params.focusComposer();
1512
1514
  };
1515
+ this.focusComposerAtEnd = () => {
1516
+ this.params.focusComposerAtEnd();
1517
+ };
1513
1518
  this.syncSelectedSkills = (nextKeys, options) => {
1514
1519
  this.params.publishSnapshot(syncSelectedSkillsIntoChatComposer({
1515
1520
  nextKeys,
@@ -1735,10 +1740,23 @@ const ChatInputBarTokenizedComposer = forwardRef(function ChatInputBarTokenizedC
1735
1740
  const focusComposer = () => {
1736
1741
  const editor = editorRef.current;
1737
1742
  if (!editor) return;
1743
+ editor.getRootElement()?.focus({ preventScroll: true });
1738
1744
  const targetSelection = selectionRef.current;
1739
- editor.focus(() => {
1740
- if (targetSelection) syncLexicalSelectionFromChatComposerSelection(editor, targetSelection);
1741
- });
1745
+ if (targetSelection) syncLexicalSelectionFromChatComposerSelection(editor, targetSelection);
1746
+ editor.focus();
1747
+ };
1748
+ const focusComposerAtEnd = () => {
1749
+ const editor = editorRef.current;
1750
+ if (!editor) return;
1751
+ const end = readChatComposerSnapshotFromEditorState(editor.getEditorState()).nodes.reduce((cursor, node) => cursor + (node.type === "text" ? node.text.length : 1), 0);
1752
+ const targetSelection = {
1753
+ start: end,
1754
+ end
1755
+ };
1756
+ selectionRef.current = targetSelection;
1757
+ editor.getRootElement()?.focus({ preventScroll: true });
1758
+ syncLexicalSelectionFromChatComposerSelection(editor, targetSelection);
1759
+ editor.focus();
1742
1760
  };
1743
1761
  const readComposerSnapshot = () => ({
1744
1762
  nodes: readCurrentNodes(),
@@ -1746,6 +1764,7 @@ const ChatInputBarTokenizedComposer = forwardRef(function ChatInputBarTokenizedC
1746
1764
  });
1747
1765
  useImperativeHandle(ref, () => createLexicalComposerHandle({
1748
1766
  focusComposer,
1767
+ focusComposerAtEnd,
1749
1768
  onSlashItemSelect: props.onSlashItemSelect,
1750
1769
  optionsReader: readComposerSnapshot,
1751
1770
  publishSnapshot
@@ -1879,21 +1898,19 @@ function InputBarHint({ hint }) {
1879
1898
  }
1880
1899
  const ChatInputBar = forwardRef(function ChatInputBar(props, ref) {
1881
1900
  const composerRef = useRef(null);
1882
- const [slashQuery, setSlashQuery] = useState(null);
1901
+ const isSlashMenuInteractionRef = useRef(false);
1883
1902
  const [activeSlashIndex, setActiveSlashIndex] = useState(0);
1884
1903
  const [activeSlashTriggerStart, setActiveSlashTriggerStart] = useState(null);
1885
1904
  const [dismissedSlashTriggerStart, setDismissedSlashTriggerStart] = useState(null);
1886
1905
  const isSlashPanelOpen = activeSlashTriggerStart !== null && dismissedSlashTriggerStart !== activeSlashTriggerStart;
1887
1906
  const activeSlashItem = props.slashMenu.items[activeSlashIndex] ?? null;
1907
+ const dismissSlashTrigger = () => activeSlashTriggerStart !== null && !isSlashMenuInteractionRef.current ? setDismissedSlashTriggerStart(activeSlashTriggerStart) : void 0;
1888
1908
  useEffect(() => {
1889
1909
  setActiveSlashIndex((current) => {
1890
1910
  if (props.slashMenu.items.length === 0) return 0;
1891
1911
  return Math.min(current, props.slashMenu.items.length - 1);
1892
1912
  });
1893
1913
  }, [props.slashMenu.items.length]);
1894
- useEffect(() => {
1895
- if (slashQuery !== null) setActiveSlashIndex(0);
1896
- }, [slashQuery]);
1897
1914
  useEffect(() => {
1898
1915
  if (activeSlashTriggerStart === null && dismissedSlashTriggerStart !== null) setDismissedSlashTriggerStart(null);
1899
1916
  }, [activeSlashTriggerStart, dismissedSlashTriggerStart]);
@@ -1913,7 +1930,8 @@ const ChatInputBar = forwardRef(function ChatInputBar(props, ref) {
1913
1930
  useImperativeHandle(ref, () => ({
1914
1931
  insertFileToken: (tokenKey, label) => composerRef.current?.insertFileToken(tokenKey, label),
1915
1932
  insertFileTokens: (tokens) => composerRef.current?.insertFileTokens(tokens),
1916
- focusComposer: () => composerRef.current?.focusComposer()
1933
+ focusComposer: () => composerRef.current?.focusComposer(),
1934
+ focusComposerAtEnd: () => composerRef.current?.focusComposerAtEnd()
1917
1935
  }), []);
1918
1936
  return /* @__PURE__ */ jsx("div", {
1919
1937
  className: "border-t border-gray-200/80 bg-white p-4",
@@ -1936,14 +1954,15 @@ const ChatInputBar = forwardRef(function ChatInputBar(props, ref) {
1936
1954
  onNodesChange: props.composer.onNodesChange,
1937
1955
  onFilesAdd: props.composer.onFilesAdd,
1938
1956
  onSlashQueryChange: (query) => {
1939
- setSlashQuery(query);
1957
+ if (query === null && isSlashMenuInteractionRef.current) return;
1958
+ if (query !== null) setActiveSlashIndex(0);
1940
1959
  props.composer.onSlashQueryChange?.(query);
1941
1960
  },
1942
1961
  onSlashTriggerChange: (trigger) => {
1943
1962
  setActiveSlashTriggerStart(trigger?.start ?? null);
1944
1963
  },
1945
1964
  onSlashOpenChange: (open) => {
1946
- if (!open && activeSlashTriggerStart !== null) setDismissedSlashTriggerStart(activeSlashTriggerStart);
1965
+ if (!open) dismissSlashTrigger();
1947
1966
  },
1948
1967
  onSlashActiveIndexChange: setActiveSlashIndex
1949
1968
  }), /* @__PURE__ */ jsx(ChatSlashMenu, {
@@ -1958,7 +1977,13 @@ const ChatInputBar = forwardRef(function ChatInputBar(props, ref) {
1958
1977
  composerRef.current?.insertSlashItem(item);
1959
1978
  },
1960
1979
  onOpenChange: (open) => {
1961
- if (!open && activeSlashTriggerStart !== null) setDismissedSlashTriggerStart(activeSlashTriggerStart);
1980
+ if (!open) dismissSlashTrigger();
1981
+ },
1982
+ onDetailsPointerDown: () => {
1983
+ isSlashMenuInteractionRef.current = true;
1984
+ requestAnimationFrame(() => {
1985
+ isSlashMenuInteractionRef.current = false;
1986
+ });
1962
1987
  },
1963
1988
  onSetActiveIndex: setActiveSlashIndex
1964
1989
  })]
@@ -3785,6 +3810,45 @@ function ChatMessageActionCopy({ message, texts }) {
3785
3810
  //#endregion
3786
3811
  //#region src/components/chat/ui/chat-message-list/chat-message-list.tsx
3787
3812
  const INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
3813
+ const TYPING_TEXT_SHEEN_CSS = `
3814
+ @keyframes nextclaw-chat-typing-text-sheen {
3815
+ 0% {
3816
+ background-position: 160% 0;
3817
+ }
3818
+ 100% {
3819
+ background-position: -60% 0;
3820
+ }
3821
+ }
3822
+
3823
+ .nextclaw-chat-typing-indicator__text {
3824
+ background-image: linear-gradient(
3825
+ 100deg,
3826
+ #6b7280 0%,
3827
+ #6b7280 34%,
3828
+ #a6adba 43%,
3829
+ #f8fafc 50%,
3830
+ #a6adba 57%,
3831
+ #6b7280 66%,
3832
+ #6b7280 100%
3833
+ );
3834
+ background-size: 240% 100%;
3835
+ background-position: 160% 0;
3836
+ -webkit-background-clip: text;
3837
+ background-clip: text;
3838
+ color: transparent;
3839
+ -webkit-text-fill-color: transparent;
3840
+ animation: nextclaw-chat-typing-text-sheen 2.5s linear infinite;
3841
+ }
3842
+
3843
+ @media (prefers-reduced-motion: reduce) {
3844
+ .nextclaw-chat-typing-indicator__text {
3845
+ animation: none;
3846
+ background-image: none;
3847
+ color: #6b7280;
3848
+ -webkit-text-fill-color: currentColor;
3849
+ }
3850
+ }
3851
+ `;
3788
3852
  function hasRenderableText(value) {
3789
3853
  const trimmed = value.trim();
3790
3854
  if (!trimmed) return false;
@@ -3809,6 +3873,15 @@ function ChatMessageTypingFooter() {
3809
3873
  })
3810
3874
  });
3811
3875
  }
3876
+ function ChatTypingIndicator({ label }) {
3877
+ return /* @__PURE__ */ jsxs("div", {
3878
+ className: "rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm",
3879
+ children: [/* @__PURE__ */ jsx("style", { children: TYPING_TEXT_SHEEN_CSS }), /* @__PURE__ */ jsx("span", {
3880
+ className: "nextclaw-chat-typing-indicator__text",
3881
+ children: label
3882
+ })]
3883
+ });
3884
+ }
3812
3885
  function ChatMessageList(props) {
3813
3886
  const visibleMessages = props.messages.filter(hasRenderableMessageContent);
3814
3887
  const hasRenderableAssistantDraft = visibleMessages.some((message) => message.role === "assistant" && (message.status === "streaming" || message.status === "pending"));
@@ -3846,10 +3919,7 @@ function ChatMessageList(props) {
3846
3919
  }, message.id);
3847
3920
  }), props.isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs("div", {
3848
3921
  className: "flex justify-start gap-3",
3849
- children: [/* @__PURE__ */ jsx(ChatMessageAvatar, { role: "assistant" }), /* @__PURE__ */ jsx("div", {
3850
- className: "rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm",
3851
- children: props.texts.typingLabel
3852
- })]
3922
+ children: [/* @__PURE__ */ jsx(ChatMessageAvatar, { role: "assistant" }), /* @__PURE__ */ jsx(ChatTypingIndicator, { label: props.texts.typingLabel })]
3853
3923
  }) : null]
3854
3924
  });
3855
3925
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/agent-chat-ui",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "private": false,
5
5
  "description": "Reusable Nextclaw agent chat UI primitives and default skin.",
6
6
  "type": "module",