@nextclaw/agent-chat-ui 0.2.14 → 0.2.15

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
@@ -48,6 +48,11 @@ type ChatToolbarSelectOption = {
48
48
  label: string;
49
49
  description?: string;
50
50
  };
51
+ type ChatToolbarSelectGroup = {
52
+ key: string;
53
+ label?: string;
54
+ options: ChatToolbarSelectOption[];
55
+ };
51
56
  type ChatToolbarSelect = {
52
57
  key: string;
53
58
  value?: string;
@@ -55,6 +60,7 @@ type ChatToolbarSelect = {
55
60
  selectedLabel?: string;
56
61
  icon?: ChatToolbarIcon;
57
62
  options: ChatToolbarSelectOption[];
63
+ groups?: ChatToolbarSelectGroup[];
58
64
  disabled?: boolean;
59
65
  loading?: boolean;
60
66
  emptyLabel?: string;
@@ -185,6 +191,8 @@ type ChatMessageViewModel = {
185
191
  type ChatMessageTexts = {
186
192
  copyCodeLabel: string;
187
193
  copiedCodeLabel: string;
194
+ copyMessageLabel: string;
195
+ copiedMessageLabel: string;
188
196
  typingLabel: string;
189
197
  };
190
198
  type ChatMessageListProps = {
@@ -267,4 +275,4 @@ declare function resolveChatComposerSlashTrigger(nodes: ChatComposerNode[], sele
267
275
  end: number;
268
276
  } | null;
269
277
 
270
- 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 };
278
+ 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 ChatToolbarSelectGroup, type ChatToolbarSelectOption, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
package/dist/index.js CHANGED
@@ -100,6 +100,7 @@ import * as SelectPrimitive from "@radix-ui/react-select";
100
100
  import { Check, ChevronDown, ChevronUp } from "lucide-react";
101
101
  import { jsx as jsx3, jsxs } from "react/jsx-runtime";
102
102
  var ChatSelect = SelectPrimitive.Root;
103
+ var ChatSelectGroup = SelectPrimitive.Group;
103
104
  var ChatSelectValue = SelectPrimitive.Value;
104
105
  var ChatSelectTrigger = React3.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
105
106
  SelectPrimitive.Trigger,
@@ -146,6 +147,8 @@ var ChatSelectContent = React3.forwardRef(({ className, children, position = "po
146
147
  }
147
148
  ) }));
148
149
  ChatSelectContent.displayName = SelectPrimitive.Content.displayName;
150
+ var ChatSelectLabel = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(SelectPrimitive.Label, { ref, className: cn("px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-gray-500", className), ...props }));
151
+ ChatSelectLabel.displayName = SelectPrimitive.Label.displayName;
149
152
  var ChatSelectItem = React3.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
150
153
  SelectPrimitive.Item,
151
154
  {
@@ -162,6 +165,8 @@ var ChatSelectItem = React3.forwardRef(({ className, children, ...props }, ref)
162
165
  }
163
166
  ));
164
167
  ChatSelectItem.displayName = SelectPrimitive.Item.displayName;
168
+ var ChatSelectSeparator = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(SelectPrimitive.Separator, { ref, className: cn("-mx-1 my-1 h-px bg-gray-100", className), ...props }));
169
+ ChatSelectSeparator.displayName = SelectPrimitive.Separator.displayName;
165
170
 
166
171
  // src/components/chat/default-skin/tooltip.tsx
167
172
  import * as React4 from "react";
@@ -193,7 +198,10 @@ var ChatUiPrimitives = {
193
198
  Input: ChatInput,
194
199
  Select: ChatSelect,
195
200
  SelectContent: ChatSelectContent,
201
+ SelectGroup: ChatSelectGroup,
196
202
  SelectItem: ChatSelectItem,
203
+ SelectLabel: ChatSelectLabel,
204
+ SelectSeparator: ChatSelectSeparator,
197
205
  SelectTrigger: ChatSelectTrigger,
198
206
  SelectValue: ChatSelectValue,
199
207
  Tooltip: ChatTooltip,
@@ -602,7 +610,9 @@ function resolveContentWidth(key) {
602
610
  return "";
603
611
  }
604
612
  function ToolbarSelect({ item }) {
605
- const { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } = ChatUiPrimitives;
613
+ const { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } = ChatUiPrimitives;
614
+ const groups = item.groups?.filter((group) => group.options.length > 0) ?? (item.options.length > 0 ? [{ key: `${item.key}-default`, options: item.options }] : []);
615
+ const hasOptions = groups.some((group) => group.options.length > 0);
606
616
  return /* @__PURE__ */ jsxs5(Select, { value: item.value, onValueChange: item.onValueChange, disabled: item.disabled, children: [
607
617
  /* @__PURE__ */ jsx9(
608
618
  SelectTrigger,
@@ -615,15 +625,21 @@ function ToolbarSelect({ item }) {
615
625
  }
616
626
  ),
617
627
  /* @__PURE__ */ jsxs5(SelectContent, { className: resolveContentWidth(item.key), children: [
618
- item.options.length === 0 ? item.loading ? /* @__PURE__ */ jsxs5("div", { className: "space-y-2 px-3 py-2", children: [
628
+ !hasOptions ? item.loading ? /* @__PURE__ */ jsxs5("div", { className: "space-y-2 px-3 py-2", children: [
619
629
  /* @__PURE__ */ jsx9("div", { className: "h-3 w-36 animate-pulse rounded bg-gray-200" }),
620
630
  /* @__PURE__ */ jsx9("div", { className: "h-3 w-28 animate-pulse rounded bg-gray-200" }),
621
631
  /* @__PURE__ */ jsx9("div", { className: "h-3 w-32 animate-pulse rounded bg-gray-200" })
622
632
  ] }) : item.emptyLabel ? /* @__PURE__ */ jsx9("div", { className: "px-3 py-2 text-xs text-gray-500", children: item.emptyLabel }) : null : null,
623
- item.options.map((option) => /* @__PURE__ */ jsx9(SelectItem, { value: option.value, className: "py-2", children: option.description ? /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-col gap-0.5", children: [
624
- /* @__PURE__ */ jsx9("span", { className: "truncate text-xs font-semibold text-gray-800", children: option.label }),
625
- /* @__PURE__ */ jsx9("span", { className: "truncate text-[11px] text-gray-500", children: option.description })
626
- ] }) : /* @__PURE__ */ jsx9("span", { className: "truncate text-xs font-semibold text-gray-800", children: option.label }) }, option.value))
633
+ groups.map((group, groupIndex) => /* @__PURE__ */ jsxs5("div", { children: [
634
+ groupIndex > 0 ? /* @__PURE__ */ jsx9(SelectSeparator, {}) : null,
635
+ /* @__PURE__ */ jsxs5(SelectGroup, { children: [
636
+ group.label ? /* @__PURE__ */ jsx9(SelectLabel, { children: group.label }) : null,
637
+ group.options.map((option) => /* @__PURE__ */ jsx9(SelectItem, { value: option.value, className: "py-2", children: option.description ? /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-col gap-0.5", children: [
638
+ /* @__PURE__ */ jsx9("span", { className: "truncate text-xs font-semibold text-gray-800", children: option.label }),
639
+ /* @__PURE__ */ jsx9("span", { className: "truncate text-[11px] text-gray-500", children: option.description })
640
+ ] }) : /* @__PURE__ */ jsx9("span", { className: "truncate text-xs font-semibold text-gray-800", children: option.label }) }, option.value))
641
+ ] })
642
+ ] }, group.key))
627
643
  ] })
628
644
  ] });
629
645
  }
@@ -1939,8 +1955,8 @@ function ChatMessageAvatar({ role }) {
1939
1955
  "div",
1940
1956
  {
1941
1957
  "data-testid": "chat-message-avatar-assistant",
1942
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-gray-200/80 bg-gray-900 text-white shadow-sm",
1943
- children: /* @__PURE__ */ jsx12(Bot, { className: "h-4 w-4" })
1958
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-b from-gray-700 to-gray-950 text-white shadow-md ring-1 ring-inset ring-white/20",
1959
+ children: /* @__PURE__ */ jsx12(Bot, { className: "h-[18px] w-[18px] text-white/95", strokeWidth: 2.5 })
1944
1960
  }
1945
1961
  );
1946
1962
  }
@@ -2508,19 +2524,64 @@ function ChatMessageFile({
2508
2524
  );
2509
2525
  }
2510
2526
 
2527
+ // src/components/chat/hooks/use-reasoning-block-open-state.ts
2528
+ import { useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "react";
2529
+ function useReasoningBlockOpenState(params) {
2530
+ const { isInProgress } = params;
2531
+ const [isOpen, setIsOpen] = useState6(isInProgress);
2532
+ const [keepOpenAfterCompletion, setKeepOpenAfterCompletion] = useState6(false);
2533
+ const previousInProgressRef = useRef5(isInProgress);
2534
+ useEffect6(() => {
2535
+ const wasInProgress = previousInProgressRef.current;
2536
+ if (!wasInProgress && isInProgress) {
2537
+ setIsOpen(true);
2538
+ setKeepOpenAfterCompletion(false);
2539
+ } else if (wasInProgress && !isInProgress && !keepOpenAfterCompletion) {
2540
+ setIsOpen(false);
2541
+ }
2542
+ previousInProgressRef.current = isInProgress;
2543
+ }, [isInProgress, keepOpenAfterCompletion]);
2544
+ function onSummaryClick(event) {
2545
+ event.preventDefault();
2546
+ const nextOpen = !isOpen;
2547
+ setIsOpen(nextOpen);
2548
+ if (!nextOpen) {
2549
+ setKeepOpenAfterCompletion(false);
2550
+ return;
2551
+ }
2552
+ if (isInProgress) {
2553
+ setKeepOpenAfterCompletion(true);
2554
+ }
2555
+ }
2556
+ return {
2557
+ isOpen,
2558
+ onSummaryClick
2559
+ };
2560
+ }
2561
+
2511
2562
  // src/components/chat/ui/chat-message-list/chat-reasoning-block.tsx
2512
2563
  import { jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
2513
- function ChatReasoningBlock(props) {
2514
- return /* @__PURE__ */ jsxs9("details", { className: "mt-3", open: true, children: [
2515
- /* @__PURE__ */ jsx16("summary", { className: cn("cursor-pointer text-xs", props.isUser ? "text-primary-100" : "text-gray-500"), children: props.label }),
2564
+ function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
2565
+ const { isOpen, onSummaryClick } = useReasoningBlockOpenState({
2566
+ isInProgress
2567
+ });
2568
+ return /* @__PURE__ */ jsxs9("details", { className: "mt-2", open: isOpen, children: [
2569
+ /* @__PURE__ */ jsx16(
2570
+ "summary",
2571
+ {
2572
+ className: cn("cursor-pointer text-xs", isUser ? "text-primary-100" : "text-gray-500"),
2573
+ onClick: onSummaryClick,
2574
+ children: label
2575
+ }
2576
+ ),
2516
2577
  /* @__PURE__ */ jsx16(
2517
2578
  "pre",
2518
2579
  {
2519
2580
  className: cn(
2520
- "mt-2 whitespace-pre-wrap break-words rounded-lg p-2 text-[11px]",
2521
- props.isUser ? "bg-primary-700/60" : "bg-gray-100"
2581
+ "mt-2 w-fit max-w-[500px] whitespace-pre-wrap break-all rounded-lg p-2 text-[11px]",
2582
+ isUser ? "bg-primary-700/60" : "bg-gray-100"
2522
2583
  ),
2523
- children: props.text
2584
+ children: text
2524
2585
  }
2525
2586
  )
2526
2587
  ] });
@@ -2528,7 +2589,7 @@ function ChatReasoningBlock(props) {
2528
2589
 
2529
2590
  // src/components/chat/ui/chat-message-list/tool-card/tool-card-views.tsx
2530
2591
  import { Terminal, FileText, Code2, Search as Search2, Globe } from "lucide-react";
2531
- import { useState as useState6, useEffect as useEffect6, useRef as useRef5 } from "react";
2592
+ import { useState as useState7, useEffect as useEffect7, useRef as useRef6 } from "react";
2532
2593
 
2533
2594
  // src/components/chat/ui/chat-message-list/tool-card/tool-card-root.tsx
2534
2595
  import { jsx as jsx17 } from "react/jsx-runtime";
@@ -2614,17 +2675,17 @@ function TerminalExecutionView({ card }) {
2614
2675
  const output = card.output?.trim() ?? "";
2615
2676
  const isRunning = card.statusTone === "running";
2616
2677
  const hasContent = !!(card.summary?.trim() || output.length > 0);
2617
- const wasEmptyRef = useRef5(!hasContent);
2618
- const [expanded, setExpanded] = useState6(hasContent && (isRunning || card.statusTone === "error" || output.length < 500));
2619
- const [hasUserToggled, setHasUserToggled] = useState6(false);
2620
- const prevRunningRef = useRef5(isRunning);
2621
- useEffect6(() => {
2678
+ const wasEmptyRef = useRef6(!hasContent);
2679
+ const [expanded, setExpanded] = useState7(hasContent && (isRunning || card.statusTone === "error" || output.length < 500));
2680
+ const [hasUserToggled, setHasUserToggled] = useState7(false);
2681
+ const prevRunningRef = useRef6(isRunning);
2682
+ useEffect7(() => {
2622
2683
  if (wasEmptyRef.current && hasContent && isRunning) {
2623
2684
  setExpanded(true);
2624
2685
  wasEmptyRef.current = false;
2625
2686
  }
2626
2687
  }, [hasContent, isRunning]);
2627
- useEffect6(() => {
2688
+ useEffect7(() => {
2628
2689
  if (prevRunningRef.current && !isRunning && !hasUserToggled) {
2629
2690
  setExpanded(false);
2630
2691
  }
@@ -2665,10 +2726,10 @@ function TerminalExecutionView({ card }) {
2665
2726
  function FileOperationView({ card }) {
2666
2727
  const output = card.output?.trim() ?? "";
2667
2728
  const isRunning = card.statusTone === "running";
2668
- const [expanded, setExpanded] = useState6(false);
2669
- const [hasUserToggled, setHasUserToggled] = useState6(false);
2670
- const prevRunningRef = useRef5(isRunning);
2671
- useEffect6(() => {
2729
+ const [expanded, setExpanded] = useState7(false);
2730
+ const [hasUserToggled, setHasUserToggled] = useState7(false);
2731
+ const prevRunningRef = useRef6(isRunning);
2732
+ useEffect7(() => {
2672
2733
  if (prevRunningRef.current && !isRunning && !hasUserToggled) {
2673
2734
  setExpanded(false);
2674
2735
  }
@@ -2717,11 +2778,11 @@ function FileOperationView({ card }) {
2717
2778
  }
2718
2779
  function SearchSnippetView({ card }) {
2719
2780
  const isRunning = card.statusTone === "running";
2720
- const [expanded, setExpanded] = useState6(isRunning);
2721
- const [hasUserToggled, setHasUserToggled] = useState6(false);
2722
- const prevRunningRef = useRef5(isRunning);
2781
+ const [expanded, setExpanded] = useState7(isRunning);
2782
+ const [hasUserToggled, setHasUserToggled] = useState7(false);
2783
+ const prevRunningRef = useRef6(isRunning);
2723
2784
  const output = card.output?.trim() ?? "";
2724
- useEffect6(() => {
2785
+ useEffect7(() => {
2725
2786
  if (prevRunningRef.current && !isRunning && !hasUserToggled) {
2726
2787
  setExpanded(false);
2727
2788
  }
@@ -2749,11 +2810,11 @@ function SearchSnippetView({ card }) {
2749
2810
  function GenericToolCard({ card }) {
2750
2811
  const output = card.output?.trim() ?? "";
2751
2812
  const isRunning = card.statusTone === "running";
2752
- const [expanded, setExpanded] = useState6(isRunning);
2753
- const [hasUserToggled, setHasUserToggled] = useState6(false);
2754
- const prevRunningRef = useRef5(isRunning);
2813
+ const [expanded, setExpanded] = useState7(isRunning);
2814
+ const [hasUserToggled, setHasUserToggled] = useState7(false);
2815
+ const prevRunningRef = useRef6(isRunning);
2755
2816
  const showOutputSection = card.kind === "result" || card.hasResult;
2756
- useEffect6(() => {
2817
+ useEffect7(() => {
2757
2818
  if (prevRunningRef.current && !isRunning && !hasUserToggled) {
2758
2819
  setExpanded(false);
2759
2820
  }
@@ -2825,12 +2886,13 @@ var ChatMessage = memo(function ChatMessage2(props) {
2825
2886
  const { message, texts } = props;
2826
2887
  const { role } = message;
2827
2888
  const isUser = role === "user";
2889
+ const isMessageInProgress = message.status === "pending" || message.status === "streaming";
2828
2890
  return /* @__PURE__ */ jsx23(
2829
2891
  "div",
2830
2892
  {
2831
2893
  className: cn(
2832
- "inline-block w-fit max-w-full rounded-2xl border px-4 py-3 shadow-sm",
2833
- 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"
2894
+ "inline-block w-fit max-w-full rounded-2xl border px-4 shadow-sm",
2895
+ isUser ? "border-primary bg-primary py-3 text-white" : role === "assistant" ? "border-gray-200 bg-white pb-3 pt-4 text-gray-900" : "border-orange-200/80 bg-orange-50/70 py-3 text-gray-900"
2834
2896
  ),
2835
2897
  children: /* @__PURE__ */ jsx23("div", { className: "space-y-2", children: message.parts.map((part, index) => {
2836
2898
  if (part.type === "markdown") {
@@ -2850,7 +2912,8 @@ var ChatMessage = memo(function ChatMessage2(props) {
2850
2912
  {
2851
2913
  label: part.label,
2852
2914
  text: part.text,
2853
- isUser
2915
+ isUser,
2916
+ isInProgress: isMessageInProgress && index === message.parts.length - 1
2854
2917
  },
2855
2918
  `reasoning-${index}`
2856
2919
  );
@@ -2904,8 +2967,38 @@ function ChatMessageMeta(props) {
2904
2967
  );
2905
2968
  }
2906
2969
 
2970
+ // src/components/chat/ui/chat-message-list/chat-message-action-copy.tsx
2971
+ import { useMemo as useMemo6 } from "react";
2972
+ import { Check as Check5, Copy as Copy2 } from "lucide-react";
2973
+ import { jsx as jsx24 } from "react/jsx-runtime";
2974
+ function ChatMessageActionCopy({
2975
+ message,
2976
+ texts
2977
+ }) {
2978
+ const messageText = useMemo6(() => {
2979
+ return message.parts.map((part) => {
2980
+ if (part.type === "markdown") return part.text;
2981
+ if (part.type === "unknown") return part.text;
2982
+ return "";
2983
+ }).filter((text) => !!text && text.trim().length > 0).join("\n\n");
2984
+ }, [message.parts]);
2985
+ const { copied, copy } = useCopyFeedback({ text: messageText });
2986
+ if (!messageText) return null;
2987
+ return /* @__PURE__ */ jsx24(
2988
+ "button",
2989
+ {
2990
+ type: "button",
2991
+ onClick: () => void copy(),
2992
+ className: "text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-md hover:bg-gray-100 flex items-center justify-center",
2993
+ "aria-label": copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
2994
+ title: copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
2995
+ children: copied ? /* @__PURE__ */ jsx24(Check5, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx24(Copy2, { className: "h-3.5 w-3.5" })
2996
+ }
2997
+ );
2998
+ }
2999
+
2907
3000
  // src/components/chat/ui/chat-message-list/chat-message-list.tsx
2908
- import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
3001
+ import { Fragment as Fragment4, jsx as jsx25, jsxs as jsxs15 } from "react/jsx-runtime";
2909
3002
  var INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
2910
3003
  function hasRenderableText(value) {
2911
3004
  const trimmed = value.trim();
@@ -2922,6 +3015,13 @@ function hasRenderableMessageContent(message) {
2922
3015
  return true;
2923
3016
  });
2924
3017
  }
3018
+ function ChatMessageTypingFooter() {
3019
+ return /* @__PURE__ */ jsx25("div", { className: "flex items-center gap-2 px-1 py-0.5 text-[11px] text-gray-400", children: /* @__PURE__ */ jsxs15("div", { className: "flex space-x-1 items-center h-full", children: [
3020
+ /* @__PURE__ */ jsx25("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse" }),
3021
+ /* @__PURE__ */ jsx25("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse [animation-delay:200ms]" }),
3022
+ /* @__PURE__ */ jsx25("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse [animation-delay:400ms]" })
3023
+ ] }) });
3024
+ }
2925
3025
  function ChatMessageList(props) {
2926
3026
  const visibleMessages = props.messages.filter(hasRenderableMessageContent);
2927
3027
  const hasRenderableAssistantDraft = visibleMessages.some(
@@ -2930,24 +3030,28 @@ function ChatMessageList(props) {
2930
3030
  return /* @__PURE__ */ jsxs15("div", { className: cn("space-y-5", props.className), children: [
2931
3031
  visibleMessages.map((message) => {
2932
3032
  const isUser = message.role === "user";
3033
+ const isGenerating = !isUser && (message.status === "streaming" || message.status === "pending");
2933
3034
  return /* @__PURE__ */ jsxs15("div", { className: cn("flex gap-3", isUser ? "justify-end" : "justify-start"), children: [
2934
- !isUser ? /* @__PURE__ */ jsx24(ChatMessageAvatar, { role: message.role }) : null,
3035
+ !isUser ? /* @__PURE__ */ jsx25(ChatMessageAvatar, { role: message.role }) : null,
2935
3036
  /* @__PURE__ */ jsxs15("div", { className: cn("w-fit max-w-[92%] space-y-2", isUser && "flex flex-col items-end"), children: [
2936
- /* @__PURE__ */ jsx24(ChatMessage, { message, texts: props.texts }),
2937
- /* @__PURE__ */ jsx24(ChatMessageMeta, { roleLabel: message.roleLabel, timestampLabel: message.timestampLabel, isUser })
3037
+ /* @__PURE__ */ jsx25(ChatMessage, { message, texts: props.texts }),
3038
+ /* @__PURE__ */ jsx25("div", { className: cn("flex items-center gap-2", isUser && "justify-end"), children: isGenerating ? /* @__PURE__ */ jsx25(ChatMessageTypingFooter, {}) : /* @__PURE__ */ jsxs15(Fragment4, { children: [
3039
+ /* @__PURE__ */ jsx25(ChatMessageMeta, { roleLabel: message.roleLabel, timestampLabel: message.timestampLabel, isUser }),
3040
+ !isUser ? /* @__PURE__ */ jsx25(ChatMessageActionCopy, { message, texts: props.texts }) : null
3041
+ ] }) })
2938
3042
  ] }),
2939
- isUser ? /* @__PURE__ */ jsx24(ChatMessageAvatar, { role: message.role }) : null
3043
+ isUser ? /* @__PURE__ */ jsx25(ChatMessageAvatar, { role: message.role }) : null
2940
3044
  ] }, message.id);
2941
3045
  }),
2942
3046
  props.isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs15("div", { className: "flex justify-start gap-3", children: [
2943
- /* @__PURE__ */ jsx24(ChatMessageAvatar, { role: "assistant" }),
2944
- /* @__PURE__ */ jsx24("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 })
3047
+ /* @__PURE__ */ jsx25(ChatMessageAvatar, { role: "assistant" }),
3048
+ /* @__PURE__ */ jsx25("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 })
2945
3049
  ] }) : null
2946
3050
  ] });
2947
3051
  }
2948
3052
 
2949
3053
  // src/components/chat/hooks/use-sticky-bottom-scroll.ts
2950
- import { useEffect as useEffect7, useLayoutEffect as useLayoutEffect2, useRef as useRef6 } from "react";
3054
+ import { useEffect as useEffect8, useLayoutEffect as useLayoutEffect2, useRef as useRef7 } from "react";
2951
3055
  var DEFAULT_STICKY_THRESHOLD_PX = 10;
2952
3056
  function scrollElementToBottom(element) {
2953
3057
  element.scrollTop = element.scrollHeight;
@@ -2971,11 +3075,11 @@ function queueScrollToBottom(params) {
2971
3075
  });
2972
3076
  }
2973
3077
  function useStickyBottomScroll(params) {
2974
- const isStickyRef = useRef6(true);
2975
- const isProgrammaticScrollRef = useRef6(false);
2976
- const previousResetKeyRef = useRef6(null);
2977
- const pendingInitialScrollRef = useRef6(false);
2978
- const scheduledScrollFrameRef = useRef6(null);
3078
+ const isStickyRef = useRef7(true);
3079
+ const isProgrammaticScrollRef = useRef7(false);
3080
+ const previousResetKeyRef = useRef7(null);
3081
+ const pendingInitialScrollRef = useRef7(false);
3082
+ const scheduledScrollFrameRef = useRef7(null);
2979
3083
  const onScroll = () => {
2980
3084
  if (isProgrammaticScrollRef.current) {
2981
3085
  isProgrammaticScrollRef.current = false;
@@ -2988,7 +3092,7 @@ function useStickyBottomScroll(params) {
2988
3092
  const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
2989
3093
  isStickyRef.current = distanceFromBottom <= (params.stickyThresholdPx ?? DEFAULT_STICKY_THRESHOLD_PX);
2990
3094
  };
2991
- useEffect7(() => {
3095
+ useEffect8(() => {
2992
3096
  if (previousResetKeyRef.current === params.resetKey) {
2993
3097
  return;
2994
3098
  }
@@ -2996,7 +3100,7 @@ function useStickyBottomScroll(params) {
2996
3100
  isStickyRef.current = true;
2997
3101
  pendingInitialScrollRef.current = true;
2998
3102
  }, [params.resetKey]);
2999
- useEffect7(() => {
3103
+ useEffect8(() => {
3000
3104
  const scheduledScrollFrame = scheduledScrollFrameRef.current;
3001
3105
  return () => {
3002
3106
  if (scheduledScrollFrame !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/agent-chat-ui",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "private": false,
5
5
  "description": "Reusable Nextclaw agent chat UI primitives and default skin.",
6
6
  "type": "module",
@@ -45,15 +45,16 @@
45
45
  },
46
46
  "devDependencies": {
47
47
  "@testing-library/react": "^16.3.0",
48
+ "@types/node": "^20.19.33",
48
49
  "@types/react": "^18.3.12",
49
50
  "@types/react-dom": "^18.3.1",
50
- "@vitejs/plugin-react": "^4.3.4",
51
+ "@vitejs/plugin-react": "^6.0.1",
51
52
  "jsdom": "^25.0.1",
52
53
  "prettier": "^3.3.3",
53
54
  "tsup": "^8.3.5",
54
55
  "typescript": "^5.6.3",
55
- "vite": "^6.0.1",
56
- "vitest": "^2.1.2"
56
+ "vite": "^8.0.3",
57
+ "vitest": "^4.1.2"
57
58
  },
58
59
  "scripts": {
59
60
  "build": "tsup src/index.ts --format esm --dts --out-dir dist",