@nextclaw/agent-chat-ui 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { twMerge } from "tailwind-merge";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import * as PopoverPrimitive from "@radix-ui/react-popover";
7
7
  import * as SelectPrimitive from "@radix-ui/react-select";
8
- import { AlertTriangle, AppWindow, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, ExternalLink, Eye, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, Globe, ImageIcon, Loader2, Minus, Paperclip, Puzzle, Search, Sparkles, Star, Terminal, User, Wrench } from "lucide-react";
8
+ import { AlertTriangle, AppWindow, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, CalendarClock, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, Expand, ExternalLink, Eye, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, FolderTree, Globe, Globe2, ImageIcon, Loader2, MessageSquare, Minus, Paperclip, Puzzle, Search, Send, Settings, Sparkles, Star, Terminal, User, Wrench, X } from "lucide-react";
9
9
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
10
10
  import { cva } from "class-variance-authority";
11
11
  import { ContentEditable } from "@lexical/react/LexicalContentEditable";
@@ -29,6 +29,7 @@ import sql from "highlight.js/lib/languages/sql";
29
29
  import typescript from "highlight.js/lib/languages/typescript";
30
30
  import xml from "highlight.js/lib/languages/xml";
31
31
  import yaml from "highlight.js/lib/languages/yaml";
32
+ import { createPortal } from "react-dom";
32
33
  const CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC = {
33
34
  key: "slash",
34
35
  marker: "/"
@@ -688,7 +689,7 @@ function ChatInputSurfaceHost(props) {
688
689
  }
689
690
  //#endregion
690
691
  //#region src/components/chat/default-skin/button.tsx
691
- const buttonVariants = cva("inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-all duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", {
692
+ const buttonVariants = cva("inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium transition-all duration-fast focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:pointer-events-none disabled:opacity-50", {
692
693
  variants: {
693
694
  variant: {
694
695
  default: "bg-primary text-primary-foreground hover:bg-primary-600 active:bg-primary-700 shadow-sm",
@@ -1068,7 +1069,7 @@ function AccessoryIcon({ icon }) {
1068
1069
  return icon === "paperclip" ? /* @__PURE__ */ jsx(Paperclip, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(ToolbarIcon, { icon });
1069
1070
  }
1070
1071
  const TRIGGER_WIDTH_BY_KEY = {
1071
- model: "min-w-0 max-w-full flex-1 basis-[12rem] sm:max-w-[320px]",
1072
+ model: "min-w-0 max-w-[18rem]",
1072
1073
  "session-type": "shrink-0",
1073
1074
  thinking: "shrink-0"
1074
1075
  };
@@ -1095,11 +1096,11 @@ function ToolbarSelectTriggerContent({ item }) {
1095
1096
  children: [
1096
1097
  /* @__PURE__ */ jsx(ToolbarIcon, { icon: item.icon }),
1097
1098
  /* @__PURE__ */ jsx("span", {
1098
- className: "nextclaw-chat-toolbar-mobile-label truncate text-xs font-semibold text-foreground sm:hidden [@container_nextclaw-chat-input-bar_(max-width:440px)]:hidden",
1099
+ className: "nextclaw-chat-toolbar-mobile-label truncate sm:hidden [@container_nextclaw-chat-input-bar_(max-width:440px)]:hidden",
1099
1100
  children: mobileSelectedLabel
1100
1101
  }),
1101
1102
  /* @__PURE__ */ jsx("span", {
1102
- className: "nextclaw-chat-toolbar-label hidden truncate text-xs font-semibold text-foreground sm:inline [@container_nextclaw-chat-input-bar_(max-width:440px)]:hidden",
1103
+ className: "nextclaw-chat-toolbar-label hidden truncate sm:inline [@container_nextclaw-chat-input-bar_(max-width:440px)]:hidden",
1103
1104
  children: item.selectedLabel
1104
1105
  })
1105
1106
  ]
@@ -1217,12 +1218,12 @@ function ToolbarSearchableSelect({ item }) {
1217
1218
  type: "button",
1218
1219
  "aria-label": actionLabel,
1219
1220
  "aria-pressed": isActive,
1220
- className: "mr-1 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-card hover:text-amber-500 focus:outline-none focus:ring-1 focus:ring-primary/40",
1221
+ className: "mr-1 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-card hover:text-foreground focus:outline-none focus:ring-1 focus:ring-primary/40",
1221
1222
  onClick: (event) => {
1222
1223
  event.stopPropagation();
1223
1224
  action.onToggle(option.value, !isActive);
1224
1225
  },
1225
- children: /* @__PURE__ */ jsx(Star, { className: `h-3.5 w-3.5 ${isActive ? "fill-amber-400 text-amber-500" : ""}` })
1226
+ children: /* @__PURE__ */ jsx(Star, { className: `h-3.5 w-3.5 ${isActive ? "fill-current text-foreground" : ""}` })
1226
1227
  })
1227
1228
  }), /* @__PURE__ */ jsx(TooltipContent, {
1228
1229
  side: "left",
@@ -1274,7 +1275,7 @@ function ToolbarSelect({ item }) {
1274
1275
  })]
1275
1276
  });
1276
1277
  }
1277
- function ChatInputBarToolbar({ actions, accessories, selects, skillPicker }) {
1278
+ function ChatInputBarToolbar({ actions, accessories, selects, skillPicker, trailingSelects = [] }) {
1278
1279
  const { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
1279
1280
  return /* @__PURE__ */ jsxs("div", {
1280
1281
  className: "flex flex-wrap items-end justify-between gap-2 px-3 pb-3",
@@ -1308,7 +1309,13 @@ function ChatInputBarToolbar({ actions, accessories, selects, skillPicker }) {
1308
1309
  })] }) }, item.key);
1309
1310
  })
1310
1311
  ]
1311
- }), /* @__PURE__ */ jsx(ChatInputBarActions, { ...actions })]
1312
+ }), /* @__PURE__ */ jsxs("div", {
1313
+ className: "flex shrink-0 items-end gap-1",
1314
+ children: [trailingSelects.length > 0 ? /* @__PURE__ */ jsx("div", {
1315
+ className: "nextclaw-chat-toolbar-trailing-selects flex min-w-0 items-center gap-1",
1316
+ children: trailingSelects.map((item) => /* @__PURE__ */ jsx(ToolbarSelect, { item }, item.key))
1317
+ }) : null, /* @__PURE__ */ jsx(ChatInputBarActions, { ...actions })]
1318
+ })]
1312
1319
  });
1313
1320
  }
1314
1321
  //#endregion
@@ -2413,15 +2420,15 @@ function resolveInlineTokenTone(kind) {
2413
2420
  if (kind === "panel_app") return "panel_app";
2414
2421
  return "default";
2415
2422
  }
2416
- function resolveInlineTokenBadgeClassName(tone, isUser) {
2417
- if (tone === "skill") return isUser ? "border-primary-foreground/30 bg-primary-foreground/18 text-primary-foreground" : "border-border bg-accent text-accent-foreground";
2418
- if (tone === "panel_app") return isUser ? "border-primary-foreground/30 bg-primary-foreground/18 text-primary-foreground" : "border-border bg-accent text-accent-foreground";
2419
- return isUser ? "border-primary-foreground/30 bg-primary-foreground/18 text-primary-foreground" : "border-border bg-muted text-muted-foreground";
2423
+ function resolveInlineTokenBadgeClassName(tone, isUser, interactive) {
2424
+ if (tone === "skill") return cn("border-transparent bg-transparent text-[color:var(--md-link)]", interactive && "cursor-pointer underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[color:var(--md-link)]/30", !interactive && "cursor-default");
2425
+ if (tone === "panel_app") return isUser ? "border-primary-foreground/40 bg-primary-foreground/22 text-primary-foreground" : "border-border bg-muted text-foreground";
2426
+ return isUser ? "border-primary-foreground/40 bg-primary-foreground/22 text-primary-foreground" : "border-border bg-muted text-muted-foreground";
2420
2427
  }
2421
2428
  function resolveInlineTokenIconClassName(tone, isUser) {
2422
- if (tone === "skill") return isUser ? "text-primary-foreground/70" : "text-muted-foreground";
2423
- if (tone === "panel_app") return isUser ? "text-primary-foreground/70" : "text-muted-foreground";
2424
- return isUser ? "text-primary-foreground/70" : "text-muted-foreground";
2429
+ if (tone === "skill") return "text-current opacity-80";
2430
+ if (tone === "panel_app") return isUser ? "text-primary-foreground/80" : "text-muted-foreground";
2431
+ return isUser ? "text-primary-foreground/80" : "text-muted-foreground";
2425
2432
  }
2426
2433
  function renderInlineTokenIcon(tone) {
2427
2434
  return tone === "panel_app" ? /* @__PURE__ */ jsx(AppWindow, {
@@ -2432,18 +2439,32 @@ function renderInlineTokenIcon(tone) {
2432
2439
  className: "h-3 w-3"
2433
2440
  });
2434
2441
  }
2435
- function ChatInlineTokenBadge({ kind, label, isUser }) {
2442
+ function ChatInlineTokenBadge({ kind, label, isUser, onClick }) {
2436
2443
  const tone = resolveInlineTokenTone(kind);
2437
- return /* @__PURE__ */ jsxs("span", {
2438
- className: cn("mx-[2px] inline-flex h-7 max-w-full items-center gap-1.5 rounded-xl border px-2.5 align-baseline text-[11px] font-medium shadow-[0_0_0_1px_rgba(15,23,42,0.03)]", resolveInlineTokenBadgeClassName(tone, isUser)),
2444
+ const interactive = Boolean(onClick);
2445
+ const className = cn("nextclaw-chat-inline-token mx-[2px] inline-flex max-w-full items-center gap-1 align-baseline text-[11px] font-medium", tone === "skill" ? "h-auto rounded-none px-0 py-0" : "h-7 rounded-xl border px-2.5", resolveInlineTokenBadgeClassName(tone, isUser, interactive));
2446
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
2447
+ className: cn("inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center", resolveInlineTokenIconClassName(tone, isUser)),
2448
+ children: renderInlineTokenIcon(tone)
2449
+ }), /* @__PURE__ */ jsx("span", {
2450
+ className: "truncate",
2451
+ children: label
2452
+ })] });
2453
+ if (interactive) return /* @__PURE__ */ jsx("button", {
2454
+ type: "button",
2455
+ className,
2439
2456
  title: label,
2440
- children: [/* @__PURE__ */ jsx("span", {
2441
- className: cn("inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center", resolveInlineTokenIconClassName(tone, isUser)),
2442
- children: renderInlineTokenIcon(tone)
2443
- }), /* @__PURE__ */ jsx("span", {
2444
- className: "truncate",
2445
- children: label
2446
- })]
2457
+ onClick: (event) => {
2458
+ event.preventDefault();
2459
+ event.stopPropagation();
2460
+ onClick?.();
2461
+ },
2462
+ children: content
2463
+ });
2464
+ return /* @__PURE__ */ jsx("span", {
2465
+ className,
2466
+ title: label,
2467
+ children: content
2447
2468
  });
2448
2469
  }
2449
2470
  //#endregion
@@ -2649,7 +2670,7 @@ const REGISTERED_LANGUAGES = [
2649
2670
  aliases: ["patch"]
2650
2671
  }
2651
2672
  ];
2652
- function escapeHtml(value) {
2673
+ function escapeHtml$1(value) {
2653
2674
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2654
2675
  }
2655
2676
  function normalizeLanguage(value) {
@@ -2680,7 +2701,7 @@ var ChatCodeSyntaxHighlighter = class {
2680
2701
  return this.createPlainTextResult(code, normalizedLanguage || "text");
2681
2702
  };
2682
2703
  this.createPlainTextResult = (code, language) => ({
2683
- html: escapeHtml(code),
2704
+ html: escapeHtml$1(code),
2684
2705
  language,
2685
2706
  highlighted: false
2686
2707
  });
@@ -3048,7 +3069,7 @@ function readStringProp(props, key) {
3048
3069
  const value = props[key];
3049
3070
  return typeof value === "string" && value.length > 0 ? value : null;
3050
3071
  }
3051
- function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, renderInlineDisplay }) {
3072
+ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, onInlineTokenClick, renderInlineDisplay }) {
3052
3073
  const isUser = role === "user";
3053
3074
  const remarkPlugins = useMemo(() => inlineTokens && inlineTokens.length > 0 ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm], [inlineTokens]);
3054
3075
  const markdownComponents = useMemo(() => ({
@@ -3062,7 +3083,13 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3062
3083
  if (kind && key && label && rawText) return /* @__PURE__ */ jsx(ChatInlineTokenBadge, {
3063
3084
  kind,
3064
3085
  label,
3065
- isUser
3086
+ isUser,
3087
+ onClick: onInlineTokenClick ? () => onInlineTokenClick({
3088
+ kind,
3089
+ key,
3090
+ label,
3091
+ rawText
3092
+ }) : void 0
3066
3093
  });
3067
3094
  return /* @__PURE__ */ jsx("span", {
3068
3095
  ...rest,
@@ -3146,6 +3173,7 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3146
3173
  inline,
3147
3174
  isUser,
3148
3175
  onFileOpen,
3176
+ onInlineTokenClick,
3149
3177
  renderInlineDisplay,
3150
3178
  texts
3151
3179
  ]);
@@ -3162,16 +3190,16 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3162
3190
  //#endregion
3163
3191
  //#region src/components/chat/ui/chat-message-list/chat-message-file/meta.ts
3164
3192
  const FILE_CATEGORY_TILE_CLASSES = {
3165
- archive: "border-border bg-muted text-muted-foreground",
3166
- audio: "border-border bg-muted text-muted-foreground",
3167
- code: "border-border bg-muted text-muted-foreground",
3168
- data: "border-border bg-muted text-muted-foreground",
3169
- document: "border-border bg-muted text-muted-foreground",
3170
- generic: "border-border bg-muted text-muted-foreground",
3171
- image: "border-border bg-muted text-muted-foreground",
3172
- pdf: "border-border bg-muted text-muted-foreground",
3173
- sheet: "border-border bg-muted text-muted-foreground",
3174
- video: "border-border bg-muted text-muted-foreground"
3193
+ archive: "bg-muted text-muted-foreground",
3194
+ audio: "bg-muted text-muted-foreground",
3195
+ code: "bg-muted text-muted-foreground",
3196
+ data: "bg-muted text-muted-foreground",
3197
+ document: "bg-muted text-muted-foreground",
3198
+ generic: "bg-muted text-muted-foreground",
3199
+ image: "bg-muted text-muted-foreground",
3200
+ pdf: "bg-muted text-muted-foreground",
3201
+ sheet: "bg-muted text-muted-foreground",
3202
+ video: "bg-muted text-muted-foreground"
3175
3203
  };
3176
3204
  const CODE_EXTENSIONS = new Set([
3177
3205
  "c",
@@ -3381,77 +3409,166 @@ function readFileCategoryLabel(category, texts) {
3381
3409
  }
3382
3410
  function renderMetaLine(categoryLabel, sizeLabel, isUser) {
3383
3411
  return /* @__PURE__ */ jsx("div", {
3384
- className: cn("mt-1 text-xs leading-5", isUser ? "text-primary-foreground/70" : "text-muted-foreground"),
3412
+ className: cn("mt-0.5 text-[11px] leading-4", isUser ? "text-foreground/55" : "text-muted-foreground"),
3385
3413
  children: sizeLabel ? `${categoryLabel} · ${sizeLabel}` : categoryLabel
3386
3414
  });
3387
3415
  }
3388
3416
  function renderActionPill(label, isUser, isInteractive) {
3389
3417
  return /* @__PURE__ */ jsx("span", {
3390
- className: cn("inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium", isInteractive ? isUser ? "border-primary-foreground/14 bg-primary-foreground/12 text-primary-foreground" : "border-border bg-card text-foreground" : isUser ? "border-primary-foreground/10 bg-primary-foreground/6 text-primary-foreground/62" : "border-border bg-muted text-muted-foreground"),
3418
+ className: cn("inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium", isInteractive ? isUser ? "bg-black/5 text-foreground/75" : "bg-muted text-muted-foreground" : isUser ? "bg-black/[0.03] text-foreground/45" : "bg-muted/70 text-muted-foreground/70"),
3391
3419
  children: label
3392
3420
  });
3393
3421
  }
3394
- function renderActionLink(label, href, isUser) {
3422
+ function renderActionControl(params) {
3423
+ const { label, href, isUser, onOpen } = params;
3424
+ const className = cn("inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium transition-colors", isUser ? "bg-black/5 text-foreground/75 hover:bg-black/10 hover:text-foreground" : "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground");
3425
+ if (onOpen) return /* @__PURE__ */ jsx("button", {
3426
+ type: "button",
3427
+ className,
3428
+ onClick: (event) => {
3429
+ event.preventDefault();
3430
+ event.stopPropagation();
3431
+ onOpen();
3432
+ },
3433
+ children: label
3434
+ });
3435
+ if (!href) return renderActionPill(label, isUser, false);
3395
3436
  return /* @__PURE__ */ jsx("a", {
3396
3437
  href,
3397
3438
  target: "_blank",
3398
3439
  rel: "noreferrer",
3399
- className: cn("inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium transition duration-200", isUser ? "border-primary-foreground/14 bg-primary-foreground/12 text-primary-foreground hover:border-primary-foreground/20 hover:bg-primary-foreground/16" : "border-border bg-card text-foreground hover:border-border-hover hover:bg-accent"),
3440
+ className,
3400
3441
  children: label
3401
3442
  });
3402
3443
  }
3403
3444
  function FileCategoryGlyph({ category, isUser }) {
3404
3445
  const Icon = FILE_CATEGORY_ICONS[category];
3405
3446
  return /* @__PURE__ */ jsx("div", {
3406
- className: cn("flex h-14 w-14 shrink-0 items-center justify-center rounded-[1rem] border", isUser ? "border-primary-foreground/12 bg-primary-foreground/10 text-primary-foreground" : FILE_CATEGORY_TILE_CLASSES[category]),
3447
+ className: cn("flex h-9 w-9 shrink-0 items-center justify-center rounded-lg", isUser ? "bg-black/5 text-foreground/70" : FILE_CATEGORY_TILE_CLASSES[category]),
3407
3448
  children: /* @__PURE__ */ jsx(Icon, {
3408
- className: cn("h-7 w-7", isUser ? "text-primary-foreground/92" : "text-current"),
3409
- strokeWidth: 2.2
3449
+ className: cn("h-4 w-4", isUser ? "text-foreground/70" : "text-current"),
3450
+ strokeWidth: 2
3410
3451
  })
3411
3452
  });
3412
3453
  }
3413
- function renderImagePreview(params) {
3414
- const { file, categoryLabel, sizeLabel, isUser } = params;
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);
3415
3508
  if (!file.dataUrl) return null;
3416
- return /* @__PURE__ */ jsx("a", {
3417
- href: file.dataUrl,
3418
- target: "_blank",
3419
- rel: "noreferrer",
3420
- className: "group block",
3421
- children: /* @__PURE__ */ jsxs("div", {
3422
- className: cn("relative overflow-hidden rounded-[1rem]", isUser ? "ring-1 ring-primary-foreground/10" : "bg-muted ring-1 ring-border"),
3423
- children: [/* @__PURE__ */ jsx("img", {
3424
- src: file.dataUrl,
3425
- alt: file.label,
3426
- className: "block h-auto max-h-[26rem] w-full rounded-[1rem] bg-transparent object-contain transition duration-300 group-hover:scale-[1.01]"
3427
- }), /* @__PURE__ */ jsxs("div", {
3428
- className: "pointer-events-none absolute right-3 top-3 flex items-center gap-2",
3429
- children: [/* @__PURE__ */ jsx("span", {
3430
- className: cn("inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold tracking-[0.18em] text-white backdrop-blur-sm", isUser ? "bg-black/36" : "bg-black/58"),
3431
- children: categoryLabel
3432
- }), sizeLabel ? /* @__PURE__ */ jsx("span", {
3433
- className: cn("inline-flex items-center rounded-full px-2 py-1 text-[10px] font-medium text-white/92 backdrop-blur-sm", isUser ? "bg-black/28" : "bg-black/46"),
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",
3434
3546
  children: sizeLabel
3435
- }) : null]
3436
- })]
3437
- })
3438
- });
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] });
3439
3556
  }
3440
3557
  function renderFileCardHeader(params) {
3441
3558
  const { category, file, categoryLabel, sizeLabel, isUser, action } = params;
3442
3559
  return /* @__PURE__ */ jsxs("div", {
3443
- className: "flex items-center gap-3 p-3.5",
3560
+ className: "flex items-center gap-2.5 px-2.5 py-2",
3444
3561
  children: [/* @__PURE__ */ jsx(FileCategoryGlyph, {
3445
3562
  category,
3446
3563
  isUser
3447
3564
  }), /* @__PURE__ */ jsx("div", {
3448
3565
  className: "min-w-0 flex-1",
3449
3566
  children: /* @__PURE__ */ jsxs("div", {
3450
- className: "flex items-center gap-3",
3567
+ className: "flex items-center gap-2",
3451
3568
  children: [/* @__PURE__ */ jsxs("div", {
3452
3569
  className: "min-w-0 flex-1",
3453
3570
  children: [/* @__PURE__ */ jsx("div", {
3454
- className: "truncate text-[15px] font-semibold leading-5",
3571
+ className: "truncate text-[13px] font-medium leading-5",
3455
3572
  children: file.label
3456
3573
  }), renderMetaLine(categoryLabel, sizeLabel, isUser)]
3457
3574
  }), action]
@@ -3460,7 +3577,7 @@ function renderFileCardHeader(params) {
3460
3577
  });
3461
3578
  }
3462
3579
  function renderInlineMediaCard(params) {
3463
- const { category, file, categoryLabel, sizeLabel, isUser, shellClasses, actionLabel } = params;
3580
+ const { category, file, categoryLabel, sizeLabel, isUser, shellClasses, actionLabel, onOpen } = params;
3464
3581
  if (!file.dataUrl || category !== "audio" && category !== "video") return null;
3465
3582
  const mediaMimeType = resolveRenderableMimeType(file);
3466
3583
  return /* @__PURE__ */ jsxs("div", {
@@ -3471,9 +3588,14 @@ function renderInlineMediaCard(params) {
3471
3588
  categoryLabel,
3472
3589
  sizeLabel,
3473
3590
  isUser,
3474
- action: renderActionLink(actionLabel, file.dataUrl, isUser)
3591
+ action: renderActionControl({
3592
+ label: actionLabel,
3593
+ href: file.dataUrl,
3594
+ isUser,
3595
+ onOpen: onOpen ? () => onOpen(file) : void 0
3596
+ })
3475
3597
  }), /* @__PURE__ */ jsx("div", {
3476
- className: "px-3.5 pb-3.5",
3598
+ className: "px-2.5 pb-2.5",
3477
3599
  children: category === "audio" ? /* @__PURE__ */ jsx("audio", {
3478
3600
  controls: true,
3479
3601
  preload: "metadata",
@@ -3484,7 +3606,7 @@ function renderInlineMediaCard(params) {
3484
3606
  ...mediaMimeType ? { type: mediaMimeType } : {}
3485
3607
  })
3486
3608
  }) : /* @__PURE__ */ jsx("div", {
3487
- className: cn("overflow-hidden rounded-[1rem] border", isUser ? "border-primary-foreground/10 bg-black/26" : "border-border bg-muted"),
3609
+ className: cn("overflow-hidden rounded-lg", isUser ? "bg-black/10" : "bg-muted ring-1 ring-border/70"),
3488
3610
  children: /* @__PURE__ */ jsx("video", {
3489
3611
  controls: true,
3490
3612
  preload: "metadata",
@@ -3500,20 +3622,22 @@ function renderInlineMediaCard(params) {
3500
3622
  })]
3501
3623
  });
3502
3624
  }
3503
- function ChatMessageFile({ file, isUser = false, texts }) {
3625
+ function ChatMessageFile({ file, isUser = false, texts, onOpen }) {
3504
3626
  const { category, sizeLabel } = buildChatMessageFileMeta(file);
3505
3627
  const renderAsImage = isImageFileLike(file) && Boolean(file.dataUrl);
3506
3628
  const renderAsAudio = category === "audio" && Boolean(file.dataUrl);
3507
3629
  const renderAsVideo = category === "video" && Boolean(file.dataUrl);
3508
3630
  const isInteractive = Boolean(file.dataUrl);
3509
3631
  const actionLabel = isInteractive ? texts?.attachmentOpenLabel ?? "Open" : texts?.attachmentAttachedLabel ?? "Attached";
3632
+ const expandLabel = texts?.attachmentExpandLabel ?? "Expand image";
3633
+ const closeLabel = texts?.attachmentCloseLabel ?? "Close preview";
3510
3634
  const categoryLabel = readFileCategoryLabel(category, texts);
3511
- const shellClasses = cn("block overflow-hidden rounded-[1.25rem] border transition duration-200", isUser ? "border-primary-foreground/12 bg-primary-foreground/10 text-primary-foreground" : "border-border bg-card text-card-foreground", isInteractive && (isUser ? "hover:border-primary-foreground/18 hover:bg-primary-foreground/13" : "hover:border-border-hover hover:bg-accent"));
3512
- if (renderAsImage) return renderImagePreview({
3635
+ 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, {
3637
+ expandLabel,
3638
+ closeLabel,
3513
3639
  file,
3514
- categoryLabel,
3515
- sizeLabel,
3516
- isUser
3640
+ sizeLabel
3517
3641
  });
3518
3642
  if (renderAsAudio || renderAsVideo) return renderInlineMediaCard({
3519
3643
  category,
@@ -3522,7 +3646,8 @@ function ChatMessageFile({ file, isUser = false, texts }) {
3522
3646
  sizeLabel,
3523
3647
  isUser,
3524
3648
  shellClasses,
3525
- actionLabel
3649
+ actionLabel,
3650
+ onOpen
3526
3651
  });
3527
3652
  const content = renderFileCardHeader({
3528
3653
  category,
@@ -3536,6 +3661,12 @@ function ChatMessageFile({ file, isUser = false, texts }) {
3536
3661
  className: shellClasses,
3537
3662
  children: content
3538
3663
  });
3664
+ if (onOpen) return /* @__PURE__ */ jsx("button", {
3665
+ type: "button",
3666
+ className: cn(shellClasses, "group w-full text-left"),
3667
+ onClick: () => onOpen(file),
3668
+ children: content
3669
+ });
3539
3670
  return /* @__PURE__ */ jsx("a", {
3540
3671
  href: file.dataUrl,
3541
3672
  target: "_blank",
@@ -3666,16 +3797,64 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3666
3797
  };
3667
3798
  }
3668
3799
  //#endregion
3800
+ //#region src/components/chat/ui/chat-message-list/chat-process-meta-row.tsx
3801
+ /**
3802
+ * Shared process-row metrics.
3803
+ * Collapsed process/tool rows should feel like ordinary body text lines.
3804
+ */
3805
+ 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";
3807
+ function ChatProcessLeadingIcon({ children, className }) {
3808
+ return /* @__PURE__ */ jsx("span", {
3809
+ className: cn(CHAT_PROCESS_LEADING_COL_CLASS, className),
3810
+ children
3811
+ });
3812
+ }
3813
+ function ChatProcessMetaRow({ children, className, interactive = false, onClick, role, tabIndex, onKeyDown }) {
3814
+ return /* @__PURE__ */ jsx("div", {
3815
+ 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
+ onClick,
3817
+ role,
3818
+ tabIndex,
3819
+ onKeyDown,
3820
+ children
3821
+ });
3822
+ }
3823
+ //#endregion
3824
+ //#region src/components/chat/ui/chat-message-list/chat-collapsible-meta-summary.tsx
3825
+ /**
3826
+ * Shared collapsible meta-row for process / tool-group / reasoning summaries.
3827
+ * Rendered as a plain button-like row (not <summary>) so browsers never inject
3828
+ * a default disclosure label such as "详情" / "Details".
3829
+ */
3830
+ function ChatCollapsibleMetaSummary({ label, openGroup, open = false, className, labelClassName, onClick }) {
3831
+ return /* @__PURE__ */ jsxs("button", {
3832
+ type: "button",
3833
+ 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
+ "aria-expanded": open,
3835
+ 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
3844
+ })
3845
+ })]
3846
+ });
3847
+ }
3848
+ //#endregion
3669
3849
  //#region src/components/chat/ui/chat-message-list/chat-reasoning-block.tsx
3670
- function normalizeReasoningLabel(label) {
3671
- const normalizedLabel = label.trim().toLowerCase();
3672
- if (normalizedLabel === "reasoning" || normalizedLabel === "thinking" || normalizedLabel === "推理" || normalizedLabel === "推理过程" || normalizedLabel === "思考过程" || normalizedLabel === "思考") return "思考";
3673
- return label;
3850
+ function formatReasoningLabel(label, text, characterCountTemplate) {
3851
+ const characterCount = Array.from(text).length;
3852
+ return (characterCountTemplate ?? `${label} · {count}`).split("{count}").join(String(characterCount));
3674
3853
  }
3675
- function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
3854
+ function ChatReasoningBlock({ label, text, characterCountTemplates, isUser, isInProgress }) {
3676
3855
  const { isOpen, onSummaryClick } = useReasoningBlockOpenState({ isInProgress });
3677
3856
  const scrollRef = useRef(null);
3678
- const displayLabel = normalizeReasoningLabel(label);
3857
+ const displayLabel = formatReasoningLabel(label, text, isInProgress ? characterCountTemplates?.inProgress : characterCountTemplates?.completed);
3679
3858
  const { onScroll } = useStickyBottomScroll({
3680
3859
  scrollRef,
3681
3860
  resetKey: `${displayLabel}:${isInProgress ? "streaming" : "idle"}`,
@@ -3684,36 +3863,42 @@ function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
3684
3863
  contentVersion: text,
3685
3864
  stickyThresholdPx: 20
3686
3865
  });
3687
- return /* @__PURE__ */ jsxs("details", {
3688
- className: "mt-2",
3689
- open: isOpen,
3690
- children: [/* @__PURE__ */ jsx("summary", {
3691
- className: cn("cursor-pointer text-xs", isUser ? "text-primary-100" : "text-muted-foreground"),
3692
- onClick: onSummaryClick,
3693
- children: displayLabel
3694
- }), /* @__PURE__ */ jsx("div", {
3866
+ return /* @__PURE__ */ jsxs("div", {
3867
+ className: "group/reasoning",
3868
+ children: [/* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
3869
+ openGroup: "reasoning",
3870
+ open: isOpen,
3871
+ label: displayLabel,
3872
+ labelClassName: isUser ? "text-primary-100" : void 0,
3873
+ onClick: onSummaryClick
3874
+ }), isOpen ? /* @__PURE__ */ jsx("div", {
3695
3875
  ref: scrollRef,
3696
3876
  onScroll,
3697
3877
  "data-reasoning-scroll": "true",
3698
- className: cn("mt-2 w-fit max-w-[500px] max-h-56 overflow-y-auto rounded-lg custom-scrollbar", isUser ? "bg-primary/70" : "bg-muted"),
3878
+ className: cn("mt-1 w-fit max-w-[500px] max-h-56 overflow-y-auto rounded-lg custom-scrollbar", isUser ? "bg-primary/70" : "bg-muted"),
3699
3879
  children: /* @__PURE__ */ jsx("pre", {
3700
3880
  className: "min-w-0 whitespace-pre-wrap break-all p-2 text-[11px]",
3701
3881
  children: text
3702
3882
  })
3703
- })]
3883
+ }) : null]
3704
3884
  });
3705
3885
  }
3706
3886
  //#endregion
3707
3887
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-root.tsx
3888
+ /**
3889
+ * Default tool presentation is an inline process row, not a heavy card.
3890
+ * Expanded details sit under the overview text column with clear clearance
3891
+ * from any workflow rail in the icon column.
3892
+ */
3708
3893
  function ToolCardRoot({ children, className }) {
3709
3894
  return /* @__PURE__ */ jsx("div", {
3710
- className: cn("my-2 rounded-lg border border-border bg-card shadow-sm overflow-hidden text-[12px] text-card-foreground", "w-[280px] sm:w-[360px] md:w-[480px] min-w-full max-w-full transition-all flex flex-col", className),
3895
+ className: cn("relative my-0 flex w-full min-w-0 max-w-full flex-col text-[0.925rem] leading-[1.72] text-muted-foreground", className),
3711
3896
  children
3712
3897
  });
3713
3898
  }
3714
3899
  function ToolCardContent({ children, className }) {
3715
3900
  return /* @__PURE__ */ jsx("div", {
3716
- className: cn("border-t border-border bg-muted/35 px-3 pt-1 pb-2 w-full overflow-hidden", className),
3901
+ className: cn("mt-0.5 w-full max-w-full min-w-0 overflow-hidden pl-[calc(1.15em+0.375rem)] text-[0.925rem] leading-[1.72] text-muted-foreground", className),
3717
3902
  children
3718
3903
  });
3719
3904
  }
@@ -3721,12 +3906,12 @@ function ToolCardContent({ children, className }) {
3721
3906
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-status.tsx
3722
3907
  const STATUS_STYLES = {
3723
3908
  running: {
3724
- text: "text-primary/70",
3909
+ text: "text-muted-foreground",
3725
3910
  icon: Loader2,
3726
3911
  spin: true
3727
3912
  },
3728
3913
  success: {
3729
- text: "text-primary/70",
3914
+ text: "text-muted-foreground/75",
3730
3915
  icon: Check,
3731
3916
  spin: false
3732
3917
  },
@@ -3736,20 +3921,23 @@ const STATUS_STYLES = {
3736
3921
  spin: false
3737
3922
  },
3738
3923
  cancelled: {
3739
- text: "text-muted-foreground",
3924
+ text: "text-muted-foreground/70",
3740
3925
  icon: Minus,
3741
3926
  spin: false
3742
3927
  }
3743
3928
  };
3744
- function ToolStatusLabel({ card }) {
3929
+ function ToolStatusLabel({ card, iconOnly = false }) {
3745
3930
  const style = STATUS_STYLES[card.statusTone] || STATUS_STYLES.cancelled;
3746
3931
  const Icon = style.icon;
3932
+ const showLabel = !iconOnly && (card.statusTone === "running" || card.statusTone === "error");
3747
3933
  return /* @__PURE__ */ jsxs("span", {
3748
- className: cn("inline-flex items-center gap-1.5 text-[11px] font-medium leading-none shrink-0", style.text),
3934
+ className: cn("inline-flex shrink-0 items-center gap-1 text-[0.925rem] font-normal leading-[1.72]", style.text),
3935
+ "aria-label": iconOnly ? card.statusLabel : void 0,
3749
3936
  children: [/* @__PURE__ */ jsx(Icon, {
3750
- className: cn("h-3.5 w-3.5", style.spin && "animate-spin"),
3751
- strokeWidth: 3
3752
- }), card.statusTone === "running" ? card.statusLabel : null]
3937
+ "aria-hidden": "true",
3938
+ className: cn("h-[1.05em] w-[1.05em]", style.spin && "animate-spin"),
3939
+ strokeWidth: 2.25
3940
+ }), showLabel ? card.statusLabel : null]
3753
3941
  });
3754
3942
  }
3755
3943
  //#endregion
@@ -3758,12 +3946,12 @@ function resolveToolCardActionView(action) {
3758
3946
  if (action.kind === "show-content") return {
3759
3947
  icon: Eye,
3760
3948
  label: action.label,
3761
- toneClassName: "border-border bg-card text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-primary/35"
3949
+ toneClassName: "border-border/70 bg-transparent text-muted-foreground hover:bg-muted/60 hover:text-foreground focus-visible:ring-primary/35"
3762
3950
  };
3763
3951
  return {
3764
3952
  icon: ArrowUpRight,
3765
3953
  label: action.label ?? (action.sessionKind === "child" ? "Open child session" : "Open session"),
3766
- toneClassName: "border-border bg-card text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-primary/35"
3954
+ toneClassName: "border-border/70 bg-transparent text-muted-foreground hover:bg-muted/60 hover:text-foreground focus-visible:ring-primary/35"
3767
3955
  };
3768
3956
  }
3769
3957
  function ToolCardActionButton({ action, onAction }) {
@@ -3781,12 +3969,12 @@ function ToolCardActionButton({ action, onAction }) {
3781
3969
  children: /* @__PURE__ */ jsx("button", {
3782
3970
  type: "button",
3783
3971
  onClick: handleClick,
3784
- className: cn("inline-flex h-7 w-7 items-center justify-center rounded-full border transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1", view.toneClassName),
3972
+ className: cn("inline-flex h-6 w-6 items-center justify-center rounded-md border transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1", view.toneClassName),
3785
3973
  "aria-label": view.label,
3786
3974
  title: view.label,
3787
3975
  children: /* @__PURE__ */ jsx(Icon, {
3788
3976
  className: "h-3.5 w-3.5",
3789
- strokeWidth: 2.5
3977
+ strokeWidth: 2.25
3790
3978
  })
3791
3979
  })
3792
3980
  }), /* @__PURE__ */ jsx(TooltipContent, {
@@ -3796,44 +3984,82 @@ function ToolCardActionButton({ action, onAction }) {
3796
3984
  })] })
3797
3985
  });
3798
3986
  }
3799
- function ToolCardHeader({ card, icon: Icon, expanded, canExpand, hideSummary = false, actionSlot, onToggle }) {
3987
+ function humanizeToolName(toolName) {
3988
+ return toolName.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
3989
+ }
3990
+ function handleToolHeaderKeyDown(event, onToggle) {
3991
+ if (event.key !== "Enter" && event.key !== " ") return;
3992
+ event.preventDefault();
3993
+ onToggle();
3994
+ }
3995
+ function ToolHeaderSummary({ label, summary }) {
3996
+ return /* @__PURE__ */ jsxs("span", {
3997
+ className: "min-w-0 shrink truncate",
3998
+ children: [label, summary ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
3999
+ className: "mx-1.5 select-none text-muted-foreground/45",
4000
+ children: "·"
4001
+ }), /* @__PURE__ */ jsx("span", {
4002
+ title: summary,
4003
+ children: summary
4004
+ })] }) : null]
4005
+ });
4006
+ }
4007
+ function ToolHeaderChevron({ expanded }) {
4008
+ return /* @__PURE__ */ jsx("span", {
4009
+ className: cn("inline-flex h-[1.15em] w-[1.15em] shrink-0 items-center justify-center text-muted-foreground/80 transition-opacity", expanded ? "opacity-100" : "opacity-0 group-hover/process-row:opacity-100 group-focus-within/process-row:opacity-100"),
4010
+ children: /* @__PURE__ */ jsx(ChevronRight, {
4011
+ className: cn("h-[1.05em] w-[1.05em] transition-transform", expanded && "rotate-90"),
4012
+ strokeWidth: 2.25
4013
+ })
4014
+ });
4015
+ }
4016
+ function ToolHeaderChangeSummary({ additions, deletions }) {
4017
+ if (additions === 0 && deletions === 0) return null;
4018
+ const accessibleLabel = [additions > 0 ? `+${additions}` : null, deletions > 0 ? `-${deletions}` : null].filter(Boolean).join(" ");
4019
+ return /* @__PURE__ */ jsxs("span", {
4020
+ className: "inline-flex shrink-0 items-center gap-1.5 tabular-nums text-muted-foreground/75",
4021
+ "aria-label": accessibleLabel,
4022
+ children: [additions > 0 ? /* @__PURE__ */ jsxs("span", {
4023
+ className: "transition-colors group-hover/process-row:text-emerald-600",
4024
+ children: ["+", additions]
4025
+ }) : null, deletions > 0 ? /* @__PURE__ */ jsxs("span", {
4026
+ className: "transition-colors group-hover/process-row:text-rose-600",
4027
+ children: ["-", deletions]
4028
+ }) : null]
4029
+ });
4030
+ }
4031
+ function ToolCardHeader({ card, toolLabel, changeSummary, icon: Icon, expanded, canExpand, hideSummary = false, actionSlot, onToggle }) {
3800
4032
  const summaryPart = hideSummary ? "" : card.summary?.replace(/^(command|path|args|query|input):\s*/i, "") ?? "";
3801
- return /* @__PURE__ */ jsxs("div", {
3802
- className: cn("flex items-center gap-3 px-3 py-2.5 transition-colors bg-transparent", canExpand ? "cursor-pointer hover:bg-accent/70" : ""),
3803
- onClick: onToggle,
3804
- children: [/* @__PURE__ */ jsxs("div", {
3805
- className: "flex min-w-0 flex-1 items-center gap-2 overflow-hidden font-mono text-foreground",
3806
- children: [/* @__PURE__ */ jsx(Icon, {
3807
- className: "h-4 w-4 text-primary/70 shrink-0",
3808
- strokeWidth: 3
3809
- }), /* @__PURE__ */ jsxs("div", {
3810
- className: "flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden",
3811
- children: [/* @__PURE__ */ jsx("span", {
3812
- className: "font-bold shrink-0 tracking-tight",
3813
- children: card.toolName
3814
- }), summaryPart && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
3815
- className: "text-muted-foreground/45 font-bold select-none shrink-0",
3816
- children: "›"
3817
- }), /* @__PURE__ */ jsx("span", {
3818
- className: "block min-w-0 flex-1 truncate font-normal text-muted-foreground",
3819
- title: summaryPart,
3820
- children: summaryPart
3821
- })] })]
3822
- })]
3823
- }), /* @__PURE__ */ jsxs("div", {
3824
- className: "flex items-center gap-3 shrink-0",
3825
- children: [
3826
- actionSlot,
3827
- /* @__PURE__ */ jsx(ToolStatusLabel, { card }),
3828
- canExpand && (expanded ? /* @__PURE__ */ jsx(ChevronDown, {
3829
- className: "h-4 w-4 text-muted-foreground",
3830
- strokeWidth: 3
3831
- }) : /* @__PURE__ */ jsx(ChevronRight, {
3832
- className: "h-4 w-4 text-muted-foreground",
3833
- strokeWidth: 3
3834
- }))
3835
- ]
3836
- })]
4033
+ const displayLabel = toolLabel ?? humanizeToolName(card.toolName);
4034
+ return /* @__PURE__ */ jsxs(ChatProcessMetaRow, {
4035
+ interactive: canExpand,
4036
+ onClick: canExpand ? () => onToggle() : void 0,
4037
+ role: canExpand ? "button" : void 0,
4038
+ tabIndex: canExpand ? 0 : void 0,
4039
+ onKeyDown: canExpand ? (event) => handleToolHeaderKeyDown(event, onToggle) : void 0,
4040
+ children: [
4041
+ /* @__PURE__ */ jsx(ChatProcessLeadingIcon, {
4042
+ className: "relative z-[1] rounded-sm bg-card",
4043
+ children: /* @__PURE__ */ jsx(Icon, {
4044
+ className: "h-[1.05em] w-[1.05em]",
4045
+ strokeWidth: 2.25
4046
+ })
4047
+ }),
4048
+ /* @__PURE__ */ jsx(ToolHeaderSummary, {
4049
+ label: displayLabel,
4050
+ summary: summaryPart
4051
+ }),
4052
+ changeSummary ? /* @__PURE__ */ jsx(ToolHeaderChangeSummary, { ...changeSummary }) : null,
4053
+ /* @__PURE__ */ jsx(ToolStatusLabel, {
4054
+ card,
4055
+ iconOnly: Boolean(toolLabel)
4056
+ }),
4057
+ canExpand ? /* @__PURE__ */ jsx(ToolHeaderChevron, { expanded }) : null,
4058
+ actionSlot ? /* @__PURE__ */ jsx("span", {
4059
+ className: "inline-flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover/process-row:opacity-100 group-focus-within/process-row:opacity-100",
4060
+ children: actionSlot
4061
+ }) : null
4062
+ ]
3837
4063
  });
3838
4064
  }
3839
4065
  function ToolCardHeaderAction({ action, onAction }) {
@@ -4076,7 +4302,7 @@ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
4076
4302
  className: cn("overflow-hidden bg-card", !isFirst && "border-t border-border"),
4077
4303
  children: [
4078
4304
  showMetaRow ? /* @__PURE__ */ jsxs("div", {
4079
- className: cn("flex items-center justify-between gap-4 border-b border-border px-4 text-muted-foreground", showPathRow ? "py-2" : "py-1.5"),
4305
+ className: cn("flex min-h-9 items-center justify-between gap-4 border-b border-border px-3 text-muted-foreground", showPathRow ? "py-2" : "py-1.5"),
4080
4306
  children: [/* @__PURE__ */ jsx("div", {
4081
4307
  className: "min-w-0 flex-1",
4082
4308
  children: showPathRow ? /* @__PURE__ */ jsx(TooltipProvider, {
@@ -4086,7 +4312,7 @@ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
4086
4312
  children: /* @__PURE__ */ jsx("button", {
4087
4313
  type: "button",
4088
4314
  onClick: handlePathClick,
4089
- className: cn("w-full truncate whitespace-nowrap text-left font-mono text-[12px] font-medium text-foreground", onFileOpen && "transition-colors hover:text-primary hover:underline"),
4315
+ className: cn("w-full truncate whitespace-nowrap text-left font-mono text-[12px] font-medium leading-5 text-foreground", onFileOpen && "transition-colors hover:text-primary hover:underline"),
4090
4316
  title: block.path,
4091
4317
  children: block.path
4092
4318
  })
@@ -4112,28 +4338,28 @@ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
4112
4338
  children: /* @__PURE__ */ jsx("div", {
4113
4339
  className: "overflow-x-auto custom-scrollbar",
4114
4340
  children: /* @__PURE__ */ jsx("pre", {
4115
- className: "min-w-max whitespace-pre bg-card px-4 py-2 font-mono text-[11px] leading-5 text-foreground",
4341
+ className: "min-w-max whitespace-pre bg-card px-3 py-2 font-mono text-[11px] leading-5 text-foreground",
4116
4342
  children: block.rawText
4117
4343
  })
4118
4344
  })
4119
4345
  }) : null,
4120
4346
  block.truncated && !previewBlock ? /* @__PURE__ */ jsx("div", {
4121
- className: "border-t border-border bg-muted px-4 py-2 text-[10px] text-muted-foreground",
4347
+ className: "border-t border-border bg-muted px-3 py-2 text-[10px] text-muted-foreground",
4122
4348
  children: "Showing a shortened diff preview."
4123
4349
  }) : null
4124
4350
  ]
4125
4351
  });
4126
4352
  }
4127
- function ToolCardFileOperationContent({ card, className, onFileOpen }) {
4353
+ function ToolCardFileOperationContent({ card, className, onFileOpen, showPathRow = true }) {
4128
4354
  const blocks = card.fileOperation?.blocks ?? [];
4129
4355
  const output = card.output?.trim() ?? "";
4130
4356
  if (blocks.length === 0 && !output) return null;
4131
4357
  return /* @__PURE__ */ jsxs("div", {
4132
- className: cn("overflow-hidden bg-card", className),
4358
+ className: cn("overflow-hidden rounded-md border border-border/70 bg-card", className),
4133
4359
  children: [blocks.map((block, index) => {
4134
4360
  return /* @__PURE__ */ jsx(FileOperationBlock, {
4135
4361
  block,
4136
- showPathRow: true,
4362
+ showPathRow,
4137
4363
  isFirst: index === 0,
4138
4364
  onFileOpen
4139
4365
  }, block.key);
@@ -4145,7 +4371,7 @@ function ToolCardFileOperationContent({ card, className, onFileOpen }) {
4145
4371
  children: /* @__PURE__ */ jsx("div", {
4146
4372
  className: "overflow-x-auto custom-scrollbar",
4147
4373
  children: /* @__PURE__ */ jsx("pre", {
4148
- className: "min-w-max whitespace-pre bg-card px-4 py-2 font-mono text-[11px] leading-5 text-foreground",
4374
+ className: "min-w-max whitespace-pre bg-card px-3 py-2 font-mono text-[11px] leading-5 text-foreground",
4149
4375
  children: output
4150
4376
  })
4151
4377
  })
@@ -4153,9 +4379,279 @@ function ToolCardFileOperationContent({ card, className, onFileOpen }) {
4153
4379
  });
4154
4380
  }
4155
4381
  //#endregion
4382
+ //#region src/components/chat/ui/chat-message-list/tool-card/terminal/terminal-ansi.utils.ts
4383
+ const ANSI_ESCAPE_PREFIX = String.fromCharCode(27);
4384
+ const ANSI_PATTERN = new RegExp(`${ANSI_ESCAPE_PREFIX}\\[(.*?)([ -/]*[@-~])`, "g");
4385
+ function hasAnsiSequences(value) {
4386
+ return value.includes(`${ANSI_ESCAPE_PREFIX}[`);
4387
+ }
4388
+ function stripAnsiSequences(value) {
4389
+ return value.replace(new RegExp(`${ANSI_ESCAPE_PREFIX}\\[[0-?]*[ -/]*[@-~]`, "g"), "");
4390
+ }
4391
+ function escapeHtml(value) {
4392
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4393
+ }
4394
+ const DEFAULT_STYLE = {
4395
+ bold: false,
4396
+ dim: false,
4397
+ italic: false,
4398
+ underline: false,
4399
+ inverse: false
4400
+ };
4401
+ const SGR_STYLE_PATCHES = {
4402
+ 1: { bold: true },
4403
+ 2: { dim: true },
4404
+ 3: { italic: true },
4405
+ 4: { underline: true },
4406
+ 7: { inverse: true },
4407
+ 22: {
4408
+ bold: false,
4409
+ dim: false
4410
+ },
4411
+ 23: { italic: false },
4412
+ 24: { underline: false },
4413
+ 27: { inverse: false },
4414
+ 39: { fg: void 0 },
4415
+ 49: { bg: void 0 }
4416
+ };
4417
+ const BASIC_FG = {
4418
+ 30: "black",
4419
+ 31: "red",
4420
+ 32: "green",
4421
+ 33: "yellow",
4422
+ 34: "blue",
4423
+ 35: "magenta",
4424
+ 36: "cyan",
4425
+ 37: "white",
4426
+ 90: "bright-black",
4427
+ 91: "bright-red",
4428
+ 92: "bright-green",
4429
+ 93: "bright-yellow",
4430
+ 94: "bright-blue",
4431
+ 95: "bright-magenta",
4432
+ 96: "bright-cyan",
4433
+ 97: "bright-white"
4434
+ };
4435
+ const BASIC_BG = {
4436
+ 40: "black",
4437
+ 41: "red",
4438
+ 42: "green",
4439
+ 43: "yellow",
4440
+ 44: "blue",
4441
+ 45: "magenta",
4442
+ 46: "cyan",
4443
+ 47: "white",
4444
+ 100: "bright-black",
4445
+ 101: "bright-red",
4446
+ 102: "bright-green",
4447
+ 103: "bright-yellow",
4448
+ 104: "bright-blue",
4449
+ 105: "bright-magenta",
4450
+ 106: "bright-cyan",
4451
+ 107: "bright-white"
4452
+ };
4453
+ function resolveExtendedColor(params, index) {
4454
+ const mode = params[index + 1];
4455
+ if (mode === 5 && typeof params[index + 2] === "number") return {
4456
+ color: `ansi-256-${params[index + 2]}`,
4457
+ consumed: 2
4458
+ };
4459
+ if (mode === 2 && typeof params[index + 2] === "number" && typeof params[index + 3] === "number" && typeof params[index + 4] === "number") return {
4460
+ color: `rgb(${params[index + 2]},${params[index + 3]},${params[index + 4]})`,
4461
+ consumed: 4
4462
+ };
4463
+ return null;
4464
+ }
4465
+ function applySgr(style, params) {
4466
+ let next = { ...style };
4467
+ for (let index = 0; index < params.length; index += 1) {
4468
+ const code = params[index];
4469
+ if (code === 0) return { ...DEFAULT_STYLE };
4470
+ const stylePatch = SGR_STYLE_PATCHES[code];
4471
+ if (stylePatch) {
4472
+ next = {
4473
+ ...next,
4474
+ ...stylePatch
4475
+ };
4476
+ continue;
4477
+ }
4478
+ if (code === 38 || code === 48) {
4479
+ const extendedColor = resolveExtendedColor(params, index);
4480
+ if (extendedColor) {
4481
+ next = code === 38 ? {
4482
+ ...next,
4483
+ fg: extendedColor.color
4484
+ } : {
4485
+ ...next,
4486
+ bg: extendedColor.color
4487
+ };
4488
+ index += extendedColor.consumed;
4489
+ }
4490
+ continue;
4491
+ }
4492
+ if (BASIC_FG[code]) {
4493
+ next = {
4494
+ ...next,
4495
+ fg: BASIC_FG[code]
4496
+ };
4497
+ continue;
4498
+ }
4499
+ if (BASIC_BG[code]) next = {
4500
+ ...next,
4501
+ bg: BASIC_BG[code]
4502
+ };
4503
+ }
4504
+ return next;
4505
+ }
4506
+ function styleToClassName(style) {
4507
+ const classes = ["ansi"];
4508
+ if (style.bold) classes.push("ansi-bold");
4509
+ if (style.dim) classes.push("ansi-dim");
4510
+ if (style.italic) classes.push("ansi-italic");
4511
+ if (style.underline) classes.push("ansi-underline");
4512
+ if (style.inverse) classes.push("ansi-inverse");
4513
+ if (style.fg) if (style.fg.startsWith("rgb(")) classes.push("ansi-fg-custom");
4514
+ else if (style.fg.startsWith("ansi-256-")) {
4515
+ classes.push("ansi-fg-256");
4516
+ classes.push(style.fg);
4517
+ } else classes.push(`ansi-fg-${style.fg}`);
4518
+ if (style.bg) if (style.bg.startsWith("rgb(")) classes.push("ansi-bg-custom");
4519
+ else if (style.bg.startsWith("ansi-256-")) {
4520
+ classes.push("ansi-bg-256");
4521
+ classes.push(style.bg);
4522
+ } else classes.push(`ansi-bg-${style.bg}`);
4523
+ return classes.join(" ");
4524
+ }
4525
+ function styleToInlineStyle(style) {
4526
+ const parts = [];
4527
+ if (style.fg?.startsWith("rgb(")) parts.push(`color:${style.fg}`);
4528
+ if (style.bg?.startsWith("rgb(")) parts.push(`background-color:${style.bg}`);
4529
+ return parts.length > 0 ? parts.join(";") : void 0;
4530
+ }
4531
+ function openSpan(style) {
4532
+ const className = styleToClassName(style);
4533
+ const inline = styleToInlineStyle(style);
4534
+ return inline ? `<span class="${className}" style="${inline}">` : `<span class="${className}">`;
4535
+ }
4536
+ /**
4537
+ * Lightweight ANSI SGR -> HTML converter for terminal output.
4538
+ * Handles common color/style codes without adding a dependency.
4539
+ */
4540
+ function ansiToHtml(value) {
4541
+ if (!value) return "";
4542
+ if (!hasAnsiSequences(value)) return escapeHtml(value);
4543
+ let html = "";
4544
+ let lastIndex = 0;
4545
+ let style = { ...DEFAULT_STYLE };
4546
+ let spanOpen = false;
4547
+ const flushText = (text) => {
4548
+ if (!text) return;
4549
+ if (!spanOpen) {
4550
+ html += openSpan(style);
4551
+ spanOpen = true;
4552
+ }
4553
+ html += escapeHtml(text);
4554
+ };
4555
+ const closeSpan = () => {
4556
+ if (spanOpen) {
4557
+ html += "</span>";
4558
+ spanOpen = false;
4559
+ }
4560
+ };
4561
+ value.replace(ANSI_PATTERN, (match, body, terminator, offset) => {
4562
+ flushText(value.slice(lastIndex, offset));
4563
+ lastIndex = offset + match.length;
4564
+ if (terminator !== "m") return match;
4565
+ const params = body.trim().length === 0 ? [0] : body.split(";").map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
4566
+ closeSpan();
4567
+ style = applySgr(style, params.length > 0 ? params : [0]);
4568
+ return match;
4569
+ });
4570
+ flushText(value.slice(lastIndex));
4571
+ closeSpan();
4572
+ return html;
4573
+ }
4574
+ //#endregion
4575
+ //#region src/components/chat/ui/chat-message-list/tool-card/terminal/terminal-panes.tsx
4576
+ const MAX_TERMINAL_TEXT_CHARS = 12e4;
4577
+ function clampTerminalText(value) {
4578
+ if (value.length <= MAX_TERMINAL_TEXT_CHARS) return value;
4579
+ return `${value.slice(0, MAX_TERMINAL_TEXT_CHARS)}\n…`;
4580
+ }
4581
+ function TerminalCode({ code, language, preferAnsi = false, className }) {
4582
+ const source = clampTerminalText(code);
4583
+ const content = useMemo(() => {
4584
+ if (preferAnsi && hasAnsiSequences(source)) return {
4585
+ html: ansiToHtml(source),
4586
+ language: "ansi",
4587
+ highlighted: true
4588
+ };
4589
+ const plain = stripAnsiSequences(source);
4590
+ return chatCodeSyntaxHighlighter.highlight(plain, language);
4591
+ }, [
4592
+ language,
4593
+ preferAnsi,
4594
+ source
4595
+ ]);
4596
+ return /* @__PURE__ */ jsx("code", {
4597
+ className: cn("hljs chat-terminal-code", `language-${content.language}`, className),
4598
+ "data-highlighted": content.highlighted ? "true" : "false",
4599
+ dangerouslySetInnerHTML: { __html: content.html }
4600
+ });
4601
+ }
4602
+ /**
4603
+ * Compact terminal-style surface for command tools.
4604
+ * Style only — no decorative window chrome.
4605
+ */
4606
+ function ChatTerminalSurface({ command, output, emptyLabel, isRunning, hasOutput, isError = false }) {
4607
+ return /* @__PURE__ */ jsx("div", {
4608
+ className: cn("chat-terminal-surface", isError ? "chat-terminal-surface-error" : void 0),
4609
+ "data-testid": "chat-terminal-surface",
4610
+ children: /* @__PURE__ */ jsxs("div", {
4611
+ className: "chat-terminal-body custom-scrollbar",
4612
+ children: [/* @__PURE__ */ jsxs("div", {
4613
+ className: "chat-terminal-line chat-terminal-line-command",
4614
+ children: [/* @__PURE__ */ jsx("span", {
4615
+ className: "chat-terminal-gutter",
4616
+ "aria-hidden": "true",
4617
+ children: "$"
4618
+ }), /* @__PURE__ */ jsx("div", {
4619
+ className: "chat-terminal-line-content",
4620
+ children: command ? /* @__PURE__ */ jsxs("pre", {
4621
+ className: "chat-terminal-pre",
4622
+ children: [/* @__PURE__ */ jsx(TerminalCode, {
4623
+ code: command,
4624
+ language: "bash"
4625
+ }), isRunning && !hasOutput ? /* @__PURE__ */ jsx("span", {
4626
+ className: "chat-terminal-caret",
4627
+ "aria-hidden": "true"
4628
+ }) : null]
4629
+ }) : /* @__PURE__ */ jsx("div", { className: "chat-terminal-skeleton" })
4630
+ })]
4631
+ }), (hasOutput || !isRunning) && /* @__PURE__ */ jsx("div", {
4632
+ className: "chat-terminal-output-block",
4633
+ children: hasOutput ? /* @__PURE__ */ jsxs("pre", {
4634
+ className: cn("chat-terminal-pre chat-terminal-output", isError ? "chat-terminal-pre-error" : void 0),
4635
+ children: [/* @__PURE__ */ jsx(TerminalCode, {
4636
+ code: output,
4637
+ language: isError ? "text" : "shell",
4638
+ preferAnsi: true,
4639
+ className: isError ? "chat-terminal-code-error" : void 0
4640
+ }), isRunning ? /* @__PURE__ */ jsx("span", {
4641
+ className: "chat-terminal-caret",
4642
+ "aria-hidden": "true"
4643
+ }) : null]
4644
+ }) : /* @__PURE__ */ jsx("div", {
4645
+ className: "chat-terminal-empty",
4646
+ children: emptyLabel
4647
+ })
4648
+ })]
4649
+ })
4650
+ });
4651
+ }
4652
+ //#endregion
4156
4653
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-views.tsx
4157
4654
  const TOOL_CARD_AUTO_EXPAND_DELAY_MS = 200;
4158
- const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
4159
4655
  function readNonEmptyString(value) {
4160
4656
  if (typeof value !== "string") return null;
4161
4657
  return value.length > 0 ? value : null;
@@ -4163,9 +4659,6 @@ function readNonEmptyString(value) {
4163
4659
  function isRecord(value) {
4164
4660
  return typeof value === "object" && value !== null && !Array.isArray(value);
4165
4661
  }
4166
- function stripAnsi(value) {
4167
- return value.replace(ANSI_ESCAPE_PATTERN, "");
4168
- }
4169
4662
  function isStructuredTerminalRecord(record) {
4170
4663
  return "command" in record || "workingDir" in record || "exitCode" in record || "durationMs" in record || "stdout" in record || "stderr" in record || "aggregated_output" in record || "combinedOutput" in record;
4171
4664
  }
@@ -4180,26 +4673,35 @@ function extractTerminalOutputFromRecord(record) {
4180
4673
  function normalizeTerminalOutput(rawOutput, structuredOutput) {
4181
4674
  if (isRecord(structuredOutput)) {
4182
4675
  const terminalOutput = extractTerminalOutputFromRecord(structuredOutput);
4183
- if (terminalOutput) return stripAnsi(terminalOutput);
4676
+ if (terminalOutput) return terminalOutput;
4184
4677
  if (isStructuredTerminalRecord(structuredOutput)) return "";
4185
4678
  }
4186
4679
  if (!rawOutput) return "";
4187
4680
  const trimmed = rawOutput.trim();
4188
- if (!trimmed.startsWith("{")) return stripAnsi(rawOutput);
4681
+ if (!trimmed.startsWith("{")) return rawOutput;
4189
4682
  try {
4190
4683
  const parsed = JSON.parse(trimmed);
4191
- if (!isRecord(parsed)) return stripAnsi(rawOutput);
4684
+ if (!isRecord(parsed)) return rawOutput;
4192
4685
  const terminalOutput = extractTerminalOutputFromRecord(parsed);
4193
- if (terminalOutput) return stripAnsi(terminalOutput);
4686
+ if (terminalOutput) return terminalOutput;
4194
4687
  if (isStructuredTerminalRecord(parsed)) return "";
4195
- return stripAnsi(rawOutput);
4688
+ return rawOutput;
4196
4689
  } catch {
4197
- return stripAnsi(rawOutput);
4690
+ return rawOutput;
4198
4691
  }
4199
4692
  }
4200
4693
  function shouldAutoExpandRunningFileOperation(toolName) {
4201
4694
  return toolName === "write_file" || toolName === "edit_file";
4202
4695
  }
4696
+ function countFileOperationChanges(blocks) {
4697
+ return blocks.reduce((totals, block) => ({
4698
+ additions: totals.additions + block.lines.filter((line) => line.kind === "add").length,
4699
+ deletions: totals.deletions + block.lines.filter((line) => line.kind === "remove").length
4700
+ }), {
4701
+ additions: 0,
4702
+ deletions: 0
4703
+ });
4704
+ }
4203
4705
  function useToolCardExpandedState({ canExpand, isRunning, autoExpandWhileRunning = true, expandOnError = false, statusTone }) {
4204
4706
  const [expanded, setExpanded] = useState(false);
4205
4707
  const [hasUserToggled, setHasUserToggled] = useState(false);
@@ -4258,47 +4760,20 @@ function useToolCardExpandedState({ canExpand, isRunning, autoExpandWhileRunning
4258
4760
  onToggle
4259
4761
  };
4260
4762
  }
4261
- function GenericToolSection({ label, tone, children }) {
4262
- const style = {
4263
- input: {
4264
- shell: "border-border bg-card",
4265
- header: "border-border bg-muted/55 text-muted-foreground",
4266
- dot: "bg-muted-foreground/60",
4267
- body: "text-foreground"
4268
- },
4269
- output: {
4270
- shell: "border-border bg-card",
4271
- header: "border-border bg-muted/55 text-muted-foreground",
4272
- dot: "bg-primary/70",
4273
- body: "text-foreground"
4274
- },
4275
- error: {
4276
- shell: "border-rose-200/80 bg-rose-50/85",
4277
- header: "border-rose-200/80 bg-rose-100/80 text-rose-700",
4278
- dot: "bg-rose-500/80",
4279
- body: "text-rose-950/85"
4280
- }
4281
- }[tone];
4282
- return /* @__PURE__ */ jsxs("section", {
4283
- className: cn("overflow-hidden rounded-md border shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]", style.shell),
4284
- children: [/* @__PURE__ */ jsxs("div", {
4285
- className: cn("flex items-center gap-2 border-b px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.16em]", style.header),
4286
- children: [/* @__PURE__ */ jsx("span", { className: cn("h-1.5 w-1.5 rounded-full", style.dot) }), /* @__PURE__ */ jsx("span", { children: label })]
4287
- }), /* @__PURE__ */ jsx("div", {
4288
- className: "w-full overflow-hidden",
4289
- children: /* @__PURE__ */ jsx("pre", {
4290
- className: cn("w-full max-w-full min-w-0 max-h-64 overflow-x-auto overflow-y-auto px-3 py-2.5 font-mono text-[12px] leading-relaxed whitespace-pre custom-scrollbar", style.body),
4291
- children
4292
- })
4293
- })]
4294
- });
4763
+ function extractTerminalMeta(structuredOutput) {
4764
+ if (!isRecord(structuredOutput)) return {};
4765
+ return {
4766
+ exitCode: typeof structuredOutput.exitCode === "number" ? structuredOutput.exitCode : typeof structuredOutput.exit_code === "number" ? structuredOutput.exit_code : null,
4767
+ workingDir: readNonEmptyString(structuredOutput.workingDir) ?? readNonEmptyString(structuredOutput.cwd) ?? readNonEmptyString(structuredOutput.working_directory)
4768
+ };
4295
4769
  }
4296
- function TerminalExecutionView({ card }) {
4770
+ function TerminalExecutionView({ card, toolLabel }) {
4297
4771
  const output = normalizeTerminalOutput(card.output, card.outputData);
4298
4772
  const isRunning = card.statusTone === "running";
4299
4773
  const commandPart = card.summary?.replace(/^(command|path|args|query|input):\s*/i, "");
4300
4774
  const hasOutput = output.trim().length > 0;
4301
4775
  const canExpand = isRunning || hasOutput || Boolean(commandPart?.trim());
4776
+ const meta = extractTerminalMeta(card.outputData);
4302
4777
  const { expanded, onToggle } = useToolCardExpandedState({
4303
4778
  canExpand,
4304
4779
  isRunning,
@@ -4308,38 +4783,29 @@ function TerminalExecutionView({ card }) {
4308
4783
  });
4309
4784
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4310
4785
  card,
4786
+ toolLabel,
4311
4787
  icon: Terminal,
4312
4788
  expanded,
4313
4789
  canExpand,
4314
4790
  onToggle
4315
- }), expanded && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
4316
- className: "px-3 pb-2 font-mono w-full max-h-48 overflow-y-auto custom-scrollbar min-h-0 text-[12px]",
4317
- children: /* @__PURE__ */ jsxs("div", {
4318
- className: "flex items-start gap-2 leading-relaxed",
4319
- children: [/* @__PURE__ */ jsx("span", {
4320
- className: "text-primary/55 font-medium shrink-0 select-none mt-[1px]",
4321
- children: "$"
4322
- }), /* @__PURE__ */ jsx("div", {
4323
- className: "flex-1 min-w-0",
4324
- children: commandPart ? /* @__PURE__ */ jsxs("div", {
4325
- className: "text-foreground break-words whitespace-pre-wrap tracking-tight font-medium inline-block",
4326
- children: [commandPart, isRunning && !output && /* @__PURE__ */ jsx("span", { className: "inline-block w-1.5 h-3 ml-1 bg-primary/60 animate-pulse align-middle" })]
4327
- }) : /* @__PURE__ */ jsx("div", { className: "h-3 w-32 bg-muted rounded animate-pulse mt-2" })
4328
- })]
4329
- })
4330
- }), (hasOutput || !isRunning) && /* @__PURE__ */ jsx(ToolCardContent, {
4331
- className: hasOutput ? void 0 : "bg-muted/20 py-2",
4332
- children: /* @__PURE__ */ jsxs("pre", {
4333
- className: cn("font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed", hasOutput ? "px-0" : "rounded-md border border-dashed border-border bg-card/70 px-3 py-2 italic"),
4334
- children: [hasOutput ? output : card.emptyLabel, isRunning && hasOutput && /* @__PURE__ */ jsx("span", { className: "inline-block w-1.5 h-3 ml-1 bg-primary/60 animate-pulse align-middle" })]
4791
+ }), expanded && /* @__PURE__ */ jsx(ToolCardContent, {
4792
+ className: "bg-transparent py-0",
4793
+ children: /* @__PURE__ */ jsx(ChatTerminalSurface, {
4794
+ command: commandPart,
4795
+ output,
4796
+ emptyLabel: card.emptyLabel,
4797
+ isRunning,
4798
+ hasOutput,
4799
+ isError: card.statusTone === "error",
4800
+ exitCode: meta.exitCode,
4801
+ workingDir: meta.workingDir
4335
4802
  })
4336
- })] })] });
4803
+ })] });
4337
4804
  }
4338
- function FileOperationView({ card, onFileOpen }) {
4805
+ function FileOperationView({ card, toolLabel, onFileOpen }) {
4339
4806
  const output = card.output?.trim() ?? "";
4340
4807
  const isRunning = card.statusTone === "running";
4341
- const hasStructuredPreview = Boolean(card.fileOperation?.blocks.length);
4342
- const hasContent = hasStructuredPreview || Boolean(output);
4808
+ const hasContent = Boolean(card.fileOperation?.blocks.length) || Boolean(output);
4343
4809
  const previewBlocks = card.fileOperation?.blocks ?? [];
4344
4810
  const previewLineCount = previewBlocks.reduce((count, block) => count + block.lines.length, 0);
4345
4811
  const previewCharCount = previewBlocks.reduce((count, block) => {
@@ -4354,22 +4820,26 @@ function FileOperationView({ card, onFileOpen }) {
4354
4820
  expandOnError: hasContent,
4355
4821
  statusTone: card.statusTone
4356
4822
  });
4823
+ const isEdit = card.toolName === "edit_file" || card.toolName === "write_file" || card.toolName === "apply_patch" || card.toolName === "file_change";
4357
4824
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4358
4825
  card,
4359
- icon: card.toolName === "edit_file" || card.toolName === "write_file" || card.toolName === "apply_patch" || card.toolName === "file_change" ? Code2 : FileText,
4826
+ toolLabel,
4827
+ changeSummary: isEdit ? countFileOperationChanges(previewBlocks) : void 0,
4828
+ icon: isEdit ? Code2 : FileText,
4360
4829
  expanded,
4361
4830
  canExpand: hasContent || isRunning,
4362
- hideSummary: expanded && hasStructuredPreview,
4831
+ hideSummary: false,
4363
4832
  onToggle
4364
- }), expanded && hasContent && /* @__PURE__ */ jsx(ToolCardContent, {
4365
- className: "border-t border-border bg-transparent px-0 pt-0 pb-0",
4833
+ }), expanded && hasContent ? /* @__PURE__ */ jsx(ToolCardContent, {
4834
+ className: "bg-transparent py-0",
4366
4835
  children: /* @__PURE__ */ jsx(ToolCardFileOperationContent, {
4367
4836
  card,
4368
- onFileOpen
4837
+ onFileOpen,
4838
+ showPathRow: true
4369
4839
  })
4370
- })] });
4840
+ }) : null] });
4371
4841
  }
4372
- function SearchSnippetView({ card }) {
4842
+ function SearchSnippetView({ card, toolLabel, icon: Icon = Search }) {
4373
4843
  const isRunning = card.statusTone === "running";
4374
4844
  const output = card.output?.trim() ?? "";
4375
4845
  const { expanded, onToggle } = useToolCardExpandedState({
@@ -4380,24 +4850,69 @@ function SearchSnippetView({ card }) {
4380
4850
  });
4381
4851
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4382
4852
  card,
4383
- icon: Search,
4853
+ toolLabel,
4854
+ icon: Icon,
4384
4855
  expanded,
4385
4856
  canExpand: !!output || isRunning,
4386
4857
  onToggle
4387
- }), expanded && output && /* @__PURE__ */ jsx(ToolCardContent, { children: /* @__PURE__ */ jsx("pre", {
4388
- className: "font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed",
4389
- children: output
4390
- }) })] });
4858
+ }), expanded && output && /* @__PURE__ */ jsx(ToolCardContent, {
4859
+ className: "py-0",
4860
+ children: /* @__PURE__ */ jsx("pre", {
4861
+ className: "font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed",
4862
+ children: output
4863
+ })
4864
+ })] });
4391
4865
  }
4392
- function GenericToolCard({ card, onToolAction, renderToolAgent }) {
4866
+ //#endregion
4867
+ //#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
+ function buildToolActionSlot(card, onToolAction, renderToolAgent) {
4901
+ const agentAction = card.agentId && renderToolAgent ? renderToolAgent(card.agentId) : null;
4902
+ const toolAction = card.action && onToolAction ? /* @__PURE__ */ jsx(ToolCardHeaderAction, {
4903
+ action: card.action,
4904
+ onAction: onToolAction
4905
+ }) : null;
4906
+ return agentAction || toolAction ? /* @__PURE__ */ jsxs(Fragment$1, { children: [agentAction, toolAction] }) : void 0;
4907
+ }
4908
+ function GenericToolCard({ card, toolLabel, icon: Icon = Wrench, onToolAction, renderToolAgent }) {
4393
4909
  const input = card.input?.trim() ?? "";
4394
4910
  const output = card.output?.trim() ?? "";
4395
4911
  const isRunning = card.statusTone === "running";
4396
4912
  const hasInputSection = input.length > 0;
4397
4913
  const hasOutputSection = output.length > 0;
4398
4914
  const hasContent = hasInputSection || hasOutputSection;
4399
- const inputLabel = card.inputLabel?.trim() || "Input";
4400
- const outputLabel = card.outputLabel?.trim() || "Output";
4915
+ const actionSlot = buildToolActionSlot(card, onToolAction, renderToolAgent);
4401
4916
  const { expanded, onToggle } = useToolCardExpandedState({
4402
4917
  canExpand: hasContent || isRunning,
4403
4918
  isRunning,
@@ -4406,30 +4921,28 @@ function GenericToolCard({ card, onToolAction, renderToolAgent }) {
4406
4921
  });
4407
4922
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4408
4923
  card,
4409
- icon: Globe,
4924
+ toolLabel,
4925
+ icon: Icon,
4410
4926
  expanded,
4411
4927
  canExpand: hasContent || isRunning,
4412
- actionSlot: card.agentId || card.action && onToolAction ? /* @__PURE__ */ jsxs(Fragment$1, { children: [card.agentId && renderToolAgent ? renderToolAgent(card.agentId) : null, card.action && onToolAction ? /* @__PURE__ */ jsx(ToolCardHeaderAction, {
4413
- action: card.action,
4414
- onAction: onToolAction
4415
- }) : null] }) : void 0,
4928
+ actionSlot,
4416
4929
  onToggle
4417
- }), expanded && hasContent && /* @__PURE__ */ jsxs(ToolCardContent, {
4418
- className: "bg-transparent px-2.5 py-2.5",
4930
+ }), expanded && hasContent ? /* @__PURE__ */ jsxs(ToolCardContent, {
4931
+ className: "bg-transparent py-0",
4419
4932
  children: [
4420
- hasInputSection && /* @__PURE__ */ jsx(GenericToolSection, {
4421
- label: inputLabel,
4933
+ hasInputSection ? /* @__PURE__ */ jsx(GenericToolSection, {
4934
+ label: card.inputLabel?.trim() || "Input",
4422
4935
  tone: "input",
4423
4936
  children: input
4424
- }),
4425
- hasInputSection && hasOutputSection && /* @__PURE__ */ jsx("div", { className: "h-2" }),
4426
- hasOutputSection && /* @__PURE__ */ jsx(GenericToolSection, {
4427
- label: outputLabel,
4937
+ }) : null,
4938
+ hasInputSection && hasOutputSection ? /* @__PURE__ */ jsx("div", { className: "h-2" }) : null,
4939
+ hasOutputSection ? /* @__PURE__ */ jsx(GenericToolSection, {
4940
+ label: card.outputLabel?.trim() || "Output",
4428
4941
  tone: card.statusTone === "error" ? "error" : "output",
4429
4942
  children: output
4430
- })
4943
+ }) : null
4431
4944
  ]
4432
- })] });
4945
+ }) : null] });
4433
4946
  }
4434
4947
  //#endregion
4435
4948
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-panel-app.tsx
@@ -4451,34 +4964,370 @@ function isTerminalTool(name) {
4451
4964
  const lowered = name.toLowerCase();
4452
4965
  return lowered === "exec" || lowered === "exec_command" || lowered === "execute_command" || lowered === "command_execution" || lowered === "bash" || lowered === "shell" || lowered.includes("run_");
4453
4966
  }
4454
- function isFileEditTool(name) {
4967
+ function isFileTool(name) {
4455
4968
  const lowered = name.toLowerCase();
4456
4969
  return lowered === "read_file" || lowered === "write_file" || lowered === "edit_file" || lowered === "apply_patch" || lowered === "file_change";
4457
4970
  }
4971
+ function isFileWriteTool(name) {
4972
+ const lowered = name.toLowerCase();
4973
+ return lowered === "write_file" || lowered === "edit_file" || lowered === "apply_patch" || lowered === "file_change";
4974
+ }
4458
4975
  function isSearchTool(name) {
4459
4976
  const lowered = name.toLowerCase();
4460
4977
  return lowered === "grep_search" || lowered === "find_files" || lowered.includes("search");
4461
4978
  }
4462
- function ChatToolCard({ card, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard }) {
4979
+ const BUILT_IN_TOOL_PRESENTATIONS = {
4980
+ list_dir: {
4981
+ statusKind: "directory",
4982
+ icon: FolderTree
4983
+ },
4984
+ web_fetch: {
4985
+ statusKind: "web",
4986
+ icon: Globe2
4987
+ },
4988
+ message: {
4989
+ statusKind: "message",
4990
+ icon: Send
4991
+ },
4992
+ sessions_list: {
4993
+ statusKind: "session",
4994
+ icon: MessageSquare
4995
+ },
4996
+ sessions_history: {
4997
+ statusKind: "session",
4998
+ icon: MessageSquare
4999
+ },
5000
+ sessions_request: {
5001
+ statusKind: "session",
5002
+ icon: MessageSquare
5003
+ },
5004
+ spawn: {
5005
+ statusKind: "agent",
5006
+ icon: Bot
5007
+ },
5008
+ sessions_spawn: {
5009
+ statusKind: "agent",
5010
+ icon: Bot
5011
+ },
5012
+ subagents: {
5013
+ statusKind: "agent",
5014
+ icon: Bot
5015
+ },
5016
+ memory_search: {
5017
+ statusKind: "memory",
5018
+ icon: Brain
5019
+ },
5020
+ memory_get: {
5021
+ statusKind: "memory",
5022
+ icon: Brain
5023
+ },
5024
+ cron: {
5025
+ statusKind: "schedule",
5026
+ icon: CalendarClock
5027
+ },
5028
+ gateway: {
5029
+ statusKind: "system",
5030
+ icon: Settings
5031
+ },
5032
+ view_image: {
5033
+ statusKind: "image",
5034
+ icon: ImageIcon
5035
+ },
5036
+ show_file: {
5037
+ statusKind: "display",
5038
+ icon: Eye
5039
+ },
5040
+ show_url: {
5041
+ statusKind: "display",
5042
+ icon: Eye
5043
+ },
5044
+ show_panel_app: {
5045
+ statusKind: "display",
5046
+ icon: Eye
5047
+ },
5048
+ show_content: {
5049
+ statusKind: "display",
5050
+ icon: Eye
5051
+ }
5052
+ };
5053
+ function resolveBuiltInToolPresentation(name) {
5054
+ return BUILT_IN_TOOL_PRESENTATIONS[name.toLowerCase()] ?? null;
5055
+ }
5056
+ function ChatToolCard({ card, toolStatusLabels, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard }) {
4463
5057
  if (card.panelApp) return /* @__PURE__ */ jsx(PanelAppInlineToolCard, {
4464
5058
  card,
4465
5059
  onToolAction,
4466
5060
  renderPanelAppCard
4467
5061
  });
4468
- if (isTerminalTool(card.toolName)) return /* @__PURE__ */ jsx(TerminalExecutionView, { card });
4469
- if (isFileEditTool(card.toolName)) return /* @__PURE__ */ jsx(FileOperationView, {
5062
+ const builtInPresentation = resolveBuiltInToolPresentation(card.toolName);
5063
+ const builtInLabel = builtInPresentation ? toolStatusLabels?.builtIn?.[builtInPresentation.statusKind]?.[card.statusTone] : void 0;
5064
+ if (isTerminalTool(card.toolName)) return /* @__PURE__ */ jsx(TerminalExecutionView, {
5065
+ card,
5066
+ toolLabel: toolStatusLabels?.terminal[card.statusTone]
5067
+ });
5068
+ if (isFileTool(card.toolName)) return /* @__PURE__ */ jsx(FileOperationView, {
4470
5069
  card,
5070
+ toolLabel: isFileWriteTool(card.toolName) ? toolStatusLabels?.fileEdit[card.statusTone] : toolStatusLabels?.fileRead[card.statusTone],
4471
5071
  onFileOpen
4472
5072
  });
4473
- if (isSearchTool(card.toolName)) return /* @__PURE__ */ jsx(SearchSnippetView, { card });
5073
+ if (isSearchTool(card.toolName)) return /* @__PURE__ */ jsx(SearchSnippetView, {
5074
+ card,
5075
+ toolLabel: builtInLabel ?? toolStatusLabels?.search[card.statusTone],
5076
+ icon: builtInPresentation?.icon
5077
+ });
4474
5078
  return /* @__PURE__ */ jsx(GenericToolCard, {
4475
5079
  card,
5080
+ toolLabel: builtInLabel,
5081
+ icon: builtInPresentation?.icon,
4476
5082
  onToolAction,
4477
5083
  renderToolAgent
4478
5084
  });
4479
5085
  }
4480
5086
  //#endregion
5087
+ //#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);
5090
+ const toolCount = group.parts.filter((part) => part.type === "tool-card").length;
5091
+ const showWorkflowRail = open && toolCount > 1;
5092
+ return /* @__PURE__ */ jsxs("div", {
5093
+ className: "group/tool-activity",
5094
+ children: [/* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
5095
+ openGroup: "tool-activity",
5096
+ open,
5097
+ label: group.label,
5098
+ onClick: () => setOpen((current) => !current)
5099
+ }), open ? /* @__PURE__ */ jsx("div", {
5100
+ className: "text-[0.925rem] leading-[1.72]",
5101
+ children: group.parts.map((part, index) => {
5102
+ const isLast = index === group.parts.length - 1;
5103
+ return /* @__PURE__ */ jsxs("div", {
5104
+ 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, {
5110
+ card: part.card,
5111
+ toolStatusLabels,
5112
+ onToolAction,
5113
+ onFileOpen,
5114
+ renderToolAgent,
5115
+ 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
+ })
5125
+ })]
5126
+ }, `tool-group-item-${group.startIndex + index}`);
5127
+ })
5128
+ }) : null]
5129
+ });
5130
+ }
5131
+ //#endregion
5132
+ //#region src/components/chat/ui/chat-message-list/chat-tool-activity-group.utils.ts
5133
+ const MAX_SUMMARY_SEGMENTS = 3;
5134
+ function isToolCardPart(part) {
5135
+ return part.type === "tool-card";
5136
+ }
5137
+ function resolveToolActivityFamily(toolName) {
5138
+ const lowered = toolName.toLowerCase();
5139
+ if (lowered === "list_dir") return "directory";
5140
+ if (lowered.includes("show_content") || lowered.includes("panel_app") || lowered.includes("panel")) return "panel";
5141
+ if (lowered === "spawn" || lowered.includes("subagent") || lowered.includes("session_request") || lowered.includes("agent") && !lowered.includes("read")) return "agent";
5142
+ if (lowered.includes("web_") || lowered.includes("_web") || lowered.includes("url") || lowered.includes("fetch") || lowered.includes("http")) return "web";
5143
+ if (lowered === "grep_search" || lowered === "find_files" || lowered === "search_files" || lowered.includes("search") || lowered.includes("grep")) return "search";
5144
+ if (lowered === "exec" || lowered === "exec_command" || lowered === "execute_command" || lowered === "command_execution" || lowered === "bash" || lowered === "shell" || lowered === "terminal" || lowered.includes("run_")) return "bash";
5145
+ if (lowered === "write_file" || lowered === "edit_file" || lowered === "apply_patch" || lowered === "file_change" || lowered === "multi_replace" || lowered.includes("write_file") || lowered.includes("edit_file") || lowered.includes("apply_patch")) return "edit";
5146
+ if (lowered === "read_file" || lowered === "read" || lowered === "view_file" || lowered.includes("read_file") || lowered.startsWith("read_")) return "read";
5147
+ return "other";
5148
+ }
5149
+ function toneRank(tone) {
5150
+ if (tone === "error") return 0;
5151
+ if (tone === "cancelled") return 1;
5152
+ if (tone === "running") return 2;
5153
+ return 3;
5154
+ }
5155
+ function resolveActivityUnitKeys(card, family, callIndex) {
5156
+ if (family !== "read" && family !== "edit" && family !== "directory") return [`call:${callIndex}`];
5157
+ const paths = card.fileOperation?.blocks.map(({ path }) => path.trim()).filter(Boolean);
5158
+ const summary = card.summary?.replace(/^(path|file):\s*/i, "").trim();
5159
+ return paths?.length ? paths : [summary || `call:${callIndex}`];
5160
+ }
5161
+ /**
5162
+ * Natural category+count phrases only.
5163
+ * Examples: `Read 3 files`, `运行 2 条命令`, `搜索 2 次`.
5164
+ * No command/path payload.
5165
+ */
5166
+ function formatSegment(segment, labels) {
5167
+ const template = labels.segmentTemplates[segment.family];
5168
+ const body = segment.count === 1 ? template.one : template.other.split("{count}").join(String(segment.count));
5169
+ if (segment.tone === "error") return `${body} ${labels.failedLabel}`;
5170
+ if (segment.tone === "cancelled") return `${body} ${labels.cancelledLabel}`;
5171
+ return body;
5172
+ }
5173
+ function buildToolActivitySegments(cards) {
5174
+ const order = [];
5175
+ const byFamily = /* @__PURE__ */ new Map();
5176
+ for (const [callIndex, card] of cards.entries()) {
5177
+ const family = resolveToolActivityFamily(card.toolName);
5178
+ const unitKeys = resolveActivityUnitKeys(card, family, callIndex);
5179
+ const existing = byFamily.get(family);
5180
+ if (!existing) {
5181
+ order.push(family);
5182
+ byFamily.set(family, {
5183
+ units: new Set(unitKeys),
5184
+ tone: card.statusTone
5185
+ });
5186
+ continue;
5187
+ }
5188
+ unitKeys.forEach((unitKey) => existing.units.add(unitKey));
5189
+ if (toneRank(card.statusTone) < toneRank(existing.tone)) existing.tone = card.statusTone;
5190
+ }
5191
+ const segments = order.map((family) => {
5192
+ const entry = byFamily.get(family);
5193
+ return {
5194
+ family,
5195
+ count: entry.units.size,
5196
+ tone: entry.tone
5197
+ };
5198
+ });
5199
+ segments.sort((left, right) => {
5200
+ const toneDelta = toneRank(left.tone) - toneRank(right.tone);
5201
+ if (toneDelta !== 0) return toneDelta;
5202
+ return order.indexOf(left.family) - order.indexOf(right.family);
5203
+ });
5204
+ return segments;
5205
+ }
5206
+ function formatToolActivityGroupLabel(params) {
5207
+ const { labels, segments } = params;
5208
+ if (segments.length === 0) return "";
5209
+ const visible = segments.slice(0, MAX_SUMMARY_SEGMENTS);
5210
+ const hiddenCount = segments.length - visible.length;
5211
+ const parts = visible.map((segment) => formatSegment(segment, labels));
5212
+ if (hiddenCount > 0) parts.push(`+${hiddenCount}`);
5213
+ return parts.join(" · ");
5214
+ }
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
+ */
5220
+ function groupConsecutiveToolParts(parts, labels) {
5221
+ const blocks = [];
5222
+ let index = 0;
5223
+ while (index < parts.length) {
5224
+ const part = parts[index];
5225
+ if (part.type !== "tool-card") {
5226
+ blocks.push({
5227
+ kind: "part",
5228
+ key: `part-${index}`,
5229
+ index,
5230
+ part
5231
+ });
5232
+ index += 1;
5233
+ continue;
5234
+ }
5235
+ 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
+ }
5244
+ const toolParts = candidateParts.filter(isToolCardPart);
5245
+ if (toolParts.length === 1) {
5246
+ blocks.push({
5247
+ kind: "part",
5248
+ key: `part-${startIndex}`,
5249
+ index: startIndex,
5250
+ part
5251
+ });
5252
+ index = startIndex + 1;
5253
+ continue;
5254
+ }
5255
+ const groupedParts = candidateParts.slice(0, lastToolOffset + 1);
5256
+ index = startIndex + groupedParts.length;
5257
+ const segments = buildToolActivitySegments(toolParts.map((toolPart) => toolPart.card));
5258
+ const label = formatToolActivityGroupLabel({
5259
+ segments,
5260
+ labels
5261
+ });
5262
+ blocks.push({
5263
+ kind: "tool-group",
5264
+ key: `tool-group-${startIndex}-${index - 1}`,
5265
+ group: {
5266
+ key: `tool-group-${startIndex}-${index - 1}`,
5267
+ startIndex,
5268
+ endIndex: index - 1,
5269
+ parts: groupedParts,
5270
+ segments,
5271
+ label
5272
+ }
5273
+ });
5274
+ }
5275
+ return blocks;
5276
+ }
5277
+ //#endregion
4481
5278
  //#region src/components/chat/ui/chat-message-list/chat-message.tsx
5279
+ const DEFAULT_TOOL_ACTIVITY_LABELS = {
5280
+ segmentTemplates: {
5281
+ read: {
5282
+ one: "Read 1 file",
5283
+ other: "Read {count} files"
5284
+ },
5285
+ edit: {
5286
+ one: "Edit 1 file",
5287
+ other: "Edit {count} files"
5288
+ },
5289
+ directory: {
5290
+ one: "View 1 directory",
5291
+ other: "View {count} directories"
5292
+ },
5293
+ search: {
5294
+ one: "Search 1 time",
5295
+ other: "Search {count} times"
5296
+ },
5297
+ bash: {
5298
+ one: "Run 1 command",
5299
+ other: "Run {count} commands"
5300
+ },
5301
+ web: {
5302
+ one: "Open 1 page",
5303
+ other: "Open {count} pages"
5304
+ },
5305
+ agent: {
5306
+ one: "Start 1 subtask",
5307
+ other: "Start {count} subtasks"
5308
+ },
5309
+ panel: {
5310
+ one: "Show 1 result",
5311
+ other: "Show {count} results"
5312
+ },
5313
+ other: {
5314
+ one: "Use 1 tool",
5315
+ other: "Use {count} tools"
5316
+ }
5317
+ },
5318
+ failedLabel: "failed",
5319
+ cancelledLabel: "cancelled"
5320
+ };
5321
+ function resolveToolActivityLabels(texts) {
5322
+ return {
5323
+ segmentTemplates: {
5324
+ ...DEFAULT_TOOL_ACTIVITY_LABELS.segmentTemplates,
5325
+ ...texts.toolActivitySegmentTemplates ?? {}
5326
+ },
5327
+ failedLabel: texts.toolActivityFailedLabel ?? DEFAULT_TOOL_ACTIVITY_LABELS.failedLabel,
5328
+ cancelledLabel: texts.toolActivityCancelledLabel ?? DEFAULT_TOOL_ACTIVITY_LABELS.cancelledLabel
5329
+ };
5330
+ }
4482
5331
  function isMessageInProgress(status) {
4483
5332
  return status === "pending" || status === "streaming";
4484
5333
  }
@@ -4498,7 +5347,7 @@ function splitAssistantProcess(message) {
4498
5347
  finalParts: message.parts.slice(lastProcessPartIndex + 1)
4499
5348
  };
4500
5349
  }
4501
- function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFileOpen, onToolAction, part, renderInlineDisplay, renderPanelAppCard, renderToolAgent, role, texts }) {
5350
+ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, part, renderInlineDisplay, renderPanelAppCard, renderToolAgent, role, texts }) {
4502
5351
  const { type } = part;
4503
5352
  if (type === "markdown") {
4504
5353
  const { inlineTokens, text } = part;
@@ -4508,6 +5357,7 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4508
5357
  texts,
4509
5358
  inlineTokens,
4510
5359
  onFileOpen,
5360
+ onInlineTokenClick,
4511
5361
  renderInlineDisplay
4512
5362
  }, `markdown-${index}`);
4513
5363
  }
@@ -4516,29 +5366,29 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4516
5366
  return /* @__PURE__ */ jsx(ChatReasoningBlock, {
4517
5367
  label,
4518
5368
  text,
5369
+ characterCountTemplates: texts.reasoningCharacterCountTemplates,
4519
5370
  isUser,
4520
5371
  isInProgress: isInProgress && isLastPart
4521
5372
  }, `reasoning-${index}`);
4522
5373
  }
4523
5374
  if (type === "tool-card") {
4524
5375
  const { card } = part;
4525
- return /* @__PURE__ */ jsx("div", {
4526
- className: "mt-0.5",
4527
- children: /* @__PURE__ */ jsx(ChatToolCard, {
4528
- card,
4529
- onToolAction,
4530
- onFileOpen,
4531
- renderToolAgent,
4532
- renderPanelAppCard
4533
- })
4534
- }, `tool-${index}`);
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}`);
4535
5384
  }
4536
5385
  if (type === "file") {
4537
5386
  const { file } = part;
4538
5387
  return /* @__PURE__ */ jsx(ChatMessageFile, {
4539
5388
  file,
4540
5389
  isUser,
4541
- texts
5390
+ texts,
5391
+ onOpen: onAttachmentOpen
4542
5392
  }, `file-${index}`);
4543
5393
  }
4544
5394
  if (type === "unknown") {
@@ -4560,49 +5410,102 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4560
5410
  }
4561
5411
  return null;
4562
5412
  }
4563
- const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, onFileOpen, renderInlineDisplay, renderToolAgent, renderPanelAppCard }) {
5413
+ 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) => {
5416
+ if (block.kind === "tool-group") return /* @__PURE__ */ jsx(ChatToolActivityGroup, {
5417
+ group: block.group,
5418
+ isUser,
5419
+ reasoningCharacterCountTemplates: texts.reasoningCharacterCountTemplates,
5420
+ toolStatusLabels: texts.toolStatusLabels,
5421
+ onToolAction,
5422
+ onFileOpen,
5423
+ renderToolAgent,
5424
+ renderPanelAppCard
5425
+ }, block.key);
5426
+ return renderChatMessagePart({
5427
+ part: block.part,
5428
+ index: indexOffset + block.index,
5429
+ role,
5430
+ isUser,
5431
+ isInProgress,
5432
+ isLastPart: indexOffset + block.index === indexOffset + parts.length - 1,
5433
+ texts,
5434
+ onToolAction,
5435
+ onFileOpen,
5436
+ onAttachmentOpen,
5437
+ onInlineTokenClick,
5438
+ renderInlineDisplay,
5439
+ renderToolAgent,
5440
+ renderPanelAppCard
5441
+ });
5442
+ });
5443
+ }
5444
+ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, onFileOpen, onAttachmentOpen, onInlineTokenClick, renderInlineDisplay, renderToolAgent, renderPanelAppCard }) {
4564
5445
  const { role } = message;
4565
5446
  const isUser = role === "user";
4566
5447
  const isInProgress = isMessageInProgress(message.status);
4567
5448
  const processSplit = splitAssistantProcess(message);
4568
- const renderPart = (part, index) => renderChatMessagePart({
4569
- part,
4570
- index,
4571
- role,
4572
- isUser,
4573
- isInProgress,
4574
- isLastPart: index === message.parts.length - 1,
4575
- texts,
4576
- onToolAction,
4577
- onFileOpen,
4578
- renderInlineDisplay,
4579
- renderToolAgent,
4580
- renderPanelAppCard
4581
- });
5449
+ const [processOpen, setProcessOpen] = useState(false);
4582
5450
  return /* @__PURE__ */ jsx("div", {
4583
- 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 ? "border-primary bg-primary py-3 text-primary-foreground" : role === "assistant" ? "border-border bg-card pb-3 pt-4 text-card-foreground" : "border-border bg-muted/45 py-3 text-foreground"),
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"),
4584
5452
  children: /* @__PURE__ */ jsx("div", {
4585
- className: "space-y-2",
4586
- children: processSplit ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("details", {
4587
- className: "group/process text-xs text-muted-foreground",
4588
- children: [/* @__PURE__ */ jsxs("summary", {
4589
- className: "flex cursor-pointer list-none items-center gap-1.5 border-b border-border/60 pb-2 font-medium transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 [&::-webkit-details-marker]:hidden",
4590
- children: [
4591
- /* @__PURE__ */ jsx(ChevronRight, {
4592
- className: "h-3.5 w-3.5 group-open/process:hidden",
4593
- strokeWidth: 2.5
4594
- }),
4595
- /* @__PURE__ */ jsx(ChevronDown, {
4596
- className: "hidden h-3.5 w-3.5 group-open/process:block",
4597
- strokeWidth: 2.5
4598
- }),
4599
- /* @__PURE__ */ jsx("span", { children: message.processSummary?.label })
4600
- ]
4601
- }), /* @__PURE__ */ jsx("div", {
4602
- className: "space-y-2 pt-2",
4603
- children: processSplit.processParts.map(renderPart)
4604
- })]
4605
- }), processSplit.finalParts.map((part, index) => renderPart(part, processSplit.processParts.length + index))] }) : message.parts.map(renderPart)
5453
+ className: "space-y-0",
5454
+ children: processSplit ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
5455
+ className: "group/process",
5456
+ children: [/* @__PURE__ */ jsx("div", {
5457
+ className: "mb-2 border-b border-border/60 pb-2",
5458
+ children: /* @__PURE__ */ jsx(ChatCollapsibleMetaSummary, {
5459
+ openGroup: "process",
5460
+ open: processOpen,
5461
+ label: message.processSummary?.label,
5462
+ onClick: () => setProcessOpen((current) => !current)
5463
+ })
5464
+ }), processOpen ? /* @__PURE__ */ jsx("div", {
5465
+ className: "space-y-0",
5466
+ children: renderMessageParts({
5467
+ parts: processSplit.processParts,
5468
+ role,
5469
+ isUser,
5470
+ isInProgress,
5471
+ texts,
5472
+ onToolAction,
5473
+ onFileOpen,
5474
+ onAttachmentOpen,
5475
+ onInlineTokenClick,
5476
+ renderInlineDisplay,
5477
+ renderToolAgent,
5478
+ renderPanelAppCard
5479
+ })
5480
+ }) : null]
5481
+ }), renderMessageParts({
5482
+ parts: processSplit.finalParts,
5483
+ role,
5484
+ isUser,
5485
+ isInProgress,
5486
+ texts,
5487
+ onToolAction,
5488
+ onFileOpen,
5489
+ onAttachmentOpen,
5490
+ onInlineTokenClick,
5491
+ renderInlineDisplay,
5492
+ renderToolAgent,
5493
+ renderPanelAppCard,
5494
+ indexOffset: processSplit.processParts.length
5495
+ })] }) : renderMessageParts({
5496
+ parts: message.parts,
5497
+ role,
5498
+ isUser,
5499
+ isInProgress,
5500
+ texts,
5501
+ onToolAction,
5502
+ onFileOpen,
5503
+ onAttachmentOpen,
5504
+ onInlineTokenClick,
5505
+ renderInlineDisplay,
5506
+ renderToolAgent,
5507
+ renderPanelAppCard
5508
+ })
4606
5509
  })
4607
5510
  });
4608
5511
  });
@@ -4702,7 +5605,7 @@ function ChatTypingIndicator({ label }) {
4702
5605
  })]
4703
5606
  });
4704
5607
  }
4705
- function ChatMessageList({ className, isSending, messages, onFileOpen, onToolAction, renderInlineDisplay, renderPanelAppCard, renderToolAgent, texts }) {
5608
+ function ChatMessageList({ className, isSending, messages, onAttachmentOpen, onFileOpen, onInlineTokenClick, onToolAction, renderInlineDisplay, renderPanelAppCard, renderToolAgent, texts }) {
4706
5609
  const visibleMessages = messages.filter(hasRenderableMessageContent);
4707
5610
  const hasRenderableAssistantDraft = visibleMessages.some((message) => message.role === "assistant" && (message.status === "streaming" || message.status === "pending"));
4708
5611
  return /* @__PURE__ */ jsxs("div", {
@@ -4721,6 +5624,8 @@ function ChatMessageList({ className, isSending, messages, onFileOpen, onToolAct
4721
5624
  texts,
4722
5625
  onToolAction,
4723
5626
  onFileOpen,
5627
+ onAttachmentOpen,
5628
+ onInlineTokenClick,
4724
5629
  renderInlineDisplay,
4725
5630
  renderToolAgent,
4726
5631
  renderPanelAppCard