@nextclaw/agent-chat-ui 0.6.3 → 0.6.5

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
@@ -1,18 +1,18 @@
1
1
  import * as React from "react";
2
- import { Fragment, forwardRef, memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { Fragment, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { clsx } from "clsx";
4
4
  import { twMerge } from "tailwind-merge";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import * as PopoverPrimitive from "@radix-ui/react-popover";
7
7
  import * as SelectPrimitive from "@radix-ui/react-select";
8
- import { AlertTriangle, AppWindow, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, CalendarClock, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, Expand, ExternalLink, Eye, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, FolderTree, Globe, Globe2, ImageIcon, Loader2, MessageSquare, Minus, Paperclip, Puzzle, Search, Send, Settings, Sparkles, Star, Terminal, User, Wrench, X } from "lucide-react";
8
+ import { AlertTriangle, AppWindow, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, CalendarClock, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, Expand, ExternalLink, Eye, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, FolderTree, Globe, Globe2, ImageIcon, ListChecks, Loader2, MessageSquare, Minus, Paperclip, Puzzle, Search, Send, Settings, Sparkles, Star, Terminal, User, Workflow, Wrench, X } from "lucide-react";
9
9
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
10
10
  import { cva } from "class-variance-authority";
11
11
  import { ContentEditable } from "@lexical/react/LexicalContentEditable";
12
12
  import { LexicalComposer } from "@lexical/react/LexicalComposer";
13
13
  import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
14
14
  import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
15
- import { $applyNodeReplacement, $createLineBreakNode, $createParagraphNode, $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isTextNode, $setSelection, BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, DecoratorNode, KEY_DOWN_COMMAND, SELECTION_CHANGE_COMMAND, mergeRegister } from "lexical";
15
+ import { $applyNodeReplacement, $createLineBreakNode, $createParagraphNode, $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isTextNode, $setSelection, BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, DecoratorNode, KEY_DOWN_COMMAND, SELECTION_CHANGE_COMMAND, SKIP_DOM_SELECTION_TAG, mergeRegister } from "lexical";
16
16
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
17
17
  import ReactMarkdown from "react-markdown";
18
18
  import remarkGfm from "remark-gfm";
@@ -513,7 +513,7 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
513
513
  children: [/* @__PURE__ */ jsxs("div", {
514
514
  className: "flex min-h-0 flex-col border-r border-gray-200",
515
515
  children: [!isLoading && filterViews.length > 0 ? /* @__PURE__ */ jsx("div", {
516
- className: "flex shrink-0 gap-1 overflow-x-auto px-2.5 pb-2 pt-2.5",
516
+ className: "flex shrink-0 gap-0.5 overflow-x-auto px-2 pb-1.5 pt-2",
517
517
  children: filterViews.map(({ count, key, label }) => {
518
518
  const isActive = key === resolvedActiveFilterKey;
519
519
  return /* @__PURE__ */ jsxs("button", {
@@ -521,9 +521,9 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
521
521
  "aria-pressed": isActive,
522
522
  onPointerDown: (event) => event.preventDefault(),
523
523
  onClick: () => handleFilterSelect(key),
524
- className: `inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-1 text-[11px] font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${isActive ? "border-primary/30 bg-primary/10 text-primary" : "border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50"}`,
524
+ className: `inline-flex h-7 shrink-0 items-center gap-1 rounded-md px-2 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}`,
525
525
  children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
526
- className: isActive ? "text-primary/70" : "text-gray-400",
526
+ className: isActive ? "text-gray-500" : "text-gray-400",
527
527
  children: count
528
528
  })]
529
529
  }, key);
@@ -532,58 +532,52 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
532
532
  ref: listRef,
533
533
  role: "listbox",
534
534
  "aria-label": texts.sectionLabel,
535
- className: `custom-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain ${!isLoading && filterViews.length > 0 ? "px-2.5 pb-2.5" : "p-2.5"}`,
535
+ className: `custom-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain ${!isLoading && filterViews.length > 0 ? "px-2 pb-2" : "p-2"}`,
536
536
  children: isLoading ? /* @__PURE__ */ jsx("div", {
537
537
  className: "p-2 text-xs text-gray-500",
538
538
  children: texts.loadingLabel
539
539
  }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [!hasItemSections ? /* @__PURE__ */ jsx("div", {
540
- className: "mb-2 px-2 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
540
+ className: "mb-1 px-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
541
541
  children: texts.sectionLabel
542
542
  }) : null, visibleItems.length === 0 ? /* @__PURE__ */ jsx("div", {
543
- className: "px-2 text-xs text-gray-400",
543
+ className: "px-1.5 text-xs text-gray-400",
544
544
  children: texts.emptyLabel
545
- }) : /* @__PURE__ */ jsx("div", {
546
- className: "space-y-1",
547
- children: visibleItems.map((item, index) => {
548
- const { key, sectionKey, sectionLabel, title, subtitle } = item;
549
- const isActive = index === activeIndexInRange;
550
- const previousItem = visibleItems[index - 1];
551
- return /* @__PURE__ */ jsxs("div", {
552
- className: "space-y-1",
553
- children: [hasItemSections && Boolean(sectionLabel?.trim()) && previousItem?.sectionKey !== sectionKey ? /* @__PURE__ */ jsx("div", {
554
- className: "px-2 pb-0.5 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
555
- children: sectionLabel
556
- }) : null, /* @__PURE__ */ jsxs("button", {
557
- type: "button",
558
- role: "option",
559
- "aria-selected": isActive,
560
- "data-input-surface-index": index,
561
- onPointerMove: (event) => {
562
- if (event.pointerType !== "touch") setActiveIndexForCurrentItems(index);
563
- },
564
- onPointerDown: (event) => {
565
- if (event.button > 0) return;
566
- event.preventDefault();
567
- onSelectItem(item);
568
- },
569
- onClick: (event) => {
570
- if (event.detail === 0) onSelectItem(item);
571
- },
572
- className: `flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left transition ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-50"}`,
573
- children: [/* @__PURE__ */ jsx("span", {
574
- className: "truncate text-xs font-semibold",
575
- children: title
576
- }), /* @__PURE__ */ jsx("span", {
577
- className: "truncate text-xs text-gray-500",
578
- children: subtitle
579
- })]
580
- })]
581
- }, key);
582
- })
583
- })] })
545
+ }) : /* @__PURE__ */ jsx("div", { children: visibleItems.map((item, index) => {
546
+ const { key, sectionKey, sectionLabel, title, subtitle } = item;
547
+ const isActive = index === activeIndexInRange;
548
+ const previousItem = visibleItems[index - 1];
549
+ return /* @__PURE__ */ jsxs("div", { children: [hasItemSections && Boolean(sectionLabel?.trim()) && previousItem?.sectionKey !== sectionKey ? /* @__PURE__ */ jsx("div", {
550
+ className: "px-1.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
551
+ children: sectionLabel
552
+ }) : null, /* @__PURE__ */ jsxs("button", {
553
+ type: "button",
554
+ role: "option",
555
+ "aria-selected": isActive,
556
+ "data-input-surface-index": index,
557
+ onPointerMove: (event) => {
558
+ if (event.pointerType !== "touch") setActiveIndexForCurrentItems(index);
559
+ },
560
+ onPointerDown: (event) => {
561
+ if (event.button > 0) return;
562
+ event.preventDefault();
563
+ onSelectItem(item);
564
+ },
565
+ onClick: (event) => {
566
+ if (event.detail === 0) onSelectItem(item);
567
+ },
568
+ className: `flex w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-left leading-4 transition-colors ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-100"}`,
569
+ children: [/* @__PURE__ */ jsx("span", {
570
+ className: "truncate text-xs font-medium",
571
+ children: title
572
+ }), /* @__PURE__ */ jsx("span", {
573
+ className: "truncate text-xs text-gray-500",
574
+ children: subtitle
575
+ })]
576
+ })] }, key);
577
+ }) })] })
584
578
  })]
585
579
  }), /* @__PURE__ */ jsx("div", {
586
- className: "custom-scrollbar min-h-0 min-w-0 overflow-y-auto overscroll-contain p-2.5",
580
+ className: "custom-scrollbar min-h-0 min-w-0 select-text overflow-y-auto overscroll-contain p-2.5",
587
581
  onPointerDown: onDetailsPointerDown,
588
582
  children: activeItem ? /* @__PURE__ */ jsxs("div", {
589
583
  className: "space-y-3",
@@ -640,15 +634,13 @@ function resolveInputSurfaceTriggerIdentity(currentIdentity, trigger, reason) {
640
634
  function ChatInputSurfaceHost(props) {
641
635
  const { children, inputSurface, onInputSurfaceTriggerChange, onSelectItem, triggerSpecs = [CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC] } = props;
642
636
  const menuRef = useRef(null);
637
+ const isDetailsInteractionRef = useRef(false);
643
638
  const [activeTriggerIdentity, setActiveTriggerIdentity] = useState(null);
644
639
  const isOpen = Boolean(inputSurface) && activeTriggerIdentity !== null;
645
640
  const setActiveTrigger = useCallback((identity, trigger) => {
646
641
  setActiveTriggerIdentity(identity);
647
642
  onInputSurfaceTriggerChange?.(identity ? trigger : null);
648
643
  }, [onInputSurfaceTriggerChange]);
649
- const closeInputSurface = useCallback(() => {
650
- setActiveTrigger(null, null);
651
- }, [setActiveTrigger]);
652
644
  const handleInputSurfaceSnapshotChange = useCallback((nodes, selection, reason) => {
653
645
  const trigger = resolveChatComposerActiveInputSurfaceTrigger(nodes, selection, triggerSpecs);
654
646
  const nextIdentity = resolveInputSurfaceTriggerIdentity(activeTriggerIdentity, trigger, reason);
@@ -662,8 +654,8 @@ function ChatInputSurfaceHost(props) {
662
654
  return menuRef.current?.handleKeyDown(event) ?? false;
663
655
  }, []);
664
656
  const handleInputSurfaceOpenChange = useCallback((open) => {
665
- if (!open) closeInputSurface();
666
- }, [closeInputSurface]);
657
+ if (!open && !isDetailsInteractionRef.current) setActiveTrigger(null, null);
658
+ }, [setActiveTrigger]);
667
659
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [children(useMemo(() => ({
668
660
  onInputSurfaceKeyDown: handleInputSurfaceKeyDown,
669
661
  onInputSurfaceOpenChange: handleInputSurfaceOpenChange,
@@ -680,11 +672,16 @@ function ChatInputSurfaceHost(props) {
680
672
  items: inputSurface.items,
681
673
  texts: inputSurface.texts,
682
674
  onSelectItem: (item) => {
683
- closeInputSurface();
675
+ setActiveTrigger(null, null);
684
676
  onSelectItem(item);
685
677
  },
686
678
  onOpenChange: handleInputSurfaceOpenChange,
687
- onDetailsPointerDown: (event) => event.preventDefault()
679
+ onDetailsPointerDown: () => {
680
+ isDetailsInteractionRef.current = true;
681
+ requestAnimationFrame(() => {
682
+ isDetailsInteractionRef.current = false;
683
+ });
684
+ }
688
685
  }, activeTriggerIdentity) : null] });
689
686
  }
690
687
  //#endregion
@@ -727,12 +724,6 @@ const ChatButton = React.forwardRef(({ className, variant, size, ...props }, ref
727
724
  ChatButton.displayName = "ChatButton";
728
725
  //#endregion
729
726
  //#region src/components/chat/ui/chat-input-bar/chat-input-bar-actions.tsx
730
- const SEND_ERROR_PREVIEW_MAX_CHARS = 120;
731
- function buildSendErrorPreview(value) {
732
- const compact = value.replace(/\s+/g, " ").trim();
733
- if (compact.length <= SEND_ERROR_PREVIEW_MAX_CHARS) return compact;
734
- return `${compact.slice(0, SEND_ERROR_PREVIEW_MAX_CHARS - 1)}…`;
735
- }
736
727
  function StopIcon() {
737
728
  return /* @__PURE__ */ jsx("span", {
738
729
  "aria-hidden": "true",
@@ -782,75 +773,42 @@ function ContextWindowIndicator({ contextWindow }) {
782
773
  })] })
783
774
  });
784
775
  }
785
- function ChatInputBarActions({ sendError, sendErrorDetailsLabel, isSending, canStopGeneration, sendDisabled, stopDisabled, stopHint, sendButtonLabel, stopButtonLabel, contextWindow, onSend, onStop }) {
786
- const { Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
787
- const normalizedSendError = sendError?.trim() ?? "";
788
- const sendErrorPreview = normalizedSendError ? buildSendErrorPreview(normalizedSendError) : "";
789
- const resolvedSendErrorDetailsLabel = sendErrorDetailsLabel?.trim() || "Details";
776
+ function ChatInputBarActions({ isSending, canStopGeneration, sendDisabled, stopDisabled, stopHint, sendButtonLabel, stopButtonLabel, contextWindow, onSend, onStop }) {
777
+ const { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
790
778
  return /* @__PURE__ */ jsxs("div", {
791
- className: "flex shrink-0 flex-col items-end gap-1",
792
- children: [normalizedSendError ? /* @__PURE__ */ jsxs("div", {
793
- className: "flex max-w-[420px] items-start justify-end gap-2 text-right",
794
- children: [/* @__PURE__ */ jsx("div", {
795
- className: "min-w-0 flex-1 text-[11px] text-red-600",
796
- children: /* @__PURE__ */ jsx("span", {
797
- className: "block truncate",
798
- title: normalizedSendError,
799
- children: sendErrorPreview
800
- })
801
- }), /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(PopoverTrigger, {
802
- asChild: true,
803
- children: /* @__PURE__ */ jsx("button", {
804
- type: "button",
805
- className: "shrink-0 rounded-full border border-red-200/80 bg-red-50 px-2 py-0.5 text-[10px] font-semibold text-red-700 transition-colors hover:bg-red-100",
806
- children: resolvedSendErrorDetailsLabel
807
- })
808
- }), /* @__PURE__ */ jsxs(PopoverContent, {
809
- align: "end",
810
- className: "w-[min(32rem,calc(100vw-1.5rem))] border-red-100/80 p-0",
811
- children: [/* @__PURE__ */ jsx("div", {
812
- className: "border-b border-red-100 bg-red-50/80 px-4 py-2 text-xs font-semibold text-red-700",
813
- children: resolvedSendErrorDetailsLabel
814
- }), /* @__PURE__ */ jsx("pre", {
815
- className: "max-h-80 overflow-auto whitespace-pre-wrap break-words px-4 py-3 text-xs leading-relaxed text-red-700",
816
- children: normalizedSendError
817
- })]
818
- })] })]
819
- }) : null, /* @__PURE__ */ jsxs("div", {
820
- className: "flex items-center gap-2",
821
- children: [contextWindow ? /* @__PURE__ */ jsx(ContextWindowIndicator, { contextWindow }) : null, !isSending || !sendDisabled ? /* @__PURE__ */ jsx(ChatButton, {
822
- size: "icon",
823
- className: "h-8 w-8 rounded-full",
824
- "aria-label": sendButtonLabel,
825
- onClick: () => void onSend(),
826
- disabled: sendDisabled,
827
- children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-5 w-5" })
828
- }) : canStopGeneration ? /* @__PURE__ */ jsx(ChatButton, {
779
+ className: "flex shrink-0 items-center gap-2",
780
+ children: [contextWindow ? /* @__PURE__ */ jsx(ContextWindowIndicator, { contextWindow }) : null, !isSending || !sendDisabled ? /* @__PURE__ */ jsx(ChatButton, {
781
+ size: "icon",
782
+ className: "h-8 w-8 rounded-full",
783
+ "aria-label": sendButtonLabel,
784
+ onClick: () => void onSend(),
785
+ disabled: sendDisabled,
786
+ children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-5 w-5" })
787
+ }) : canStopGeneration ? /* @__PURE__ */ jsx(ChatButton, {
788
+ size: "icon",
789
+ variant: "outline",
790
+ className: "h-8 w-8 rounded-full",
791
+ "aria-label": stopButtonLabel,
792
+ onClick: () => void onStop(),
793
+ disabled: stopDisabled,
794
+ children: /* @__PURE__ */ jsx(StopIcon, {})
795
+ }) : /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
796
+ asChild: true,
797
+ children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(ChatButton, {
829
798
  size: "icon",
830
799
  variant: "outline",
831
800
  className: "h-8 w-8 rounded-full",
832
801
  "aria-label": stopButtonLabel,
833
- onClick: () => void onStop(),
834
- disabled: stopDisabled,
802
+ disabled: true,
835
803
  children: /* @__PURE__ */ jsx(StopIcon, {})
836
- }) : /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
837
- asChild: true,
838
- children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(ChatButton, {
839
- size: "icon",
840
- variant: "outline",
841
- className: "h-8 w-8 rounded-full",
842
- "aria-label": stopButtonLabel,
843
- disabled: true,
844
- children: /* @__PURE__ */ jsx(StopIcon, {})
845
- }) })
846
- }), /* @__PURE__ */ jsx(TooltipContent, {
847
- side: "top",
848
- children: /* @__PURE__ */ jsx("p", {
849
- className: "text-xs",
850
- children: stopHint
851
- })
852
- })] }) })]
853
- })]
804
+ }) })
805
+ }), /* @__PURE__ */ jsx(TooltipContent, {
806
+ side: "top",
807
+ children: /* @__PURE__ */ jsx("p", {
808
+ className: "text-xs",
809
+ children: stopHint
810
+ })
811
+ })] }) })]
854
812
  });
855
813
  }
856
814
  //#endregion
@@ -1160,16 +1118,16 @@ function ToolbarSearchableSelect({ item }) {
1160
1118
  children: [/* @__PURE__ */ jsx(ToolbarSelectTriggerContent, { item }), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-1 h-3.5 w-3.5 shrink-0 text-muted-foreground/70 [@container_nextclaw-chat-input-bar_(max-width:440px)]:hidden" })]
1161
1119
  })
1162
1120
  }), /* @__PURE__ */ jsxs(PopoverContent, {
1163
- className: `flex flex-col overflow-hidden p-2 ${CONTENT_WIDTH_BY_KEY[item.key] ?? ""}`,
1121
+ className: `flex flex-col overflow-hidden p-1.5 ${CONTENT_WIDTH_BY_KEY[item.key] ?? ""}`,
1164
1122
  style: { maxHeight: TOOLBAR_POPOVER_MAX_HEIGHT },
1165
1123
  children: [
1166
1124
  /* @__PURE__ */ jsxs("div", {
1167
- className: "relative mb-2 shrink-0",
1125
+ className: "relative mb-1.5 shrink-0",
1168
1126
  children: [/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/70" }), /* @__PURE__ */ jsx(Input, {
1169
1127
  value: query,
1170
1128
  onChange: (event) => setQuery(event.currentTarget.value),
1171
1129
  placeholder: item.search?.placeholder ?? item.placeholder,
1172
- className: "h-8 rounded-lg pl-8 text-xs"
1130
+ className: "h-7 rounded-md pl-8 text-xs"
1173
1131
  })]
1174
1132
  }),
1175
1133
  !hasOptions ? item.loading ? /* @__PURE__ */ jsxs("div", {
@@ -1190,9 +1148,9 @@ function ToolbarSearchableSelect({ item }) {
1190
1148
  /* @__PURE__ */ jsx("div", {
1191
1149
  className: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
1192
1150
  children: filteredGroups.map((group, groupIndex) => /* @__PURE__ */ jsxs("div", {
1193
- className: groupIndex > 0 ? "border-t border-border pt-1" : void 0,
1151
+ className: groupIndex > 0 ? "border-t border-border pt-0.5" : void 0,
1194
1152
  children: [group.label ? /* @__PURE__ */ jsx("div", {
1195
- className: "px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground",
1153
+ className: "px-2 pb-0.5 pt-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground",
1196
1154
  children: group.label
1197
1155
  }) : null, group.options.map((option) => {
1198
1156
  const isSelected = item.value === option.value;
@@ -1202,7 +1160,7 @@ function ToolbarSearchableSelect({ item }) {
1202
1160
  className: "group flex items-center gap-1 rounded-md hover:bg-accent",
1203
1161
  children: [/* @__PURE__ */ jsxs("button", {
1204
1162
  type: "button",
1205
- className: "flex min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-2 text-left",
1163
+ className: "flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-2 py-1.5 text-left leading-4",
1206
1164
  onClick: () => {
1207
1165
  item.onValueChange(option.value);
1208
1166
  setOpen(false);
@@ -1269,7 +1227,7 @@ function ToolbarSelect({ item }) {
1269
1227
  children: item.emptyLabel
1270
1228
  }) : null : null, groups.map((group, groupIndex) => /* @__PURE__ */ jsxs("div", { children: [groupIndex > 0 ? /* @__PURE__ */ jsx(SelectSeparator, {}) : null, /* @__PURE__ */ jsxs(SelectGroup, { children: [group.label ? /* @__PURE__ */ jsx(SelectLabel, { children: group.label }) : null, group.options.map((option) => /* @__PURE__ */ jsx(SelectItem, {
1271
1229
  value: option.value,
1272
- className: "py-2",
1230
+ className: "py-1.5",
1273
1231
  children: /* @__PURE__ */ jsx(ToolbarSelectOptionContent, { option })
1274
1232
  }, option.value))] })] }, group.key))]
1275
1233
  })]
@@ -1667,12 +1625,12 @@ function writeChatComposerStateToLexicalRoot(nodes, selection) {
1667
1625
  nextSelection.focus.set(focus.key, focus.offset, focus.type);
1668
1626
  $setSelection(nextSelection);
1669
1627
  }
1670
- function syncLexicalEditorFromChatComposerState(editor, nodes, selection) {
1628
+ function syncLexicalEditorFromChatComposerState(editor, nodes, selection, preserveDomSelection = false) {
1671
1629
  editor.update(() => {
1672
1630
  writeChatComposerStateToLexicalRoot(nodes, selection);
1673
- });
1631
+ }, preserveDomSelection ? { tag: SKIP_DOM_SELECTION_TAG } : void 0);
1674
1632
  }
1675
- function syncLexicalSelectionFromChatComposerSelection(editor, selection) {
1633
+ function syncLexicalSelectionFromChatComposerSelection(editor, selection, preserveDomSelection = false) {
1676
1634
  editor.update(() => {
1677
1635
  const nextSelection = $createRangeSelection();
1678
1636
  const anchor = buildSelectionPointFromOffset(selection.start);
@@ -1680,7 +1638,7 @@ function syncLexicalSelectionFromChatComposerSelection(editor, selection) {
1680
1638
  nextSelection.anchor.set(anchor.key, anchor.offset, anchor.type);
1681
1639
  nextSelection.focus.set(focus.key, focus.offset, focus.type);
1682
1640
  $setSelection(nextSelection);
1683
- });
1641
+ }, preserveDomSelection ? { tag: SKIP_DOM_SELECTION_TAG } : void 0);
1684
1642
  }
1685
1643
  //#endregion
1686
1644
  //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-lexical-operations.ts
@@ -1991,11 +1949,12 @@ var ChatComposerLexicalOwner = class {
1991
1949
  const shouldSyncDocument = nextSignature !== this.editorSignatureRef.current;
1992
1950
  if (!shouldSyncDocument && !pendingSelection) return;
1993
1951
  this.startApplyingExternalUpdate();
1952
+ const preserveDomSelection = editor.getRootElement() !== document.activeElement;
1994
1953
  if (shouldSyncDocument) {
1995
- syncLexicalEditorFromChatComposerState(editor, nodes, pendingSelection);
1954
+ syncLexicalEditorFromChatComposerState(editor, nodes, pendingSelection, preserveDomSelection);
1996
1955
  this.editorSignatureRef.current = nextSignature;
1997
1956
  this.lastPublishedSignatureRef.current = nextSignature;
1998
- } else if (pendingSelection) syncLexicalSelectionFromChatComposerSelection(editor, pendingSelection);
1957
+ } else if (pendingSelection) syncLexicalSelectionFromChatComposerSelection(editor, pendingSelection, preserveDomSelection);
1999
1958
  if (pendingSelection) {
2000
1959
  this.selectionRef.current = pendingSelection;
2001
1960
  this.pendingSelectionRef.current = null;
@@ -2294,6 +2253,11 @@ const ChatInputBarTokenizedComposer = forwardRef(function ChatInputBarTokenizedC
2294
2253
  ChatInputBarTokenizedComposer.displayName = "LexicalChatInputBarTokenizedComposer";
2295
2254
  //#endregion
2296
2255
  //#region src/components/chat/ui/chat-input-bar/chat-input-bar.tsx
2256
+ const SEND_ERROR_PREVIEW_MAX_CHARS = 120;
2257
+ function buildSendErrorPreview(value) {
2258
+ const compact = value.replace(/\s+/g, " ").trim();
2259
+ return compact.length <= SEND_ERROR_PREVIEW_MAX_CHARS ? compact : `${compact.slice(0, SEND_ERROR_PREVIEW_MAX_CHARS - 1)}…`;
2260
+ }
2297
2261
  function InputBarHint({ hint }) {
2298
2262
  if (!hint) return null;
2299
2263
  if (hint.loading) return /* @__PURE__ */ jsx("div", {
@@ -2316,7 +2280,44 @@ function InputBarHint({ hint }) {
2316
2280
  })
2317
2281
  });
2318
2282
  }
2319
- const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSurface, slashMenu, surface, toolbar: toolbarProps, topSlot }, ref) {
2283
+ function ChatInputBarSendError({ sendError, sendErrorDetailsLabel }) {
2284
+ const normalizedSendError = sendError?.trim() ?? "";
2285
+ if (!normalizedSendError) return null;
2286
+ const { Popover, PopoverContent, PopoverTrigger } = ChatUiPrimitives;
2287
+ const sendErrorPreview = buildSendErrorPreview(normalizedSendError);
2288
+ const resolvedSendErrorDetailsLabel = sendErrorDetailsLabel?.trim() || "Details";
2289
+ return /* @__PURE__ */ jsx("div", {
2290
+ className: "px-3 pb-2 sm:px-4",
2291
+ children: /* @__PURE__ */ jsxs("div", {
2292
+ "aria-live": "polite",
2293
+ 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",
2294
+ role: "status",
2295
+ children: [/* @__PURE__ */ jsx("span", {
2296
+ className: "min-w-0 flex-1 truncate leading-5",
2297
+ title: normalizedSendError,
2298
+ children: sendErrorPreview
2299
+ }), /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(PopoverTrigger, {
2300
+ asChild: true,
2301
+ children: /* @__PURE__ */ jsx("button", {
2302
+ type: "button",
2303
+ 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",
2304
+ children: resolvedSendErrorDetailsLabel
2305
+ })
2306
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
2307
+ align: "end",
2308
+ className: "w-[min(32rem,calc(100vw-1.5rem))] border-red-100/80 p-0",
2309
+ children: [/* @__PURE__ */ jsx("div", {
2310
+ className: "border-b border-red-100 bg-red-50/80 px-4 py-2 text-xs font-semibold text-red-700",
2311
+ children: resolvedSendErrorDetailsLabel
2312
+ }), /* @__PURE__ */ jsx("pre", {
2313
+ className: "max-h-80 overflow-auto whitespace-pre-wrap break-words px-4 py-3 text-xs leading-relaxed text-red-700",
2314
+ children: normalizedSendError
2315
+ })]
2316
+ })] })]
2317
+ })
2318
+ });
2319
+ }
2320
+ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSurface, sendError, sendErrorDetailsLabel, slashMenu, surface, toolbar: toolbarProps, topSlot }, ref) {
2320
2321
  const composerRef = useRef(null);
2321
2322
  const resolvedInputSurface = inputSurface ?? (slashMenu ? {
2322
2323
  isLoading: slashMenu.isLoading,
@@ -2384,6 +2385,10 @@ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSur
2384
2385
  })
2385
2386
  }),
2386
2387
  /* @__PURE__ */ jsx(InputBarHint, { hint }),
2388
+ /* @__PURE__ */ jsx(ChatInputBarSendError, {
2389
+ sendError,
2390
+ sendErrorDetailsLabel
2391
+ }),
2387
2392
  /* @__PURE__ */ jsx(ChatInputBarToolbar, { ...toolbar })
2388
2393
  ]
2389
2394
  })
@@ -2393,22 +2398,25 @@ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSur
2393
2398
  ChatInputBar.displayName = "ChatInputBar";
2394
2399
  //#endregion
2395
2400
  //#region src/components/chat/ui/chat-message-list/chat-message-avatar.tsx
2396
- function ChatMessageAvatar({ role }) {
2401
+ function ChatMessageAvatar({ role, size = "default" }) {
2402
+ const compact = size === "compact";
2403
+ const frameSize = compact ? "h-7 w-7" : "h-8 w-8";
2404
+ const iconSize = compact ? "h-3.5 w-3.5" : "h-4 w-4";
2397
2405
  if (role === "user") return /* @__PURE__ */ jsx("div", {
2398
2406
  "data-testid": "chat-message-avatar-user",
2399
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm",
2400
- children: /* @__PURE__ */ jsx(User, { className: "h-4 w-4" })
2407
+ className: cn("flex shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm", frameSize),
2408
+ children: /* @__PURE__ */ jsx(User, { className: iconSize })
2401
2409
  });
2402
2410
  if (role === "tool") return /* @__PURE__ */ jsx("div", {
2403
2411
  "data-testid": "chat-message-avatar-tool",
2404
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-accent text-accent-foreground shadow-sm ring-1 ring-border",
2405
- children: /* @__PURE__ */ jsx(Wrench, { className: "h-4 w-4" })
2412
+ className: cn("flex shrink-0 items-center justify-center rounded-full bg-accent text-accent-foreground shadow-sm ring-1 ring-border", frameSize),
2413
+ children: /* @__PURE__ */ jsx(Wrench, { className: iconSize })
2406
2414
  });
2407
2415
  return /* @__PURE__ */ jsx("div", {
2408
2416
  "data-testid": "chat-message-avatar-assistant",
2409
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-foreground shadow-sm ring-1 ring-border",
2417
+ className: cn("flex shrink-0 items-center justify-center rounded-full bg-muted text-foreground shadow-sm ring-1 ring-border", frameSize),
2410
2418
  children: /* @__PURE__ */ jsx(Bot, {
2411
- className: "h-[18px] w-[18px] text-current",
2419
+ className: cn(compact ? "h-4 w-4" : "h-[18px] w-[18px]", "text-current"),
2412
2420
  strokeWidth: 2.5
2413
2421
  })
2414
2422
  });
@@ -2773,7 +2781,7 @@ function isRecord$1(value) {
2773
2781
  function readString(value) {
2774
2782
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2775
2783
  }
2776
- function readPositiveInteger(value) {
2784
+ function readPositiveInteger$1(value) {
2777
2785
  return Number.isInteger(value) && Number(value) > 0 ? Number(value) : void 0;
2778
2786
  }
2779
2787
  function readFilePreviewViewer(value) {
@@ -2793,8 +2801,8 @@ function readFileTarget(record) {
2793
2801
  type: "file",
2794
2802
  payload: {
2795
2803
  path,
2796
- line: readPositiveInteger(payload.line),
2797
- column: readPositiveInteger(payload.column),
2804
+ line: readPositiveInteger$1(payload.line),
2805
+ column: readPositiveInteger$1(payload.column),
2798
2806
  viewer: readFilePreviewViewer(payload.viewer)
2799
2807
  }
2800
2808
  };
@@ -2929,57 +2937,345 @@ function ChatInlineDisplay({ display, renderInlineDisplay }) {
2929
2937
  return /* @__PURE__ */ jsx(ChatInlineDisplayFallback, { display });
2930
2938
  }
2931
2939
  //#endregion
2932
- //#region src/components/chat/ui/chat-message-list/chat-message-markdown.tsx
2933
- const MARKDOWN_MAX_CHARS = 14e4;
2934
- const SAFE_LINK_PROTOCOLS = new Set([
2940
+ //#region src/components/chat/ui/chat-message-list/mermaid/chat-mermaid-diagram.tsx
2941
+ const STREAMING_RENDER_DELAY_MS = 300;
2942
+ let mermaidModulePromise = null;
2943
+ function loadMermaid() {
2944
+ mermaidModulePromise ??= import("mermaid").then((module) => module.default);
2945
+ return mermaidModulePromise;
2946
+ }
2947
+ function readMermaidTheme() {
2948
+ if (typeof document === "undefined") return "default";
2949
+ const root = document.documentElement;
2950
+ return root.getAttribute("data-theme-appearance") === "dark" || root.classList.contains("dark") ? "dark" : "default";
2951
+ }
2952
+ function useMermaidTheme() {
2953
+ const [theme, setTheme] = useState(readMermaidTheme);
2954
+ useEffect(() => {
2955
+ const root = document.documentElement;
2956
+ const observer = new MutationObserver(() => setTheme(readMermaidTheme()));
2957
+ observer.observe(root, {
2958
+ attributes: true,
2959
+ attributeFilter: ["class", "data-theme-appearance"]
2960
+ });
2961
+ return () => observer.disconnect();
2962
+ }, []);
2963
+ return theme;
2964
+ }
2965
+ function ChatMermaidDiagram({ isStreaming, source, texts }) {
2966
+ const reactId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
2967
+ const renderSequence = useRef(0);
2968
+ const theme = useMermaidTheme();
2969
+ const normalizedSource = source.trim();
2970
+ const renderKey = `${theme}:${normalizedSource}`;
2971
+ const [state, setState] = useState(null);
2972
+ const diagramLabel = texts.mermaidDiagramLabel ?? "Mermaid diagram";
2973
+ const errorLabel = texts.mermaidRenderErrorLabel ?? "Diagram could not be rendered";
2974
+ useEffect(() => {
2975
+ let active = true;
2976
+ const renderDiagram = async () => {
2977
+ try {
2978
+ const mermaid = await loadMermaid();
2979
+ if (!active) return;
2980
+ renderSequence.current += 1;
2981
+ const diagramId = `nextclaw-mermaid-${reactId}-${renderSequence.current}`;
2982
+ mermaid.initialize({
2983
+ securityLevel: "strict",
2984
+ startOnLoad: false,
2985
+ suppressErrorRendering: true,
2986
+ theme
2987
+ });
2988
+ const valid = await mermaid.parse(normalizedSource, { suppressErrors: true });
2989
+ if (!active) return;
2990
+ if (!valid) throw new Error("Invalid Mermaid diagram");
2991
+ const { svg } = await mermaid.render(diagramId, normalizedSource);
2992
+ if (active) setState({
2993
+ key: renderKey,
2994
+ status: "rendered",
2995
+ svg
2996
+ });
2997
+ } catch {
2998
+ if (active && !isStreaming) setState({
2999
+ key: renderKey,
3000
+ status: "error"
3001
+ });
3002
+ }
3003
+ };
3004
+ const timeout = window.setTimeout(() => void renderDiagram(), isStreaming ? STREAMING_RENDER_DELAY_MS : 0);
3005
+ return () => {
3006
+ active = false;
3007
+ window.clearTimeout(timeout);
3008
+ };
3009
+ }, [
3010
+ isStreaming,
3011
+ normalizedSource,
3012
+ reactId,
3013
+ renderKey,
3014
+ theme
3015
+ ]);
3016
+ const currentState = state?.key === renderKey ? state : null;
3017
+ if (currentState?.status === "error") return /* @__PURE__ */ jsxs("div", {
3018
+ "data-chat-mermaid-error": "true",
3019
+ className: "my-3 overflow-hidden rounded-xl border border-border bg-muted/25",
3020
+ children: [/* @__PURE__ */ jsx("div", {
3021
+ className: "border-b border-border px-3 py-2 text-xs text-muted-foreground",
3022
+ children: errorLabel
3023
+ }), /* @__PURE__ */ jsx(ChatCodeBlock, {
3024
+ className: "language-mermaid",
3025
+ texts,
3026
+ children: normalizedSource
3027
+ })]
3028
+ });
3029
+ const renderedState = state?.status === "rendered" ? state : null;
3030
+ if (!renderedState) return /* @__PURE__ */ jsx(ChatCodeBlock, {
3031
+ className: "language-mermaid",
3032
+ texts,
3033
+ children: normalizedSource
3034
+ });
3035
+ return /* @__PURE__ */ jsx("figure", {
3036
+ "aria-label": diagramLabel,
3037
+ "aria-busy": !currentState,
3038
+ "data-chat-mermaid-diagram": "true",
3039
+ "data-chat-mermaid-updating": !currentState || void 0,
3040
+ className: "my-3 min-h-24 overflow-auto rounded-xl border border-border bg-muted/20 p-3",
3041
+ children: /* @__PURE__ */ jsx("div", {
3042
+ className: "min-w-fit [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full",
3043
+ dangerouslySetInnerHTML: { __html: renderedState.svg }
3044
+ })
3045
+ });
3046
+ }
3047
+ //#endregion
3048
+ //#region src/components/chat/ui/chat-message-list/chat-message-file/chat-message-image-preview.tsx
3049
+ function ChatMessageImageLightbox({ alt, closeLabel, onClose, src }) {
3050
+ const titleId = useId();
3051
+ useEffect(() => {
3052
+ const previousOverflow = document.body.style.overflow;
3053
+ document.body.style.overflow = "hidden";
3054
+ const onKeyDown = (event) => {
3055
+ if (event.key === "Escape") {
3056
+ event.preventDefault();
3057
+ onClose();
3058
+ }
3059
+ };
3060
+ window.addEventListener("keydown", onKeyDown);
3061
+ return () => {
3062
+ document.body.style.overflow = previousOverflow;
3063
+ window.removeEventListener("keydown", onKeyDown);
3064
+ };
3065
+ }, [onClose]);
3066
+ return createPortal(/* @__PURE__ */ jsxs("div", {
3067
+ role: "dialog",
3068
+ "aria-modal": "true",
3069
+ "aria-labelledby": titleId,
3070
+ className: "fixed inset-0 z-[var(--z-modal,10050)] flex items-center justify-center bg-black/80 p-4 backdrop-blur-[2px]",
3071
+ "data-testid": "chat-message-image-lightbox",
3072
+ onClick: onClose,
3073
+ children: [
3074
+ /* @__PURE__ */ jsx("span", {
3075
+ id: titleId,
3076
+ className: "sr-only",
3077
+ children: alt
3078
+ }),
3079
+ /* @__PURE__ */ jsx("button", {
3080
+ type: "button",
3081
+ "aria-label": closeLabel,
3082
+ 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",
3083
+ onClick: (event) => {
3084
+ event.stopPropagation();
3085
+ onClose();
3086
+ },
3087
+ children: /* @__PURE__ */ jsx(X, {
3088
+ className: "h-4 w-4",
3089
+ strokeWidth: 2
3090
+ })
3091
+ }),
3092
+ /* @__PURE__ */ jsx("img", {
3093
+ src,
3094
+ alt,
3095
+ className: "max-h-[min(92vh,100%)] max-w-[min(96vw,100%)] object-contain shadow-2xl",
3096
+ onClick: (event) => event.stopPropagation()
3097
+ })
3098
+ ]
3099
+ }), document.body);
3100
+ }
3101
+ function ChatMessageImagePreview({ alt, expandLabel, closeLabel, sizeLabel, src }) {
3102
+ const [isExpanded, setIsExpanded] = useState(false);
3103
+ const openLightbox = () => setIsExpanded(true);
3104
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
3105
+ "data-chat-message-image-preview": true,
3106
+ className: "group/image relative block w-fit max-w-[min(100%,32rem)] overflow-hidden rounded-lg",
3107
+ children: [
3108
+ /* @__PURE__ */ jsx("button", {
3109
+ type: "button",
3110
+ className: "block w-fit max-w-[min(100%,32rem)] text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
3111
+ onClick: openLightbox,
3112
+ onDoubleClick: (event) => {
3113
+ event.preventDefault();
3114
+ openLightbox();
3115
+ },
3116
+ "aria-label": expandLabel,
3117
+ children: /* @__PURE__ */ jsx("img", {
3118
+ src,
3119
+ alt,
3120
+ className: "block h-auto w-auto max-h-[26rem] max-w-full rounded-lg bg-transparent object-contain"
3121
+ })
3122
+ }),
3123
+ /* @__PURE__ */ jsx("button", {
3124
+ type: "button",
3125
+ "aria-label": expandLabel,
3126
+ 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",
3127
+ onClick: (event) => {
3128
+ event.preventDefault();
3129
+ event.stopPropagation();
3130
+ openLightbox();
3131
+ },
3132
+ children: /* @__PURE__ */ jsx(Expand, {
3133
+ className: "h-3.5 w-3.5",
3134
+ strokeWidth: 2
3135
+ })
3136
+ }),
3137
+ sizeLabel ? /* @__PURE__ */ jsx("span", {
3138
+ 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",
3139
+ children: /* @__PURE__ */ jsx("span", {
3140
+ 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",
3141
+ children: sizeLabel
3142
+ })
3143
+ }) : null
3144
+ ]
3145
+ }), isExpanded ? /* @__PURE__ */ jsx(ChatMessageImageLightbox, {
3146
+ alt,
3147
+ closeLabel,
3148
+ onClose: () => setIsExpanded(false),
3149
+ src
3150
+ }) : null] });
3151
+ }
3152
+ //#endregion
3153
+ //#region src/components/chat/ui/chat-message-list/utils/chat-local-resource.utils.ts
3154
+ const SAFE_BROWSER_LINK_PROTOCOLS = new Set([
2935
3155
  "http:",
2936
3156
  "https:",
2937
3157
  "mailto:",
2938
3158
  "tel:"
2939
3159
  ]);
2940
- const INLINE_TOKEN_KIND_ATTR = "data-chat-inline-token-kind";
2941
- const INLINE_TOKEN_KEY_ATTR = "data-chat-inline-token-key";
2942
- const INLINE_TOKEN_LABEL_ATTR = "data-chat-inline-token-label";
2943
- const INLINE_TOKEN_RAW_TEXT_ATTR = "data-chat-inline-token-raw-text";
2944
- const PROJECT_RELATIVE_FILE_HREF_PATTERN = new RegExp(`^(?![a-zA-Z][a-zA-Z\\d+.-]*:)(?!//)(?:(?:[^/\\s?#]+/)+[^?#\\s]+\\.[A-Za-z0-9_-]+|[^/\\s?#]+\\.(?:cjs|css|cts|html?|js|json|jsx|mdx?|mjs|mts|tsx?|txt|ya?ml))(?::\\d+(?::\\d+)?)?(?:[?#].*)?$`, "i");
2945
- function trimMarkdown(value) {
2946
- if (value.length <= MARKDOWN_MAX_CHARS) return value;
2947
- return `${value.slice(0, MARKDOWN_MAX_CHARS)}\n\n...`;
3160
+ const URI_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
3161
+ const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[a-z]:[\\/]/i;
3162
+ const WINDOWS_FILE_URI_PATH_PATTERN = /^\/[a-z]:[\\/]/i;
3163
+ const FILE_URI_PROTOCOL = "file:";
3164
+ function isSchemeLessResourceHref(href) {
3165
+ return !href.startsWith("//") && !URI_SCHEME_PATTERN.test(href);
2948
3166
  }
2949
- function resolveSafeHref(href) {
2950
- if (!href) return null;
2951
- if (href.startsWith("#") || href.startsWith("/") || href.startsWith("./") || href.startsWith("../") || PROJECT_RELATIVE_FILE_HREF_PATTERN.test(href)) return href;
3167
+ function readPositiveInteger(value) {
3168
+ const parsed = Number(value);
3169
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
3170
+ }
3171
+ function readFileUriHref(href) {
3172
+ let url;
2952
3173
  try {
2953
- const url = new URL(href);
2954
- return SAFE_LINK_PROTOCOLS.has(url.protocol) ? href : null;
3174
+ url = new URL(href);
3175
+ } catch {
3176
+ return null;
3177
+ }
3178
+ if (url.protocol !== FILE_URI_PROTOCOL || url.hostname && url.hostname.toLowerCase() !== "localhost") return null;
3179
+ let path;
3180
+ try {
3181
+ path = decodeURIComponent(url.pathname);
2955
3182
  } catch {
2956
3183
  return null;
2957
3184
  }
3185
+ if (WINDOWS_FILE_URI_PATH_PATTERN.test(path)) path = path.slice(1);
3186
+ return path ? {
3187
+ path,
3188
+ query: url.searchParams,
3189
+ fragment: url.hash.slice(1)
3190
+ } : null;
3191
+ }
3192
+ function readSchemeLessFileHref(href) {
3193
+ const hashIndex = href.indexOf("#");
3194
+ const hrefWithoutFragment = hashIndex >= 0 ? href.slice(0, hashIndex) : href;
3195
+ const fragment = hashIndex >= 0 ? href.slice(hashIndex + 1) : "";
3196
+ const queryIndex = hrefWithoutFragment.indexOf("?");
3197
+ const encodedPath = queryIndex >= 0 ? hrefWithoutFragment.slice(0, queryIndex) : hrefWithoutFragment;
3198
+ const encodedQuery = queryIndex >= 0 ? hrefWithoutFragment.slice(queryIndex + 1) : "";
3199
+ let path;
3200
+ try {
3201
+ path = decodeURIComponent(encodedPath);
3202
+ } catch {
3203
+ return null;
3204
+ }
3205
+ return path ? {
3206
+ path,
3207
+ query: new URLSearchParams(encodedQuery),
3208
+ fragment
3209
+ } : null;
3210
+ }
3211
+ function readLinePosition(fragment) {
3212
+ const match = /^L(\d+)(?:C(\d+))?(?:-L\d+(?:C\d+)?)?$/i.exec(fragment);
3213
+ const line = readPositiveInteger(match?.[1]);
3214
+ return {
3215
+ line,
3216
+ column: line ? readPositiveInteger(match?.[2]) : void 0
3217
+ };
3218
+ }
3219
+ function transformChatResourceHref(href) {
3220
+ if (WINDOWS_ABSOLUTE_PATH_PATTERN.test(href) || isSchemeLessResourceHref(href)) return href;
3221
+ try {
3222
+ const url = new URL(href);
3223
+ if (SAFE_BROWSER_LINK_PROTOCOLS.has(url.protocol)) return href;
3224
+ return url.protocol === FILE_URI_PROTOCOL && readFileUriHref(href) ? href : "";
3225
+ } catch {
3226
+ return "";
3227
+ }
3228
+ }
3229
+ function resolveSafeChatResourceHref(href) {
3230
+ if (!href) return null;
3231
+ return transformChatResourceHref(href) || null;
2958
3232
  }
2959
- function isExternalHref(href) {
3233
+ function isExternalChatResourceHref(href) {
2960
3234
  return /^https?:\/\//i.test(href);
2961
3235
  }
2962
- function looksLikeLocalFileHref(href) {
2963
- return href.startsWith("./") || href.startsWith("../") || href.startsWith("/Users/") || href.startsWith("/home/") || href.startsWith("/tmp/") || href.startsWith("/var/") || PROJECT_RELATIVE_FILE_HREF_PATTERN.test(href) || /^\/.+\.[A-Za-z0-9_-]+(?::\d+(?::\d+)?)?$/.test(href);
2964
- }
2965
- function parseLocalFileAction(href) {
2966
- const viewer = new URLSearchParams(href.split("#")[0]?.split("?")[1] ?? "").get("viewer");
2967
- const normalizedHref = href.split("#")[0]?.split("?")[0] ?? href;
2968
- const decodedHref = decodeURIComponent(normalizedHref);
2969
- if (!looksLikeLocalFileHref(decodedHref)) return null;
2970
- const lineMatch = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(decodedHref);
2971
- const rawPath = lineMatch?.[1] ?? decodedHref;
2972
- const line = lineMatch?.[2] ? Number(lineMatch[2]) : void 0;
2973
- const column = lineMatch?.[3] ? Number(lineMatch[3]) : void 0;
3236
+ function parseChatLocalFileAction(href) {
3237
+ const fileHref = href.toLowerCase().startsWith(FILE_URI_PROTOCOL) ? readFileUriHref(href) : readSchemeLessFileHref(href);
3238
+ const path = fileHref?.path;
3239
+ if (!path || path.startsWith("#") || path.startsWith("//") || !WINDOWS_ABSOLUTE_PATH_PATTERN.test(path) && URI_SCHEME_PATTERN.test(path)) return null;
3240
+ const viewer = fileHref.query.get("viewer");
3241
+ const fragmentPosition = readLinePosition(fileHref.fragment);
3242
+ const lineMatch = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(path);
3243
+ const rawPath = lineMatch?.[1] ?? path;
3244
+ const line = fragmentPosition.line ?? readPositiveInteger(lineMatch?.[2]);
3245
+ const column = fragmentPosition.column ?? (line ? readPositiveInteger(lineMatch?.[3]) : void 0);
2974
3246
  return {
2975
3247
  path: rawPath,
2976
- label: rawPath.split("/").filter(Boolean).pop() ?? rawPath,
3248
+ label: rawPath.split(/[\\/]/).filter(Boolean).pop() ?? rawPath,
2977
3249
  viewMode: "preview",
2978
3250
  ...viewer === "source" || viewer === "rendered" ? { previewViewer: viewer } : {},
2979
3251
  ...typeof line === "number" ? { line } : {},
2980
3252
  ...typeof column === "number" ? { column } : {}
2981
3253
  };
2982
3254
  }
3255
+ //#endregion
3256
+ //#region src/components/chat/ui/chat-message-list/chat-message-markdown.tsx
3257
+ const MARKDOWN_MAX_CHARS = 14e4;
3258
+ const INLINE_TOKEN_KIND_ATTR = "data-chat-inline-token-kind";
3259
+ const INLINE_TOKEN_KEY_ATTR = "data-chat-inline-token-key";
3260
+ const INLINE_TOKEN_LABEL_ATTR = "data-chat-inline-token-label";
3261
+ const INLINE_TOKEN_RAW_TEXT_ATTR = "data-chat-inline-token-raw-text";
3262
+ function trimMarkdown(value) {
3263
+ if (value.length <= MARKDOWN_MAX_CHARS) return value;
3264
+ return `${value.slice(0, MARKDOWN_MAX_CHARS)}\n\n...`;
3265
+ }
3266
+ function isSingleLineImageParagraph(node) {
3267
+ let imageCount = 0;
3268
+ for (const child of node?.children ?? []) {
3269
+ if (child.type === "text") {
3270
+ const text = child.value ?? "";
3271
+ if (text.trim().length > 0 || text.includes("\n") || text.includes("\r")) return false;
3272
+ continue;
3273
+ }
3274
+ if (child.tagName !== "img") return false;
3275
+ imageCount += 1;
3276
+ }
3277
+ return imageCount >= 3;
3278
+ }
2983
3279
  function prepareInlineTokens(inlineTokens) {
2984
3280
  if (!inlineTokens || inlineTokens.length === 0) return [];
2985
3281
  const seenRawTexts = /* @__PURE__ */ new Set();
@@ -3046,144 +3342,170 @@ function splitTextNodeByInlineTokens(value, tokens) {
3046
3342
  }];
3047
3343
  }
3048
3344
  function transformInlineTokenTextNodes(node, tokens) {
3049
- if (node.type === "code" || node.type === "inlineCode" || !node.children) return;
3050
- const nextChildren = [];
3051
- for (const child of node.children) {
3052
- if (child.type === "text" && typeof child.value === "string") {
3053
- nextChildren.push(...splitTextNodeByInlineTokens(child.value, tokens));
3054
- continue;
3055
- }
3056
- transformInlineTokenTextNodes(child, tokens);
3057
- nextChildren.push(child);
3058
- }
3059
- node.children = nextChildren;
3345
+ if (node.type === "code" || node.type === "inlineCode" || !node.children) return node;
3346
+ return {
3347
+ ...node,
3348
+ children: node.children.flatMap((child) => child.type === "text" && typeof child.value === "string" ? splitTextNodeByInlineTokens(child.value, tokens) : [transformInlineTokenTextNodes(child, tokens)])
3349
+ };
3060
3350
  }
3061
3351
  function createRemarkInlineTokenPlugin(inlineTokens) {
3062
3352
  const tokens = prepareInlineTokens(inlineTokens);
3063
- return () => (tree) => {
3064
- if (tokens.length === 0) return;
3065
- transformInlineTokenTextNodes(tree, tokens);
3066
- };
3353
+ return () => (tree) => tokens.length > 0 ? transformInlineTokenTextNodes(tree, tokens) : tree;
3067
3354
  }
3068
3355
  function readStringProp(props, key) {
3069
3356
  const value = props[key];
3070
3357
  return typeof value === "string" && value.length > 0 ? value : null;
3071
3358
  }
3072
- function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, onInlineTokenClick, renderInlineDisplay }) {
3073
- const isUser = role === "user";
3074
- const remarkPlugins = useMemo(() => inlineTokens && inlineTokens.length > 0 ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm], [inlineTokens]);
3075
- const markdownComponents = useMemo(() => ({
3076
- p: ({ children }) => inline ? /* @__PURE__ */ jsx(Fragment$1, { children }) : /* @__PURE__ */ jsx("p", { children }),
3077
- span: ({ node: _node, children, ...rest }) => {
3078
- const restProps = rest;
3079
- const kind = readStringProp(restProps, INLINE_TOKEN_KIND_ATTR);
3080
- const key = readStringProp(restProps, INLINE_TOKEN_KEY_ATTR);
3081
- const label = readStringProp(restProps, INLINE_TOKEN_LABEL_ATTR);
3082
- const rawText = readStringProp(restProps, INLINE_TOKEN_RAW_TEXT_ATTR);
3083
- if (kind && key && label && rawText) return /* @__PURE__ */ jsx(ChatInlineTokenBadge, {
3359
+ const ChatMessageMarkdownRuntimeContext = createContext(null);
3360
+ function useChatMessageMarkdownRuntime() {
3361
+ const runtime = useContext(ChatMessageMarkdownRuntimeContext);
3362
+ if (!runtime) throw new Error("Chat message Markdown renderer requires its runtime context");
3363
+ return runtime;
3364
+ }
3365
+ const CHAT_MESSAGE_MARKDOWN_COMPONENTS = {
3366
+ p: function ChatMarkdownParagraph({ node, children }) {
3367
+ const { inline } = useChatMessageMarkdownRuntime();
3368
+ return inline ? /* @__PURE__ */ jsx(Fragment$1, { children }) : /* @__PURE__ */ jsx("p", {
3369
+ "data-chat-image-row": isSingleLineImageParagraph(node) ? "three-column" : void 0,
3370
+ children
3371
+ });
3372
+ },
3373
+ span: function ChatMarkdownSpan({ node: _node, children, ...rest }) {
3374
+ const { isUser, onInlineTokenClick } = useChatMessageMarkdownRuntime();
3375
+ const restProps = rest;
3376
+ const kind = readStringProp(restProps, INLINE_TOKEN_KIND_ATTR);
3377
+ const key = readStringProp(restProps, INLINE_TOKEN_KEY_ATTR);
3378
+ const label = readStringProp(restProps, INLINE_TOKEN_LABEL_ATTR);
3379
+ const rawText = readStringProp(restProps, INLINE_TOKEN_RAW_TEXT_ATTR);
3380
+ if (kind && key && label && rawText) return /* @__PURE__ */ jsx(ChatInlineTokenBadge, {
3381
+ kind,
3382
+ label,
3383
+ isUser,
3384
+ onClick: onInlineTokenClick ? () => onInlineTokenClick({
3084
3385
  kind,
3386
+ key,
3085
3387
  label,
3086
- isUser,
3087
- onClick: onInlineTokenClick ? () => onInlineTokenClick({
3088
- kind,
3089
- key,
3090
- label,
3091
- rawText
3092
- }) : void 0
3093
- });
3094
- return /* @__PURE__ */ jsx("span", {
3095
- ...rest,
3096
- children
3097
- });
3098
- },
3099
- a: ({ node: _node, href, children, ...rest }) => {
3100
- const safeHref = resolveSafeHref(href);
3101
- if (!safeHref) return /* @__PURE__ */ jsx("span", {
3102
- className: "chat-link-invalid",
3103
- children
3104
- });
3105
- const external = isExternalHref(safeHref);
3106
- const localFileAction = external ? null : parseLocalFileAction(safeHref);
3107
- const handleClick = (event) => {
3108
- if (!onFileOpen || !localFileAction) return;
3109
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3388
+ rawText
3389
+ }) : void 0
3390
+ });
3391
+ return /* @__PURE__ */ jsx("span", {
3392
+ ...rest,
3393
+ children
3394
+ });
3395
+ },
3396
+ a: function ChatMarkdownLink({ node: _node, href, children, ...rest }) {
3397
+ const { onFileOpen } = useChatMessageMarkdownRuntime();
3398
+ const safeHref = resolveSafeChatResourceHref(href);
3399
+ const external = safeHref ? isExternalChatResourceHref(safeHref) : false;
3400
+ const localFileAction = external ? null : safeHref ? parseChatLocalFileAction(safeHref) : null;
3401
+ const handleClick = (event) => {
3402
+ if (!safeHref) {
3110
3403
  event.preventDefault();
3111
- onFileOpen(localFileAction);
3112
- };
3113
- return /* @__PURE__ */ jsx("a", {
3114
- ...rest,
3115
- href: safeHref,
3116
- onClick: handleClick,
3117
- target: external ? "_blank" : void 0,
3118
- rel: external ? "noreferrer noopener" : void 0,
3119
- children
3120
- });
3121
- },
3122
- table: ({ node: _node, children, ...rest }) => /* @__PURE__ */ jsx("div", {
3404
+ return;
3405
+ }
3406
+ if (!onFileOpen || !localFileAction) return;
3407
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3408
+ event.preventDefault();
3409
+ onFileOpen(localFileAction);
3410
+ };
3411
+ return /* @__PURE__ */ jsx("a", {
3412
+ ...rest,
3413
+ className: cn(rest.className, !safeHref && "chat-link-invalid"),
3414
+ href: safeHref ?? "#",
3415
+ "aria-disabled": !safeHref || void 0,
3416
+ onClick: handleClick,
3417
+ target: external ? "_blank" : void 0,
3418
+ rel: external ? "noreferrer noopener" : void 0,
3419
+ children
3420
+ });
3421
+ },
3422
+ table: function ChatMarkdownTable({ node: _node, children, ...rest }) {
3423
+ return /* @__PURE__ */ jsx("div", {
3123
3424
  className: "chat-table-wrap",
3124
3425
  children: /* @__PURE__ */ jsx("table", {
3125
3426
  ...rest,
3126
3427
  children
3127
3428
  })
3128
- }),
3129
- input: ({ node: _node, type, checked, ...rest }) => {
3130
- if (type !== "checkbox") return /* @__PURE__ */ jsx("input", {
3131
- ...rest,
3132
- type
3133
- });
3134
- return /* @__PURE__ */ jsx("input", {
3135
- ...rest,
3136
- type: "checkbox",
3137
- checked,
3138
- readOnly: true,
3139
- disabled: true,
3140
- className: "chat-task-checkbox"
3141
- });
3142
- },
3143
- img: ({ node: _node, src, alt, ...rest }) => {
3144
- const safeSrc = resolveSafeHref(src);
3145
- if (!safeSrc) return null;
3146
- return /* @__PURE__ */ jsx("img", {
3147
- ...rest,
3148
- src: safeSrc,
3149
- alt: alt || "",
3150
- loading: "lazy",
3151
- decoding: "async"
3152
- });
3429
+ });
3430
+ },
3431
+ input: function ChatMarkdownInput({ node: _node, type, checked, ...rest }) {
3432
+ if (type !== "checkbox") return /* @__PURE__ */ jsx("input", {
3433
+ ...rest,
3434
+ type
3435
+ });
3436
+ return /* @__PURE__ */ jsx("input", {
3437
+ ...rest,
3438
+ type: "checkbox",
3439
+ checked,
3440
+ readOnly: true,
3441
+ disabled: true,
3442
+ className: "chat-task-checkbox"
3443
+ });
3444
+ },
3445
+ img: function ChatMarkdownImage({ node: _node, src, alt }) {
3446
+ const { resolveFileContentUrl, texts } = useChatMessageMarkdownRuntime();
3447
+ const safeSrc = resolveSafeChatResourceHref(src);
3448
+ if (!safeSrc) return null;
3449
+ const localFileAction = parseChatLocalFileAction(safeSrc);
3450
+ const resolvedSrc = localFileAction && resolveFileContentUrl ? resolveFileContentUrl(localFileAction) : safeSrc;
3451
+ if (!resolvedSrc) return null;
3452
+ return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3453
+ alt: alt || "",
3454
+ closeLabel: texts.attachmentCloseLabel ?? "Close preview",
3455
+ expandLabel: texts.attachmentExpandLabel ?? "Expand image",
3456
+ sizeLabel: null,
3457
+ src: resolvedSrc
3458
+ });
3459
+ },
3460
+ code: function ChatMarkdownCode({ node: _node, className, children, ...rest }) {
3461
+ const { isStreaming, renderInlineDisplay, texts } = useChatMessageMarkdownRuntime();
3462
+ const plainText = String(children ?? "");
3463
+ if (!className && !plainText.includes("\n")) return /* @__PURE__ */ jsx("code", {
3464
+ ...rest,
3465
+ className: cn("chat-inline-code", className),
3466
+ children
3467
+ });
3468
+ const inlineDisplay = isChatInlineDisplayLanguage(className) ? parseChatInlineDisplayDirective(plainText) : null;
3469
+ if (inlineDisplay) return /* @__PURE__ */ jsx(ChatInlineDisplay, {
3470
+ display: inlineDisplay,
3471
+ renderInlineDisplay
3472
+ });
3473
+ if (className?.split(" ").includes("language-mermaid")) return /* @__PURE__ */ jsx(ChatMermaidDiagram, {
3474
+ isStreaming,
3475
+ source: plainText,
3476
+ texts
3477
+ });
3478
+ return /* @__PURE__ */ jsx(ChatCodeBlock, {
3479
+ className,
3480
+ texts,
3481
+ children
3482
+ });
3483
+ }
3484
+ };
3485
+ function ChatMessageMarkdown({ text, role, texts, inline = false, isStreaming = false, inlineTokens, onFileOpen, onInlineTokenClick, resolveFileContentUrl, renderInlineDisplay }) {
3486
+ const isUser = role === "user";
3487
+ const remarkPlugins = inlineTokens?.length ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm];
3488
+ const WrapperTag = inline ? "span" : "div";
3489
+ return /* @__PURE__ */ jsx(ChatMessageMarkdownRuntimeContext.Provider, {
3490
+ value: {
3491
+ inline,
3492
+ isStreaming,
3493
+ isUser,
3494
+ onFileOpen,
3495
+ onInlineTokenClick,
3496
+ renderInlineDisplay,
3497
+ resolveFileContentUrl,
3498
+ texts
3153
3499
  },
3154
- code: ({ node: _node, className, children, ...rest }) => {
3155
- const plainText = String(children ?? "");
3156
- if (!className && !plainText.includes("\n")) return /* @__PURE__ */ jsx("code", {
3157
- ...rest,
3158
- className: cn("chat-inline-code", className),
3159
- children
3160
- });
3161
- const inlineDisplay = isChatInlineDisplayLanguage(className) ? parseChatInlineDisplayDirective(plainText) : null;
3162
- if (inlineDisplay) return /* @__PURE__ */ jsx(ChatInlineDisplay, {
3163
- display: inlineDisplay,
3164
- renderInlineDisplay
3165
- });
3166
- return /* @__PURE__ */ jsx(ChatCodeBlock, {
3167
- className,
3168
- texts,
3169
- children
3170
- });
3171
- }
3172
- }), [
3173
- inline,
3174
- isUser,
3175
- onFileOpen,
3176
- onInlineTokenClick,
3177
- renderInlineDisplay,
3178
- texts
3179
- ]);
3180
- return /* @__PURE__ */ jsx(inline ? "span" : "div", {
3181
- className: cn("chat-markdown", isUser ? "chat-markdown-user" : "chat-markdown-assistant"),
3182
- children: /* @__PURE__ */ jsx(ReactMarkdown, {
3183
- skipHtml: true,
3184
- remarkPlugins,
3185
- components: markdownComponents,
3186
- children: trimMarkdown(text)
3500
+ children: /* @__PURE__ */ jsx(WrapperTag, {
3501
+ className: cn("chat-markdown", isUser ? "chat-markdown-user" : "chat-markdown-assistant"),
3502
+ children: /* @__PURE__ */ jsx(ReactMarkdown, {
3503
+ skipHtml: true,
3504
+ remarkPlugins,
3505
+ components: CHAT_MESSAGE_MARKDOWN_COMPONENTS,
3506
+ urlTransform: transformChatResourceHref,
3507
+ children: trimMarkdown(text)
3508
+ })
3187
3509
  })
3188
3510
  });
3189
3511
  }
@@ -3451,109 +3773,6 @@ function FileCategoryGlyph({ category, isUser }) {
3451
3773
  })
3452
3774
  });
3453
3775
  }
3454
- function ChatMessageImageLightbox({ alt, closeLabel, onClose, src }) {
3455
- const titleId = useId();
3456
- useEffect(() => {
3457
- const previousOverflow = document.body.style.overflow;
3458
- document.body.style.overflow = "hidden";
3459
- const onKeyDown = (event) => {
3460
- if (event.key === "Escape") {
3461
- event.preventDefault();
3462
- onClose();
3463
- }
3464
- };
3465
- window.addEventListener("keydown", onKeyDown);
3466
- return () => {
3467
- document.body.style.overflow = previousOverflow;
3468
- window.removeEventListener("keydown", onKeyDown);
3469
- };
3470
- }, [onClose]);
3471
- return createPortal(/* @__PURE__ */ jsxs("div", {
3472
- role: "dialog",
3473
- "aria-modal": "true",
3474
- "aria-labelledby": titleId,
3475
- className: "fixed inset-0 z-[var(--z-modal,10050)] flex items-center justify-center bg-black/80 p-4 backdrop-blur-[2px]",
3476
- "data-testid": "chat-message-image-lightbox",
3477
- onClick: onClose,
3478
- children: [
3479
- /* @__PURE__ */ jsx("span", {
3480
- id: titleId,
3481
- className: "sr-only",
3482
- children: alt
3483
- }),
3484
- /* @__PURE__ */ jsx("button", {
3485
- type: "button",
3486
- "aria-label": closeLabel,
3487
- className: "absolute right-4 top-4 inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/55 text-white transition-colors hover:bg-black/70 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/70",
3488
- onClick: (event) => {
3489
- event.stopPropagation();
3490
- onClose();
3491
- },
3492
- children: /* @__PURE__ */ jsx(X, {
3493
- className: "h-4 w-4",
3494
- strokeWidth: 2
3495
- })
3496
- }),
3497
- /* @__PURE__ */ jsx("img", {
3498
- src,
3499
- alt,
3500
- className: "max-h-[min(92vh,100%)] max-w-[min(96vw,100%)] object-contain shadow-2xl",
3501
- onClick: (event) => event.stopPropagation()
3502
- })
3503
- ]
3504
- }), document.body);
3505
- }
3506
- function ChatMessageImagePreview({ expandLabel, closeLabel, file, sizeLabel }) {
3507
- const [isExpanded, setIsExpanded] = useState(false);
3508
- if (!file.dataUrl) return null;
3509
- const openLightbox = () => setIsExpanded(true);
3510
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
3511
- className: "group/image relative w-fit max-w-full overflow-hidden rounded-xl",
3512
- children: [
3513
- /* @__PURE__ */ jsx("button", {
3514
- type: "button",
3515
- className: "block w-fit max-w-full text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
3516
- onClick: openLightbox,
3517
- onDoubleClick: (event) => {
3518
- event.preventDefault();
3519
- openLightbox();
3520
- },
3521
- "aria-label": expandLabel,
3522
- children: /* @__PURE__ */ jsx("img", {
3523
- src: file.dataUrl,
3524
- alt: file.label,
3525
- className: "block h-auto max-h-[26rem] max-w-full rounded-xl bg-transparent object-contain"
3526
- })
3527
- }),
3528
- /* @__PURE__ */ jsx("button", {
3529
- type: "button",
3530
- "aria-label": expandLabel,
3531
- className: "absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center rounded-md bg-black/45 text-white opacity-0 transition-opacity duration-150 hover:bg-black/60 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-white/70 group-hover/image:opacity-100",
3532
- onClick: (event) => {
3533
- event.preventDefault();
3534
- event.stopPropagation();
3535
- openLightbox();
3536
- },
3537
- children: /* @__PURE__ */ jsx(Expand, {
3538
- className: "h-3.5 w-3.5",
3539
- strokeWidth: 2
3540
- })
3541
- }),
3542
- sizeLabel ? /* @__PURE__ */ jsx("div", {
3543
- className: "pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-start bg-gradient-to-t from-black/50 via-black/15 to-transparent px-2.5 pb-2 pt-8 opacity-0 transition-opacity duration-150 group-hover/image:opacity-100 group-focus-within/image:opacity-100",
3544
- children: /* @__PURE__ */ jsx("span", {
3545
- className: "inline-flex items-center rounded-md bg-black/40 px-1.5 py-0.5 text-[10px] font-medium text-white/95 backdrop-blur-sm",
3546
- children: sizeLabel
3547
- })
3548
- }) : null
3549
- ]
3550
- }), isExpanded ? /* @__PURE__ */ jsx(ChatMessageImageLightbox, {
3551
- alt: file.label,
3552
- closeLabel,
3553
- onClose: () => setIsExpanded(false),
3554
- src: file.dataUrl
3555
- }) : null] });
3556
- }
3557
3776
  function renderFileCardHeader(params) {
3558
3777
  const { category, file, categoryLabel, sizeLabel, isUser, action } = params;
3559
3778
  return /* @__PURE__ */ jsxs("div", {
@@ -3633,11 +3852,12 @@ function ChatMessageFile({ file, isUser = false, texts, onOpen }) {
3633
3852
  const closeLabel = texts?.attachmentCloseLabel ?? "Close preview";
3634
3853
  const categoryLabel = readFileCategoryLabel(category, texts);
3635
3854
  const shellClasses = cn("block overflow-hidden rounded-xl border transition-colors", isUser ? "border-black/8 bg-black/[0.03] text-foreground" : "border-border/70 bg-card text-card-foreground", isInteractive && (isUser ? "hover:bg-black/[0.05]" : "hover:bg-muted/50"));
3636
- if (renderAsImage) return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3855
+ if (renderAsImage && file.dataUrl) return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3856
+ alt: file.label,
3637
3857
  expandLabel,
3638
3858
  closeLabel,
3639
- file,
3640
- sizeLabel
3859
+ sizeLabel,
3860
+ src: file.dataUrl
3641
3861
  });
3642
3862
  if (renderAsAudio || renderAsVideo) return renderInlineMediaCard({
3643
3863
  category,
@@ -3676,6 +3896,54 @@ function ChatMessageFile({ file, isUser = false, texts, onOpen }) {
3676
3896
  });
3677
3897
  }
3678
3898
  //#endregion
3899
+ //#region src/components/chat/ui/chat-message-list/chat-message-file/chat-message-image-row.tsx
3900
+ function ChatMessageImageRow({ group, indexOffset, isUser, texts, onOpen }) {
3901
+ return /* @__PURE__ */ jsx("div", {
3902
+ "data-chat-message-image-row": "three-column",
3903
+ "data-chat-message-wide-content": "true",
3904
+ className: "grid w-full grid-cols-3 items-start gap-3",
3905
+ children: group.items.map(({ index, part }) => /* @__PURE__ */ jsx(ChatMessageFile, {
3906
+ file: part.file,
3907
+ isUser,
3908
+ texts,
3909
+ onOpen
3910
+ }, `file-${indexOffset + index}`))
3911
+ });
3912
+ }
3913
+ //#endregion
3914
+ //#region src/components/chat/ui/chat-message-list/chat-message-file/chat-message-image-group.utils.ts
3915
+ function isPreviewableImageBlock(block) {
3916
+ return Boolean(block?.kind === "part" && block.part.type === "file" && block.part.file.isImage && block.part.file.dataUrl);
3917
+ }
3918
+ function groupConsecutiveImageFileBlocks(blocks) {
3919
+ const grouped = [];
3920
+ let index = 0;
3921
+ while (index < blocks.length) {
3922
+ const block = blocks[index];
3923
+ if (!isPreviewableImageBlock(block)) {
3924
+ if (block) grouped.push(block);
3925
+ index += 1;
3926
+ continue;
3927
+ }
3928
+ const startIndex = index;
3929
+ const items = [];
3930
+ while (isPreviewableImageBlock(blocks[index])) {
3931
+ items.push(blocks[index]);
3932
+ index += 1;
3933
+ }
3934
+ if (items.length < 3) {
3935
+ grouped.push(...items);
3936
+ continue;
3937
+ }
3938
+ grouped.push({
3939
+ kind: "image-group",
3940
+ key: `image-group-${startIndex}`,
3941
+ items
3942
+ });
3943
+ }
3944
+ return grouped;
3945
+ }
3946
+ //#endregion
3679
3947
  //#region src/components/chat/hooks/use-reasoning-block-open-state.ts
3680
3948
  function useReasoningBlockOpenState(params) {
3681
3949
  const { isInProgress } = params;
@@ -3711,7 +3979,6 @@ const DEFAULT_STICKY_THRESHOLD_PX = 10;
3711
3979
  function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey, scrollRef, stickyThresholdPx }) {
3712
3980
  const [isAtBottom, setIsAtBottom] = useState(true);
3713
3981
  const isStickyRef = useRef(true);
3714
- const isProgrammaticScrollRef = useRef(false);
3715
3982
  const previousResetKeyRef = useRef(null);
3716
3983
  const pendingInitialScrollRef = useRef(false);
3717
3984
  const scheduledScrollFrameRef = useRef(null);
@@ -3729,7 +3996,6 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3729
3996
  scheduledScrollFrameRef.current = null;
3730
3997
  const currentElement = scrollRef.current;
3731
3998
  if (!currentElement) return;
3732
- isProgrammaticScrollRef.current = true;
3733
3999
  if (typeof currentElement.scrollTo === "function") currentElement.scrollTo({
3734
4000
  top: currentElement.scrollHeight,
3735
4001
  behavior
@@ -3742,10 +4008,6 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3742
4008
  queueScrollToBottom();
3743
4009
  }, [queueScrollToBottom, updateStickyState]);
3744
4010
  const onScroll = useCallback(() => {
3745
- if (isProgrammaticScrollRef.current) {
3746
- isProgrammaticScrollRef.current = false;
3747
- return;
3748
- }
3749
4011
  const element = scrollRef.current;
3750
4012
  if (!element) return;
3751
4013
  updateStickyState(resolveIsAtBottom(element));
@@ -3803,15 +4065,23 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3803
4065
  * Collapsed process/tool rows should feel like ordinary body text lines.
3804
4066
  */
3805
4067
  const CHAT_PROCESS_LEADING_COL_CLASS = "inline-flex h-[1.15em] w-[1.15em] shrink-0 items-center justify-center text-current";
3806
- const CHAT_PROCESS_META_ROW_CLASS = "group/process-row flex w-full min-w-0 items-center gap-1.5 py-0 text-[0.925rem] font-normal leading-[1.72] text-muted-foreground";
4068
+ const CHAT_PROCESS_META_ROW_CLASS = "group/process-row flex w-full min-w-0 items-center gap-1.5 py-0 text-[0.925rem] font-normal leading-[1.72] text-muted-foreground/80";
3807
4069
  function ChatProcessLeadingIcon({ children, className }) {
3808
4070
  return /* @__PURE__ */ jsx("span", {
3809
4071
  className: cn(CHAT_PROCESS_LEADING_COL_CLASS, className),
3810
4072
  children
3811
4073
  });
3812
4074
  }
4075
+ function ChatProcessWorkflowRail({ position }) {
4076
+ return /* @__PURE__ */ jsx("span", {
4077
+ "aria-hidden": "true",
4078
+ "data-tool-workflow-rail": "true",
4079
+ 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")
4080
+ });
4081
+ }
3813
4082
  function ChatProcessMetaRow({ children, className, interactive = false, onClick, role, tabIndex, onKeyDown }) {
3814
4083
  return /* @__PURE__ */ jsx("div", {
4084
+ "data-chat-process-meta-row": "true",
3815
4085
  className: cn(CHAT_PROCESS_META_ROW_CLASS, interactive ? "cursor-pointer transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35" : null, className),
3816
4086
  onClick,
3817
4087
  role,
@@ -3827,22 +4097,33 @@ function ChatProcessMetaRow({ children, className, interactive = false, onClick,
3827
4097
  * Rendered as a plain button-like row (not <summary>) so browsers never inject
3828
4098
  * a default disclosure label such as "详情" / "Details".
3829
4099
  */
3830
- function ChatCollapsibleMetaSummary({ label, openGroup, open = false, className, labelClassName, onClick }) {
4100
+ function ChatCollapsibleMetaSummary({ label, openGroup, open = false, icon: Icon, className, leadingIconClassName, labelClassName, onClick }) {
3831
4101
  return /* @__PURE__ */ jsxs("button", {
3832
4102
  type: "button",
4103
+ "data-chat-process-meta-row": "true",
3833
4104
  className: cn(CHAT_PROCESS_META_ROW_CLASS, "m-0 w-full border-0 bg-transparent p-0 text-left shadow-none transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35", className),
3834
4105
  "aria-expanded": open,
3835
4106
  onClick,
3836
- children: [/* @__PURE__ */ jsx("span", {
3837
- className: cn("min-w-0 shrink truncate", labelClassName),
3838
- children: label
3839
- }), /* @__PURE__ */ jsx("span", {
3840
- className: cn("inline-flex h-[1.15em] w-[1.15em] shrink-0 items-center justify-center text-muted-foreground/80 transition-opacity", open ? "opacity-100" : "opacity-0 group-hover/process-row:opacity-100 focus-visible:opacity-100"),
3841
- children: /* @__PURE__ */ jsx(ChevronRight, {
3842
- className: cn("h-[1.05em] w-[1.05em] transition-transform", open && "rotate-90"),
3843
- strokeWidth: 2.25
4107
+ children: [
4108
+ Icon ? /* @__PURE__ */ jsx(ChatProcessLeadingIcon, {
4109
+ className: cn("relative z-[1] rounded-sm", leadingIconClassName),
4110
+ children: /* @__PURE__ */ jsx(Icon, {
4111
+ className: "h-[1.05em] w-[1.05em]",
4112
+ strokeWidth: 2.25
4113
+ })
4114
+ }) : null,
4115
+ /* @__PURE__ */ jsx("span", {
4116
+ className: cn("min-w-0 shrink truncate", labelClassName),
4117
+ children: label
4118
+ }),
4119
+ /* @__PURE__ */ jsx("span", {
4120
+ 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"),
4121
+ children: /* @__PURE__ */ jsx(ChevronRight, {
4122
+ className: cn("h-[1.05em] w-[1.05em] transition-transform", open && "rotate-90"),
4123
+ strokeWidth: 2.25
4124
+ })
3844
4125
  })
3845
- })]
4126
+ ]
3846
4127
  });
3847
4128
  }
3848
4129
  //#endregion
@@ -3857,7 +4138,7 @@ function ChatReasoningBlock({ label, text, characterCountTemplates, isUser, isIn
3857
4138
  const displayLabel = formatReasoningLabel(label, text, isInProgress ? characterCountTemplates?.inProgress : characterCountTemplates?.completed);
3858
4139
  const { onScroll } = useStickyBottomScroll({
3859
4140
  scrollRef,
3860
- resetKey: `${displayLabel}:${isInProgress ? "streaming" : "idle"}`,
4141
+ resetKey: "reasoning",
3861
4142
  isLoading: false,
3862
4143
  hasContent: text.length > 0,
3863
4144
  contentVersion: text,
@@ -3868,6 +4149,8 @@ function ChatReasoningBlock({ label, text, characterCountTemplates, isUser, isIn
3868
4149
  children: [/* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
3869
4150
  openGroup: "reasoning",
3870
4151
  open: isOpen,
4152
+ icon: Brain,
4153
+ leadingIconClassName: isUser ? "bg-primary" : "bg-card",
3871
4154
  label: displayLabel,
3872
4155
  labelClassName: isUser ? "text-primary-100" : void 0,
3873
4156
  onClick: onSummaryClick
@@ -3902,6 +4185,38 @@ function ToolCardContent({ children, className }) {
3902
4185
  children
3903
4186
  });
3904
4187
  }
4188
+ function ToolCardDetailSection({ label, tone, children }) {
4189
+ const style = {
4190
+ input: {
4191
+ dot: "bg-muted-foreground/60",
4192
+ body: "text-foreground"
4193
+ },
4194
+ output: {
4195
+ dot: "bg-primary/70",
4196
+ body: "text-foreground"
4197
+ },
4198
+ error: {
4199
+ dot: "bg-rose-500/80",
4200
+ body: "text-rose-950/85"
4201
+ }
4202
+ }[tone];
4203
+ return /* @__PURE__ */ jsxs("section", {
4204
+ className: "overflow-hidden rounded-md border border-border/70 bg-muted/20",
4205
+ children: [/* @__PURE__ */ jsxs("div", {
4206
+ 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",
4207
+ children: [/* @__PURE__ */ jsx("span", { className: cn("h-1.5 w-1.5 rounded-full", style.dot) }), /* @__PURE__ */ jsx("span", {
4208
+ className: "normal-case tracking-normal",
4209
+ children: label
4210
+ })]
4211
+ }), /* @__PURE__ */ jsx("div", {
4212
+ className: "w-full overflow-hidden",
4213
+ children: /* @__PURE__ */ jsx("pre", {
4214
+ 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),
4215
+ children
4216
+ })
4217
+ })]
4218
+ });
4219
+ }
3905
4220
  //#endregion
3906
4221
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-status.tsx
3907
4222
  const STATUS_STYLES = {
@@ -4077,6 +4392,9 @@ function readVisibleLineNumber(line) {
4077
4392
  const value = line.newLineNumber ?? line.oldLineNumber;
4078
4393
  return typeof value === "number" ? String(value) : "";
4079
4394
  }
4395
+ function isTargetLine(line, targetLine) {
4396
+ return typeof targetLine === "number" && (line.newLineNumber ?? line.oldLineNumber) === targetLine;
4397
+ }
4080
4398
  function readLineKey(prefix, line, index) {
4081
4399
  return `${prefix}-${index}-${line.oldLineNumber ?? "x"}-${line.newLineNumber ?? "x"}`;
4082
4400
  }
@@ -4120,25 +4438,34 @@ function getCodeRowTone(line) {
4120
4438
  if (line.kind === "add") return "bg-emerald-50 text-emerald-950";
4121
4439
  return "bg-card text-foreground";
4122
4440
  }
4123
- function FileOperationLineNumberCell({ layout, line, lineNumberColumnWidth }) {
4441
+ function FileOperationLineNumberCell({ layout, line, lineNumberColumnWidth, target }) {
4124
4442
  return /* @__PURE__ */ jsx("span", {
4125
4443
  "data-file-line-number-cell": "true",
4126
4444
  style: layout === "compact" ? {
4127
4445
  width: lineNumberColumnWidth,
4128
4446
  minWidth: lineNumberColumnWidth
4129
4447
  } : void 0,
4130
- className: cn(FILE_LINE_NUMBER_CELL_CLASS_NAME, layout === "compact" ? "sticky left-0 z-[1]" : "w-full shrink-0", getLineNumberTone(line)),
4448
+ className: cn(FILE_LINE_NUMBER_CELL_CLASS_NAME, layout === "compact" ? "sticky left-0 z-[1]" : "w-full shrink-0", getLineNumberTone(line), target ? "bg-gray-200 font-medium text-gray-950" : null),
4131
4449
  children: readVisibleLineNumber(line)
4132
4450
  });
4133
4451
  }
4134
- function FileOperationCodeCell({ language, line }) {
4452
+ function FileOperationCodeCell({ language, line, target, targetColumn, targetRef }) {
4135
4453
  const highlightedCode = useMemo(() => chatCodeSyntaxHighlighter.highlight(line.text || " ", language), [language, line.text]);
4136
- return /* @__PURE__ */ jsx("span", {
4454
+ const visibleTargetColumn = typeof targetColumn === "number" && Number.isSafeInteger(targetColumn) && targetColumn > 0 ? Math.min(targetColumn, line.text.length + 1) : null;
4455
+ return /* @__PURE__ */ jsxs("span", {
4456
+ ref: visibleTargetColumn ? void 0 : targetRef,
4137
4457
  "data-file-code-row": "true",
4138
4458
  "data-file-code-language": highlightedCode.language,
4139
4459
  "data-highlighted": highlightedCode.highlighted ? "true" : "false",
4140
- className: cn("chat-file-code-syntax hljs block min-w-0 flex-1 whitespace-pre px-2.5", readLanguageClassName(highlightedCode.language), getCodeRowTone(line)),
4141
- dangerouslySetInnerHTML: { __html: highlightedCode.html }
4460
+ "data-file-target-column": visibleTargetColumn ?? void 0,
4461
+ className: cn("chat-file-code-syntax hljs relative block min-w-0 flex-1 whitespace-pre px-2.5", readLanguageClassName(highlightedCode.language), getCodeRowTone(line), target ? "bg-gray-100/90" : null),
4462
+ children: [/* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: highlightedCode.html } }), visibleTargetColumn ? /* @__PURE__ */ jsx("span", {
4463
+ ref: targetRef,
4464
+ "aria-hidden": "true",
4465
+ "data-file-target-caret": "true",
4466
+ className: "pointer-events-none absolute inset-y-0 w-px bg-primary",
4467
+ style: { left: `calc(0.625rem + ${visibleTargetColumn - 1}ch)` }
4468
+ }) : null]
4142
4469
  });
4143
4470
  }
4144
4471
  function FileOperationLineRow({ language, line, showLineNumbers, lineNumberColumnWidth }) {
@@ -4155,11 +4482,22 @@ function FileOperationLineRow({ language, line, showLineNumbers, lineNumberColum
4155
4482
  })]
4156
4483
  });
4157
4484
  }
4158
- function FileOperationWorkspaceSurface({ block }) {
4485
+ function FileOperationWorkspaceSurface({ block, targetColumn, targetLine }) {
4159
4486
  const showLineNumbers = readHasBlockLineNumbers(block);
4160
4487
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
4161
4488
  const codeColumnWidth = readCodeColumnWidth(block);
4162
4489
  const language = readBlockLanguage(block);
4490
+ const targetRef = useRef(null);
4491
+ useLayoutEffect(() => {
4492
+ targetRef.current?.scrollIntoView({
4493
+ block: "center",
4494
+ inline: targetColumn ? "center" : "nearest"
4495
+ });
4496
+ }, [
4497
+ block,
4498
+ targetColumn,
4499
+ targetLine
4500
+ ]);
4163
4501
  return /* @__PURE__ */ jsxs("div", {
4164
4502
  "data-file-code-surface": "true",
4165
4503
  "data-file-code-surface-layout": "workspace",
@@ -4174,7 +4512,8 @@ function FileOperationWorkspaceSurface({ block }) {
4174
4512
  children: [block.lines.map((line, index) => /* @__PURE__ */ jsx(FileOperationLineNumberCell, {
4175
4513
  layout: "workspace",
4176
4514
  line,
4177
- lineNumberColumnWidth
4515
+ lineNumberColumnWidth,
4516
+ target: isTargetLine(line, targetLine)
4178
4517
  }, readLineKey("gutter", line, index))), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 border-r border-border bg-muted" })]
4179
4518
  }) : null, /* @__PURE__ */ jsxs("div", {
4180
4519
  "data-file-code-canvas": "true",
@@ -4183,20 +4522,32 @@ function FileOperationWorkspaceSurface({ block }) {
4183
4522
  "data-file-code-stack": "true",
4184
4523
  className: "min-w-full",
4185
4524
  style: { minWidth: codeColumnWidth },
4186
- children: block.lines.map((line, index) => /* @__PURE__ */ jsx("div", {
4187
- "data-file-code-canvas-row": "true",
4188
- className: FILE_ROW_CLASS_NAME,
4189
- children: /* @__PURE__ */ jsx(FileOperationCodeCell, {
4190
- language,
4191
- line
4192
- })
4193
- }, readLineKey("code", line, index)))
4525
+ children: block.lines.map((line, index) => {
4526
+ const target = isTargetLine(line, targetLine);
4527
+ return /* @__PURE__ */ jsx("div", {
4528
+ "data-file-code-canvas-row": "true",
4529
+ "data-file-target-line": target ? "true" : void 0,
4530
+ "aria-current": target ? "location" : void 0,
4531
+ className: FILE_ROW_CLASS_NAME,
4532
+ children: /* @__PURE__ */ jsx(FileOperationCodeCell, {
4533
+ language,
4534
+ line,
4535
+ target,
4536
+ targetColumn: target ? targetColumn : null,
4537
+ targetRef: target ? targetRef : void 0
4538
+ })
4539
+ }, readLineKey("code", line, index));
4540
+ })
4194
4541
  }), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-card" })]
4195
4542
  })]
4196
4543
  });
4197
4544
  }
4198
- function FileOperationCodeSurface({ block, layout = "compact" }) {
4199
- if (layout === "workspace") return /* @__PURE__ */ jsx(FileOperationWorkspaceSurface, { block });
4545
+ function FileOperationCodeSurface({ block, layout = "compact", targetColumn, targetLine }) {
4546
+ if (layout === "workspace") return /* @__PURE__ */ jsx(FileOperationWorkspaceSurface, {
4547
+ block,
4548
+ targetColumn,
4549
+ targetLine
4550
+ });
4200
4551
  const showLineNumbers = readHasBlockLineNumbers(block);
4201
4552
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
4202
4553
  const codeColumnWidth = readCodeColumnWidth(block);
@@ -4803,9 +5154,12 @@ function TerminalExecutionView({ card, toolLabel }) {
4803
5154
  })] });
4804
5155
  }
4805
5156
  function FileOperationView({ card, toolLabel, onFileOpen }) {
5157
+ const input = card.input?.trim() ?? "";
4806
5158
  const output = card.output?.trim() ?? "";
4807
5159
  const isRunning = card.statusTone === "running";
4808
- const hasContent = Boolean(card.fileOperation?.blocks.length) || Boolean(output);
5160
+ const hasStructuredPreview = Boolean(card.fileOperation?.blocks.length);
5161
+ const showRawInput = Boolean(input) && !hasStructuredPreview;
5162
+ const hasContent = hasStructuredPreview || Boolean(input) || Boolean(output);
4809
5163
  const previewBlocks = card.fileOperation?.blocks ?? [];
4810
5164
  const previewLineCount = previewBlocks.reduce((count, block) => count + block.lines.length, 0);
4811
5165
  const previewCharCount = previewBlocks.reduce((count, block) => {
@@ -4830,13 +5184,21 @@ function FileOperationView({ card, toolLabel, onFileOpen }) {
4830
5184
  canExpand: hasContent || isRunning,
4831
5185
  hideSummary: false,
4832
5186
  onToggle
4833
- }), expanded && hasContent ? /* @__PURE__ */ jsx(ToolCardContent, {
5187
+ }), expanded && hasContent ? /* @__PURE__ */ jsxs(ToolCardContent, {
4834
5188
  className: "bg-transparent py-0",
4835
- children: /* @__PURE__ */ jsx(ToolCardFileOperationContent, {
4836
- card,
4837
- onFileOpen,
4838
- showPathRow: true
4839
- })
5189
+ children: [
5190
+ showRawInput ? /* @__PURE__ */ jsx(ToolCardDetailSection, {
5191
+ label: card.inputLabel?.trim() || "Input",
5192
+ tone: "input",
5193
+ children: input
5194
+ }) : null,
5195
+ showRawInput && output ? /* @__PURE__ */ jsx("div", { className: "h-2" }) : null,
5196
+ hasStructuredPreview || output ? /* @__PURE__ */ jsx(ToolCardFileOperationContent, {
5197
+ card,
5198
+ onFileOpen,
5199
+ showPathRow: true
5200
+ }) : null
5201
+ ]
4840
5202
  }) : null] });
4841
5203
  }
4842
5204
  function SearchSnippetView({ card, toolLabel, icon: Icon = Search }) {
@@ -4865,38 +5227,6 @@ function SearchSnippetView({ card, toolLabel, icon: Icon = Search }) {
4865
5227
  }
4866
5228
  //#endregion
4867
5229
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-generic.tsx
4868
- function GenericToolSection({ label, tone, children }) {
4869
- const style = {
4870
- input: {
4871
- dot: "bg-muted-foreground/60",
4872
- body: "text-foreground"
4873
- },
4874
- output: {
4875
- dot: "bg-primary/70",
4876
- body: "text-foreground"
4877
- },
4878
- error: {
4879
- dot: "bg-rose-500/80",
4880
- body: "text-rose-950/85"
4881
- }
4882
- }[tone];
4883
- return /* @__PURE__ */ jsxs("section", {
4884
- className: "overflow-hidden rounded-md border border-border/70 bg-muted/20",
4885
- children: [/* @__PURE__ */ jsxs("div", {
4886
- className: "flex items-center gap-2 border-b border-border/60 px-2.5 py-1.5 text-[10px] font-medium tracking-wide text-muted-foreground",
4887
- children: [/* @__PURE__ */ jsx("span", { className: cn("h-1.5 w-1.5 rounded-full", style.dot) }), /* @__PURE__ */ jsx("span", {
4888
- className: "normal-case tracking-normal",
4889
- children: label
4890
- })]
4891
- }), /* @__PURE__ */ jsx("div", {
4892
- className: "w-full overflow-hidden",
4893
- children: /* @__PURE__ */ jsx("pre", {
4894
- className: cn("w-full max-w-full min-w-0 max-h-64 overflow-x-auto overflow-y-auto px-2.5 py-2 font-mono text-[12px] leading-relaxed whitespace-pre custom-scrollbar", style.body),
4895
- children
4896
- })
4897
- })]
4898
- });
4899
- }
4900
5230
  function buildToolActionSlot(card, onToolAction, renderToolAgent) {
4901
5231
  const agentAction = card.agentId && renderToolAgent ? renderToolAgent(card.agentId) : null;
4902
5232
  const toolAction = card.action && onToolAction ? /* @__PURE__ */ jsx(ToolCardHeaderAction, {
@@ -4930,13 +5260,13 @@ function GenericToolCard({ card, toolLabel, icon: Icon = Wrench, onToolAction, r
4930
5260
  }), expanded && hasContent ? /* @__PURE__ */ jsxs(ToolCardContent, {
4931
5261
  className: "bg-transparent py-0",
4932
5262
  children: [
4933
- hasInputSection ? /* @__PURE__ */ jsx(GenericToolSection, {
5263
+ hasInputSection ? /* @__PURE__ */ jsx(ToolCardDetailSection, {
4934
5264
  label: card.inputLabel?.trim() || "Input",
4935
5265
  tone: "input",
4936
5266
  children: input
4937
5267
  }) : null,
4938
5268
  hasInputSection && hasOutputSection ? /* @__PURE__ */ jsx("div", { className: "h-2" }) : null,
4939
- hasOutputSection ? /* @__PURE__ */ jsx(GenericToolSection, {
5269
+ hasOutputSection ? /* @__PURE__ */ jsx(ToolCardDetailSection, {
4940
5270
  label: card.outputLabel?.trim() || "Output",
4941
5271
  tone: card.statusTone === "error" ? "error" : "output",
4942
5272
  children: output
@@ -5085,8 +5415,7 @@ function ChatToolCard({ card, toolStatusLabels, onToolAction, onFileOpen, render
5085
5415
  }
5086
5416
  //#endregion
5087
5417
  //#region src/components/chat/ui/chat-message-list/chat-tool-activity-group.tsx
5088
- function ChatToolActivityGroup({ group, isUser, reasoningCharacterCountTemplates, toolStatusLabels, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard }) {
5089
- const [open, setOpen] = useState(false);
5418
+ function ChatToolActivityGroup({ group, open, isUser, reasoningCharacterCountTemplates, toolStatusLabels, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard, onOpenChange }) {
5090
5419
  const toolCount = group.parts.filter((part) => part.type === "tool-card").length;
5091
5420
  const showWorkflowRail = open && toolCount > 1;
5092
5421
  return /* @__PURE__ */ jsxs("div", {
@@ -5094,34 +5423,29 @@ function ChatToolActivityGroup({ group, isUser, reasoningCharacterCountTemplates
5094
5423
  children: [/* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
5095
5424
  openGroup: "tool-activity",
5096
5425
  open,
5426
+ icon: Workflow,
5427
+ leadingIconClassName: "bg-card",
5097
5428
  label: group.label,
5098
- onClick: () => setOpen((current) => !current)
5429
+ onClick: () => onOpenChange(!open)
5099
5430
  }), open ? /* @__PURE__ */ jsx("div", {
5100
5431
  className: "text-[0.925rem] leading-[1.72]",
5101
5432
  children: group.parts.map((part, index) => {
5102
5433
  const isLast = index === group.parts.length - 1;
5103
5434
  return /* @__PURE__ */ jsxs("div", {
5104
5435
  className: "relative min-w-0",
5105
- children: [showWorkflowRail ? /* @__PURE__ */ jsx("div", {
5106
- "aria-hidden": "true",
5107
- "data-tool-workflow-rail": "true",
5108
- className: cn("pointer-events-none absolute left-[0.575em] w-px -translate-x-1/2 bg-border/70", index === 0 ? "bottom-0 top-[0.86em]" : isLast ? "top-0 h-[0.86em]" : "inset-y-0")
5109
- }) : null, part.type === "tool-card" ? /* @__PURE__ */ jsx(ChatToolCard, {
5436
+ children: [showWorkflowRail ? /* @__PURE__ */ jsx(ChatProcessWorkflowRail, { position: index === 0 ? "first" : isLast ? "last" : "middle" }) : null, part.type === "tool-card" ? /* @__PURE__ */ jsx(ChatToolCard, {
5110
5437
  card: part.card,
5111
5438
  toolStatusLabels,
5112
5439
  onToolAction,
5113
5440
  onFileOpen,
5114
5441
  renderToolAgent,
5115
5442
  renderPanelAppCard
5116
- }) : /* @__PURE__ */ jsx("div", {
5117
- className: "pl-[calc(1.15em+0.375rem)]",
5118
- children: /* @__PURE__ */ jsx(ChatReasoningBlock, {
5119
- label: part.label,
5120
- text: part.text,
5121
- characterCountTemplates: reasoningCharacterCountTemplates,
5122
- isUser,
5123
- isInProgress: false
5124
- })
5443
+ }) : /* @__PURE__ */ jsx(ChatReasoningBlock, {
5444
+ label: part.label,
5445
+ text: part.text,
5446
+ characterCountTemplates: reasoningCharacterCountTemplates,
5447
+ isUser,
5448
+ isInProgress: false
5125
5449
  })]
5126
5450
  }, `tool-group-item-${group.startIndex + index}`);
5127
5451
  })
@@ -5134,6 +5458,9 @@ const MAX_SUMMARY_SEGMENTS = 3;
5134
5458
  function isToolCardPart(part) {
5135
5459
  return part.type === "tool-card";
5136
5460
  }
5461
+ function isGroupableToolPart(part) {
5462
+ return part?.type === "reasoning" || part?.type === "tool-card" && !part.card.panelApp;
5463
+ }
5137
5464
  function resolveToolActivityFamily(toolName) {
5138
5465
  const lowered = toolName.toLowerCase();
5139
5466
  if (lowered === "list_dir") return "directory";
@@ -5212,17 +5539,29 @@ function formatToolActivityGroupLabel(params) {
5212
5539
  if (hiddenCount > 0) parts.push(`+${hiddenCount}`);
5213
5540
  return parts.join(" · ");
5214
5541
  }
5215
- /**
5216
- * Group tool cards separated only by reasoning into one visible workflow.
5217
- * Markdown, files and unknown parts still break the run.
5218
- * Single tool-cards stay ungrouped so they keep their native card UI.
5219
- */
5542
+ function collectToolGroupParts(parts, startIndex) {
5543
+ const candidateParts = [];
5544
+ let index = startIndex;
5545
+ let lastToolOffset = -1;
5546
+ while (true) {
5547
+ const candidate = parts[index];
5548
+ if (!isGroupableToolPart(candidate)) break;
5549
+ candidateParts.push(candidate);
5550
+ if (candidate.type === "tool-card") lastToolOffset = candidateParts.length - 1;
5551
+ index += 1;
5552
+ }
5553
+ return {
5554
+ candidateParts,
5555
+ lastToolOffset
5556
+ };
5557
+ }
5558
+ /** Groups tool cards across reasoning; content and interactive panel apps remain stable boundaries. */
5220
5559
  function groupConsecutiveToolParts(parts, labels) {
5221
5560
  const blocks = [];
5222
5561
  let index = 0;
5223
5562
  while (index < parts.length) {
5224
5563
  const part = parts[index];
5225
- if (part.type !== "tool-card") {
5564
+ if (part.type !== "tool-card" || part.card.panelApp) {
5226
5565
  blocks.push({
5227
5566
  kind: "part",
5228
5567
  key: `part-${index}`,
@@ -5233,14 +5572,7 @@ function groupConsecutiveToolParts(parts, labels) {
5233
5572
  continue;
5234
5573
  }
5235
5574
  const startIndex = index;
5236
- const candidateParts = [];
5237
- let lastToolOffset = -1;
5238
- while (index < parts.length && (parts[index]?.type === "tool-card" || parts[index]?.type === "reasoning")) {
5239
- const candidate = parts[index];
5240
- candidateParts.push(candidate);
5241
- if (candidate.type === "tool-card") lastToolOffset = candidateParts.length - 1;
5242
- index += 1;
5243
- }
5575
+ const { candidateParts, lastToolOffset } = collectToolGroupParts(parts, startIndex);
5244
5576
  const toolParts = candidateParts.filter(isToolCardPart);
5245
5577
  if (toolParts.length === 1) {
5246
5578
  blocks.push({
@@ -5261,9 +5593,9 @@ function groupConsecutiveToolParts(parts, labels) {
5261
5593
  });
5262
5594
  blocks.push({
5263
5595
  kind: "tool-group",
5264
- key: `tool-group-${startIndex}-${index - 1}`,
5596
+ key: `tool-group-${startIndex}`,
5265
5597
  group: {
5266
- key: `tool-group-${startIndex}-${index - 1}`,
5598
+ key: `tool-group-${startIndex}`,
5267
5599
  startIndex,
5268
5600
  endIndex: index - 1,
5269
5601
  parts: groupedParts,
@@ -5347,7 +5679,7 @@ function splitAssistantProcess(message) {
5347
5679
  finalParts: message.parts.slice(lastProcessPartIndex + 1)
5348
5680
  };
5349
5681
  }
5350
- function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, part, renderInlineDisplay, renderPanelAppCard, renderToolAgent, role, texts }) {
5682
+ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, part, renderInlineDisplay, renderPanelAppCard, renderToolAgent, resolveFileContentUrl, role, texts }) {
5351
5683
  const { type } = part;
5352
5684
  if (type === "markdown") {
5353
5685
  const { inlineTokens, text } = part;
@@ -5355,9 +5687,11 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAtta
5355
5687
  text,
5356
5688
  role,
5357
5689
  texts,
5690
+ isStreaming: isInProgress && isLastPart,
5358
5691
  inlineTokens,
5359
5692
  onFileOpen,
5360
5693
  onInlineTokenClick,
5694
+ resolveFileContentUrl,
5361
5695
  renderInlineDisplay
5362
5696
  }, `markdown-${index}`);
5363
5697
  }
@@ -5373,14 +5707,17 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAtta
5373
5707
  }
5374
5708
  if (type === "tool-card") {
5375
5709
  const { card } = part;
5376
- return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(ChatToolCard, {
5377
- card,
5378
- toolStatusLabels: texts.toolStatusLabels,
5379
- onToolAction,
5380
- onFileOpen,
5381
- renderToolAgent,
5382
- renderPanelAppCard
5383
- }) }, `tool-${index}`);
5710
+ return /* @__PURE__ */ jsxs("div", {
5711
+ className: "relative min-w-0",
5712
+ children: [/* @__PURE__ */ jsx(ChatProcessWorkflowRail, { position: "single" }), /* @__PURE__ */ jsx(ChatToolCard, {
5713
+ card,
5714
+ toolStatusLabels: texts.toolStatusLabels,
5715
+ onToolAction,
5716
+ onFileOpen,
5717
+ renderToolAgent,
5718
+ renderPanelAppCard
5719
+ })]
5720
+ }, `tool-${index}`);
5384
5721
  }
5385
5722
  if (type === "file") {
5386
5723
  const { file } = part;
@@ -5411,17 +5748,26 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAtta
5411
5748
  return null;
5412
5749
  }
5413
5750
  function renderMessageParts(params) {
5414
- const { isInProgress, isUser, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, parts, renderInlineDisplay, renderPanelAppCard, renderToolAgent, role, texts, indexOffset = 0 } = params;
5415
- return groupConsecutiveToolParts(parts, resolveToolActivityLabels(texts)).map((block) => {
5751
+ const { isInProgress, isUser, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolActivityOpenChange, onToolAction, openToolGroupKeys, parts, renderInlineDisplay, renderPanelAppCard, renderToolAgent, resolveFileContentUrl, role, texts, indexOffset = 0 } = params;
5752
+ return groupConsecutiveImageFileBlocks(groupConsecutiveToolParts(parts, resolveToolActivityLabels(texts))).map((block) => {
5753
+ if (block.kind === "image-group") return /* @__PURE__ */ jsx(ChatMessageImageRow, {
5754
+ group: block,
5755
+ indexOffset,
5756
+ isUser,
5757
+ texts,
5758
+ onOpen: onAttachmentOpen
5759
+ }, `${block.key}-${indexOffset}`);
5416
5760
  if (block.kind === "tool-group") return /* @__PURE__ */ jsx(ChatToolActivityGroup, {
5417
5761
  group: block.group,
5762
+ open: openToolGroupKeys.has(block.group.key),
5418
5763
  isUser,
5419
5764
  reasoningCharacterCountTemplates: texts.reasoningCharacterCountTemplates,
5420
5765
  toolStatusLabels: texts.toolStatusLabels,
5421
5766
  onToolAction,
5422
5767
  onFileOpen,
5423
5768
  renderToolAgent,
5424
- renderPanelAppCard
5769
+ renderPanelAppCard,
5770
+ onOpenChange: (open) => onToolActivityOpenChange(block.group.key, open)
5425
5771
  }, block.key);
5426
5772
  return renderChatMessagePart({
5427
5773
  part: block.part,
@@ -5435,20 +5781,33 @@ function renderMessageParts(params) {
5435
5781
  onFileOpen,
5436
5782
  onAttachmentOpen,
5437
5783
  onInlineTokenClick,
5784
+ resolveFileContentUrl,
5438
5785
  renderInlineDisplay,
5439
5786
  renderToolAgent,
5440
5787
  renderPanelAppCard
5441
5788
  });
5442
5789
  });
5443
5790
  }
5444
- const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, onFileOpen, onAttachmentOpen, onInlineTokenClick, renderInlineDisplay, renderToolAgent, renderPanelAppCard }) {
5791
+ const ChatMessage = memo(function ChatMessage({ layout, message, texts, onToolAction, onFileOpen, onAttachmentOpen, onInlineTokenClick, renderInlineDisplay, renderToolAgent, renderPanelAppCard, resolveFileContentUrl }) {
5445
5792
  const { role } = message;
5446
5793
  const isUser = role === "user";
5794
+ const isFlat = layout === "flat" && !isUser;
5447
5795
  const isInProgress = isMessageInProgress(message.status);
5448
5796
  const processSplit = splitAssistantProcess(message);
5449
5797
  const [processOpen, setProcessOpen] = useState(false);
5798
+ const [openToolGroupKeys, setOpenToolGroupKeys] = useState(() => /* @__PURE__ */ new Set());
5799
+ const handleToolActivityOpenChange = (groupKey, open) => {
5800
+ setOpenToolGroupKeys((current) => {
5801
+ const next = new Set(current);
5802
+ if (open) next.add(groupKey);
5803
+ else next.delete(groupKey);
5804
+ return next;
5805
+ });
5806
+ if (open) setProcessOpen(true);
5807
+ };
5450
5808
  return /* @__PURE__ */ jsx("div", {
5451
- className: cn("inline-block w-fit max-w-full rounded-2xl border px-4 shadow-sm has-[[data-chat-message-wide-content=true]]:w-full", isUser ? "nextclaw-chat-message-user rounded-[1.45rem] border-primary bg-primary py-2.5 text-primary-foreground shadow-none" : role === "assistant" ? "border-border bg-card pb-3 pt-4 text-card-foreground" : "border-border bg-muted/45 py-3 text-foreground"),
5809
+ "data-chat-message-surface": isFlat ? "flat" : "card",
5810
+ className: cn("max-w-full has-[[data-chat-message-wide-content=true]]:w-full", isFlat ? "block w-full text-foreground" : "inline-block w-fit rounded-2xl border px-4 shadow-sm", !isFlat && (isUser ? "nextclaw-chat-message-user rounded-[1.45rem] border-primary bg-primary py-2.5 text-primary-foreground shadow-none" : role === "assistant" ? "border-border bg-card pb-3 pt-4 text-card-foreground" : "border-border bg-muted/45 py-3 text-foreground")),
5452
5811
  children: /* @__PURE__ */ jsx("div", {
5453
5812
  className: "space-y-0",
5454
5813
  children: processSplit ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
@@ -5458,6 +5817,8 @@ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, on
5458
5817
  children: /* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
5459
5818
  openGroup: "process",
5460
5819
  open: processOpen,
5820
+ icon: ListChecks,
5821
+ leadingIconClassName: "bg-card",
5461
5822
  label: message.processSummary?.label,
5462
5823
  onClick: () => setProcessOpen((current) => !current)
5463
5824
  })
@@ -5473,6 +5834,9 @@ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, on
5473
5834
  onFileOpen,
5474
5835
  onAttachmentOpen,
5475
5836
  onInlineTokenClick,
5837
+ resolveFileContentUrl,
5838
+ openToolGroupKeys,
5839
+ onToolActivityOpenChange: handleToolActivityOpenChange,
5476
5840
  renderInlineDisplay,
5477
5841
  renderToolAgent,
5478
5842
  renderPanelAppCard
@@ -5488,6 +5852,9 @@ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, on
5488
5852
  onFileOpen,
5489
5853
  onAttachmentOpen,
5490
5854
  onInlineTokenClick,
5855
+ resolveFileContentUrl,
5856
+ openToolGroupKeys,
5857
+ onToolActivityOpenChange: handleToolActivityOpenChange,
5491
5858
  renderInlineDisplay,
5492
5859
  renderToolAgent,
5493
5860
  renderPanelAppCard,
@@ -5502,6 +5869,9 @@ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, on
5502
5869
  onFileOpen,
5503
5870
  onAttachmentOpen,
5504
5871
  onInlineTokenClick,
5872
+ resolveFileContentUrl,
5873
+ openToolGroupKeys,
5874
+ onToolActivityOpenChange: handleToolActivityOpenChange,
5505
5875
  renderInlineDisplay,
5506
5876
  renderToolAgent,
5507
5877
  renderPanelAppCard
@@ -5520,14 +5890,25 @@ function ChatMessageActionCopy({ message, texts }) {
5520
5890
  }).filter((text) => !!text && text.trim().length > 0).join("\n\n");
5521
5891
  }, [message.parts]);
5522
5892
  const { copied, copy } = useCopyFeedback({ text: messageText });
5893
+ const label = copied ? texts.copiedMessageLabel : texts.copyMessageLabel;
5894
+ const { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
5523
5895
  if (!messageText) return null;
5524
- return /* @__PURE__ */ jsx("button", {
5525
- type: "button",
5526
- onClick: () => void copy(),
5527
- className: "text-muted-foreground hover:text-foreground transition-colors p-1 rounded-md hover:bg-accent flex items-center justify-center",
5528
- "aria-label": copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
5529
- title: copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
5530
- children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
5896
+ return /* @__PURE__ */ jsx(TooltipProvider, {
5897
+ delayDuration: 250,
5898
+ children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
5899
+ asChild: true,
5900
+ children: /* @__PURE__ */ jsx("button", {
5901
+ type: "button",
5902
+ onClick: () => void copy(),
5903
+ className: "flex items-center justify-center rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35",
5904
+ "aria-label": label,
5905
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
5906
+ })
5907
+ }), /* @__PURE__ */ jsx(TooltipContent, {
5908
+ side: "top",
5909
+ className: "text-xs",
5910
+ children: label
5911
+ })] })
5531
5912
  });
5532
5913
  }
5533
5914
  //#endregion
@@ -5542,7 +5923,6 @@ const TYPING_TEXT_SHEEN_CSS = `
5542
5923
  background-position: -60% 0;
5543
5924
  }
5544
5925
  }
5545
-
5546
5926
  .nextclaw-chat-typing-indicator__text {
5547
5927
  background-image: linear-gradient(
5548
5928
  100deg,
@@ -5596,16 +5976,17 @@ function ChatMessageTypingFooter() {
5596
5976
  })
5597
5977
  });
5598
5978
  }
5599
- function ChatTypingIndicator({ label }) {
5979
+ function ChatTypingIndicator({ label, layout }) {
5600
5980
  return /* @__PURE__ */ jsxs("div", {
5601
- className: "rounded-2xl border border-border bg-card px-4 py-3 text-sm text-muted-foreground shadow-sm",
5981
+ "data-chat-message-surface": layout === "flat" ? "flat" : "card",
5982
+ className: cn("text-sm text-muted-foreground", layout === "flat" ? "py-1" : "rounded-2xl border border-border bg-card px-4 py-3 shadow-sm"),
5602
5983
  children: [/* @__PURE__ */ jsx("style", { children: TYPING_TEXT_SHEEN_CSS }), /* @__PURE__ */ jsx("span", {
5603
5984
  className: "nextclaw-chat-typing-indicator__text",
5604
5985
  children: label
5605
5986
  })]
5606
5987
  });
5607
5988
  }
5608
- function ChatMessageList({ className, isSending, messages, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, renderInlineDisplay, renderPanelAppCard, renderToolAgent, texts }) {
5989
+ function ChatMessageList({ className, isSending, layout = "card", messages, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, renderInlineDisplay, renderPanelAppCard, renderToolAgent, resolveFileContentUrl, texts }) {
5609
5990
  const visibleMessages = messages.filter(hasRenderableMessageContent);
5610
5991
  const hasRenderableAssistantDraft = visibleMessages.some((message) => message.role === "assistant" && (message.status === "streaming" || message.status === "pending"));
5611
5992
  return /* @__PURE__ */ jsxs("div", {
@@ -5613,23 +5994,63 @@ function ChatMessageList({ className, isSending, messages, onAttachmentOpen, onF
5613
5994
  children: [visibleMessages.map((message) => {
5614
5995
  const isUser = message.role === "user";
5615
5996
  const isGenerating = !isUser && (message.status === "streaming" || message.status === "pending");
5997
+ const content = /* @__PURE__ */ jsx(ChatMessage, {
5998
+ layout,
5999
+ message,
6000
+ texts,
6001
+ onToolAction,
6002
+ onFileOpen,
6003
+ onAttachmentOpen,
6004
+ onInlineTokenClick,
6005
+ resolveFileContentUrl,
6006
+ renderInlineDisplay,
6007
+ renderToolAgent,
6008
+ renderPanelAppCard
6009
+ });
6010
+ if (layout === "flat" && !isUser) return /* @__PURE__ */ jsxs("article", {
6011
+ "data-chat-message-layout": "flat",
6012
+ className: "w-full min-w-0 space-y-2",
6013
+ children: [
6014
+ /* @__PURE__ */ jsxs("div", {
6015
+ "data-chat-message-header": "flat",
6016
+ className: "flex min-w-0 items-center gap-2.5",
6017
+ children: [/* @__PURE__ */ jsx(ChatMessageAvatar, {
6018
+ role: message.role,
6019
+ size: "compact"
6020
+ }), /* @__PURE__ */ jsx("span", {
6021
+ className: "truncate text-sm font-semibold text-foreground",
6022
+ children: message.roleLabel
6023
+ })]
6024
+ }),
6025
+ /* @__PURE__ */ jsx("div", {
6026
+ "data-chat-message-body": "flat",
6027
+ className: "min-w-0 w-full",
6028
+ children: content
6029
+ }),
6030
+ isGenerating ? /* @__PURE__ */ jsx("div", {
6031
+ className: "w-full",
6032
+ children: /* @__PURE__ */ jsx(ChatMessageTypingFooter, {})
6033
+ }) : /* @__PURE__ */ jsxs("div", {
6034
+ "data-chat-message-footer": "flat",
6035
+ className: "flex items-center gap-2",
6036
+ children: [/* @__PURE__ */ jsx("span", {
6037
+ className: "px-1 text-[11px] leading-4 text-muted-foreground",
6038
+ children: message.timestampLabel
6039
+ }), /* @__PURE__ */ jsx(ChatMessageActionCopy, {
6040
+ message,
6041
+ texts
6042
+ })]
6043
+ })
6044
+ ]
6045
+ }, message.id);
5616
6046
  return /* @__PURE__ */ jsxs("div", {
6047
+ "data-chat-message-layout": "card",
5617
6048
  className: cn("flex gap-3", isUser ? "justify-end" : "justify-start"),
5618
6049
  children: [
5619
6050
  !isUser ? /* @__PURE__ */ jsx(ChatMessageAvatar, { role: message.role }) : null,
5620
6051
  /* @__PURE__ */ jsxs("div", {
5621
6052
  className: cn("w-fit max-w-[92%] space-y-2 has-[[data-chat-message-wide-content=true]]:w-full", isUser && "flex flex-col items-end"),
5622
- children: [/* @__PURE__ */ jsx(ChatMessage, {
5623
- message,
5624
- texts,
5625
- onToolAction,
5626
- onFileOpen,
5627
- onAttachmentOpen,
5628
- onInlineTokenClick,
5629
- renderInlineDisplay,
5630
- renderToolAgent,
5631
- renderPanelAppCard
5632
- }), /* @__PURE__ */ jsx("div", {
6053
+ children: [content, /* @__PURE__ */ jsx("div", {
5633
6054
  className: cn("flex items-center gap-2", isUser && "justify-end"),
5634
6055
  children: isGenerating ? /* @__PURE__ */ jsx(ChatMessageTypingFooter, {}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
5635
6056
  className: cn("px-1 text-[11px] leading-4 text-muted-foreground", isUser ? "text-right" : "text-left"),
@@ -5638,18 +6059,25 @@ function ChatMessageList({ className, isSending, messages, onAttachmentOpen, onF
5638
6059
  " · ",
5639
6060
  message.timestampLabel
5640
6061
  ]
5641
- }), !isUser ? /* @__PURE__ */ jsx(ChatMessageActionCopy, {
6062
+ }), /* @__PURE__ */ jsx(ChatMessageActionCopy, {
5642
6063
  message,
5643
6064
  texts
5644
- }) : null] })
6065
+ })] })
5645
6066
  })]
5646
6067
  }),
5647
6068
  isUser ? /* @__PURE__ */ jsx(ChatMessageAvatar, { role: message.role }) : null
5648
6069
  ]
5649
6070
  }, message.id);
5650
6071
  }), isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs("div", {
6072
+ "data-chat-message-layout": layout,
5651
6073
  className: "flex justify-start gap-3",
5652
- children: [/* @__PURE__ */ jsx(ChatMessageAvatar, { role: "assistant" }), /* @__PURE__ */ jsx(ChatTypingIndicator, { label: texts.typingLabel })]
6074
+ children: [/* @__PURE__ */ jsx(ChatMessageAvatar, {
6075
+ role: "assistant",
6076
+ size: layout === "flat" ? "compact" : "default"
6077
+ }), /* @__PURE__ */ jsx(ChatTypingIndicator, {
6078
+ label: texts.typingLabel,
6079
+ layout
6080
+ })]
5653
6081
  }) : null]
5654
6082
  });
5655
6083
  }