@nextclaw/agent-chat-ui 0.6.2 → 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, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, ExternalLink, Eye, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, Globe, ImageIcon, Loader2, Minus, Paperclip, Puzzle, Search, Sparkles, Star, Terminal, User, Wrench } 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";
@@ -29,6 +29,7 @@ import sql from "highlight.js/lib/languages/sql";
29
29
  import typescript from "highlight.js/lib/languages/typescript";
30
30
  import xml from "highlight.js/lib/languages/xml";
31
31
  import yaml from "highlight.js/lib/languages/yaml";
32
+ import { createPortal } from "react-dom";
32
33
  const CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC = {
33
34
  key: "slash",
34
35
  marker: "/"
@@ -512,7 +513,7 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
512
513
  children: [/* @__PURE__ */ jsxs("div", {
513
514
  className: "flex min-h-0 flex-col border-r border-gray-200",
514
515
  children: [!isLoading && filterViews.length > 0 ? /* @__PURE__ */ jsx("div", {
515
- 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",
516
517
  children: filterViews.map(({ count, key, label }) => {
517
518
  const isActive = key === resolvedActiveFilterKey;
518
519
  return /* @__PURE__ */ jsxs("button", {
@@ -520,9 +521,9 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
520
521
  "aria-pressed": isActive,
521
522
  onPointerDown: (event) => event.preventDefault(),
522
523
  onClick: () => handleFilterSelect(key),
523
- 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"}`,
524
525
  children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
525
- className: isActive ? "text-primary/70" : "text-gray-400",
526
+ className: isActive ? "text-gray-500" : "text-gray-400",
526
527
  children: count
527
528
  })]
528
529
  }, key);
@@ -531,58 +532,52 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
531
532
  ref: listRef,
532
533
  role: "listbox",
533
534
  "aria-label": texts.sectionLabel,
534
- 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"}`,
535
536
  children: isLoading ? /* @__PURE__ */ jsx("div", {
536
537
  className: "p-2 text-xs text-gray-500",
537
538
  children: texts.loadingLabel
538
539
  }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [!hasItemSections ? /* @__PURE__ */ jsx("div", {
539
- 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",
540
541
  children: texts.sectionLabel
541
542
  }) : null, visibleItems.length === 0 ? /* @__PURE__ */ jsx("div", {
542
- className: "px-2 text-xs text-gray-400",
543
+ className: "px-1.5 text-xs text-gray-400",
543
544
  children: texts.emptyLabel
544
- }) : /* @__PURE__ */ jsx("div", {
545
- className: "space-y-1",
546
- children: visibleItems.map((item, index) => {
547
- const { key, sectionKey, sectionLabel, title, subtitle } = item;
548
- const isActive = index === activeIndexInRange;
549
- const previousItem = visibleItems[index - 1];
550
- return /* @__PURE__ */ jsxs("div", {
551
- className: "space-y-1",
552
- children: [hasItemSections && Boolean(sectionLabel?.trim()) && previousItem?.sectionKey !== sectionKey ? /* @__PURE__ */ jsx("div", {
553
- className: "px-2 pb-0.5 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
554
- children: sectionLabel
555
- }) : null, /* @__PURE__ */ jsxs("button", {
556
- type: "button",
557
- role: "option",
558
- "aria-selected": isActive,
559
- "data-input-surface-index": index,
560
- onPointerMove: (event) => {
561
- if (event.pointerType !== "touch") setActiveIndexForCurrentItems(index);
562
- },
563
- onPointerDown: (event) => {
564
- if (event.button > 0) return;
565
- event.preventDefault();
566
- onSelectItem(item);
567
- },
568
- onClick: (event) => {
569
- if (event.detail === 0) onSelectItem(item);
570
- },
571
- 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"}`,
572
- children: [/* @__PURE__ */ jsx("span", {
573
- className: "truncate text-xs font-semibold",
574
- children: title
575
- }), /* @__PURE__ */ jsx("span", {
576
- className: "truncate text-xs text-gray-500",
577
- children: subtitle
578
- })]
579
- })]
580
- }, key);
581
- })
582
- })] })
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
+ }) })] })
583
578
  })]
584
579
  }), /* @__PURE__ */ jsx("div", {
585
- 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",
586
581
  onPointerDown: onDetailsPointerDown,
587
582
  children: activeItem ? /* @__PURE__ */ jsxs("div", {
588
583
  className: "space-y-3",
@@ -639,15 +634,13 @@ function resolveInputSurfaceTriggerIdentity(currentIdentity, trigger, reason) {
639
634
  function ChatInputSurfaceHost(props) {
640
635
  const { children, inputSurface, onInputSurfaceTriggerChange, onSelectItem, triggerSpecs = [CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC] } = props;
641
636
  const menuRef = useRef(null);
637
+ const isDetailsInteractionRef = useRef(false);
642
638
  const [activeTriggerIdentity, setActiveTriggerIdentity] = useState(null);
643
639
  const isOpen = Boolean(inputSurface) && activeTriggerIdentity !== null;
644
640
  const setActiveTrigger = useCallback((identity, trigger) => {
645
641
  setActiveTriggerIdentity(identity);
646
642
  onInputSurfaceTriggerChange?.(identity ? trigger : null);
647
643
  }, [onInputSurfaceTriggerChange]);
648
- const closeInputSurface = useCallback(() => {
649
- setActiveTrigger(null, null);
650
- }, [setActiveTrigger]);
651
644
  const handleInputSurfaceSnapshotChange = useCallback((nodes, selection, reason) => {
652
645
  const trigger = resolveChatComposerActiveInputSurfaceTrigger(nodes, selection, triggerSpecs);
653
646
  const nextIdentity = resolveInputSurfaceTriggerIdentity(activeTriggerIdentity, trigger, reason);
@@ -661,8 +654,8 @@ function ChatInputSurfaceHost(props) {
661
654
  return menuRef.current?.handleKeyDown(event) ?? false;
662
655
  }, []);
663
656
  const handleInputSurfaceOpenChange = useCallback((open) => {
664
- if (!open) closeInputSurface();
665
- }, [closeInputSurface]);
657
+ if (!open && !isDetailsInteractionRef.current) setActiveTrigger(null, null);
658
+ }, [setActiveTrigger]);
666
659
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [children(useMemo(() => ({
667
660
  onInputSurfaceKeyDown: handleInputSurfaceKeyDown,
668
661
  onInputSurfaceOpenChange: handleInputSurfaceOpenChange,
@@ -679,16 +672,21 @@ function ChatInputSurfaceHost(props) {
679
672
  items: inputSurface.items,
680
673
  texts: inputSurface.texts,
681
674
  onSelectItem: (item) => {
682
- closeInputSurface();
675
+ setActiveTrigger(null, null);
683
676
  onSelectItem(item);
684
677
  },
685
678
  onOpenChange: handleInputSurfaceOpenChange,
686
- onDetailsPointerDown: (event) => event.preventDefault()
679
+ onDetailsPointerDown: () => {
680
+ isDetailsInteractionRef.current = true;
681
+ requestAnimationFrame(() => {
682
+ isDetailsInteractionRef.current = false;
683
+ });
684
+ }
687
685
  }, activeTriggerIdentity) : null] });
688
686
  }
689
687
  //#endregion
690
688
  //#region src/components/chat/default-skin/button.tsx
691
- const buttonVariants = cva("inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-all duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", {
689
+ const buttonVariants = cva("inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium transition-all duration-fast focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:pointer-events-none disabled:opacity-50", {
692
690
  variants: {
693
691
  variant: {
694
692
  default: "bg-primary text-primary-foreground hover:bg-primary-600 active:bg-primary-700 shadow-sm",
@@ -726,12 +724,6 @@ const ChatButton = React.forwardRef(({ className, variant, size, ...props }, ref
726
724
  ChatButton.displayName = "ChatButton";
727
725
  //#endregion
728
726
  //#region src/components/chat/ui/chat-input-bar/chat-input-bar-actions.tsx
729
- const SEND_ERROR_PREVIEW_MAX_CHARS = 120;
730
- function buildSendErrorPreview(value) {
731
- const compact = value.replace(/\s+/g, " ").trim();
732
- if (compact.length <= SEND_ERROR_PREVIEW_MAX_CHARS) return compact;
733
- return `${compact.slice(0, SEND_ERROR_PREVIEW_MAX_CHARS - 1)}…`;
734
- }
735
727
  function StopIcon() {
736
728
  return /* @__PURE__ */ jsx("span", {
737
729
  "aria-hidden": "true",
@@ -781,75 +773,42 @@ function ContextWindowIndicator({ contextWindow }) {
781
773
  })] })
782
774
  });
783
775
  }
784
- function ChatInputBarActions({ sendError, sendErrorDetailsLabel, isSending, canStopGeneration, sendDisabled, stopDisabled, stopHint, sendButtonLabel, stopButtonLabel, contextWindow, onSend, onStop }) {
785
- const { Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
786
- const normalizedSendError = sendError?.trim() ?? "";
787
- const sendErrorPreview = normalizedSendError ? buildSendErrorPreview(normalizedSendError) : "";
788
- 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;
789
778
  return /* @__PURE__ */ jsxs("div", {
790
- className: "flex shrink-0 flex-col items-end gap-1",
791
- children: [normalizedSendError ? /* @__PURE__ */ jsxs("div", {
792
- className: "flex max-w-[420px] items-start justify-end gap-2 text-right",
793
- children: [/* @__PURE__ */ jsx("div", {
794
- className: "min-w-0 flex-1 text-[11px] text-red-600",
795
- children: /* @__PURE__ */ jsx("span", {
796
- className: "block truncate",
797
- title: normalizedSendError,
798
- children: sendErrorPreview
799
- })
800
- }), /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(PopoverTrigger, {
801
- asChild: true,
802
- children: /* @__PURE__ */ jsx("button", {
803
- type: "button",
804
- 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",
805
- children: resolvedSendErrorDetailsLabel
806
- })
807
- }), /* @__PURE__ */ jsxs(PopoverContent, {
808
- align: "end",
809
- className: "w-[min(32rem,calc(100vw-1.5rem))] border-red-100/80 p-0",
810
- children: [/* @__PURE__ */ jsx("div", {
811
- className: "border-b border-red-100 bg-red-50/80 px-4 py-2 text-xs font-semibold text-red-700",
812
- children: resolvedSendErrorDetailsLabel
813
- }), /* @__PURE__ */ jsx("pre", {
814
- className: "max-h-80 overflow-auto whitespace-pre-wrap break-words px-4 py-3 text-xs leading-relaxed text-red-700",
815
- children: normalizedSendError
816
- })]
817
- })] })]
818
- }) : null, /* @__PURE__ */ jsxs("div", {
819
- className: "flex items-center gap-2",
820
- children: [contextWindow ? /* @__PURE__ */ jsx(ContextWindowIndicator, { contextWindow }) : null, !isSending || !sendDisabled ? /* @__PURE__ */ jsx(ChatButton, {
821
- size: "icon",
822
- className: "h-8 w-8 rounded-full",
823
- "aria-label": sendButtonLabel,
824
- onClick: () => void onSend(),
825
- disabled: sendDisabled,
826
- children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-5 w-5" })
827
- }) : 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, {
828
798
  size: "icon",
829
799
  variant: "outline",
830
800
  className: "h-8 w-8 rounded-full",
831
801
  "aria-label": stopButtonLabel,
832
- onClick: () => void onStop(),
833
- disabled: stopDisabled,
802
+ disabled: true,
834
803
  children: /* @__PURE__ */ jsx(StopIcon, {})
835
- }) : /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
836
- asChild: true,
837
- children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(ChatButton, {
838
- size: "icon",
839
- variant: "outline",
840
- className: "h-8 w-8 rounded-full",
841
- "aria-label": stopButtonLabel,
842
- disabled: true,
843
- children: /* @__PURE__ */ jsx(StopIcon, {})
844
- }) })
845
- }), /* @__PURE__ */ jsx(TooltipContent, {
846
- side: "top",
847
- children: /* @__PURE__ */ jsx("p", {
848
- className: "text-xs",
849
- children: stopHint
850
- })
851
- })] }) })]
852
- })]
804
+ }) })
805
+ }), /* @__PURE__ */ jsx(TooltipContent, {
806
+ side: "top",
807
+ children: /* @__PURE__ */ jsx("p", {
808
+ className: "text-xs",
809
+ children: stopHint
810
+ })
811
+ })] }) })]
853
812
  });
854
813
  }
855
814
  //#endregion
@@ -1159,16 +1118,16 @@ function ToolbarSearchableSelect({ item }) {
1159
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" })]
1160
1119
  })
1161
1120
  }), /* @__PURE__ */ jsxs(PopoverContent, {
1162
- 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] ?? ""}`,
1163
1122
  style: { maxHeight: TOOLBAR_POPOVER_MAX_HEIGHT },
1164
1123
  children: [
1165
1124
  /* @__PURE__ */ jsxs("div", {
1166
- className: "relative mb-2 shrink-0",
1125
+ className: "relative mb-1.5 shrink-0",
1167
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, {
1168
1127
  value: query,
1169
1128
  onChange: (event) => setQuery(event.currentTarget.value),
1170
1129
  placeholder: item.search?.placeholder ?? item.placeholder,
1171
- className: "h-8 rounded-lg pl-8 text-xs"
1130
+ className: "h-7 rounded-md pl-8 text-xs"
1172
1131
  })]
1173
1132
  }),
1174
1133
  !hasOptions ? item.loading ? /* @__PURE__ */ jsxs("div", {
@@ -1189,9 +1148,9 @@ function ToolbarSearchableSelect({ item }) {
1189
1148
  /* @__PURE__ */ jsx("div", {
1190
1149
  className: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
1191
1150
  children: filteredGroups.map((group, groupIndex) => /* @__PURE__ */ jsxs("div", {
1192
- className: groupIndex > 0 ? "border-t border-border pt-1" : void 0,
1151
+ className: groupIndex > 0 ? "border-t border-border pt-0.5" : void 0,
1193
1152
  children: [group.label ? /* @__PURE__ */ jsx("div", {
1194
- 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",
1195
1154
  children: group.label
1196
1155
  }) : null, group.options.map((option) => {
1197
1156
  const isSelected = item.value === option.value;
@@ -1201,7 +1160,7 @@ function ToolbarSearchableSelect({ item }) {
1201
1160
  className: "group flex items-center gap-1 rounded-md hover:bg-accent",
1202
1161
  children: [/* @__PURE__ */ jsxs("button", {
1203
1162
  type: "button",
1204
- 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",
1205
1164
  onClick: () => {
1206
1165
  item.onValueChange(option.value);
1207
1166
  setOpen(false);
@@ -1268,7 +1227,7 @@ function ToolbarSelect({ item }) {
1268
1227
  children: item.emptyLabel
1269
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, {
1270
1229
  value: option.value,
1271
- className: "py-2",
1230
+ className: "py-1.5",
1272
1231
  children: /* @__PURE__ */ jsx(ToolbarSelectOptionContent, { option })
1273
1232
  }, option.value))] })] }, group.key))]
1274
1233
  })]
@@ -2293,6 +2252,11 @@ const ChatInputBarTokenizedComposer = forwardRef(function ChatInputBarTokenizedC
2293
2252
  ChatInputBarTokenizedComposer.displayName = "LexicalChatInputBarTokenizedComposer";
2294
2253
  //#endregion
2295
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
+ }
2296
2260
  function InputBarHint({ hint }) {
2297
2261
  if (!hint) return null;
2298
2262
  if (hint.loading) return /* @__PURE__ */ jsx("div", {
@@ -2315,7 +2279,44 @@ function InputBarHint({ hint }) {
2315
2279
  })
2316
2280
  });
2317
2281
  }
2318
- 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) {
2319
2320
  const composerRef = useRef(null);
2320
2321
  const resolvedInputSurface = inputSurface ?? (slashMenu ? {
2321
2322
  isLoading: slashMenu.isLoading,
@@ -2383,6 +2384,10 @@ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSur
2383
2384
  })
2384
2385
  }),
2385
2386
  /* @__PURE__ */ jsx(InputBarHint, { hint }),
2387
+ /* @__PURE__ */ jsx(ChatInputBarSendError, {
2388
+ sendError,
2389
+ sendErrorDetailsLabel
2390
+ }),
2386
2391
  /* @__PURE__ */ jsx(ChatInputBarToolbar, { ...toolbar })
2387
2392
  ]
2388
2393
  })
@@ -2392,22 +2397,25 @@ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSur
2392
2397
  ChatInputBar.displayName = "ChatInputBar";
2393
2398
  //#endregion
2394
2399
  //#region src/components/chat/ui/chat-message-list/chat-message-avatar.tsx
2395
- 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";
2396
2404
  if (role === "user") return /* @__PURE__ */ jsx("div", {
2397
2405
  "data-testid": "chat-message-avatar-user",
2398
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm",
2399
- 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 })
2400
2408
  });
2401
2409
  if (role === "tool") return /* @__PURE__ */ jsx("div", {
2402
2410
  "data-testid": "chat-message-avatar-tool",
2403
- 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",
2404
- 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 })
2405
2413
  });
2406
2414
  return /* @__PURE__ */ jsx("div", {
2407
2415
  "data-testid": "chat-message-avatar-assistant",
2408
- 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),
2409
2417
  children: /* @__PURE__ */ jsx(Bot, {
2410
- className: "h-[18px] w-[18px] text-current",
2418
+ className: cn(compact ? "h-4 w-4" : "h-[18px] w-[18px]", "text-current"),
2411
2419
  strokeWidth: 2.5
2412
2420
  })
2413
2421
  });
@@ -2419,15 +2427,15 @@ function resolveInlineTokenTone(kind) {
2419
2427
  if (kind === "panel_app") return "panel_app";
2420
2428
  return "default";
2421
2429
  }
2422
- function resolveInlineTokenBadgeClassName(tone, isUser) {
2423
- if (tone === "skill") return isUser ? "border-primary-foreground/30 bg-primary-foreground/18 text-primary-foreground" : "border-border bg-accent text-accent-foreground";
2424
- if (tone === "panel_app") return isUser ? "border-primary-foreground/30 bg-primary-foreground/18 text-primary-foreground" : "border-border bg-accent text-accent-foreground";
2425
- return isUser ? "border-primary-foreground/30 bg-primary-foreground/18 text-primary-foreground" : "border-border bg-muted text-muted-foreground";
2430
+ function resolveInlineTokenBadgeClassName(tone, isUser, interactive) {
2431
+ if (tone === "skill") return cn("border-transparent bg-transparent text-[color:var(--md-link)]", interactive && "cursor-pointer underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[color:var(--md-link)]/30", !interactive && "cursor-default");
2432
+ if (tone === "panel_app") return isUser ? "border-primary-foreground/40 bg-primary-foreground/22 text-primary-foreground" : "border-border bg-muted text-foreground";
2433
+ return isUser ? "border-primary-foreground/40 bg-primary-foreground/22 text-primary-foreground" : "border-border bg-muted text-muted-foreground";
2426
2434
  }
2427
2435
  function resolveInlineTokenIconClassName(tone, isUser) {
2428
- if (tone === "skill") return isUser ? "text-primary-foreground/70" : "text-muted-foreground";
2429
- if (tone === "panel_app") return isUser ? "text-primary-foreground/70" : "text-muted-foreground";
2430
- return isUser ? "text-primary-foreground/70" : "text-muted-foreground";
2436
+ if (tone === "skill") return "text-current opacity-80";
2437
+ if (tone === "panel_app") return isUser ? "text-primary-foreground/80" : "text-muted-foreground";
2438
+ return isUser ? "text-primary-foreground/80" : "text-muted-foreground";
2431
2439
  }
2432
2440
  function renderInlineTokenIcon(tone) {
2433
2441
  return tone === "panel_app" ? /* @__PURE__ */ jsx(AppWindow, {
@@ -2438,18 +2446,32 @@ function renderInlineTokenIcon(tone) {
2438
2446
  className: "h-3 w-3"
2439
2447
  });
2440
2448
  }
2441
- function ChatInlineTokenBadge({ kind, label, isUser }) {
2449
+ function ChatInlineTokenBadge({ kind, label, isUser, onClick }) {
2442
2450
  const tone = resolveInlineTokenTone(kind);
2443
- return /* @__PURE__ */ jsxs("span", {
2444
- className: cn("mx-[2px] inline-flex h-7 max-w-full items-center gap-1.5 rounded-xl border px-2.5 align-baseline text-[11px] font-medium shadow-[0_0_0_1px_rgba(15,23,42,0.03)]", resolveInlineTokenBadgeClassName(tone, isUser)),
2451
+ const interactive = Boolean(onClick);
2452
+ const className = cn("nextclaw-chat-inline-token mx-[2px] inline-flex max-w-full items-center gap-1 align-baseline text-[11px] font-medium", tone === "skill" ? "h-auto rounded-none px-0 py-0" : "h-7 rounded-xl border px-2.5", resolveInlineTokenBadgeClassName(tone, isUser, interactive));
2453
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
2454
+ className: cn("inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center", resolveInlineTokenIconClassName(tone, isUser)),
2455
+ children: renderInlineTokenIcon(tone)
2456
+ }), /* @__PURE__ */ jsx("span", {
2457
+ className: "truncate",
2458
+ children: label
2459
+ })] });
2460
+ if (interactive) return /* @__PURE__ */ jsx("button", {
2461
+ type: "button",
2462
+ className,
2445
2463
  title: label,
2446
- children: [/* @__PURE__ */ jsx("span", {
2447
- className: cn("inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center", resolveInlineTokenIconClassName(tone, isUser)),
2448
- children: renderInlineTokenIcon(tone)
2449
- }), /* @__PURE__ */ jsx("span", {
2450
- className: "truncate",
2451
- children: label
2452
- })]
2464
+ onClick: (event) => {
2465
+ event.preventDefault();
2466
+ event.stopPropagation();
2467
+ onClick?.();
2468
+ },
2469
+ children: content
2470
+ });
2471
+ return /* @__PURE__ */ jsx("span", {
2472
+ className,
2473
+ title: label,
2474
+ children: content
2453
2475
  });
2454
2476
  }
2455
2477
  //#endregion
@@ -2655,7 +2677,7 @@ const REGISTERED_LANGUAGES = [
2655
2677
  aliases: ["patch"]
2656
2678
  }
2657
2679
  ];
2658
- function escapeHtml(value) {
2680
+ function escapeHtml$1(value) {
2659
2681
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2660
2682
  }
2661
2683
  function normalizeLanguage(value) {
@@ -2686,7 +2708,7 @@ var ChatCodeSyntaxHighlighter = class {
2686
2708
  return this.createPlainTextResult(code, normalizedLanguage || "text");
2687
2709
  };
2688
2710
  this.createPlainTextResult = (code, language) => ({
2689
- html: escapeHtml(code),
2711
+ html: escapeHtml$1(code),
2690
2712
  language,
2691
2713
  highlighted: false
2692
2714
  });
@@ -2914,26 +2936,126 @@ function ChatInlineDisplay({ display, renderInlineDisplay }) {
2914
2936
  return /* @__PURE__ */ jsx(ChatInlineDisplayFallback, { display });
2915
2937
  }
2916
2938
  //#endregion
2917
- //#region src/components/chat/ui/chat-message-list/chat-message-markdown.tsx
2918
- 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
2919
3045
  const SAFE_LINK_PROTOCOLS = new Set([
2920
3046
  "http:",
2921
3047
  "https:",
2922
3048
  "mailto:",
2923
3049
  "tel:"
2924
3050
  ]);
2925
- const INLINE_TOKEN_KIND_ATTR = "data-chat-inline-token-kind";
2926
- const INLINE_TOKEN_KEY_ATTR = "data-chat-inline-token-key";
2927
- const INLINE_TOKEN_LABEL_ATTR = "data-chat-inline-token-label";
2928
- const INLINE_TOKEN_RAW_TEXT_ATTR = "data-chat-inline-token-raw-text";
2929
- 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");
2930
- function trimMarkdown(value) {
2931
- if (value.length <= MARKDOWN_MAX_CHARS) return value;
2932
- 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);
2933
3055
  }
2934
- function resolveSafeHref(href) {
3056
+ function resolveSafeChatResourceHref(href) {
2935
3057
  if (!href) return null;
2936
- 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;
2937
3059
  try {
2938
3060
  const url = new URL(href);
2939
3061
  return SAFE_LINK_PROTOCOLS.has(url.protocol) ? href : null;
@@ -2941,30 +3063,57 @@ function resolveSafeHref(href) {
2941
3063
  return null;
2942
3064
  }
2943
3065
  }
2944
- function isExternalHref(href) {
3066
+ function isExternalChatResourceHref(href) {
2945
3067
  return /^https?:\/\//i.test(href);
2946
3068
  }
2947
- function looksLikeLocalFileHref(href) {
2948
- 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);
2949
- }
2950
- function parseLocalFileAction(href) {
2951
- const viewer = new URLSearchParams(href.split("#")[0]?.split("?")[1] ?? "").get("viewer");
2952
- const normalizedHref = href.split("#")[0]?.split("?")[0] ?? href;
2953
- const decodedHref = decodeURIComponent(normalizedHref);
2954
- if (!looksLikeLocalFileHref(decodedHref)) return null;
2955
- const lineMatch = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(decodedHref);
2956
- 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;
2957
3082
  const line = lineMatch?.[2] ? Number(lineMatch[2]) : void 0;
2958
3083
  const column = lineMatch?.[3] ? Number(lineMatch[3]) : void 0;
2959
3084
  return {
2960
3085
  path: rawPath,
2961
- label: rawPath.split("/").filter(Boolean).pop() ?? rawPath,
3086
+ label: rawPath.split(/[\\/]/).filter(Boolean).pop() ?? rawPath,
2962
3087
  viewMode: "preview",
2963
3088
  ...viewer === "source" || viewer === "rendered" ? { previewViewer: viewer } : {},
2964
3089
  ...typeof line === "number" ? { line } : {},
2965
3090
  ...typeof column === "number" ? { column } : {}
2966
3091
  };
2967
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
+ }
2968
3117
  function prepareInlineTokens(inlineTokens) {
2969
3118
  if (!inlineTokens || inlineTokens.length === 0) return [];
2970
3119
  const seenRawTexts = /* @__PURE__ */ new Set();
@@ -3054,11 +3203,14 @@ function readStringProp(props, key) {
3054
3203
  const value = props[key];
3055
3204
  return typeof value === "string" && value.length > 0 ? value : null;
3056
3205
  }
3057
- function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, renderInlineDisplay }) {
3206
+ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, onInlineTokenClick, resolveFileContentUrl, renderInlineDisplay }) {
3058
3207
  const isUser = role === "user";
3059
3208
  const remarkPlugins = useMemo(() => inlineTokens && inlineTokens.length > 0 ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm], [inlineTokens]);
3060
3209
  const markdownComponents = useMemo(() => ({
3061
- 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
+ }),
3062
3214
  span: ({ node: _node, children, ...rest }) => {
3063
3215
  const restProps = rest;
3064
3216
  const kind = readStringProp(restProps, INLINE_TOKEN_KIND_ATTR);
@@ -3068,7 +3220,13 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3068
3220
  if (kind && key && label && rawText) return /* @__PURE__ */ jsx(ChatInlineTokenBadge, {
3069
3221
  kind,
3070
3222
  label,
3071
- isUser
3223
+ isUser,
3224
+ onClick: onInlineTokenClick ? () => onInlineTokenClick({
3225
+ kind,
3226
+ key,
3227
+ label,
3228
+ rawText
3229
+ }) : void 0
3072
3230
  });
3073
3231
  return /* @__PURE__ */ jsx("span", {
3074
3232
  ...rest,
@@ -3076,14 +3234,14 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3076
3234
  });
3077
3235
  },
3078
3236
  a: ({ node: _node, href, children, ...rest }) => {
3079
- const safeHref = resolveSafeHref(href);
3080
- if (!safeHref) return /* @__PURE__ */ jsx("span", {
3081
- className: "chat-link-invalid",
3082
- children
3083
- });
3084
- const external = isExternalHref(safeHref);
3085
- 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;
3086
3240
  const handleClick = (event) => {
3241
+ if (!safeHref) {
3242
+ event.preventDefault();
3243
+ return;
3244
+ }
3087
3245
  if (!onFileOpen || !localFileAction) return;
3088
3246
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3089
3247
  event.preventDefault();
@@ -3091,7 +3249,9 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3091
3249
  };
3092
3250
  return /* @__PURE__ */ jsx("a", {
3093
3251
  ...rest,
3094
- href: safeHref,
3252
+ className: cn(rest.className, !safeHref && "chat-link-invalid"),
3253
+ href: safeHref ?? "#",
3254
+ "aria-disabled": !safeHref || void 0,
3095
3255
  onClick: handleClick,
3096
3256
  target: external ? "_blank" : void 0,
3097
3257
  rel: external ? "noreferrer noopener" : void 0,
@@ -3119,15 +3279,18 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3119
3279
  className: "chat-task-checkbox"
3120
3280
  });
3121
3281
  },
3122
- img: ({ node: _node, src, alt, ...rest }) => {
3123
- const safeSrc = resolveSafeHref(src);
3282
+ img: ({ node: _node, src, alt }) => {
3283
+ const safeSrc = resolveSafeChatResourceHref(src);
3124
3284
  if (!safeSrc) return null;
3125
- return /* @__PURE__ */ jsx("img", {
3126
- ...rest,
3127
- 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, {
3128
3289
  alt: alt || "",
3129
- loading: "lazy",
3130
- decoding: "async"
3290
+ closeLabel: texts.attachmentCloseLabel ?? "Close preview",
3291
+ expandLabel: texts.attachmentExpandLabel ?? "Expand image",
3292
+ sizeLabel: null,
3293
+ src: resolvedSrc
3131
3294
  });
3132
3295
  },
3133
3296
  code: ({ node: _node, className, children, ...rest }) => {
@@ -3152,7 +3315,9 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3152
3315
  inline,
3153
3316
  isUser,
3154
3317
  onFileOpen,
3318
+ onInlineTokenClick,
3155
3319
  renderInlineDisplay,
3320
+ resolveFileContentUrl,
3156
3321
  texts
3157
3322
  ]);
3158
3323
  return /* @__PURE__ */ jsx(inline ? "span" : "div", {
@@ -3168,16 +3333,16 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3168
3333
  //#endregion
3169
3334
  //#region src/components/chat/ui/chat-message-list/chat-message-file/meta.ts
3170
3335
  const FILE_CATEGORY_TILE_CLASSES = {
3171
- archive: "border-border bg-muted text-muted-foreground",
3172
- audio: "border-border bg-muted text-muted-foreground",
3173
- code: "border-border bg-muted text-muted-foreground",
3174
- data: "border-border bg-muted text-muted-foreground",
3175
- document: "border-border bg-muted text-muted-foreground",
3176
- generic: "border-border bg-muted text-muted-foreground",
3177
- image: "border-border bg-muted text-muted-foreground",
3178
- pdf: "border-border bg-muted text-muted-foreground",
3179
- sheet: "border-border bg-muted text-muted-foreground",
3180
- video: "border-border bg-muted text-muted-foreground"
3336
+ archive: "bg-muted text-muted-foreground",
3337
+ audio: "bg-muted text-muted-foreground",
3338
+ code: "bg-muted text-muted-foreground",
3339
+ data: "bg-muted text-muted-foreground",
3340
+ document: "bg-muted text-muted-foreground",
3341
+ generic: "bg-muted text-muted-foreground",
3342
+ image: "bg-muted text-muted-foreground",
3343
+ pdf: "bg-muted text-muted-foreground",
3344
+ sheet: "bg-muted text-muted-foreground",
3345
+ video: "bg-muted text-muted-foreground"
3181
3346
  };
3182
3347
  const CODE_EXTENSIONS = new Set([
3183
3348
  "c",
@@ -3387,77 +3552,63 @@ function readFileCategoryLabel(category, texts) {
3387
3552
  }
3388
3553
  function renderMetaLine(categoryLabel, sizeLabel, isUser) {
3389
3554
  return /* @__PURE__ */ jsx("div", {
3390
- className: cn("mt-1 text-xs leading-5", isUser ? "text-primary-foreground/70" : "text-muted-foreground"),
3555
+ className: cn("mt-0.5 text-[11px] leading-4", isUser ? "text-foreground/55" : "text-muted-foreground"),
3391
3556
  children: sizeLabel ? `${categoryLabel} · ${sizeLabel}` : categoryLabel
3392
3557
  });
3393
3558
  }
3394
3559
  function renderActionPill(label, isUser, isInteractive) {
3395
3560
  return /* @__PURE__ */ jsx("span", {
3396
- className: cn("inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium", isInteractive ? isUser ? "border-primary-foreground/14 bg-primary-foreground/12 text-primary-foreground" : "border-border bg-card text-foreground" : isUser ? "border-primary-foreground/10 bg-primary-foreground/6 text-primary-foreground/62" : "border-border bg-muted text-muted-foreground"),
3561
+ className: cn("inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium", isInteractive ? isUser ? "bg-black/5 text-foreground/75" : "bg-muted text-muted-foreground" : isUser ? "bg-black/[0.03] text-foreground/45" : "bg-muted/70 text-muted-foreground/70"),
3397
3562
  children: label
3398
3563
  });
3399
3564
  }
3400
- function renderActionLink(label, href, isUser) {
3565
+ function renderActionControl(params) {
3566
+ const { label, href, isUser, onOpen } = params;
3567
+ const className = cn("inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium transition-colors", isUser ? "bg-black/5 text-foreground/75 hover:bg-black/10 hover:text-foreground" : "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground");
3568
+ if (onOpen) return /* @__PURE__ */ jsx("button", {
3569
+ type: "button",
3570
+ className,
3571
+ onClick: (event) => {
3572
+ event.preventDefault();
3573
+ event.stopPropagation();
3574
+ onOpen();
3575
+ },
3576
+ children: label
3577
+ });
3578
+ if (!href) return renderActionPill(label, isUser, false);
3401
3579
  return /* @__PURE__ */ jsx("a", {
3402
3580
  href,
3403
3581
  target: "_blank",
3404
3582
  rel: "noreferrer",
3405
- className: cn("inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium transition duration-200", isUser ? "border-primary-foreground/14 bg-primary-foreground/12 text-primary-foreground hover:border-primary-foreground/20 hover:bg-primary-foreground/16" : "border-border bg-card text-foreground hover:border-border-hover hover:bg-accent"),
3583
+ className,
3406
3584
  children: label
3407
3585
  });
3408
3586
  }
3409
3587
  function FileCategoryGlyph({ category, isUser }) {
3410
3588
  const Icon = FILE_CATEGORY_ICONS[category];
3411
3589
  return /* @__PURE__ */ jsx("div", {
3412
- className: cn("flex h-14 w-14 shrink-0 items-center justify-center rounded-[1rem] border", isUser ? "border-primary-foreground/12 bg-primary-foreground/10 text-primary-foreground" : FILE_CATEGORY_TILE_CLASSES[category]),
3590
+ className: cn("flex h-9 w-9 shrink-0 items-center justify-center rounded-lg", isUser ? "bg-black/5 text-foreground/70" : FILE_CATEGORY_TILE_CLASSES[category]),
3413
3591
  children: /* @__PURE__ */ jsx(Icon, {
3414
- className: cn("h-7 w-7", isUser ? "text-primary-foreground/92" : "text-current"),
3415
- strokeWidth: 2.2
3416
- })
3417
- });
3418
- }
3419
- function renderImagePreview(params) {
3420
- const { file, categoryLabel, sizeLabel, isUser } = params;
3421
- if (!file.dataUrl) return null;
3422
- return /* @__PURE__ */ jsx("a", {
3423
- href: file.dataUrl,
3424
- target: "_blank",
3425
- rel: "noreferrer",
3426
- className: "group block",
3427
- children: /* @__PURE__ */ jsxs("div", {
3428
- className: cn("relative overflow-hidden rounded-[1rem]", isUser ? "ring-1 ring-primary-foreground/10" : "bg-muted ring-1 ring-border"),
3429
- children: [/* @__PURE__ */ jsx("img", {
3430
- src: file.dataUrl,
3431
- alt: file.label,
3432
- className: "block h-auto max-h-[26rem] w-full rounded-[1rem] bg-transparent object-contain transition duration-300 group-hover:scale-[1.01]"
3433
- }), /* @__PURE__ */ jsxs("div", {
3434
- className: "pointer-events-none absolute right-3 top-3 flex items-center gap-2",
3435
- children: [/* @__PURE__ */ jsx("span", {
3436
- className: cn("inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold tracking-[0.18em] text-white backdrop-blur-sm", isUser ? "bg-black/36" : "bg-black/58"),
3437
- children: categoryLabel
3438
- }), sizeLabel ? /* @__PURE__ */ jsx("span", {
3439
- className: cn("inline-flex items-center rounded-full px-2 py-1 text-[10px] font-medium text-white/92 backdrop-blur-sm", isUser ? "bg-black/28" : "bg-black/46"),
3440
- children: sizeLabel
3441
- }) : null]
3442
- })]
3592
+ className: cn("h-4 w-4", isUser ? "text-foreground/70" : "text-current"),
3593
+ strokeWidth: 2
3443
3594
  })
3444
3595
  });
3445
3596
  }
3446
3597
  function renderFileCardHeader(params) {
3447
3598
  const { category, file, categoryLabel, sizeLabel, isUser, action } = params;
3448
3599
  return /* @__PURE__ */ jsxs("div", {
3449
- className: "flex items-center gap-3 p-3.5",
3600
+ className: "flex items-center gap-2.5 px-2.5 py-2",
3450
3601
  children: [/* @__PURE__ */ jsx(FileCategoryGlyph, {
3451
3602
  category,
3452
3603
  isUser
3453
3604
  }), /* @__PURE__ */ jsx("div", {
3454
3605
  className: "min-w-0 flex-1",
3455
3606
  children: /* @__PURE__ */ jsxs("div", {
3456
- className: "flex items-center gap-3",
3607
+ className: "flex items-center gap-2",
3457
3608
  children: [/* @__PURE__ */ jsxs("div", {
3458
3609
  className: "min-w-0 flex-1",
3459
3610
  children: [/* @__PURE__ */ jsx("div", {
3460
- className: "truncate text-[15px] font-semibold leading-5",
3611
+ className: "truncate text-[13px] font-medium leading-5",
3461
3612
  children: file.label
3462
3613
  }), renderMetaLine(categoryLabel, sizeLabel, isUser)]
3463
3614
  }), action]
@@ -3466,7 +3617,7 @@ function renderFileCardHeader(params) {
3466
3617
  });
3467
3618
  }
3468
3619
  function renderInlineMediaCard(params) {
3469
- const { category, file, categoryLabel, sizeLabel, isUser, shellClasses, actionLabel } = params;
3620
+ const { category, file, categoryLabel, sizeLabel, isUser, shellClasses, actionLabel, onOpen } = params;
3470
3621
  if (!file.dataUrl || category !== "audio" && category !== "video") return null;
3471
3622
  const mediaMimeType = resolveRenderableMimeType(file);
3472
3623
  return /* @__PURE__ */ jsxs("div", {
@@ -3477,9 +3628,14 @@ function renderInlineMediaCard(params) {
3477
3628
  categoryLabel,
3478
3629
  sizeLabel,
3479
3630
  isUser,
3480
- action: renderActionLink(actionLabel, file.dataUrl, isUser)
3631
+ action: renderActionControl({
3632
+ label: actionLabel,
3633
+ href: file.dataUrl,
3634
+ isUser,
3635
+ onOpen: onOpen ? () => onOpen(file) : void 0
3636
+ })
3481
3637
  }), /* @__PURE__ */ jsx("div", {
3482
- className: "px-3.5 pb-3.5",
3638
+ className: "px-2.5 pb-2.5",
3483
3639
  children: category === "audio" ? /* @__PURE__ */ jsx("audio", {
3484
3640
  controls: true,
3485
3641
  preload: "metadata",
@@ -3490,7 +3646,7 @@ function renderInlineMediaCard(params) {
3490
3646
  ...mediaMimeType ? { type: mediaMimeType } : {}
3491
3647
  })
3492
3648
  }) : /* @__PURE__ */ jsx("div", {
3493
- className: cn("overflow-hidden rounded-[1rem] border", isUser ? "border-primary-foreground/10 bg-black/26" : "border-border bg-muted"),
3649
+ className: cn("overflow-hidden rounded-lg", isUser ? "bg-black/10" : "bg-muted ring-1 ring-border/70"),
3494
3650
  children: /* @__PURE__ */ jsx("video", {
3495
3651
  controls: true,
3496
3652
  preload: "metadata",
@@ -3506,20 +3662,23 @@ function renderInlineMediaCard(params) {
3506
3662
  })]
3507
3663
  });
3508
3664
  }
3509
- function ChatMessageFile({ file, isUser = false, texts }) {
3665
+ function ChatMessageFile({ file, isUser = false, texts, onOpen }) {
3510
3666
  const { category, sizeLabel } = buildChatMessageFileMeta(file);
3511
3667
  const renderAsImage = isImageFileLike(file) && Boolean(file.dataUrl);
3512
3668
  const renderAsAudio = category === "audio" && Boolean(file.dataUrl);
3513
3669
  const renderAsVideo = category === "video" && Boolean(file.dataUrl);
3514
3670
  const isInteractive = Boolean(file.dataUrl);
3515
3671
  const actionLabel = isInteractive ? texts?.attachmentOpenLabel ?? "Open" : texts?.attachmentAttachedLabel ?? "Attached";
3672
+ const expandLabel = texts?.attachmentExpandLabel ?? "Expand image";
3673
+ const closeLabel = texts?.attachmentCloseLabel ?? "Close preview";
3516
3674
  const categoryLabel = readFileCategoryLabel(category, texts);
3517
- const shellClasses = cn("block overflow-hidden rounded-[1.25rem] border transition duration-200", isUser ? "border-primary-foreground/12 bg-primary-foreground/10 text-primary-foreground" : "border-border bg-card text-card-foreground", isInteractive && (isUser ? "hover:border-primary-foreground/18 hover:bg-primary-foreground/13" : "hover:border-border-hover hover:bg-accent"));
3518
- if (renderAsImage) return renderImagePreview({
3519
- file,
3520
- categoryLabel,
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"));
3676
+ if (renderAsImage && file.dataUrl) return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3677
+ alt: file.label,
3678
+ expandLabel,
3679
+ closeLabel,
3521
3680
  sizeLabel,
3522
- isUser
3681
+ src: file.dataUrl
3523
3682
  });
3524
3683
  if (renderAsAudio || renderAsVideo) return renderInlineMediaCard({
3525
3684
  category,
@@ -3528,7 +3687,8 @@ function ChatMessageFile({ file, isUser = false, texts }) {
3528
3687
  sizeLabel,
3529
3688
  isUser,
3530
3689
  shellClasses,
3531
- actionLabel
3690
+ actionLabel,
3691
+ onOpen
3532
3692
  });
3533
3693
  const content = renderFileCardHeader({
3534
3694
  category,
@@ -3542,6 +3702,12 @@ function ChatMessageFile({ file, isUser = false, texts }) {
3542
3702
  className: shellClasses,
3543
3703
  children: content
3544
3704
  });
3705
+ if (onOpen) return /* @__PURE__ */ jsx("button", {
3706
+ type: "button",
3707
+ className: cn(shellClasses, "group w-full text-left"),
3708
+ onClick: () => onOpen(file),
3709
+ children: content
3710
+ });
3545
3711
  return /* @__PURE__ */ jsx("a", {
3546
3712
  href: file.dataUrl,
3547
3713
  target: "_blank",
@@ -3551,6 +3717,54 @@ function ChatMessageFile({ file, isUser = false, texts }) {
3551
3717
  });
3552
3718
  }
3553
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
3554
3768
  //#region src/components/chat/hooks/use-reasoning-block-open-state.ts
3555
3769
  function useReasoningBlockOpenState(params) {
3556
3770
  const { isInProgress } = params;
@@ -3586,7 +3800,6 @@ const DEFAULT_STICKY_THRESHOLD_PX = 10;
3586
3800
  function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey, scrollRef, stickyThresholdPx }) {
3587
3801
  const [isAtBottom, setIsAtBottom] = useState(true);
3588
3802
  const isStickyRef = useRef(true);
3589
- const isProgrammaticScrollRef = useRef(false);
3590
3803
  const previousResetKeyRef = useRef(null);
3591
3804
  const pendingInitialScrollRef = useRef(false);
3592
3805
  const scheduledScrollFrameRef = useRef(null);
@@ -3604,7 +3817,6 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3604
3817
  scheduledScrollFrameRef.current = null;
3605
3818
  const currentElement = scrollRef.current;
3606
3819
  if (!currentElement) return;
3607
- isProgrammaticScrollRef.current = true;
3608
3820
  if (typeof currentElement.scrollTo === "function") currentElement.scrollTo({
3609
3821
  top: currentElement.scrollHeight,
3610
3822
  behavior
@@ -3617,10 +3829,6 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3617
3829
  queueScrollToBottom();
3618
3830
  }, [queueScrollToBottom, updateStickyState]);
3619
3831
  const onScroll = useCallback(() => {
3620
- if (isProgrammaticScrollRef.current) {
3621
- isProgrammaticScrollRef.current = false;
3622
- return;
3623
- }
3624
3832
  const element = scrollRef.current;
3625
3833
  if (!element) return;
3626
3834
  updateStickyState(resolveIsAtBottom(element));
@@ -3672,67 +3880,174 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3672
3880
  };
3673
3881
  }
3674
3882
  //#endregion
3883
+ //#region src/components/chat/ui/chat-message-list/chat-process-meta-row.tsx
3884
+ /**
3885
+ * Shared process-row metrics.
3886
+ * Collapsed process/tool rows should feel like ordinary body text lines.
3887
+ */
3888
+ const CHAT_PROCESS_LEADING_COL_CLASS = "inline-flex h-[1.15em] w-[1.15em] shrink-0 items-center justify-center text-current";
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";
3890
+ function ChatProcessLeadingIcon({ children, className }) {
3891
+ return /* @__PURE__ */ jsx("span", {
3892
+ className: cn(CHAT_PROCESS_LEADING_COL_CLASS, className),
3893
+ children
3894
+ });
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
+ }
3903
+ function ChatProcessMetaRow({ children, className, interactive = false, onClick, role, tabIndex, onKeyDown }) {
3904
+ return /* @__PURE__ */ jsx("div", {
3905
+ "data-chat-process-meta-row": "true",
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),
3907
+ onClick,
3908
+ role,
3909
+ tabIndex,
3910
+ onKeyDown,
3911
+ children
3912
+ });
3913
+ }
3914
+ //#endregion
3915
+ //#region src/components/chat/ui/chat-message-list/chat-collapsible-meta-summary.tsx
3916
+ /**
3917
+ * Shared collapsible meta-row for process / tool-group / reasoning summaries.
3918
+ * Rendered as a plain button-like row (not <summary>) so browsers never inject
3919
+ * a default disclosure label such as "详情" / "Details".
3920
+ */
3921
+ function ChatCollapsibleMetaSummary({ label, openGroup, open = false, icon: Icon, className, leadingIconClassName, labelClassName, onClick }) {
3922
+ return /* @__PURE__ */ jsxs("button", {
3923
+ type: "button",
3924
+ "data-chat-process-meta-row": "true",
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),
3926
+ "aria-expanded": open,
3927
+ onClick,
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
+ })
3946
+ })
3947
+ ]
3948
+ });
3949
+ }
3950
+ //#endregion
3675
3951
  //#region src/components/chat/ui/chat-message-list/chat-reasoning-block.tsx
3676
- function normalizeReasoningLabel(label) {
3677
- const normalizedLabel = label.trim().toLowerCase();
3678
- if (normalizedLabel === "reasoning" || normalizedLabel === "thinking" || normalizedLabel === "推理" || normalizedLabel === "推理过程" || normalizedLabel === "思考过程" || normalizedLabel === "思考") return "思考";
3679
- return label;
3952
+ function formatReasoningLabel(label, text, characterCountTemplate) {
3953
+ const characterCount = Array.from(text).length;
3954
+ return (characterCountTemplate ?? `${label} · {count}`).split("{count}").join(String(characterCount));
3680
3955
  }
3681
- function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
3956
+ function ChatReasoningBlock({ label, text, characterCountTemplates, isUser, isInProgress }) {
3682
3957
  const { isOpen, onSummaryClick } = useReasoningBlockOpenState({ isInProgress });
3683
3958
  const scrollRef = useRef(null);
3684
- const displayLabel = normalizeReasoningLabel(label);
3959
+ const displayLabel = formatReasoningLabel(label, text, isInProgress ? characterCountTemplates?.inProgress : characterCountTemplates?.completed);
3685
3960
  const { onScroll } = useStickyBottomScroll({
3686
3961
  scrollRef,
3687
- resetKey: `${displayLabel}:${isInProgress ? "streaming" : "idle"}`,
3962
+ resetKey: "reasoning",
3688
3963
  isLoading: false,
3689
3964
  hasContent: text.length > 0,
3690
3965
  contentVersion: text,
3691
3966
  stickyThresholdPx: 20
3692
3967
  });
3693
- return /* @__PURE__ */ jsxs("details", {
3694
- className: "mt-2",
3695
- open: isOpen,
3696
- children: [/* @__PURE__ */ jsx("summary", {
3697
- className: cn("cursor-pointer text-xs", isUser ? "text-primary-100" : "text-muted-foreground"),
3698
- onClick: onSummaryClick,
3699
- children: displayLabel
3700
- }), /* @__PURE__ */ jsx("div", {
3968
+ return /* @__PURE__ */ jsxs("div", {
3969
+ className: "group/reasoning",
3970
+ children: [/* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
3971
+ openGroup: "reasoning",
3972
+ open: isOpen,
3973
+ icon: Brain,
3974
+ leadingIconClassName: isUser ? "bg-primary" : "bg-card",
3975
+ label: displayLabel,
3976
+ labelClassName: isUser ? "text-primary-100" : void 0,
3977
+ onClick: onSummaryClick
3978
+ }), isOpen ? /* @__PURE__ */ jsx("div", {
3701
3979
  ref: scrollRef,
3702
3980
  onScroll,
3703
3981
  "data-reasoning-scroll": "true",
3704
- className: cn("mt-2 w-fit max-w-[500px] max-h-56 overflow-y-auto rounded-lg custom-scrollbar", isUser ? "bg-primary/70" : "bg-muted"),
3982
+ className: cn("mt-1 w-fit max-w-[500px] max-h-56 overflow-y-auto rounded-lg custom-scrollbar", isUser ? "bg-primary/70" : "bg-muted"),
3705
3983
  children: /* @__PURE__ */ jsx("pre", {
3706
3984
  className: "min-w-0 whitespace-pre-wrap break-all p-2 text-[11px]",
3707
3985
  children: text
3708
3986
  })
3709
- })]
3987
+ }) : null]
3710
3988
  });
3711
3989
  }
3712
3990
  //#endregion
3713
3991
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-root.tsx
3992
+ /**
3993
+ * Default tool presentation is an inline process row, not a heavy card.
3994
+ * Expanded details sit under the overview text column with clear clearance
3995
+ * from any workflow rail in the icon column.
3996
+ */
3714
3997
  function ToolCardRoot({ children, className }) {
3715
3998
  return /* @__PURE__ */ jsx("div", {
3716
- className: cn("my-2 rounded-lg border border-border bg-card shadow-sm overflow-hidden text-[12px] text-card-foreground", "w-[280px] sm:w-[360px] md:w-[480px] min-w-full max-w-full transition-all flex flex-col", className),
3999
+ className: cn("relative my-0 flex w-full min-w-0 max-w-full flex-col text-[0.925rem] leading-[1.72] text-muted-foreground", className),
3717
4000
  children
3718
4001
  });
3719
4002
  }
3720
4003
  function ToolCardContent({ children, className }) {
3721
4004
  return /* @__PURE__ */ jsx("div", {
3722
- className: cn("border-t border-border bg-muted/35 px-3 pt-1 pb-2 w-full overflow-hidden", className),
4005
+ className: cn("mt-0.5 w-full max-w-full min-w-0 overflow-hidden pl-[calc(1.15em+0.375rem)] text-[0.925rem] leading-[1.72] text-muted-foreground", className),
3723
4006
  children
3724
4007
  });
3725
4008
  }
3726
- //#endregion
3727
- //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-status.tsx
3728
- const STATUS_STYLES = {
3729
- running: {
3730
- text: "text-primary/70",
3731
- icon: Loader2,
3732
- spin: true
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
+ }
4041
+ //#endregion
4042
+ //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-status.tsx
4043
+ const STATUS_STYLES = {
4044
+ running: {
4045
+ text: "text-muted-foreground",
4046
+ icon: Loader2,
4047
+ spin: true
3733
4048
  },
3734
4049
  success: {
3735
- text: "text-primary/70",
4050
+ text: "text-muted-foreground/75",
3736
4051
  icon: Check,
3737
4052
  spin: false
3738
4053
  },
@@ -3742,20 +4057,23 @@ const STATUS_STYLES = {
3742
4057
  spin: false
3743
4058
  },
3744
4059
  cancelled: {
3745
- text: "text-muted-foreground",
4060
+ text: "text-muted-foreground/70",
3746
4061
  icon: Minus,
3747
4062
  spin: false
3748
4063
  }
3749
4064
  };
3750
- function ToolStatusLabel({ card }) {
4065
+ function ToolStatusLabel({ card, iconOnly = false }) {
3751
4066
  const style = STATUS_STYLES[card.statusTone] || STATUS_STYLES.cancelled;
3752
4067
  const Icon = style.icon;
4068
+ const showLabel = !iconOnly && (card.statusTone === "running" || card.statusTone === "error");
3753
4069
  return /* @__PURE__ */ jsxs("span", {
3754
- className: cn("inline-flex items-center gap-1.5 text-[11px] font-medium leading-none shrink-0", style.text),
4070
+ className: cn("inline-flex shrink-0 items-center gap-1 text-[0.925rem] font-normal leading-[1.72]", style.text),
4071
+ "aria-label": iconOnly ? card.statusLabel : void 0,
3755
4072
  children: [/* @__PURE__ */ jsx(Icon, {
3756
- className: cn("h-3.5 w-3.5", style.spin && "animate-spin"),
3757
- strokeWidth: 3
3758
- }), card.statusTone === "running" ? card.statusLabel : null]
4073
+ "aria-hidden": "true",
4074
+ className: cn("h-[1.05em] w-[1.05em]", style.spin && "animate-spin"),
4075
+ strokeWidth: 2.25
4076
+ }), showLabel ? card.statusLabel : null]
3759
4077
  });
3760
4078
  }
3761
4079
  //#endregion
@@ -3764,12 +4082,12 @@ function resolveToolCardActionView(action) {
3764
4082
  if (action.kind === "show-content") return {
3765
4083
  icon: Eye,
3766
4084
  label: action.label,
3767
- toneClassName: "border-border bg-card text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-primary/35"
4085
+ toneClassName: "border-border/70 bg-transparent text-muted-foreground hover:bg-muted/60 hover:text-foreground focus-visible:ring-primary/35"
3768
4086
  };
3769
4087
  return {
3770
4088
  icon: ArrowUpRight,
3771
4089
  label: action.label ?? (action.sessionKind === "child" ? "Open child session" : "Open session"),
3772
- toneClassName: "border-border bg-card text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-primary/35"
4090
+ toneClassName: "border-border/70 bg-transparent text-muted-foreground hover:bg-muted/60 hover:text-foreground focus-visible:ring-primary/35"
3773
4091
  };
3774
4092
  }
3775
4093
  function ToolCardActionButton({ action, onAction }) {
@@ -3787,12 +4105,12 @@ function ToolCardActionButton({ action, onAction }) {
3787
4105
  children: /* @__PURE__ */ jsx("button", {
3788
4106
  type: "button",
3789
4107
  onClick: handleClick,
3790
- className: cn("inline-flex h-7 w-7 items-center justify-center rounded-full border transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1", view.toneClassName),
4108
+ className: cn("inline-flex h-6 w-6 items-center justify-center rounded-md border transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1", view.toneClassName),
3791
4109
  "aria-label": view.label,
3792
4110
  title: view.label,
3793
4111
  children: /* @__PURE__ */ jsx(Icon, {
3794
4112
  className: "h-3.5 w-3.5",
3795
- strokeWidth: 2.5
4113
+ strokeWidth: 2.25
3796
4114
  })
3797
4115
  })
3798
4116
  }), /* @__PURE__ */ jsx(TooltipContent, {
@@ -3802,44 +4120,82 @@ function ToolCardActionButton({ action, onAction }) {
3802
4120
  })] })
3803
4121
  });
3804
4122
  }
3805
- function ToolCardHeader({ card, icon: Icon, expanded, canExpand, hideSummary = false, actionSlot, onToggle }) {
4123
+ function humanizeToolName(toolName) {
4124
+ return toolName.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
4125
+ }
4126
+ function handleToolHeaderKeyDown(event, onToggle) {
4127
+ if (event.key !== "Enter" && event.key !== " ") return;
4128
+ event.preventDefault();
4129
+ onToggle();
4130
+ }
4131
+ function ToolHeaderSummary({ label, summary }) {
4132
+ return /* @__PURE__ */ jsxs("span", {
4133
+ className: "min-w-0 shrink truncate",
4134
+ children: [label, summary ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
4135
+ className: "mx-1.5 select-none text-muted-foreground/45",
4136
+ children: "·"
4137
+ }), /* @__PURE__ */ jsx("span", {
4138
+ title: summary,
4139
+ children: summary
4140
+ })] }) : null]
4141
+ });
4142
+ }
4143
+ function ToolHeaderChevron({ expanded }) {
4144
+ return /* @__PURE__ */ jsx("span", {
4145
+ className: cn("inline-flex h-[1.15em] w-[1.15em] shrink-0 items-center justify-center text-muted-foreground/80 transition-opacity", expanded ? "opacity-100" : "opacity-0 group-hover/process-row:opacity-100 group-focus-within/process-row:opacity-100"),
4146
+ children: /* @__PURE__ */ jsx(ChevronRight, {
4147
+ className: cn("h-[1.05em] w-[1.05em] transition-transform", expanded && "rotate-90"),
4148
+ strokeWidth: 2.25
4149
+ })
4150
+ });
4151
+ }
4152
+ function ToolHeaderChangeSummary({ additions, deletions }) {
4153
+ if (additions === 0 && deletions === 0) return null;
4154
+ const accessibleLabel = [additions > 0 ? `+${additions}` : null, deletions > 0 ? `-${deletions}` : null].filter(Boolean).join(" ");
4155
+ return /* @__PURE__ */ jsxs("span", {
4156
+ className: "inline-flex shrink-0 items-center gap-1.5 tabular-nums text-muted-foreground/75",
4157
+ "aria-label": accessibleLabel,
4158
+ children: [additions > 0 ? /* @__PURE__ */ jsxs("span", {
4159
+ className: "transition-colors group-hover/process-row:text-emerald-600",
4160
+ children: ["+", additions]
4161
+ }) : null, deletions > 0 ? /* @__PURE__ */ jsxs("span", {
4162
+ className: "transition-colors group-hover/process-row:text-rose-600",
4163
+ children: ["-", deletions]
4164
+ }) : null]
4165
+ });
4166
+ }
4167
+ function ToolCardHeader({ card, toolLabel, changeSummary, icon: Icon, expanded, canExpand, hideSummary = false, actionSlot, onToggle }) {
3806
4168
  const summaryPart = hideSummary ? "" : card.summary?.replace(/^(command|path|args|query|input):\s*/i, "") ?? "";
3807
- return /* @__PURE__ */ jsxs("div", {
3808
- className: cn("flex items-center gap-3 px-3 py-2.5 transition-colors bg-transparent", canExpand ? "cursor-pointer hover:bg-accent/70" : ""),
3809
- onClick: onToggle,
3810
- children: [/* @__PURE__ */ jsxs("div", {
3811
- className: "flex min-w-0 flex-1 items-center gap-2 overflow-hidden font-mono text-foreground",
3812
- children: [/* @__PURE__ */ jsx(Icon, {
3813
- className: "h-4 w-4 text-primary/70 shrink-0",
3814
- strokeWidth: 3
3815
- }), /* @__PURE__ */ jsxs("div", {
3816
- className: "flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden",
3817
- children: [/* @__PURE__ */ jsx("span", {
3818
- className: "font-bold shrink-0 tracking-tight",
3819
- children: card.toolName
3820
- }), summaryPart && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
3821
- className: "text-muted-foreground/45 font-bold select-none shrink-0",
3822
- children: "›"
3823
- }), /* @__PURE__ */ jsx("span", {
3824
- className: "block min-w-0 flex-1 truncate font-normal text-muted-foreground",
3825
- title: summaryPart,
3826
- children: summaryPart
3827
- })] })]
3828
- })]
3829
- }), /* @__PURE__ */ jsxs("div", {
3830
- className: "flex items-center gap-3 shrink-0",
3831
- children: [
3832
- actionSlot,
3833
- /* @__PURE__ */ jsx(ToolStatusLabel, { card }),
3834
- canExpand && (expanded ? /* @__PURE__ */ jsx(ChevronDown, {
3835
- className: "h-4 w-4 text-muted-foreground",
3836
- strokeWidth: 3
3837
- }) : /* @__PURE__ */ jsx(ChevronRight, {
3838
- className: "h-4 w-4 text-muted-foreground",
3839
- strokeWidth: 3
3840
- }))
3841
- ]
3842
- })]
4169
+ const displayLabel = toolLabel ?? humanizeToolName(card.toolName);
4170
+ return /* @__PURE__ */ jsxs(ChatProcessMetaRow, {
4171
+ interactive: canExpand,
4172
+ onClick: canExpand ? () => onToggle() : void 0,
4173
+ role: canExpand ? "button" : void 0,
4174
+ tabIndex: canExpand ? 0 : void 0,
4175
+ onKeyDown: canExpand ? (event) => handleToolHeaderKeyDown(event, onToggle) : void 0,
4176
+ children: [
4177
+ /* @__PURE__ */ jsx(ChatProcessLeadingIcon, {
4178
+ className: "relative z-[1] rounded-sm bg-card",
4179
+ children: /* @__PURE__ */ jsx(Icon, {
4180
+ className: "h-[1.05em] w-[1.05em]",
4181
+ strokeWidth: 2.25
4182
+ })
4183
+ }),
4184
+ /* @__PURE__ */ jsx(ToolHeaderSummary, {
4185
+ label: displayLabel,
4186
+ summary: summaryPart
4187
+ }),
4188
+ changeSummary ? /* @__PURE__ */ jsx(ToolHeaderChangeSummary, { ...changeSummary }) : null,
4189
+ /* @__PURE__ */ jsx(ToolStatusLabel, {
4190
+ card,
4191
+ iconOnly: Boolean(toolLabel)
4192
+ }),
4193
+ canExpand ? /* @__PURE__ */ jsx(ToolHeaderChevron, { expanded }) : null,
4194
+ actionSlot ? /* @__PURE__ */ jsx("span", {
4195
+ className: "inline-flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover/process-row:opacity-100 group-focus-within/process-row:opacity-100",
4196
+ children: actionSlot
4197
+ }) : null
4198
+ ]
3843
4199
  });
3844
4200
  }
3845
4201
  function ToolCardHeaderAction({ action, onAction }) {
@@ -4082,7 +4438,7 @@ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
4082
4438
  className: cn("overflow-hidden bg-card", !isFirst && "border-t border-border"),
4083
4439
  children: [
4084
4440
  showMetaRow ? /* @__PURE__ */ jsxs("div", {
4085
- className: cn("flex items-center justify-between gap-4 border-b border-border px-4 text-muted-foreground", showPathRow ? "py-2" : "py-1.5"),
4441
+ className: cn("flex min-h-9 items-center justify-between gap-4 border-b border-border px-3 text-muted-foreground", showPathRow ? "py-2" : "py-1.5"),
4086
4442
  children: [/* @__PURE__ */ jsx("div", {
4087
4443
  className: "min-w-0 flex-1",
4088
4444
  children: showPathRow ? /* @__PURE__ */ jsx(TooltipProvider, {
@@ -4092,7 +4448,7 @@ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
4092
4448
  children: /* @__PURE__ */ jsx("button", {
4093
4449
  type: "button",
4094
4450
  onClick: handlePathClick,
4095
- className: cn("w-full truncate whitespace-nowrap text-left font-mono text-[12px] font-medium text-foreground", onFileOpen && "transition-colors hover:text-primary hover:underline"),
4451
+ className: cn("w-full truncate whitespace-nowrap text-left font-mono text-[12px] font-medium leading-5 text-foreground", onFileOpen && "transition-colors hover:text-primary hover:underline"),
4096
4452
  title: block.path,
4097
4453
  children: block.path
4098
4454
  })
@@ -4118,28 +4474,28 @@ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
4118
4474
  children: /* @__PURE__ */ jsx("div", {
4119
4475
  className: "overflow-x-auto custom-scrollbar",
4120
4476
  children: /* @__PURE__ */ jsx("pre", {
4121
- className: "min-w-max whitespace-pre bg-card px-4 py-2 font-mono text-[11px] leading-5 text-foreground",
4477
+ className: "min-w-max whitespace-pre bg-card px-3 py-2 font-mono text-[11px] leading-5 text-foreground",
4122
4478
  children: block.rawText
4123
4479
  })
4124
4480
  })
4125
4481
  }) : null,
4126
4482
  block.truncated && !previewBlock ? /* @__PURE__ */ jsx("div", {
4127
- className: "border-t border-border bg-muted px-4 py-2 text-[10px] text-muted-foreground",
4483
+ className: "border-t border-border bg-muted px-3 py-2 text-[10px] text-muted-foreground",
4128
4484
  children: "Showing a shortened diff preview."
4129
4485
  }) : null
4130
4486
  ]
4131
4487
  });
4132
4488
  }
4133
- function ToolCardFileOperationContent({ card, className, onFileOpen }) {
4489
+ function ToolCardFileOperationContent({ card, className, onFileOpen, showPathRow = true }) {
4134
4490
  const blocks = card.fileOperation?.blocks ?? [];
4135
4491
  const output = card.output?.trim() ?? "";
4136
4492
  if (blocks.length === 0 && !output) return null;
4137
4493
  return /* @__PURE__ */ jsxs("div", {
4138
- className: cn("overflow-hidden bg-card", className),
4494
+ className: cn("overflow-hidden rounded-md border border-border/70 bg-card", className),
4139
4495
  children: [blocks.map((block, index) => {
4140
4496
  return /* @__PURE__ */ jsx(FileOperationBlock, {
4141
4497
  block,
4142
- showPathRow: true,
4498
+ showPathRow,
4143
4499
  isFirst: index === 0,
4144
4500
  onFileOpen
4145
4501
  }, block.key);
@@ -4151,7 +4507,7 @@ function ToolCardFileOperationContent({ card, className, onFileOpen }) {
4151
4507
  children: /* @__PURE__ */ jsx("div", {
4152
4508
  className: "overflow-x-auto custom-scrollbar",
4153
4509
  children: /* @__PURE__ */ jsx("pre", {
4154
- className: "min-w-max whitespace-pre bg-card px-4 py-2 font-mono text-[11px] leading-5 text-foreground",
4510
+ className: "min-w-max whitespace-pre bg-card px-3 py-2 font-mono text-[11px] leading-5 text-foreground",
4155
4511
  children: output
4156
4512
  })
4157
4513
  })
@@ -4159,9 +4515,279 @@ function ToolCardFileOperationContent({ card, className, onFileOpen }) {
4159
4515
  });
4160
4516
  }
4161
4517
  //#endregion
4518
+ //#region src/components/chat/ui/chat-message-list/tool-card/terminal/terminal-ansi.utils.ts
4519
+ const ANSI_ESCAPE_PREFIX = String.fromCharCode(27);
4520
+ const ANSI_PATTERN = new RegExp(`${ANSI_ESCAPE_PREFIX}\\[(.*?)([ -/]*[@-~])`, "g");
4521
+ function hasAnsiSequences(value) {
4522
+ return value.includes(`${ANSI_ESCAPE_PREFIX}[`);
4523
+ }
4524
+ function stripAnsiSequences(value) {
4525
+ return value.replace(new RegExp(`${ANSI_ESCAPE_PREFIX}\\[[0-?]*[ -/]*[@-~]`, "g"), "");
4526
+ }
4527
+ function escapeHtml(value) {
4528
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4529
+ }
4530
+ const DEFAULT_STYLE = {
4531
+ bold: false,
4532
+ dim: false,
4533
+ italic: false,
4534
+ underline: false,
4535
+ inverse: false
4536
+ };
4537
+ const SGR_STYLE_PATCHES = {
4538
+ 1: { bold: true },
4539
+ 2: { dim: true },
4540
+ 3: { italic: true },
4541
+ 4: { underline: true },
4542
+ 7: { inverse: true },
4543
+ 22: {
4544
+ bold: false,
4545
+ dim: false
4546
+ },
4547
+ 23: { italic: false },
4548
+ 24: { underline: false },
4549
+ 27: { inverse: false },
4550
+ 39: { fg: void 0 },
4551
+ 49: { bg: void 0 }
4552
+ };
4553
+ const BASIC_FG = {
4554
+ 30: "black",
4555
+ 31: "red",
4556
+ 32: "green",
4557
+ 33: "yellow",
4558
+ 34: "blue",
4559
+ 35: "magenta",
4560
+ 36: "cyan",
4561
+ 37: "white",
4562
+ 90: "bright-black",
4563
+ 91: "bright-red",
4564
+ 92: "bright-green",
4565
+ 93: "bright-yellow",
4566
+ 94: "bright-blue",
4567
+ 95: "bright-magenta",
4568
+ 96: "bright-cyan",
4569
+ 97: "bright-white"
4570
+ };
4571
+ const BASIC_BG = {
4572
+ 40: "black",
4573
+ 41: "red",
4574
+ 42: "green",
4575
+ 43: "yellow",
4576
+ 44: "blue",
4577
+ 45: "magenta",
4578
+ 46: "cyan",
4579
+ 47: "white",
4580
+ 100: "bright-black",
4581
+ 101: "bright-red",
4582
+ 102: "bright-green",
4583
+ 103: "bright-yellow",
4584
+ 104: "bright-blue",
4585
+ 105: "bright-magenta",
4586
+ 106: "bright-cyan",
4587
+ 107: "bright-white"
4588
+ };
4589
+ function resolveExtendedColor(params, index) {
4590
+ const mode = params[index + 1];
4591
+ if (mode === 5 && typeof params[index + 2] === "number") return {
4592
+ color: `ansi-256-${params[index + 2]}`,
4593
+ consumed: 2
4594
+ };
4595
+ if (mode === 2 && typeof params[index + 2] === "number" && typeof params[index + 3] === "number" && typeof params[index + 4] === "number") return {
4596
+ color: `rgb(${params[index + 2]},${params[index + 3]},${params[index + 4]})`,
4597
+ consumed: 4
4598
+ };
4599
+ return null;
4600
+ }
4601
+ function applySgr(style, params) {
4602
+ let next = { ...style };
4603
+ for (let index = 0; index < params.length; index += 1) {
4604
+ const code = params[index];
4605
+ if (code === 0) return { ...DEFAULT_STYLE };
4606
+ const stylePatch = SGR_STYLE_PATCHES[code];
4607
+ if (stylePatch) {
4608
+ next = {
4609
+ ...next,
4610
+ ...stylePatch
4611
+ };
4612
+ continue;
4613
+ }
4614
+ if (code === 38 || code === 48) {
4615
+ const extendedColor = resolveExtendedColor(params, index);
4616
+ if (extendedColor) {
4617
+ next = code === 38 ? {
4618
+ ...next,
4619
+ fg: extendedColor.color
4620
+ } : {
4621
+ ...next,
4622
+ bg: extendedColor.color
4623
+ };
4624
+ index += extendedColor.consumed;
4625
+ }
4626
+ continue;
4627
+ }
4628
+ if (BASIC_FG[code]) {
4629
+ next = {
4630
+ ...next,
4631
+ fg: BASIC_FG[code]
4632
+ };
4633
+ continue;
4634
+ }
4635
+ if (BASIC_BG[code]) next = {
4636
+ ...next,
4637
+ bg: BASIC_BG[code]
4638
+ };
4639
+ }
4640
+ return next;
4641
+ }
4642
+ function styleToClassName(style) {
4643
+ const classes = ["ansi"];
4644
+ if (style.bold) classes.push("ansi-bold");
4645
+ if (style.dim) classes.push("ansi-dim");
4646
+ if (style.italic) classes.push("ansi-italic");
4647
+ if (style.underline) classes.push("ansi-underline");
4648
+ if (style.inverse) classes.push("ansi-inverse");
4649
+ if (style.fg) if (style.fg.startsWith("rgb(")) classes.push("ansi-fg-custom");
4650
+ else if (style.fg.startsWith("ansi-256-")) {
4651
+ classes.push("ansi-fg-256");
4652
+ classes.push(style.fg);
4653
+ } else classes.push(`ansi-fg-${style.fg}`);
4654
+ if (style.bg) if (style.bg.startsWith("rgb(")) classes.push("ansi-bg-custom");
4655
+ else if (style.bg.startsWith("ansi-256-")) {
4656
+ classes.push("ansi-bg-256");
4657
+ classes.push(style.bg);
4658
+ } else classes.push(`ansi-bg-${style.bg}`);
4659
+ return classes.join(" ");
4660
+ }
4661
+ function styleToInlineStyle(style) {
4662
+ const parts = [];
4663
+ if (style.fg?.startsWith("rgb(")) parts.push(`color:${style.fg}`);
4664
+ if (style.bg?.startsWith("rgb(")) parts.push(`background-color:${style.bg}`);
4665
+ return parts.length > 0 ? parts.join(";") : void 0;
4666
+ }
4667
+ function openSpan(style) {
4668
+ const className = styleToClassName(style);
4669
+ const inline = styleToInlineStyle(style);
4670
+ return inline ? `<span class="${className}" style="${inline}">` : `<span class="${className}">`;
4671
+ }
4672
+ /**
4673
+ * Lightweight ANSI SGR -> HTML converter for terminal output.
4674
+ * Handles common color/style codes without adding a dependency.
4675
+ */
4676
+ function ansiToHtml(value) {
4677
+ if (!value) return "";
4678
+ if (!hasAnsiSequences(value)) return escapeHtml(value);
4679
+ let html = "";
4680
+ let lastIndex = 0;
4681
+ let style = { ...DEFAULT_STYLE };
4682
+ let spanOpen = false;
4683
+ const flushText = (text) => {
4684
+ if (!text) return;
4685
+ if (!spanOpen) {
4686
+ html += openSpan(style);
4687
+ spanOpen = true;
4688
+ }
4689
+ html += escapeHtml(text);
4690
+ };
4691
+ const closeSpan = () => {
4692
+ if (spanOpen) {
4693
+ html += "</span>";
4694
+ spanOpen = false;
4695
+ }
4696
+ };
4697
+ value.replace(ANSI_PATTERN, (match, body, terminator, offset) => {
4698
+ flushText(value.slice(lastIndex, offset));
4699
+ lastIndex = offset + match.length;
4700
+ if (terminator !== "m") return match;
4701
+ const params = body.trim().length === 0 ? [0] : body.split(";").map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
4702
+ closeSpan();
4703
+ style = applySgr(style, params.length > 0 ? params : [0]);
4704
+ return match;
4705
+ });
4706
+ flushText(value.slice(lastIndex));
4707
+ closeSpan();
4708
+ return html;
4709
+ }
4710
+ //#endregion
4711
+ //#region src/components/chat/ui/chat-message-list/tool-card/terminal/terminal-panes.tsx
4712
+ const MAX_TERMINAL_TEXT_CHARS = 12e4;
4713
+ function clampTerminalText(value) {
4714
+ if (value.length <= MAX_TERMINAL_TEXT_CHARS) return value;
4715
+ return `${value.slice(0, MAX_TERMINAL_TEXT_CHARS)}\n…`;
4716
+ }
4717
+ function TerminalCode({ code, language, preferAnsi = false, className }) {
4718
+ const source = clampTerminalText(code);
4719
+ const content = useMemo(() => {
4720
+ if (preferAnsi && hasAnsiSequences(source)) return {
4721
+ html: ansiToHtml(source),
4722
+ language: "ansi",
4723
+ highlighted: true
4724
+ };
4725
+ const plain = stripAnsiSequences(source);
4726
+ return chatCodeSyntaxHighlighter.highlight(plain, language);
4727
+ }, [
4728
+ language,
4729
+ preferAnsi,
4730
+ source
4731
+ ]);
4732
+ return /* @__PURE__ */ jsx("code", {
4733
+ className: cn("hljs chat-terminal-code", `language-${content.language}`, className),
4734
+ "data-highlighted": content.highlighted ? "true" : "false",
4735
+ dangerouslySetInnerHTML: { __html: content.html }
4736
+ });
4737
+ }
4738
+ /**
4739
+ * Compact terminal-style surface for command tools.
4740
+ * Style only — no decorative window chrome.
4741
+ */
4742
+ function ChatTerminalSurface({ command, output, emptyLabel, isRunning, hasOutput, isError = false }) {
4743
+ return /* @__PURE__ */ jsx("div", {
4744
+ className: cn("chat-terminal-surface", isError ? "chat-terminal-surface-error" : void 0),
4745
+ "data-testid": "chat-terminal-surface",
4746
+ children: /* @__PURE__ */ jsxs("div", {
4747
+ className: "chat-terminal-body custom-scrollbar",
4748
+ children: [/* @__PURE__ */ jsxs("div", {
4749
+ className: "chat-terminal-line chat-terminal-line-command",
4750
+ children: [/* @__PURE__ */ jsx("span", {
4751
+ className: "chat-terminal-gutter",
4752
+ "aria-hidden": "true",
4753
+ children: "$"
4754
+ }), /* @__PURE__ */ jsx("div", {
4755
+ className: "chat-terminal-line-content",
4756
+ children: command ? /* @__PURE__ */ jsxs("pre", {
4757
+ className: "chat-terminal-pre",
4758
+ children: [/* @__PURE__ */ jsx(TerminalCode, {
4759
+ code: command,
4760
+ language: "bash"
4761
+ }), isRunning && !hasOutput ? /* @__PURE__ */ jsx("span", {
4762
+ className: "chat-terminal-caret",
4763
+ "aria-hidden": "true"
4764
+ }) : null]
4765
+ }) : /* @__PURE__ */ jsx("div", { className: "chat-terminal-skeleton" })
4766
+ })]
4767
+ }), (hasOutput || !isRunning) && /* @__PURE__ */ jsx("div", {
4768
+ className: "chat-terminal-output-block",
4769
+ children: hasOutput ? /* @__PURE__ */ jsxs("pre", {
4770
+ className: cn("chat-terminal-pre chat-terminal-output", isError ? "chat-terminal-pre-error" : void 0),
4771
+ children: [/* @__PURE__ */ jsx(TerminalCode, {
4772
+ code: output,
4773
+ language: isError ? "text" : "shell",
4774
+ preferAnsi: true,
4775
+ className: isError ? "chat-terminal-code-error" : void 0
4776
+ }), isRunning ? /* @__PURE__ */ jsx("span", {
4777
+ className: "chat-terminal-caret",
4778
+ "aria-hidden": "true"
4779
+ }) : null]
4780
+ }) : /* @__PURE__ */ jsx("div", {
4781
+ className: "chat-terminal-empty",
4782
+ children: emptyLabel
4783
+ })
4784
+ })]
4785
+ })
4786
+ });
4787
+ }
4788
+ //#endregion
4162
4789
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-views.tsx
4163
4790
  const TOOL_CARD_AUTO_EXPAND_DELAY_MS = 200;
4164
- const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
4165
4791
  function readNonEmptyString(value) {
4166
4792
  if (typeof value !== "string") return null;
4167
4793
  return value.length > 0 ? value : null;
@@ -4169,9 +4795,6 @@ function readNonEmptyString(value) {
4169
4795
  function isRecord(value) {
4170
4796
  return typeof value === "object" && value !== null && !Array.isArray(value);
4171
4797
  }
4172
- function stripAnsi(value) {
4173
- return value.replace(ANSI_ESCAPE_PATTERN, "");
4174
- }
4175
4798
  function isStructuredTerminalRecord(record) {
4176
4799
  return "command" in record || "workingDir" in record || "exitCode" in record || "durationMs" in record || "stdout" in record || "stderr" in record || "aggregated_output" in record || "combinedOutput" in record;
4177
4800
  }
@@ -4186,26 +4809,35 @@ function extractTerminalOutputFromRecord(record) {
4186
4809
  function normalizeTerminalOutput(rawOutput, structuredOutput) {
4187
4810
  if (isRecord(structuredOutput)) {
4188
4811
  const terminalOutput = extractTerminalOutputFromRecord(structuredOutput);
4189
- if (terminalOutput) return stripAnsi(terminalOutput);
4812
+ if (terminalOutput) return terminalOutput;
4190
4813
  if (isStructuredTerminalRecord(structuredOutput)) return "";
4191
4814
  }
4192
4815
  if (!rawOutput) return "";
4193
4816
  const trimmed = rawOutput.trim();
4194
- if (!trimmed.startsWith("{")) return stripAnsi(rawOutput);
4817
+ if (!trimmed.startsWith("{")) return rawOutput;
4195
4818
  try {
4196
4819
  const parsed = JSON.parse(trimmed);
4197
- if (!isRecord(parsed)) return stripAnsi(rawOutput);
4820
+ if (!isRecord(parsed)) return rawOutput;
4198
4821
  const terminalOutput = extractTerminalOutputFromRecord(parsed);
4199
- if (terminalOutput) return stripAnsi(terminalOutput);
4822
+ if (terminalOutput) return terminalOutput;
4200
4823
  if (isStructuredTerminalRecord(parsed)) return "";
4201
- return stripAnsi(rawOutput);
4824
+ return rawOutput;
4202
4825
  } catch {
4203
- return stripAnsi(rawOutput);
4826
+ return rawOutput;
4204
4827
  }
4205
4828
  }
4206
4829
  function shouldAutoExpandRunningFileOperation(toolName) {
4207
4830
  return toolName === "write_file" || toolName === "edit_file";
4208
4831
  }
4832
+ function countFileOperationChanges(blocks) {
4833
+ return blocks.reduce((totals, block) => ({
4834
+ additions: totals.additions + block.lines.filter((line) => line.kind === "add").length,
4835
+ deletions: totals.deletions + block.lines.filter((line) => line.kind === "remove").length
4836
+ }), {
4837
+ additions: 0,
4838
+ deletions: 0
4839
+ });
4840
+ }
4209
4841
  function useToolCardExpandedState({ canExpand, isRunning, autoExpandWhileRunning = true, expandOnError = false, statusTone }) {
4210
4842
  const [expanded, setExpanded] = useState(false);
4211
4843
  const [hasUserToggled, setHasUserToggled] = useState(false);
@@ -4264,47 +4896,20 @@ function useToolCardExpandedState({ canExpand, isRunning, autoExpandWhileRunning
4264
4896
  onToggle
4265
4897
  };
4266
4898
  }
4267
- function GenericToolSection({ label, tone, children }) {
4268
- const style = {
4269
- input: {
4270
- shell: "border-border bg-card",
4271
- header: "border-border bg-muted/55 text-muted-foreground",
4272
- dot: "bg-muted-foreground/60",
4273
- body: "text-foreground"
4274
- },
4275
- output: {
4276
- shell: "border-border bg-card",
4277
- header: "border-border bg-muted/55 text-muted-foreground",
4278
- dot: "bg-primary/70",
4279
- body: "text-foreground"
4280
- },
4281
- error: {
4282
- shell: "border-rose-200/80 bg-rose-50/85",
4283
- header: "border-rose-200/80 bg-rose-100/80 text-rose-700",
4284
- dot: "bg-rose-500/80",
4285
- body: "text-rose-950/85"
4286
- }
4287
- }[tone];
4288
- return /* @__PURE__ */ jsxs("section", {
4289
- className: cn("overflow-hidden rounded-md border shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]", style.shell),
4290
- children: [/* @__PURE__ */ jsxs("div", {
4291
- className: cn("flex items-center gap-2 border-b px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.16em]", style.header),
4292
- children: [/* @__PURE__ */ jsx("span", { className: cn("h-1.5 w-1.5 rounded-full", style.dot) }), /* @__PURE__ */ jsx("span", { children: label })]
4293
- }), /* @__PURE__ */ jsx("div", {
4294
- className: "w-full overflow-hidden",
4295
- children: /* @__PURE__ */ jsx("pre", {
4296
- className: cn("w-full max-w-full min-w-0 max-h-64 overflow-x-auto overflow-y-auto px-3 py-2.5 font-mono text-[12px] leading-relaxed whitespace-pre custom-scrollbar", style.body),
4297
- children
4298
- })
4299
- })]
4300
- });
4899
+ function extractTerminalMeta(structuredOutput) {
4900
+ if (!isRecord(structuredOutput)) return {};
4901
+ return {
4902
+ exitCode: typeof structuredOutput.exitCode === "number" ? structuredOutput.exitCode : typeof structuredOutput.exit_code === "number" ? structuredOutput.exit_code : null,
4903
+ workingDir: readNonEmptyString(structuredOutput.workingDir) ?? readNonEmptyString(structuredOutput.cwd) ?? readNonEmptyString(structuredOutput.working_directory)
4904
+ };
4301
4905
  }
4302
- function TerminalExecutionView({ card }) {
4906
+ function TerminalExecutionView({ card, toolLabel }) {
4303
4907
  const output = normalizeTerminalOutput(card.output, card.outputData);
4304
4908
  const isRunning = card.statusTone === "running";
4305
4909
  const commandPart = card.summary?.replace(/^(command|path|args|query|input):\s*/i, "");
4306
4910
  const hasOutput = output.trim().length > 0;
4307
4911
  const canExpand = isRunning || hasOutput || Boolean(commandPart?.trim());
4912
+ const meta = extractTerminalMeta(card.outputData);
4308
4913
  const { expanded, onToggle } = useToolCardExpandedState({
4309
4914
  canExpand,
4310
4915
  isRunning,
@@ -4314,38 +4919,32 @@ function TerminalExecutionView({ card }) {
4314
4919
  });
4315
4920
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4316
4921
  card,
4922
+ toolLabel,
4317
4923
  icon: Terminal,
4318
4924
  expanded,
4319
4925
  canExpand,
4320
4926
  onToggle
4321
- }), expanded && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
4322
- className: "px-3 pb-2 font-mono w-full max-h-48 overflow-y-auto custom-scrollbar min-h-0 text-[12px]",
4323
- children: /* @__PURE__ */ jsxs("div", {
4324
- className: "flex items-start gap-2 leading-relaxed",
4325
- children: [/* @__PURE__ */ jsx("span", {
4326
- className: "text-primary/55 font-medium shrink-0 select-none mt-[1px]",
4327
- children: "$"
4328
- }), /* @__PURE__ */ jsx("div", {
4329
- className: "flex-1 min-w-0",
4330
- children: commandPart ? /* @__PURE__ */ jsxs("div", {
4331
- className: "text-foreground break-words whitespace-pre-wrap tracking-tight font-medium inline-block",
4332
- children: [commandPart, isRunning && !output && /* @__PURE__ */ jsx("span", { className: "inline-block w-1.5 h-3 ml-1 bg-primary/60 animate-pulse align-middle" })]
4333
- }) : /* @__PURE__ */ jsx("div", { className: "h-3 w-32 bg-muted rounded animate-pulse mt-2" })
4334
- })]
4927
+ }), expanded && /* @__PURE__ */ jsx(ToolCardContent, {
4928
+ className: "bg-transparent py-0",
4929
+ children: /* @__PURE__ */ jsx(ChatTerminalSurface, {
4930
+ command: commandPart,
4931
+ output,
4932
+ emptyLabel: card.emptyLabel,
4933
+ isRunning,
4934
+ hasOutput,
4935
+ isError: card.statusTone === "error",
4936
+ exitCode: meta.exitCode,
4937
+ workingDir: meta.workingDir
4335
4938
  })
4336
- }), (hasOutput || !isRunning) && /* @__PURE__ */ jsx(ToolCardContent, {
4337
- className: hasOutput ? void 0 : "bg-muted/20 py-2",
4338
- children: /* @__PURE__ */ jsxs("pre", {
4339
- className: cn("font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed", hasOutput ? "px-0" : "rounded-md border border-dashed border-border bg-card/70 px-3 py-2 italic"),
4340
- children: [hasOutput ? output : card.emptyLabel, isRunning && hasOutput && /* @__PURE__ */ jsx("span", { className: "inline-block w-1.5 h-3 ml-1 bg-primary/60 animate-pulse align-middle" })]
4341
- })
4342
- })] })] });
4939
+ })] });
4343
4940
  }
4344
- function FileOperationView({ card, onFileOpen }) {
4941
+ function FileOperationView({ card, toolLabel, onFileOpen }) {
4942
+ const input = card.input?.trim() ?? "";
4345
4943
  const output = card.output?.trim() ?? "";
4346
4944
  const isRunning = card.statusTone === "running";
4347
4945
  const hasStructuredPreview = Boolean(card.fileOperation?.blocks.length);
4348
- const hasContent = hasStructuredPreview || Boolean(output);
4946
+ const showRawInput = Boolean(input) && !hasStructuredPreview;
4947
+ const hasContent = hasStructuredPreview || Boolean(input) || Boolean(output);
4349
4948
  const previewBlocks = card.fileOperation?.blocks ?? [];
4350
4949
  const previewLineCount = previewBlocks.reduce((count, block) => count + block.lines.length, 0);
4351
4950
  const previewCharCount = previewBlocks.reduce((count, block) => {
@@ -4360,22 +4959,34 @@ function FileOperationView({ card, onFileOpen }) {
4360
4959
  expandOnError: hasContent,
4361
4960
  statusTone: card.statusTone
4362
4961
  });
4962
+ const isEdit = card.toolName === "edit_file" || card.toolName === "write_file" || card.toolName === "apply_patch" || card.toolName === "file_change";
4363
4963
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4364
4964
  card,
4365
- icon: card.toolName === "edit_file" || card.toolName === "write_file" || card.toolName === "apply_patch" || card.toolName === "file_change" ? Code2 : FileText,
4965
+ toolLabel,
4966
+ changeSummary: isEdit ? countFileOperationChanges(previewBlocks) : void 0,
4967
+ icon: isEdit ? Code2 : FileText,
4366
4968
  expanded,
4367
4969
  canExpand: hasContent || isRunning,
4368
- hideSummary: expanded && hasStructuredPreview,
4970
+ hideSummary: false,
4369
4971
  onToggle
4370
- }), expanded && hasContent && /* @__PURE__ */ jsx(ToolCardContent, {
4371
- className: "border-t border-border bg-transparent px-0 pt-0 pb-0",
4372
- children: /* @__PURE__ */ jsx(ToolCardFileOperationContent, {
4373
- card,
4374
- onFileOpen
4375
- })
4376
- })] });
4972
+ }), expanded && hasContent ? /* @__PURE__ */ jsxs(ToolCardContent, {
4973
+ className: "bg-transparent py-0",
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
+ ]
4987
+ }) : null] });
4377
4988
  }
4378
- function SearchSnippetView({ card }) {
4989
+ function SearchSnippetView({ card, toolLabel, icon: Icon = Search }) {
4379
4990
  const isRunning = card.statusTone === "running";
4380
4991
  const output = card.output?.trim() ?? "";
4381
4992
  const { expanded, onToggle } = useToolCardExpandedState({
@@ -4386,24 +4997,37 @@ function SearchSnippetView({ card }) {
4386
4997
  });
4387
4998
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4388
4999
  card,
4389
- icon: Search,
5000
+ toolLabel,
5001
+ icon: Icon,
4390
5002
  expanded,
4391
5003
  canExpand: !!output || isRunning,
4392
5004
  onToggle
4393
- }), expanded && output && /* @__PURE__ */ jsx(ToolCardContent, { children: /* @__PURE__ */ jsx("pre", {
4394
- className: "font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed",
4395
- children: output
4396
- }) })] });
5005
+ }), expanded && output && /* @__PURE__ */ jsx(ToolCardContent, {
5006
+ className: "py-0",
5007
+ children: /* @__PURE__ */ jsx("pre", {
5008
+ className: "font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed",
5009
+ children: output
5010
+ })
5011
+ })] });
4397
5012
  }
4398
- function GenericToolCard({ card, onToolAction, renderToolAgent }) {
5013
+ //#endregion
5014
+ //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-generic.tsx
5015
+ function buildToolActionSlot(card, onToolAction, renderToolAgent) {
5016
+ const agentAction = card.agentId && renderToolAgent ? renderToolAgent(card.agentId) : null;
5017
+ const toolAction = card.action && onToolAction ? /* @__PURE__ */ jsx(ToolCardHeaderAction, {
5018
+ action: card.action,
5019
+ onAction: onToolAction
5020
+ }) : null;
5021
+ return agentAction || toolAction ? /* @__PURE__ */ jsxs(Fragment$1, { children: [agentAction, toolAction] }) : void 0;
5022
+ }
5023
+ function GenericToolCard({ card, toolLabel, icon: Icon = Wrench, onToolAction, renderToolAgent }) {
4399
5024
  const input = card.input?.trim() ?? "";
4400
5025
  const output = card.output?.trim() ?? "";
4401
5026
  const isRunning = card.statusTone === "running";
4402
5027
  const hasInputSection = input.length > 0;
4403
5028
  const hasOutputSection = output.length > 0;
4404
5029
  const hasContent = hasInputSection || hasOutputSection;
4405
- const inputLabel = card.inputLabel?.trim() || "Input";
4406
- const outputLabel = card.outputLabel?.trim() || "Output";
5030
+ const actionSlot = buildToolActionSlot(card, onToolAction, renderToolAgent);
4407
5031
  const { expanded, onToggle } = useToolCardExpandedState({
4408
5032
  canExpand: hasContent || isRunning,
4409
5033
  isRunning,
@@ -4412,30 +5036,28 @@ function GenericToolCard({ card, onToolAction, renderToolAgent }) {
4412
5036
  });
4413
5037
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4414
5038
  card,
4415
- icon: Globe,
5039
+ toolLabel,
5040
+ icon: Icon,
4416
5041
  expanded,
4417
5042
  canExpand: hasContent || isRunning,
4418
- actionSlot: card.agentId || card.action && onToolAction ? /* @__PURE__ */ jsxs(Fragment$1, { children: [card.agentId && renderToolAgent ? renderToolAgent(card.agentId) : null, card.action && onToolAction ? /* @__PURE__ */ jsx(ToolCardHeaderAction, {
4419
- action: card.action,
4420
- onAction: onToolAction
4421
- }) : null] }) : void 0,
5043
+ actionSlot,
4422
5044
  onToggle
4423
- }), expanded && hasContent && /* @__PURE__ */ jsxs(ToolCardContent, {
4424
- className: "bg-transparent px-2.5 py-2.5",
5045
+ }), expanded && hasContent ? /* @__PURE__ */ jsxs(ToolCardContent, {
5046
+ className: "bg-transparent py-0",
4425
5047
  children: [
4426
- hasInputSection && /* @__PURE__ */ jsx(GenericToolSection, {
4427
- label: inputLabel,
5048
+ hasInputSection ? /* @__PURE__ */ jsx(ToolCardDetailSection, {
5049
+ label: card.inputLabel?.trim() || "Input",
4428
5050
  tone: "input",
4429
5051
  children: input
4430
- }),
4431
- hasInputSection && hasOutputSection && /* @__PURE__ */ jsx("div", { className: "h-2" }),
4432
- hasOutputSection && /* @__PURE__ */ jsx(GenericToolSection, {
4433
- label: outputLabel,
5052
+ }) : null,
5053
+ hasInputSection && hasOutputSection ? /* @__PURE__ */ jsx("div", { className: "h-2" }) : null,
5054
+ hasOutputSection ? /* @__PURE__ */ jsx(ToolCardDetailSection, {
5055
+ label: card.outputLabel?.trim() || "Output",
4434
5056
  tone: card.statusTone === "error" ? "error" : "output",
4435
5057
  children: output
4436
- })
5058
+ }) : null
4437
5059
  ]
4438
- })] });
5060
+ }) : null] });
4439
5061
  }
4440
5062
  //#endregion
4441
5063
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-panel-app.tsx
@@ -4457,34 +5079,364 @@ function isTerminalTool(name) {
4457
5079
  const lowered = name.toLowerCase();
4458
5080
  return lowered === "exec" || lowered === "exec_command" || lowered === "execute_command" || lowered === "command_execution" || lowered === "bash" || lowered === "shell" || lowered.includes("run_");
4459
5081
  }
4460
- function isFileEditTool(name) {
5082
+ function isFileTool(name) {
4461
5083
  const lowered = name.toLowerCase();
4462
5084
  return lowered === "read_file" || lowered === "write_file" || lowered === "edit_file" || lowered === "apply_patch" || lowered === "file_change";
4463
5085
  }
5086
+ function isFileWriteTool(name) {
5087
+ const lowered = name.toLowerCase();
5088
+ return lowered === "write_file" || lowered === "edit_file" || lowered === "apply_patch" || lowered === "file_change";
5089
+ }
4464
5090
  function isSearchTool(name) {
4465
5091
  const lowered = name.toLowerCase();
4466
5092
  return lowered === "grep_search" || lowered === "find_files" || lowered.includes("search");
4467
5093
  }
4468
- function ChatToolCard({ card, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard }) {
5094
+ const BUILT_IN_TOOL_PRESENTATIONS = {
5095
+ list_dir: {
5096
+ statusKind: "directory",
5097
+ icon: FolderTree
5098
+ },
5099
+ web_fetch: {
5100
+ statusKind: "web",
5101
+ icon: Globe2
5102
+ },
5103
+ message: {
5104
+ statusKind: "message",
5105
+ icon: Send
5106
+ },
5107
+ sessions_list: {
5108
+ statusKind: "session",
5109
+ icon: MessageSquare
5110
+ },
5111
+ sessions_history: {
5112
+ statusKind: "session",
5113
+ icon: MessageSquare
5114
+ },
5115
+ sessions_request: {
5116
+ statusKind: "session",
5117
+ icon: MessageSquare
5118
+ },
5119
+ spawn: {
5120
+ statusKind: "agent",
5121
+ icon: Bot
5122
+ },
5123
+ sessions_spawn: {
5124
+ statusKind: "agent",
5125
+ icon: Bot
5126
+ },
5127
+ subagents: {
5128
+ statusKind: "agent",
5129
+ icon: Bot
5130
+ },
5131
+ memory_search: {
5132
+ statusKind: "memory",
5133
+ icon: Brain
5134
+ },
5135
+ memory_get: {
5136
+ statusKind: "memory",
5137
+ icon: Brain
5138
+ },
5139
+ cron: {
5140
+ statusKind: "schedule",
5141
+ icon: CalendarClock
5142
+ },
5143
+ gateway: {
5144
+ statusKind: "system",
5145
+ icon: Settings
5146
+ },
5147
+ view_image: {
5148
+ statusKind: "image",
5149
+ icon: ImageIcon
5150
+ },
5151
+ show_file: {
5152
+ statusKind: "display",
5153
+ icon: Eye
5154
+ },
5155
+ show_url: {
5156
+ statusKind: "display",
5157
+ icon: Eye
5158
+ },
5159
+ show_panel_app: {
5160
+ statusKind: "display",
5161
+ icon: Eye
5162
+ },
5163
+ show_content: {
5164
+ statusKind: "display",
5165
+ icon: Eye
5166
+ }
5167
+ };
5168
+ function resolveBuiltInToolPresentation(name) {
5169
+ return BUILT_IN_TOOL_PRESENTATIONS[name.toLowerCase()] ?? null;
5170
+ }
5171
+ function ChatToolCard({ card, toolStatusLabels, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard }) {
4469
5172
  if (card.panelApp) return /* @__PURE__ */ jsx(PanelAppInlineToolCard, {
4470
5173
  card,
4471
5174
  onToolAction,
4472
5175
  renderPanelAppCard
4473
5176
  });
4474
- if (isTerminalTool(card.toolName)) return /* @__PURE__ */ jsx(TerminalExecutionView, { card });
4475
- if (isFileEditTool(card.toolName)) return /* @__PURE__ */ jsx(FileOperationView, {
5177
+ const builtInPresentation = resolveBuiltInToolPresentation(card.toolName);
5178
+ const builtInLabel = builtInPresentation ? toolStatusLabels?.builtIn?.[builtInPresentation.statusKind]?.[card.statusTone] : void 0;
5179
+ if (isTerminalTool(card.toolName)) return /* @__PURE__ */ jsx(TerminalExecutionView, {
5180
+ card,
5181
+ toolLabel: toolStatusLabels?.terminal[card.statusTone]
5182
+ });
5183
+ if (isFileTool(card.toolName)) return /* @__PURE__ */ jsx(FileOperationView, {
4476
5184
  card,
5185
+ toolLabel: isFileWriteTool(card.toolName) ? toolStatusLabels?.fileEdit[card.statusTone] : toolStatusLabels?.fileRead[card.statusTone],
4477
5186
  onFileOpen
4478
5187
  });
4479
- if (isSearchTool(card.toolName)) return /* @__PURE__ */ jsx(SearchSnippetView, { card });
5188
+ if (isSearchTool(card.toolName)) return /* @__PURE__ */ jsx(SearchSnippetView, {
5189
+ card,
5190
+ toolLabel: builtInLabel ?? toolStatusLabels?.search[card.statusTone],
5191
+ icon: builtInPresentation?.icon
5192
+ });
4480
5193
  return /* @__PURE__ */ jsx(GenericToolCard, {
4481
5194
  card,
5195
+ toolLabel: builtInLabel,
5196
+ icon: builtInPresentation?.icon,
4482
5197
  onToolAction,
4483
5198
  renderToolAgent
4484
5199
  });
4485
5200
  }
4486
5201
  //#endregion
5202
+ //#region src/components/chat/ui/chat-message-list/chat-tool-activity-group.tsx
5203
+ function ChatToolActivityGroup({ group, open, isUser, reasoningCharacterCountTemplates, toolStatusLabels, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard, onOpenChange }) {
5204
+ const toolCount = group.parts.filter((part) => part.type === "tool-card").length;
5205
+ const showWorkflowRail = open && toolCount > 1;
5206
+ return /* @__PURE__ */ jsxs("div", {
5207
+ className: "group/tool-activity",
5208
+ children: [/* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
5209
+ openGroup: "tool-activity",
5210
+ open,
5211
+ icon: Workflow,
5212
+ leadingIconClassName: "bg-card",
5213
+ label: group.label,
5214
+ onClick: () => onOpenChange(!open)
5215
+ }), open ? /* @__PURE__ */ jsx("div", {
5216
+ className: "text-[0.925rem] leading-[1.72]",
5217
+ children: group.parts.map((part, index) => {
5218
+ const isLast = index === group.parts.length - 1;
5219
+ return /* @__PURE__ */ jsxs("div", {
5220
+ className: "relative min-w-0",
5221
+ children: [showWorkflowRail ? /* @__PURE__ */ jsx(ChatProcessWorkflowRail, { position: index === 0 ? "first" : isLast ? "last" : "middle" }) : null, part.type === "tool-card" ? /* @__PURE__ */ jsx(ChatToolCard, {
5222
+ card: part.card,
5223
+ toolStatusLabels,
5224
+ onToolAction,
5225
+ onFileOpen,
5226
+ renderToolAgent,
5227
+ renderPanelAppCard
5228
+ }) : /* @__PURE__ */ jsx(ChatReasoningBlock, {
5229
+ label: part.label,
5230
+ text: part.text,
5231
+ characterCountTemplates: reasoningCharacterCountTemplates,
5232
+ isUser,
5233
+ isInProgress: false
5234
+ })]
5235
+ }, `tool-group-item-${group.startIndex + index}`);
5236
+ })
5237
+ }) : null]
5238
+ });
5239
+ }
5240
+ //#endregion
5241
+ //#region src/components/chat/ui/chat-message-list/chat-tool-activity-group.utils.ts
5242
+ const MAX_SUMMARY_SEGMENTS = 3;
5243
+ function isToolCardPart(part) {
5244
+ return part.type === "tool-card";
5245
+ }
5246
+ function resolveToolActivityFamily(toolName) {
5247
+ const lowered = toolName.toLowerCase();
5248
+ if (lowered === "list_dir") return "directory";
5249
+ if (lowered.includes("show_content") || lowered.includes("panel_app") || lowered.includes("panel")) return "panel";
5250
+ if (lowered === "spawn" || lowered.includes("subagent") || lowered.includes("session_request") || lowered.includes("agent") && !lowered.includes("read")) return "agent";
5251
+ if (lowered.includes("web_") || lowered.includes("_web") || lowered.includes("url") || lowered.includes("fetch") || lowered.includes("http")) return "web";
5252
+ if (lowered === "grep_search" || lowered === "find_files" || lowered === "search_files" || lowered.includes("search") || lowered.includes("grep")) return "search";
5253
+ if (lowered === "exec" || lowered === "exec_command" || lowered === "execute_command" || lowered === "command_execution" || lowered === "bash" || lowered === "shell" || lowered === "terminal" || lowered.includes("run_")) return "bash";
5254
+ if (lowered === "write_file" || lowered === "edit_file" || lowered === "apply_patch" || lowered === "file_change" || lowered === "multi_replace" || lowered.includes("write_file") || lowered.includes("edit_file") || lowered.includes("apply_patch")) return "edit";
5255
+ if (lowered === "read_file" || lowered === "read" || lowered === "view_file" || lowered.includes("read_file") || lowered.startsWith("read_")) return "read";
5256
+ return "other";
5257
+ }
5258
+ function toneRank(tone) {
5259
+ if (tone === "error") return 0;
5260
+ if (tone === "cancelled") return 1;
5261
+ if (tone === "running") return 2;
5262
+ return 3;
5263
+ }
5264
+ function resolveActivityUnitKeys(card, family, callIndex) {
5265
+ if (family !== "read" && family !== "edit" && family !== "directory") return [`call:${callIndex}`];
5266
+ const paths = card.fileOperation?.blocks.map(({ path }) => path.trim()).filter(Boolean);
5267
+ const summary = card.summary?.replace(/^(path|file):\s*/i, "").trim();
5268
+ return paths?.length ? paths : [summary || `call:${callIndex}`];
5269
+ }
5270
+ /**
5271
+ * Natural category+count phrases only.
5272
+ * Examples: `Read 3 files`, `运行 2 条命令`, `搜索 2 次`.
5273
+ * No command/path payload.
5274
+ */
5275
+ function formatSegment(segment, labels) {
5276
+ const template = labels.segmentTemplates[segment.family];
5277
+ const body = segment.count === 1 ? template.one : template.other.split("{count}").join(String(segment.count));
5278
+ if (segment.tone === "error") return `${body} ${labels.failedLabel}`;
5279
+ if (segment.tone === "cancelled") return `${body} ${labels.cancelledLabel}`;
5280
+ return body;
5281
+ }
5282
+ function buildToolActivitySegments(cards) {
5283
+ const order = [];
5284
+ const byFamily = /* @__PURE__ */ new Map();
5285
+ for (const [callIndex, card] of cards.entries()) {
5286
+ const family = resolveToolActivityFamily(card.toolName);
5287
+ const unitKeys = resolveActivityUnitKeys(card, family, callIndex);
5288
+ const existing = byFamily.get(family);
5289
+ if (!existing) {
5290
+ order.push(family);
5291
+ byFamily.set(family, {
5292
+ units: new Set(unitKeys),
5293
+ tone: card.statusTone
5294
+ });
5295
+ continue;
5296
+ }
5297
+ unitKeys.forEach((unitKey) => existing.units.add(unitKey));
5298
+ if (toneRank(card.statusTone) < toneRank(existing.tone)) existing.tone = card.statusTone;
5299
+ }
5300
+ const segments = order.map((family) => {
5301
+ const entry = byFamily.get(family);
5302
+ return {
5303
+ family,
5304
+ count: entry.units.size,
5305
+ tone: entry.tone
5306
+ };
5307
+ });
5308
+ segments.sort((left, right) => {
5309
+ const toneDelta = toneRank(left.tone) - toneRank(right.tone);
5310
+ if (toneDelta !== 0) return toneDelta;
5311
+ return order.indexOf(left.family) - order.indexOf(right.family);
5312
+ });
5313
+ return segments;
5314
+ }
5315
+ function formatToolActivityGroupLabel(params) {
5316
+ const { labels, segments } = params;
5317
+ if (segments.length === 0) return "";
5318
+ const visible = segments.slice(0, MAX_SUMMARY_SEGMENTS);
5319
+ const hiddenCount = segments.length - visible.length;
5320
+ const parts = visible.map((segment) => formatSegment(segment, labels));
5321
+ if (hiddenCount > 0) parts.push(`+${hiddenCount}`);
5322
+ return parts.join(" · ");
5323
+ }
5324
+ /**
5325
+ * Group tool cards separated only by reasoning into one visible workflow.
5326
+ * Markdown, files and unknown parts still break the run.
5327
+ * Single tool-cards stay ungrouped so they keep their native card UI.
5328
+ */
5329
+ function groupConsecutiveToolParts(parts, labels) {
5330
+ const blocks = [];
5331
+ let index = 0;
5332
+ while (index < parts.length) {
5333
+ const part = parts[index];
5334
+ if (part.type !== "tool-card") {
5335
+ blocks.push({
5336
+ kind: "part",
5337
+ key: `part-${index}`,
5338
+ index,
5339
+ part
5340
+ });
5341
+ index += 1;
5342
+ continue;
5343
+ }
5344
+ const startIndex = index;
5345
+ const candidateParts = [];
5346
+ let lastToolOffset = -1;
5347
+ while (index < parts.length && (parts[index]?.type === "tool-card" || parts[index]?.type === "reasoning")) {
5348
+ const candidate = parts[index];
5349
+ candidateParts.push(candidate);
5350
+ if (candidate.type === "tool-card") lastToolOffset = candidateParts.length - 1;
5351
+ index += 1;
5352
+ }
5353
+ const toolParts = candidateParts.filter(isToolCardPart);
5354
+ if (toolParts.length === 1) {
5355
+ blocks.push({
5356
+ kind: "part",
5357
+ key: `part-${startIndex}`,
5358
+ index: startIndex,
5359
+ part
5360
+ });
5361
+ index = startIndex + 1;
5362
+ continue;
5363
+ }
5364
+ const groupedParts = candidateParts.slice(0, lastToolOffset + 1);
5365
+ index = startIndex + groupedParts.length;
5366
+ const segments = buildToolActivitySegments(toolParts.map((toolPart) => toolPart.card));
5367
+ const label = formatToolActivityGroupLabel({
5368
+ segments,
5369
+ labels
5370
+ });
5371
+ blocks.push({
5372
+ kind: "tool-group",
5373
+ key: `tool-group-${startIndex}`,
5374
+ group: {
5375
+ key: `tool-group-${startIndex}`,
5376
+ startIndex,
5377
+ endIndex: index - 1,
5378
+ parts: groupedParts,
5379
+ segments,
5380
+ label
5381
+ }
5382
+ });
5383
+ }
5384
+ return blocks;
5385
+ }
5386
+ //#endregion
4487
5387
  //#region src/components/chat/ui/chat-message-list/chat-message.tsx
5388
+ const DEFAULT_TOOL_ACTIVITY_LABELS = {
5389
+ segmentTemplates: {
5390
+ read: {
5391
+ one: "Read 1 file",
5392
+ other: "Read {count} files"
5393
+ },
5394
+ edit: {
5395
+ one: "Edit 1 file",
5396
+ other: "Edit {count} files"
5397
+ },
5398
+ directory: {
5399
+ one: "View 1 directory",
5400
+ other: "View {count} directories"
5401
+ },
5402
+ search: {
5403
+ one: "Search 1 time",
5404
+ other: "Search {count} times"
5405
+ },
5406
+ bash: {
5407
+ one: "Run 1 command",
5408
+ other: "Run {count} commands"
5409
+ },
5410
+ web: {
5411
+ one: "Open 1 page",
5412
+ other: "Open {count} pages"
5413
+ },
5414
+ agent: {
5415
+ one: "Start 1 subtask",
5416
+ other: "Start {count} subtasks"
5417
+ },
5418
+ panel: {
5419
+ one: "Show 1 result",
5420
+ other: "Show {count} results"
5421
+ },
5422
+ other: {
5423
+ one: "Use 1 tool",
5424
+ other: "Use {count} tools"
5425
+ }
5426
+ },
5427
+ failedLabel: "failed",
5428
+ cancelledLabel: "cancelled"
5429
+ };
5430
+ function resolveToolActivityLabels(texts) {
5431
+ return {
5432
+ segmentTemplates: {
5433
+ ...DEFAULT_TOOL_ACTIVITY_LABELS.segmentTemplates,
5434
+ ...texts.toolActivitySegmentTemplates ?? {}
5435
+ },
5436
+ failedLabel: texts.toolActivityFailedLabel ?? DEFAULT_TOOL_ACTIVITY_LABELS.failedLabel,
5437
+ cancelledLabel: texts.toolActivityCancelledLabel ?? DEFAULT_TOOL_ACTIVITY_LABELS.cancelledLabel
5438
+ };
5439
+ }
4488
5440
  function isMessageInProgress(status) {
4489
5441
  return status === "pending" || status === "streaming";
4490
5442
  }
@@ -4504,7 +5456,7 @@ function splitAssistantProcess(message) {
4504
5456
  finalParts: message.parts.slice(lastProcessPartIndex + 1)
4505
5457
  };
4506
5458
  }
4507
- function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFileOpen, 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 }) {
4508
5460
  const { type } = part;
4509
5461
  if (type === "markdown") {
4510
5462
  const { inlineTokens, text } = part;
@@ -4514,6 +5466,8 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4514
5466
  texts,
4515
5467
  inlineTokens,
4516
5468
  onFileOpen,
5469
+ onInlineTokenClick,
5470
+ resolveFileContentUrl,
4517
5471
  renderInlineDisplay
4518
5472
  }, `markdown-${index}`);
4519
5473
  }
@@ -4522,21 +5476,23 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4522
5476
  return /* @__PURE__ */ jsx(ChatReasoningBlock, {
4523
5477
  label,
4524
5478
  text,
5479
+ characterCountTemplates: texts.reasoningCharacterCountTemplates,
4525
5480
  isUser,
4526
5481
  isInProgress: isInProgress && isLastPart
4527
5482
  }, `reasoning-${index}`);
4528
5483
  }
4529
5484
  if (type === "tool-card") {
4530
5485
  const { card } = part;
4531
- return /* @__PURE__ */ jsx("div", {
4532
- className: "mt-0.5",
4533
- children: /* @__PURE__ */ jsx(ChatToolCard, {
5486
+ return /* @__PURE__ */ jsxs("div", {
5487
+ className: "relative min-w-0",
5488
+ children: [/* @__PURE__ */ jsx(ChatProcessWorkflowRail, { position: "single" }), /* @__PURE__ */ jsx(ChatToolCard, {
4534
5489
  card,
5490
+ toolStatusLabels: texts.toolStatusLabels,
4535
5491
  onToolAction,
4536
5492
  onFileOpen,
4537
5493
  renderToolAgent,
4538
5494
  renderPanelAppCard
4539
- })
5495
+ })]
4540
5496
  }, `tool-${index}`);
4541
5497
  }
4542
5498
  if (type === "file") {
@@ -4544,7 +5500,8 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4544
5500
  return /* @__PURE__ */ jsx(ChatMessageFile, {
4545
5501
  file,
4546
5502
  isUser,
4547
- texts
5503
+ texts,
5504
+ onOpen: onAttachmentOpen
4548
5505
  }, `file-${index}`);
4549
5506
  }
4550
5507
  if (type === "unknown") {
@@ -4566,49 +5523,135 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4566
5523
  }
4567
5524
  return null;
4568
5525
  }
4569
- const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, onFileOpen, renderInlineDisplay, renderToolAgent, renderPanelAppCard }) {
5526
+ function renderMessageParts(params) {
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}`);
5536
+ if (block.kind === "tool-group") return /* @__PURE__ */ jsx(ChatToolActivityGroup, {
5537
+ group: block.group,
5538
+ open: openToolGroupKeys.has(block.group.key),
5539
+ isUser,
5540
+ reasoningCharacterCountTemplates: texts.reasoningCharacterCountTemplates,
5541
+ toolStatusLabels: texts.toolStatusLabels,
5542
+ onToolAction,
5543
+ onFileOpen,
5544
+ renderToolAgent,
5545
+ renderPanelAppCard,
5546
+ onOpenChange: (open) => onToolActivityOpenChange(block.group.key, open)
5547
+ }, block.key);
5548
+ return renderChatMessagePart({
5549
+ part: block.part,
5550
+ index: indexOffset + block.index,
5551
+ role,
5552
+ isUser,
5553
+ isInProgress,
5554
+ isLastPart: indexOffset + block.index === indexOffset + parts.length - 1,
5555
+ texts,
5556
+ onToolAction,
5557
+ onFileOpen,
5558
+ onAttachmentOpen,
5559
+ onInlineTokenClick,
5560
+ resolveFileContentUrl,
5561
+ renderInlineDisplay,
5562
+ renderToolAgent,
5563
+ renderPanelAppCard
5564
+ });
5565
+ });
5566
+ }
5567
+ const ChatMessage = memo(function ChatMessage({ layout, message, texts, onToolAction, onFileOpen, onAttachmentOpen, onInlineTokenClick, renderInlineDisplay, renderToolAgent, renderPanelAppCard, resolveFileContentUrl }) {
4570
5568
  const { role } = message;
4571
5569
  const isUser = role === "user";
5570
+ const isFlat = layout === "flat" && !isUser;
4572
5571
  const isInProgress = isMessageInProgress(message.status);
4573
5572
  const processSplit = splitAssistantProcess(message);
4574
- const renderPart = (part, index) => renderChatMessagePart({
4575
- part,
4576
- index,
4577
- role,
4578
- isUser,
4579
- isInProgress,
4580
- isLastPart: index === message.parts.length - 1,
4581
- texts,
4582
- onToolAction,
4583
- onFileOpen,
4584
- renderInlineDisplay,
4585
- renderToolAgent,
4586
- renderPanelAppCard
4587
- });
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
+ };
4588
5584
  return /* @__PURE__ */ jsx("div", {
4589
- 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")),
4590
5587
  children: /* @__PURE__ */ jsx("div", {
4591
- className: "space-y-2",
4592
- children: processSplit ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("details", {
4593
- className: "group/process text-xs text-muted-foreground",
4594
- children: [/* @__PURE__ */ jsxs("summary", {
4595
- className: "flex cursor-pointer list-none items-center gap-1.5 border-b border-border/60 pb-2 font-medium transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 [&::-webkit-details-marker]:hidden",
4596
- children: [
4597
- /* @__PURE__ */ jsx(ChevronRight, {
4598
- className: "h-3.5 w-3.5 group-open/process:hidden",
4599
- strokeWidth: 2.5
4600
- }),
4601
- /* @__PURE__ */ jsx(ChevronDown, {
4602
- className: "hidden h-3.5 w-3.5 group-open/process:block",
4603
- strokeWidth: 2.5
4604
- }),
4605
- /* @__PURE__ */ jsx("span", { children: message.processSummary?.label })
4606
- ]
4607
- }), /* @__PURE__ */ jsx("div", {
4608
- className: "space-y-2 pt-2",
4609
- children: processSplit.processParts.map(renderPart)
4610
- })]
4611
- }), processSplit.finalParts.map((part, index) => renderPart(part, processSplit.processParts.length + index))] }) : message.parts.map(renderPart)
5588
+ className: "space-y-0",
5589
+ children: processSplit ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
5590
+ className: "group/process",
5591
+ children: [/* @__PURE__ */ jsx("div", {
5592
+ className: "mb-2 border-b border-border/60 pb-2",
5593
+ children: /* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
5594
+ openGroup: "process",
5595
+ open: processOpen,
5596
+ icon: ListChecks,
5597
+ leadingIconClassName: "bg-card",
5598
+ label: message.processSummary?.label,
5599
+ onClick: () => setProcessOpen((current) => !current)
5600
+ })
5601
+ }), processOpen ? /* @__PURE__ */ jsx("div", {
5602
+ className: "space-y-0",
5603
+ children: renderMessageParts({
5604
+ parts: processSplit.processParts,
5605
+ role,
5606
+ isUser,
5607
+ isInProgress,
5608
+ texts,
5609
+ onToolAction,
5610
+ onFileOpen,
5611
+ onAttachmentOpen,
5612
+ onInlineTokenClick,
5613
+ resolveFileContentUrl,
5614
+ openToolGroupKeys,
5615
+ onToolActivityOpenChange: handleToolActivityOpenChange,
5616
+ renderInlineDisplay,
5617
+ renderToolAgent,
5618
+ renderPanelAppCard
5619
+ })
5620
+ }) : null]
5621
+ }), renderMessageParts({
5622
+ parts: processSplit.finalParts,
5623
+ role,
5624
+ isUser,
5625
+ isInProgress,
5626
+ texts,
5627
+ onToolAction,
5628
+ onFileOpen,
5629
+ onAttachmentOpen,
5630
+ onInlineTokenClick,
5631
+ resolveFileContentUrl,
5632
+ openToolGroupKeys,
5633
+ onToolActivityOpenChange: handleToolActivityOpenChange,
5634
+ renderInlineDisplay,
5635
+ renderToolAgent,
5636
+ renderPanelAppCard,
5637
+ indexOffset: processSplit.processParts.length
5638
+ })] }) : renderMessageParts({
5639
+ parts: message.parts,
5640
+ role,
5641
+ isUser,
5642
+ isInProgress,
5643
+ texts,
5644
+ onToolAction,
5645
+ onFileOpen,
5646
+ onAttachmentOpen,
5647
+ onInlineTokenClick,
5648
+ resolveFileContentUrl,
5649
+ openToolGroupKeys,
5650
+ onToolActivityOpenChange: handleToolActivityOpenChange,
5651
+ renderInlineDisplay,
5652
+ renderToolAgent,
5653
+ renderPanelAppCard
5654
+ })
4612
5655
  })
4613
5656
  });
4614
5657
  });
@@ -4645,7 +5688,6 @@ const TYPING_TEXT_SHEEN_CSS = `
4645
5688
  background-position: -60% 0;
4646
5689
  }
4647
5690
  }
4648
-
4649
5691
  .nextclaw-chat-typing-indicator__text {
4650
5692
  background-image: linear-gradient(
4651
5693
  100deg,
@@ -4699,16 +5741,17 @@ function ChatMessageTypingFooter() {
4699
5741
  })
4700
5742
  });
4701
5743
  }
4702
- function ChatTypingIndicator({ label }) {
5744
+ function ChatTypingIndicator({ label, layout }) {
4703
5745
  return /* @__PURE__ */ jsxs("div", {
4704
- 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"),
4705
5748
  children: [/* @__PURE__ */ jsx("style", { children: TYPING_TEXT_SHEEN_CSS }), /* @__PURE__ */ jsx("span", {
4706
5749
  className: "nextclaw-chat-typing-indicator__text",
4707
5750
  children: label
4708
5751
  })]
4709
5752
  });
4710
5753
  }
4711
- function ChatMessageList({ className, isSending, messages, onFileOpen, onToolAction, renderInlineDisplay, renderPanelAppCard, renderToolAgent, texts }) {
5754
+ function ChatMessageList({ className, isSending, layout = "card", messages, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, renderInlineDisplay, renderPanelAppCard, renderToolAgent, resolveFileContentUrl, texts }) {
4712
5755
  const visibleMessages = messages.filter(hasRenderableMessageContent);
4713
5756
  const hasRenderableAssistantDraft = visibleMessages.some((message) => message.role === "assistant" && (message.status === "streaming" || message.status === "pending"));
4714
5757
  return /* @__PURE__ */ jsxs("div", {
@@ -4716,21 +5759,63 @@ function ChatMessageList({ className, isSending, messages, onFileOpen, onToolAct
4716
5759
  children: [visibleMessages.map((message) => {
4717
5760
  const isUser = message.role === "user";
4718
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);
4719
5811
  return /* @__PURE__ */ jsxs("div", {
5812
+ "data-chat-message-layout": "card",
4720
5813
  className: cn("flex gap-3", isUser ? "justify-end" : "justify-start"),
4721
5814
  children: [
4722
5815
  !isUser ? /* @__PURE__ */ jsx(ChatMessageAvatar, { role: message.role }) : null,
4723
5816
  /* @__PURE__ */ jsxs("div", {
4724
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"),
4725
- children: [/* @__PURE__ */ jsx(ChatMessage, {
4726
- message,
4727
- texts,
4728
- onToolAction,
4729
- onFileOpen,
4730
- renderInlineDisplay,
4731
- renderToolAgent,
4732
- renderPanelAppCard
4733
- }), /* @__PURE__ */ jsx("div", {
5818
+ children: [content, /* @__PURE__ */ jsx("div", {
4734
5819
  className: cn("flex items-center gap-2", isUser && "justify-end"),
4735
5820
  children: isGenerating ? /* @__PURE__ */ jsx(ChatMessageTypingFooter, {}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
4736
5821
  className: cn("px-1 text-[11px] leading-4 text-muted-foreground", isUser ? "text-right" : "text-left"),
@@ -4749,8 +5834,15 @@ function ChatMessageList({ className, isSending, messages, onFileOpen, onToolAct
4749
5834
  ]
4750
5835
  }, message.id);
4751
5836
  }), isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs("div", {
5837
+ "data-chat-message-layout": layout,
4752
5838
  className: "flex justify-start gap-3",
4753
- 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
+ })]
4754
5846
  }) : null]
4755
5847
  });
4756
5848
  }