@nextclaw/agent-chat-ui 0.2.18 → 0.2.20

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.d.ts CHANGED
@@ -151,17 +151,38 @@ type ChatInputBarProps = {
151
151
  toolbar: ChatInputBarToolbarProps;
152
152
  };
153
153
  type ChatMessageRole = "user" | "assistant" | "tool" | "system" | "message";
154
+ type ChatFileOperationLineViewModel = {
155
+ kind: "context" | "add" | "remove";
156
+ text: string;
157
+ oldLineNumber?: number;
158
+ newLineNumber?: number;
159
+ };
160
+ type ChatFileOperationBlockViewModel = {
161
+ key: string;
162
+ path: string;
163
+ display?: "preview" | "diff";
164
+ caption?: string;
165
+ lines: ChatFileOperationLineViewModel[];
166
+ rawText?: string;
167
+ truncated?: boolean;
168
+ };
154
169
  type ChatToolPartViewModel = {
155
170
  kind: "call" | "result";
156
171
  toolName: string;
157
172
  summary?: string;
173
+ inputLabel?: string;
174
+ input?: string;
158
175
  output?: string;
176
+ outputData?: unknown;
159
177
  hasResult: boolean;
160
178
  statusTone: "running" | "success" | "error" | "cancelled";
161
179
  statusLabel: string;
162
180
  titleLabel: string;
163
181
  outputLabel: string;
164
182
  emptyLabel: string;
183
+ fileOperation?: {
184
+ blocks: ChatFileOperationBlockViewModel[];
185
+ };
165
186
  };
166
187
  type ChatInlineTokenViewModel = {
167
188
  kind: string;
@@ -299,4 +320,4 @@ declare function resolveChatComposerSlashTrigger(nodes: ChatComposerNode[], sele
299
320
  end: number;
300
321
  } | null;
301
322
 
302
- export { type ChatComposerNode, type ChatComposerSelection, type ChatComposerTextNode, type ChatComposerTokenKind, type ChatComposerTokenNode, type ChatInlineContentSegmentViewModel, type ChatInlineHint, type ChatInlineTokenViewModel, ChatInputBar, type ChatInputBarActionsProps, type ChatInputBarHandle, type ChatInputBarProps, type ChatInputBarToolbarProps, ChatMessageList, type ChatMessageListProps, type ChatMessagePartViewModel, type ChatMessageRole, type ChatMessageTexts, type ChatMessageViewModel, type ChatSelectedItem, type ChatSkillPickerOption, type ChatSkillPickerProps, type ChatSlashItem, type ChatSlashMenuProps, type ChatTexts, type ChatToolPartViewModel, type ChatToolbarAccessory, type ChatToolbarAccessoryIcon, type ChatToolbarIcon, type ChatToolbarSelect, type ChatToolbarSelectGroup, type ChatToolbarSelectOption, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
323
+ export { type ChatComposerNode, type ChatComposerSelection, type ChatComposerTextNode, type ChatComposerTokenKind, type ChatComposerTokenNode, type ChatFileOperationBlockViewModel, type ChatFileOperationLineViewModel, type ChatInlineContentSegmentViewModel, type ChatInlineHint, type ChatInlineTokenViewModel, ChatInputBar, type ChatInputBarActionsProps, type ChatInputBarHandle, type ChatInputBarProps, type ChatInputBarToolbarProps, ChatMessageList, type ChatMessageListProps, type ChatMessagePartViewModel, type ChatMessageRole, type ChatMessageTexts, type ChatMessageViewModel, type ChatSelectedItem, type ChatSkillPickerOption, type ChatSkillPickerProps, type ChatSlashItem, type ChatSlashMenuProps, type ChatTexts, type ChatToolPartViewModel, type ChatToolbarAccessory, type ChatToolbarAccessoryIcon, type ChatToolbarIcon, type ChatToolbarSelect, type ChatToolbarSelectGroup, type ChatToolbarSelectOption, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
package/dist/index.js CHANGED
@@ -2314,8 +2314,8 @@ function ChatInlineTokenBadge({
2314
2314
  "span",
2315
2315
  {
2316
2316
  className: cn(
2317
- "mx-[2px] inline-flex h-7 max-w-full items-center gap-1.5 rounded-lg border px-2 align-baseline text-[11px] font-medium",
2318
- isSkill ? isUser ? "border-white/25 bg-white/16 text-white" : "border-primary/12 bg-primary/8 text-primary" : isUser ? "border-white/20 bg-white/12 text-white" : "border-slate-200/80 bg-slate-50 text-slate-700"
2317
+ "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)]",
2318
+ isSkill ? isUser ? "border-emerald-200/35 bg-emerald-400/18 text-emerald-50/95" : "border-emerald-200/70 bg-emerald-50 text-emerald-700" : isUser ? "border-white/30 bg-white/18 text-white" : "border-slate-200/80 bg-slate-100 text-slate-700"
2319
2319
  ),
2320
2320
  title: label,
2321
2321
  children: [
@@ -2324,7 +2324,7 @@ function ChatInlineTokenBadge({
2324
2324
  {
2325
2325
  className: cn(
2326
2326
  "inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center",
2327
- isSkill ? isUser ? "text-white/75" : "text-primary/70" : isUser ? "text-white/70" : "text-slate-500"
2327
+ isSkill ? isUser ? "text-emerald-100/90" : "text-emerald-600" : isUser ? "text-white/70" : "text-slate-500"
2328
2328
  ),
2329
2329
  children: /* @__PURE__ */ jsx15(ChatInlineSkillIcon, {})
2330
2330
  }
@@ -2752,7 +2752,7 @@ function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
2752
2752
 
2753
2753
  // src/components/chat/ui/chat-message-list/tool-card/tool-card-views.tsx
2754
2754
  import { Terminal, FileText, Code2, Search as Search2, Globe } from "lucide-react";
2755
- import { useState as useState7, useEffect as useEffect7, useRef as useRef6 } from "react";
2755
+ import { useState as useState7, useEffect as useEffect8, useRef as useRef8 } from "react";
2756
2756
 
2757
2757
  // src/components/chat/ui/chat-message-list/tool-card/tool-card-root.tsx
2758
2758
  import { jsx as jsx18 } from "react/jsx-runtime";
@@ -2801,9 +2801,10 @@ function ToolCardHeader({
2801
2801
  icon: Icon2,
2802
2802
  expanded,
2803
2803
  canExpand,
2804
+ hideSummary = false,
2804
2805
  onToggle
2805
2806
  }) {
2806
- const summaryPart = card.summary?.replace(/^(command|path|args|query|input):\s*/i, "");
2807
+ const summaryPart = hideSummary ? "" : card.summary?.replace(/^(command|path|args|query|input):\s*/i, "") ?? "";
2807
2808
  return /* @__PURE__ */ jsxs12(
2808
2809
  "div",
2809
2810
  {
@@ -2832,56 +2833,607 @@ function ToolCardHeader({
2832
2833
  );
2833
2834
  }
2834
2835
 
2835
- // src/components/chat/ui/chat-message-list/tool-card/tool-card-views.tsx
2836
- import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
2837
- function TerminalExecutionView({ card }) {
2836
+ // src/components/chat/hooks/use-sticky-bottom-scroll.ts
2837
+ import { useEffect as useEffect7, useLayoutEffect as useLayoutEffect2, useRef as useRef6 } from "react";
2838
+ var DEFAULT_STICKY_THRESHOLD_PX = 10;
2839
+ function scrollElementToBottom(element) {
2840
+ element.scrollTop = element.scrollHeight;
2841
+ }
2842
+ function queueScrollToBottom(params) {
2843
+ const element = params.scrollRef.current;
2844
+ if (!element) {
2845
+ return;
2846
+ }
2847
+ if (params.scheduledScrollFrameRef.current !== null) {
2848
+ cancelAnimationFrame(params.scheduledScrollFrameRef.current);
2849
+ }
2850
+ params.scheduledScrollFrameRef.current = requestAnimationFrame(() => {
2851
+ params.scheduledScrollFrameRef.current = null;
2852
+ const currentElement = params.scrollRef.current;
2853
+ if (!currentElement) {
2854
+ return;
2855
+ }
2856
+ params.isProgrammaticScrollRef.current = true;
2857
+ scrollElementToBottom(currentElement);
2858
+ });
2859
+ }
2860
+ function useStickyBottomScroll(params) {
2861
+ const isStickyRef = useRef6(true);
2862
+ const isProgrammaticScrollRef = useRef6(false);
2863
+ const previousResetKeyRef = useRef6(null);
2864
+ const pendingInitialScrollRef = useRef6(false);
2865
+ const scheduledScrollFrameRef = useRef6(null);
2866
+ const onScroll = () => {
2867
+ if (isProgrammaticScrollRef.current) {
2868
+ isProgrammaticScrollRef.current = false;
2869
+ return;
2870
+ }
2871
+ const element = params.scrollRef.current;
2872
+ if (!element) {
2873
+ return;
2874
+ }
2875
+ const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
2876
+ isStickyRef.current = distanceFromBottom <= (params.stickyThresholdPx ?? DEFAULT_STICKY_THRESHOLD_PX);
2877
+ };
2878
+ useEffect7(() => {
2879
+ if (previousResetKeyRef.current === params.resetKey) {
2880
+ return;
2881
+ }
2882
+ previousResetKeyRef.current = params.resetKey;
2883
+ isStickyRef.current = true;
2884
+ pendingInitialScrollRef.current = true;
2885
+ }, [params.resetKey]);
2886
+ useEffect7(() => {
2887
+ const scheduledScrollFrame = scheduledScrollFrameRef.current;
2888
+ return () => {
2889
+ if (scheduledScrollFrame !== null) {
2890
+ cancelAnimationFrame(scheduledScrollFrame);
2891
+ }
2892
+ };
2893
+ }, []);
2894
+ useLayoutEffect2(() => {
2895
+ if (!pendingInitialScrollRef.current || params.isLoading || !params.hasContent) {
2896
+ return;
2897
+ }
2898
+ const element = params.scrollRef.current;
2899
+ if (!element) {
2900
+ return;
2901
+ }
2902
+ pendingInitialScrollRef.current = false;
2903
+ queueScrollToBottom({
2904
+ scrollRef: params.scrollRef,
2905
+ scheduledScrollFrameRef,
2906
+ isProgrammaticScrollRef
2907
+ });
2908
+ }, [params.hasContent, params.isLoading, params.scrollRef]);
2909
+ useLayoutEffect2(() => {
2910
+ if (!isStickyRef.current || !params.hasContent) {
2911
+ return;
2912
+ }
2913
+ const element = params.scrollRef.current;
2914
+ if (!element) {
2915
+ return;
2916
+ }
2917
+ queueScrollToBottom({
2918
+ scrollRef: params.scrollRef,
2919
+ scheduledScrollFrameRef,
2920
+ isProgrammaticScrollRef
2921
+ });
2922
+ }, [params.contentVersion, params.hasContent, params.scrollRef]);
2923
+ return { onScroll };
2924
+ }
2925
+
2926
+ // src/components/chat/ui/chat-message-list/tool-card/tool-card-file-operation.tsx
2927
+ import { Fragment as Fragment4, useRef as useRef7 } from "react";
2928
+
2929
+ // src/components/chat/ui/chat-message-list/tool-card/tool-card-file-operation-lines.tsx
2930
+ import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
2931
+ var FILE_ROW_CLASS_NAME = "h-5 font-mono text-[11px] leading-5";
2932
+ var FILE_GUTTER_NUMBER_CELL_CLASS_NAME = "sticky left-0 z-[1] flex h-5 items-center justify-center px-2.5 tabular-nums select-none";
2933
+ function formatLineNumber(value) {
2934
+ return typeof value === "number" ? String(value) : "";
2935
+ }
2936
+ function readVisibleLineNumber(line) {
2937
+ return formatLineNumber(line.newLineNumber ?? line.oldLineNumber);
2938
+ }
2939
+ function readLineKey(prefix, line, index) {
2940
+ return `${prefix}-${index}-${line.oldLineNumber ?? "x"}-${line.newLineNumber ?? "x"}`;
2941
+ }
2942
+ function hasVisibleLineNumber(line) {
2943
+ return typeof line.newLineNumber === "number" || typeof line.oldLineNumber === "number";
2944
+ }
2945
+ function readHasBlockLineNumbers(block) {
2946
+ return block.lines.some(hasVisibleLineNumber);
2947
+ }
2948
+ function readLineNumberColumnWidth(block) {
2949
+ const maxDigits = block.lines.reduce((currentMax, line) => {
2950
+ if (!hasVisibleLineNumber(line)) {
2951
+ return currentMax;
2952
+ }
2953
+ return Math.max(currentMax, readVisibleLineNumber(line).length);
2954
+ }, 0);
2955
+ const width = Math.max(6.5, Math.min(8, maxDigits + 3.5));
2956
+ return `${width}ch`;
2957
+ }
2958
+ function buildRowTemplateColumns(params) {
2959
+ if (!params.showLineNumbers) {
2960
+ return void 0;
2961
+ }
2962
+ return {
2963
+ gridTemplateColumns: `${params.lineNumberColumnWidth} minmax(0, 1fr)`
2964
+ };
2965
+ }
2966
+ function getLineNumberTone(line) {
2967
+ if (line.kind === "remove") {
2968
+ return "border-r border-rose-200 bg-rose-50 text-rose-700";
2969
+ }
2970
+ if (line.kind === "add") {
2971
+ return "border-r border-emerald-200 bg-emerald-50 text-emerald-700";
2972
+ }
2973
+ return "border-r border-stone-200 bg-stone-100 text-stone-500";
2974
+ }
2975
+ function getCodeRowTone(line) {
2976
+ if (line.kind === "remove") {
2977
+ return "bg-rose-50 text-rose-950";
2978
+ }
2979
+ if (line.kind === "add") {
2980
+ return "bg-emerald-50 text-emerald-950";
2981
+ }
2982
+ return "bg-white text-amber-950/80";
2983
+ }
2984
+ function FileOperationLineRow({
2985
+ line,
2986
+ showLineNumbers,
2987
+ lineNumberColumnWidth
2988
+ }) {
2989
+ return /* @__PURE__ */ jsxs13(
2990
+ "div",
2991
+ {
2992
+ "data-file-line-row": "true",
2993
+ className: cn("grid w-max min-w-full", FILE_ROW_CLASS_NAME),
2994
+ style: buildRowTemplateColumns({
2995
+ showLineNumbers,
2996
+ lineNumberColumnWidth
2997
+ }),
2998
+ children: [
2999
+ showLineNumbers ? /* @__PURE__ */ jsx21(
3000
+ "span",
3001
+ {
3002
+ "data-file-line-number-cell": "true",
3003
+ style: { width: lineNumberColumnWidth },
3004
+ className: cn(
3005
+ FILE_GUTTER_NUMBER_CELL_CLASS_NAME,
3006
+ getLineNumberTone(line)
3007
+ ),
3008
+ children: readVisibleLineNumber(line)
3009
+ }
3010
+ ) : null,
3011
+ /* @__PURE__ */ jsx21(
3012
+ "span",
3013
+ {
3014
+ "data-file-code-row": "true",
3015
+ className: cn(
3016
+ "block min-w-full whitespace-pre px-2.5",
3017
+ getCodeRowTone(line)
3018
+ ),
3019
+ children: line.text || " "
3020
+ }
3021
+ )
3022
+ ]
3023
+ }
3024
+ );
3025
+ }
3026
+ function FileOperationLinesGrid({
3027
+ block
3028
+ }) {
3029
+ const showLineNumbers = readHasBlockLineNumbers(block);
3030
+ const lineNumberColumnWidth = readLineNumberColumnWidth(block);
3031
+ return /* @__PURE__ */ jsx21("div", { className: "overflow-x-auto custom-scrollbar-amber bg-white", children: block.lines.map((line, index) => /* @__PURE__ */ jsx21(
3032
+ FileOperationLineRow,
3033
+ {
3034
+ line,
3035
+ showLineNumbers,
3036
+ lineNumberColumnWidth
3037
+ },
3038
+ readLineKey("row", line, index)
3039
+ )) });
3040
+ }
3041
+
3042
+ // src/components/chat/ui/chat-message-list/tool-card/tool-card-file-operation.tsx
3043
+ import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
3044
+ function isPreviewBlock(block) {
3045
+ return block.display === "preview";
3046
+ }
3047
+ function readFileOperationContentVersion(block) {
3048
+ if (block.rawText) {
3049
+ return block.rawText;
3050
+ }
3051
+ return block.lines.map(
3052
+ (line) => [
3053
+ line.kind,
3054
+ line.oldLineNumber ?? "",
3055
+ line.newLineNumber ?? "",
3056
+ line.text
3057
+ ].join(":")
3058
+ ).join("\n");
3059
+ }
3060
+ function getCaptionTone(part) {
3061
+ if (/^\+\d+$/.test(part)) {
3062
+ return "text-emerald-700";
3063
+ }
3064
+ if (/^-\d+$/.test(part)) {
3065
+ return "text-rose-700";
3066
+ }
3067
+ return "text-stone-500";
3068
+ }
3069
+ function renderCaption(caption) {
3070
+ const parts = caption.split("\xB7").map((part) => part.trim()).filter((part) => /^\+\d+$/.test(part) || /^-\d+$/.test(part));
3071
+ if (parts.length === 0) {
3072
+ return null;
3073
+ }
3074
+ return /* @__PURE__ */ jsx22("div", { className: "flex shrink-0 items-center gap-x-1 whitespace-nowrap text-[10px] font-medium uppercase tracking-[0.08em]", children: parts.map((part, index) => /* @__PURE__ */ jsxs14(Fragment4, { children: [
3075
+ index > 0 ? /* @__PURE__ */ jsx22("span", { className: "text-stone-300", children: "\xB7" }) : null,
3076
+ /* @__PURE__ */ jsx22("span", { className: cn(getCaptionTone(part)), children: part })
3077
+ ] }, `${part}-${index}`)) });
3078
+ }
3079
+ function StickyFileOperationScrollArea({
3080
+ children,
3081
+ contentVersion,
3082
+ resetKey,
3083
+ className,
3084
+ scrollKind
3085
+ }) {
3086
+ const scrollRef = useRef7(null);
3087
+ const { onScroll } = useStickyBottomScroll({
3088
+ scrollRef,
3089
+ resetKey,
3090
+ isLoading: false,
3091
+ hasContent: true,
3092
+ contentVersion,
3093
+ stickyThresholdPx: 20
3094
+ });
3095
+ return /* @__PURE__ */ jsx22(
3096
+ "div",
3097
+ {
3098
+ ref: scrollRef,
3099
+ onScroll,
3100
+ "data-file-scroll-kind": scrollKind,
3101
+ className: cn(
3102
+ "overflow-y-auto overscroll-contain bg-white custom-scrollbar-amber",
3103
+ className
3104
+ ),
3105
+ children
3106
+ }
3107
+ );
3108
+ }
3109
+ function FileOperationBlock({
3110
+ block,
3111
+ showPathRow,
3112
+ isFirst
3113
+ }) {
3114
+ const { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
3115
+ const previewBlock = isPreviewBlock(block);
3116
+ const showMetaRow = showPathRow || Boolean(block.caption);
3117
+ return /* @__PURE__ */ jsxs14(
3118
+ "section",
3119
+ {
3120
+ className: cn(
3121
+ "overflow-hidden bg-white",
3122
+ !isFirst && "border-t border-stone-200/80"
3123
+ ),
3124
+ children: [
3125
+ showMetaRow ? /* @__PURE__ */ jsxs14(
3126
+ "div",
3127
+ {
3128
+ className: cn(
3129
+ "flex items-center justify-between gap-4 border-b border-stone-200/80 px-4 text-stone-700",
3130
+ showPathRow ? "py-2" : "py-1.5"
3131
+ ),
3132
+ children: [
3133
+ /* @__PURE__ */ jsx22("div", { className: "min-w-0 flex-1", children: showPathRow ? /* @__PURE__ */ jsx22(TooltipProvider, { delayDuration: 250, children: /* @__PURE__ */ jsxs14(Tooltip, { children: [
3134
+ /* @__PURE__ */ jsx22(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx22(
3135
+ "div",
3136
+ {
3137
+ className: "truncate whitespace-nowrap font-mono text-[12px] font-medium text-stone-700",
3138
+ title: block.path,
3139
+ children: block.path
3140
+ }
3141
+ ) }),
3142
+ /* @__PURE__ */ jsx22(
3143
+ TooltipContent,
3144
+ {
3145
+ side: "top",
3146
+ className: "max-w-[420px] text-xs font-mono break-all",
3147
+ children: block.path
3148
+ }
3149
+ )
3150
+ ] }) }) : null }),
3151
+ block.caption ? renderCaption(block.caption) : null
3152
+ ]
3153
+ }
3154
+ ) : null,
3155
+ block.lines.length > 0 ? /* @__PURE__ */ jsx22(
3156
+ StickyFileOperationScrollArea,
3157
+ {
3158
+ resetKey: `block:${block.key}`,
3159
+ contentVersion: readFileOperationContentVersion(block),
3160
+ className: "max-h-72",
3161
+ scrollKind: "block",
3162
+ children: /* @__PURE__ */ jsx22(FileOperationLinesGrid, { block })
3163
+ }
3164
+ ) : block.rawText ? /* @__PURE__ */ jsx22(
3165
+ StickyFileOperationScrollArea,
3166
+ {
3167
+ resetKey: `raw:${block.key}`,
3168
+ contentVersion: block.rawText,
3169
+ className: "max-h-72",
3170
+ scrollKind: "raw",
3171
+ children: /* @__PURE__ */ jsx22("div", { className: "overflow-x-auto custom-scrollbar-amber", children: /* @__PURE__ */ jsx22("pre", { className: "min-w-max whitespace-pre bg-white px-4 py-2 font-mono text-[11px] leading-5 text-amber-950/80", children: block.rawText }) })
3172
+ }
3173
+ ) : null,
3174
+ block.truncated && !previewBlock ? /* @__PURE__ */ jsx22("div", { className: "border-t border-stone-200/85 bg-stone-50 px-4 py-2 text-[10px] text-stone-500", children: "Showing a shortened diff preview." }) : null
3175
+ ]
3176
+ }
3177
+ );
3178
+ }
3179
+ function ToolCardFileOperationContent({
3180
+ card,
3181
+ className
3182
+ }) {
3183
+ const blocks = card.fileOperation?.blocks ?? [];
2838
3184
  const output = card.output?.trim() ?? "";
2839
- const isRunning = card.statusTone === "running";
2840
- const hasContent = !!(card.summary?.trim() || output.length > 0);
2841
- const wasEmptyRef = useRef6(!hasContent);
2842
- const [expanded, setExpanded] = useState7(hasContent && (isRunning || card.statusTone === "error"));
3185
+ if (blocks.length === 0 && !output) {
3186
+ return null;
3187
+ }
3188
+ return /* @__PURE__ */ jsxs14("div", { className: cn("overflow-hidden bg-white", className), children: [
3189
+ blocks.map((block, index) => {
3190
+ return /* @__PURE__ */ jsx22(
3191
+ FileOperationBlock,
3192
+ {
3193
+ block,
3194
+ showPathRow: true,
3195
+ isFirst: index === 0
3196
+ },
3197
+ block.key
3198
+ );
3199
+ }),
3200
+ output ? /* @__PURE__ */ jsx22(
3201
+ StickyFileOperationScrollArea,
3202
+ {
3203
+ resetKey: `output:${card.toolName}:${card.summary ?? "none"}`,
3204
+ contentVersion: output,
3205
+ className: cn(
3206
+ "max-h-56",
3207
+ blocks.length > 0 && "border-t border-stone-200/80"
3208
+ ),
3209
+ scrollKind: "output",
3210
+ children: /* @__PURE__ */ jsx22("div", { className: "overflow-x-auto custom-scrollbar-amber", children: /* @__PURE__ */ jsx22("pre", { className: "min-w-max whitespace-pre bg-white px-4 py-2 font-mono text-[11px] leading-5 text-amber-950/80", children: output }) })
3211
+ }
3212
+ ) : null
3213
+ ] });
3214
+ }
3215
+
3216
+ // src/components/chat/ui/chat-message-list/tool-card/tool-card-views.tsx
3217
+ import { Fragment as Fragment5, jsx as jsx23, jsxs as jsxs15 } from "react/jsx-runtime";
3218
+ var TOOL_CARD_AUTO_EXPAND_DELAY_MS = 200;
3219
+ var ANSI_ESCAPE_PREFIX = String.fromCharCode(27);
3220
+ var ANSI_ESCAPE_PATTERN = new RegExp(`${ANSI_ESCAPE_PREFIX}\\[[0-?]*[ -/]*[@-~]`, "g");
3221
+ function readNonEmptyString(value) {
3222
+ if (typeof value !== "string") {
3223
+ return null;
3224
+ }
3225
+ return value.length > 0 ? value : null;
3226
+ }
3227
+ function isRecord(value) {
3228
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3229
+ }
3230
+ function stripAnsi(value) {
3231
+ return value.replace(ANSI_ESCAPE_PATTERN, "");
3232
+ }
3233
+ function isStructuredTerminalRecord(record) {
3234
+ 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;
3235
+ }
3236
+ function extractTerminalOutputFromRecord(record) {
3237
+ const aggregatedOutput = readNonEmptyString(record.aggregated_output) ?? readNonEmptyString(record.combinedOutput) ?? readNonEmptyString(record.output);
3238
+ if (aggregatedOutput) {
3239
+ return aggregatedOutput;
3240
+ }
3241
+ const stdout = readNonEmptyString(record.stdout);
3242
+ const stderr = readNonEmptyString(record.stderr);
3243
+ if (!stdout && !stderr) {
3244
+ return null;
3245
+ }
3246
+ return [stdout, stderr].filter((value) => Boolean(value)).join("\n");
3247
+ }
3248
+ function normalizeTerminalOutput(rawOutput, structuredOutput) {
3249
+ if (isRecord(structuredOutput)) {
3250
+ const terminalOutput = extractTerminalOutputFromRecord(structuredOutput);
3251
+ if (terminalOutput) {
3252
+ return stripAnsi(terminalOutput);
3253
+ }
3254
+ if (isStructuredTerminalRecord(structuredOutput)) {
3255
+ return "";
3256
+ }
3257
+ }
3258
+ if (!rawOutput) {
3259
+ return "";
3260
+ }
3261
+ const trimmed = rawOutput.trim();
3262
+ if (!trimmed.startsWith("{")) {
3263
+ return stripAnsi(rawOutput);
3264
+ }
3265
+ try {
3266
+ const parsed = JSON.parse(trimmed);
3267
+ if (!isRecord(parsed)) {
3268
+ return stripAnsi(rawOutput);
3269
+ }
3270
+ const terminalOutput = extractTerminalOutputFromRecord(parsed);
3271
+ if (terminalOutput) {
3272
+ return stripAnsi(terminalOutput);
3273
+ }
3274
+ if (isStructuredTerminalRecord(parsed)) {
3275
+ return "";
3276
+ }
3277
+ return stripAnsi(rawOutput);
3278
+ } catch {
3279
+ return stripAnsi(rawOutput);
3280
+ }
3281
+ }
3282
+ function useToolCardExpandedState({
3283
+ canExpand,
3284
+ isRunning,
3285
+ autoExpandWhileRunning = true,
3286
+ expandOnError = false,
3287
+ statusTone
3288
+ }) {
3289
+ const [expanded, setExpanded] = useState7(false);
2843
3290
  const [hasUserToggled, setHasUserToggled] = useState7(false);
2844
- const prevRunningRef = useRef6(isRunning);
2845
- useEffect7(() => {
2846
- if (wasEmptyRef.current && hasContent && isRunning) {
3291
+ const expandTimerRef = useRef8(null);
3292
+ const prevRunningRef = useRef8(isRunning);
3293
+ const isFirstRenderRef = useRef8(true);
3294
+ useEffect8(() => {
3295
+ return () => {
3296
+ if (expandTimerRef.current !== null) {
3297
+ window.clearTimeout(expandTimerRef.current);
3298
+ }
3299
+ };
3300
+ }, []);
3301
+ useEffect8(() => {
3302
+ if (expandOnError && statusTone === "error" && canExpand && !hasUserToggled) {
3303
+ if (expandTimerRef.current !== null) {
3304
+ window.clearTimeout(expandTimerRef.current);
3305
+ expandTimerRef.current = null;
3306
+ }
2847
3307
  setExpanded(true);
2848
- wasEmptyRef.current = false;
3308
+ prevRunningRef.current = isRunning;
3309
+ isFirstRenderRef.current = false;
3310
+ return;
2849
3311
  }
2850
- }, [hasContent, isRunning]);
2851
- useEffect7(() => {
2852
- if (prevRunningRef.current && !isRunning && !hasUserToggled) {
2853
- setExpanded(false);
3312
+ if (autoExpandWhileRunning && isRunning && canExpand && !hasUserToggled && !expanded && (isFirstRenderRef.current || !prevRunningRef.current)) {
3313
+ expandTimerRef.current = window.setTimeout(() => {
3314
+ setExpanded(true);
3315
+ expandTimerRef.current = null;
3316
+ }, TOOL_CARD_AUTO_EXPAND_DELAY_MS);
3317
+ }
3318
+ if (!isRunning) {
3319
+ if (expandTimerRef.current !== null) {
3320
+ window.clearTimeout(expandTimerRef.current);
3321
+ expandTimerRef.current = null;
3322
+ }
3323
+ if (prevRunningRef.current && !hasUserToggled) {
3324
+ setExpanded(false);
3325
+ }
2854
3326
  }
2855
3327
  prevRunningRef.current = isRunning;
2856
- }, [isRunning, hasUserToggled]);
3328
+ isFirstRenderRef.current = false;
3329
+ }, [autoExpandWhileRunning, canExpand, expandOnError, expanded, hasUserToggled, isRunning, statusTone]);
2857
3330
  const onToggle = () => {
2858
- if (!output && !isRunning) return;
2859
- setExpanded(!expanded);
3331
+ if (!canExpand) {
3332
+ return;
3333
+ }
3334
+ if (expandTimerRef.current !== null) {
3335
+ window.clearTimeout(expandTimerRef.current);
3336
+ expandTimerRef.current = null;
3337
+ }
3338
+ setExpanded((current) => !current);
2860
3339
  setHasUserToggled(true);
2861
3340
  };
3341
+ return { expanded, onToggle };
3342
+ }
3343
+ function GenericToolSection({
3344
+ label,
3345
+ tone,
3346
+ children
3347
+ }) {
3348
+ const tones = {
3349
+ input: {
3350
+ shell: "border-stone-200/80 bg-stone-50/90",
3351
+ header: "border-stone-200/80 bg-stone-100/85 text-stone-500",
3352
+ dot: "bg-stone-400/80",
3353
+ body: "text-stone-700"
3354
+ },
3355
+ output: {
3356
+ shell: "border-amber-200/70 bg-white/90",
3357
+ header: "border-amber-200/70 bg-amber-50/90 text-amber-700",
3358
+ dot: "bg-amber-500/80",
3359
+ body: "text-amber-950/80"
3360
+ },
3361
+ error: {
3362
+ shell: "border-rose-200/80 bg-rose-50/85",
3363
+ header: "border-rose-200/80 bg-rose-100/80 text-rose-700",
3364
+ dot: "bg-rose-500/80",
3365
+ body: "text-rose-950/85"
3366
+ }
3367
+ };
3368
+ const style = tones[tone];
3369
+ return /* @__PURE__ */ jsxs15(
3370
+ "section",
3371
+ {
3372
+ className: cn(
3373
+ "overflow-hidden rounded-md border shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]",
3374
+ style.shell
3375
+ ),
3376
+ children: [
3377
+ /* @__PURE__ */ jsxs15(
3378
+ "div",
3379
+ {
3380
+ className: cn(
3381
+ "flex items-center gap-2 border-b px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.16em]",
3382
+ style.header
3383
+ ),
3384
+ children: [
3385
+ /* @__PURE__ */ jsx23("span", { className: cn("h-1.5 w-1.5 rounded-full", style.dot) }),
3386
+ /* @__PURE__ */ jsx23("span", { children: label })
3387
+ ]
3388
+ }
3389
+ ),
3390
+ /* @__PURE__ */ jsx23("div", { className: "w-full overflow-hidden", children: /* @__PURE__ */ jsx23(
3391
+ "pre",
3392
+ {
3393
+ className: cn(
3394
+ "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-amber",
3395
+ style.body
3396
+ ),
3397
+ children
3398
+ }
3399
+ ) })
3400
+ ]
3401
+ }
3402
+ );
3403
+ }
3404
+ function TerminalExecutionView({ card }) {
3405
+ const output = normalizeTerminalOutput(card.output, card.outputData);
3406
+ const isRunning = card.statusTone === "running";
3407
+ const hasContent = !!(card.summary?.trim() || output.trim().length > 0);
3408
+ const { expanded, onToggle } = useToolCardExpandedState({
3409
+ canExpand: output.trim().length > 0 || isRunning,
3410
+ isRunning,
3411
+ expandOnError: hasContent,
3412
+ statusTone: card.statusTone
3413
+ });
2862
3414
  const commandPart = card.summary?.replace(/^(command|path|args|query|input):\s*/i, "");
2863
- return /* @__PURE__ */ jsxs13(ToolCardRoot, { children: [
2864
- /* @__PURE__ */ jsx21(
3415
+ return /* @__PURE__ */ jsxs15(ToolCardRoot, { children: [
3416
+ /* @__PURE__ */ jsx23(
2865
3417
  ToolCardHeader,
2866
3418
  {
2867
3419
  card,
2868
3420
  icon: Terminal,
2869
3421
  expanded,
2870
- canExpand: !!output || isRunning,
3422
+ canExpand: output.trim().length > 0 || isRunning,
2871
3423
  onToggle
2872
3424
  }
2873
3425
  ),
2874
- expanded && /* @__PURE__ */ jsxs13(Fragment4, { children: [
2875
- /* @__PURE__ */ jsx21("div", { className: "px-3 pb-2 font-mono w-full max-h-48 overflow-y-auto custom-scrollbar-amber min-h-0 text-[12px]", children: /* @__PURE__ */ jsxs13("div", { className: "flex items-start gap-2 leading-relaxed", children: [
2876
- /* @__PURE__ */ jsx21("span", { className: "text-amber-500/50 font-medium shrink-0 select-none mt-[1px]", children: "$" }),
2877
- /* @__PURE__ */ jsx21("div", { className: "flex-1 min-w-0", children: commandPart ? /* @__PURE__ */ jsxs13("div", { className: "text-amber-950/80 break-words whitespace-pre-wrap tracking-tight font-medium inline-block", children: [
3426
+ expanded && /* @__PURE__ */ jsxs15(Fragment5, { children: [
3427
+ /* @__PURE__ */ jsx23("div", { className: "px-3 pb-2 font-mono w-full max-h-48 overflow-y-auto custom-scrollbar-amber min-h-0 text-[12px]", children: /* @__PURE__ */ jsxs15("div", { className: "flex items-start gap-2 leading-relaxed", children: [
3428
+ /* @__PURE__ */ jsx23("span", { className: "text-amber-500/50 font-medium shrink-0 select-none mt-[1px]", children: "$" }),
3429
+ /* @__PURE__ */ jsx23("div", { className: "flex-1 min-w-0", children: commandPart ? /* @__PURE__ */ jsxs15("div", { className: "text-amber-950/80 break-words whitespace-pre-wrap tracking-tight font-medium inline-block", children: [
2878
3430
  commandPart,
2879
- isRunning && !output && /* @__PURE__ */ jsx21("span", { className: "inline-block w-1.5 h-3 ml-1 bg-amber-500/60 animate-pulse align-middle" })
2880
- ] }) : /* @__PURE__ */ jsx21("div", { className: "h-3 w-32 bg-amber-200/30 rounded animate-pulse mt-2" }) })
3431
+ isRunning && !output && /* @__PURE__ */ jsx23("span", { className: "inline-block w-1.5 h-3 ml-1 bg-amber-500/60 animate-pulse align-middle" })
3432
+ ] }) : /* @__PURE__ */ jsx23("div", { className: "h-3 w-32 bg-amber-200/30 rounded animate-pulse mt-2" }) })
2881
3433
  ] }) }),
2882
- output && /* @__PURE__ */ jsx21(ToolCardContent, { children: /* @__PURE__ */ jsxs13("pre", { className: "font-mono text-[12px] text-amber-950/70 whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar-amber leading-relaxed px-0", children: [
3434
+ output && /* @__PURE__ */ jsx23(ToolCardContent, { children: /* @__PURE__ */ jsxs15("pre", { className: "font-mono text-[12px] text-amber-950/70 whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar-amber leading-relaxed px-0", children: [
2883
3435
  output,
2884
- isRunning && /* @__PURE__ */ jsx21("span", { className: "inline-block w-1.5 h-3 ml-1 bg-amber-500/60 animate-pulse align-middle" })
3436
+ isRunning && /* @__PURE__ */ jsx23("span", { className: "inline-block w-1.5 h-3 ml-1 bg-amber-500/60 animate-pulse align-middle" })
2885
3437
  ] }) })
2886
3438
  ] })
2887
3439
  ] });
@@ -2889,75 +3441,50 @@ function TerminalExecutionView({ card }) {
2889
3441
  function FileOperationView({ card }) {
2890
3442
  const output = card.output?.trim() ?? "";
2891
3443
  const isRunning = card.statusTone === "running";
2892
- const [expanded, setExpanded] = useState7(false);
2893
- const [hasUserToggled, setHasUserToggled] = useState7(false);
2894
- const prevRunningRef = useRef6(isRunning);
2895
- useEffect7(() => {
2896
- if (prevRunningRef.current && !isRunning && !hasUserToggled) {
2897
- setExpanded(false);
2898
- }
2899
- prevRunningRef.current = isRunning;
2900
- }, [isRunning, hasUserToggled]);
2901
- const onToggle = () => {
2902
- if (!output && !isRunning) return;
2903
- setExpanded(!expanded);
2904
- setHasUserToggled(true);
2905
- };
2906
- const isEdit = card.toolName === "edit_file" || card.toolName === "write_file";
2907
- const renderLine = (line, idx) => {
2908
- if (isEdit) {
2909
- if (line.startsWith("+") && !line.startsWith("+++")) {
2910
- return /* @__PURE__ */ jsxs13("div", { className: "bg-emerald-500/10 text-emerald-700 px-2 py-0.5 w-full break-all whitespace-pre-wrap", children: [
2911
- /* @__PURE__ */ jsx21("span", { className: "select-none opacity-40 mr-2 w-3 inline-block shrink-0", children: "+" }),
2912
- /* @__PURE__ */ jsx21("span", { children: line.slice(1) })
2913
- ] }, idx);
2914
- }
2915
- if (line.startsWith("-") && !line.startsWith("---")) {
2916
- return /* @__PURE__ */ jsxs13("div", { className: "bg-rose-500/10 text-rose-700 px-2 py-0.5 w-full break-all whitespace-pre-wrap", children: [
2917
- /* @__PURE__ */ jsx21("span", { className: "select-none opacity-40 mr-2 w-3 inline-block shrink-0", children: "-" }),
2918
- /* @__PURE__ */ jsx21("span", { className: "line-through decoration-rose-400/50", children: line.slice(1) })
2919
- ] }, idx);
2920
- }
2921
- }
2922
- return /* @__PURE__ */ jsx21("div", { className: "py-0.5 text-amber-950/80 w-full break-all whitespace-pre-wrap", children: /* @__PURE__ */ jsx21("span", { children: line }) }, idx);
2923
- };
2924
- const lines = output.split("\n");
2925
- const maxLines = 15;
2926
- const isLong = lines.length > maxLines;
2927
- const displayLines = !expanded && isLong ? lines.slice(0, maxLines) : lines;
2928
- return /* @__PURE__ */ jsxs13(ToolCardRoot, { children: [
2929
- /* @__PURE__ */ jsx21(
3444
+ const hasStructuredPreview = Boolean(card.fileOperation?.blocks.length);
3445
+ const hasContent = hasStructuredPreview || Boolean(output);
3446
+ const previewBlocks = card.fileOperation?.blocks ?? [];
3447
+ const previewLineCount = previewBlocks.reduce((count, block) => count + block.lines.length, 0);
3448
+ const previewCharCount = previewBlocks.reduce((count, block) => {
3449
+ if (block.rawText) {
3450
+ return count + block.rawText.length;
3451
+ }
3452
+ return count + block.lines.reduce((lineCount, line) => lineCount + line.text.length + 1, 0);
3453
+ }, 0);
3454
+ const shouldAutoExpandWhileRunning = !(isRunning && card.toolName === "write_file" && (previewLineCount > 24 || previewCharCount > 1200));
3455
+ const { expanded, onToggle } = useToolCardExpandedState({
3456
+ canExpand: hasContent || isRunning,
3457
+ isRunning,
3458
+ autoExpandWhileRunning: shouldAutoExpandWhileRunning,
3459
+ expandOnError: hasContent,
3460
+ statusTone: card.statusTone
3461
+ });
3462
+ const isEdit = card.toolName === "edit_file" || card.toolName === "write_file" || card.toolName === "apply_patch" || card.toolName === "file_change";
3463
+ return /* @__PURE__ */ jsxs15(ToolCardRoot, { children: [
3464
+ /* @__PURE__ */ jsx23(
2930
3465
  ToolCardHeader,
2931
3466
  {
2932
3467
  card,
2933
3468
  icon: isEdit ? Code2 : FileText,
2934
3469
  expanded,
2935
- canExpand: !!output || isRunning,
3470
+ canExpand: hasContent || isRunning,
3471
+ hideSummary: expanded && hasStructuredPreview,
2936
3472
  onToggle
2937
3473
  }
2938
3474
  ),
2939
- expanded && output && /* @__PURE__ */ jsx21(ToolCardContent, { children: /* @__PURE__ */ jsx21("div", { className: "font-mono text-[12px] leading-relaxed max-h-48 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar-amber text-amber-950/80 w-full", children: displayLines.map(renderLine) }) })
3475
+ expanded && hasContent && /* @__PURE__ */ jsx23(ToolCardContent, { className: "border-t border-amber-200/20 bg-transparent px-0 pt-0 pb-0", children: /* @__PURE__ */ jsx23(ToolCardFileOperationContent, { card }) })
2940
3476
  ] });
2941
3477
  }
2942
3478
  function SearchSnippetView({ card }) {
2943
3479
  const isRunning = card.statusTone === "running";
2944
- const [expanded, setExpanded] = useState7(isRunning);
2945
- const [hasUserToggled, setHasUserToggled] = useState7(false);
2946
- const prevRunningRef = useRef6(isRunning);
2947
3480
  const output = card.output?.trim() ?? "";
2948
- useEffect7(() => {
2949
- if (prevRunningRef.current && !isRunning && !hasUserToggled) {
2950
- setExpanded(false);
2951
- }
2952
- prevRunningRef.current = isRunning;
2953
- }, [isRunning, hasUserToggled]);
2954
- const onToggle = () => {
2955
- if (!output && !isRunning) return;
2956
- setExpanded(!expanded);
2957
- setHasUserToggled(true);
2958
- };
2959
- return /* @__PURE__ */ jsxs13(ToolCardRoot, { children: [
2960
- /* @__PURE__ */ jsx21(
3481
+ const { expanded, onToggle } = useToolCardExpandedState({
3482
+ canExpand: !!output || isRunning,
3483
+ isRunning,
3484
+ statusTone: card.statusTone
3485
+ });
3486
+ return /* @__PURE__ */ jsxs15(ToolCardRoot, { children: [
3487
+ /* @__PURE__ */ jsx23(
2961
3488
  ToolCardHeader,
2962
3489
  {
2963
3490
  card,
@@ -2967,51 +3494,58 @@ function SearchSnippetView({ card }) {
2967
3494
  onToggle
2968
3495
  }
2969
3496
  ),
2970
- expanded && output && /* @__PURE__ */ jsx21(ToolCardContent, { children: /* @__PURE__ */ jsx21("pre", { className: "font-mono text-[12px] text-amber-950/70 whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar-amber leading-relaxed", children: output }) })
3497
+ expanded && output && /* @__PURE__ */ jsx23(ToolCardContent, { children: /* @__PURE__ */ jsx23("pre", { className: "font-mono text-[12px] text-amber-950/70 whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar-amber leading-relaxed", children: output }) })
2971
3498
  ] });
2972
3499
  }
2973
3500
  function GenericToolCard({ card }) {
3501
+ const input = card.input?.trim() ?? "";
2974
3502
  const output = card.output?.trim() ?? "";
2975
3503
  const isRunning = card.statusTone === "running";
2976
- const [expanded, setExpanded] = useState7(isRunning);
2977
- const [hasUserToggled, setHasUserToggled] = useState7(false);
2978
- const prevRunningRef = useRef6(isRunning);
2979
- const showOutputSection = card.kind === "result" || card.hasResult;
2980
- useEffect7(() => {
2981
- if (prevRunningRef.current && !isRunning && !hasUserToggled) {
2982
- setExpanded(false);
2983
- }
2984
- prevRunningRef.current = isRunning;
2985
- }, [isRunning, hasUserToggled]);
2986
- const onToggle = () => {
2987
- if (!showOutputSection) return;
2988
- setExpanded(!expanded);
2989
- setHasUserToggled(true);
2990
- };
2991
- return /* @__PURE__ */ jsxs13(ToolCardRoot, { children: [
2992
- /* @__PURE__ */ jsx21(
3504
+ const hasInputSection = input.length > 0;
3505
+ const hasOutputSection = output.length > 0;
3506
+ const hasContent = hasInputSection || hasOutputSection;
3507
+ const inputLabel = card.inputLabel?.trim() || "Input";
3508
+ const outputLabel = card.outputLabel?.trim() || "Output";
3509
+ const { expanded, onToggle } = useToolCardExpandedState({
3510
+ canExpand: hasContent || isRunning,
3511
+ isRunning,
3512
+ statusTone: card.statusTone
3513
+ });
3514
+ return /* @__PURE__ */ jsxs15(ToolCardRoot, { children: [
3515
+ /* @__PURE__ */ jsx23(
2993
3516
  ToolCardHeader,
2994
3517
  {
2995
3518
  card,
2996
3519
  icon: Globe,
2997
3520
  expanded,
2998
- canExpand: showOutputSection,
3521
+ canExpand: hasContent || isRunning,
2999
3522
  onToggle
3000
3523
  }
3001
3524
  ),
3002
- expanded && output && /* @__PURE__ */ jsx21(ToolCardContent, { children: /* @__PURE__ */ jsx21("pre", { className: "text-amber-950/80 whitespace-pre-wrap break-all overflow-y-auto overflow-x-hidden max-h-64 custom-scrollbar-amber w-full min-w-0 max-w-full leading-relaxed", children: output }) })
3525
+ expanded && hasContent && /* @__PURE__ */ jsxs15(ToolCardContent, { className: "bg-transparent px-2.5 py-2.5", children: [
3526
+ hasInputSection && /* @__PURE__ */ jsx23(GenericToolSection, { label: inputLabel, tone: "input", children: input }),
3527
+ hasInputSection && hasOutputSection && /* @__PURE__ */ jsx23("div", { className: "h-2" }),
3528
+ hasOutputSection && /* @__PURE__ */ jsx23(
3529
+ GenericToolSection,
3530
+ {
3531
+ label: outputLabel,
3532
+ tone: card.statusTone === "error" ? "error" : "output",
3533
+ children: output
3534
+ }
3535
+ )
3536
+ ] })
3003
3537
  ] });
3004
3538
  }
3005
3539
 
3006
3540
  // src/components/chat/ui/chat-message-list/chat-tool-card.tsx
3007
- import { jsx as jsx22 } from "react/jsx-runtime";
3541
+ import { jsx as jsx24 } from "react/jsx-runtime";
3008
3542
  function isTerminalTool(name) {
3009
3543
  const lowered = name.toLowerCase();
3010
- return lowered === "exec" || lowered === "execute_command" || lowered === "bash" || lowered === "shell" || lowered.includes("run_");
3544
+ return lowered === "exec" || lowered === "exec_command" || lowered === "execute_command" || lowered === "command_execution" || lowered === "bash" || lowered === "shell" || lowered.includes("run_");
3011
3545
  }
3012
3546
  function isFileEditTool(name) {
3013
3547
  const lowered = name.toLowerCase();
3014
- return lowered === "read_file" || lowered === "write_file" || lowered === "edit_file" || lowered === "apply_patch";
3548
+ return lowered === "read_file" || lowered === "write_file" || lowered === "edit_file" || lowered === "apply_patch" || lowered === "file_change";
3015
3549
  }
3016
3550
  function isSearchTool(name) {
3017
3551
  const lowered = name.toLowerCase();
@@ -3019,48 +3553,48 @@ function isSearchTool(name) {
3019
3553
  }
3020
3554
  function ChatToolCard({ card }) {
3021
3555
  if (isTerminalTool(card.toolName)) {
3022
- return /* @__PURE__ */ jsx22(TerminalExecutionView, { card });
3556
+ return /* @__PURE__ */ jsx24(TerminalExecutionView, { card });
3023
3557
  }
3024
3558
  if (isFileEditTool(card.toolName)) {
3025
- return /* @__PURE__ */ jsx22(FileOperationView, { card });
3559
+ return /* @__PURE__ */ jsx24(FileOperationView, { card });
3026
3560
  }
3027
3561
  if (isSearchTool(card.toolName)) {
3028
- return /* @__PURE__ */ jsx22(SearchSnippetView, { card });
3562
+ return /* @__PURE__ */ jsx24(SearchSnippetView, { card });
3029
3563
  }
3030
- return /* @__PURE__ */ jsx22(GenericToolCard, { card });
3564
+ return /* @__PURE__ */ jsx24(GenericToolCard, { card });
3031
3565
  }
3032
3566
 
3033
3567
  // src/components/chat/ui/chat-message-list/chat-unknown-part.tsx
3034
- import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
3568
+ import { jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
3035
3569
  function ChatUnknownPart(props) {
3036
- return /* @__PURE__ */ jsxs14("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-2 text-xs text-gray-600", children: [
3037
- /* @__PURE__ */ jsxs14("div", { className: "font-semibold text-gray-700", children: [
3570
+ return /* @__PURE__ */ jsxs16("div", { className: "rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-2 text-xs text-gray-600", children: [
3571
+ /* @__PURE__ */ jsxs16("div", { className: "font-semibold text-gray-700", children: [
3038
3572
  props.label,
3039
3573
  ": ",
3040
3574
  props.rawType
3041
3575
  ] }),
3042
- props.text ? /* @__PURE__ */ jsx23("pre", { className: "mt-1 whitespace-pre-wrap break-words text-[11px] text-gray-500", children: props.text }) : null
3576
+ props.text ? /* @__PURE__ */ jsx25("pre", { className: "mt-1 whitespace-pre-wrap break-words text-[11px] text-gray-500", children: props.text }) : null
3043
3577
  ] });
3044
3578
  }
3045
3579
 
3046
3580
  // src/components/chat/ui/chat-message-list/chat-message.tsx
3047
- import { jsx as jsx24 } from "react/jsx-runtime";
3581
+ import { jsx as jsx26 } from "react/jsx-runtime";
3048
3582
  var ChatMessage = memo(function ChatMessage2(props) {
3049
3583
  const { message, texts } = props;
3050
3584
  const { role } = message;
3051
3585
  const isUser = role === "user";
3052
3586
  const isMessageInProgress = message.status === "pending" || message.status === "streaming";
3053
- return /* @__PURE__ */ jsx24(
3587
+ return /* @__PURE__ */ jsx26(
3054
3588
  "div",
3055
3589
  {
3056
3590
  className: cn(
3057
3591
  "inline-block w-fit max-w-full rounded-2xl border px-4 shadow-sm",
3058
3592
  isUser ? "border-primary bg-primary py-3 text-white" : role === "assistant" ? "border-gray-200 bg-white pb-3 pt-4 text-gray-900" : "border-orange-200/80 bg-orange-50/70 py-3 text-gray-900"
3059
3593
  ),
3060
- children: /* @__PURE__ */ jsx24("div", { className: "space-y-2", children: message.parts.map((part, index) => {
3594
+ children: /* @__PURE__ */ jsx26("div", { className: "space-y-2", children: message.parts.map((part, index) => {
3061
3595
  const { type } = part;
3062
3596
  if (type === "markdown") {
3063
- return /* @__PURE__ */ jsx24(
3597
+ return /* @__PURE__ */ jsx26(
3064
3598
  ChatMessageMarkdown,
3065
3599
  {
3066
3600
  text: part.text,
@@ -3071,7 +3605,7 @@ var ChatMessage = memo(function ChatMessage2(props) {
3071
3605
  );
3072
3606
  }
3073
3607
  if (type === "inline-content") {
3074
- return /* @__PURE__ */ jsx24(
3608
+ return /* @__PURE__ */ jsx26(
3075
3609
  ChatMessageInlineContent,
3076
3610
  {
3077
3611
  segments: part.segments,
@@ -3082,7 +3616,7 @@ var ChatMessage = memo(function ChatMessage2(props) {
3082
3616
  );
3083
3617
  }
3084
3618
  if (type === "reasoning") {
3085
- return /* @__PURE__ */ jsx24(
3619
+ return /* @__PURE__ */ jsx26(
3086
3620
  ChatReasoningBlock,
3087
3621
  {
3088
3622
  label: part.label,
@@ -3094,10 +3628,10 @@ var ChatMessage = memo(function ChatMessage2(props) {
3094
3628
  );
3095
3629
  }
3096
3630
  if (type === "tool-card") {
3097
- return /* @__PURE__ */ jsx24("div", { className: "mt-0.5", children: /* @__PURE__ */ jsx24(ChatToolCard, { card: part.card }) }, `tool-${index}`);
3631
+ return /* @__PURE__ */ jsx26("div", { className: "mt-0.5", children: /* @__PURE__ */ jsx26(ChatToolCard, { card: part.card }) }, `tool-${index}`);
3098
3632
  }
3099
3633
  if (type === "file") {
3100
- return /* @__PURE__ */ jsx24(
3634
+ return /* @__PURE__ */ jsx26(
3101
3635
  ChatMessageFile,
3102
3636
  {
3103
3637
  file: part.file,
@@ -3107,7 +3641,7 @@ var ChatMessage = memo(function ChatMessage2(props) {
3107
3641
  );
3108
3642
  }
3109
3643
  if (type === "unknown") {
3110
- return /* @__PURE__ */ jsx24(
3644
+ return /* @__PURE__ */ jsx26(
3111
3645
  ChatUnknownPart,
3112
3646
  {
3113
3647
  label: part.label,
@@ -3124,9 +3658,9 @@ var ChatMessage = memo(function ChatMessage2(props) {
3124
3658
  });
3125
3659
 
3126
3660
  // src/components/chat/ui/chat-message-list/chat-message-meta.tsx
3127
- import { jsxs as jsxs15 } from "react/jsx-runtime";
3661
+ import { jsxs as jsxs17 } from "react/jsx-runtime";
3128
3662
  function ChatMessageMeta(props) {
3129
- return /* @__PURE__ */ jsxs15(
3663
+ return /* @__PURE__ */ jsxs17(
3130
3664
  "div",
3131
3665
  {
3132
3666
  className: cn(
@@ -3145,7 +3679,7 @@ function ChatMessageMeta(props) {
3145
3679
  // src/components/chat/ui/chat-message-list/chat-message-action-copy.tsx
3146
3680
  import { useMemo as useMemo6 } from "react";
3147
3681
  import { Check as Check5, Copy as Copy2 } from "lucide-react";
3148
- import { jsx as jsx25 } from "react/jsx-runtime";
3682
+ import { jsx as jsx27 } from "react/jsx-runtime";
3149
3683
  function ChatMessageActionCopy({
3150
3684
  message,
3151
3685
  texts
@@ -3159,7 +3693,7 @@ function ChatMessageActionCopy({
3159
3693
  }, [message.parts]);
3160
3694
  const { copied, copy } = useCopyFeedback({ text: messageText });
3161
3695
  if (!messageText) return null;
3162
- return /* @__PURE__ */ jsx25(
3696
+ return /* @__PURE__ */ jsx27(
3163
3697
  "button",
3164
3698
  {
3165
3699
  type: "button",
@@ -3167,13 +3701,13 @@ function ChatMessageActionCopy({
3167
3701
  className: "text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-md hover:bg-gray-100 flex items-center justify-center",
3168
3702
  "aria-label": copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
3169
3703
  title: copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
3170
- children: copied ? /* @__PURE__ */ jsx25(Check5, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx25(Copy2, { className: "h-3.5 w-3.5" })
3704
+ children: copied ? /* @__PURE__ */ jsx27(Check5, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx27(Copy2, { className: "h-3.5 w-3.5" })
3171
3705
  }
3172
3706
  );
3173
3707
  }
3174
3708
 
3175
3709
  // src/components/chat/ui/chat-message-list/chat-message-list.tsx
3176
- import { Fragment as Fragment5, jsx as jsx26, jsxs as jsxs16 } from "react/jsx-runtime";
3710
+ import { Fragment as Fragment6, jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
3177
3711
  var INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
3178
3712
  function hasRenderableText(value) {
3179
3713
  const trimmed = value.trim();
@@ -3191,10 +3725,10 @@ function hasRenderableMessageContent(message) {
3191
3725
  });
3192
3726
  }
3193
3727
  function ChatMessageTypingFooter() {
3194
- return /* @__PURE__ */ jsx26("div", { className: "flex items-center gap-2 px-1 py-0.5 text-[11px] text-gray-400", children: /* @__PURE__ */ jsxs16("div", { className: "flex space-x-1 items-center h-full", children: [
3195
- /* @__PURE__ */ jsx26("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse" }),
3196
- /* @__PURE__ */ jsx26("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse [animation-delay:200ms]" }),
3197
- /* @__PURE__ */ jsx26("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse [animation-delay:400ms]" })
3728
+ return /* @__PURE__ */ jsx28("div", { className: "flex items-center gap-2 px-1 py-0.5 text-[11px] text-gray-400", children: /* @__PURE__ */ jsxs18("div", { className: "flex space-x-1 items-center h-full", children: [
3729
+ /* @__PURE__ */ jsx28("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse" }),
3730
+ /* @__PURE__ */ jsx28("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse [animation-delay:200ms]" }),
3731
+ /* @__PURE__ */ jsx28("div", { className: "h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse [animation-delay:400ms]" })
3198
3732
  ] }) });
3199
3733
  }
3200
3734
  function ChatMessageList(props) {
@@ -3202,118 +3736,28 @@ function ChatMessageList(props) {
3202
3736
  const hasRenderableAssistantDraft = visibleMessages.some(
3203
3737
  (message) => message.role === "assistant" && (message.status === "streaming" || message.status === "pending")
3204
3738
  );
3205
- return /* @__PURE__ */ jsxs16("div", { className: cn("space-y-5", props.className), children: [
3739
+ return /* @__PURE__ */ jsxs18("div", { className: cn("space-y-5", props.className), children: [
3206
3740
  visibleMessages.map((message) => {
3207
3741
  const isUser = message.role === "user";
3208
3742
  const isGenerating = !isUser && (message.status === "streaming" || message.status === "pending");
3209
- return /* @__PURE__ */ jsxs16("div", { className: cn("flex gap-3", isUser ? "justify-end" : "justify-start"), children: [
3210
- !isUser ? /* @__PURE__ */ jsx26(ChatMessageAvatar, { role: message.role }) : null,
3211
- /* @__PURE__ */ jsxs16("div", { className: cn("w-fit max-w-[92%] space-y-2", isUser && "flex flex-col items-end"), children: [
3212
- /* @__PURE__ */ jsx26(ChatMessage, { message, texts: props.texts }),
3213
- /* @__PURE__ */ jsx26("div", { className: cn("flex items-center gap-2", isUser && "justify-end"), children: isGenerating ? /* @__PURE__ */ jsx26(ChatMessageTypingFooter, {}) : /* @__PURE__ */ jsxs16(Fragment5, { children: [
3214
- /* @__PURE__ */ jsx26(ChatMessageMeta, { roleLabel: message.roleLabel, timestampLabel: message.timestampLabel, isUser }),
3215
- !isUser ? /* @__PURE__ */ jsx26(ChatMessageActionCopy, { message, texts: props.texts }) : null
3743
+ return /* @__PURE__ */ jsxs18("div", { className: cn("flex gap-3", isUser ? "justify-end" : "justify-start"), children: [
3744
+ !isUser ? /* @__PURE__ */ jsx28(ChatMessageAvatar, { role: message.role }) : null,
3745
+ /* @__PURE__ */ jsxs18("div", { className: cn("w-fit max-w-[92%] space-y-2", isUser && "flex flex-col items-end"), children: [
3746
+ /* @__PURE__ */ jsx28(ChatMessage, { message, texts: props.texts }),
3747
+ /* @__PURE__ */ jsx28("div", { className: cn("flex items-center gap-2", isUser && "justify-end"), children: isGenerating ? /* @__PURE__ */ jsx28(ChatMessageTypingFooter, {}) : /* @__PURE__ */ jsxs18(Fragment6, { children: [
3748
+ /* @__PURE__ */ jsx28(ChatMessageMeta, { roleLabel: message.roleLabel, timestampLabel: message.timestampLabel, isUser }),
3749
+ !isUser ? /* @__PURE__ */ jsx28(ChatMessageActionCopy, { message, texts: props.texts }) : null
3216
3750
  ] }) })
3217
3751
  ] }),
3218
- isUser ? /* @__PURE__ */ jsx26(ChatMessageAvatar, { role: message.role }) : null
3752
+ isUser ? /* @__PURE__ */ jsx28(ChatMessageAvatar, { role: message.role }) : null
3219
3753
  ] }, message.id);
3220
3754
  }),
3221
- props.isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs16("div", { className: "flex justify-start gap-3", children: [
3222
- /* @__PURE__ */ jsx26(ChatMessageAvatar, { role: "assistant" }),
3223
- /* @__PURE__ */ jsx26("div", { className: "rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm", children: props.texts.typingLabel })
3755
+ props.isSending && !hasRenderableAssistantDraft ? /* @__PURE__ */ jsxs18("div", { className: "flex justify-start gap-3", children: [
3756
+ /* @__PURE__ */ jsx28(ChatMessageAvatar, { role: "assistant" }),
3757
+ /* @__PURE__ */ jsx28("div", { className: "rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm", children: props.texts.typingLabel })
3224
3758
  ] }) : null
3225
3759
  ] });
3226
3760
  }
3227
-
3228
- // src/components/chat/hooks/use-sticky-bottom-scroll.ts
3229
- import { useEffect as useEffect8, useLayoutEffect as useLayoutEffect2, useRef as useRef7 } from "react";
3230
- var DEFAULT_STICKY_THRESHOLD_PX = 10;
3231
- function scrollElementToBottom(element) {
3232
- element.scrollTop = element.scrollHeight;
3233
- }
3234
- function queueScrollToBottom(params) {
3235
- const element = params.scrollRef.current;
3236
- if (!element) {
3237
- return;
3238
- }
3239
- if (params.scheduledScrollFrameRef.current !== null) {
3240
- cancelAnimationFrame(params.scheduledScrollFrameRef.current);
3241
- }
3242
- params.scheduledScrollFrameRef.current = requestAnimationFrame(() => {
3243
- params.scheduledScrollFrameRef.current = null;
3244
- const currentElement = params.scrollRef.current;
3245
- if (!currentElement) {
3246
- return;
3247
- }
3248
- params.isProgrammaticScrollRef.current = true;
3249
- scrollElementToBottom(currentElement);
3250
- });
3251
- }
3252
- function useStickyBottomScroll(params) {
3253
- const isStickyRef = useRef7(true);
3254
- const isProgrammaticScrollRef = useRef7(false);
3255
- const previousResetKeyRef = useRef7(null);
3256
- const pendingInitialScrollRef = useRef7(false);
3257
- const scheduledScrollFrameRef = useRef7(null);
3258
- const onScroll = () => {
3259
- if (isProgrammaticScrollRef.current) {
3260
- isProgrammaticScrollRef.current = false;
3261
- return;
3262
- }
3263
- const element = params.scrollRef.current;
3264
- if (!element) {
3265
- return;
3266
- }
3267
- const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
3268
- isStickyRef.current = distanceFromBottom <= (params.stickyThresholdPx ?? DEFAULT_STICKY_THRESHOLD_PX);
3269
- };
3270
- useEffect8(() => {
3271
- if (previousResetKeyRef.current === params.resetKey) {
3272
- return;
3273
- }
3274
- previousResetKeyRef.current = params.resetKey;
3275
- isStickyRef.current = true;
3276
- pendingInitialScrollRef.current = true;
3277
- }, [params.resetKey]);
3278
- useEffect8(() => {
3279
- const scheduledScrollFrame = scheduledScrollFrameRef.current;
3280
- return () => {
3281
- if (scheduledScrollFrame !== null) {
3282
- cancelAnimationFrame(scheduledScrollFrame);
3283
- }
3284
- };
3285
- }, []);
3286
- useLayoutEffect2(() => {
3287
- if (!pendingInitialScrollRef.current || params.isLoading || !params.hasContent) {
3288
- return;
3289
- }
3290
- const element = params.scrollRef.current;
3291
- if (!element) {
3292
- return;
3293
- }
3294
- pendingInitialScrollRef.current = false;
3295
- queueScrollToBottom({
3296
- scrollRef: params.scrollRef,
3297
- scheduledScrollFrameRef,
3298
- isProgrammaticScrollRef
3299
- });
3300
- }, [params.hasContent, params.isLoading, params.scrollRef]);
3301
- useLayoutEffect2(() => {
3302
- if (!isStickyRef.current || !params.hasContent) {
3303
- return;
3304
- }
3305
- const element = params.scrollRef.current;
3306
- if (!element) {
3307
- return;
3308
- }
3309
- queueScrollToBottom({
3310
- scrollRef: params.scrollRef,
3311
- scheduledScrollFrameRef,
3312
- isProgrammaticScrollRef
3313
- });
3314
- }, [params.contentVersion, params.hasContent, params.scrollRef]);
3315
- return { onScroll };
3316
- }
3317
3761
  export {
3318
3762
  ChatInputBar,
3319
3763
  ChatMessageList,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/agent-chat-ui",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "private": false,
5
5
  "description": "Reusable Nextclaw agent chat UI primitives and default skin.",
6
6
  "type": "module",