@nextclaw/agent-chat-ui 0.6.4 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { Fragment, forwardRef, memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { Fragment, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { clsx } from "clsx";
4
4
  import { twMerge } from "tailwind-merge";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -12,7 +12,7 @@ import { ContentEditable } from "@lexical/react/LexicalContentEditable";
12
12
  import { LexicalComposer } from "@lexical/react/LexicalComposer";
13
13
  import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
14
14
  import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
15
- import { $applyNodeReplacement, $createLineBreakNode, $createParagraphNode, $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isTextNode, $setSelection, BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, DecoratorNode, KEY_DOWN_COMMAND, SELECTION_CHANGE_COMMAND, mergeRegister } from "lexical";
15
+ import { $applyNodeReplacement, $createLineBreakNode, $createParagraphNode, $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isTextNode, $setSelection, BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, DecoratorNode, KEY_DOWN_COMMAND, SELECTION_CHANGE_COMMAND, SKIP_DOM_SELECTION_TAG, mergeRegister } from "lexical";
16
16
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
17
17
  import ReactMarkdown from "react-markdown";
18
18
  import remarkGfm from "remark-gfm";
@@ -1625,12 +1625,12 @@ function writeChatComposerStateToLexicalRoot(nodes, selection) {
1625
1625
  nextSelection.focus.set(focus.key, focus.offset, focus.type);
1626
1626
  $setSelection(nextSelection);
1627
1627
  }
1628
- function syncLexicalEditorFromChatComposerState(editor, nodes, selection) {
1628
+ function syncLexicalEditorFromChatComposerState(editor, nodes, selection, preserveDomSelection = false) {
1629
1629
  editor.update(() => {
1630
1630
  writeChatComposerStateToLexicalRoot(nodes, selection);
1631
- });
1631
+ }, preserveDomSelection ? { tag: SKIP_DOM_SELECTION_TAG } : void 0);
1632
1632
  }
1633
- function syncLexicalSelectionFromChatComposerSelection(editor, selection) {
1633
+ function syncLexicalSelectionFromChatComposerSelection(editor, selection, preserveDomSelection = false) {
1634
1634
  editor.update(() => {
1635
1635
  const nextSelection = $createRangeSelection();
1636
1636
  const anchor = buildSelectionPointFromOffset(selection.start);
@@ -1638,7 +1638,7 @@ function syncLexicalSelectionFromChatComposerSelection(editor, selection) {
1638
1638
  nextSelection.anchor.set(anchor.key, anchor.offset, anchor.type);
1639
1639
  nextSelection.focus.set(focus.key, focus.offset, focus.type);
1640
1640
  $setSelection(nextSelection);
1641
- });
1641
+ }, preserveDomSelection ? { tag: SKIP_DOM_SELECTION_TAG } : void 0);
1642
1642
  }
1643
1643
  //#endregion
1644
1644
  //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-lexical-operations.ts
@@ -1949,11 +1949,12 @@ var ChatComposerLexicalOwner = class {
1949
1949
  const shouldSyncDocument = nextSignature !== this.editorSignatureRef.current;
1950
1950
  if (!shouldSyncDocument && !pendingSelection) return;
1951
1951
  this.startApplyingExternalUpdate();
1952
+ const preserveDomSelection = editor.getRootElement() !== document.activeElement;
1952
1953
  if (shouldSyncDocument) {
1953
- syncLexicalEditorFromChatComposerState(editor, nodes, pendingSelection);
1954
+ syncLexicalEditorFromChatComposerState(editor, nodes, pendingSelection, preserveDomSelection);
1954
1955
  this.editorSignatureRef.current = nextSignature;
1955
1956
  this.lastPublishedSignatureRef.current = nextSignature;
1956
- } else if (pendingSelection) syncLexicalSelectionFromChatComposerSelection(editor, pendingSelection);
1957
+ } else if (pendingSelection) syncLexicalSelectionFromChatComposerSelection(editor, pendingSelection, preserveDomSelection);
1957
1958
  if (pendingSelection) {
1958
1959
  this.selectionRef.current = pendingSelection;
1959
1960
  this.pendingSelectionRef.current = null;
@@ -2780,7 +2781,7 @@ function isRecord$1(value) {
2780
2781
  function readString(value) {
2781
2782
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2782
2783
  }
2783
- function readPositiveInteger(value) {
2784
+ function readPositiveInteger$1(value) {
2784
2785
  return Number.isInteger(value) && Number(value) > 0 ? Number(value) : void 0;
2785
2786
  }
2786
2787
  function readFilePreviewViewer(value) {
@@ -2800,8 +2801,8 @@ function readFileTarget(record) {
2800
2801
  type: "file",
2801
2802
  payload: {
2802
2803
  path,
2803
- line: readPositiveInteger(payload.line),
2804
- column: readPositiveInteger(payload.column),
2804
+ line: readPositiveInteger$1(payload.line),
2805
+ column: readPositiveInteger$1(payload.column),
2805
2806
  viewer: readFilePreviewViewer(payload.viewer)
2806
2807
  }
2807
2808
  };
@@ -2936,6 +2937,114 @@ function ChatInlineDisplay({ display, renderInlineDisplay }) {
2936
2937
  return /* @__PURE__ */ jsx(ChatInlineDisplayFallback, { display });
2937
2938
  }
2938
2939
  //#endregion
2940
+ //#region src/components/chat/ui/chat-message-list/mermaid/chat-mermaid-diagram.tsx
2941
+ const STREAMING_RENDER_DELAY_MS = 300;
2942
+ let mermaidModulePromise = null;
2943
+ function loadMermaid() {
2944
+ mermaidModulePromise ??= import("mermaid").then((module) => module.default);
2945
+ return mermaidModulePromise;
2946
+ }
2947
+ function readMermaidTheme() {
2948
+ if (typeof document === "undefined") return "default";
2949
+ const root = document.documentElement;
2950
+ return root.getAttribute("data-theme-appearance") === "dark" || root.classList.contains("dark") ? "dark" : "default";
2951
+ }
2952
+ function useMermaidTheme() {
2953
+ const [theme, setTheme] = useState(readMermaidTheme);
2954
+ useEffect(() => {
2955
+ const root = document.documentElement;
2956
+ const observer = new MutationObserver(() => setTheme(readMermaidTheme()));
2957
+ observer.observe(root, {
2958
+ attributes: true,
2959
+ attributeFilter: ["class", "data-theme-appearance"]
2960
+ });
2961
+ return () => observer.disconnect();
2962
+ }, []);
2963
+ return theme;
2964
+ }
2965
+ function ChatMermaidDiagram({ isStreaming, source, texts }) {
2966
+ const reactId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
2967
+ const renderSequence = useRef(0);
2968
+ const theme = useMermaidTheme();
2969
+ const normalizedSource = source.trim();
2970
+ const renderKey = `${theme}:${normalizedSource}`;
2971
+ const [state, setState] = useState(null);
2972
+ const diagramLabel = texts.mermaidDiagramLabel ?? "Mermaid diagram";
2973
+ const errorLabel = texts.mermaidRenderErrorLabel ?? "Diagram could not be rendered";
2974
+ useEffect(() => {
2975
+ let active = true;
2976
+ const renderDiagram = async () => {
2977
+ try {
2978
+ const mermaid = await loadMermaid();
2979
+ if (!active) return;
2980
+ renderSequence.current += 1;
2981
+ const diagramId = `nextclaw-mermaid-${reactId}-${renderSequence.current}`;
2982
+ mermaid.initialize({
2983
+ securityLevel: "strict",
2984
+ startOnLoad: false,
2985
+ suppressErrorRendering: true,
2986
+ theme
2987
+ });
2988
+ const valid = await mermaid.parse(normalizedSource, { suppressErrors: true });
2989
+ if (!active) return;
2990
+ if (!valid) throw new Error("Invalid Mermaid diagram");
2991
+ const { svg } = await mermaid.render(diagramId, normalizedSource);
2992
+ if (active) setState({
2993
+ key: renderKey,
2994
+ status: "rendered",
2995
+ svg
2996
+ });
2997
+ } catch {
2998
+ if (active && !isStreaming) setState({
2999
+ key: renderKey,
3000
+ status: "error"
3001
+ });
3002
+ }
3003
+ };
3004
+ const timeout = window.setTimeout(() => void renderDiagram(), isStreaming ? STREAMING_RENDER_DELAY_MS : 0);
3005
+ return () => {
3006
+ active = false;
3007
+ window.clearTimeout(timeout);
3008
+ };
3009
+ }, [
3010
+ isStreaming,
3011
+ normalizedSource,
3012
+ reactId,
3013
+ renderKey,
3014
+ theme
3015
+ ]);
3016
+ const currentState = state?.key === renderKey ? state : null;
3017
+ if (currentState?.status === "error") return /* @__PURE__ */ jsxs("div", {
3018
+ "data-chat-mermaid-error": "true",
3019
+ className: "my-3 overflow-hidden rounded-xl border border-border bg-muted/25",
3020
+ children: [/* @__PURE__ */ jsx("div", {
3021
+ className: "border-b border-border px-3 py-2 text-xs text-muted-foreground",
3022
+ children: errorLabel
3023
+ }), /* @__PURE__ */ jsx(ChatCodeBlock, {
3024
+ className: "language-mermaid",
3025
+ texts,
3026
+ children: normalizedSource
3027
+ })]
3028
+ });
3029
+ const renderedState = state?.status === "rendered" ? state : null;
3030
+ if (!renderedState) return /* @__PURE__ */ jsx(ChatCodeBlock, {
3031
+ className: "language-mermaid",
3032
+ texts,
3033
+ children: normalizedSource
3034
+ });
3035
+ return /* @__PURE__ */ jsx("figure", {
3036
+ "aria-label": diagramLabel,
3037
+ "aria-busy": !currentState,
3038
+ "data-chat-mermaid-diagram": "true",
3039
+ "data-chat-mermaid-updating": !currentState || void 0,
3040
+ className: "my-3 min-h-24 overflow-auto rounded-xl border border-border bg-muted/20 p-3",
3041
+ children: /* @__PURE__ */ jsx("div", {
3042
+ className: "min-w-fit [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full",
3043
+ dangerouslySetInnerHTML: { __html: renderedState.svg }
3044
+ })
3045
+ });
3046
+ }
3047
+ //#endregion
2939
3048
  //#region src/components/chat/ui/chat-message-list/chat-message-file/chat-message-image-preview.tsx
2940
3049
  function ChatMessageImageLightbox({ alt, closeLabel, onClose, src }) {
2941
3050
  const titleId = useId();
@@ -3042,7 +3151,7 @@ function ChatMessageImagePreview({ alt, expandLabel, closeLabel, sizeLabel, src
3042
3151
  }
3043
3152
  //#endregion
3044
3153
  //#region src/components/chat/ui/chat-message-list/utils/chat-local-resource.utils.ts
3045
- const SAFE_LINK_PROTOCOLS = new Set([
3154
+ const SAFE_BROWSER_LINK_PROTOCOLS = new Set([
3046
3155
  "http:",
3047
3156
  "https:",
3048
3157
  "mailto:",
@@ -3050,37 +3159,90 @@ const SAFE_LINK_PROTOCOLS = new Set([
3050
3159
  ]);
3051
3160
  const URI_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
3052
3161
  const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[a-z]:[\\/]/i;
3162
+ const WINDOWS_FILE_URI_PATH_PATTERN = /^\/[a-z]:[\\/]/i;
3163
+ const FILE_URI_PROTOCOL = "file:";
3053
3164
  function isSchemeLessResourceHref(href) {
3054
3165
  return !href.startsWith("//") && !URI_SCHEME_PATTERN.test(href);
3055
3166
  }
3056
- function resolveSafeChatResourceHref(href) {
3057
- if (!href) return null;
3058
- if (WINDOWS_ABSOLUTE_PATH_PATTERN.test(href) || isSchemeLessResourceHref(href)) return href;
3167
+ function readPositiveInteger(value) {
3168
+ const parsed = Number(value);
3169
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
3170
+ }
3171
+ function readFileUriHref(href) {
3172
+ let url;
3059
3173
  try {
3060
- const url = new URL(href);
3061
- return SAFE_LINK_PROTOCOLS.has(url.protocol) ? href : null;
3174
+ url = new URL(href);
3062
3175
  } catch {
3063
3176
  return null;
3064
3177
  }
3178
+ if (url.protocol !== FILE_URI_PROTOCOL || url.hostname && url.hostname.toLowerCase() !== "localhost") return null;
3179
+ let path;
3180
+ try {
3181
+ path = decodeURIComponent(url.pathname);
3182
+ } catch {
3183
+ return null;
3184
+ }
3185
+ if (WINDOWS_FILE_URI_PATH_PATTERN.test(path)) path = path.slice(1);
3186
+ return path ? {
3187
+ path,
3188
+ query: url.searchParams,
3189
+ fragment: url.hash.slice(1)
3190
+ } : null;
3065
3191
  }
3066
- function isExternalChatResourceHref(href) {
3067
- return /^https?:\/\//i.test(href);
3068
- }
3069
- function parseChatLocalFileAction(href) {
3070
- const normalizedHref = href.split("#")[0] ?? href;
3071
- const [encodedPath, encodedQuery = ""] = normalizedHref.split("?", 2);
3192
+ function readSchemeLessFileHref(href) {
3193
+ const hashIndex = href.indexOf("#");
3194
+ const hrefWithoutFragment = hashIndex >= 0 ? href.slice(0, hashIndex) : href;
3195
+ const fragment = hashIndex >= 0 ? href.slice(hashIndex + 1) : "";
3196
+ const queryIndex = hrefWithoutFragment.indexOf("?");
3197
+ const encodedPath = queryIndex >= 0 ? hrefWithoutFragment.slice(0, queryIndex) : hrefWithoutFragment;
3198
+ const encodedQuery = queryIndex >= 0 ? hrefWithoutFragment.slice(queryIndex + 1) : "";
3072
3199
  let path;
3073
3200
  try {
3074
- path = decodeURIComponent(encodedPath ?? normalizedHref);
3201
+ path = decodeURIComponent(encodedPath);
3075
3202
  } catch {
3076
3203
  return null;
3077
3204
  }
3205
+ return path ? {
3206
+ path,
3207
+ query: new URLSearchParams(encodedQuery),
3208
+ fragment
3209
+ } : null;
3210
+ }
3211
+ function readLinePosition(fragment) {
3212
+ const match = /^L(\d+)(?:C(\d+))?(?:-L\d+(?:C\d+)?)?$/i.exec(fragment);
3213
+ const line = readPositiveInteger(match?.[1]);
3214
+ return {
3215
+ line,
3216
+ column: line ? readPositiveInteger(match?.[2]) : void 0
3217
+ };
3218
+ }
3219
+ function transformChatResourceHref(href) {
3220
+ if (WINDOWS_ABSOLUTE_PATH_PATTERN.test(href) || isSchemeLessResourceHref(href)) return href;
3221
+ try {
3222
+ const url = new URL(href);
3223
+ if (SAFE_BROWSER_LINK_PROTOCOLS.has(url.protocol)) return href;
3224
+ return url.protocol === FILE_URI_PROTOCOL && readFileUriHref(href) ? href : "";
3225
+ } catch {
3226
+ return "";
3227
+ }
3228
+ }
3229
+ function resolveSafeChatResourceHref(href) {
3230
+ if (!href) return null;
3231
+ return transformChatResourceHref(href) || null;
3232
+ }
3233
+ function isExternalChatResourceHref(href) {
3234
+ return /^https?:\/\//i.test(href);
3235
+ }
3236
+ function parseChatLocalFileAction(href) {
3237
+ const fileHref = href.toLowerCase().startsWith(FILE_URI_PROTOCOL) ? readFileUriHref(href) : readSchemeLessFileHref(href);
3238
+ const path = fileHref?.path;
3078
3239
  if (!path || path.startsWith("#") || path.startsWith("//") || !WINDOWS_ABSOLUTE_PATH_PATTERN.test(path) && URI_SCHEME_PATTERN.test(path)) return null;
3079
- const viewer = new URLSearchParams(encodedQuery).get("viewer");
3240
+ const viewer = fileHref.query.get("viewer");
3241
+ const fragmentPosition = readLinePosition(fileHref.fragment);
3080
3242
  const lineMatch = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(path);
3081
3243
  const rawPath = lineMatch?.[1] ?? path;
3082
- const line = lineMatch?.[2] ? Number(lineMatch[2]) : void 0;
3083
- const column = lineMatch?.[3] ? Number(lineMatch[3]) : void 0;
3244
+ const line = fragmentPosition.line ?? readPositiveInteger(lineMatch?.[2]);
3245
+ const column = fragmentPosition.column ?? (line ? readPositiveInteger(lineMatch?.[3]) : void 0);
3084
3246
  return {
3085
3247
  path: rawPath,
3086
3248
  label: rawPath.split(/[\\/]/).filter(Boolean).pop() ?? rawPath,
@@ -3180,153 +3342,170 @@ function splitTextNodeByInlineTokens(value, tokens) {
3180
3342
  }];
3181
3343
  }
3182
3344
  function transformInlineTokenTextNodes(node, tokens) {
3183
- if (node.type === "code" || node.type === "inlineCode" || !node.children) return;
3184
- const nextChildren = [];
3185
- for (const child of node.children) {
3186
- if (child.type === "text" && typeof child.value === "string") {
3187
- nextChildren.push(...splitTextNodeByInlineTokens(child.value, tokens));
3188
- continue;
3189
- }
3190
- transformInlineTokenTextNodes(child, tokens);
3191
- nextChildren.push(child);
3192
- }
3193
- node.children = nextChildren;
3345
+ if (node.type === "code" || node.type === "inlineCode" || !node.children) return node;
3346
+ return {
3347
+ ...node,
3348
+ children: node.children.flatMap((child) => child.type === "text" && typeof child.value === "string" ? splitTextNodeByInlineTokens(child.value, tokens) : [transformInlineTokenTextNodes(child, tokens)])
3349
+ };
3194
3350
  }
3195
3351
  function createRemarkInlineTokenPlugin(inlineTokens) {
3196
3352
  const tokens = prepareInlineTokens(inlineTokens);
3197
- return () => (tree) => {
3198
- if (tokens.length === 0) return;
3199
- transformInlineTokenTextNodes(tree, tokens);
3200
- };
3353
+ return () => (tree) => tokens.length > 0 ? transformInlineTokenTextNodes(tree, tokens) : tree;
3201
3354
  }
3202
3355
  function readStringProp(props, key) {
3203
3356
  const value = props[key];
3204
3357
  return typeof value === "string" && value.length > 0 ? value : null;
3205
3358
  }
3206
- function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, onInlineTokenClick, resolveFileContentUrl, renderInlineDisplay }) {
3207
- const isUser = role === "user";
3208
- const remarkPlugins = useMemo(() => inlineTokens && inlineTokens.length > 0 ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm], [inlineTokens]);
3209
- const markdownComponents = useMemo(() => ({
3210
- p: ({ node, children }) => inline ? /* @__PURE__ */ jsx(Fragment$1, { children }) : /* @__PURE__ */ jsx("p", {
3359
+ const ChatMessageMarkdownRuntimeContext = createContext(null);
3360
+ function useChatMessageMarkdownRuntime() {
3361
+ const runtime = useContext(ChatMessageMarkdownRuntimeContext);
3362
+ if (!runtime) throw new Error("Chat message Markdown renderer requires its runtime context");
3363
+ return runtime;
3364
+ }
3365
+ const CHAT_MESSAGE_MARKDOWN_COMPONENTS = {
3366
+ p: function ChatMarkdownParagraph({ node, children }) {
3367
+ const { inline } = useChatMessageMarkdownRuntime();
3368
+ return inline ? /* @__PURE__ */ jsx(Fragment$1, { children }) : /* @__PURE__ */ jsx("p", {
3211
3369
  "data-chat-image-row": isSingleLineImageParagraph(node) ? "three-column" : void 0,
3212
3370
  children
3213
- }),
3214
- span: ({ node: _node, children, ...rest }) => {
3215
- const restProps = rest;
3216
- const kind = readStringProp(restProps, INLINE_TOKEN_KIND_ATTR);
3217
- const key = readStringProp(restProps, INLINE_TOKEN_KEY_ATTR);
3218
- const label = readStringProp(restProps, INLINE_TOKEN_LABEL_ATTR);
3219
- const rawText = readStringProp(restProps, INLINE_TOKEN_RAW_TEXT_ATTR);
3220
- if (kind && key && label && rawText) return /* @__PURE__ */ jsx(ChatInlineTokenBadge, {
3371
+ });
3372
+ },
3373
+ span: function ChatMarkdownSpan({ node: _node, children, ...rest }) {
3374
+ const { isUser, onInlineTokenClick } = useChatMessageMarkdownRuntime();
3375
+ const restProps = rest;
3376
+ const kind = readStringProp(restProps, INLINE_TOKEN_KIND_ATTR);
3377
+ const key = readStringProp(restProps, INLINE_TOKEN_KEY_ATTR);
3378
+ const label = readStringProp(restProps, INLINE_TOKEN_LABEL_ATTR);
3379
+ const rawText = readStringProp(restProps, INLINE_TOKEN_RAW_TEXT_ATTR);
3380
+ if (kind && key && label && rawText) return /* @__PURE__ */ jsx(ChatInlineTokenBadge, {
3381
+ kind,
3382
+ label,
3383
+ isUser,
3384
+ onClick: onInlineTokenClick ? () => onInlineTokenClick({
3221
3385
  kind,
3386
+ key,
3222
3387
  label,
3223
- isUser,
3224
- onClick: onInlineTokenClick ? () => onInlineTokenClick({
3225
- kind,
3226
- key,
3227
- label,
3228
- rawText
3229
- }) : void 0
3230
- });
3231
- return /* @__PURE__ */ jsx("span", {
3232
- ...rest,
3233
- children
3234
- });
3235
- },
3236
- a: ({ node: _node, href, children, ...rest }) => {
3237
- const safeHref = resolveSafeChatResourceHref(href);
3238
- const external = safeHref ? isExternalChatResourceHref(safeHref) : false;
3239
- const localFileAction = external ? null : safeHref ? parseChatLocalFileAction(safeHref) : null;
3240
- const handleClick = (event) => {
3241
- if (!safeHref) {
3242
- event.preventDefault();
3243
- return;
3244
- }
3245
- if (!onFileOpen || !localFileAction) return;
3246
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3388
+ rawText
3389
+ }) : void 0
3390
+ });
3391
+ return /* @__PURE__ */ jsx("span", {
3392
+ ...rest,
3393
+ children
3394
+ });
3395
+ },
3396
+ a: function ChatMarkdownLink({ node: _node, href, children, ...rest }) {
3397
+ const { onFileOpen } = useChatMessageMarkdownRuntime();
3398
+ const safeHref = resolveSafeChatResourceHref(href);
3399
+ const external = safeHref ? isExternalChatResourceHref(safeHref) : false;
3400
+ const localFileAction = external ? null : safeHref ? parseChatLocalFileAction(safeHref) : null;
3401
+ const handleClick = (event) => {
3402
+ if (!safeHref) {
3247
3403
  event.preventDefault();
3248
- onFileOpen(localFileAction);
3249
- };
3250
- return /* @__PURE__ */ jsx("a", {
3251
- ...rest,
3252
- className: cn(rest.className, !safeHref && "chat-link-invalid"),
3253
- href: safeHref ?? "#",
3254
- "aria-disabled": !safeHref || void 0,
3255
- onClick: handleClick,
3256
- target: external ? "_blank" : void 0,
3257
- rel: external ? "noreferrer noopener" : void 0,
3258
- children
3259
- });
3260
- },
3261
- table: ({ node: _node, children, ...rest }) => /* @__PURE__ */ jsx("div", {
3404
+ return;
3405
+ }
3406
+ if (!onFileOpen || !localFileAction) return;
3407
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3408
+ event.preventDefault();
3409
+ onFileOpen(localFileAction);
3410
+ };
3411
+ return /* @__PURE__ */ jsx("a", {
3412
+ ...rest,
3413
+ className: cn(rest.className, !safeHref && "chat-link-invalid"),
3414
+ href: safeHref ?? "#",
3415
+ "aria-disabled": !safeHref || void 0,
3416
+ onClick: handleClick,
3417
+ target: external ? "_blank" : void 0,
3418
+ rel: external ? "noreferrer noopener" : void 0,
3419
+ children
3420
+ });
3421
+ },
3422
+ table: function ChatMarkdownTable({ node: _node, children, ...rest }) {
3423
+ return /* @__PURE__ */ jsx("div", {
3262
3424
  className: "chat-table-wrap",
3263
3425
  children: /* @__PURE__ */ jsx("table", {
3264
3426
  ...rest,
3265
3427
  children
3266
3428
  })
3267
- }),
3268
- input: ({ node: _node, type, checked, ...rest }) => {
3269
- if (type !== "checkbox") return /* @__PURE__ */ jsx("input", {
3270
- ...rest,
3271
- type
3272
- });
3273
- return /* @__PURE__ */ jsx("input", {
3274
- ...rest,
3275
- type: "checkbox",
3276
- checked,
3277
- readOnly: true,
3278
- disabled: true,
3279
- className: "chat-task-checkbox"
3280
- });
3281
- },
3282
- img: ({ node: _node, src, alt }) => {
3283
- const safeSrc = resolveSafeChatResourceHref(src);
3284
- if (!safeSrc) return null;
3285
- const localFileAction = parseChatLocalFileAction(safeSrc);
3286
- const resolvedSrc = localFileAction && resolveFileContentUrl ? resolveFileContentUrl(localFileAction) : safeSrc;
3287
- if (!resolvedSrc) return null;
3288
- return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3289
- alt: alt || "",
3290
- closeLabel: texts.attachmentCloseLabel ?? "Close preview",
3291
- expandLabel: texts.attachmentExpandLabel ?? "Expand image",
3292
- sizeLabel: null,
3293
- src: resolvedSrc
3294
- });
3429
+ });
3430
+ },
3431
+ input: function ChatMarkdownInput({ node: _node, type, checked, ...rest }) {
3432
+ if (type !== "checkbox") return /* @__PURE__ */ jsx("input", {
3433
+ ...rest,
3434
+ type
3435
+ });
3436
+ return /* @__PURE__ */ jsx("input", {
3437
+ ...rest,
3438
+ type: "checkbox",
3439
+ checked,
3440
+ readOnly: true,
3441
+ disabled: true,
3442
+ className: "chat-task-checkbox"
3443
+ });
3444
+ },
3445
+ img: function ChatMarkdownImage({ node: _node, src, alt }) {
3446
+ const { resolveFileContentUrl, texts } = useChatMessageMarkdownRuntime();
3447
+ const safeSrc = resolveSafeChatResourceHref(src);
3448
+ if (!safeSrc) return null;
3449
+ const localFileAction = parseChatLocalFileAction(safeSrc);
3450
+ const resolvedSrc = localFileAction && resolveFileContentUrl ? resolveFileContentUrl(localFileAction) : safeSrc;
3451
+ if (!resolvedSrc) return null;
3452
+ return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3453
+ alt: alt || "",
3454
+ closeLabel: texts.attachmentCloseLabel ?? "Close preview",
3455
+ expandLabel: texts.attachmentExpandLabel ?? "Expand image",
3456
+ sizeLabel: null,
3457
+ src: resolvedSrc
3458
+ });
3459
+ },
3460
+ code: function ChatMarkdownCode({ node: _node, className, children, ...rest }) {
3461
+ const { isStreaming, renderInlineDisplay, texts } = useChatMessageMarkdownRuntime();
3462
+ const plainText = String(children ?? "");
3463
+ if (!className && !plainText.includes("\n")) return /* @__PURE__ */ jsx("code", {
3464
+ ...rest,
3465
+ className: cn("chat-inline-code", className),
3466
+ children
3467
+ });
3468
+ const inlineDisplay = isChatInlineDisplayLanguage(className) ? parseChatInlineDisplayDirective(plainText) : null;
3469
+ if (inlineDisplay) return /* @__PURE__ */ jsx(ChatInlineDisplay, {
3470
+ display: inlineDisplay,
3471
+ renderInlineDisplay
3472
+ });
3473
+ if (className?.split(" ").includes("language-mermaid")) return /* @__PURE__ */ jsx(ChatMermaidDiagram, {
3474
+ isStreaming,
3475
+ source: plainText,
3476
+ texts
3477
+ });
3478
+ return /* @__PURE__ */ jsx(ChatCodeBlock, {
3479
+ className,
3480
+ texts,
3481
+ children
3482
+ });
3483
+ }
3484
+ };
3485
+ function ChatMessageMarkdown({ text, role, texts, inline = false, isStreaming = false, inlineTokens, onFileOpen, onInlineTokenClick, resolveFileContentUrl, renderInlineDisplay }) {
3486
+ const isUser = role === "user";
3487
+ const remarkPlugins = inlineTokens?.length ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm];
3488
+ const WrapperTag = inline ? "span" : "div";
3489
+ return /* @__PURE__ */ jsx(ChatMessageMarkdownRuntimeContext.Provider, {
3490
+ value: {
3491
+ inline,
3492
+ isStreaming,
3493
+ isUser,
3494
+ onFileOpen,
3495
+ onInlineTokenClick,
3496
+ renderInlineDisplay,
3497
+ resolveFileContentUrl,
3498
+ texts
3295
3499
  },
3296
- code: ({ node: _node, className, children, ...rest }) => {
3297
- const plainText = String(children ?? "");
3298
- if (!className && !plainText.includes("\n")) return /* @__PURE__ */ jsx("code", {
3299
- ...rest,
3300
- className: cn("chat-inline-code", className),
3301
- children
3302
- });
3303
- const inlineDisplay = isChatInlineDisplayLanguage(className) ? parseChatInlineDisplayDirective(plainText) : null;
3304
- if (inlineDisplay) return /* @__PURE__ */ jsx(ChatInlineDisplay, {
3305
- display: inlineDisplay,
3306
- renderInlineDisplay
3307
- });
3308
- return /* @__PURE__ */ jsx(ChatCodeBlock, {
3309
- className,
3310
- texts,
3311
- children
3312
- });
3313
- }
3314
- }), [
3315
- inline,
3316
- isUser,
3317
- onFileOpen,
3318
- onInlineTokenClick,
3319
- renderInlineDisplay,
3320
- resolveFileContentUrl,
3321
- texts
3322
- ]);
3323
- return /* @__PURE__ */ jsx(inline ? "span" : "div", {
3324
- className: cn("chat-markdown", isUser ? "chat-markdown-user" : "chat-markdown-assistant"),
3325
- children: /* @__PURE__ */ jsx(ReactMarkdown, {
3326
- skipHtml: true,
3327
- remarkPlugins,
3328
- components: markdownComponents,
3329
- children: trimMarkdown(text)
3500
+ children: /* @__PURE__ */ jsx(WrapperTag, {
3501
+ className: cn("chat-markdown", isUser ? "chat-markdown-user" : "chat-markdown-assistant"),
3502
+ children: /* @__PURE__ */ jsx(ReactMarkdown, {
3503
+ skipHtml: true,
3504
+ remarkPlugins,
3505
+ components: CHAT_MESSAGE_MARKDOWN_COMPONENTS,
3506
+ urlTransform: transformChatResourceHref,
3507
+ children: trimMarkdown(text)
3508
+ })
3330
3509
  })
3331
3510
  });
3332
3511
  }
@@ -4213,6 +4392,9 @@ function readVisibleLineNumber(line) {
4213
4392
  const value = line.newLineNumber ?? line.oldLineNumber;
4214
4393
  return typeof value === "number" ? String(value) : "";
4215
4394
  }
4395
+ function isTargetLine(line, targetLine) {
4396
+ return typeof targetLine === "number" && (line.newLineNumber ?? line.oldLineNumber) === targetLine;
4397
+ }
4216
4398
  function readLineKey(prefix, line, index) {
4217
4399
  return `${prefix}-${index}-${line.oldLineNumber ?? "x"}-${line.newLineNumber ?? "x"}`;
4218
4400
  }
@@ -4256,25 +4438,34 @@ function getCodeRowTone(line) {
4256
4438
  if (line.kind === "add") return "bg-emerald-50 text-emerald-950";
4257
4439
  return "bg-card text-foreground";
4258
4440
  }
4259
- function FileOperationLineNumberCell({ layout, line, lineNumberColumnWidth }) {
4441
+ function FileOperationLineNumberCell({ layout, line, lineNumberColumnWidth, target }) {
4260
4442
  return /* @__PURE__ */ jsx("span", {
4261
4443
  "data-file-line-number-cell": "true",
4262
4444
  style: layout === "compact" ? {
4263
4445
  width: lineNumberColumnWidth,
4264
4446
  minWidth: lineNumberColumnWidth
4265
4447
  } : void 0,
4266
- className: cn(FILE_LINE_NUMBER_CELL_CLASS_NAME, layout === "compact" ? "sticky left-0 z-[1]" : "w-full shrink-0", getLineNumberTone(line)),
4448
+ className: cn(FILE_LINE_NUMBER_CELL_CLASS_NAME, layout === "compact" ? "sticky left-0 z-[1]" : "w-full shrink-0", getLineNumberTone(line), target ? "bg-gray-200 font-medium text-gray-950" : null),
4267
4449
  children: readVisibleLineNumber(line)
4268
4450
  });
4269
4451
  }
4270
- function FileOperationCodeCell({ language, line }) {
4452
+ function FileOperationCodeCell({ language, line, target, targetColumn, targetRef }) {
4271
4453
  const highlightedCode = useMemo(() => chatCodeSyntaxHighlighter.highlight(line.text || " ", language), [language, line.text]);
4272
- return /* @__PURE__ */ jsx("span", {
4454
+ const visibleTargetColumn = typeof targetColumn === "number" && Number.isSafeInteger(targetColumn) && targetColumn > 0 ? Math.min(targetColumn, line.text.length + 1) : null;
4455
+ return /* @__PURE__ */ jsxs("span", {
4456
+ ref: visibleTargetColumn ? void 0 : targetRef,
4273
4457
  "data-file-code-row": "true",
4274
4458
  "data-file-code-language": highlightedCode.language,
4275
4459
  "data-highlighted": highlightedCode.highlighted ? "true" : "false",
4276
- className: cn("chat-file-code-syntax hljs block min-w-0 flex-1 whitespace-pre px-2.5", readLanguageClassName(highlightedCode.language), getCodeRowTone(line)),
4277
- dangerouslySetInnerHTML: { __html: highlightedCode.html }
4460
+ "data-file-target-column": visibleTargetColumn ?? void 0,
4461
+ className: cn("chat-file-code-syntax hljs relative block min-w-0 flex-1 whitespace-pre px-2.5", readLanguageClassName(highlightedCode.language), getCodeRowTone(line), target ? "bg-gray-100/90" : null),
4462
+ children: [/* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: highlightedCode.html } }), visibleTargetColumn ? /* @__PURE__ */ jsx("span", {
4463
+ ref: targetRef,
4464
+ "aria-hidden": "true",
4465
+ "data-file-target-caret": "true",
4466
+ className: "pointer-events-none absolute inset-y-0 w-px bg-primary",
4467
+ style: { left: `calc(0.625rem + ${visibleTargetColumn - 1}ch)` }
4468
+ }) : null]
4278
4469
  });
4279
4470
  }
4280
4471
  function FileOperationLineRow({ language, line, showLineNumbers, lineNumberColumnWidth }) {
@@ -4291,11 +4482,22 @@ function FileOperationLineRow({ language, line, showLineNumbers, lineNumberColum
4291
4482
  })]
4292
4483
  });
4293
4484
  }
4294
- function FileOperationWorkspaceSurface({ block }) {
4485
+ function FileOperationWorkspaceSurface({ block, targetColumn, targetLine }) {
4295
4486
  const showLineNumbers = readHasBlockLineNumbers(block);
4296
4487
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
4297
4488
  const codeColumnWidth = readCodeColumnWidth(block);
4298
4489
  const language = readBlockLanguage(block);
4490
+ const targetRef = useRef(null);
4491
+ useLayoutEffect(() => {
4492
+ targetRef.current?.scrollIntoView({
4493
+ block: "center",
4494
+ inline: targetColumn ? "center" : "nearest"
4495
+ });
4496
+ }, [
4497
+ block,
4498
+ targetColumn,
4499
+ targetLine
4500
+ ]);
4299
4501
  return /* @__PURE__ */ jsxs("div", {
4300
4502
  "data-file-code-surface": "true",
4301
4503
  "data-file-code-surface-layout": "workspace",
@@ -4310,7 +4512,8 @@ function FileOperationWorkspaceSurface({ block }) {
4310
4512
  children: [block.lines.map((line, index) => /* @__PURE__ */ jsx(FileOperationLineNumberCell, {
4311
4513
  layout: "workspace",
4312
4514
  line,
4313
- lineNumberColumnWidth
4515
+ lineNumberColumnWidth,
4516
+ target: isTargetLine(line, targetLine)
4314
4517
  }, readLineKey("gutter", line, index))), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 border-r border-border bg-muted" })]
4315
4518
  }) : null, /* @__PURE__ */ jsxs("div", {
4316
4519
  "data-file-code-canvas": "true",
@@ -4319,20 +4522,32 @@ function FileOperationWorkspaceSurface({ block }) {
4319
4522
  "data-file-code-stack": "true",
4320
4523
  className: "min-w-full",
4321
4524
  style: { minWidth: codeColumnWidth },
4322
- children: block.lines.map((line, index) => /* @__PURE__ */ jsx("div", {
4323
- "data-file-code-canvas-row": "true",
4324
- className: FILE_ROW_CLASS_NAME,
4325
- children: /* @__PURE__ */ jsx(FileOperationCodeCell, {
4326
- language,
4327
- line
4328
- })
4329
- }, readLineKey("code", line, index)))
4525
+ children: block.lines.map((line, index) => {
4526
+ const target = isTargetLine(line, targetLine);
4527
+ return /* @__PURE__ */ jsx("div", {
4528
+ "data-file-code-canvas-row": "true",
4529
+ "data-file-target-line": target ? "true" : void 0,
4530
+ "aria-current": target ? "location" : void 0,
4531
+ className: FILE_ROW_CLASS_NAME,
4532
+ children: /* @__PURE__ */ jsx(FileOperationCodeCell, {
4533
+ language,
4534
+ line,
4535
+ target,
4536
+ targetColumn: target ? targetColumn : null,
4537
+ targetRef: target ? targetRef : void 0
4538
+ })
4539
+ }, readLineKey("code", line, index));
4540
+ })
4330
4541
  }), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-card" })]
4331
4542
  })]
4332
4543
  });
4333
4544
  }
4334
- function FileOperationCodeSurface({ block, layout = "compact" }) {
4335
- if (layout === "workspace") return /* @__PURE__ */ jsx(FileOperationWorkspaceSurface, { block });
4545
+ function FileOperationCodeSurface({ block, layout = "compact", targetColumn, targetLine }) {
4546
+ if (layout === "workspace") return /* @__PURE__ */ jsx(FileOperationWorkspaceSurface, {
4547
+ block,
4548
+ targetColumn,
4549
+ targetLine
4550
+ });
4336
4551
  const showLineNumbers = readHasBlockLineNumbers(block);
4337
4552
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
4338
4553
  const codeColumnWidth = readCodeColumnWidth(block);
@@ -5243,6 +5458,9 @@ const MAX_SUMMARY_SEGMENTS = 3;
5243
5458
  function isToolCardPart(part) {
5244
5459
  return part.type === "tool-card";
5245
5460
  }
5461
+ function isGroupableToolPart(part) {
5462
+ return part?.type === "reasoning" || part?.type === "tool-card" && !part.card.panelApp;
5463
+ }
5246
5464
  function resolveToolActivityFamily(toolName) {
5247
5465
  const lowered = toolName.toLowerCase();
5248
5466
  if (lowered === "list_dir") return "directory";
@@ -5321,17 +5539,29 @@ function formatToolActivityGroupLabel(params) {
5321
5539
  if (hiddenCount > 0) parts.push(`+${hiddenCount}`);
5322
5540
  return parts.join(" · ");
5323
5541
  }
5324
- /**
5325
- * Group tool cards separated only by reasoning into one visible workflow.
5326
- * Markdown, files and unknown parts still break the run.
5327
- * Single tool-cards stay ungrouped so they keep their native card UI.
5328
- */
5542
+ function collectToolGroupParts(parts, startIndex) {
5543
+ const candidateParts = [];
5544
+ let index = startIndex;
5545
+ let lastToolOffset = -1;
5546
+ while (true) {
5547
+ const candidate = parts[index];
5548
+ if (!isGroupableToolPart(candidate)) break;
5549
+ candidateParts.push(candidate);
5550
+ if (candidate.type === "tool-card") lastToolOffset = candidateParts.length - 1;
5551
+ index += 1;
5552
+ }
5553
+ return {
5554
+ candidateParts,
5555
+ lastToolOffset
5556
+ };
5557
+ }
5558
+ /** Groups tool cards across reasoning; content and interactive panel apps remain stable boundaries. */
5329
5559
  function groupConsecutiveToolParts(parts, labels) {
5330
5560
  const blocks = [];
5331
5561
  let index = 0;
5332
5562
  while (index < parts.length) {
5333
5563
  const part = parts[index];
5334
- if (part.type !== "tool-card") {
5564
+ if (part.type !== "tool-card" || part.card.panelApp) {
5335
5565
  blocks.push({
5336
5566
  kind: "part",
5337
5567
  key: `part-${index}`,
@@ -5342,14 +5572,7 @@ function groupConsecutiveToolParts(parts, labels) {
5342
5572
  continue;
5343
5573
  }
5344
5574
  const startIndex = index;
5345
- const candidateParts = [];
5346
- let lastToolOffset = -1;
5347
- while (index < parts.length && (parts[index]?.type === "tool-card" || parts[index]?.type === "reasoning")) {
5348
- const candidate = parts[index];
5349
- candidateParts.push(candidate);
5350
- if (candidate.type === "tool-card") lastToolOffset = candidateParts.length - 1;
5351
- index += 1;
5352
- }
5575
+ const { candidateParts, lastToolOffset } = collectToolGroupParts(parts, startIndex);
5353
5576
  const toolParts = candidateParts.filter(isToolCardPart);
5354
5577
  if (toolParts.length === 1) {
5355
5578
  blocks.push({
@@ -5464,6 +5687,7 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAtta
5464
5687
  text,
5465
5688
  role,
5466
5689
  texts,
5690
+ isStreaming: isInProgress && isLastPart,
5467
5691
  inlineTokens,
5468
5692
  onFileOpen,
5469
5693
  onInlineTokenClick,
@@ -5666,14 +5890,25 @@ function ChatMessageActionCopy({ message, texts }) {
5666
5890
  }).filter((text) => !!text && text.trim().length > 0).join("\n\n");
5667
5891
  }, [message.parts]);
5668
5892
  const { copied, copy } = useCopyFeedback({ text: messageText });
5893
+ const label = copied ? texts.copiedMessageLabel : texts.copyMessageLabel;
5894
+ const { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
5669
5895
  if (!messageText) return null;
5670
- return /* @__PURE__ */ jsx("button", {
5671
- type: "button",
5672
- onClick: () => void copy(),
5673
- className: "text-muted-foreground hover:text-foreground transition-colors p-1 rounded-md hover:bg-accent flex items-center justify-center",
5674
- "aria-label": copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
5675
- title: copied ? texts.copiedMessageLabel : texts.copyMessageLabel,
5676
- children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
5896
+ return /* @__PURE__ */ jsx(TooltipProvider, {
5897
+ delayDuration: 250,
5898
+ children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
5899
+ asChild: true,
5900
+ children: /* @__PURE__ */ jsx("button", {
5901
+ type: "button",
5902
+ onClick: () => void copy(),
5903
+ className: "flex items-center justify-center rounded-md p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35",
5904
+ "aria-label": label,
5905
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
5906
+ })
5907
+ }), /* @__PURE__ */ jsx(TooltipContent, {
5908
+ side: "top",
5909
+ className: "text-xs",
5910
+ children: label
5911
+ })] })
5677
5912
  });
5678
5913
  }
5679
5914
  //#endregion
@@ -5824,10 +6059,10 @@ function ChatMessageList({ className, isSending, layout = "card", messages, onAt
5824
6059
  " · ",
5825
6060
  message.timestampLabel
5826
6061
  ]
5827
- }), !isUser ? /* @__PURE__ */ jsx(ChatMessageActionCopy, {
6062
+ }), /* @__PURE__ */ jsx(ChatMessageActionCopy, {
5828
6063
  message,
5829
6064
  texts
5830
- }) : null] })
6065
+ })] })
5831
6066
  })]
5832
6067
  }),
5833
6068
  isUser ? /* @__PURE__ */ jsx(ChatMessageAvatar, { role: message.role }) : null