@nextclaw/agent-chat-ui 0.3.5 → 0.3.7

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
@@ -102,6 +102,7 @@ type ChatSkillPickerProps = {
102
102
  };
103
103
  type ChatInputBarActionsProps = {
104
104
  sendError?: string | null;
105
+ sendErrorDetailsLabel?: string;
105
106
  isSending: boolean;
106
107
  canStopGeneration: boolean;
107
108
  sendDisabled: boolean;
@@ -164,7 +165,13 @@ type ChatFileOperationBlockViewModel = {
164
165
  display?: "preview" | "diff";
165
166
  caption?: string;
166
167
  lines: ChatFileOperationLineViewModel[];
168
+ fullLines?: ChatFileOperationLineViewModel[];
167
169
  rawText?: string;
170
+ beforeText?: string;
171
+ afterText?: string;
172
+ patchText?: string;
173
+ oldStartLine?: number;
174
+ newStartLine?: number;
168
175
  truncated?: boolean;
169
176
  };
170
177
  type ChatToolActionViewModel = {
@@ -175,6 +182,20 @@ type ChatToolActionViewModel = {
175
182
  label?: string;
176
183
  parentSessionId?: string;
177
184
  };
185
+ type ChatFileOpenActionViewModel = {
186
+ path: string;
187
+ label?: string;
188
+ viewMode: "preview" | "diff";
189
+ line?: number;
190
+ column?: number;
191
+ rawText?: string;
192
+ beforeText?: string;
193
+ afterText?: string;
194
+ patchText?: string;
195
+ oldStartLine?: number;
196
+ newStartLine?: number;
197
+ fullLines?: ChatFileOperationLineViewModel[];
198
+ };
178
199
  type ChatToolPartViewModel = {
179
200
  kind: "call" | "result";
180
201
  toolName: string;
@@ -262,6 +283,7 @@ type ChatMessageListProps = {
262
283
  texts: ChatMessageTexts;
263
284
  className?: string;
264
285
  onToolAction?: (action: ChatToolActionViewModel) => void;
286
+ onFileOpen?: (action: ChatFileOpenActionViewModel) => void;
265
287
  renderToolAgent?: (agentId: string) => ReactNode;
266
288
  };
267
289
  //#endregion
@@ -279,6 +301,36 @@ declare const ChatInputBar: _$react.ForwardRefExoticComponent<ChatInputBarProps
279
301
  //#region src/components/chat/ui/chat-message-list/chat-message-list.d.ts
280
302
  declare function ChatMessageList(props: ChatMessageListProps): _$react_jsx_runtime0.JSX.Element;
281
303
  //#endregion
304
+ //#region src/components/chat/ui/chat-message-list/chat-message-markdown.d.ts
305
+ type ChatMessageMarkdownProps = {
306
+ text: string;
307
+ role: ChatMessageRole;
308
+ texts: Pick<ChatMessageTexts, 'copyCodeLabel' | 'copiedCodeLabel'>;
309
+ inline?: boolean;
310
+ onFileOpen?: (action: ChatFileOpenActionViewModel) => void;
311
+ };
312
+ declare function ChatMessageMarkdown({
313
+ text,
314
+ role,
315
+ texts,
316
+ inline,
317
+ onFileOpen
318
+ }: ChatMessageMarkdownProps): _$react_jsx_runtime0.JSX.Element;
319
+ //#endregion
320
+ //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-file-operation-lines.d.ts
321
+ declare function FileOperationCodeSurface({
322
+ block,
323
+ layout
324
+ }: {
325
+ block: ChatFileOperationBlockViewModel;
326
+ layout?: "compact" | "workspace";
327
+ }): _$react_jsx_runtime0.JSX.Element;
328
+ declare function FileOperationLinesGrid({
329
+ block
330
+ }: {
331
+ block: ChatFileOperationBlockViewModel;
332
+ }): _$react_jsx_runtime0.JSX.Element;
333
+ //#endregion
282
334
  //#region src/components/chat/hooks/use-active-item-scroll.d.ts
283
335
  type UseActiveItemScrollParams = {
284
336
  containerRef: RefObject<HTMLElement>;
@@ -345,4 +397,4 @@ declare function resolveChatComposerSlashTrigger(nodes: ChatComposerNode[], sele
345
397
  end: number;
346
398
  } | null;
347
399
  //#endregion
348
- 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 ChatToolActionViewModel, 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 };
400
+ export { type ChatComposerNode, type ChatComposerSelection, type ChatComposerTextNode, type ChatComposerTokenKind, type ChatComposerTokenNode, type ChatFileOpenActionViewModel, type ChatFileOperationBlockViewModel, type ChatFileOperationLineViewModel, type ChatInlineContentSegmentViewModel, type ChatInlineHint, type ChatInlineTokenViewModel, ChatInputBar, type ChatInputBarActionsProps, type ChatInputBarHandle, type ChatInputBarProps, type ChatInputBarToolbarProps, ChatMessageList, type ChatMessageListProps, ChatMessageMarkdown, type ChatMessagePartViewModel, type ChatMessageRole, type ChatMessageTexts, type ChatMessageViewModel, type ChatSelectedItem, type ChatSkillPickerOption, type ChatSkillPickerProps, type ChatSlashItem, type ChatSlashMenuProps, type ChatTexts, type ChatToolActionViewModel, type ChatToolPartViewModel, type ChatToolbarAccessory, type ChatToolbarAccessoryIcon, type ChatToolbarIcon, type ChatToolbarSelect, type ChatToolbarSelectGroup, type ChatToolbarSelectOption, FileOperationCodeSurface, FileOperationLinesGrid, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
package/dist/index.js CHANGED
@@ -341,6 +341,12 @@ const ChatButton = React.forwardRef(({ className, variant, size, ...props }, ref
341
341
  ChatButton.displayName = "ChatButton";
342
342
  //#endregion
343
343
  //#region src/components/chat/ui/chat-input-bar/chat-input-bar-actions.tsx
344
+ const SEND_ERROR_PREVIEW_MAX_CHARS = 120;
345
+ function buildSendErrorPreview(value) {
346
+ const compact = value.replace(/\s+/g, " ").trim();
347
+ if (compact.length <= SEND_ERROR_PREVIEW_MAX_CHARS) return compact;
348
+ return `${compact.slice(0, SEND_ERROR_PREVIEW_MAX_CHARS - 1)}…`;
349
+ }
344
350
  function StopIcon() {
345
351
  return /* @__PURE__ */ jsx("span", {
346
352
  "aria-hidden": "true",
@@ -348,22 +354,49 @@ function StopIcon() {
348
354
  className: "block h-3 w-3 rounded-[2px] bg-gray-700 shadow-[inset_0_0_0_1px_rgba(17,24,39,0.06)]"
349
355
  });
350
356
  }
351
- function ChatInputBarActions(props) {
352
- const { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
357
+ function ChatInputBarActions({ sendError, sendErrorDetailsLabel, isSending, canStopGeneration, sendDisabled, stopDisabled, stopHint, sendButtonLabel, stopButtonLabel, onSend, onStop }) {
358
+ const { Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
359
+ const normalizedSendError = sendError?.trim() ?? "";
360
+ const sendErrorPreview = normalizedSendError ? buildSendErrorPreview(normalizedSendError) : "";
361
+ const resolvedSendErrorDetailsLabel = sendErrorDetailsLabel?.trim() || "Details";
353
362
  return /* @__PURE__ */ jsxs("div", {
354
363
  className: "flex flex-col items-end gap-1",
355
- children: [props.sendError?.trim() ? /* @__PURE__ */ jsx("div", {
356
- className: "max-w-[420px] text-right text-[11px] text-red-600",
357
- children: props.sendError
364
+ children: [normalizedSendError ? /* @__PURE__ */ jsxs("div", {
365
+ className: "flex max-w-[420px] items-start justify-end gap-2 text-right",
366
+ children: [/* @__PURE__ */ jsx("div", {
367
+ className: "min-w-0 flex-1 text-[11px] text-red-600",
368
+ children: /* @__PURE__ */ jsx("span", {
369
+ className: "block truncate",
370
+ title: normalizedSendError,
371
+ children: sendErrorPreview
372
+ })
373
+ }), /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsx(PopoverTrigger, {
374
+ asChild: true,
375
+ children: /* @__PURE__ */ jsx("button", {
376
+ type: "button",
377
+ className: "shrink-0 rounded-full border border-red-200/80 bg-red-50 px-2 py-0.5 text-[10px] font-semibold text-red-700 transition-colors hover:bg-red-100",
378
+ children: resolvedSendErrorDetailsLabel
379
+ })
380
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
381
+ align: "end",
382
+ className: "w-[min(32rem,calc(100vw-1.5rem))] border-red-100/80 p-0",
383
+ children: [/* @__PURE__ */ jsx("div", {
384
+ className: "border-b border-red-100 bg-red-50/80 px-4 py-2 text-xs font-semibold text-red-700",
385
+ children: resolvedSendErrorDetailsLabel
386
+ }), /* @__PURE__ */ jsx("pre", {
387
+ className: "max-h-80 overflow-auto whitespace-pre-wrap break-words px-4 py-3 text-xs leading-relaxed text-red-700",
388
+ children: normalizedSendError
389
+ })]
390
+ })] })]
358
391
  }) : null, /* @__PURE__ */ jsx("div", {
359
392
  className: "flex items-center gap-2",
360
- children: props.isSending ? props.canStopGeneration ? /* @__PURE__ */ jsx(ChatButton, {
393
+ children: isSending ? canStopGeneration ? /* @__PURE__ */ jsx(ChatButton, {
361
394
  size: "icon",
362
395
  variant: "outline",
363
396
  className: "h-8 w-8 rounded-full",
364
- "aria-label": props.stopButtonLabel,
365
- onClick: () => void props.onStop(),
366
- disabled: props.stopDisabled,
397
+ "aria-label": stopButtonLabel,
398
+ onClick: () => void onStop(),
399
+ disabled: stopDisabled,
367
400
  children: /* @__PURE__ */ jsx(StopIcon, {})
368
401
  }) : /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
369
402
  asChild: true,
@@ -371,7 +404,7 @@ function ChatInputBarActions(props) {
371
404
  size: "icon",
372
405
  variant: "outline",
373
406
  className: "h-8 w-8 rounded-full",
374
- "aria-label": props.stopButtonLabel,
407
+ "aria-label": stopButtonLabel,
375
408
  disabled: true,
376
409
  children: /* @__PURE__ */ jsx(StopIcon, {})
377
410
  }) })
@@ -379,14 +412,14 @@ function ChatInputBarActions(props) {
379
412
  side: "top",
380
413
  children: /* @__PURE__ */ jsx("p", {
381
414
  className: "text-xs",
382
- children: props.stopHint
415
+ children: stopHint
383
416
  })
384
417
  })] }) }) : /* @__PURE__ */ jsx(ChatButton, {
385
418
  size: "icon",
386
419
  className: "h-8 w-8 rounded-full",
387
- "aria-label": props.sendButtonLabel,
388
- onClick: () => void props.onSend(),
389
- disabled: props.sendDisabled,
420
+ "aria-label": sendButtonLabel,
421
+ onClick: () => void onSend(),
422
+ disabled: sendDisabled,
390
423
  children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-5 w-5" })
391
424
  })
392
425
  })]
@@ -2047,7 +2080,26 @@ function resolveSafeHref(href) {
2047
2080
  function isExternalHref(href) {
2048
2081
  return /^https?:\/\//i.test(href);
2049
2082
  }
2050
- function ChatMessageMarkdown({ text, role, texts, inline = false }) {
2083
+ function looksLikeLocalFileHref(href) {
2084
+ return href.startsWith("./") || href.startsWith("../") || href.startsWith("/Users/") || href.startsWith("/home/") || href.startsWith("/tmp/") || href.startsWith("/var/") || /^\/.+\.[A-Za-z0-9_-]+(?::\d+(?::\d+)?)?$/.test(href);
2085
+ }
2086
+ function parseLocalFileAction(href) {
2087
+ const normalizedHref = href.split("#")[0]?.split("?")[0] ?? href;
2088
+ const decodedHref = decodeURIComponent(normalizedHref);
2089
+ if (!looksLikeLocalFileHref(decodedHref)) return null;
2090
+ const lineMatch = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(decodedHref);
2091
+ const rawPath = lineMatch?.[1] ?? decodedHref;
2092
+ const line = lineMatch?.[2] ? Number(lineMatch[2]) : void 0;
2093
+ const column = lineMatch?.[3] ? Number(lineMatch[3]) : void 0;
2094
+ return {
2095
+ path: rawPath,
2096
+ label: rawPath.split("/").filter(Boolean).pop() ?? rawPath,
2097
+ viewMode: "preview",
2098
+ ...typeof line === "number" ? { line } : {},
2099
+ ...typeof column === "number" ? { column } : {}
2100
+ };
2101
+ }
2102
+ function ChatMessageMarkdown({ text, role, texts, inline = false, onFileOpen }) {
2051
2103
  const isUser = role === "user";
2052
2104
  const markdownComponents = useMemo(() => ({
2053
2105
  p: ({ children }) => inline ? /* @__PURE__ */ jsx(Fragment$1, { children }) : /* @__PURE__ */ jsx("p", { children }),
@@ -2058,9 +2110,17 @@ function ChatMessageMarkdown({ text, role, texts, inline = false }) {
2058
2110
  children
2059
2111
  });
2060
2112
  const external = isExternalHref(safeHref);
2113
+ const localFileAction = external ? null : parseLocalFileAction(safeHref);
2114
+ const handleClick = (event) => {
2115
+ if (!onFileOpen || !localFileAction) return;
2116
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
2117
+ event.preventDefault();
2118
+ onFileOpen(localFileAction);
2119
+ };
2061
2120
  return /* @__PURE__ */ jsx("a", {
2062
2121
  ...rest,
2063
2122
  href: safeHref,
2123
+ onClick: handleClick,
2064
2124
  target: external ? "_blank" : void 0,
2065
2125
  rel: external ? "noreferrer noopener" : void 0,
2066
2126
  children
@@ -2110,7 +2170,11 @@ function ChatMessageMarkdown({ text, role, texts, inline = false }) {
2110
2170
  children
2111
2171
  });
2112
2172
  }
2113
- }), [inline, texts]);
2173
+ }), [
2174
+ inline,
2175
+ onFileOpen,
2176
+ texts
2177
+ ]);
2114
2178
  return /* @__PURE__ */ jsx(inline ? "span" : "div", {
2115
2179
  className: cn("chat-markdown", isUser ? "chat-markdown-user" : "chat-markdown-assistant"),
2116
2180
  children: /* @__PURE__ */ jsx(ReactMarkdown, {
@@ -2158,7 +2222,7 @@ function ChatInlineTokenBadge({ kind, label, isUser }) {
2158
2222
  })]
2159
2223
  });
2160
2224
  }
2161
- function ChatMessageInlineContent({ segments, role, texts }) {
2225
+ function ChatMessageInlineContent({ segments, role, texts, onFileOpen }) {
2162
2226
  const isUser = role === "user";
2163
2227
  return /* @__PURE__ */ jsx("div", {
2164
2228
  className: "whitespace-pre-wrap break-words leading-6",
@@ -2176,7 +2240,8 @@ function ChatMessageInlineContent({ segments, role, texts }) {
2176
2240
  text: segment.text,
2177
2241
  role,
2178
2242
  texts,
2179
- inline: true
2243
+ inline: true,
2244
+ onFileOpen
2180
2245
  }, `markdown-${index}`);
2181
2246
  })
2182
2247
  });
@@ -2568,7 +2633,7 @@ function ChatReasoningBlock({ label, text, isUser, isInProgress }) {
2568
2633
  ref: scrollRef,
2569
2634
  onScroll,
2570
2635
  "data-reasoning-scroll": "true",
2571
- className: cn("mt-2 w-fit max-w-[500px] max-h-56 overflow-y-auto overscroll-contain rounded-lg custom-scrollbar-amber", isUser ? "bg-primary-700/60" : "bg-gray-100"),
2636
+ className: cn("mt-2 w-fit max-w-[500px] max-h-56 overflow-y-auto rounded-lg custom-scrollbar-amber", isUser ? "bg-primary-700/60" : "bg-gray-100"),
2572
2637
  children: /* @__PURE__ */ jsx("pre", {
2573
2638
  className: "min-w-0 whitespace-pre-wrap break-all p-2 text-[11px]",
2574
2639
  children: text
@@ -2648,15 +2713,15 @@ function ToolCardSessionActionButton({ action, onAction }) {
2648
2713
  function ToolCardHeader({ card, icon: Icon, expanded, canExpand, hideSummary = false, actionSlot, onToggle }) {
2649
2714
  const summaryPart = hideSummary ? "" : card.summary?.replace(/^(command|path|args|query|input):\s*/i, "") ?? "";
2650
2715
  return /* @__PURE__ */ jsxs("div", {
2651
- className: cn("flex items-center justify-between px-3 py-2.5 transition-colors bg-transparent", canExpand ? "cursor-pointer hover:bg-amber-100/30" : ""),
2716
+ className: cn("flex items-center gap-3 px-3 py-2.5 transition-colors bg-transparent", canExpand ? "cursor-pointer hover:bg-amber-100/30" : ""),
2652
2717
  onClick: onToggle,
2653
2718
  children: [/* @__PURE__ */ jsxs("div", {
2654
- className: "flex items-center gap-2 font-mono min-w-0 max-w-[calc(100%-80px)] text-amber-950/80",
2719
+ className: "flex min-w-0 flex-1 items-center gap-2 overflow-hidden font-mono text-amber-950/80",
2655
2720
  children: [/* @__PURE__ */ jsx(Icon, {
2656
2721
  className: "h-4 w-4 text-amber-600/80 shrink-0",
2657
2722
  strokeWidth: 3
2658
2723
  }), /* @__PURE__ */ jsxs("div", {
2659
- className: "flex items-center gap-1.5 min-w-0",
2724
+ className: "flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden",
2660
2725
  children: [/* @__PURE__ */ jsx("span", {
2661
2726
  className: "font-bold shrink-0 tracking-tight",
2662
2727
  children: card.toolName
@@ -2664,7 +2729,7 @@ function ToolCardHeader({ card, icon: Icon, expanded, canExpand, hideSummary = f
2664
2729
  className: "text-amber-300 font-bold select-none shrink-0",
2665
2730
  children: "›"
2666
2731
  }), /* @__PURE__ */ jsx("span", {
2667
- className: "truncate flex-1 min-w-0 font-normal",
2732
+ className: "block min-w-0 flex-1 truncate font-normal",
2668
2733
  title: summaryPart,
2669
2734
  children: summaryPart
2670
2735
  })] })]
@@ -2693,8 +2758,9 @@ function ToolCardHeaderSessionAction({ action, onAction }) {
2693
2758
  }
2694
2759
  //#endregion
2695
2760
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-file-operation-lines.tsx
2696
- const FILE_ROW_CLASS_NAME = "h-5 font-mono text-[11px] leading-5";
2697
- const 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";
2761
+ const FILE_TEXT_CLASS_NAME = "font-mono text-[11px] leading-5";
2762
+ const FILE_ROW_CLASS_NAME = `h-5 ${FILE_TEXT_CLASS_NAME}`;
2763
+ const FILE_LINE_NUMBER_CELL_CLASS_NAME = "flex h-5 items-center justify-center px-2.5 tabular-nums select-none";
2698
2764
  function formatLineNumber(value) {
2699
2765
  return typeof value === "number" ? String(value) : "";
2700
2766
  }
@@ -2731,6 +2797,22 @@ function getCodeRowTone(line) {
2731
2797
  if (line.kind === "add") return "bg-emerald-50 text-emerald-950";
2732
2798
  return "bg-white text-amber-950/80";
2733
2799
  }
2800
+ function FileOperationLineNumberCell({ layout, line, lineNumberColumnWidth }) {
2801
+ const lineNumberCellClassName = layout === "compact" ? "sticky left-0 z-[1]" : "w-full shrink-0";
2802
+ return /* @__PURE__ */ jsx("span", {
2803
+ "data-file-line-number-cell": "true",
2804
+ style: layout === "compact" ? { width: lineNumberColumnWidth } : void 0,
2805
+ className: cn(FILE_LINE_NUMBER_CELL_CLASS_NAME, lineNumberCellClassName, getLineNumberTone(line)),
2806
+ children: readVisibleLineNumber(line)
2807
+ });
2808
+ }
2809
+ function FileOperationCodeCell({ line }) {
2810
+ return /* @__PURE__ */ jsx("span", {
2811
+ "data-file-code-row": "true",
2812
+ className: cn("block min-w-full whitespace-pre px-2.5", getCodeRowTone(line)),
2813
+ children: line.text || " "
2814
+ });
2815
+ }
2734
2816
  function FileOperationLineRow({ line, showLineNumbers, lineNumberColumnWidth }) {
2735
2817
  return /* @__PURE__ */ jsxs("div", {
2736
2818
  "data-file-line-row": "true",
@@ -2739,22 +2821,50 @@ function FileOperationLineRow({ line, showLineNumbers, lineNumberColumnWidth })
2739
2821
  showLineNumbers,
2740
2822
  lineNumberColumnWidth
2741
2823
  }),
2742
- children: [showLineNumbers ? /* @__PURE__ */ jsx("span", {
2743
- "data-file-line-number-cell": "true",
2744
- style: { width: lineNumberColumnWidth },
2745
- className: cn(FILE_GUTTER_NUMBER_CELL_CLASS_NAME, getLineNumberTone(line)),
2746
- children: readVisibleLineNumber(line)
2747
- }) : null, /* @__PURE__ */ jsx("span", {
2748
- "data-file-code-row": "true",
2749
- className: cn("block min-w-full whitespace-pre px-2.5", getCodeRowTone(line)),
2750
- children: line.text || " "
2824
+ children: [showLineNumbers ? /* @__PURE__ */ jsx(FileOperationLineNumberCell, {
2825
+ layout: "compact",
2826
+ line,
2827
+ lineNumberColumnWidth
2828
+ }) : null, /* @__PURE__ */ jsx(FileOperationCodeCell, { line })]
2829
+ });
2830
+ }
2831
+ function FileOperationWorkspaceSurface({ block }) {
2832
+ const showLineNumbers = readHasBlockLineNumbers(block);
2833
+ const lineNumberColumnWidth = readLineNumberColumnWidth(block);
2834
+ return /* @__PURE__ */ jsxs("div", {
2835
+ "data-file-code-surface": "true",
2836
+ "data-file-code-surface-layout": "workspace",
2837
+ className: "flex h-full min-h-full min-w-full bg-white",
2838
+ children: [showLineNumbers ? /* @__PURE__ */ jsxs("div", {
2839
+ "data-file-code-gutter": "true",
2840
+ style: {
2841
+ width: lineNumberColumnWidth,
2842
+ minWidth: lineNumberColumnWidth
2843
+ },
2844
+ className: cn("flex h-full min-h-full shrink-0 flex-col", FILE_TEXT_CLASS_NAME),
2845
+ children: [block.lines.map((line, index) => /* @__PURE__ */ jsx(FileOperationLineNumberCell, {
2846
+ layout: "workspace",
2847
+ line,
2848
+ lineNumberColumnWidth
2849
+ }, readLineKey("gutter", line, index))), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 border-r border-stone-200 bg-stone-100" })]
2850
+ }) : null, /* @__PURE__ */ jsxs("div", {
2851
+ "data-file-code-canvas": "true",
2852
+ className: "flex h-full min-h-full min-w-0 flex-1 flex-col bg-white",
2853
+ children: [block.lines.map((line, index) => /* @__PURE__ */ jsx("div", {
2854
+ "data-file-code-canvas-row": "true",
2855
+ className: FILE_ROW_CLASS_NAME,
2856
+ children: /* @__PURE__ */ jsx(FileOperationCodeCell, { line })
2857
+ }, readLineKey("code", line, index))), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-white" })]
2751
2858
  })]
2752
2859
  });
2753
2860
  }
2754
- function FileOperationLinesGrid({ block }) {
2861
+ function FileOperationCodeSurface({ block, layout = "compact" }) {
2862
+ if (layout === "workspace") return /* @__PURE__ */ jsx(FileOperationWorkspaceSurface, { block });
2755
2863
  const showLineNumbers = readHasBlockLineNumbers(block);
2756
2864
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
2757
2865
  return /* @__PURE__ */ jsx("div", {
2866
+ "data-file-code-surface": "true",
2867
+ "data-file-code-surface-layout": "compact",
2758
2868
  className: "overflow-x-auto custom-scrollbar-amber bg-white",
2759
2869
  children: block.lines.map((line, index) => /* @__PURE__ */ jsx(FileOperationLineRow, {
2760
2870
  line,
@@ -2763,6 +2873,12 @@ function FileOperationLinesGrid({ block }) {
2763
2873
  }, readLineKey("row", line, index)))
2764
2874
  });
2765
2875
  }
2876
+ function FileOperationLinesGrid({ block }) {
2877
+ return /* @__PURE__ */ jsx(FileOperationCodeSurface, {
2878
+ block,
2879
+ layout: "compact"
2880
+ });
2881
+ }
2766
2882
  //#endregion
2767
2883
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-file-operation.tsx
2768
2884
  function isPreviewBlock(block) {
@@ -2796,6 +2912,20 @@ function renderCaption(caption) {
2796
2912
  })] }, `${part}-${index}`))
2797
2913
  });
2798
2914
  }
2915
+ function buildFileOpenAction(block) {
2916
+ return {
2917
+ path: block.path,
2918
+ label: block.path.split("/").filter(Boolean).pop() ?? block.path,
2919
+ viewMode: block.display === "diff" ? "diff" : "preview",
2920
+ ...block.rawText ? { rawText: block.rawText } : {},
2921
+ ...block.beforeText ? { beforeText: block.beforeText } : {},
2922
+ ...block.afterText ? { afterText: block.afterText } : {},
2923
+ ...block.patchText ? { patchText: block.patchText } : {},
2924
+ ...typeof block.oldStartLine === "number" ? { oldStartLine: block.oldStartLine } : {},
2925
+ ...typeof block.newStartLine === "number" ? { newStartLine: block.newStartLine } : {},
2926
+ fullLines: block.fullLines ?? block.lines
2927
+ };
2928
+ }
2799
2929
  function StickyFileOperationScrollArea({ children, contentVersion, resetKey, className, scrollKind }) {
2800
2930
  const scrollRef = useRef(null);
2801
2931
  const { onScroll } = useStickyBottomScroll({
@@ -2810,14 +2940,18 @@ function StickyFileOperationScrollArea({ children, contentVersion, resetKey, cla
2810
2940
  ref: scrollRef,
2811
2941
  onScroll,
2812
2942
  "data-file-scroll-kind": scrollKind,
2813
- className: cn("overflow-y-auto overscroll-contain bg-white custom-scrollbar-amber", className),
2943
+ className: cn("overflow-y-auto bg-white custom-scrollbar-amber", className),
2814
2944
  children
2815
2945
  });
2816
2946
  }
2817
- function FileOperationBlock({ block, showPathRow, isFirst }) {
2947
+ function FileOperationBlock({ block, showPathRow, isFirst, onFileOpen }) {
2818
2948
  const { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
2819
2949
  const previewBlock = isPreviewBlock(block);
2820
2950
  const showMetaRow = showPathRow || Boolean(block.caption);
2951
+ const handlePathClick = () => {
2952
+ if (!onFileOpen) return;
2953
+ onFileOpen(buildFileOpenAction(block));
2954
+ };
2821
2955
  return /* @__PURE__ */ jsxs("section", {
2822
2956
  className: cn("overflow-hidden bg-white", !isFirst && "border-t border-stone-200/80"),
2823
2957
  children: [
@@ -2829,8 +2963,10 @@ function FileOperationBlock({ block, showPathRow, isFirst }) {
2829
2963
  delayDuration: 250,
2830
2964
  children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
2831
2965
  asChild: true,
2832
- children: /* @__PURE__ */ jsx("div", {
2833
- className: "truncate whitespace-nowrap font-mono text-[12px] font-medium text-stone-700",
2966
+ children: /* @__PURE__ */ jsx("button", {
2967
+ type: "button",
2968
+ onClick: handlePathClick,
2969
+ className: cn("w-full truncate whitespace-nowrap text-left font-mono text-[12px] font-medium text-stone-700", onFileOpen && "transition-colors hover:text-amber-900 hover:underline"),
2834
2970
  title: block.path,
2835
2971
  children: block.path
2836
2972
  })
@@ -2868,7 +3004,7 @@ function FileOperationBlock({ block, showPathRow, isFirst }) {
2868
3004
  ]
2869
3005
  });
2870
3006
  }
2871
- function ToolCardFileOperationContent({ card, className }) {
3007
+ function ToolCardFileOperationContent({ card, className, onFileOpen }) {
2872
3008
  const blocks = card.fileOperation?.blocks ?? [];
2873
3009
  const output = card.output?.trim() ?? "";
2874
3010
  if (blocks.length === 0 && !output) return null;
@@ -2878,7 +3014,8 @@ function ToolCardFileOperationContent({ card, className }) {
2878
3014
  return /* @__PURE__ */ jsx(FileOperationBlock, {
2879
3015
  block,
2880
3016
  showPathRow: true,
2881
- isFirst: index === 0
3017
+ isFirst: index === 0,
3018
+ onFileOpen
2882
3019
  }, block.key);
2883
3020
  }), output ? /* @__PURE__ */ jsx(StickyFileOperationScrollArea, {
2884
3021
  resetKey: `output:${card.toolName}:${card.summary ?? "none"}`,
@@ -3074,7 +3211,7 @@ function TerminalExecutionView({ card }) {
3074
3211
  children: [output, isRunning && /* @__PURE__ */ jsx("span", { className: "inline-block w-1.5 h-3 ml-1 bg-amber-500/60 animate-pulse align-middle" })]
3075
3212
  }) })] })] });
3076
3213
  }
3077
- function FileOperationView({ card }) {
3214
+ function FileOperationView({ card, onFileOpen }) {
3078
3215
  const output = card.output?.trim() ?? "";
3079
3216
  const isRunning = card.statusTone === "running";
3080
3217
  const hasStructuredPreview = Boolean(card.fileOperation?.blocks.length);
@@ -3102,7 +3239,10 @@ function FileOperationView({ card }) {
3102
3239
  onToggle
3103
3240
  }), expanded && hasContent && /* @__PURE__ */ jsx(ToolCardContent, {
3104
3241
  className: "border-t border-amber-200/20 bg-transparent px-0 pt-0 pb-0",
3105
- children: /* @__PURE__ */ jsx(ToolCardFileOperationContent, { card })
3242
+ children: /* @__PURE__ */ jsx(ToolCardFileOperationContent, {
3243
+ card,
3244
+ onFileOpen
3245
+ })
3106
3246
  })] });
3107
3247
  }
3108
3248
  function SearchSnippetView({ card }) {
@@ -3181,9 +3321,12 @@ function isSearchTool(name) {
3181
3321
  const lowered = name.toLowerCase();
3182
3322
  return lowered === "grep_search" || lowered === "find_files" || lowered.includes("search");
3183
3323
  }
3184
- function ChatToolCard({ card, onToolAction, renderToolAgent }) {
3324
+ function ChatToolCard({ card, onToolAction, onFileOpen, renderToolAgent }) {
3185
3325
  if (isTerminalTool(card.toolName)) return /* @__PURE__ */ jsx(TerminalExecutionView, { card });
3186
- if (isFileEditTool(card.toolName)) return /* @__PURE__ */ jsx(FileOperationView, { card });
3326
+ if (isFileEditTool(card.toolName)) return /* @__PURE__ */ jsx(FileOperationView, {
3327
+ card,
3328
+ onFileOpen
3329
+ });
3187
3330
  if (isSearchTool(card.toolName)) return /* @__PURE__ */ jsx(SearchSnippetView, { card });
3188
3331
  return /* @__PURE__ */ jsx(GenericToolCard, {
3189
3332
  card,
@@ -3212,7 +3355,7 @@ function ChatUnknownPart(props) {
3212
3355
  //#endregion
3213
3356
  //#region src/components/chat/ui/chat-message-list/chat-message.tsx
3214
3357
  const ChatMessage = memo(function ChatMessage(props) {
3215
- const { message, texts, onToolAction, renderToolAgent } = props;
3358
+ const { message, texts, onToolAction, onFileOpen, renderToolAgent } = props;
3216
3359
  const { role } = message;
3217
3360
  const isUser = role === "user";
3218
3361
  const isMessageInProgress = message.status === "pending" || message.status === "streaming";
@@ -3225,12 +3368,14 @@ const ChatMessage = memo(function ChatMessage(props) {
3225
3368
  if (type === "markdown") return /* @__PURE__ */ jsx(ChatMessageMarkdown, {
3226
3369
  text: part.text,
3227
3370
  role,
3228
- texts
3371
+ texts,
3372
+ onFileOpen
3229
3373
  }, `markdown-${index}`);
3230
3374
  if (type === "inline-content") return /* @__PURE__ */ jsx(ChatMessageInlineContent, {
3231
3375
  segments: part.segments,
3232
3376
  role,
3233
- texts
3377
+ texts,
3378
+ onFileOpen
3234
3379
  }, `inline-content-${index}`);
3235
3380
  if (type === "reasoning") return /* @__PURE__ */ jsx(ChatReasoningBlock, {
3236
3381
  label: part.label,
@@ -3243,6 +3388,7 @@ const ChatMessage = memo(function ChatMessage(props) {
3243
3388
  children: /* @__PURE__ */ jsx(ChatToolCard, {
3244
3389
  card: part.card,
3245
3390
  onToolAction,
3391
+ onFileOpen,
3246
3392
  renderToolAgent
3247
3393
  })
3248
3394
  }, `tool-${index}`);
@@ -3339,6 +3485,7 @@ function ChatMessageList(props) {
3339
3485
  message,
3340
3486
  texts: props.texts,
3341
3487
  onToolAction: props.onToolAction,
3488
+ onFileOpen: props.onFileOpen,
3342
3489
  renderToolAgent: props.renderToolAgent
3343
3490
  }), /* @__PURE__ */ jsx("div", {
3344
3491
  className: cn("flex items-center gap-2", isUser && "justify-end"),
@@ -3365,4 +3512,4 @@ function ChatMessageList(props) {
3365
3512
  });
3366
3513
  }
3367
3514
  //#endregion
3368
- export { ChatInputBar, ChatMessageList, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
3515
+ export { ChatInputBar, ChatMessageList, ChatMessageMarkdown, FileOperationCodeSurface, FileOperationLinesGrid, copyText, createChatComposerNodesFromText, createChatComposerTextNode, createChatComposerTokenNode, createEmptyChatComposerNodes, extractChatComposerTokenKeys, normalizeChatComposerNodes, removeChatComposerTokenNodes, replaceChatComposerRange, resolveChatComposerSlashTrigger, serializeChatComposerDocument, serializeChatComposerPlainText, useActiveItemScroll, useCopyFeedback, useElementWidth, useStickyBottomScroll };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/agent-chat-ui",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
5
  "description": "Reusable Nextclaw agent chat UI primitives and default skin.",
6
6
  "type": "module",