@nextclaw/agent-chat-ui 0.6.3 → 0.6.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.js CHANGED
@@ -5,7 +5,7 @@ import { twMerge } from "tailwind-merge";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import * as PopoverPrimitive from "@radix-ui/react-popover";
7
7
  import * as SelectPrimitive from "@radix-ui/react-select";
8
- import { AlertTriangle, AppWindow, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, CalendarClock, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, Expand, ExternalLink, Eye, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, FolderTree, Globe, Globe2, ImageIcon, Loader2, MessageSquare, Minus, Paperclip, Puzzle, Search, Send, Settings, Sparkles, Star, Terminal, User, Wrench, X } from "lucide-react";
8
+ import { AlertTriangle, AppWindow, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, CalendarClock, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, Expand, ExternalLink, Eye, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, FolderTree, Globe, Globe2, ImageIcon, ListChecks, Loader2, MessageSquare, Minus, Paperclip, Puzzle, Search, Send, Settings, Sparkles, Star, Terminal, User, Workflow, Wrench, X } from "lucide-react";
9
9
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
10
10
  import { cva } from "class-variance-authority";
11
11
  import { ContentEditable } from "@lexical/react/LexicalContentEditable";
@@ -513,7 +513,7 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
513
513
  children: [/* @__PURE__ */ jsxs("div", {
514
514
  className: "flex min-h-0 flex-col border-r border-gray-200",
515
515
  children: [!isLoading && filterViews.length > 0 ? /* @__PURE__ */ jsx("div", {
516
- className: "flex shrink-0 gap-1 overflow-x-auto px-2.5 pb-2 pt-2.5",
516
+ className: "flex shrink-0 gap-0.5 overflow-x-auto px-2 pb-1.5 pt-2",
517
517
  children: filterViews.map(({ count, key, label }) => {
518
518
  const isActive = key === resolvedActiveFilterKey;
519
519
  return /* @__PURE__ */ jsxs("button", {
@@ -521,9 +521,9 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
521
521
  "aria-pressed": isActive,
522
522
  onPointerDown: (event) => event.preventDefault(),
523
523
  onClick: () => handleFilterSelect(key),
524
- className: `inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-1 text-[11px] font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${isActive ? "border-primary/30 bg-primary/10 text-primary" : "border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50"}`,
524
+ className: `inline-flex h-7 shrink-0 items-center gap-1 rounded-md px-2 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}`,
525
525
  children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
526
- className: isActive ? "text-primary/70" : "text-gray-400",
526
+ className: isActive ? "text-gray-500" : "text-gray-400",
527
527
  children: count
528
528
  })]
529
529
  }, key);
@@ -532,58 +532,52 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
532
532
  ref: listRef,
533
533
  role: "listbox",
534
534
  "aria-label": texts.sectionLabel,
535
- className: `custom-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain ${!isLoading && filterViews.length > 0 ? "px-2.5 pb-2.5" : "p-2.5"}`,
535
+ className: `custom-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain ${!isLoading && filterViews.length > 0 ? "px-2 pb-2" : "p-2"}`,
536
536
  children: isLoading ? /* @__PURE__ */ jsx("div", {
537
537
  className: "p-2 text-xs text-gray-500",
538
538
  children: texts.loadingLabel
539
539
  }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [!hasItemSections ? /* @__PURE__ */ jsx("div", {
540
- className: "mb-2 px-2 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
540
+ className: "mb-1 px-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
541
541
  children: texts.sectionLabel
542
542
  }) : null, visibleItems.length === 0 ? /* @__PURE__ */ jsx("div", {
543
- className: "px-2 text-xs text-gray-400",
543
+ className: "px-1.5 text-xs text-gray-400",
544
544
  children: texts.emptyLabel
545
- }) : /* @__PURE__ */ jsx("div", {
546
- className: "space-y-1",
547
- children: visibleItems.map((item, index) => {
548
- const { key, sectionKey, sectionLabel, title, subtitle } = item;
549
- const isActive = index === activeIndexInRange;
550
- const previousItem = visibleItems[index - 1];
551
- return /* @__PURE__ */ jsxs("div", {
552
- className: "space-y-1",
553
- children: [hasItemSections && Boolean(sectionLabel?.trim()) && previousItem?.sectionKey !== sectionKey ? /* @__PURE__ */ jsx("div", {
554
- className: "px-2 pb-0.5 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
555
- children: sectionLabel
556
- }) : null, /* @__PURE__ */ jsxs("button", {
557
- type: "button",
558
- role: "option",
559
- "aria-selected": isActive,
560
- "data-input-surface-index": index,
561
- onPointerMove: (event) => {
562
- if (event.pointerType !== "touch") setActiveIndexForCurrentItems(index);
563
- },
564
- onPointerDown: (event) => {
565
- if (event.button > 0) return;
566
- event.preventDefault();
567
- onSelectItem(item);
568
- },
569
- onClick: (event) => {
570
- if (event.detail === 0) onSelectItem(item);
571
- },
572
- className: `flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left transition ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-50"}`,
573
- children: [/* @__PURE__ */ jsx("span", {
574
- className: "truncate text-xs font-semibold",
575
- children: title
576
- }), /* @__PURE__ */ jsx("span", {
577
- className: "truncate text-xs text-gray-500",
578
- children: subtitle
579
- })]
580
- })]
581
- }, key);
582
- })
583
- })] })
545
+ }) : /* @__PURE__ */ jsx("div", { children: visibleItems.map((item, index) => {
546
+ const { key, sectionKey, sectionLabel, title, subtitle } = item;
547
+ const isActive = index === activeIndexInRange;
548
+ const previousItem = visibleItems[index - 1];
549
+ return /* @__PURE__ */ jsxs("div", { children: [hasItemSections && Boolean(sectionLabel?.trim()) && previousItem?.sectionKey !== sectionKey ? /* @__PURE__ */ jsx("div", {
550
+ className: "px-1.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
551
+ children: sectionLabel
552
+ }) : null, /* @__PURE__ */ jsxs("button", {
553
+ type: "button",
554
+ role: "option",
555
+ "aria-selected": isActive,
556
+ "data-input-surface-index": index,
557
+ onPointerMove: (event) => {
558
+ if (event.pointerType !== "touch") setActiveIndexForCurrentItems(index);
559
+ },
560
+ onPointerDown: (event) => {
561
+ if (event.button > 0) return;
562
+ event.preventDefault();
563
+ onSelectItem(item);
564
+ },
565
+ onClick: (event) => {
566
+ if (event.detail === 0) onSelectItem(item);
567
+ },
568
+ className: `flex w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-left leading-4 transition-colors ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-100"}`,
569
+ children: [/* @__PURE__ */ jsx("span", {
570
+ className: "truncate text-xs font-medium",
571
+ children: title
572
+ }), /* @__PURE__ */ jsx("span", {
573
+ className: "truncate text-xs text-gray-500",
574
+ children: subtitle
575
+ })]
576
+ })] }, key);
577
+ }) })] })
584
578
  })]
585
579
  }), /* @__PURE__ */ jsx("div", {
586
- className: "custom-scrollbar min-h-0 min-w-0 overflow-y-auto overscroll-contain p-2.5",
580
+ className: "custom-scrollbar min-h-0 min-w-0 select-text overflow-y-auto overscroll-contain p-2.5",
587
581
  onPointerDown: onDetailsPointerDown,
588
582
  children: activeItem ? /* @__PURE__ */ jsxs("div", {
589
583
  className: "space-y-3",
@@ -640,15 +634,13 @@ function resolveInputSurfaceTriggerIdentity(currentIdentity, trigger, reason) {
640
634
  function ChatInputSurfaceHost(props) {
641
635
  const { children, inputSurface, onInputSurfaceTriggerChange, onSelectItem, triggerSpecs = [CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC] } = props;
642
636
  const menuRef = useRef(null);
637
+ const isDetailsInteractionRef = useRef(false);
643
638
  const [activeTriggerIdentity, setActiveTriggerIdentity] = useState(null);
644
639
  const isOpen = Boolean(inputSurface) && activeTriggerIdentity !== null;
645
640
  const setActiveTrigger = useCallback((identity, trigger) => {
646
641
  setActiveTriggerIdentity(identity);
647
642
  onInputSurfaceTriggerChange?.(identity ? trigger : null);
648
643
  }, [onInputSurfaceTriggerChange]);
649
- const closeInputSurface = useCallback(() => {
650
- setActiveTrigger(null, null);
651
- }, [setActiveTrigger]);
652
644
  const handleInputSurfaceSnapshotChange = useCallback((nodes, selection, reason) => {
653
645
  const trigger = resolveChatComposerActiveInputSurfaceTrigger(nodes, selection, triggerSpecs);
654
646
  const nextIdentity = resolveInputSurfaceTriggerIdentity(activeTriggerIdentity, trigger, reason);
@@ -662,8 +654,8 @@ function ChatInputSurfaceHost(props) {
662
654
  return menuRef.current?.handleKeyDown(event) ?? false;
663
655
  }, []);
664
656
  const handleInputSurfaceOpenChange = useCallback((open) => {
665
- if (!open) closeInputSurface();
666
- }, [closeInputSurface]);
657
+ if (!open && !isDetailsInteractionRef.current) setActiveTrigger(null, null);
658
+ }, [setActiveTrigger]);
667
659
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [children(useMemo(() => ({
668
660
  onInputSurfaceKeyDown: handleInputSurfaceKeyDown,
669
661
  onInputSurfaceOpenChange: handleInputSurfaceOpenChange,
@@ -680,11 +672,16 @@ function ChatInputSurfaceHost(props) {
680
672
  items: inputSurface.items,
681
673
  texts: inputSurface.texts,
682
674
  onSelectItem: (item) => {
683
- closeInputSurface();
675
+ setActiveTrigger(null, null);
684
676
  onSelectItem(item);
685
677
  },
686
678
  onOpenChange: handleInputSurfaceOpenChange,
687
- onDetailsPointerDown: (event) => event.preventDefault()
679
+ onDetailsPointerDown: () => {
680
+ isDetailsInteractionRef.current = true;
681
+ requestAnimationFrame(() => {
682
+ isDetailsInteractionRef.current = false;
683
+ });
684
+ }
688
685
  }, activeTriggerIdentity) : null] });
689
686
  }
690
687
  //#endregion
@@ -727,12 +724,6 @@ const ChatButton = React.forwardRef(({ className, variant, size, ...props }, ref
727
724
  ChatButton.displayName = "ChatButton";
728
725
  //#endregion
729
726
  //#region src/components/chat/ui/chat-input-bar/chat-input-bar-actions.tsx
730
- const SEND_ERROR_PREVIEW_MAX_CHARS = 120;
731
- function buildSendErrorPreview(value) {
732
- const compact = value.replace(/\s+/g, " ").trim();
733
- if (compact.length <= SEND_ERROR_PREVIEW_MAX_CHARS) return compact;
734
- return `${compact.slice(0, SEND_ERROR_PREVIEW_MAX_CHARS - 1)}…`;
735
- }
736
727
  function StopIcon() {
737
728
  return /* @__PURE__ */ jsx("span", {
738
729
  "aria-hidden": "true",
@@ -782,75 +773,42 @@ function ContextWindowIndicator({ contextWindow }) {
782
773
  })] })
783
774
  });
784
775
  }
785
- function ChatInputBarActions({ sendError, sendErrorDetailsLabel, isSending, canStopGeneration, sendDisabled, stopDisabled, stopHint, sendButtonLabel, stopButtonLabel, contextWindow, onSend, onStop }) {
786
- const { Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
787
- const normalizedSendError = sendError?.trim() ?? "";
788
- const sendErrorPreview = normalizedSendError ? buildSendErrorPreview(normalizedSendError) : "";
789
- const resolvedSendErrorDetailsLabel = sendErrorDetailsLabel?.trim() || "Details";
776
+ function ChatInputBarActions({ isSending, canStopGeneration, sendDisabled, stopDisabled, stopHint, sendButtonLabel, stopButtonLabel, contextWindow, onSend, onStop }) {
777
+ const { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
790
778
  return /* @__PURE__ */ jsxs("div", {
791
- className: "flex shrink-0 flex-col items-end gap-1",
792
- children: [normalizedSendError ? /* @__PURE__ */ jsxs("div", {
793
- className: "flex max-w-[420px] items-start justify-end gap-2 text-right",
794
- children: [/* @__PURE__ */ jsx("div", {
795
- className: "min-w-0 flex-1 text-[11px] text-red-600",
796
- children: /* @__PURE__ */ jsx("span", {
797
- className: "block truncate",
798
- title: normalizedSendError,
799
- children: sendErrorPreview
800
- })
801
- }), /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(PopoverTrigger, {
802
- asChild: true,
803
- children: /* @__PURE__ */ jsx("button", {
804
- type: "button",
805
- className: "shrink-0 rounded-full border border-red-200/80 bg-red-50 px-2 py-0.5 text-[10px] font-semibold text-red-700 transition-colors hover:bg-red-100",
806
- children: resolvedSendErrorDetailsLabel
807
- })
808
- }), /* @__PURE__ */ jsxs(PopoverContent, {
809
- align: "end",
810
- className: "w-[min(32rem,calc(100vw-1.5rem))] border-red-100/80 p-0",
811
- children: [/* @__PURE__ */ jsx("div", {
812
- className: "border-b border-red-100 bg-red-50/80 px-4 py-2 text-xs font-semibold text-red-700",
813
- children: resolvedSendErrorDetailsLabel
814
- }), /* @__PURE__ */ jsx("pre", {
815
- className: "max-h-80 overflow-auto whitespace-pre-wrap break-words px-4 py-3 text-xs leading-relaxed text-red-700",
816
- children: normalizedSendError
817
- })]
818
- })] })]
819
- }) : null, /* @__PURE__ */ jsxs("div", {
820
- className: "flex items-center gap-2",
821
- children: [contextWindow ? /* @__PURE__ */ jsx(ContextWindowIndicator, { contextWindow }) : null, !isSending || !sendDisabled ? /* @__PURE__ */ jsx(ChatButton, {
822
- size: "icon",
823
- className: "h-8 w-8 rounded-full",
824
- "aria-label": sendButtonLabel,
825
- onClick: () => void onSend(),
826
- disabled: sendDisabled,
827
- children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-5 w-5" })
828
- }) : canStopGeneration ? /* @__PURE__ */ jsx(ChatButton, {
779
+ className: "flex shrink-0 items-center gap-2",
780
+ children: [contextWindow ? /* @__PURE__ */ jsx(ContextWindowIndicator, { contextWindow }) : null, !isSending || !sendDisabled ? /* @__PURE__ */ jsx(ChatButton, {
781
+ size: "icon",
782
+ className: "h-8 w-8 rounded-full",
783
+ "aria-label": sendButtonLabel,
784
+ onClick: () => void onSend(),
785
+ disabled: sendDisabled,
786
+ children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-5 w-5" })
787
+ }) : canStopGeneration ? /* @__PURE__ */ jsx(ChatButton, {
788
+ size: "icon",
789
+ variant: "outline",
790
+ className: "h-8 w-8 rounded-full",
791
+ "aria-label": stopButtonLabel,
792
+ onClick: () => void onStop(),
793
+ disabled: stopDisabled,
794
+ children: /* @__PURE__ */ jsx(StopIcon, {})
795
+ }) : /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
796
+ asChild: true,
797
+ children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(ChatButton, {
829
798
  size: "icon",
830
799
  variant: "outline",
831
800
  className: "h-8 w-8 rounded-full",
832
801
  "aria-label": stopButtonLabel,
833
- onClick: () => void onStop(),
834
- disabled: stopDisabled,
802
+ disabled: true,
835
803
  children: /* @__PURE__ */ jsx(StopIcon, {})
836
- }) : /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
837
- asChild: true,
838
- children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(ChatButton, {
839
- size: "icon",
840
- variant: "outline",
841
- className: "h-8 w-8 rounded-full",
842
- "aria-label": stopButtonLabel,
843
- disabled: true,
844
- children: /* @__PURE__ */ jsx(StopIcon, {})
845
- }) })
846
- }), /* @__PURE__ */ jsx(TooltipContent, {
847
- side: "top",
848
- children: /* @__PURE__ */ jsx("p", {
849
- className: "text-xs",
850
- children: stopHint
851
- })
852
- })] }) })]
853
- })]
804
+ }) })
805
+ }), /* @__PURE__ */ jsx(TooltipContent, {
806
+ side: "top",
807
+ children: /* @__PURE__ */ jsx("p", {
808
+ className: "text-xs",
809
+ children: stopHint
810
+ })
811
+ })] }) })]
854
812
  });
855
813
  }
856
814
  //#endregion
@@ -1160,16 +1118,16 @@ function ToolbarSearchableSelect({ item }) {
1160
1118
  children: [/* @__PURE__ */ jsx(ToolbarSelectTriggerContent, { item }), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-1 h-3.5 w-3.5 shrink-0 text-muted-foreground/70 [@container_nextclaw-chat-input-bar_(max-width:440px)]:hidden" })]
1161
1119
  })
1162
1120
  }), /* @__PURE__ */ jsxs(PopoverContent, {
1163
- className: `flex flex-col overflow-hidden p-2 ${CONTENT_WIDTH_BY_KEY[item.key] ?? ""}`,
1121
+ className: `flex flex-col overflow-hidden p-1.5 ${CONTENT_WIDTH_BY_KEY[item.key] ?? ""}`,
1164
1122
  style: { maxHeight: TOOLBAR_POPOVER_MAX_HEIGHT },
1165
1123
  children: [
1166
1124
  /* @__PURE__ */ jsxs("div", {
1167
- className: "relative mb-2 shrink-0",
1125
+ className: "relative mb-1.5 shrink-0",
1168
1126
  children: [/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/70" }), /* @__PURE__ */ jsx(Input, {
1169
1127
  value: query,
1170
1128
  onChange: (event) => setQuery(event.currentTarget.value),
1171
1129
  placeholder: item.search?.placeholder ?? item.placeholder,
1172
- className: "h-8 rounded-lg pl-8 text-xs"
1130
+ className: "h-7 rounded-md pl-8 text-xs"
1173
1131
  })]
1174
1132
  }),
1175
1133
  !hasOptions ? item.loading ? /* @__PURE__ */ jsxs("div", {
@@ -1190,9 +1148,9 @@ function ToolbarSearchableSelect({ item }) {
1190
1148
  /* @__PURE__ */ jsx("div", {
1191
1149
  className: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
1192
1150
  children: filteredGroups.map((group, groupIndex) => /* @__PURE__ */ jsxs("div", {
1193
- className: groupIndex > 0 ? "border-t border-border pt-1" : void 0,
1151
+ className: groupIndex > 0 ? "border-t border-border pt-0.5" : void 0,
1194
1152
  children: [group.label ? /* @__PURE__ */ jsx("div", {
1195
- className: "px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground",
1153
+ className: "px-2 pb-0.5 pt-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground",
1196
1154
  children: group.label
1197
1155
  }) : null, group.options.map((option) => {
1198
1156
  const isSelected = item.value === option.value;
@@ -1202,7 +1160,7 @@ function ToolbarSearchableSelect({ item }) {
1202
1160
  className: "group flex items-center gap-1 rounded-md hover:bg-accent",
1203
1161
  children: [/* @__PURE__ */ jsxs("button", {
1204
1162
  type: "button",
1205
- className: "flex min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-2 text-left",
1163
+ className: "flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-2 py-1.5 text-left leading-4",
1206
1164
  onClick: () => {
1207
1165
  item.onValueChange(option.value);
1208
1166
  setOpen(false);
@@ -1269,7 +1227,7 @@ function ToolbarSelect({ item }) {
1269
1227
  children: item.emptyLabel
1270
1228
  }) : null : null, groups.map((group, groupIndex) => /* @__PURE__ */ jsxs("div", { children: [groupIndex > 0 ? /* @__PURE__ */ jsx(SelectSeparator, {}) : null, /* @__PURE__ */ jsxs(SelectGroup, { children: [group.label ? /* @__PURE__ */ jsx(SelectLabel, { children: group.label }) : null, group.options.map((option) => /* @__PURE__ */ jsx(SelectItem, {
1271
1229
  value: option.value,
1272
- className: "py-2",
1230
+ className: "py-1.5",
1273
1231
  children: /* @__PURE__ */ jsx(ToolbarSelectOptionContent, { option })
1274
1232
  }, option.value))] })] }, group.key))]
1275
1233
  })]
@@ -2294,6 +2252,11 @@ const ChatInputBarTokenizedComposer = forwardRef(function ChatInputBarTokenizedC
2294
2252
  ChatInputBarTokenizedComposer.displayName = "LexicalChatInputBarTokenizedComposer";
2295
2253
  //#endregion
2296
2254
  //#region src/components/chat/ui/chat-input-bar/chat-input-bar.tsx
2255
+ const SEND_ERROR_PREVIEW_MAX_CHARS = 120;
2256
+ function buildSendErrorPreview(value) {
2257
+ const compact = value.replace(/\s+/g, " ").trim();
2258
+ return compact.length <= SEND_ERROR_PREVIEW_MAX_CHARS ? compact : `${compact.slice(0, SEND_ERROR_PREVIEW_MAX_CHARS - 1)}…`;
2259
+ }
2297
2260
  function InputBarHint({ hint }) {
2298
2261
  if (!hint) return null;
2299
2262
  if (hint.loading) return /* @__PURE__ */ jsx("div", {
@@ -2316,7 +2279,44 @@ function InputBarHint({ hint }) {
2316
2279
  })
2317
2280
  });
2318
2281
  }
2319
- const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSurface, slashMenu, surface, toolbar: toolbarProps, topSlot }, ref) {
2282
+ function ChatInputBarSendError({ sendError, sendErrorDetailsLabel }) {
2283
+ const normalizedSendError = sendError?.trim() ?? "";
2284
+ if (!normalizedSendError) return null;
2285
+ const { Popover, PopoverContent, PopoverTrigger } = ChatUiPrimitives;
2286
+ const sendErrorPreview = buildSendErrorPreview(normalizedSendError);
2287
+ const resolvedSendErrorDetailsLabel = sendErrorDetailsLabel?.trim() || "Details";
2288
+ return /* @__PURE__ */ jsx("div", {
2289
+ className: "px-3 pb-2 sm:px-4",
2290
+ children: /* @__PURE__ */ jsxs("div", {
2291
+ "aria-live": "polite",
2292
+ className: "flex min-w-0 items-start justify-between gap-3 rounded-lg border border-red-200/80 bg-red-50/80 px-3 py-2 text-xs text-red-700",
2293
+ role: "status",
2294
+ children: [/* @__PURE__ */ jsx("span", {
2295
+ className: "min-w-0 flex-1 truncate leading-5",
2296
+ title: normalizedSendError,
2297
+ children: sendErrorPreview
2298
+ }), /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(PopoverTrigger, {
2299
+ asChild: true,
2300
+ children: /* @__PURE__ */ jsx("button", {
2301
+ type: "button",
2302
+ className: "inline-flex h-7 shrink-0 items-center rounded-md border border-red-200/80 bg-background px-2 text-xs font-medium text-red-700 transition-colors hover:bg-red-100",
2303
+ children: resolvedSendErrorDetailsLabel
2304
+ })
2305
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
2306
+ align: "end",
2307
+ className: "w-[min(32rem,calc(100vw-1.5rem))] border-red-100/80 p-0",
2308
+ children: [/* @__PURE__ */ jsx("div", {
2309
+ className: "border-b border-red-100 bg-red-50/80 px-4 py-2 text-xs font-semibold text-red-700",
2310
+ children: resolvedSendErrorDetailsLabel
2311
+ }), /* @__PURE__ */ jsx("pre", {
2312
+ className: "max-h-80 overflow-auto whitespace-pre-wrap break-words px-4 py-3 text-xs leading-relaxed text-red-700",
2313
+ children: normalizedSendError
2314
+ })]
2315
+ })] })]
2316
+ })
2317
+ });
2318
+ }
2319
+ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSurface, sendError, sendErrorDetailsLabel, slashMenu, surface, toolbar: toolbarProps, topSlot }, ref) {
2320
2320
  const composerRef = useRef(null);
2321
2321
  const resolvedInputSurface = inputSurface ?? (slashMenu ? {
2322
2322
  isLoading: slashMenu.isLoading,
@@ -2384,6 +2384,10 @@ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSur
2384
2384
  })
2385
2385
  }),
2386
2386
  /* @__PURE__ */ jsx(InputBarHint, { hint }),
2387
+ /* @__PURE__ */ jsx(ChatInputBarSendError, {
2388
+ sendError,
2389
+ sendErrorDetailsLabel
2390
+ }),
2387
2391
  /* @__PURE__ */ jsx(ChatInputBarToolbar, { ...toolbar })
2388
2392
  ]
2389
2393
  })
@@ -2393,22 +2397,25 @@ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSur
2393
2397
  ChatInputBar.displayName = "ChatInputBar";
2394
2398
  //#endregion
2395
2399
  //#region src/components/chat/ui/chat-message-list/chat-message-avatar.tsx
2396
- function ChatMessageAvatar({ role }) {
2400
+ function ChatMessageAvatar({ role, size = "default" }) {
2401
+ const compact = size === "compact";
2402
+ const frameSize = compact ? "h-7 w-7" : "h-8 w-8";
2403
+ const iconSize = compact ? "h-3.5 w-3.5" : "h-4 w-4";
2397
2404
  if (role === "user") return /* @__PURE__ */ jsx("div", {
2398
2405
  "data-testid": "chat-message-avatar-user",
2399
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm",
2400
- children: /* @__PURE__ */ jsx(User, { className: "h-4 w-4" })
2406
+ className: cn("flex shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm", frameSize),
2407
+ children: /* @__PURE__ */ jsx(User, { className: iconSize })
2401
2408
  });
2402
2409
  if (role === "tool") return /* @__PURE__ */ jsx("div", {
2403
2410
  "data-testid": "chat-message-avatar-tool",
2404
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-accent text-accent-foreground shadow-sm ring-1 ring-border",
2405
- children: /* @__PURE__ */ jsx(Wrench, { className: "h-4 w-4" })
2411
+ className: cn("flex shrink-0 items-center justify-center rounded-full bg-accent text-accent-foreground shadow-sm ring-1 ring-border", frameSize),
2412
+ children: /* @__PURE__ */ jsx(Wrench, { className: iconSize })
2406
2413
  });
2407
2414
  return /* @__PURE__ */ jsx("div", {
2408
2415
  "data-testid": "chat-message-avatar-assistant",
2409
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-foreground shadow-sm ring-1 ring-border",
2416
+ className: cn("flex shrink-0 items-center justify-center rounded-full bg-muted text-foreground shadow-sm ring-1 ring-border", frameSize),
2410
2417
  children: /* @__PURE__ */ jsx(Bot, {
2411
- className: "h-[18px] w-[18px] text-current",
2418
+ className: cn(compact ? "h-4 w-4" : "h-[18px] w-[18px]", "text-current"),
2412
2419
  strokeWidth: 2.5
2413
2420
  })
2414
2421
  });
@@ -2929,26 +2936,126 @@ function ChatInlineDisplay({ display, renderInlineDisplay }) {
2929
2936
  return /* @__PURE__ */ jsx(ChatInlineDisplayFallback, { display });
2930
2937
  }
2931
2938
  //#endregion
2932
- //#region src/components/chat/ui/chat-message-list/chat-message-markdown.tsx
2933
- const MARKDOWN_MAX_CHARS = 14e4;
2939
+ //#region src/components/chat/ui/chat-message-list/chat-message-file/chat-message-image-preview.tsx
2940
+ function ChatMessageImageLightbox({ alt, closeLabel, onClose, src }) {
2941
+ const titleId = useId();
2942
+ useEffect(() => {
2943
+ const previousOverflow = document.body.style.overflow;
2944
+ document.body.style.overflow = "hidden";
2945
+ const onKeyDown = (event) => {
2946
+ if (event.key === "Escape") {
2947
+ event.preventDefault();
2948
+ onClose();
2949
+ }
2950
+ };
2951
+ window.addEventListener("keydown", onKeyDown);
2952
+ return () => {
2953
+ document.body.style.overflow = previousOverflow;
2954
+ window.removeEventListener("keydown", onKeyDown);
2955
+ };
2956
+ }, [onClose]);
2957
+ return createPortal(/* @__PURE__ */ jsxs("div", {
2958
+ role: "dialog",
2959
+ "aria-modal": "true",
2960
+ "aria-labelledby": titleId,
2961
+ className: "fixed inset-0 z-[var(--z-modal,10050)] flex items-center justify-center bg-black/80 p-4 backdrop-blur-[2px]",
2962
+ "data-testid": "chat-message-image-lightbox",
2963
+ onClick: onClose,
2964
+ children: [
2965
+ /* @__PURE__ */ jsx("span", {
2966
+ id: titleId,
2967
+ className: "sr-only",
2968
+ children: alt
2969
+ }),
2970
+ /* @__PURE__ */ jsx("button", {
2971
+ type: "button",
2972
+ "aria-label": closeLabel,
2973
+ className: "absolute right-4 top-4 inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/55 text-white transition-colors hover:bg-black/70 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/70",
2974
+ onClick: (event) => {
2975
+ event.stopPropagation();
2976
+ onClose();
2977
+ },
2978
+ children: /* @__PURE__ */ jsx(X, {
2979
+ className: "h-4 w-4",
2980
+ strokeWidth: 2
2981
+ })
2982
+ }),
2983
+ /* @__PURE__ */ jsx("img", {
2984
+ src,
2985
+ alt,
2986
+ className: "max-h-[min(92vh,100%)] max-w-[min(96vw,100%)] object-contain shadow-2xl",
2987
+ onClick: (event) => event.stopPropagation()
2988
+ })
2989
+ ]
2990
+ }), document.body);
2991
+ }
2992
+ function ChatMessageImagePreview({ alt, expandLabel, closeLabel, sizeLabel, src }) {
2993
+ const [isExpanded, setIsExpanded] = useState(false);
2994
+ const openLightbox = () => setIsExpanded(true);
2995
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
2996
+ "data-chat-message-image-preview": true,
2997
+ className: "group/image relative block w-fit max-w-[min(100%,32rem)] overflow-hidden rounded-lg",
2998
+ children: [
2999
+ /* @__PURE__ */ jsx("button", {
3000
+ type: "button",
3001
+ className: "block w-fit max-w-[min(100%,32rem)] text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
3002
+ onClick: openLightbox,
3003
+ onDoubleClick: (event) => {
3004
+ event.preventDefault();
3005
+ openLightbox();
3006
+ },
3007
+ "aria-label": expandLabel,
3008
+ children: /* @__PURE__ */ jsx("img", {
3009
+ src,
3010
+ alt,
3011
+ className: "block h-auto w-auto max-h-[26rem] max-w-full rounded-lg bg-transparent object-contain"
3012
+ })
3013
+ }),
3014
+ /* @__PURE__ */ jsx("button", {
3015
+ type: "button",
3016
+ "aria-label": expandLabel,
3017
+ className: "absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center rounded-md bg-black/45 text-white opacity-0 transition-opacity duration-150 hover:bg-black/60 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/70 group-hover/image:opacity-100",
3018
+ onClick: (event) => {
3019
+ event.preventDefault();
3020
+ event.stopPropagation();
3021
+ openLightbox();
3022
+ },
3023
+ children: /* @__PURE__ */ jsx(Expand, {
3024
+ className: "h-3.5 w-3.5",
3025
+ strokeWidth: 2
3026
+ })
3027
+ }),
3028
+ sizeLabel ? /* @__PURE__ */ jsx("span", {
3029
+ className: "pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-start bg-gradient-to-t from-black/50 via-black/15 to-transparent px-2.5 pb-2 pt-8 opacity-0 transition-opacity duration-150 group-hover/image:opacity-100 group-focus-within/image:opacity-100",
3030
+ children: /* @__PURE__ */ jsx("span", {
3031
+ className: "inline-flex items-center rounded-md bg-black/40 px-1.5 py-0.5 text-[10px] font-medium text-white/95 backdrop-blur-sm",
3032
+ children: sizeLabel
3033
+ })
3034
+ }) : null
3035
+ ]
3036
+ }), isExpanded ? /* @__PURE__ */ jsx(ChatMessageImageLightbox, {
3037
+ alt,
3038
+ closeLabel,
3039
+ onClose: () => setIsExpanded(false),
3040
+ src
3041
+ }) : null] });
3042
+ }
3043
+ //#endregion
3044
+ //#region src/components/chat/ui/chat-message-list/utils/chat-local-resource.utils.ts
2934
3045
  const SAFE_LINK_PROTOCOLS = new Set([
2935
3046
  "http:",
2936
3047
  "https:",
2937
3048
  "mailto:",
2938
3049
  "tel:"
2939
3050
  ]);
2940
- const INLINE_TOKEN_KIND_ATTR = "data-chat-inline-token-kind";
2941
- const INLINE_TOKEN_KEY_ATTR = "data-chat-inline-token-key";
2942
- const INLINE_TOKEN_LABEL_ATTR = "data-chat-inline-token-label";
2943
- const INLINE_TOKEN_RAW_TEXT_ATTR = "data-chat-inline-token-raw-text";
2944
- const PROJECT_RELATIVE_FILE_HREF_PATTERN = new RegExp(`^(?![a-zA-Z][a-zA-Z\\d+.-]*:)(?!//)(?:(?:[^/\\s?#]+/)+[^?#\\s]+\\.[A-Za-z0-9_-]+|[^/\\s?#]+\\.(?:cjs|css|cts|html?|js|json|jsx|mdx?|mjs|mts|tsx?|txt|ya?ml))(?::\\d+(?::\\d+)?)?(?:[?#].*)?$`, "i");
2945
- function trimMarkdown(value) {
2946
- if (value.length <= MARKDOWN_MAX_CHARS) return value;
2947
- return `${value.slice(0, MARKDOWN_MAX_CHARS)}\n\n...`;
3051
+ const URI_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
3052
+ const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[a-z]:[\\/]/i;
3053
+ function isSchemeLessResourceHref(href) {
3054
+ return !href.startsWith("//") && !URI_SCHEME_PATTERN.test(href);
2948
3055
  }
2949
- function resolveSafeHref(href) {
3056
+ function resolveSafeChatResourceHref(href) {
2950
3057
  if (!href) return null;
2951
- if (href.startsWith("#") || href.startsWith("/") || href.startsWith("./") || href.startsWith("../") || PROJECT_RELATIVE_FILE_HREF_PATTERN.test(href)) return href;
3058
+ if (WINDOWS_ABSOLUTE_PATH_PATTERN.test(href) || isSchemeLessResourceHref(href)) return href;
2952
3059
  try {
2953
3060
  const url = new URL(href);
2954
3061
  return SAFE_LINK_PROTOCOLS.has(url.protocol) ? href : null;
@@ -2956,30 +3063,57 @@ function resolveSafeHref(href) {
2956
3063
  return null;
2957
3064
  }
2958
3065
  }
2959
- function isExternalHref(href) {
3066
+ function isExternalChatResourceHref(href) {
2960
3067
  return /^https?:\/\//i.test(href);
2961
3068
  }
2962
- function looksLikeLocalFileHref(href) {
2963
- return href.startsWith("./") || href.startsWith("../") || href.startsWith("/Users/") || href.startsWith("/home/") || href.startsWith("/tmp/") || href.startsWith("/var/") || PROJECT_RELATIVE_FILE_HREF_PATTERN.test(href) || /^\/.+\.[A-Za-z0-9_-]+(?::\d+(?::\d+)?)?$/.test(href);
2964
- }
2965
- function parseLocalFileAction(href) {
2966
- const viewer = new URLSearchParams(href.split("#")[0]?.split("?")[1] ?? "").get("viewer");
2967
- const normalizedHref = href.split("#")[0]?.split("?")[0] ?? href;
2968
- const decodedHref = decodeURIComponent(normalizedHref);
2969
- if (!looksLikeLocalFileHref(decodedHref)) return null;
2970
- const lineMatch = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(decodedHref);
2971
- const rawPath = lineMatch?.[1] ?? decodedHref;
3069
+ function parseChatLocalFileAction(href) {
3070
+ const normalizedHref = href.split("#")[0] ?? href;
3071
+ const [encodedPath, encodedQuery = ""] = normalizedHref.split("?", 2);
3072
+ let path;
3073
+ try {
3074
+ path = decodeURIComponent(encodedPath ?? normalizedHref);
3075
+ } catch {
3076
+ return null;
3077
+ }
3078
+ if (!path || path.startsWith("#") || path.startsWith("//") || !WINDOWS_ABSOLUTE_PATH_PATTERN.test(path) && URI_SCHEME_PATTERN.test(path)) return null;
3079
+ const viewer = new URLSearchParams(encodedQuery).get("viewer");
3080
+ const lineMatch = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(path);
3081
+ const rawPath = lineMatch?.[1] ?? path;
2972
3082
  const line = lineMatch?.[2] ? Number(lineMatch[2]) : void 0;
2973
3083
  const column = lineMatch?.[3] ? Number(lineMatch[3]) : void 0;
2974
3084
  return {
2975
3085
  path: rawPath,
2976
- label: rawPath.split("/").filter(Boolean).pop() ?? rawPath,
3086
+ label: rawPath.split(/[\\/]/).filter(Boolean).pop() ?? rawPath,
2977
3087
  viewMode: "preview",
2978
3088
  ...viewer === "source" || viewer === "rendered" ? { previewViewer: viewer } : {},
2979
3089
  ...typeof line === "number" ? { line } : {},
2980
3090
  ...typeof column === "number" ? { column } : {}
2981
3091
  };
2982
3092
  }
3093
+ //#endregion
3094
+ //#region src/components/chat/ui/chat-message-list/chat-message-markdown.tsx
3095
+ const MARKDOWN_MAX_CHARS = 14e4;
3096
+ const INLINE_TOKEN_KIND_ATTR = "data-chat-inline-token-kind";
3097
+ const INLINE_TOKEN_KEY_ATTR = "data-chat-inline-token-key";
3098
+ const INLINE_TOKEN_LABEL_ATTR = "data-chat-inline-token-label";
3099
+ const INLINE_TOKEN_RAW_TEXT_ATTR = "data-chat-inline-token-raw-text";
3100
+ function trimMarkdown(value) {
3101
+ if (value.length <= MARKDOWN_MAX_CHARS) return value;
3102
+ return `${value.slice(0, MARKDOWN_MAX_CHARS)}\n\n...`;
3103
+ }
3104
+ function isSingleLineImageParagraph(node) {
3105
+ let imageCount = 0;
3106
+ for (const child of node?.children ?? []) {
3107
+ if (child.type === "text") {
3108
+ const text = child.value ?? "";
3109
+ if (text.trim().length > 0 || text.includes("\n") || text.includes("\r")) return false;
3110
+ continue;
3111
+ }
3112
+ if (child.tagName !== "img") return false;
3113
+ imageCount += 1;
3114
+ }
3115
+ return imageCount >= 3;
3116
+ }
2983
3117
  function prepareInlineTokens(inlineTokens) {
2984
3118
  if (!inlineTokens || inlineTokens.length === 0) return [];
2985
3119
  const seenRawTexts = /* @__PURE__ */ new Set();
@@ -3069,11 +3203,14 @@ function readStringProp(props, key) {
3069
3203
  const value = props[key];
3070
3204
  return typeof value === "string" && value.length > 0 ? value : null;
3071
3205
  }
3072
- function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, onInlineTokenClick, renderInlineDisplay }) {
3206
+ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, onInlineTokenClick, resolveFileContentUrl, renderInlineDisplay }) {
3073
3207
  const isUser = role === "user";
3074
3208
  const remarkPlugins = useMemo(() => inlineTokens && inlineTokens.length > 0 ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm], [inlineTokens]);
3075
3209
  const markdownComponents = useMemo(() => ({
3076
- p: ({ children }) => inline ? /* @__PURE__ */ jsx(Fragment$1, { children }) : /* @__PURE__ */ jsx("p", { children }),
3210
+ p: ({ node, children }) => inline ? /* @__PURE__ */ jsx(Fragment$1, { children }) : /* @__PURE__ */ jsx("p", {
3211
+ "data-chat-image-row": isSingleLineImageParagraph(node) ? "three-column" : void 0,
3212
+ children
3213
+ }),
3077
3214
  span: ({ node: _node, children, ...rest }) => {
3078
3215
  const restProps = rest;
3079
3216
  const kind = readStringProp(restProps, INLINE_TOKEN_KIND_ATTR);
@@ -3097,14 +3234,14 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3097
3234
  });
3098
3235
  },
3099
3236
  a: ({ node: _node, href, children, ...rest }) => {
3100
- const safeHref = resolveSafeHref(href);
3101
- if (!safeHref) return /* @__PURE__ */ jsx("span", {
3102
- className: "chat-link-invalid",
3103
- children
3104
- });
3105
- const external = isExternalHref(safeHref);
3106
- const localFileAction = external ? null : parseLocalFileAction(safeHref);
3237
+ const safeHref = resolveSafeChatResourceHref(href);
3238
+ const external = safeHref ? isExternalChatResourceHref(safeHref) : false;
3239
+ const localFileAction = external ? null : safeHref ? parseChatLocalFileAction(safeHref) : null;
3107
3240
  const handleClick = (event) => {
3241
+ if (!safeHref) {
3242
+ event.preventDefault();
3243
+ return;
3244
+ }
3108
3245
  if (!onFileOpen || !localFileAction) return;
3109
3246
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3110
3247
  event.preventDefault();
@@ -3112,7 +3249,9 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3112
3249
  };
3113
3250
  return /* @__PURE__ */ jsx("a", {
3114
3251
  ...rest,
3115
- href: safeHref,
3252
+ className: cn(rest.className, !safeHref && "chat-link-invalid"),
3253
+ href: safeHref ?? "#",
3254
+ "aria-disabled": !safeHref || void 0,
3116
3255
  onClick: handleClick,
3117
3256
  target: external ? "_blank" : void 0,
3118
3257
  rel: external ? "noreferrer noopener" : void 0,
@@ -3140,15 +3279,18 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3140
3279
  className: "chat-task-checkbox"
3141
3280
  });
3142
3281
  },
3143
- img: ({ node: _node, src, alt, ...rest }) => {
3144
- const safeSrc = resolveSafeHref(src);
3282
+ img: ({ node: _node, src, alt }) => {
3283
+ const safeSrc = resolveSafeChatResourceHref(src);
3145
3284
  if (!safeSrc) return null;
3146
- return /* @__PURE__ */ jsx("img", {
3147
- ...rest,
3148
- src: safeSrc,
3285
+ const localFileAction = parseChatLocalFileAction(safeSrc);
3286
+ const resolvedSrc = localFileAction && resolveFileContentUrl ? resolveFileContentUrl(localFileAction) : safeSrc;
3287
+ if (!resolvedSrc) return null;
3288
+ return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3149
3289
  alt: alt || "",
3150
- loading: "lazy",
3151
- decoding: "async"
3290
+ closeLabel: texts.attachmentCloseLabel ?? "Close preview",
3291
+ expandLabel: texts.attachmentExpandLabel ?? "Expand image",
3292
+ sizeLabel: null,
3293
+ src: resolvedSrc
3152
3294
  });
3153
3295
  },
3154
3296
  code: ({ node: _node, className, children, ...rest }) => {
@@ -3175,6 +3317,7 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3175
3317
  onFileOpen,
3176
3318
  onInlineTokenClick,
3177
3319
  renderInlineDisplay,
3320
+ resolveFileContentUrl,
3178
3321
  texts
3179
3322
  ]);
3180
3323
  return /* @__PURE__ */ jsx(inline ? "span" : "div", {
@@ -3451,109 +3594,6 @@ function FileCategoryGlyph({ category, isUser }) {
3451
3594
  })
3452
3595
  });
3453
3596
  }
3454
- function ChatMessageImageLightbox({ alt, closeLabel, onClose, src }) {
3455
- const titleId = useId();
3456
- useEffect(() => {
3457
- const previousOverflow = document.body.style.overflow;
3458
- document.body.style.overflow = "hidden";
3459
- const onKeyDown = (event) => {
3460
- if (event.key === "Escape") {
3461
- event.preventDefault();
3462
- onClose();
3463
- }
3464
- };
3465
- window.addEventListener("keydown", onKeyDown);
3466
- return () => {
3467
- document.body.style.overflow = previousOverflow;
3468
- window.removeEventListener("keydown", onKeyDown);
3469
- };
3470
- }, [onClose]);
3471
- return createPortal(/* @__PURE__ */ jsxs("div", {
3472
- role: "dialog",
3473
- "aria-modal": "true",
3474
- "aria-labelledby": titleId,
3475
- className: "fixed inset-0 z-[var(--z-modal,10050)] flex items-center justify-center bg-black/80 p-4 backdrop-blur-[2px]",
3476
- "data-testid": "chat-message-image-lightbox",
3477
- onClick: onClose,
3478
- children: [
3479
- /* @__PURE__ */ jsx("span", {
3480
- id: titleId,
3481
- className: "sr-only",
3482
- children: alt
3483
- }),
3484
- /* @__PURE__ */ jsx("button", {
3485
- type: "button",
3486
- "aria-label": closeLabel,
3487
- className: "absolute right-4 top-4 inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/55 text-white transition-colors hover:bg-black/70 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/70",
3488
- onClick: (event) => {
3489
- event.stopPropagation();
3490
- onClose();
3491
- },
3492
- children: /* @__PURE__ */ jsx(X, {
3493
- className: "h-4 w-4",
3494
- strokeWidth: 2
3495
- })
3496
- }),
3497
- /* @__PURE__ */ jsx("img", {
3498
- src,
3499
- alt,
3500
- className: "max-h-[min(92vh,100%)] max-w-[min(96vw,100%)] object-contain shadow-2xl",
3501
- onClick: (event) => event.stopPropagation()
3502
- })
3503
- ]
3504
- }), document.body);
3505
- }
3506
- function ChatMessageImagePreview({ expandLabel, closeLabel, file, sizeLabel }) {
3507
- const [isExpanded, setIsExpanded] = useState(false);
3508
- if (!file.dataUrl) return null;
3509
- const openLightbox = () => setIsExpanded(true);
3510
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
3511
- className: "group/image relative w-fit max-w-full overflow-hidden rounded-xl",
3512
- children: [
3513
- /* @__PURE__ */ jsx("button", {
3514
- type: "button",
3515
- className: "block w-fit max-w-full text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
3516
- onClick: openLightbox,
3517
- onDoubleClick: (event) => {
3518
- event.preventDefault();
3519
- openLightbox();
3520
- },
3521
- "aria-label": expandLabel,
3522
- children: /* @__PURE__ */ jsx("img", {
3523
- src: file.dataUrl,
3524
- alt: file.label,
3525
- className: "block h-auto max-h-[26rem] max-w-full rounded-xl bg-transparent object-contain"
3526
- })
3527
- }),
3528
- /* @__PURE__ */ jsx("button", {
3529
- type: "button",
3530
- "aria-label": expandLabel,
3531
- className: "absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center rounded-md bg-black/45 text-white opacity-0 transition-opacity duration-150 hover:bg-black/60 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/70 group-hover/image:opacity-100",
3532
- onClick: (event) => {
3533
- event.preventDefault();
3534
- event.stopPropagation();
3535
- openLightbox();
3536
- },
3537
- children: /* @__PURE__ */ jsx(Expand, {
3538
- className: "h-3.5 w-3.5",
3539
- strokeWidth: 2
3540
- })
3541
- }),
3542
- sizeLabel ? /* @__PURE__ */ jsx("div", {
3543
- className: "pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-start bg-gradient-to-t from-black/50 via-black/15 to-transparent px-2.5 pb-2 pt-8 opacity-0 transition-opacity duration-150 group-hover/image:opacity-100 group-focus-within/image:opacity-100",
3544
- children: /* @__PURE__ */ jsx("span", {
3545
- className: "inline-flex items-center rounded-md bg-black/40 px-1.5 py-0.5 text-[10px] font-medium text-white/95 backdrop-blur-sm",
3546
- children: sizeLabel
3547
- })
3548
- }) : null
3549
- ]
3550
- }), isExpanded ? /* @__PURE__ */ jsx(ChatMessageImageLightbox, {
3551
- alt: file.label,
3552
- closeLabel,
3553
- onClose: () => setIsExpanded(false),
3554
- src: file.dataUrl
3555
- }) : null] });
3556
- }
3557
3597
  function renderFileCardHeader(params) {
3558
3598
  const { category, file, categoryLabel, sizeLabel, isUser, action } = params;
3559
3599
  return /* @__PURE__ */ jsxs("div", {
@@ -3633,11 +3673,12 @@ function ChatMessageFile({ file, isUser = false, texts, onOpen }) {
3633
3673
  const closeLabel = texts?.attachmentCloseLabel ?? "Close preview";
3634
3674
  const categoryLabel = readFileCategoryLabel(category, texts);
3635
3675
  const shellClasses = cn("block overflow-hidden rounded-xl border transition-colors", isUser ? "border-black/8 bg-black/[0.03] text-foreground" : "border-border/70 bg-card text-card-foreground", isInteractive && (isUser ? "hover:bg-black/[0.05]" : "hover:bg-muted/50"));
3636
- if (renderAsImage) return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3676
+ if (renderAsImage && file.dataUrl) return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3677
+ alt: file.label,
3637
3678
  expandLabel,
3638
3679
  closeLabel,
3639
- file,
3640
- sizeLabel
3680
+ sizeLabel,
3681
+ src: file.dataUrl
3641
3682
  });
3642
3683
  if (renderAsAudio || renderAsVideo) return renderInlineMediaCard({
3643
3684
  category,
@@ -3676,6 +3717,54 @@ function ChatMessageFile({ file, isUser = false, texts, onOpen }) {
3676
3717
  });
3677
3718
  }
3678
3719
  //#endregion
3720
+ //#region src/components/chat/ui/chat-message-list/chat-message-file/chat-message-image-row.tsx
3721
+ function ChatMessageImageRow({ group, indexOffset, isUser, texts, onOpen }) {
3722
+ return /* @__PURE__ */ jsx("div", {
3723
+ "data-chat-message-image-row": "three-column",
3724
+ "data-chat-message-wide-content": "true",
3725
+ className: "grid w-full grid-cols-3 items-start gap-3",
3726
+ children: group.items.map(({ index, part }) => /* @__PURE__ */ jsx(ChatMessageFile, {
3727
+ file: part.file,
3728
+ isUser,
3729
+ texts,
3730
+ onOpen
3731
+ }, `file-${indexOffset + index}`))
3732
+ });
3733
+ }
3734
+ //#endregion
3735
+ //#region src/components/chat/ui/chat-message-list/chat-message-file/chat-message-image-group.utils.ts
3736
+ function isPreviewableImageBlock(block) {
3737
+ return Boolean(block?.kind === "part" && block.part.type === "file" && block.part.file.isImage && block.part.file.dataUrl);
3738
+ }
3739
+ function groupConsecutiveImageFileBlocks(blocks) {
3740
+ const grouped = [];
3741
+ let index = 0;
3742
+ while (index < blocks.length) {
3743
+ const block = blocks[index];
3744
+ if (!isPreviewableImageBlock(block)) {
3745
+ if (block) grouped.push(block);
3746
+ index += 1;
3747
+ continue;
3748
+ }
3749
+ const startIndex = index;
3750
+ const items = [];
3751
+ while (isPreviewableImageBlock(blocks[index])) {
3752
+ items.push(blocks[index]);
3753
+ index += 1;
3754
+ }
3755
+ if (items.length < 3) {
3756
+ grouped.push(...items);
3757
+ continue;
3758
+ }
3759
+ grouped.push({
3760
+ kind: "image-group",
3761
+ key: `image-group-${startIndex}`,
3762
+ items
3763
+ });
3764
+ }
3765
+ return grouped;
3766
+ }
3767
+ //#endregion
3679
3768
  //#region src/components/chat/hooks/use-reasoning-block-open-state.ts
3680
3769
  function useReasoningBlockOpenState(params) {
3681
3770
  const { isInProgress } = params;
@@ -3711,7 +3800,6 @@ const DEFAULT_STICKY_THRESHOLD_PX = 10;
3711
3800
  function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey, scrollRef, stickyThresholdPx }) {
3712
3801
  const [isAtBottom, setIsAtBottom] = useState(true);
3713
3802
  const isStickyRef = useRef(true);
3714
- const isProgrammaticScrollRef = useRef(false);
3715
3803
  const previousResetKeyRef = useRef(null);
3716
3804
  const pendingInitialScrollRef = useRef(false);
3717
3805
  const scheduledScrollFrameRef = useRef(null);
@@ -3729,7 +3817,6 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3729
3817
  scheduledScrollFrameRef.current = null;
3730
3818
  const currentElement = scrollRef.current;
3731
3819
  if (!currentElement) return;
3732
- isProgrammaticScrollRef.current = true;
3733
3820
  if (typeof currentElement.scrollTo === "function") currentElement.scrollTo({
3734
3821
  top: currentElement.scrollHeight,
3735
3822
  behavior
@@ -3742,10 +3829,6 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3742
3829
  queueScrollToBottom();
3743
3830
  }, [queueScrollToBottom, updateStickyState]);
3744
3831
  const onScroll = useCallback(() => {
3745
- if (isProgrammaticScrollRef.current) {
3746
- isProgrammaticScrollRef.current = false;
3747
- return;
3748
- }
3749
3832
  const element = scrollRef.current;
3750
3833
  if (!element) return;
3751
3834
  updateStickyState(resolveIsAtBottom(element));
@@ -3803,15 +3886,23 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3803
3886
  * Collapsed process/tool rows should feel like ordinary body text lines.
3804
3887
  */
3805
3888
  const CHAT_PROCESS_LEADING_COL_CLASS = "inline-flex h-[1.15em] w-[1.15em] shrink-0 items-center justify-center text-current";
3806
- const CHAT_PROCESS_META_ROW_CLASS = "group/process-row flex w-full min-w-0 items-center gap-1.5 py-0 text-[0.925rem] font-normal leading-[1.72] text-muted-foreground";
3889
+ const CHAT_PROCESS_META_ROW_CLASS = "group/process-row flex w-full min-w-0 items-center gap-1.5 py-0 text-[0.925rem] font-normal leading-[1.72] text-muted-foreground/80";
3807
3890
  function ChatProcessLeadingIcon({ children, className }) {
3808
3891
  return /* @__PURE__ */ jsx("span", {
3809
3892
  className: cn(CHAT_PROCESS_LEADING_COL_CLASS, className),
3810
3893
  children
3811
3894
  });
3812
3895
  }
3896
+ function ChatProcessWorkflowRail({ position }) {
3897
+ return /* @__PURE__ */ jsx("span", {
3898
+ "aria-hidden": "true",
3899
+ "data-tool-workflow-rail": "true",
3900
+ className: cn("pointer-events-none absolute left-[0.575em] w-px -translate-x-1/2 bg-border/70", position === "first" ? "bottom-0 top-[0.86em]" : position === "last" ? "top-0 h-[0.86em]" : "inset-y-0")
3901
+ });
3902
+ }
3813
3903
  function ChatProcessMetaRow({ children, className, interactive = false, onClick, role, tabIndex, onKeyDown }) {
3814
3904
  return /* @__PURE__ */ jsx("div", {
3905
+ "data-chat-process-meta-row": "true",
3815
3906
  className: cn(CHAT_PROCESS_META_ROW_CLASS, interactive ? "cursor-pointer transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35" : null, className),
3816
3907
  onClick,
3817
3908
  role,
@@ -3827,22 +3918,33 @@ function ChatProcessMetaRow({ children, className, interactive = false, onClick,
3827
3918
  * Rendered as a plain button-like row (not <summary>) so browsers never inject
3828
3919
  * a default disclosure label such as "详情" / "Details".
3829
3920
  */
3830
- function ChatCollapsibleMetaSummary({ label, openGroup, open = false, className, labelClassName, onClick }) {
3921
+ function ChatCollapsibleMetaSummary({ label, openGroup, open = false, icon: Icon, className, leadingIconClassName, labelClassName, onClick }) {
3831
3922
  return /* @__PURE__ */ jsxs("button", {
3832
3923
  type: "button",
3924
+ "data-chat-process-meta-row": "true",
3833
3925
  className: cn(CHAT_PROCESS_META_ROW_CLASS, "m-0 w-full border-0 bg-transparent p-0 text-left shadow-none transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35", className),
3834
3926
  "aria-expanded": open,
3835
3927
  onClick,
3836
- children: [/* @__PURE__ */ jsx("span", {
3837
- className: cn("min-w-0 shrink truncate", labelClassName),
3838
- children: label
3839
- }), /* @__PURE__ */ jsx("span", {
3840
- className: cn("inline-flex h-[1.15em] w-[1.15em] shrink-0 items-center justify-center text-muted-foreground/80 transition-opacity", open ? "opacity-100" : "opacity-0 group-hover/process-row:opacity-100 focus-visible:opacity-100"),
3841
- children: /* @__PURE__ */ jsx(ChevronRight, {
3842
- className: cn("h-[1.05em] w-[1.05em] transition-transform", open && "rotate-90"),
3843
- strokeWidth: 2.25
3928
+ children: [
3929
+ Icon ? /* @__PURE__ */ jsx(ChatProcessLeadingIcon, {
3930
+ className: cn("relative z-[1] rounded-sm", leadingIconClassName),
3931
+ children: /* @__PURE__ */ jsx(Icon, {
3932
+ className: "h-[1.05em] w-[1.05em]",
3933
+ strokeWidth: 2.25
3934
+ })
3935
+ }) : null,
3936
+ /* @__PURE__ */ jsx("span", {
3937
+ className: cn("min-w-0 shrink truncate", labelClassName),
3938
+ children: label
3939
+ }),
3940
+ /* @__PURE__ */ jsx("span", {
3941
+ className: cn("inline-flex h-[1.15em] w-[1.15em] shrink-0 items-center justify-center text-muted-foreground/80 transition-opacity", open ? "opacity-100" : "opacity-0 group-hover/process-row:opacity-100 focus-visible:opacity-100"),
3942
+ children: /* @__PURE__ */ jsx(ChevronRight, {
3943
+ className: cn("h-[1.05em] w-[1.05em] transition-transform", open && "rotate-90"),
3944
+ strokeWidth: 2.25
3945
+ })
3844
3946
  })
3845
- })]
3947
+ ]
3846
3948
  });
3847
3949
  }
3848
3950
  //#endregion
@@ -3857,7 +3959,7 @@ function ChatReasoningBlock({ label, text, characterCountTemplates, isUser, isIn
3857
3959
  const displayLabel = formatReasoningLabel(label, text, isInProgress ? characterCountTemplates?.inProgress : characterCountTemplates?.completed);
3858
3960
  const { onScroll } = useStickyBottomScroll({
3859
3961
  scrollRef,
3860
- resetKey: `${displayLabel}:${isInProgress ? "streaming" : "idle"}`,
3962
+ resetKey: "reasoning",
3861
3963
  isLoading: false,
3862
3964
  hasContent: text.length > 0,
3863
3965
  contentVersion: text,
@@ -3868,6 +3970,8 @@ function ChatReasoningBlock({ label, text, characterCountTemplates, isUser, isIn
3868
3970
  children: [/* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
3869
3971
  openGroup: "reasoning",
3870
3972
  open: isOpen,
3973
+ icon: Brain,
3974
+ leadingIconClassName: isUser ? "bg-primary" : "bg-card",
3871
3975
  label: displayLabel,
3872
3976
  labelClassName: isUser ? "text-primary-100" : void 0,
3873
3977
  onClick: onSummaryClick
@@ -3902,6 +4006,38 @@ function ToolCardContent({ children, className }) {
3902
4006
  children
3903
4007
  });
3904
4008
  }
4009
+ function ToolCardDetailSection({ label, tone, children }) {
4010
+ const style = {
4011
+ input: {
4012
+ dot: "bg-muted-foreground/60",
4013
+ body: "text-foreground"
4014
+ },
4015
+ output: {
4016
+ dot: "bg-primary/70",
4017
+ body: "text-foreground"
4018
+ },
4019
+ error: {
4020
+ dot: "bg-rose-500/80",
4021
+ body: "text-rose-950/85"
4022
+ }
4023
+ }[tone];
4024
+ return /* @__PURE__ */ jsxs("section", {
4025
+ className: "overflow-hidden rounded-md border border-border/70 bg-muted/20",
4026
+ children: [/* @__PURE__ */ jsxs("div", {
4027
+ className: "flex items-center gap-2 border-b border-border/60 px-2.5 py-1.5 text-[10px] font-medium tracking-wide text-muted-foreground",
4028
+ children: [/* @__PURE__ */ jsx("span", { className: cn("h-1.5 w-1.5 rounded-full", style.dot) }), /* @__PURE__ */ jsx("span", {
4029
+ className: "normal-case tracking-normal",
4030
+ children: label
4031
+ })]
4032
+ }), /* @__PURE__ */ jsx("div", {
4033
+ className: "w-full overflow-hidden",
4034
+ children: /* @__PURE__ */ jsx("pre", {
4035
+ className: cn("w-full max-w-full min-w-0 max-h-64 overflow-x-auto overflow-y-auto px-2.5 py-2 font-mono text-[12px] leading-relaxed whitespace-pre custom-scrollbar", style.body),
4036
+ children
4037
+ })
4038
+ })]
4039
+ });
4040
+ }
3905
4041
  //#endregion
3906
4042
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-status.tsx
3907
4043
  const STATUS_STYLES = {
@@ -4803,9 +4939,12 @@ function TerminalExecutionView({ card, toolLabel }) {
4803
4939
  })] });
4804
4940
  }
4805
4941
  function FileOperationView({ card, toolLabel, onFileOpen }) {
4942
+ const input = card.input?.trim() ?? "";
4806
4943
  const output = card.output?.trim() ?? "";
4807
4944
  const isRunning = card.statusTone === "running";
4808
- const hasContent = Boolean(card.fileOperation?.blocks.length) || Boolean(output);
4945
+ const hasStructuredPreview = Boolean(card.fileOperation?.blocks.length);
4946
+ const showRawInput = Boolean(input) && !hasStructuredPreview;
4947
+ const hasContent = hasStructuredPreview || Boolean(input) || Boolean(output);
4809
4948
  const previewBlocks = card.fileOperation?.blocks ?? [];
4810
4949
  const previewLineCount = previewBlocks.reduce((count, block) => count + block.lines.length, 0);
4811
4950
  const previewCharCount = previewBlocks.reduce((count, block) => {
@@ -4830,13 +4969,21 @@ function FileOperationView({ card, toolLabel, onFileOpen }) {
4830
4969
  canExpand: hasContent || isRunning,
4831
4970
  hideSummary: false,
4832
4971
  onToggle
4833
- }), expanded && hasContent ? /* @__PURE__ */ jsx(ToolCardContent, {
4972
+ }), expanded && hasContent ? /* @__PURE__ */ jsxs(ToolCardContent, {
4834
4973
  className: "bg-transparent py-0",
4835
- children: /* @__PURE__ */ jsx(ToolCardFileOperationContent, {
4836
- card,
4837
- onFileOpen,
4838
- showPathRow: true
4839
- })
4974
+ children: [
4975
+ showRawInput ? /* @__PURE__ */ jsx(ToolCardDetailSection, {
4976
+ label: card.inputLabel?.trim() || "Input",
4977
+ tone: "input",
4978
+ children: input
4979
+ }) : null,
4980
+ showRawInput && output ? /* @__PURE__ */ jsx("div", { className: "h-2" }) : null,
4981
+ hasStructuredPreview || output ? /* @__PURE__ */ jsx(ToolCardFileOperationContent, {
4982
+ card,
4983
+ onFileOpen,
4984
+ showPathRow: true
4985
+ }) : null
4986
+ ]
4840
4987
  }) : null] });
4841
4988
  }
4842
4989
  function SearchSnippetView({ card, toolLabel, icon: Icon = Search }) {
@@ -4865,38 +5012,6 @@ function SearchSnippetView({ card, toolLabel, icon: Icon = Search }) {
4865
5012
  }
4866
5013
  //#endregion
4867
5014
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-generic.tsx
4868
- function GenericToolSection({ label, tone, children }) {
4869
- const style = {
4870
- input: {
4871
- dot: "bg-muted-foreground/60",
4872
- body: "text-foreground"
4873
- },
4874
- output: {
4875
- dot: "bg-primary/70",
4876
- body: "text-foreground"
4877
- },
4878
- error: {
4879
- dot: "bg-rose-500/80",
4880
- body: "text-rose-950/85"
4881
- }
4882
- }[tone];
4883
- return /* @__PURE__ */ jsxs("section", {
4884
- className: "overflow-hidden rounded-md border border-border/70 bg-muted/20",
4885
- children: [/* @__PURE__ */ jsxs("div", {
4886
- className: "flex items-center gap-2 border-b border-border/60 px-2.5 py-1.5 text-[10px] font-medium tracking-wide text-muted-foreground",
4887
- children: [/* @__PURE__ */ jsx("span", { className: cn("h-1.5 w-1.5 rounded-full", style.dot) }), /* @__PURE__ */ jsx("span", {
4888
- className: "normal-case tracking-normal",
4889
- children: label
4890
- })]
4891
- }), /* @__PURE__ */ jsx("div", {
4892
- className: "w-full overflow-hidden",
4893
- children: /* @__PURE__ */ jsx("pre", {
4894
- className: cn("w-full max-w-full min-w-0 max-h-64 overflow-x-auto overflow-y-auto px-2.5 py-2 font-mono text-[12px] leading-relaxed whitespace-pre custom-scrollbar", style.body),
4895
- children
4896
- })
4897
- })]
4898
- });
4899
- }
4900
5015
  function buildToolActionSlot(card, onToolAction, renderToolAgent) {
4901
5016
  const agentAction = card.agentId && renderToolAgent ? renderToolAgent(card.agentId) : null;
4902
5017
  const toolAction = card.action && onToolAction ? /* @__PURE__ */ jsx(ToolCardHeaderAction, {
@@ -4930,13 +5045,13 @@ function GenericToolCard({ card, toolLabel, icon: Icon = Wrench, onToolAction, r
4930
5045
  }), expanded && hasContent ? /* @__PURE__ */ jsxs(ToolCardContent, {
4931
5046
  className: "bg-transparent py-0",
4932
5047
  children: [
4933
- hasInputSection ? /* @__PURE__ */ jsx(GenericToolSection, {
5048
+ hasInputSection ? /* @__PURE__ */ jsx(ToolCardDetailSection, {
4934
5049
  label: card.inputLabel?.trim() || "Input",
4935
5050
  tone: "input",
4936
5051
  children: input
4937
5052
  }) : null,
4938
5053
  hasInputSection && hasOutputSection ? /* @__PURE__ */ jsx("div", { className: "h-2" }) : null,
4939
- hasOutputSection ? /* @__PURE__ */ jsx(GenericToolSection, {
5054
+ hasOutputSection ? /* @__PURE__ */ jsx(ToolCardDetailSection, {
4940
5055
  label: card.outputLabel?.trim() || "Output",
4941
5056
  tone: card.statusTone === "error" ? "error" : "output",
4942
5057
  children: output
@@ -5085,8 +5200,7 @@ function ChatToolCard({ card, toolStatusLabels, onToolAction, onFileOpen, render
5085
5200
  }
5086
5201
  //#endregion
5087
5202
  //#region src/components/chat/ui/chat-message-list/chat-tool-activity-group.tsx
5088
- function ChatToolActivityGroup({ group, isUser, reasoningCharacterCountTemplates, toolStatusLabels, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard }) {
5089
- const [open, setOpen] = useState(false);
5203
+ function ChatToolActivityGroup({ group, open, isUser, reasoningCharacterCountTemplates, toolStatusLabels, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard, onOpenChange }) {
5090
5204
  const toolCount = group.parts.filter((part) => part.type === "tool-card").length;
5091
5205
  const showWorkflowRail = open && toolCount > 1;
5092
5206
  return /* @__PURE__ */ jsxs("div", {
@@ -5094,34 +5208,29 @@ function ChatToolActivityGroup({ group, isUser, reasoningCharacterCountTemplates
5094
5208
  children: [/* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
5095
5209
  openGroup: "tool-activity",
5096
5210
  open,
5211
+ icon: Workflow,
5212
+ leadingIconClassName: "bg-card",
5097
5213
  label: group.label,
5098
- onClick: () => setOpen((current) => !current)
5214
+ onClick: () => onOpenChange(!open)
5099
5215
  }), open ? /* @__PURE__ */ jsx("div", {
5100
5216
  className: "text-[0.925rem] leading-[1.72]",
5101
5217
  children: group.parts.map((part, index) => {
5102
5218
  const isLast = index === group.parts.length - 1;
5103
5219
  return /* @__PURE__ */ jsxs("div", {
5104
5220
  className: "relative min-w-0",
5105
- children: [showWorkflowRail ? /* @__PURE__ */ jsx("div", {
5106
- "aria-hidden": "true",
5107
- "data-tool-workflow-rail": "true",
5108
- className: cn("pointer-events-none absolute left-[0.575em] w-px -translate-x-1/2 bg-border/70", index === 0 ? "bottom-0 top-[0.86em]" : isLast ? "top-0 h-[0.86em]" : "inset-y-0")
5109
- }) : null, part.type === "tool-card" ? /* @__PURE__ */ jsx(ChatToolCard, {
5221
+ children: [showWorkflowRail ? /* @__PURE__ */ jsx(ChatProcessWorkflowRail, { position: index === 0 ? "first" : isLast ? "last" : "middle" }) : null, part.type === "tool-card" ? /* @__PURE__ */ jsx(ChatToolCard, {
5110
5222
  card: part.card,
5111
5223
  toolStatusLabels,
5112
5224
  onToolAction,
5113
5225
  onFileOpen,
5114
5226
  renderToolAgent,
5115
5227
  renderPanelAppCard
5116
- }) : /* @__PURE__ */ jsx("div", {
5117
- className: "pl-[calc(1.15em+0.375rem)]",
5118
- children: /* @__PURE__ */ jsx(ChatReasoningBlock, {
5119
- label: part.label,
5120
- text: part.text,
5121
- characterCountTemplates: reasoningCharacterCountTemplates,
5122
- isUser,
5123
- isInProgress: false
5124
- })
5228
+ }) : /* @__PURE__ */ jsx(ChatReasoningBlock, {
5229
+ label: part.label,
5230
+ text: part.text,
5231
+ characterCountTemplates: reasoningCharacterCountTemplates,
5232
+ isUser,
5233
+ isInProgress: false
5125
5234
  })]
5126
5235
  }, `tool-group-item-${group.startIndex + index}`);
5127
5236
  })
@@ -5261,9 +5370,9 @@ function groupConsecutiveToolParts(parts, labels) {
5261
5370
  });
5262
5371
  blocks.push({
5263
5372
  kind: "tool-group",
5264
- key: `tool-group-${startIndex}-${index - 1}`,
5373
+ key: `tool-group-${startIndex}`,
5265
5374
  group: {
5266
- key: `tool-group-${startIndex}-${index - 1}`,
5375
+ key: `tool-group-${startIndex}`,
5267
5376
  startIndex,
5268
5377
  endIndex: index - 1,
5269
5378
  parts: groupedParts,
@@ -5347,7 +5456,7 @@ function splitAssistantProcess(message) {
5347
5456
  finalParts: message.parts.slice(lastProcessPartIndex + 1)
5348
5457
  };
5349
5458
  }
5350
- function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, part, renderInlineDisplay, renderPanelAppCard, renderToolAgent, role, texts }) {
5459
+ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, part, renderInlineDisplay, renderPanelAppCard, renderToolAgent, resolveFileContentUrl, role, texts }) {
5351
5460
  const { type } = part;
5352
5461
  if (type === "markdown") {
5353
5462
  const { inlineTokens, text } = part;
@@ -5358,6 +5467,7 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAtta
5358
5467
  inlineTokens,
5359
5468
  onFileOpen,
5360
5469
  onInlineTokenClick,
5470
+ resolveFileContentUrl,
5361
5471
  renderInlineDisplay
5362
5472
  }, `markdown-${index}`);
5363
5473
  }
@@ -5373,14 +5483,17 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAtta
5373
5483
  }
5374
5484
  if (type === "tool-card") {
5375
5485
  const { card } = part;
5376
- return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(ChatToolCard, {
5377
- card,
5378
- toolStatusLabels: texts.toolStatusLabels,
5379
- onToolAction,
5380
- onFileOpen,
5381
- renderToolAgent,
5382
- renderPanelAppCard
5383
- }) }, `tool-${index}`);
5486
+ return /* @__PURE__ */ jsxs("div", {
5487
+ className: "relative min-w-0",
5488
+ children: [/* @__PURE__ */ jsx(ChatProcessWorkflowRail, { position: "single" }), /* @__PURE__ */ jsx(ChatToolCard, {
5489
+ card,
5490
+ toolStatusLabels: texts.toolStatusLabels,
5491
+ onToolAction,
5492
+ onFileOpen,
5493
+ renderToolAgent,
5494
+ renderPanelAppCard
5495
+ })]
5496
+ }, `tool-${index}`);
5384
5497
  }
5385
5498
  if (type === "file") {
5386
5499
  const { file } = part;
@@ -5411,17 +5524,26 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAtta
5411
5524
  return null;
5412
5525
  }
5413
5526
  function renderMessageParts(params) {
5414
- const { isInProgress, isUser, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, parts, renderInlineDisplay, renderPanelAppCard, renderToolAgent, role, texts, indexOffset = 0 } = params;
5415
- return groupConsecutiveToolParts(parts, resolveToolActivityLabels(texts)).map((block) => {
5527
+ const { isInProgress, isUser, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolActivityOpenChange, onToolAction, openToolGroupKeys, parts, renderInlineDisplay, renderPanelAppCard, renderToolAgent, resolveFileContentUrl, role, texts, indexOffset = 0 } = params;
5528
+ return groupConsecutiveImageFileBlocks(groupConsecutiveToolParts(parts, resolveToolActivityLabels(texts))).map((block) => {
5529
+ if (block.kind === "image-group") return /* @__PURE__ */ jsx(ChatMessageImageRow, {
5530
+ group: block,
5531
+ indexOffset,
5532
+ isUser,
5533
+ texts,
5534
+ onOpen: onAttachmentOpen
5535
+ }, `${block.key}-${indexOffset}`);
5416
5536
  if (block.kind === "tool-group") return /* @__PURE__ */ jsx(ChatToolActivityGroup, {
5417
5537
  group: block.group,
5538
+ open: openToolGroupKeys.has(block.group.key),
5418
5539
  isUser,
5419
5540
  reasoningCharacterCountTemplates: texts.reasoningCharacterCountTemplates,
5420
5541
  toolStatusLabels: texts.toolStatusLabels,
5421
5542
  onToolAction,
5422
5543
  onFileOpen,
5423
5544
  renderToolAgent,
5424
- renderPanelAppCard
5545
+ renderPanelAppCard,
5546
+ onOpenChange: (open) => onToolActivityOpenChange(block.group.key, open)
5425
5547
  }, block.key);
5426
5548
  return renderChatMessagePart({
5427
5549
  part: block.part,
@@ -5435,20 +5557,33 @@ function renderMessageParts(params) {
5435
5557
  onFileOpen,
5436
5558
  onAttachmentOpen,
5437
5559
  onInlineTokenClick,
5560
+ resolveFileContentUrl,
5438
5561
  renderInlineDisplay,
5439
5562
  renderToolAgent,
5440
5563
  renderPanelAppCard
5441
5564
  });
5442
5565
  });
5443
5566
  }
5444
- const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, onFileOpen, onAttachmentOpen, onInlineTokenClick, renderInlineDisplay, renderToolAgent, renderPanelAppCard }) {
5567
+ const ChatMessage = memo(function ChatMessage({ layout, message, texts, onToolAction, onFileOpen, onAttachmentOpen, onInlineTokenClick, renderInlineDisplay, renderToolAgent, renderPanelAppCard, resolveFileContentUrl }) {
5445
5568
  const { role } = message;
5446
5569
  const isUser = role === "user";
5570
+ const isFlat = layout === "flat" && !isUser;
5447
5571
  const isInProgress = isMessageInProgress(message.status);
5448
5572
  const processSplit = splitAssistantProcess(message);
5449
5573
  const [processOpen, setProcessOpen] = useState(false);
5574
+ const [openToolGroupKeys, setOpenToolGroupKeys] = useState(() => /* @__PURE__ */ new Set());
5575
+ const handleToolActivityOpenChange = (groupKey, open) => {
5576
+ setOpenToolGroupKeys((current) => {
5577
+ const next = new Set(current);
5578
+ if (open) next.add(groupKey);
5579
+ else next.delete(groupKey);
5580
+ return next;
5581
+ });
5582
+ if (open) setProcessOpen(true);
5583
+ };
5450
5584
  return /* @__PURE__ */ jsx("div", {
5451
- className: cn("inline-block w-fit max-w-full rounded-2xl border px-4 shadow-sm has-[[data-chat-message-wide-content=true]]:w-full", isUser ? "nextclaw-chat-message-user rounded-[1.45rem] border-primary bg-primary py-2.5 text-primary-foreground shadow-none" : role === "assistant" ? "border-border bg-card pb-3 pt-4 text-card-foreground" : "border-border bg-muted/45 py-3 text-foreground"),
5585
+ "data-chat-message-surface": isFlat ? "flat" : "card",
5586
+ className: cn("max-w-full has-[[data-chat-message-wide-content=true]]:w-full", isFlat ? "block w-full text-foreground" : "inline-block w-fit rounded-2xl border px-4 shadow-sm", !isFlat && (isUser ? "nextclaw-chat-message-user rounded-[1.45rem] border-primary bg-primary py-2.5 text-primary-foreground shadow-none" : role === "assistant" ? "border-border bg-card pb-3 pt-4 text-card-foreground" : "border-border bg-muted/45 py-3 text-foreground")),
5452
5587
  children: /* @__PURE__ */ jsx("div", {
5453
5588
  className: "space-y-0",
5454
5589
  children: processSplit ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
@@ -5458,6 +5593,8 @@ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, on
5458
5593
  children: /* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
5459
5594
  openGroup: "process",
5460
5595
  open: processOpen,
5596
+ icon: ListChecks,
5597
+ leadingIconClassName: "bg-card",
5461
5598
  label: message.processSummary?.label,
5462
5599
  onClick: () => setProcessOpen((current) => !current)
5463
5600
  })
@@ -5473,6 +5610,9 @@ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, on
5473
5610
  onFileOpen,
5474
5611
  onAttachmentOpen,
5475
5612
  onInlineTokenClick,
5613
+ resolveFileContentUrl,
5614
+ openToolGroupKeys,
5615
+ onToolActivityOpenChange: handleToolActivityOpenChange,
5476
5616
  renderInlineDisplay,
5477
5617
  renderToolAgent,
5478
5618
  renderPanelAppCard
@@ -5488,6 +5628,9 @@ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, on
5488
5628
  onFileOpen,
5489
5629
  onAttachmentOpen,
5490
5630
  onInlineTokenClick,
5631
+ resolveFileContentUrl,
5632
+ openToolGroupKeys,
5633
+ onToolActivityOpenChange: handleToolActivityOpenChange,
5491
5634
  renderInlineDisplay,
5492
5635
  renderToolAgent,
5493
5636
  renderPanelAppCard,
@@ -5502,6 +5645,9 @@ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, on
5502
5645
  onFileOpen,
5503
5646
  onAttachmentOpen,
5504
5647
  onInlineTokenClick,
5648
+ resolveFileContentUrl,
5649
+ openToolGroupKeys,
5650
+ onToolActivityOpenChange: handleToolActivityOpenChange,
5505
5651
  renderInlineDisplay,
5506
5652
  renderToolAgent,
5507
5653
  renderPanelAppCard
@@ -5542,7 +5688,6 @@ const TYPING_TEXT_SHEEN_CSS = `
5542
5688
  background-position: -60% 0;
5543
5689
  }
5544
5690
  }
5545
-
5546
5691
  .nextclaw-chat-typing-indicator__text {
5547
5692
  background-image: linear-gradient(
5548
5693
  100deg,
@@ -5596,16 +5741,17 @@ function ChatMessageTypingFooter() {
5596
5741
  })
5597
5742
  });
5598
5743
  }
5599
- function ChatTypingIndicator({ label }) {
5744
+ function ChatTypingIndicator({ label, layout }) {
5600
5745
  return /* @__PURE__ */ jsxs("div", {
5601
- className: "rounded-2xl border border-border bg-card px-4 py-3 text-sm text-muted-foreground shadow-sm",
5746
+ "data-chat-message-surface": layout === "flat" ? "flat" : "card",
5747
+ className: cn("text-sm text-muted-foreground", layout === "flat" ? "py-1" : "rounded-2xl border border-border bg-card px-4 py-3 shadow-sm"),
5602
5748
  children: [/* @__PURE__ */ jsx("style", { children: TYPING_TEXT_SHEEN_CSS }), /* @__PURE__ */ jsx("span", {
5603
5749
  className: "nextclaw-chat-typing-indicator__text",
5604
5750
  children: label
5605
5751
  })]
5606
5752
  });
5607
5753
  }
5608
- function ChatMessageList({ className, isSending, messages, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, renderInlineDisplay, renderPanelAppCard, renderToolAgent, texts }) {
5754
+ function ChatMessageList({ className, isSending, layout = "card", messages, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, renderInlineDisplay, renderPanelAppCard, renderToolAgent, resolveFileContentUrl, texts }) {
5609
5755
  const visibleMessages = messages.filter(hasRenderableMessageContent);
5610
5756
  const hasRenderableAssistantDraft = visibleMessages.some((message) => message.role === "assistant" && (message.status === "streaming" || message.status === "pending"));
5611
5757
  return /* @__PURE__ */ jsxs("div", {
@@ -5613,23 +5759,63 @@ function ChatMessageList({ className, isSending, messages, onAttachmentOpen, onF
5613
5759
  children: [visibleMessages.map((message) => {
5614
5760
  const isUser = message.role === "user";
5615
5761
  const isGenerating = !isUser && (message.status === "streaming" || message.status === "pending");
5762
+ const content = /* @__PURE__ */ jsx(ChatMessage, {
5763
+ layout,
5764
+ message,
5765
+ texts,
5766
+ onToolAction,
5767
+ onFileOpen,
5768
+ onAttachmentOpen,
5769
+ onInlineTokenClick,
5770
+ resolveFileContentUrl,
5771
+ renderInlineDisplay,
5772
+ renderToolAgent,
5773
+ renderPanelAppCard
5774
+ });
5775
+ if (layout === "flat" && !isUser) return /* @__PURE__ */ jsxs("article", {
5776
+ "data-chat-message-layout": "flat",
5777
+ className: "w-full min-w-0 space-y-2",
5778
+ children: [
5779
+ /* @__PURE__ */ jsxs("div", {
5780
+ "data-chat-message-header": "flat",
5781
+ className: "flex min-w-0 items-center gap-2.5",
5782
+ children: [/* @__PURE__ */ jsx(ChatMessageAvatar, {
5783
+ role: message.role,
5784
+ size: "compact"
5785
+ }), /* @__PURE__ */ jsx("span", {
5786
+ className: "truncate text-sm font-semibold text-foreground",
5787
+ children: message.roleLabel
5788
+ })]
5789
+ }),
5790
+ /* @__PURE__ */ jsx("div", {
5791
+ "data-chat-message-body": "flat",
5792
+ className: "min-w-0 w-full",
5793
+ children: content
5794
+ }),
5795
+ isGenerating ? /* @__PURE__ */ jsx("div", {
5796
+ className: "w-full",
5797
+ children: /* @__PURE__ */ jsx(ChatMessageTypingFooter, {})
5798
+ }) : /* @__PURE__ */ jsxs("div", {
5799
+ "data-chat-message-footer": "flat",
5800
+ className: "flex items-center gap-2",
5801
+ children: [/* @__PURE__ */ jsx("span", {
5802
+ className: "px-1 text-[11px] leading-4 text-muted-foreground",
5803
+ children: message.timestampLabel
5804
+ }), /* @__PURE__ */ jsx(ChatMessageActionCopy, {
5805
+ message,
5806
+ texts
5807
+ })]
5808
+ })
5809
+ ]
5810
+ }, message.id);
5616
5811
  return /* @__PURE__ */ jsxs("div", {
5812
+ "data-chat-message-layout": "card",
5617
5813
  className: cn("flex gap-3", isUser ? "justify-end" : "justify-start"),
5618
5814
  children: [
5619
5815
  !isUser ? /* @__PURE__ */ jsx(ChatMessageAvatar, { role: message.role }) : null,
5620
5816
  /* @__PURE__ */ jsxs("div", {
5621
5817
  className: cn("w-fit max-w-[92%] space-y-2 has-[[data-chat-message-wide-content=true]]:w-full", isUser && "flex flex-col items-end"),
5622
- children: [/* @__PURE__ */ jsx(ChatMessage, {
5623
- message,
5624
- texts,
5625
- onToolAction,
5626
- onFileOpen,
5627
- onAttachmentOpen,
5628
- onInlineTokenClick,
5629
- renderInlineDisplay,
5630
- renderToolAgent,
5631
- renderPanelAppCard
5632
- }), /* @__PURE__ */ jsx("div", {
5818
+ children: [content, /* @__PURE__ */ jsx("div", {
5633
5819
  className: cn("flex items-center gap-2", isUser && "justify-end"),
5634
5820
  children: isGenerating ? /* @__PURE__ */ jsx(ChatMessageTypingFooter, {}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
5635
5821
  className: cn("px-1 text-[11px] leading-4 text-muted-foreground", isUser ? "text-right" : "text-left"),
@@ -5648,8 +5834,15 @@ function ChatMessageList({ className, isSending, messages, onAttachmentOpen, onF
5648
5834
  ]
5649
5835
  }, message.id);
5650
5836
  }), isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs("div", {
5837
+ "data-chat-message-layout": layout,
5651
5838
  className: "flex justify-start gap-3",
5652
- children: [/* @__PURE__ */ jsx(ChatMessageAvatar, { role: "assistant" }), /* @__PURE__ */ jsx(ChatTypingIndicator, { label: texts.typingLabel })]
5839
+ children: [/* @__PURE__ */ jsx(ChatMessageAvatar, {
5840
+ role: "assistant",
5841
+ size: layout === "flat" ? "compact" : "default"
5842
+ }), /* @__PURE__ */ jsx(ChatTypingIndicator, {
5843
+ label: texts.typingLabel,
5844
+ layout
5845
+ })]
5653
5846
  }) : null]
5654
5847
  });
5655
5848
  }