@nextclaw/agent-chat-ui 0.6.2 → 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",
@@ -2419,15 +2420,15 @@ function resolveInlineTokenTone(kind) {
2419
2420
  if (kind === "panel_app") return "panel_app";
2420
2421
  return "default";
2421
2422
  }
2422
- function resolveInlineTokenBadgeClassName(tone, isUser) {
2423
- if (tone === "skill") return isUser ? "border-primary-foreground/30 bg-primary-foreground/18 text-primary-foreground" : "border-border bg-accent text-accent-foreground";
2424
- if (tone === "panel_app") return isUser ? "border-primary-foreground/30 bg-primary-foreground/18 text-primary-foreground" : "border-border bg-accent text-accent-foreground";
2425
- return isUser ? "border-primary-foreground/30 bg-primary-foreground/18 text-primary-foreground" : "border-border bg-muted text-muted-foreground";
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";
2426
2427
  }
2427
2428
  function resolveInlineTokenIconClassName(tone, isUser) {
2428
- if (tone === "skill") return isUser ? "text-primary-foreground/70" : "text-muted-foreground";
2429
- if (tone === "panel_app") return isUser ? "text-primary-foreground/70" : "text-muted-foreground";
2430
- return isUser ? "text-primary-foreground/70" : "text-muted-foreground";
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";
2431
2432
  }
2432
2433
  function renderInlineTokenIcon(tone) {
2433
2434
  return tone === "panel_app" ? /* @__PURE__ */ jsx(AppWindow, {
@@ -2438,18 +2439,32 @@ function renderInlineTokenIcon(tone) {
2438
2439
  className: "h-3 w-3"
2439
2440
  });
2440
2441
  }
2441
- function ChatInlineTokenBadge({ kind, label, isUser }) {
2442
+ function ChatInlineTokenBadge({ kind, label, isUser, onClick }) {
2442
2443
  const tone = resolveInlineTokenTone(kind);
2443
- return /* @__PURE__ */ jsxs("span", {
2444
- className: cn("mx-[2px] inline-flex h-7 max-w-full items-center gap-1.5 rounded-xl border px-2.5 align-baseline text-[11px] font-medium shadow-[0_0_0_1px_rgba(15,23,42,0.03)]", resolveInlineTokenBadgeClassName(tone, isUser)),
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,
2445
2456
  title: label,
2446
- children: [/* @__PURE__ */ jsx("span", {
2447
- className: cn("inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center", resolveInlineTokenIconClassName(tone, isUser)),
2448
- children: renderInlineTokenIcon(tone)
2449
- }), /* @__PURE__ */ jsx("span", {
2450
- className: "truncate",
2451
- children: label
2452
- })]
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
2453
2468
  });
2454
2469
  }
2455
2470
  //#endregion
@@ -2655,7 +2670,7 @@ const REGISTERED_LANGUAGES = [
2655
2670
  aliases: ["patch"]
2656
2671
  }
2657
2672
  ];
2658
- function escapeHtml(value) {
2673
+ function escapeHtml$1(value) {
2659
2674
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2660
2675
  }
2661
2676
  function normalizeLanguage(value) {
@@ -2686,7 +2701,7 @@ var ChatCodeSyntaxHighlighter = class {
2686
2701
  return this.createPlainTextResult(code, normalizedLanguage || "text");
2687
2702
  };
2688
2703
  this.createPlainTextResult = (code, language) => ({
2689
- html: escapeHtml(code),
2704
+ html: escapeHtml$1(code),
2690
2705
  language,
2691
2706
  highlighted: false
2692
2707
  });
@@ -3054,7 +3069,7 @@ function readStringProp(props, key) {
3054
3069
  const value = props[key];
3055
3070
  return typeof value === "string" && value.length > 0 ? value : null;
3056
3071
  }
3057
- function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, renderInlineDisplay }) {
3072
+ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, onInlineTokenClick, renderInlineDisplay }) {
3058
3073
  const isUser = role === "user";
3059
3074
  const remarkPlugins = useMemo(() => inlineTokens && inlineTokens.length > 0 ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm], [inlineTokens]);
3060
3075
  const markdownComponents = useMemo(() => ({
@@ -3068,7 +3083,13 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3068
3083
  if (kind && key && label && rawText) return /* @__PURE__ */ jsx(ChatInlineTokenBadge, {
3069
3084
  kind,
3070
3085
  label,
3071
- isUser
3086
+ isUser,
3087
+ onClick: onInlineTokenClick ? () => onInlineTokenClick({
3088
+ kind,
3089
+ key,
3090
+ label,
3091
+ rawText
3092
+ }) : void 0
3072
3093
  });
3073
3094
  return /* @__PURE__ */ jsx("span", {
3074
3095
  ...rest,
@@ -3152,6 +3173,7 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3152
3173
  inline,
3153
3174
  isUser,
3154
3175
  onFileOpen,
3176
+ onInlineTokenClick,
3155
3177
  renderInlineDisplay,
3156
3178
  texts
3157
3179
  ]);
@@ -3168,16 +3190,16 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
3168
3190
  //#endregion
3169
3191
  //#region src/components/chat/ui/chat-message-list/chat-message-file/meta.ts
3170
3192
  const FILE_CATEGORY_TILE_CLASSES = {
3171
- archive: "border-border bg-muted text-muted-foreground",
3172
- audio: "border-border bg-muted text-muted-foreground",
3173
- code: "border-border bg-muted text-muted-foreground",
3174
- data: "border-border bg-muted text-muted-foreground",
3175
- document: "border-border bg-muted text-muted-foreground",
3176
- generic: "border-border bg-muted text-muted-foreground",
3177
- image: "border-border bg-muted text-muted-foreground",
3178
- pdf: "border-border bg-muted text-muted-foreground",
3179
- sheet: "border-border bg-muted text-muted-foreground",
3180
- video: "border-border bg-muted text-muted-foreground"
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"
3181
3203
  };
3182
3204
  const CODE_EXTENSIONS = new Set([
3183
3205
  "c",
@@ -3387,77 +3409,166 @@ function readFileCategoryLabel(category, texts) {
3387
3409
  }
3388
3410
  function renderMetaLine(categoryLabel, sizeLabel, isUser) {
3389
3411
  return /* @__PURE__ */ jsx("div", {
3390
- 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"),
3391
3413
  children: sizeLabel ? `${categoryLabel} · ${sizeLabel}` : categoryLabel
3392
3414
  });
3393
3415
  }
3394
3416
  function renderActionPill(label, isUser, isInteractive) {
3395
3417
  return /* @__PURE__ */ jsx("span", {
3396
- className: cn("inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium", isInteractive ? isUser ? "border-primary-foreground/14 bg-primary-foreground/12 text-primary-foreground" : "border-border bg-card text-foreground" : isUser ? "border-primary-foreground/10 bg-primary-foreground/6 text-primary-foreground/62" : "border-border bg-muted text-muted-foreground"),
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"),
3397
3419
  children: label
3398
3420
  });
3399
3421
  }
3400
- 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);
3401
3436
  return /* @__PURE__ */ jsx("a", {
3402
3437
  href,
3403
3438
  target: "_blank",
3404
3439
  rel: "noreferrer",
3405
- className: cn("inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium transition duration-200", isUser ? "border-primary-foreground/14 bg-primary-foreground/12 text-primary-foreground hover:border-primary-foreground/20 hover:bg-primary-foreground/16" : "border-border bg-card text-foreground hover:border-border-hover hover:bg-accent"),
3440
+ className,
3406
3441
  children: label
3407
3442
  });
3408
3443
  }
3409
3444
  function FileCategoryGlyph({ category, isUser }) {
3410
3445
  const Icon = FILE_CATEGORY_ICONS[category];
3411
3446
  return /* @__PURE__ */ jsx("div", {
3412
- className: cn("flex h-14 w-14 shrink-0 items-center justify-center rounded-[1rem] border", isUser ? "border-primary-foreground/12 bg-primary-foreground/10 text-primary-foreground" : FILE_CATEGORY_TILE_CLASSES[category]),
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]),
3413
3448
  children: /* @__PURE__ */ jsx(Icon, {
3414
- className: cn("h-7 w-7", isUser ? "text-primary-foreground/92" : "text-current"),
3415
- strokeWidth: 2.2
3449
+ className: cn("h-4 w-4", isUser ? "text-foreground/70" : "text-current"),
3450
+ strokeWidth: 2
3416
3451
  })
3417
3452
  });
3418
3453
  }
3419
- function renderImagePreview(params) {
3420
- 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);
3421
3508
  if (!file.dataUrl) return null;
3422
- return /* @__PURE__ */ jsx("a", {
3423
- href: file.dataUrl,
3424
- target: "_blank",
3425
- rel: "noreferrer",
3426
- className: "group block",
3427
- children: /* @__PURE__ */ jsxs("div", {
3428
- className: cn("relative overflow-hidden rounded-[1rem]", isUser ? "ring-1 ring-primary-foreground/10" : "bg-muted ring-1 ring-border"),
3429
- children: [/* @__PURE__ */ jsx("img", {
3430
- src: file.dataUrl,
3431
- alt: file.label,
3432
- className: "block h-auto max-h-[26rem] w-full rounded-[1rem] bg-transparent object-contain transition duration-300 group-hover:scale-[1.01]"
3433
- }), /* @__PURE__ */ jsxs("div", {
3434
- className: "pointer-events-none absolute right-3 top-3 flex items-center gap-2",
3435
- children: [/* @__PURE__ */ jsx("span", {
3436
- className: cn("inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold tracking-[0.18em] text-white backdrop-blur-sm", isUser ? "bg-black/36" : "bg-black/58"),
3437
- children: categoryLabel
3438
- }), sizeLabel ? /* @__PURE__ */ jsx("span", {
3439
- className: cn("inline-flex items-center rounded-full px-2 py-1 text-[10px] font-medium text-white/92 backdrop-blur-sm", isUser ? "bg-black/28" : "bg-black/46"),
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",
3440
3546
  children: sizeLabel
3441
- }) : null]
3442
- })]
3443
- })
3444
- });
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] });
3445
3556
  }
3446
3557
  function renderFileCardHeader(params) {
3447
3558
  const { category, file, categoryLabel, sizeLabel, isUser, action } = params;
3448
3559
  return /* @__PURE__ */ jsxs("div", {
3449
- className: "flex items-center gap-3 p-3.5",
3560
+ className: "flex items-center gap-2.5 px-2.5 py-2",
3450
3561
  children: [/* @__PURE__ */ jsx(FileCategoryGlyph, {
3451
3562
  category,
3452
3563
  isUser
3453
3564
  }), /* @__PURE__ */ jsx("div", {
3454
3565
  className: "min-w-0 flex-1",
3455
3566
  children: /* @__PURE__ */ jsxs("div", {
3456
- className: "flex items-center gap-3",
3567
+ className: "flex items-center gap-2",
3457
3568
  children: [/* @__PURE__ */ jsxs("div", {
3458
3569
  className: "min-w-0 flex-1",
3459
3570
  children: [/* @__PURE__ */ jsx("div", {
3460
- className: "truncate text-[15px] font-semibold leading-5",
3571
+ className: "truncate text-[13px] font-medium leading-5",
3461
3572
  children: file.label
3462
3573
  }), renderMetaLine(categoryLabel, sizeLabel, isUser)]
3463
3574
  }), action]
@@ -3466,7 +3577,7 @@ function renderFileCardHeader(params) {
3466
3577
  });
3467
3578
  }
3468
3579
  function renderInlineMediaCard(params) {
3469
- const { category, file, categoryLabel, sizeLabel, isUser, shellClasses, actionLabel } = params;
3580
+ const { category, file, categoryLabel, sizeLabel, isUser, shellClasses, actionLabel, onOpen } = params;
3470
3581
  if (!file.dataUrl || category !== "audio" && category !== "video") return null;
3471
3582
  const mediaMimeType = resolveRenderableMimeType(file);
3472
3583
  return /* @__PURE__ */ jsxs("div", {
@@ -3477,9 +3588,14 @@ function renderInlineMediaCard(params) {
3477
3588
  categoryLabel,
3478
3589
  sizeLabel,
3479
3590
  isUser,
3480
- 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
+ })
3481
3597
  }), /* @__PURE__ */ jsx("div", {
3482
- className: "px-3.5 pb-3.5",
3598
+ className: "px-2.5 pb-2.5",
3483
3599
  children: category === "audio" ? /* @__PURE__ */ jsx("audio", {
3484
3600
  controls: true,
3485
3601
  preload: "metadata",
@@ -3490,7 +3606,7 @@ function renderInlineMediaCard(params) {
3490
3606
  ...mediaMimeType ? { type: mediaMimeType } : {}
3491
3607
  })
3492
3608
  }) : /* @__PURE__ */ jsx("div", {
3493
- 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"),
3494
3610
  children: /* @__PURE__ */ jsx("video", {
3495
3611
  controls: true,
3496
3612
  preload: "metadata",
@@ -3506,20 +3622,22 @@ function renderInlineMediaCard(params) {
3506
3622
  })]
3507
3623
  });
3508
3624
  }
3509
- function ChatMessageFile({ file, isUser = false, texts }) {
3625
+ function ChatMessageFile({ file, isUser = false, texts, onOpen }) {
3510
3626
  const { category, sizeLabel } = buildChatMessageFileMeta(file);
3511
3627
  const renderAsImage = isImageFileLike(file) && Boolean(file.dataUrl);
3512
3628
  const renderAsAudio = category === "audio" && Boolean(file.dataUrl);
3513
3629
  const renderAsVideo = category === "video" && Boolean(file.dataUrl);
3514
3630
  const isInteractive = Boolean(file.dataUrl);
3515
3631
  const actionLabel = isInteractive ? texts?.attachmentOpenLabel ?? "Open" : texts?.attachmentAttachedLabel ?? "Attached";
3632
+ const expandLabel = texts?.attachmentExpandLabel ?? "Expand image";
3633
+ const closeLabel = texts?.attachmentCloseLabel ?? "Close preview";
3516
3634
  const categoryLabel = readFileCategoryLabel(category, texts);
3517
- const shellClasses = cn("block overflow-hidden rounded-[1.25rem] border transition duration-200", isUser ? "border-primary-foreground/12 bg-primary-foreground/10 text-primary-foreground" : "border-border bg-card text-card-foreground", isInteractive && (isUser ? "hover:border-primary-foreground/18 hover:bg-primary-foreground/13" : "hover:border-border-hover hover:bg-accent"));
3518
- if (renderAsImage) return renderImagePreview({
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,
3519
3639
  file,
3520
- categoryLabel,
3521
- sizeLabel,
3522
- isUser
3640
+ sizeLabel
3523
3641
  });
3524
3642
  if (renderAsAudio || renderAsVideo) return renderInlineMediaCard({
3525
3643
  category,
@@ -3528,7 +3646,8 @@ function ChatMessageFile({ file, isUser = false, texts }) {
3528
3646
  sizeLabel,
3529
3647
  isUser,
3530
3648
  shellClasses,
3531
- actionLabel
3649
+ actionLabel,
3650
+ onOpen
3532
3651
  });
3533
3652
  const content = renderFileCardHeader({
3534
3653
  category,
@@ -3542,6 +3661,12 @@ function ChatMessageFile({ file, isUser = false, texts }) {
3542
3661
  className: shellClasses,
3543
3662
  children: content
3544
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
+ });
3545
3670
  return /* @__PURE__ */ jsx("a", {
3546
3671
  href: file.dataUrl,
3547
3672
  target: "_blank",
@@ -3672,16 +3797,64 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3672
3797
  };
3673
3798
  }
3674
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
3675
3849
  //#region src/components/chat/ui/chat-message-list/chat-reasoning-block.tsx
3676
- function normalizeReasoningLabel(label) {
3677
- const normalizedLabel = label.trim().toLowerCase();
3678
- if (normalizedLabel === "reasoning" || normalizedLabel === "thinking" || normalizedLabel === "推理" || normalizedLabel === "推理过程" || normalizedLabel === "思考过程" || normalizedLabel === "思考") return "思考";
3679
- return label;
3850
+ function formatReasoningLabel(label, text, characterCountTemplate) {
3851
+ const characterCount = Array.from(text).length;
3852
+ return (characterCountTemplate ?? `${label} · {count}`).split("{count}").join(String(characterCount));
3680
3853
  }
3681
- function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
3854
+ function ChatReasoningBlock({ label, text, characterCountTemplates, isUser, isInProgress }) {
3682
3855
  const { isOpen, onSummaryClick } = useReasoningBlockOpenState({ isInProgress });
3683
3856
  const scrollRef = useRef(null);
3684
- const displayLabel = normalizeReasoningLabel(label);
3857
+ const displayLabel = formatReasoningLabel(label, text, isInProgress ? characterCountTemplates?.inProgress : characterCountTemplates?.completed);
3685
3858
  const { onScroll } = useStickyBottomScroll({
3686
3859
  scrollRef,
3687
3860
  resetKey: `${displayLabel}:${isInProgress ? "streaming" : "idle"}`,
@@ -3690,36 +3863,42 @@ function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
3690
3863
  contentVersion: text,
3691
3864
  stickyThresholdPx: 20
3692
3865
  });
3693
- return /* @__PURE__ */ jsxs("details", {
3694
- className: "mt-2",
3695
- open: isOpen,
3696
- children: [/* @__PURE__ */ jsx("summary", {
3697
- className: cn("cursor-pointer text-xs", isUser ? "text-primary-100" : "text-muted-foreground"),
3698
- onClick: onSummaryClick,
3699
- children: displayLabel
3700
- }), /* @__PURE__ */ jsx("div", {
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", {
3701
3875
  ref: scrollRef,
3702
3876
  onScroll,
3703
3877
  "data-reasoning-scroll": "true",
3704
- className: cn("mt-2 w-fit max-w-[500px] max-h-56 overflow-y-auto rounded-lg custom-scrollbar", isUser ? "bg-primary/70" : "bg-muted"),
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"),
3705
3879
  children: /* @__PURE__ */ jsx("pre", {
3706
3880
  className: "min-w-0 whitespace-pre-wrap break-all p-2 text-[11px]",
3707
3881
  children: text
3708
3882
  })
3709
- })]
3883
+ }) : null]
3710
3884
  });
3711
3885
  }
3712
3886
  //#endregion
3713
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
+ */
3714
3893
  function ToolCardRoot({ children, className }) {
3715
3894
  return /* @__PURE__ */ jsx("div", {
3716
- className: cn("my-2 rounded-lg border border-border bg-card shadow-sm overflow-hidden text-[12px] text-card-foreground", "w-[280px] sm:w-[360px] md:w-[480px] min-w-full max-w-full transition-all flex flex-col", className),
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),
3717
3896
  children
3718
3897
  });
3719
3898
  }
3720
3899
  function ToolCardContent({ children, className }) {
3721
3900
  return /* @__PURE__ */ jsx("div", {
3722
- className: cn("border-t border-border bg-muted/35 px-3 pt-1 pb-2 w-full overflow-hidden", className),
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),
3723
3902
  children
3724
3903
  });
3725
3904
  }
@@ -3727,12 +3906,12 @@ function ToolCardContent({ children, className }) {
3727
3906
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-status.tsx
3728
3907
  const STATUS_STYLES = {
3729
3908
  running: {
3730
- text: "text-primary/70",
3909
+ text: "text-muted-foreground",
3731
3910
  icon: Loader2,
3732
3911
  spin: true
3733
3912
  },
3734
3913
  success: {
3735
- text: "text-primary/70",
3914
+ text: "text-muted-foreground/75",
3736
3915
  icon: Check,
3737
3916
  spin: false
3738
3917
  },
@@ -3742,20 +3921,23 @@ const STATUS_STYLES = {
3742
3921
  spin: false
3743
3922
  },
3744
3923
  cancelled: {
3745
- text: "text-muted-foreground",
3924
+ text: "text-muted-foreground/70",
3746
3925
  icon: Minus,
3747
3926
  spin: false
3748
3927
  }
3749
3928
  };
3750
- function ToolStatusLabel({ card }) {
3929
+ function ToolStatusLabel({ card, iconOnly = false }) {
3751
3930
  const style = STATUS_STYLES[card.statusTone] || STATUS_STYLES.cancelled;
3752
3931
  const Icon = style.icon;
3932
+ const showLabel = !iconOnly && (card.statusTone === "running" || card.statusTone === "error");
3753
3933
  return /* @__PURE__ */ jsxs("span", {
3754
- 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,
3755
3936
  children: [/* @__PURE__ */ jsx(Icon, {
3756
- className: cn("h-3.5 w-3.5", style.spin && "animate-spin"),
3757
- strokeWidth: 3
3758
- }), card.statusTone === "running" ? card.statusLabel : null]
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]
3759
3941
  });
3760
3942
  }
3761
3943
  //#endregion
@@ -3764,12 +3946,12 @@ function resolveToolCardActionView(action) {
3764
3946
  if (action.kind === "show-content") return {
3765
3947
  icon: Eye,
3766
3948
  label: action.label,
3767
- 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"
3768
3950
  };
3769
3951
  return {
3770
3952
  icon: ArrowUpRight,
3771
3953
  label: action.label ?? (action.sessionKind === "child" ? "Open child session" : "Open session"),
3772
- toneClassName: "border-border bg-card text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-primary/35"
3954
+ toneClassName: "border-border/70 bg-transparent text-muted-foreground hover:bg-muted/60 hover:text-foreground focus-visible:ring-primary/35"
3773
3955
  };
3774
3956
  }
3775
3957
  function ToolCardActionButton({ action, onAction }) {
@@ -3787,12 +3969,12 @@ function ToolCardActionButton({ action, onAction }) {
3787
3969
  children: /* @__PURE__ */ jsx("button", {
3788
3970
  type: "button",
3789
3971
  onClick: handleClick,
3790
- className: cn("inline-flex h-7 w-7 items-center justify-center rounded-full border transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1", view.toneClassName),
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),
3791
3973
  "aria-label": view.label,
3792
3974
  title: view.label,
3793
3975
  children: /* @__PURE__ */ jsx(Icon, {
3794
3976
  className: "h-3.5 w-3.5",
3795
- strokeWidth: 2.5
3977
+ strokeWidth: 2.25
3796
3978
  })
3797
3979
  })
3798
3980
  }), /* @__PURE__ */ jsx(TooltipContent, {
@@ -3802,44 +3984,82 @@ function ToolCardActionButton({ action, onAction }) {
3802
3984
  })] })
3803
3985
  });
3804
3986
  }
3805
- 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 }) {
3806
4032
  const summaryPart = hideSummary ? "" : card.summary?.replace(/^(command|path|args|query|input):\s*/i, "") ?? "";
3807
- return /* @__PURE__ */ jsxs("div", {
3808
- className: cn("flex items-center gap-3 px-3 py-2.5 transition-colors bg-transparent", canExpand ? "cursor-pointer hover:bg-accent/70" : ""),
3809
- onClick: onToggle,
3810
- children: [/* @__PURE__ */ jsxs("div", {
3811
- className: "flex min-w-0 flex-1 items-center gap-2 overflow-hidden font-mono text-foreground",
3812
- children: [/* @__PURE__ */ jsx(Icon, {
3813
- className: "h-4 w-4 text-primary/70 shrink-0",
3814
- strokeWidth: 3
3815
- }), /* @__PURE__ */ jsxs("div", {
3816
- className: "flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden",
3817
- children: [/* @__PURE__ */ jsx("span", {
3818
- className: "font-bold shrink-0 tracking-tight",
3819
- children: card.toolName
3820
- }), summaryPart && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
3821
- className: "text-muted-foreground/45 font-bold select-none shrink-0",
3822
- children: "›"
3823
- }), /* @__PURE__ */ jsx("span", {
3824
- className: "block min-w-0 flex-1 truncate font-normal text-muted-foreground",
3825
- title: summaryPart,
3826
- children: summaryPart
3827
- })] })]
3828
- })]
3829
- }), /* @__PURE__ */ jsxs("div", {
3830
- className: "flex items-center gap-3 shrink-0",
3831
- children: [
3832
- actionSlot,
3833
- /* @__PURE__ */ jsx(ToolStatusLabel, { card }),
3834
- canExpand && (expanded ? /* @__PURE__ */ jsx(ChevronDown, {
3835
- className: "h-4 w-4 text-muted-foreground",
3836
- strokeWidth: 3
3837
- }) : /* @__PURE__ */ jsx(ChevronRight, {
3838
- className: "h-4 w-4 text-muted-foreground",
3839
- strokeWidth: 3
3840
- }))
3841
- ]
3842
- })]
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
+ ]
3843
4063
  });
3844
4064
  }
3845
4065
  function ToolCardHeaderAction({ action, onAction }) {
@@ -4082,7 +4302,7 @@ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
4082
4302
  className: cn("overflow-hidden bg-card", !isFirst && "border-t border-border"),
4083
4303
  children: [
4084
4304
  showMetaRow ? /* @__PURE__ */ jsxs("div", {
4085
- className: cn("flex items-center justify-between gap-4 border-b border-border px-4 text-muted-foreground", showPathRow ? "py-2" : "py-1.5"),
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"),
4086
4306
  children: [/* @__PURE__ */ jsx("div", {
4087
4307
  className: "min-w-0 flex-1",
4088
4308
  children: showPathRow ? /* @__PURE__ */ jsx(TooltipProvider, {
@@ -4092,7 +4312,7 @@ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
4092
4312
  children: /* @__PURE__ */ jsx("button", {
4093
4313
  type: "button",
4094
4314
  onClick: handlePathClick,
4095
- className: cn("w-full truncate whitespace-nowrap text-left font-mono text-[12px] font-medium text-foreground", onFileOpen && "transition-colors hover:text-primary hover:underline"),
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"),
4096
4316
  title: block.path,
4097
4317
  children: block.path
4098
4318
  })
@@ -4118,28 +4338,28 @@ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
4118
4338
  children: /* @__PURE__ */ jsx("div", {
4119
4339
  className: "overflow-x-auto custom-scrollbar",
4120
4340
  children: /* @__PURE__ */ jsx("pre", {
4121
- className: "min-w-max whitespace-pre bg-card px-4 py-2 font-mono text-[11px] leading-5 text-foreground",
4341
+ className: "min-w-max whitespace-pre bg-card px-3 py-2 font-mono text-[11px] leading-5 text-foreground",
4122
4342
  children: block.rawText
4123
4343
  })
4124
4344
  })
4125
4345
  }) : null,
4126
4346
  block.truncated && !previewBlock ? /* @__PURE__ */ jsx("div", {
4127
- 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",
4128
4348
  children: "Showing a shortened diff preview."
4129
4349
  }) : null
4130
4350
  ]
4131
4351
  });
4132
4352
  }
4133
- function ToolCardFileOperationContent({ card, className, onFileOpen }) {
4353
+ function ToolCardFileOperationContent({ card, className, onFileOpen, showPathRow = true }) {
4134
4354
  const blocks = card.fileOperation?.blocks ?? [];
4135
4355
  const output = card.output?.trim() ?? "";
4136
4356
  if (blocks.length === 0 && !output) return null;
4137
4357
  return /* @__PURE__ */ jsxs("div", {
4138
- className: cn("overflow-hidden bg-card", className),
4358
+ className: cn("overflow-hidden rounded-md border border-border/70 bg-card", className),
4139
4359
  children: [blocks.map((block, index) => {
4140
4360
  return /* @__PURE__ */ jsx(FileOperationBlock, {
4141
4361
  block,
4142
- showPathRow: true,
4362
+ showPathRow,
4143
4363
  isFirst: index === 0,
4144
4364
  onFileOpen
4145
4365
  }, block.key);
@@ -4151,7 +4371,7 @@ function ToolCardFileOperationContent({ card, className, onFileOpen }) {
4151
4371
  children: /* @__PURE__ */ jsx("div", {
4152
4372
  className: "overflow-x-auto custom-scrollbar",
4153
4373
  children: /* @__PURE__ */ jsx("pre", {
4154
- className: "min-w-max whitespace-pre bg-card px-4 py-2 font-mono text-[11px] leading-5 text-foreground",
4374
+ className: "min-w-max whitespace-pre bg-card px-3 py-2 font-mono text-[11px] leading-5 text-foreground",
4155
4375
  children: output
4156
4376
  })
4157
4377
  })
@@ -4159,9 +4379,279 @@ function ToolCardFileOperationContent({ card, className, onFileOpen }) {
4159
4379
  });
4160
4380
  }
4161
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
4162
4653
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-views.tsx
4163
4654
  const TOOL_CARD_AUTO_EXPAND_DELAY_MS = 200;
4164
- const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
4165
4655
  function readNonEmptyString(value) {
4166
4656
  if (typeof value !== "string") return null;
4167
4657
  return value.length > 0 ? value : null;
@@ -4169,9 +4659,6 @@ function readNonEmptyString(value) {
4169
4659
  function isRecord(value) {
4170
4660
  return typeof value === "object" && value !== null && !Array.isArray(value);
4171
4661
  }
4172
- function stripAnsi(value) {
4173
- return value.replace(ANSI_ESCAPE_PATTERN, "");
4174
- }
4175
4662
  function isStructuredTerminalRecord(record) {
4176
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;
4177
4664
  }
@@ -4186,26 +4673,35 @@ function extractTerminalOutputFromRecord(record) {
4186
4673
  function normalizeTerminalOutput(rawOutput, structuredOutput) {
4187
4674
  if (isRecord(structuredOutput)) {
4188
4675
  const terminalOutput = extractTerminalOutputFromRecord(structuredOutput);
4189
- if (terminalOutput) return stripAnsi(terminalOutput);
4676
+ if (terminalOutput) return terminalOutput;
4190
4677
  if (isStructuredTerminalRecord(structuredOutput)) return "";
4191
4678
  }
4192
4679
  if (!rawOutput) return "";
4193
4680
  const trimmed = rawOutput.trim();
4194
- if (!trimmed.startsWith("{")) return stripAnsi(rawOutput);
4681
+ if (!trimmed.startsWith("{")) return rawOutput;
4195
4682
  try {
4196
4683
  const parsed = JSON.parse(trimmed);
4197
- if (!isRecord(parsed)) return stripAnsi(rawOutput);
4684
+ if (!isRecord(parsed)) return rawOutput;
4198
4685
  const terminalOutput = extractTerminalOutputFromRecord(parsed);
4199
- if (terminalOutput) return stripAnsi(terminalOutput);
4686
+ if (terminalOutput) return terminalOutput;
4200
4687
  if (isStructuredTerminalRecord(parsed)) return "";
4201
- return stripAnsi(rawOutput);
4688
+ return rawOutput;
4202
4689
  } catch {
4203
- return stripAnsi(rawOutput);
4690
+ return rawOutput;
4204
4691
  }
4205
4692
  }
4206
4693
  function shouldAutoExpandRunningFileOperation(toolName) {
4207
4694
  return toolName === "write_file" || toolName === "edit_file";
4208
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
+ }
4209
4705
  function useToolCardExpandedState({ canExpand, isRunning, autoExpandWhileRunning = true, expandOnError = false, statusTone }) {
4210
4706
  const [expanded, setExpanded] = useState(false);
4211
4707
  const [hasUserToggled, setHasUserToggled] = useState(false);
@@ -4264,47 +4760,20 @@ function useToolCardExpandedState({ canExpand, isRunning, autoExpandWhileRunning
4264
4760
  onToggle
4265
4761
  };
4266
4762
  }
4267
- function GenericToolSection({ label, tone, children }) {
4268
- const style = {
4269
- input: {
4270
- shell: "border-border bg-card",
4271
- header: "border-border bg-muted/55 text-muted-foreground",
4272
- dot: "bg-muted-foreground/60",
4273
- body: "text-foreground"
4274
- },
4275
- output: {
4276
- shell: "border-border bg-card",
4277
- header: "border-border bg-muted/55 text-muted-foreground",
4278
- dot: "bg-primary/70",
4279
- body: "text-foreground"
4280
- },
4281
- error: {
4282
- shell: "border-rose-200/80 bg-rose-50/85",
4283
- header: "border-rose-200/80 bg-rose-100/80 text-rose-700",
4284
- dot: "bg-rose-500/80",
4285
- body: "text-rose-950/85"
4286
- }
4287
- }[tone];
4288
- return /* @__PURE__ */ jsxs("section", {
4289
- className: cn("overflow-hidden rounded-md border shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]", style.shell),
4290
- children: [/* @__PURE__ */ jsxs("div", {
4291
- className: cn("flex items-center gap-2 border-b px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.16em]", style.header),
4292
- children: [/* @__PURE__ */ jsx("span", { className: cn("h-1.5 w-1.5 rounded-full", style.dot) }), /* @__PURE__ */ jsx("span", { children: label })]
4293
- }), /* @__PURE__ */ jsx("div", {
4294
- className: "w-full overflow-hidden",
4295
- children: /* @__PURE__ */ jsx("pre", {
4296
- className: cn("w-full max-w-full min-w-0 max-h-64 overflow-x-auto overflow-y-auto px-3 py-2.5 font-mono text-[12px] leading-relaxed whitespace-pre custom-scrollbar", style.body),
4297
- children
4298
- })
4299
- })]
4300
- });
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
+ };
4301
4769
  }
4302
- function TerminalExecutionView({ card }) {
4770
+ function TerminalExecutionView({ card, toolLabel }) {
4303
4771
  const output = normalizeTerminalOutput(card.output, card.outputData);
4304
4772
  const isRunning = card.statusTone === "running";
4305
4773
  const commandPart = card.summary?.replace(/^(command|path|args|query|input):\s*/i, "");
4306
4774
  const hasOutput = output.trim().length > 0;
4307
4775
  const canExpand = isRunning || hasOutput || Boolean(commandPart?.trim());
4776
+ const meta = extractTerminalMeta(card.outputData);
4308
4777
  const { expanded, onToggle } = useToolCardExpandedState({
4309
4778
  canExpand,
4310
4779
  isRunning,
@@ -4314,38 +4783,29 @@ function TerminalExecutionView({ card }) {
4314
4783
  });
4315
4784
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4316
4785
  card,
4786
+ toolLabel,
4317
4787
  icon: Terminal,
4318
4788
  expanded,
4319
4789
  canExpand,
4320
4790
  onToggle
4321
- }), expanded && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
4322
- className: "px-3 pb-2 font-mono w-full max-h-48 overflow-y-auto custom-scrollbar min-h-0 text-[12px]",
4323
- children: /* @__PURE__ */ jsxs("div", {
4324
- className: "flex items-start gap-2 leading-relaxed",
4325
- children: [/* @__PURE__ */ jsx("span", {
4326
- className: "text-primary/55 font-medium shrink-0 select-none mt-[1px]",
4327
- children: "$"
4328
- }), /* @__PURE__ */ jsx("div", {
4329
- className: "flex-1 min-w-0",
4330
- children: commandPart ? /* @__PURE__ */ jsxs("div", {
4331
- className: "text-foreground break-words whitespace-pre-wrap tracking-tight font-medium inline-block",
4332
- children: [commandPart, isRunning && !output && /* @__PURE__ */ jsx("span", { className: "inline-block w-1.5 h-3 ml-1 bg-primary/60 animate-pulse align-middle" })]
4333
- }) : /* @__PURE__ */ jsx("div", { className: "h-3 w-32 bg-muted rounded animate-pulse mt-2" })
4334
- })]
4335
- })
4336
- }), (hasOutput || !isRunning) && /* @__PURE__ */ jsx(ToolCardContent, {
4337
- className: hasOutput ? void 0 : "bg-muted/20 py-2",
4338
- children: /* @__PURE__ */ jsxs("pre", {
4339
- className: cn("font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed", hasOutput ? "px-0" : "rounded-md border border-dashed border-border bg-card/70 px-3 py-2 italic"),
4340
- children: [hasOutput ? output : card.emptyLabel, isRunning && hasOutput && /* @__PURE__ */ jsx("span", { className: "inline-block w-1.5 h-3 ml-1 bg-primary/60 animate-pulse align-middle" })]
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
4341
4802
  })
4342
- })] })] });
4803
+ })] });
4343
4804
  }
4344
- function FileOperationView({ card, onFileOpen }) {
4805
+ function FileOperationView({ card, toolLabel, onFileOpen }) {
4345
4806
  const output = card.output?.trim() ?? "";
4346
4807
  const isRunning = card.statusTone === "running";
4347
- const hasStructuredPreview = Boolean(card.fileOperation?.blocks.length);
4348
- const hasContent = hasStructuredPreview || Boolean(output);
4808
+ const hasContent = Boolean(card.fileOperation?.blocks.length) || Boolean(output);
4349
4809
  const previewBlocks = card.fileOperation?.blocks ?? [];
4350
4810
  const previewLineCount = previewBlocks.reduce((count, block) => count + block.lines.length, 0);
4351
4811
  const previewCharCount = previewBlocks.reduce((count, block) => {
@@ -4360,22 +4820,26 @@ function FileOperationView({ card, onFileOpen }) {
4360
4820
  expandOnError: hasContent,
4361
4821
  statusTone: card.statusTone
4362
4822
  });
4823
+ const isEdit = card.toolName === "edit_file" || card.toolName === "write_file" || card.toolName === "apply_patch" || card.toolName === "file_change";
4363
4824
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4364
4825
  card,
4365
- 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,
4366
4829
  expanded,
4367
4830
  canExpand: hasContent || isRunning,
4368
- hideSummary: expanded && hasStructuredPreview,
4831
+ hideSummary: false,
4369
4832
  onToggle
4370
- }), expanded && hasContent && /* @__PURE__ */ jsx(ToolCardContent, {
4371
- 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",
4372
4835
  children: /* @__PURE__ */ jsx(ToolCardFileOperationContent, {
4373
4836
  card,
4374
- onFileOpen
4837
+ onFileOpen,
4838
+ showPathRow: true
4375
4839
  })
4376
- })] });
4840
+ }) : null] });
4377
4841
  }
4378
- function SearchSnippetView({ card }) {
4842
+ function SearchSnippetView({ card, toolLabel, icon: Icon = Search }) {
4379
4843
  const isRunning = card.statusTone === "running";
4380
4844
  const output = card.output?.trim() ?? "";
4381
4845
  const { expanded, onToggle } = useToolCardExpandedState({
@@ -4386,24 +4850,69 @@ function SearchSnippetView({ card }) {
4386
4850
  });
4387
4851
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4388
4852
  card,
4389
- icon: Search,
4853
+ toolLabel,
4854
+ icon: Icon,
4390
4855
  expanded,
4391
4856
  canExpand: !!output || isRunning,
4392
4857
  onToggle
4393
- }), expanded && output && /* @__PURE__ */ jsx(ToolCardContent, { children: /* @__PURE__ */ jsx("pre", {
4394
- className: "font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed",
4395
- children: output
4396
- }) })] });
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
+ })] });
4397
4865
  }
4398
- 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 }) {
4399
4909
  const input = card.input?.trim() ?? "";
4400
4910
  const output = card.output?.trim() ?? "";
4401
4911
  const isRunning = card.statusTone === "running";
4402
4912
  const hasInputSection = input.length > 0;
4403
4913
  const hasOutputSection = output.length > 0;
4404
4914
  const hasContent = hasInputSection || hasOutputSection;
4405
- const inputLabel = card.inputLabel?.trim() || "Input";
4406
- const outputLabel = card.outputLabel?.trim() || "Output";
4915
+ const actionSlot = buildToolActionSlot(card, onToolAction, renderToolAgent);
4407
4916
  const { expanded, onToggle } = useToolCardExpandedState({
4408
4917
  canExpand: hasContent || isRunning,
4409
4918
  isRunning,
@@ -4412,30 +4921,28 @@ function GenericToolCard({ card, onToolAction, renderToolAgent }) {
4412
4921
  });
4413
4922
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4414
4923
  card,
4415
- icon: Globe,
4924
+ toolLabel,
4925
+ icon: Icon,
4416
4926
  expanded,
4417
4927
  canExpand: hasContent || isRunning,
4418
- actionSlot: card.agentId || card.action && onToolAction ? /* @__PURE__ */ jsxs(Fragment$1, { children: [card.agentId && renderToolAgent ? renderToolAgent(card.agentId) : null, card.action && onToolAction ? /* @__PURE__ */ jsx(ToolCardHeaderAction, {
4419
- action: card.action,
4420
- onAction: onToolAction
4421
- }) : null] }) : void 0,
4928
+ actionSlot,
4422
4929
  onToggle
4423
- }), expanded && hasContent && /* @__PURE__ */ jsxs(ToolCardContent, {
4424
- className: "bg-transparent px-2.5 py-2.5",
4930
+ }), expanded && hasContent ? /* @__PURE__ */ jsxs(ToolCardContent, {
4931
+ className: "bg-transparent py-0",
4425
4932
  children: [
4426
- hasInputSection && /* @__PURE__ */ jsx(GenericToolSection, {
4427
- label: inputLabel,
4933
+ hasInputSection ? /* @__PURE__ */ jsx(GenericToolSection, {
4934
+ label: card.inputLabel?.trim() || "Input",
4428
4935
  tone: "input",
4429
4936
  children: input
4430
- }),
4431
- hasInputSection && hasOutputSection && /* @__PURE__ */ jsx("div", { className: "h-2" }),
4432
- hasOutputSection && /* @__PURE__ */ jsx(GenericToolSection, {
4433
- 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",
4434
4941
  tone: card.statusTone === "error" ? "error" : "output",
4435
4942
  children: output
4436
- })
4943
+ }) : null
4437
4944
  ]
4438
- })] });
4945
+ }) : null] });
4439
4946
  }
4440
4947
  //#endregion
4441
4948
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-panel-app.tsx
@@ -4457,34 +4964,370 @@ function isTerminalTool(name) {
4457
4964
  const lowered = name.toLowerCase();
4458
4965
  return lowered === "exec" || lowered === "exec_command" || lowered === "execute_command" || lowered === "command_execution" || lowered === "bash" || lowered === "shell" || lowered.includes("run_");
4459
4966
  }
4460
- function isFileEditTool(name) {
4967
+ function isFileTool(name) {
4461
4968
  const lowered = name.toLowerCase();
4462
4969
  return lowered === "read_file" || lowered === "write_file" || lowered === "edit_file" || lowered === "apply_patch" || lowered === "file_change";
4463
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
+ }
4464
4975
  function isSearchTool(name) {
4465
4976
  const lowered = name.toLowerCase();
4466
4977
  return lowered === "grep_search" || lowered === "find_files" || lowered.includes("search");
4467
4978
  }
4468
- 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 }) {
4469
5057
  if (card.panelApp) return /* @__PURE__ */ jsx(PanelAppInlineToolCard, {
4470
5058
  card,
4471
5059
  onToolAction,
4472
5060
  renderPanelAppCard
4473
5061
  });
4474
- if (isTerminalTool(card.toolName)) return /* @__PURE__ */ jsx(TerminalExecutionView, { card });
4475
- 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, {
4476
5065
  card,
5066
+ toolLabel: toolStatusLabels?.terminal[card.statusTone]
5067
+ });
5068
+ if (isFileTool(card.toolName)) return /* @__PURE__ */ jsx(FileOperationView, {
5069
+ card,
5070
+ toolLabel: isFileWriteTool(card.toolName) ? toolStatusLabels?.fileEdit[card.statusTone] : toolStatusLabels?.fileRead[card.statusTone],
4477
5071
  onFileOpen
4478
5072
  });
4479
- 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
+ });
4480
5078
  return /* @__PURE__ */ jsx(GenericToolCard, {
4481
5079
  card,
5080
+ toolLabel: builtInLabel,
5081
+ icon: builtInPresentation?.icon,
4482
5082
  onToolAction,
4483
5083
  renderToolAgent
4484
5084
  });
4485
5085
  }
4486
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
4487
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
+ }
4488
5331
  function isMessageInProgress(status) {
4489
5332
  return status === "pending" || status === "streaming";
4490
5333
  }
@@ -4504,7 +5347,7 @@ function splitAssistantProcess(message) {
4504
5347
  finalParts: message.parts.slice(lastProcessPartIndex + 1)
4505
5348
  };
4506
5349
  }
4507
- 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 }) {
4508
5351
  const { type } = part;
4509
5352
  if (type === "markdown") {
4510
5353
  const { inlineTokens, text } = part;
@@ -4514,6 +5357,7 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4514
5357
  texts,
4515
5358
  inlineTokens,
4516
5359
  onFileOpen,
5360
+ onInlineTokenClick,
4517
5361
  renderInlineDisplay
4518
5362
  }, `markdown-${index}`);
4519
5363
  }
@@ -4522,29 +5366,29 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4522
5366
  return /* @__PURE__ */ jsx(ChatReasoningBlock, {
4523
5367
  label,
4524
5368
  text,
5369
+ characterCountTemplates: texts.reasoningCharacterCountTemplates,
4525
5370
  isUser,
4526
5371
  isInProgress: isInProgress && isLastPart
4527
5372
  }, `reasoning-${index}`);
4528
5373
  }
4529
5374
  if (type === "tool-card") {
4530
5375
  const { card } = part;
4531
- return /* @__PURE__ */ jsx("div", {
4532
- className: "mt-0.5",
4533
- children: /* @__PURE__ */ jsx(ChatToolCard, {
4534
- card,
4535
- onToolAction,
4536
- onFileOpen,
4537
- renderToolAgent,
4538
- renderPanelAppCard
4539
- })
4540
- }, `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}`);
4541
5384
  }
4542
5385
  if (type === "file") {
4543
5386
  const { file } = part;
4544
5387
  return /* @__PURE__ */ jsx(ChatMessageFile, {
4545
5388
  file,
4546
5389
  isUser,
4547
- texts
5390
+ texts,
5391
+ onOpen: onAttachmentOpen
4548
5392
  }, `file-${index}`);
4549
5393
  }
4550
5394
  if (type === "unknown") {
@@ -4566,49 +5410,102 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFile
4566
5410
  }
4567
5411
  return null;
4568
5412
  }
4569
- 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 }) {
4570
5445
  const { role } = message;
4571
5446
  const isUser = role === "user";
4572
5447
  const isInProgress = isMessageInProgress(message.status);
4573
5448
  const processSplit = splitAssistantProcess(message);
4574
- const renderPart = (part, index) => renderChatMessagePart({
4575
- part,
4576
- index,
4577
- role,
4578
- isUser,
4579
- isInProgress,
4580
- isLastPart: index === message.parts.length - 1,
4581
- texts,
4582
- onToolAction,
4583
- onFileOpen,
4584
- renderInlineDisplay,
4585
- renderToolAgent,
4586
- renderPanelAppCard
4587
- });
5449
+ const [processOpen, setProcessOpen] = useState(false);
4588
5450
  return /* @__PURE__ */ jsx("div", {
4589
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"),
4590
5452
  children: /* @__PURE__ */ jsx("div", {
4591
- className: "space-y-2",
4592
- children: processSplit ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("details", {
4593
- className: "group/process text-xs text-muted-foreground",
4594
- children: [/* @__PURE__ */ jsxs("summary", {
4595
- className: "flex cursor-pointer list-none items-center gap-1.5 border-b border-border/60 pb-2 font-medium transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 [&::-webkit-details-marker]:hidden",
4596
- children: [
4597
- /* @__PURE__ */ jsx(ChevronRight, {
4598
- className: "h-3.5 w-3.5 group-open/process:hidden",
4599
- strokeWidth: 2.5
4600
- }),
4601
- /* @__PURE__ */ jsx(ChevronDown, {
4602
- className: "hidden h-3.5 w-3.5 group-open/process:block",
4603
- strokeWidth: 2.5
4604
- }),
4605
- /* @__PURE__ */ jsx("span", { children: message.processSummary?.label })
4606
- ]
4607
- }), /* @__PURE__ */ jsx("div", {
4608
- className: "space-y-2 pt-2",
4609
- children: processSplit.processParts.map(renderPart)
4610
- })]
4611
- }), processSplit.finalParts.map((part, index) => renderPart(part, processSplit.processParts.length + index))] }) : message.parts.map(renderPart)
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
+ })
4612
5509
  })
4613
5510
  });
4614
5511
  });
@@ -4708,7 +5605,7 @@ function ChatTypingIndicator({ label }) {
4708
5605
  })]
4709
5606
  });
4710
5607
  }
4711
- 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 }) {
4712
5609
  const visibleMessages = messages.filter(hasRenderableMessageContent);
4713
5610
  const hasRenderableAssistantDraft = visibleMessages.some((message) => message.role === "assistant" && (message.status === "streaming" || message.status === "pending"));
4714
5611
  return /* @__PURE__ */ jsxs("div", {
@@ -4727,6 +5624,8 @@ function ChatMessageList({ className, isSending, messages, onFileOpen, onToolAct
4727
5624
  texts,
4728
5625
  onToolAction,
4729
5626
  onFileOpen,
5627
+ onAttachmentOpen,
5628
+ onInlineTokenClick,
4730
5629
  renderInlineDisplay,
4731
5630
  renderToolAgent,
4732
5631
  renderPanelAppCard