@nextclaw/agent-chat-ui 0.6.4 → 0.6.6

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,18 +1,18 @@
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
+ import { AlertTriangle, AppWindow, ArrowLeft, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, CalendarClock, Check, ChevronDown, ChevronRight, ChevronUp, CircleAlert, Code2, Command, Copy, Expand, ExternalLink, Eye, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, Files, Folder, FolderTree, Globe, Globe2, ImageIcon, ListChecks, Loader2, MessageSquare, Minus, PanelsTopLeft, Paperclip, Puzzle, Search, Send, Settings, Sparkles, Star, Terminal, User, Workflow, Wrench, X } from "lucide-react";
3
4
  import { clsx } from "clsx";
4
5
  import { twMerge } from "tailwind-merge";
5
6
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
7
  import * as PopoverPrimitive from "@radix-ui/react-popover";
7
8
  import * as SelectPrimitive from "@radix-ui/react-select";
8
- import { AlertTriangle, AppWindow, ArrowUp, ArrowUpRight, Bot, Brain, BrainCircuit, CalendarClock, Check, ChevronDown, ChevronRight, ChevronUp, Code2, Copy, Expand, ExternalLink, Eye, File, FileArchive, FileAudio2, FileCode2, FileImage, FileJson2, FileSpreadsheet, FileText, FileVideo2, FolderTree, Globe, Globe2, ImageIcon, ListChecks, Loader2, MessageSquare, Minus, Paperclip, Puzzle, Search, Send, Settings, Sparkles, Star, Terminal, User, Workflow, Wrench, X } from "lucide-react";
9
9
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
10
10
  import { cva } from "class-variance-authority";
11
11
  import { ContentEditable } from "@lexical/react/LexicalContentEditable";
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, COMPOSITION_END_TAG, 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";
@@ -394,16 +394,142 @@ const ChatUiPrimitives = {
394
394
  TooltipTrigger: ChatTooltipTrigger
395
395
  };
396
396
  //#endregion
397
+ //#region src/components/chat/ui/input-surface/chat-input-surface-path-preview.tsx
398
+ function ChatInputSurfacePathPreview({ pathPreview }) {
399
+ return /* @__PURE__ */ jsxs("div", {
400
+ className: "rounded-lg border border-gray-200 bg-gray-50/70 px-3 py-2.5",
401
+ children: [/* @__PURE__ */ jsxs("div", {
402
+ className: "flex items-center gap-2 text-xs font-medium text-gray-600",
403
+ children: [/* @__PURE__ */ jsx(Folder, {
404
+ "aria-hidden": "true",
405
+ className: "h-4 w-4 text-gray-400"
406
+ }), /* @__PURE__ */ jsx("span", {
407
+ className: "truncate",
408
+ children: pathPreview.rootLabel
409
+ })]
410
+ }), /* @__PURE__ */ jsx("div", {
411
+ className: "mt-2",
412
+ children: pathPreview.segments.map((segment, index) => {
413
+ const Icon = segment.kind === "directory" ? Folder : File;
414
+ return /* @__PURE__ */ jsxs("div", {
415
+ className: "relative flex min-w-0 items-center gap-2 py-1 text-xs text-gray-700",
416
+ style: { paddingLeft: `${(index + 1) * 18}px` },
417
+ children: [
418
+ /* @__PURE__ */ jsx("span", {
419
+ "aria-hidden": "true",
420
+ className: "absolute bottom-1/2 top-[-0.25rem] w-px bg-gray-200",
421
+ style: { left: `${index * 18 + 7}px` }
422
+ }),
423
+ /* @__PURE__ */ jsx(Icon, {
424
+ "aria-hidden": "true",
425
+ className: "h-4 w-4 shrink-0 text-gray-400"
426
+ }),
427
+ /* @__PURE__ */ jsx("span", {
428
+ className: "truncate font-medium",
429
+ children: segment.label
430
+ })
431
+ ]
432
+ }, `${index}:${segment.label}`);
433
+ })
434
+ })]
435
+ });
436
+ }
437
+ //#endregion
397
438
  //#region src/components/chat/ui/input-surface/chat-input-surface-menu.tsx
398
439
  const INPUT_SURFACE_PANEL_MAX_WIDTH = 680;
399
440
  const INPUT_SURFACE_PANEL_DESKTOP_SHRINK_RATIO = .82;
400
441
  const INPUT_SURFACE_PANEL_DESKTOP_MIN_WIDTH = 560;
401
442
  const INPUT_SURFACE_PANEL_MAX_HEIGHT = createChatPopoverAvailableHeightLimit("24rem");
402
443
  const INPUT_SURFACE_PANEL_MIN_HEIGHT = createChatPopoverAvailableHeightLimit("240px");
444
+ const INPUT_SURFACE_ITEM_ICONS = {
445
+ back: ArrowLeft,
446
+ command: Command,
447
+ file: File,
448
+ files: Files,
449
+ folder: Folder,
450
+ "panel-app": PanelsTopLeft,
451
+ skill: Puzzle
452
+ };
403
453
  function itemMatchesFilter(item, filter) {
404
454
  if (!filter.sectionKeys || filter.sectionKeys.length === 0) return true;
405
455
  return Boolean(item.sectionKey && filter.sectionKeys.includes(item.sectionKey));
406
456
  }
457
+ function resolveActiveFilterKey(filters, activeFilterKey) {
458
+ if (!filters?.length) return null;
459
+ return filters.some((filter) => filter.key === activeFilterKey) ? activeFilterKey : filters[0].key;
460
+ }
461
+ function InputSurfaceItemIcon({ icon }) {
462
+ const Icon = INPUT_SURFACE_ITEM_ICONS[icon];
463
+ return /* @__PURE__ */ jsx("span", {
464
+ "aria-hidden": "true",
465
+ "data-input-surface-icon": icon,
466
+ className: "inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-gray-100 text-gray-500",
467
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" })
468
+ });
469
+ }
470
+ function InputSurfaceItemDetails({ item, texts }) {
471
+ if (!item) return /* @__PURE__ */ jsx("div", {
472
+ className: "text-xs text-gray-500",
473
+ children: texts.hintLabel
474
+ });
475
+ return /* @__PURE__ */ jsxs("div", {
476
+ className: "space-y-3",
477
+ children: [
478
+ /* @__PURE__ */ jsxs("div", {
479
+ className: "flex items-center gap-2",
480
+ children: [item.subtitle ? /* @__PURE__ */ jsx("span", {
481
+ className: "inline-flex rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-semibold text-primary",
482
+ children: item.subtitle
483
+ }) : null, /* @__PURE__ */ jsx("span", {
484
+ className: "text-sm font-semibold text-gray-900",
485
+ children: item.title
486
+ })]
487
+ }),
488
+ /* @__PURE__ */ jsx("p", {
489
+ className: "text-xs leading-5 text-gray-600",
490
+ children: item.description
491
+ }),
492
+ item.pathPreview ? /* @__PURE__ */ jsx(ChatInputSurfacePathPreview, { pathPreview: item.pathPreview }) : null,
493
+ /* @__PURE__ */ jsx("div", {
494
+ className: "space-y-1",
495
+ children: item.detailLines.map((line) => /* @__PURE__ */ jsx("div", {
496
+ className: "min-w-0 break-all rounded-md bg-gray-50 px-2 py-1 text-[11px] leading-5 text-gray-600",
497
+ children: line
498
+ }, line))
499
+ }),
500
+ /* @__PURE__ */ jsx("div", {
501
+ className: "pt-1 text-[11px] text-gray-500",
502
+ children: item.hintLabel ?? texts.itemHintLabel
503
+ })
504
+ ]
505
+ });
506
+ }
507
+ function handleInputSurfaceMenuKeyDown(params) {
508
+ const { activeIndex, event, isOpen, items, onOpenChange, onSelectItem, setActiveIndex } = params;
509
+ if (!isOpen) return false;
510
+ if (event.key === "Escape") {
511
+ event.preventDefault();
512
+ onOpenChange(false);
513
+ return true;
514
+ }
515
+ if (items.length === 0) return false;
516
+ if (event.key === "ArrowDown") {
517
+ event.preventDefault();
518
+ setActiveIndex((index) => (index + 1) % items.length);
519
+ return true;
520
+ }
521
+ if (event.key === "ArrowUp") {
522
+ event.preventDefault();
523
+ setActiveIndex((index) => (index - 1 + items.length) % items.length);
524
+ return true;
525
+ }
526
+ if (event.key === "Enter" && !event.shiftKey || event.key === "Tab") {
527
+ event.preventDefault();
528
+ onSelectItem(items[activeIndex]);
529
+ return true;
530
+ }
531
+ return false;
532
+ }
407
533
  const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref) {
408
534
  const { Popover, PopoverAnchor, PopoverContent } = ChatUiPrimitives;
409
535
  const { elementRef: anchorRef, width: panelWidth } = useElementWidth();
@@ -412,12 +538,9 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
412
538
  index: 0,
413
539
  itemsSignature: ""
414
540
  });
415
- const { isOpen, isLoading, filterOptions, items, texts, onSelectItem, onOpenChange, onDetailsPointerDown } = props;
541
+ const { isOpen, isLoading, filterOptions, items, notice, texts, onSelectItem, onOpenChange, onDetailsPointerDown } = props;
416
542
  const [activeFilterKey, setActiveFilterKey] = useState(filterOptions?.[0]?.key ?? null);
417
- const resolvedActiveFilterKey = useMemo(() => {
418
- if (!filterOptions?.length) return null;
419
- return filterOptions.some((filter) => filter.key === activeFilterKey) ? activeFilterKey : filterOptions[0].key;
420
- }, [activeFilterKey, filterOptions]);
543
+ const resolvedActiveFilterKey = useMemo(() => resolveActiveFilterKey(filterOptions, activeFilterKey), [activeFilterKey, filterOptions]);
421
544
  const activeFilter = useMemo(() => filterOptions?.find((filter) => filter.key === resolvedActiveFilterKey) ?? null, [filterOptions, resolvedActiveFilterKey]);
422
545
  const filterViews = useMemo(() => filterOptions?.map((filter) => ({
423
546
  ...filter,
@@ -449,31 +572,15 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
449
572
  if (!panelWidth) return;
450
573
  return Math.min(panelWidth > INPUT_SURFACE_PANEL_DESKTOP_MIN_WIDTH ? panelWidth * INPUT_SURFACE_PANEL_DESKTOP_SHRINK_RATIO : panelWidth, INPUT_SURFACE_PANEL_MAX_WIDTH);
451
574
  }, [panelWidth]);
452
- useImperativeHandle(ref, () => ({ handleKeyDown: (event) => {
453
- if (!isOpen) return false;
454
- if (event.key === "Escape") {
455
- event.preventDefault();
456
- onOpenChange(false);
457
- return true;
458
- }
459
- if (visibleItems.length === 0) return false;
460
- if (event.key === "ArrowDown") {
461
- event.preventDefault();
462
- setActiveIndexForCurrentItems((index) => (index + 1) % visibleItems.length);
463
- return true;
464
- }
465
- if (event.key === "ArrowUp") {
466
- event.preventDefault();
467
- setActiveIndexForCurrentItems((index) => (index - 1 + visibleItems.length) % visibleItems.length);
468
- return true;
469
- }
470
- if (event.key === "Enter" && !event.shiftKey || event.key === "Tab") {
471
- event.preventDefault();
472
- onSelectItem(visibleItems[activeIndexInRange]);
473
- return true;
474
- }
475
- return false;
476
- } }), [
575
+ useImperativeHandle(ref, () => ({ handleKeyDown: (event) => handleInputSurfaceMenuKeyDown({
576
+ activeIndex: activeIndexInRange,
577
+ event,
578
+ isOpen,
579
+ items: visibleItems,
580
+ onOpenChange,
581
+ onSelectItem,
582
+ setActiveIndex: setActiveIndexForCurrentItems
583
+ }) }), [
477
584
  activeIndexInRange,
478
585
  isOpen,
479
586
  onOpenChange,
@@ -512,105 +619,90 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
512
619
  style: { minHeight: INPUT_SURFACE_PANEL_MIN_HEIGHT },
513
620
  children: [/* @__PURE__ */ jsxs("div", {
514
621
  className: "flex min-h-0 flex-col border-r border-gray-200",
515
- children: [!isLoading && filterViews.length > 0 ? /* @__PURE__ */ jsx("div", {
516
- className: "flex shrink-0 gap-0.5 overflow-x-auto px-2 pb-1.5 pt-2",
517
- children: filterViews.map(({ count, key, label }) => {
518
- const isActive = key === resolvedActiveFilterKey;
519
- return /* @__PURE__ */ jsxs("button", {
520
- type: "button",
521
- "aria-pressed": isActive,
522
- onPointerDown: (event) => event.preventDefault(),
523
- onClick: () => handleFilterSelect(key),
524
- className: `inline-flex h-7 shrink-0 items-center gap-1 rounded-md px-2 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}`,
525
- children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
526
- className: isActive ? "text-gray-500" : "text-gray-400",
527
- children: count
528
- })]
529
- }, key);
622
+ children: [
623
+ !isLoading && filterViews.length > 0 ? /* @__PURE__ */ jsx("div", {
624
+ className: "flex shrink-0 gap-0.5 overflow-x-auto px-2 pb-1.5 pt-2",
625
+ children: filterViews.map(({ count, key, label }) => {
626
+ const isActive = key === resolvedActiveFilterKey;
627
+ return /* @__PURE__ */ jsxs("button", {
628
+ type: "button",
629
+ "aria-pressed": isActive,
630
+ onPointerDown: (event) => event.preventDefault(),
631
+ onClick: () => handleFilterSelect(key),
632
+ className: `inline-flex h-7 shrink-0 items-center gap-1 rounded-md px-2 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}`,
633
+ children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
634
+ className: isActive ? "text-gray-500" : "text-gray-400",
635
+ children: count
636
+ })]
637
+ }, key);
638
+ })
639
+ }) : null,
640
+ !isLoading && notice ? /* @__PURE__ */ jsxs("div", {
641
+ className: "mx-2 mb-1.5 flex items-start gap-2 rounded-lg border border-red-200 bg-red-50 px-2.5 py-2 text-xs leading-4 text-red-700",
642
+ children: [/* @__PURE__ */ jsx(CircleAlert, {
643
+ "aria-hidden": "true",
644
+ className: "mt-0.5 h-3.5 w-3.5 shrink-0"
645
+ }), /* @__PURE__ */ jsx("span", { children: notice.message })]
646
+ }) : null,
647
+ /* @__PURE__ */ jsx("div", {
648
+ ref: listRef,
649
+ role: "listbox",
650
+ "aria-label": texts.sectionLabel,
651
+ className: `custom-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain ${!isLoading && filterViews.length > 0 ? "px-2 pb-2" : "p-2"}`,
652
+ children: isLoading ? /* @__PURE__ */ jsx("div", {
653
+ className: "p-2 text-xs text-gray-500",
654
+ children: texts.loadingLabel
655
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [!hasItemSections ? /* @__PURE__ */ jsx("div", {
656
+ className: "mb-1 px-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
657
+ children: texts.sectionLabel
658
+ }) : null, visibleItems.length === 0 ? /* @__PURE__ */ jsx("div", {
659
+ className: "px-1.5 text-xs text-gray-400",
660
+ children: texts.emptyLabel
661
+ }) : /* @__PURE__ */ jsx("div", { children: visibleItems.map((item, index) => {
662
+ const { icon, key, sectionKey, sectionLabel, title, subtitle } = item;
663
+ const isActive = index === activeIndexInRange;
664
+ const previousItem = visibleItems[index - 1];
665
+ return /* @__PURE__ */ jsxs("div", { children: [hasItemSections && Boolean(sectionLabel?.trim()) && previousItem?.sectionKey !== sectionKey ? /* @__PURE__ */ jsx("div", {
666
+ className: "px-1.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
667
+ children: sectionLabel
668
+ }) : null, /* @__PURE__ */ jsxs("button", {
669
+ type: "button",
670
+ role: "option",
671
+ "aria-selected": isActive,
672
+ "data-input-surface-index": index,
673
+ onPointerMove: (event) => {
674
+ if (event.pointerType !== "touch") setActiveIndexForCurrentItems(index);
675
+ },
676
+ onPointerDown: (event) => {
677
+ if (event.button > 0) return;
678
+ event.preventDefault();
679
+ onSelectItem(item);
680
+ },
681
+ onClick: (event) => {
682
+ if (event.detail === 0) onSelectItem(item);
683
+ },
684
+ className: `flex w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-left leading-4 transition-colors ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-100"}`,
685
+ children: [
686
+ icon ? /* @__PURE__ */ jsx(InputSurfaceItemIcon, { icon }) : null,
687
+ /* @__PURE__ */ jsx("span", {
688
+ className: "truncate text-xs font-medium",
689
+ children: title
690
+ }),
691
+ /* @__PURE__ */ jsx("span", {
692
+ className: "truncate text-xs text-gray-500",
693
+ children: subtitle
694
+ })
695
+ ]
696
+ })] }, key);
697
+ }) })] })
530
698
  })
531
- }) : null, /* @__PURE__ */ jsx("div", {
532
- ref: listRef,
533
- role: "listbox",
534
- "aria-label": texts.sectionLabel,
535
- className: `custom-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain ${!isLoading && filterViews.length > 0 ? "px-2 pb-2" : "p-2"}`,
536
- children: isLoading ? /* @__PURE__ */ jsx("div", {
537
- className: "p-2 text-xs text-gray-500",
538
- children: texts.loadingLabel
539
- }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [!hasItemSections ? /* @__PURE__ */ jsx("div", {
540
- className: "mb-1 px-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
541
- children: texts.sectionLabel
542
- }) : null, visibleItems.length === 0 ? /* @__PURE__ */ jsx("div", {
543
- className: "px-1.5 text-xs text-gray-400",
544
- children: texts.emptyLabel
545
- }) : /* @__PURE__ */ jsx("div", { children: visibleItems.map((item, index) => {
546
- const { key, sectionKey, sectionLabel, title, subtitle } = item;
547
- const isActive = index === activeIndexInRange;
548
- const previousItem = visibleItems[index - 1];
549
- return /* @__PURE__ */ jsxs("div", { children: [hasItemSections && Boolean(sectionLabel?.trim()) && previousItem?.sectionKey !== sectionKey ? /* @__PURE__ */ jsx("div", {
550
- className: "px-1.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
551
- children: sectionLabel
552
- }) : null, /* @__PURE__ */ jsxs("button", {
553
- type: "button",
554
- role: "option",
555
- "aria-selected": isActive,
556
- "data-input-surface-index": index,
557
- onPointerMove: (event) => {
558
- if (event.pointerType !== "touch") setActiveIndexForCurrentItems(index);
559
- },
560
- onPointerDown: (event) => {
561
- if (event.button > 0) return;
562
- event.preventDefault();
563
- onSelectItem(item);
564
- },
565
- onClick: (event) => {
566
- if (event.detail === 0) onSelectItem(item);
567
- },
568
- className: `flex w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-left leading-4 transition-colors ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-100"}`,
569
- children: [/* @__PURE__ */ jsx("span", {
570
- className: "truncate text-xs font-medium",
571
- children: title
572
- }), /* @__PURE__ */ jsx("span", {
573
- className: "truncate text-xs text-gray-500",
574
- children: subtitle
575
- })]
576
- })] }, key);
577
- }) })] })
578
- })]
699
+ ]
579
700
  }), /* @__PURE__ */ jsx("div", {
580
701
  className: "custom-scrollbar min-h-0 min-w-0 select-text overflow-y-auto overscroll-contain p-2.5",
581
702
  onPointerDown: onDetailsPointerDown,
582
- children: activeItem ? /* @__PURE__ */ jsxs("div", {
583
- className: "space-y-3",
584
- children: [
585
- /* @__PURE__ */ jsxs("div", {
586
- className: "flex items-center gap-2",
587
- children: [/* @__PURE__ */ jsx("span", {
588
- className: "inline-flex rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-semibold text-primary",
589
- children: activeItem.subtitle
590
- }), /* @__PURE__ */ jsx("span", {
591
- className: "text-sm font-semibold text-gray-900",
592
- children: activeItem.title
593
- })]
594
- }),
595
- /* @__PURE__ */ jsx("p", {
596
- className: "text-xs leading-5 text-gray-600",
597
- children: activeItem.description
598
- }),
599
- /* @__PURE__ */ jsx("div", {
600
- className: "space-y-1",
601
- children: activeItem.detailLines.map((line) => /* @__PURE__ */ jsx("div", {
602
- className: "min-w-0 break-all rounded-md bg-gray-50 px-2 py-1 text-[11px] leading-5 text-gray-600",
603
- children: line
604
- }, line))
605
- }),
606
- /* @__PURE__ */ jsx("div", {
607
- className: "pt-1 text-[11px] text-gray-500",
608
- children: activeItem.hintLabel ?? texts.itemHintLabel
609
- })
610
- ]
611
- }) : /* @__PURE__ */ jsx("div", {
612
- className: "text-xs text-gray-500",
613
- children: texts.hintLabel
703
+ children: /* @__PURE__ */ jsx(InputSurfaceItemDetails, {
704
+ item: activeItem,
705
+ texts
614
706
  })
615
707
  })]
616
708
  })
@@ -623,7 +715,7 @@ function getInputSurfaceTriggerIdentity(trigger) {
623
715
  return `${trigger.key}:${trigger.marker}:${trigger.start}`;
624
716
  }
625
717
  function isInputSurfaceCreateEvent(trigger, reason) {
626
- return reason.type === "insert-text" && reason.text === trigger.marker && trigger.query === "" && trigger.end === trigger.start + trigger.marker.length;
718
+ return reason.type === "insert-text" && trigger.query === "" && trigger.end === trigger.start + trigger.marker.length;
627
719
  }
628
720
  function resolveInputSurfaceTriggerIdentity(currentIdentity, trigger, reason) {
629
721
  if (!trigger) return null;
@@ -635,21 +727,19 @@ function ChatInputSurfaceHost(props) {
635
727
  const { children, inputSurface, onInputSurfaceTriggerChange, onSelectItem, triggerSpecs = [CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC] } = props;
636
728
  const menuRef = useRef(null);
637
729
  const isDetailsInteractionRef = useRef(false);
730
+ const activeTriggerIdentityRef = useRef(null);
638
731
  const [activeTriggerIdentity, setActiveTriggerIdentity] = useState(null);
639
732
  const isOpen = Boolean(inputSurface) && activeTriggerIdentity !== null;
640
733
  const setActiveTrigger = useCallback((identity, trigger) => {
734
+ activeTriggerIdentityRef.current = identity;
641
735
  setActiveTriggerIdentity(identity);
642
736
  onInputSurfaceTriggerChange?.(identity ? trigger : null);
643
737
  }, [onInputSurfaceTriggerChange]);
644
738
  const handleInputSurfaceSnapshotChange = useCallback((nodes, selection, reason) => {
645
739
  const trigger = resolveChatComposerActiveInputSurfaceTrigger(nodes, selection, triggerSpecs);
646
- const nextIdentity = resolveInputSurfaceTriggerIdentity(activeTriggerIdentity, trigger, reason);
740
+ const nextIdentity = resolveInputSurfaceTriggerIdentity(activeTriggerIdentityRef.current, trigger, reason);
647
741
  setActiveTrigger(nextIdentity, nextIdentity ? trigger : null);
648
- }, [
649
- activeTriggerIdentity,
650
- setActiveTrigger,
651
- triggerSpecs
652
- ]);
742
+ }, [setActiveTrigger, triggerSpecs]);
653
743
  const handleInputSurfaceKeyDown = useCallback((event) => {
654
744
  return menuRef.current?.handleKeyDown(event) ?? false;
655
745
  }, []);
@@ -670,9 +760,10 @@ function ChatInputSurfaceHost(props) {
670
760
  isLoading: inputSurface.isLoading,
671
761
  filterOptions: inputSurface.filterOptions,
672
762
  items: inputSurface.items,
763
+ notice: inputSurface.notice,
673
764
  texts: inputSurface.texts,
674
765
  onSelectItem: (item) => {
675
- setActiveTrigger(null, null);
766
+ if (item.selectionBehavior !== "navigate") setActiveTrigger(null, null);
676
767
  onSelectItem(item);
677
768
  },
678
769
  onOpenChange: handleInputSurfaceOpenChange,
@@ -824,42 +915,42 @@ function filterOptions(options, query) {
824
915
  ].filter((value) => typeof value === "string" && value.trim().length > 0).join(" ").toLowerCase().includes(keyword);
825
916
  });
826
917
  }
827
- const SKILL_PICKER_MAX_HEIGHT = createChatPopoverAvailableHeightLimit("24rem");
918
+ const SKILL_PICKER_HEIGHT = createChatPopoverAvailableHeightLimit("20rem");
828
919
  function ChatInputBarSkillPicker(props) {
829
920
  const { Input, Popover, PopoverContent, PopoverTrigger } = ChatUiPrimitives;
830
921
  const { picker } = props;
831
922
  const listRef = useRef(null);
832
923
  const listId = useId();
833
924
  const [query, setQuery] = useState("");
925
+ const [activeGroupKey, setActiveGroupKey] = useState(null);
834
926
  const [activeIndex, setActiveIndex] = useState(0);
835
927
  const selectedSet = useMemo(() => new Set(picker.selectedKeys), [picker.selectedKeys]);
836
928
  const selectedCount = picker.selectedKeys.length;
837
- const isSearchActive = query.trim().length > 0;
929
+ const availableGroups = useMemo(() => picker.groups?.filter((group) => group.options.length > 0) ?? [], [picker.groups]);
930
+ const resolvedActiveGroupKey = availableGroups.some((group) => group.key === activeGroupKey) ? activeGroupKey : null;
838
931
  const groupedOptions = useMemo(() => {
839
- if (isSearchActive) return null;
840
- const groups = picker.groups?.filter((group) => group.options.length > 0) ?? [];
841
- return groups.length > 0 ? groups : null;
842
- }, [isSearchActive, picker.groups]);
932
+ if (availableGroups.length === 0) return null;
933
+ return (resolvedActiveGroupKey ? availableGroups.filter((group) => group.key === resolvedActiveGroupKey) : availableGroups).map((group) => ({
934
+ ...group,
935
+ options: filterOptions(group.options, query)
936
+ })).filter((group) => group.options.length > 0);
937
+ }, [
938
+ availableGroups,
939
+ query,
940
+ resolvedActiveGroupKey
941
+ ]);
843
942
  const visibleOptions = useMemo(() => {
844
- if (groupedOptions) return groupedOptions.flatMap((group) => group.options);
943
+ if (groupedOptions !== null) return groupedOptions.flatMap((group) => group.options);
845
944
  return filterOptions(picker.options, query);
846
945
  }, [
847
946
  groupedOptions,
848
947
  picker.options,
849
948
  query
850
949
  ]);
851
- useEffect(() => {
852
- setActiveIndex(0);
853
- }, [query]);
854
- useEffect(() => {
855
- setActiveIndex((current) => {
856
- if (visibleOptions.length === 0) return 0;
857
- return Math.min(current, visibleOptions.length - 1);
858
- });
859
- }, [visibleOptions.length]);
950
+ const resolvedActiveIndex = visibleOptions.length === 0 ? 0 : Math.min(activeIndex, visibleOptions.length - 1);
860
951
  useActiveItemScroll({
861
952
  containerRef: listRef,
862
- activeIndex,
953
+ activeIndex: resolvedActiveIndex,
863
954
  itemCount: visibleOptions.length,
864
955
  isEnabled: visibleOptions.length > 0,
865
956
  getItemSelector: (index) => `[data-skill-index="${index}"]`
@@ -875,17 +966,17 @@ function ChatInputBarSkillPicker(props) {
875
966
  if (visibleOptions.length === 0) return;
876
967
  if (event.key === "ArrowDown") {
877
968
  event.preventDefault();
878
- setActiveIndex((current) => Math.min(current + 1, visibleOptions.length - 1));
969
+ setActiveIndex(Math.min(resolvedActiveIndex + 1, visibleOptions.length - 1));
879
970
  return;
880
971
  }
881
972
  if (event.key === "ArrowUp") {
882
973
  event.preventDefault();
883
- setActiveIndex((current) => Math.max(current - 1, 0));
974
+ setActiveIndex(Math.max(resolvedActiveIndex - 1, 0));
884
975
  return;
885
976
  }
886
977
  if (event.key === "Enter") {
887
978
  event.preventDefault();
888
- const activeOption = visibleOptions[activeIndex];
979
+ const activeOption = visibleOptions[resolvedActiveIndex];
889
980
  if (activeOption) onToggleOption(activeOption.key);
890
981
  }
891
982
  };
@@ -912,28 +1003,63 @@ function ChatInputBarSkillPicker(props) {
912
1003
  side: "top",
913
1004
  align: "start",
914
1005
  className: "flex w-[min(360px,calc(100vw-1rem))] flex-col overflow-hidden p-0",
915
- style: { maxHeight: SKILL_PICKER_MAX_HEIGHT },
1006
+ style: { height: SKILL_PICKER_HEIGHT },
916
1007
  children: [
917
1008
  /* @__PURE__ */ jsxs("div", {
918
1009
  className: "shrink-0 space-y-2 border-b border-border px-4 py-3",
919
- children: [/* @__PURE__ */ jsx("div", {
920
- className: "text-sm font-semibold text-foreground",
921
- children: picker.title
922
- }), /* @__PURE__ */ jsxs("div", {
923
- className: "relative",
924
- children: [/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" }), /* @__PURE__ */ jsx(Input, {
925
- value: query,
926
- onChange: (event) => setQuery(event.target.value),
927
- onKeyDown: onSearchKeyDown,
928
- placeholder: picker.searchPlaceholder,
929
- role: "combobox",
930
- "aria-controls": listId,
931
- "aria-expanded": "true",
932
- "aria-autocomplete": "list",
933
- "aria-activedescendant": visibleOptions[activeIndex] ? `${listId}-option-${activeIndex}` : void 0,
934
- className: "h-8 rounded-lg pl-8 text-xs"
935
- })]
936
- })]
1010
+ children: [
1011
+ /* @__PURE__ */ jsx("div", {
1012
+ className: "text-sm font-semibold text-foreground",
1013
+ children: picker.title
1014
+ }),
1015
+ /* @__PURE__ */ jsxs("div", {
1016
+ className: "relative",
1017
+ children: [/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" }), /* @__PURE__ */ jsx(Input, {
1018
+ value: query,
1019
+ onChange: (event) => {
1020
+ setQuery(event.target.value);
1021
+ setActiveIndex(0);
1022
+ },
1023
+ onKeyDown: onSearchKeyDown,
1024
+ placeholder: picker.searchPlaceholder,
1025
+ role: "combobox",
1026
+ "aria-controls": listId,
1027
+ "aria-expanded": "true",
1028
+ "aria-autocomplete": "list",
1029
+ "aria-activedescendant": visibleOptions[resolvedActiveIndex] ? `${listId}-option-${resolvedActiveIndex}` : void 0,
1030
+ className: "h-8 rounded-lg pl-8 text-xs"
1031
+ })]
1032
+ }),
1033
+ availableGroups.length > 0 ? /* @__PURE__ */ jsx("div", {
1034
+ className: "flex flex-wrap gap-1",
1035
+ "aria-label": picker.allGroupsLabel,
1036
+ children: [{
1037
+ key: null,
1038
+ label: picker.allGroupsLabel,
1039
+ count: picker.options.length
1040
+ }, ...availableGroups.map((group) => ({
1041
+ key: group.key,
1042
+ label: group.label || group.key,
1043
+ count: group.options.length
1044
+ }))].map((group) => {
1045
+ const isActive = group.key === resolvedActiveGroupKey;
1046
+ return /* @__PURE__ */ jsxs("button", {
1047
+ type: "button",
1048
+ "aria-pressed": isActive,
1049
+ onMouseDown: (event) => event.preventDefault(),
1050
+ onClick: () => {
1051
+ setActiveGroupKey(group.key);
1052
+ setActiveIndex(0);
1053
+ },
1054
+ className: `inline-flex h-6 items-center gap-1 rounded-md px-2 text-[10px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${isActive ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground hover:text-foreground"}`,
1055
+ children: [/* @__PURE__ */ jsx("span", { children: group.label }), /* @__PURE__ */ jsx("span", {
1056
+ className: "opacity-70",
1057
+ children: group.count
1058
+ })]
1059
+ }, group.key ?? "all-skills");
1060
+ })
1061
+ }) : null
1062
+ ]
937
1063
  }),
938
1064
  /* @__PURE__ */ jsx("div", {
939
1065
  ref: listRef,
@@ -962,7 +1088,7 @@ function ChatInputBarSkillPicker(props) {
962
1088
  const index = visibleIndex;
963
1089
  visibleIndex += 1;
964
1090
  const isSelected = selectedSet.has(option.key);
965
- const isActive = index === activeIndex;
1091
+ const isActive = index === resolvedActiveIndex;
966
1092
  return /* @__PURE__ */ jsxs("div", {
967
1093
  id: `${listId}-option-${index}`,
968
1094
  role: "option",
@@ -1316,11 +1442,18 @@ function buildTokenClassName(tokenKind) {
1316
1442
  ].join(" ");
1317
1443
  }
1318
1444
  function ChatComposerTokenChip({ label, tokenKind }) {
1445
+ const isWorkspaceReference = tokenKind === "workspace_file" || tokenKind === "workspace_directory";
1319
1446
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
1320
1447
  className: tokenKind === "file" ? "inline-flex h-4.5 w-4.5 shrink-0 items-center justify-center rounded-md bg-card text-muted-foreground ring-1 ring-border" : "inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center text-primary/70",
1321
1448
  children: tokenKind === "file" ? /* @__PURE__ */ jsx(ImageIcon, {
1322
1449
  "aria-hidden": "true",
1323
1450
  className: "h-3 w-3"
1451
+ }) : tokenKind === "workspace_file" ? /* @__PURE__ */ jsx(FileText, {
1452
+ "aria-hidden": "true",
1453
+ className: "h-3 w-3"
1454
+ }) : tokenKind === "workspace_directory" ? /* @__PURE__ */ jsx(Folder, {
1455
+ "aria-hidden": "true",
1456
+ className: "h-3 w-3"
1324
1457
  }) : tokenKind === "panel_app" ? /* @__PURE__ */ jsx(AppWindow, {
1325
1458
  "aria-hidden": "true",
1326
1459
  className: "h-3 w-3"
@@ -1329,7 +1462,7 @@ function ChatComposerTokenChip({ label, tokenKind }) {
1329
1462
  className: "h-3 w-3"
1330
1463
  })
1331
1464
  }), /* @__PURE__ */ jsx("span", {
1332
- className: tokenKind === "file" ? "min-w-0 flex-1 truncate text-[12px] font-medium text-foreground" : "truncate",
1465
+ className: tokenKind === "file" ? "min-w-0 flex-1 truncate text-[12px] font-medium text-foreground" : isWorkspaceReference ? "max-w-[16rem] truncate" : "truncate",
1333
1466
  children: label
1334
1467
  })] });
1335
1468
  }
@@ -1424,6 +1557,7 @@ function $isChatComposerTokenNode(node) {
1424
1557
  }
1425
1558
  //#endregion
1426
1559
  //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-lexical-editor-state.ts
1560
+ const CHAT_COMPOSER_EXTERNAL_UPDATE_TAG = "nextclaw-chat-composer-external-update";
1427
1561
  function getComposerLeafDescriptors() {
1428
1562
  const root = $getRoot();
1429
1563
  const paragraph = root.getFirstChild();
@@ -1625,12 +1759,12 @@ function writeChatComposerStateToLexicalRoot(nodes, selection) {
1625
1759
  nextSelection.focus.set(focus.key, focus.offset, focus.type);
1626
1760
  $setSelection(nextSelection);
1627
1761
  }
1628
- function syncLexicalEditorFromChatComposerState(editor, nodes, selection) {
1762
+ function syncLexicalEditorFromChatComposerState(editor, nodes, selection, preserveDomSelection = false) {
1629
1763
  editor.update(() => {
1630
1764
  writeChatComposerStateToLexicalRoot(nodes, selection);
1631
- });
1765
+ }, { tag: preserveDomSelection ? [CHAT_COMPOSER_EXTERNAL_UPDATE_TAG, SKIP_DOM_SELECTION_TAG] : CHAT_COMPOSER_EXTERNAL_UPDATE_TAG });
1632
1766
  }
1633
- function syncLexicalSelectionFromChatComposerSelection(editor, selection) {
1767
+ function syncLexicalSelectionFromChatComposerSelection(editor, selection, preserveDomSelection = false) {
1634
1768
  editor.update(() => {
1635
1769
  const nextSelection = $createRangeSelection();
1636
1770
  const anchor = buildSelectionPointFromOffset(selection.start);
@@ -1638,7 +1772,7 @@ function syncLexicalSelectionFromChatComposerSelection(editor, selection) {
1638
1772
  nextSelection.anchor.set(anchor.key, anchor.offset, anchor.type);
1639
1773
  nextSelection.focus.set(focus.key, focus.offset, focus.type);
1640
1774
  $setSelection(nextSelection);
1641
- });
1775
+ }, { tag: preserveDomSelection ? [CHAT_COMPOSER_EXTERNAL_UPDATE_TAG, SKIP_DOM_SELECTION_TAG] : CHAT_COMPOSER_EXTERNAL_UPDATE_TAG });
1642
1776
  }
1643
1777
  //#endregion
1644
1778
  //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-lexical-operations.ts
@@ -1707,19 +1841,6 @@ function insertInputSurfaceItemIntoChatComposer(params) {
1707
1841
  function getChatComposerNodesSignature(nodes) {
1708
1842
  return nodes.map((node) => node.type === "text" ? `text:${node.text}` : `token:${node.tokenKind}:${node.tokenKey}:${node.label}`).join("");
1709
1843
  }
1710
- function replaceChatComposerSelectionWithText(params) {
1711
- const { nodes, selection, text } = params;
1712
- const documentLength = getDocumentLength(nodes);
1713
- const [selectionStart, selectionEnd] = selection ? [Math.min(selection.start, selection.end), Math.max(selection.start, selection.end)] : [documentLength, documentLength];
1714
- const nextOffset = selectionStart + text.length;
1715
- return {
1716
- nodes: replaceChatComposerRange(nodes, selectionStart, selectionEnd, [createChatComposerTextNode(text)]),
1717
- selection: {
1718
- start: nextOffset,
1719
- end: nextOffset
1720
- }
1721
- };
1722
- }
1723
1844
  function insertFileTokenIntoChatComposer(params) {
1724
1845
  const { label, nodes, selection, tokenKey } = params;
1725
1846
  return insertToken({
@@ -1766,93 +1887,43 @@ function syncSelectedSkillsIntoChatComposer(params) {
1766
1887
  selection
1767
1888
  };
1768
1889
  }
1769
- function deleteChatComposerContent(params) {
1770
- const { direction, nodes, selection: currentSelection } = params;
1771
- const documentLength = getDocumentLength(nodes);
1772
- const [selectionStart, selectionEnd] = currentSelection ? [Math.min(currentSelection.start, currentSelection.end), Math.max(currentSelection.start, currentSelection.end)] : [documentLength, documentLength];
1773
- let rangeStart = selectionStart;
1774
- let rangeEnd = selectionEnd;
1775
- if (rangeStart === rangeEnd) if (direction === "backward" && rangeStart > 0) rangeStart -= 1;
1776
- else if (direction === "forward" && rangeEnd < documentLength) rangeEnd += 1;
1777
- else return {
1778
- nodes: normalizeChatComposerNodes(nodes),
1779
- selection: {
1780
- start: rangeStart,
1781
- end: rangeEnd
1782
- }
1783
- };
1784
- return {
1785
- nodes: replaceChatComposerRange(nodes, rangeStart, rangeEnd, []),
1786
- selection: {
1787
- start: rangeStart,
1788
- end: rangeStart
1789
- }
1790
- };
1791
- }
1792
1890
  //#endregion
1793
1891
  //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-lexical-controller.ts
1892
+ function deleteAdjacentChatComposerToken(event) {
1893
+ if (event.key !== "Backspace" && event.key !== "Delete") return false;
1894
+ const selection = $getSelection();
1895
+ if ($isRangeSelection(selection) && !selection.isCollapsed()) return false;
1896
+ let candidate = null;
1897
+ if ($isRangeSelection(selection)) {
1898
+ const { anchor } = selection;
1899
+ const anchorNode = anchor.getNode();
1900
+ if (anchor.type === "element" && $isElementNode(anchorNode)) {
1901
+ const candidateIndex = event.key === "Backspace" ? anchor.offset - 1 : anchor.offset;
1902
+ candidate = anchorNode.getChildAtIndex(candidateIndex);
1903
+ } else if (event.key === "Backspace" && anchor.offset === 0) candidate = anchorNode.getPreviousSibling();
1904
+ else if (event.key === "Delete" && anchor.offset === anchorNode.getTextContentSize()) candidate = anchorNode.getNextSibling();
1905
+ } else if (event.key === "Backspace") {
1906
+ const paragraph = $getRoot().getFirstChild();
1907
+ candidate = $isElementNode(paragraph) ? paragraph.getLastChild() : null;
1908
+ }
1909
+ if ($isTextNode(candidate) && candidate.getTextContentSize() === 0) candidate = event.key === "Backspace" ? candidate.getPreviousSibling() : candidate.getNextSibling();
1910
+ if (!$isChatComposerTokenNode(candidate)) return false;
1911
+ event.preventDefault();
1912
+ candidate.remove();
1913
+ return true;
1914
+ }
1794
1915
  function resolveLexicalComposerKeyboardAction(params) {
1795
1916
  const { canStopGeneration, isComposing, isSending, key, shiftKey } = params;
1796
- if (key === "Escape") {
1797
- if (isSending && canStopGeneration) return { type: "stop-generation" };
1798
- return { type: "noop" };
1799
- }
1800
- if (key === "Enter" && shiftKey) return { type: "insert-line-break" };
1801
- if (key === "Enter") return { type: "send-message" };
1802
- if (!isComposing && (key === "Backspace" || key === "Delete")) return {
1803
- type: "delete-content",
1804
- direction: key === "Backspace" ? "backward" : "forward"
1805
- };
1917
+ if (isComposing) return { type: "noop" };
1918
+ if (key === "Escape") return isSending && canStopGeneration ? { type: "stop-generation" } : { type: "noop" };
1919
+ if (key === "Enter" && !shiftKey) return { type: "send-message" };
1806
1920
  return { type: "noop" };
1807
1921
  }
1808
- function getChatComposerContentSignature(nodes) {
1809
- return JSON.stringify(nodes.map((node) => node.type === "text" ? {
1810
- text: node.text,
1811
- type: node.type
1812
- } : {
1813
- label: node.label,
1814
- tokenKey: node.tokenKey,
1815
- tokenKind: node.tokenKind,
1816
- type: node.type
1817
- }));
1818
- }
1819
- function handleLexicalComposerBeforeInput(params) {
1820
- const { disabled, event, isComposing, publishSnapshot, snapshotReader } = params;
1821
- const nativeEvent = event.nativeEvent;
1822
- const shouldInsertText = nativeEvent.inputType === "insertText" || nativeEvent.inputType === "insertReplacementText";
1823
- if (disabled || isComposing || nativeEvent.isComposing || !shouldInsertText || !nativeEvent.data) return;
1824
- event.preventDefault();
1825
- publishSnapshot(replaceChatComposerSelectionWithText({
1826
- nodes: snapshotReader().nodes,
1827
- selection: snapshotReader().selection,
1828
- text: nativeEvent.data
1829
- }), { inputSurfaceReason: {
1830
- type: "insert-text",
1831
- text: nativeEvent.data
1832
- } });
1833
- }
1834
- function handleLexicalComposerCompositionEnd(params) {
1835
- const { compositionStartSnapshot, data, fallbackSnapshot, publishSnapshot, snapshotReader } = params;
1836
- const currentSnapshot = snapshotReader();
1837
- const editorSnapshot = fallbackSnapshot();
1838
- const baseSnapshot = compositionStartSnapshot ?? currentSnapshot;
1839
- publishSnapshot(getChatComposerContentSignature(editorSnapshot.nodes) !== getChatComposerContentSignature(baseSnapshot.nodes) ? editorSnapshot : data.length > 0 ? replaceChatComposerSelectionWithText({
1840
- nodes: baseSnapshot.nodes,
1841
- selection: baseSnapshot.selection,
1842
- text: data
1843
- }) : editorSnapshot, {
1844
- forcePublish: true,
1845
- inputSurfaceReason: {
1846
- type: "insert-text",
1847
- text: data
1848
- }
1849
- });
1850
- }
1851
1922
  function handleLexicalComposerKeyboardCommand(params) {
1852
- const { actions, nativeEvent, publishSnapshot, snapshot } = params;
1923
+ const { actions, isComposing, nativeEvent } = params;
1853
1924
  const action = resolveLexicalComposerKeyboardAction({
1854
1925
  canStopGeneration: actions.canStopGeneration,
1855
- isComposing: nativeEvent.isComposing,
1926
+ isComposing,
1856
1927
  isSending: actions.isSending,
1857
1928
  key: nativeEvent.key,
1858
1929
  shiftKey: nativeEvent.shiftKey
@@ -1862,26 +1933,9 @@ function handleLexicalComposerKeyboardCommand(params) {
1862
1933
  case "stop-generation":
1863
1934
  actions.onStop();
1864
1935
  return true;
1865
- case "insert-line-break":
1866
- publishSnapshot(replaceChatComposerSelectionWithText({
1867
- nodes: snapshot.nodes,
1868
- selection: snapshot.selection,
1869
- text: "\n"
1870
- }), { inputSurfaceReason: {
1871
- type: "insert-text",
1872
- text: "\n"
1873
- } });
1874
- return true;
1875
1936
  case "send-message":
1876
1937
  actions.onSend();
1877
1938
  return true;
1878
- case "delete-content":
1879
- publishSnapshot(deleteChatComposerContent({
1880
- direction: action.direction,
1881
- nodes: snapshot.nodes,
1882
- selection: snapshot.selection
1883
- }), { inputSurfaceReason: { type: "delete-content" } });
1884
- return true;
1885
1939
  case "noop": return false;
1886
1940
  }
1887
1941
  }
@@ -1895,13 +1949,10 @@ function getChatComposerDocumentLength(nodes) {
1895
1949
  }
1896
1950
  var ChatComposerLexicalOwner = class {
1897
1951
  constructor() {
1898
- this.isApplyingExternalUpdateRef = createMutableRef(false);
1899
- this.isComposingRef = createMutableRef(false);
1900
1952
  this.pendingOwnerSignatureRef = createMutableRef(null);
1901
1953
  this.pendingSelectionRef = createMutableRef(null);
1902
1954
  this.selectionRef = createMutableRef(null);
1903
1955
  this.shouldFocusAfterSyncRef = createMutableRef(false);
1904
- this.compositionStartSnapshotRef = createMutableRef(null);
1905
1956
  this.editorSignatureRef = createMutableRef("");
1906
1957
  this.lastPublishedSignatureRef = createMutableRef("");
1907
1958
  this.pendingInputSurfaceReasonRef = createMutableRef(null);
@@ -1912,14 +1963,17 @@ var ChatComposerLexicalOwner = class {
1912
1963
  };
1913
1964
  this.bindEditor = (editor) => {
1914
1965
  this.editor = editor;
1966
+ const signature = getChatComposerNodesSignature(readChatComposerSnapshotFromEditorState(editor.getEditorState()).nodes);
1967
+ this.editorSignatureRef.current = signature;
1968
+ this.lastPublishedSignatureRef.current = signature;
1915
1969
  return () => {
1916
1970
  if (this.editor === editor) this.editor = null;
1917
1971
  };
1918
1972
  };
1919
1973
  this.registerEditorListeners = (editor) => {
1920
- return mergeRegister(editor.registerUpdateListener(({ editorState }) => {
1974
+ return mergeRegister(editor.registerUpdateListener(({ editorState, tags }) => {
1921
1975
  const runtime = this.getRuntime();
1922
- this.handleEditorUpdate(editorState, runtime.callbacks);
1976
+ this.handleEditorUpdate(editorState, runtime.callbacks, tags);
1923
1977
  }), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
1924
1978
  const runtime = this.getRuntime();
1925
1979
  this.handleSelectionChange(editor, runtime.callbacks);
@@ -1932,13 +1986,12 @@ var ChatComposerLexicalOwner = class {
1932
1986
  return this.handleKeyDown({
1933
1987
  actions: runtime.actions,
1934
1988
  callbacks: runtime.callbacks,
1935
- event,
1936
- fallbackNodes: runtime.fallbackNodes
1989
+ event
1937
1990
  });
1938
1991
  }, COMMAND_PRIORITY_HIGH));
1939
1992
  };
1940
1993
  this.syncExternalState = (editor, nodes) => {
1941
- if (this.isComposingRef.current) return;
1994
+ if (editor.isComposing()) return;
1942
1995
  const nextSignature = getChatComposerNodesSignature(nodes);
1943
1996
  const pendingSelection = this.pendingSelectionRef.current;
1944
1997
  const pendingOwnerSignature = this.pendingOwnerSignatureRef.current;
@@ -1948,12 +2001,12 @@ var ChatComposerLexicalOwner = class {
1948
2001
  }
1949
2002
  const shouldSyncDocument = nextSignature !== this.editorSignatureRef.current;
1950
2003
  if (!shouldSyncDocument && !pendingSelection) return;
1951
- this.startApplyingExternalUpdate();
2004
+ const preserveDomSelection = editor.getRootElement() !== document.activeElement;
1952
2005
  if (shouldSyncDocument) {
1953
- syncLexicalEditorFromChatComposerState(editor, nodes, pendingSelection);
1954
2006
  this.editorSignatureRef.current = nextSignature;
1955
2007
  this.lastPublishedSignatureRef.current = nextSignature;
1956
- } else if (pendingSelection) syncLexicalSelectionFromChatComposerSelection(editor, pendingSelection);
2008
+ syncLexicalEditorFromChatComposerState(editor, nodes, pendingSelection, preserveDomSelection);
2009
+ } else if (pendingSelection) syncLexicalSelectionFromChatComposerSelection(editor, pendingSelection, preserveDomSelection);
1957
2010
  if (pendingSelection) {
1958
2011
  this.selectionRef.current = pendingSelection;
1959
2012
  this.pendingSelectionRef.current = null;
@@ -1974,12 +2027,11 @@ var ChatComposerLexicalOwner = class {
1974
2027
  const { editor } = this;
1975
2028
  this.pendingOwnerSignatureRef.current = signature;
1976
2029
  if (editor) {
1977
- this.startApplyingExternalUpdate();
1978
- syncLexicalEditorFromChatComposerState(editor, snapshot.nodes, snapshot.selection);
1979
2030
  this.editorSignatureRef.current = signature;
2031
+ syncLexicalEditorFromChatComposerState(editor, snapshot.nodes, snapshot.selection);
1980
2032
  }
1981
2033
  callbacks.onInputSurfaceSnapshotChange?.(snapshot.nodes, snapshot.selection, options?.inputSurfaceReason ?? { type: "programmatic" });
1982
- if (options?.forcePublish || signature !== this.lastPublishedSignatureRef.current) {
2034
+ if (signature !== this.lastPublishedSignatureRef.current) {
1983
2035
  this.lastPublishedSignatureRef.current = signature;
1984
2036
  callbacks.onNodesChange(snapshot.nodes);
1985
2037
  }
@@ -2004,11 +2056,10 @@ var ChatComposerLexicalOwner = class {
2004
2056
  this.pendingSelectionRef.current = targetSelection;
2005
2057
  editor.getRootElement()?.focus({ preventScroll: true });
2006
2058
  if (nodes) {
2007
- this.startApplyingExternalUpdate();
2008
2059
  const signature = getChatComposerNodesSignature(nodes);
2009
- syncLexicalEditorFromChatComposerState(editor, nodes, targetSelection);
2010
2060
  this.editorSignatureRef.current = signature;
2011
2061
  this.lastPublishedSignatureRef.current = signature;
2062
+ syncLexicalEditorFromChatComposerState(editor, nodes, targetSelection);
2012
2063
  } else syncLexicalSelectionFromChatComposerSelection(editor, targetSelection);
2013
2064
  editor.focus(() => {
2014
2065
  syncLexicalSelectionFromChatComposerSelection(editor, targetSelection);
@@ -2068,55 +2119,35 @@ var ChatComposerLexicalOwner = class {
2068
2119
  }
2069
2120
  };
2070
2121
  };
2071
- this.handleBeforeInput = (params) => {
2072
- const { disabled, event } = params;
2073
- const { callbacks, fallbackNodes } = this.getRuntime();
2074
- handleLexicalComposerBeforeInput({
2075
- disabled,
2076
- event,
2077
- isComposing: this.isComposingRef.current,
2078
- publishSnapshot: (snapshot, options) => this.publishSnapshot(snapshot, callbacks, options),
2079
- snapshotReader: () => this.readComposerSnapshot(fallbackNodes)
2080
- });
2081
- };
2082
- this.handleCompositionStart = () => {
2083
- const { fallbackNodes } = this.getRuntime();
2084
- this.compositionStartSnapshotRef.current = this.readComposerSnapshot(fallbackNodes);
2085
- this.isComposingRef.current = true;
2086
- };
2087
- this.handleCompositionEnd = (params) => {
2088
- const { data } = params;
2089
- const { callbacks, fallbackNodes } = this.getRuntime();
2090
- this.isComposingRef.current = false;
2091
- handleLexicalComposerCompositionEnd({
2092
- compositionStartSnapshot: this.compositionStartSnapshotRef.current,
2093
- data,
2094
- fallbackSnapshot: () => this.readComposerSnapshot(fallbackNodes),
2095
- publishSnapshot: (snapshot, options) => this.publishSnapshot(snapshot, callbacks, options),
2096
- snapshotReader: () => this.readComposerSnapshot(fallbackNodes)
2097
- });
2098
- this.compositionStartSnapshotRef.current = null;
2099
- };
2100
2122
  this.handleKeyDown = (params) => {
2101
- const { actions, callbacks, event, fallbackNodes } = params;
2102
- if (event.key.length === 1 && !event.isComposing && !event.altKey && !event.ctrlKey && !event.metaKey) this.pendingInputSurfaceReasonRef.current = {
2123
+ const { actions, callbacks, event } = params;
2124
+ const isComposing = event.isComposing || (this.editor?.isComposing() ?? false);
2125
+ if (event.key.length === 1 && !isComposing && !event.altKey && !event.ctrlKey && !event.metaKey) this.pendingInputSurfaceReasonRef.current = {
2103
2126
  type: "insert-text",
2104
2127
  text: event.key
2105
2128
  };
2106
- const snapshot = this.readComposerSnapshot(fallbackNodes);
2107
- if (callbacks.onInputSurfaceKeyDown?.(event)) return true;
2129
+ else if (!isComposing && (event.key === "Backspace" || event.key === "Delete")) this.pendingInputSurfaceReasonRef.current = { type: "delete-content" };
2130
+ else if (!isComposing && event.key === "Enter" && event.shiftKey) this.pendingInputSurfaceReasonRef.current = {
2131
+ type: "insert-text",
2132
+ text: "\n"
2133
+ };
2134
+ if (callbacks.onInputSurfaceKeyDown?.(event)) {
2135
+ this.pendingInputSurfaceReasonRef.current = null;
2136
+ return true;
2137
+ }
2138
+ if (!isComposing && deleteAdjacentChatComposerToken(event)) return true;
2108
2139
  return handleLexicalComposerKeyboardCommand({
2109
2140
  actions,
2110
- nativeEvent: event,
2111
- publishSnapshot: (nextSnapshot, options) => this.publishSnapshot(nextSnapshot, callbacks, options),
2112
- snapshot
2141
+ isComposing,
2142
+ nativeEvent: event
2113
2143
  });
2114
2144
  };
2115
- this.handleEditorUpdate = (editorState, callbacks) => {
2145
+ this.handleEditorUpdate = (editorState, callbacks, tags) => {
2116
2146
  const snapshot = readChatComposerSnapshotFromEditorState(editorState);
2117
2147
  const signature = getChatComposerNodesSignature(snapshot.nodes);
2148
+ const expectedExternalSignature = this.editorSignatureRef.current;
2118
2149
  this.editorSignatureRef.current = signature;
2119
- if (this.isApplyingExternalUpdateRef.current || this.isComposingRef.current) return;
2150
+ if (tags.has("nextclaw-chat-composer-external-update") && signature === expectedExternalSignature || this.editor?.isComposing() && !tags.has(COMPOSITION_END_TAG)) return;
2120
2151
  this.selectionRef.current = snapshot.selection;
2121
2152
  callbacks.onInputSurfaceSnapshotChange?.(snapshot.nodes, snapshot.selection, this.consumeInputSurfaceReason() ?? { type: "sync" });
2122
2153
  if (signature === this.lastPublishedSignatureRef.current) return;
@@ -2125,7 +2156,7 @@ var ChatComposerLexicalOwner = class {
2125
2156
  };
2126
2157
  this.handleSelectionChange = (editor, callbacks) => {
2127
2158
  const snapshot = readChatComposerSnapshotFromEditorState(editor.getEditorState());
2128
- if (this.isComposingRef.current) return;
2159
+ if (editor.isComposing()) return;
2129
2160
  this.selectionRef.current = snapshot.selection;
2130
2161
  callbacks.onInputSurfaceSnapshotChange?.(snapshot.nodes, snapshot.selection, { type: "selection" });
2131
2162
  };
@@ -2138,12 +2169,6 @@ var ChatComposerLexicalOwner = class {
2138
2169
  const { callbacks, fallbackNodes } = this.getRuntime();
2139
2170
  this.publishSnapshot(createSnapshot(this.readComposerSnapshot(fallbackNodes)), callbacks, options);
2140
2171
  };
2141
- this.startApplyingExternalUpdate = () => {
2142
- this.isApplyingExternalUpdateRef.current = true;
2143
- requestAnimationFrame(() => {
2144
- this.isApplyingExternalUpdateRef.current = false;
2145
- });
2146
- };
2147
2172
  this.getRuntime = () => {
2148
2173
  if (!this.runtime) throw new Error("ChatComposerLexicalOwner runtime has not been configured.");
2149
2174
  return this.runtime;
@@ -2214,19 +2239,6 @@ const ChatInputBarTokenizedComposer = forwardRef(function ChatInputBarTokenizedC
2214
2239
  children: /* @__PURE__ */ jsx(PlainTextPlugin, {
2215
2240
  contentEditable: /* @__PURE__ */ jsx(ContentEditable, {
2216
2241
  className: "min-h-7 max-h-[188px] w-full overflow-y-auto whitespace-pre-wrap break-words bg-transparent py-0.5 text-sm leading-6 text-foreground outline-none",
2217
- onBeforeInput: (event) => {
2218
- owner.handleBeforeInput({
2219
- disabled,
2220
- event
2221
- });
2222
- },
2223
- onCompositionEnd: (event) => {
2224
- const nativeEvent = event.nativeEvent;
2225
- owner.handleCompositionEnd({ data: typeof nativeEvent.data === "string" ? nativeEvent.data : "" });
2226
- },
2227
- onCompositionStart: () => {
2228
- owner.handleCompositionStart();
2229
- },
2230
2242
  onPaste: (event) => {
2231
2243
  const files = Array.from(event.clipboardData.files ?? []);
2232
2244
  if (files.length > 0 && onFilesAdd) {
@@ -2366,7 +2378,13 @@ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSur
2366
2378
  children: /* @__PURE__ */ jsx(ChatInputSurfaceHost, {
2367
2379
  inputSurface: resolvedInputSurface,
2368
2380
  onInputSurfaceTriggerChange: composer.onInputSurfaceTriggerChange,
2369
- onSelectItem: (item) => composerRef.current?.insertInputSurfaceItem(item, composer.inputSurfaceTriggerSpecs),
2381
+ onSelectItem: (item) => {
2382
+ if (item.selectionBehavior === "navigate") {
2383
+ resolvedInputSurface?.onSelectItem?.(item);
2384
+ return;
2385
+ }
2386
+ composerRef.current?.insertInputSurfaceItem(item, composer.inputSurfaceTriggerSpecs);
2387
+ },
2370
2388
  triggerSpecs: composer.inputSurfaceTriggerSpecs,
2371
2389
  children: ({ onInputSurfaceKeyDown, onInputSurfaceOpenChange, onInputSurfaceSnapshotChange }) => /* @__PURE__ */ jsx(ChatInputBarTokenizedComposer, {
2372
2390
  ref: composerRef,
@@ -2425,6 +2443,7 @@ function ChatMessageAvatar({ role, size = "default" }) {
2425
2443
  function resolveInlineTokenTone(kind) {
2426
2444
  if (kind === "skill") return "skill";
2427
2445
  if (kind === "panel_app") return "panel_app";
2446
+ if (kind === "workspace_file" || kind === "workspace_directory") return "workspace";
2428
2447
  return "default";
2429
2448
  }
2430
2449
  function resolveInlineTokenBadgeClassName(tone, isUser, interactive) {
@@ -2437,10 +2456,16 @@ function resolveInlineTokenIconClassName(tone, isUser) {
2437
2456
  if (tone === "panel_app") return isUser ? "text-primary-foreground/80" : "text-muted-foreground";
2438
2457
  return isUser ? "text-primary-foreground/80" : "text-muted-foreground";
2439
2458
  }
2440
- function renderInlineTokenIcon(tone) {
2459
+ function renderInlineTokenIcon(tone, kind) {
2441
2460
  return tone === "panel_app" ? /* @__PURE__ */ jsx(AppWindow, {
2442
2461
  "aria-hidden": "true",
2443
2462
  className: "h-3 w-3"
2463
+ }) : kind === "workspace_file" ? /* @__PURE__ */ jsx(FileText, {
2464
+ "aria-hidden": "true",
2465
+ className: "h-3 w-3"
2466
+ }) : kind === "workspace_directory" ? /* @__PURE__ */ jsx(Folder, {
2467
+ "aria-hidden": "true",
2468
+ className: "h-3 w-3"
2444
2469
  }) : /* @__PURE__ */ jsx(Puzzle, {
2445
2470
  "aria-hidden": "true",
2446
2471
  className: "h-3 w-3"
@@ -2452,7 +2477,7 @@ function ChatInlineTokenBadge({ kind, label, isUser, onClick }) {
2452
2477
  const className = cn("nextclaw-chat-inline-token mx-[2px] inline-flex max-w-full items-center gap-1 align-baseline text-[11px] font-medium", tone === "skill" ? "h-auto rounded-none px-0 py-0" : "h-7 rounded-xl border px-2.5", resolveInlineTokenBadgeClassName(tone, isUser, interactive));
2453
2478
  const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
2454
2479
  className: cn("inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center", resolveInlineTokenIconClassName(tone, isUser)),
2455
- children: renderInlineTokenIcon(tone)
2480
+ children: renderInlineTokenIcon(tone, kind)
2456
2481
  }), /* @__PURE__ */ jsx("span", {
2457
2482
  className: "truncate",
2458
2483
  children: label
@@ -2780,7 +2805,7 @@ function isRecord$1(value) {
2780
2805
  function readString(value) {
2781
2806
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2782
2807
  }
2783
- function readPositiveInteger(value) {
2808
+ function readPositiveInteger$1(value) {
2784
2809
  return Number.isInteger(value) && Number(value) > 0 ? Number(value) : void 0;
2785
2810
  }
2786
2811
  function readFilePreviewViewer(value) {
@@ -2800,8 +2825,8 @@ function readFileTarget(record) {
2800
2825
  type: "file",
2801
2826
  payload: {
2802
2827
  path,
2803
- line: readPositiveInteger(payload.line),
2804
- column: readPositiveInteger(payload.column),
2828
+ line: readPositiveInteger$1(payload.line),
2829
+ column: readPositiveInteger$1(payload.column),
2805
2830
  viewer: readFilePreviewViewer(payload.viewer)
2806
2831
  }
2807
2832
  };
@@ -2936,6 +2961,114 @@ function ChatInlineDisplay({ display, renderInlineDisplay }) {
2936
2961
  return /* @__PURE__ */ jsx(ChatInlineDisplayFallback, { display });
2937
2962
  }
2938
2963
  //#endregion
2964
+ //#region src/components/chat/ui/chat-message-list/mermaid/chat-mermaid-diagram.tsx
2965
+ const STREAMING_RENDER_DELAY_MS = 300;
2966
+ let mermaidModulePromise = null;
2967
+ function loadMermaid() {
2968
+ mermaidModulePromise ??= import("mermaid").then((module) => module.default);
2969
+ return mermaidModulePromise;
2970
+ }
2971
+ function readMermaidTheme() {
2972
+ if (typeof document === "undefined") return "default";
2973
+ const root = document.documentElement;
2974
+ return root.getAttribute("data-theme-appearance") === "dark" || root.classList.contains("dark") ? "dark" : "default";
2975
+ }
2976
+ function useMermaidTheme() {
2977
+ const [theme, setTheme] = useState(readMermaidTheme);
2978
+ useEffect(() => {
2979
+ const root = document.documentElement;
2980
+ const observer = new MutationObserver(() => setTheme(readMermaidTheme()));
2981
+ observer.observe(root, {
2982
+ attributes: true,
2983
+ attributeFilter: ["class", "data-theme-appearance"]
2984
+ });
2985
+ return () => observer.disconnect();
2986
+ }, []);
2987
+ return theme;
2988
+ }
2989
+ function ChatMermaidDiagram({ isStreaming, source, texts }) {
2990
+ const reactId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
2991
+ const renderSequence = useRef(0);
2992
+ const theme = useMermaidTheme();
2993
+ const normalizedSource = source.trim();
2994
+ const renderKey = `${theme}:${normalizedSource}`;
2995
+ const [state, setState] = useState(null);
2996
+ const diagramLabel = texts.mermaidDiagramLabel ?? "Mermaid diagram";
2997
+ const errorLabel = texts.mermaidRenderErrorLabel ?? "Diagram could not be rendered";
2998
+ useEffect(() => {
2999
+ let active = true;
3000
+ const renderDiagram = async () => {
3001
+ try {
3002
+ const mermaid = await loadMermaid();
3003
+ if (!active) return;
3004
+ renderSequence.current += 1;
3005
+ const diagramId = `nextclaw-mermaid-${reactId}-${renderSequence.current}`;
3006
+ mermaid.initialize({
3007
+ securityLevel: "strict",
3008
+ startOnLoad: false,
3009
+ suppressErrorRendering: true,
3010
+ theme
3011
+ });
3012
+ const valid = await mermaid.parse(normalizedSource, { suppressErrors: true });
3013
+ if (!active) return;
3014
+ if (!valid) throw new Error("Invalid Mermaid diagram");
3015
+ const { svg } = await mermaid.render(diagramId, normalizedSource);
3016
+ if (active) setState({
3017
+ key: renderKey,
3018
+ status: "rendered",
3019
+ svg
3020
+ });
3021
+ } catch {
3022
+ if (active && !isStreaming) setState({
3023
+ key: renderKey,
3024
+ status: "error"
3025
+ });
3026
+ }
3027
+ };
3028
+ const timeout = window.setTimeout(() => void renderDiagram(), isStreaming ? STREAMING_RENDER_DELAY_MS : 0);
3029
+ return () => {
3030
+ active = false;
3031
+ window.clearTimeout(timeout);
3032
+ };
3033
+ }, [
3034
+ isStreaming,
3035
+ normalizedSource,
3036
+ reactId,
3037
+ renderKey,
3038
+ theme
3039
+ ]);
3040
+ const currentState = state?.key === renderKey ? state : null;
3041
+ if (currentState?.status === "error") return /* @__PURE__ */ jsxs("div", {
3042
+ "data-chat-mermaid-error": "true",
3043
+ className: "my-3 overflow-hidden rounded-xl border border-border bg-muted/25",
3044
+ children: [/* @__PURE__ */ jsx("div", {
3045
+ className: "border-b border-border px-3 py-2 text-xs text-muted-foreground",
3046
+ children: errorLabel
3047
+ }), /* @__PURE__ */ jsx(ChatCodeBlock, {
3048
+ className: "language-mermaid",
3049
+ texts,
3050
+ children: normalizedSource
3051
+ })]
3052
+ });
3053
+ const renderedState = state?.status === "rendered" ? state : null;
3054
+ if (!renderedState) return /* @__PURE__ */ jsx(ChatCodeBlock, {
3055
+ className: "language-mermaid",
3056
+ texts,
3057
+ children: normalizedSource
3058
+ });
3059
+ return /* @__PURE__ */ jsx("figure", {
3060
+ "aria-label": diagramLabel,
3061
+ "aria-busy": !currentState,
3062
+ "data-chat-mermaid-diagram": "true",
3063
+ "data-chat-mermaid-updating": !currentState || void 0,
3064
+ className: "my-3 min-h-24 overflow-auto rounded-xl border border-border bg-muted/20 p-3",
3065
+ children: /* @__PURE__ */ jsx("div", {
3066
+ className: "min-w-fit [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full",
3067
+ dangerouslySetInnerHTML: { __html: renderedState.svg }
3068
+ })
3069
+ });
3070
+ }
3071
+ //#endregion
2939
3072
  //#region src/components/chat/ui/chat-message-list/chat-message-file/chat-message-image-preview.tsx
2940
3073
  function ChatMessageImageLightbox({ alt, closeLabel, onClose, src }) {
2941
3074
  const titleId = useId();
@@ -3042,7 +3175,7 @@ function ChatMessageImagePreview({ alt, expandLabel, closeLabel, sizeLabel, src
3042
3175
  }
3043
3176
  //#endregion
3044
3177
  //#region src/components/chat/ui/chat-message-list/utils/chat-local-resource.utils.ts
3045
- const SAFE_LINK_PROTOCOLS = new Set([
3178
+ const SAFE_BROWSER_LINK_PROTOCOLS = new Set([
3046
3179
  "http:",
3047
3180
  "https:",
3048
3181
  "mailto:",
@@ -3050,37 +3183,90 @@ const SAFE_LINK_PROTOCOLS = new Set([
3050
3183
  ]);
3051
3184
  const URI_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
3052
3185
  const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[a-z]:[\\/]/i;
3186
+ const WINDOWS_FILE_URI_PATH_PATTERN = /^\/[a-z]:[\\/]/i;
3187
+ const FILE_URI_PROTOCOL = "file:";
3053
3188
  function isSchemeLessResourceHref(href) {
3054
3189
  return !href.startsWith("//") && !URI_SCHEME_PATTERN.test(href);
3055
3190
  }
3056
- function resolveSafeChatResourceHref(href) {
3057
- if (!href) return null;
3058
- if (WINDOWS_ABSOLUTE_PATH_PATTERN.test(href) || isSchemeLessResourceHref(href)) return href;
3191
+ function readPositiveInteger(value) {
3192
+ const parsed = Number(value);
3193
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
3194
+ }
3195
+ function readFileUriHref(href) {
3196
+ let url;
3059
3197
  try {
3060
- const url = new URL(href);
3061
- return SAFE_LINK_PROTOCOLS.has(url.protocol) ? href : null;
3198
+ url = new URL(href);
3062
3199
  } catch {
3063
3200
  return null;
3064
3201
  }
3202
+ if (url.protocol !== FILE_URI_PROTOCOL || url.hostname && url.hostname.toLowerCase() !== "localhost") return null;
3203
+ let path;
3204
+ try {
3205
+ path = decodeURIComponent(url.pathname);
3206
+ } catch {
3207
+ return null;
3208
+ }
3209
+ if (WINDOWS_FILE_URI_PATH_PATTERN.test(path)) path = path.slice(1);
3210
+ return path ? {
3211
+ path,
3212
+ query: url.searchParams,
3213
+ fragment: url.hash.slice(1)
3214
+ } : null;
3065
3215
  }
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);
3216
+ function readSchemeLessFileHref(href) {
3217
+ const hashIndex = href.indexOf("#");
3218
+ const hrefWithoutFragment = hashIndex >= 0 ? href.slice(0, hashIndex) : href;
3219
+ const fragment = hashIndex >= 0 ? href.slice(hashIndex + 1) : "";
3220
+ const queryIndex = hrefWithoutFragment.indexOf("?");
3221
+ const encodedPath = queryIndex >= 0 ? hrefWithoutFragment.slice(0, queryIndex) : hrefWithoutFragment;
3222
+ const encodedQuery = queryIndex >= 0 ? hrefWithoutFragment.slice(queryIndex + 1) : "";
3072
3223
  let path;
3073
3224
  try {
3074
- path = decodeURIComponent(encodedPath ?? normalizedHref);
3225
+ path = decodeURIComponent(encodedPath);
3075
3226
  } catch {
3076
3227
  return null;
3077
3228
  }
3229
+ return path ? {
3230
+ path,
3231
+ query: new URLSearchParams(encodedQuery),
3232
+ fragment
3233
+ } : null;
3234
+ }
3235
+ function readLinePosition(fragment) {
3236
+ const match = /^L(\d+)(?:C(\d+))?(?:-L\d+(?:C\d+)?)?$/i.exec(fragment);
3237
+ const line = readPositiveInteger(match?.[1]);
3238
+ return {
3239
+ line,
3240
+ column: line ? readPositiveInteger(match?.[2]) : void 0
3241
+ };
3242
+ }
3243
+ function transformChatResourceHref(href) {
3244
+ if (WINDOWS_ABSOLUTE_PATH_PATTERN.test(href) || isSchemeLessResourceHref(href)) return href;
3245
+ try {
3246
+ const url = new URL(href);
3247
+ if (SAFE_BROWSER_LINK_PROTOCOLS.has(url.protocol)) return href;
3248
+ return url.protocol === FILE_URI_PROTOCOL && readFileUriHref(href) ? href : "";
3249
+ } catch {
3250
+ return "";
3251
+ }
3252
+ }
3253
+ function resolveSafeChatResourceHref(href) {
3254
+ if (!href) return null;
3255
+ return transformChatResourceHref(href) || null;
3256
+ }
3257
+ function isExternalChatResourceHref(href) {
3258
+ return /^https?:\/\//i.test(href);
3259
+ }
3260
+ function parseChatLocalFileAction(href) {
3261
+ const fileHref = href.toLowerCase().startsWith(FILE_URI_PROTOCOL) ? readFileUriHref(href) : readSchemeLessFileHref(href);
3262
+ const path = fileHref?.path;
3078
3263
  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");
3264
+ const viewer = fileHref.query.get("viewer");
3265
+ const fragmentPosition = readLinePosition(fileHref.fragment);
3080
3266
  const lineMatch = /^(.*?)(?::(\d+)(?::(\d+))?)$/.exec(path);
3081
3267
  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;
3268
+ const line = fragmentPosition.line ?? readPositiveInteger(lineMatch?.[2]);
3269
+ const column = fragmentPosition.column ?? (line ? readPositiveInteger(lineMatch?.[3]) : void 0);
3084
3270
  return {
3085
3271
  path: rawPath,
3086
3272
  label: rawPath.split(/[\\/]/).filter(Boolean).pop() ?? rawPath,
@@ -3180,153 +3366,170 @@ function splitTextNodeByInlineTokens(value, tokens) {
3180
3366
  }];
3181
3367
  }
3182
3368
  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;
3369
+ if (node.type === "code" || node.type === "inlineCode" || !node.children) return node;
3370
+ return {
3371
+ ...node,
3372
+ children: node.children.flatMap((child) => child.type === "text" && typeof child.value === "string" ? splitTextNodeByInlineTokens(child.value, tokens) : [transformInlineTokenTextNodes(child, tokens)])
3373
+ };
3194
3374
  }
3195
3375
  function createRemarkInlineTokenPlugin(inlineTokens) {
3196
3376
  const tokens = prepareInlineTokens(inlineTokens);
3197
- return () => (tree) => {
3198
- if (tokens.length === 0) return;
3199
- transformInlineTokenTextNodes(tree, tokens);
3200
- };
3377
+ return () => (tree) => tokens.length > 0 ? transformInlineTokenTextNodes(tree, tokens) : tree;
3201
3378
  }
3202
3379
  function readStringProp(props, key) {
3203
3380
  const value = props[key];
3204
3381
  return typeof value === "string" && value.length > 0 ? value : null;
3205
3382
  }
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", {
3383
+ const ChatMessageMarkdownRuntimeContext = createContext(null);
3384
+ function useChatMessageMarkdownRuntime() {
3385
+ const runtime = useContext(ChatMessageMarkdownRuntimeContext);
3386
+ if (!runtime) throw new Error("Chat message Markdown renderer requires its runtime context");
3387
+ return runtime;
3388
+ }
3389
+ const CHAT_MESSAGE_MARKDOWN_COMPONENTS = {
3390
+ p: function ChatMarkdownParagraph({ node, children }) {
3391
+ const { inline } = useChatMessageMarkdownRuntime();
3392
+ return inline ? /* @__PURE__ */ jsx(Fragment$1, { children }) : /* @__PURE__ */ jsx("p", {
3211
3393
  "data-chat-image-row": isSingleLineImageParagraph(node) ? "three-column" : void 0,
3212
3394
  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, {
3395
+ });
3396
+ },
3397
+ span: function ChatMarkdownSpan({ node: _node, children, ...rest }) {
3398
+ const { isUser, onInlineTokenClick } = useChatMessageMarkdownRuntime();
3399
+ const restProps = rest;
3400
+ const kind = readStringProp(restProps, INLINE_TOKEN_KIND_ATTR);
3401
+ const key = readStringProp(restProps, INLINE_TOKEN_KEY_ATTR);
3402
+ const label = readStringProp(restProps, INLINE_TOKEN_LABEL_ATTR);
3403
+ const rawText = readStringProp(restProps, INLINE_TOKEN_RAW_TEXT_ATTR);
3404
+ if (kind && key && label && rawText) return /* @__PURE__ */ jsx(ChatInlineTokenBadge, {
3405
+ kind,
3406
+ label,
3407
+ isUser,
3408
+ onClick: onInlineTokenClick ? () => onInlineTokenClick({
3221
3409
  kind,
3410
+ key,
3222
3411
  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;
3412
+ rawText
3413
+ }) : void 0
3414
+ });
3415
+ return /* @__PURE__ */ jsx("span", {
3416
+ ...rest,
3417
+ children
3418
+ });
3419
+ },
3420
+ a: function ChatMarkdownLink({ node: _node, href, children, ...rest }) {
3421
+ const { onFileOpen } = useChatMessageMarkdownRuntime();
3422
+ const safeHref = resolveSafeChatResourceHref(href);
3423
+ const external = safeHref ? isExternalChatResourceHref(safeHref) : false;
3424
+ const localFileAction = external ? null : safeHref ? parseChatLocalFileAction(safeHref) : null;
3425
+ const handleClick = (event) => {
3426
+ if (!safeHref) {
3247
3427
  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", {
3428
+ return;
3429
+ }
3430
+ if (!onFileOpen || !localFileAction) return;
3431
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3432
+ event.preventDefault();
3433
+ onFileOpen(localFileAction);
3434
+ };
3435
+ return /* @__PURE__ */ jsx("a", {
3436
+ ...rest,
3437
+ className: cn(rest.className, !safeHref && "chat-link-invalid"),
3438
+ href: safeHref ?? "#",
3439
+ "aria-disabled": !safeHref || void 0,
3440
+ onClick: handleClick,
3441
+ target: external ? "_blank" : void 0,
3442
+ rel: external ? "noreferrer noopener" : void 0,
3443
+ children
3444
+ });
3445
+ },
3446
+ table: function ChatMarkdownTable({ node: _node, children, ...rest }) {
3447
+ return /* @__PURE__ */ jsx("div", {
3262
3448
  className: "chat-table-wrap",
3263
3449
  children: /* @__PURE__ */ jsx("table", {
3264
3450
  ...rest,
3265
3451
  children
3266
3452
  })
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
- });
3453
+ });
3454
+ },
3455
+ input: function ChatMarkdownInput({ node: _node, type, checked, ...rest }) {
3456
+ if (type !== "checkbox") return /* @__PURE__ */ jsx("input", {
3457
+ ...rest,
3458
+ type
3459
+ });
3460
+ return /* @__PURE__ */ jsx("input", {
3461
+ ...rest,
3462
+ type: "checkbox",
3463
+ checked,
3464
+ readOnly: true,
3465
+ disabled: true,
3466
+ className: "chat-task-checkbox"
3467
+ });
3468
+ },
3469
+ img: function ChatMarkdownImage({ node: _node, src, alt }) {
3470
+ const { resolveFileContentUrl, texts } = useChatMessageMarkdownRuntime();
3471
+ const safeSrc = resolveSafeChatResourceHref(src);
3472
+ if (!safeSrc) return null;
3473
+ const localFileAction = parseChatLocalFileAction(safeSrc);
3474
+ const resolvedSrc = localFileAction && resolveFileContentUrl ? resolveFileContentUrl(localFileAction) : safeSrc;
3475
+ if (!resolvedSrc) return null;
3476
+ return /* @__PURE__ */ jsx(ChatMessageImagePreview, {
3477
+ alt: alt || "",
3478
+ closeLabel: texts.attachmentCloseLabel ?? "Close preview",
3479
+ expandLabel: texts.attachmentExpandLabel ?? "Expand image",
3480
+ sizeLabel: null,
3481
+ src: resolvedSrc
3482
+ });
3483
+ },
3484
+ code: function ChatMarkdownCode({ node: _node, className, children, ...rest }) {
3485
+ const { isStreaming, renderInlineDisplay, texts } = useChatMessageMarkdownRuntime();
3486
+ const plainText = String(children ?? "");
3487
+ if (!className && !plainText.includes("\n")) return /* @__PURE__ */ jsx("code", {
3488
+ ...rest,
3489
+ className: cn("chat-inline-code", className),
3490
+ children
3491
+ });
3492
+ const inlineDisplay = isChatInlineDisplayLanguage(className) ? parseChatInlineDisplayDirective(plainText) : null;
3493
+ if (inlineDisplay) return /* @__PURE__ */ jsx(ChatInlineDisplay, {
3494
+ display: inlineDisplay,
3495
+ renderInlineDisplay
3496
+ });
3497
+ if (className?.split(" ").includes("language-mermaid")) return /* @__PURE__ */ jsx(ChatMermaidDiagram, {
3498
+ isStreaming,
3499
+ source: plainText,
3500
+ texts
3501
+ });
3502
+ return /* @__PURE__ */ jsx(ChatCodeBlock, {
3503
+ className,
3504
+ texts,
3505
+ children
3506
+ });
3507
+ }
3508
+ };
3509
+ function ChatMessageMarkdown({ text, role, texts, inline = false, isStreaming = false, inlineTokens, onFileOpen, onInlineTokenClick, resolveFileContentUrl, renderInlineDisplay }) {
3510
+ const isUser = role === "user";
3511
+ const remarkPlugins = inlineTokens?.length ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm];
3512
+ const WrapperTag = inline ? "span" : "div";
3513
+ return /* @__PURE__ */ jsx(ChatMessageMarkdownRuntimeContext.Provider, {
3514
+ value: {
3515
+ inline,
3516
+ isStreaming,
3517
+ isUser,
3518
+ onFileOpen,
3519
+ onInlineTokenClick,
3520
+ renderInlineDisplay,
3521
+ resolveFileContentUrl,
3522
+ texts
3295
3523
  },
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)
3524
+ children: /* @__PURE__ */ jsx(WrapperTag, {
3525
+ className: cn("chat-markdown", isUser ? "chat-markdown-user" : "chat-markdown-assistant"),
3526
+ children: /* @__PURE__ */ jsx(ReactMarkdown, {
3527
+ skipHtml: true,
3528
+ remarkPlugins,
3529
+ components: CHAT_MESSAGE_MARKDOWN_COMPONENTS,
3530
+ urlTransform: transformChatResourceHref,
3531
+ children: trimMarkdown(text)
3532
+ })
3330
3533
  })
3331
3534
  });
3332
3535
  }
@@ -4210,8 +4413,10 @@ const FILE_TEXT_CLASS_NAME = "font-mono text-[11px] leading-5";
4210
4413
  const FILE_ROW_CLASS_NAME = `flex h-5 w-full ${FILE_TEXT_CLASS_NAME}`;
4211
4414
  const FILE_LINE_NUMBER_CELL_CLASS_NAME = "flex h-5 items-center justify-center px-2.5 tabular-nums select-none";
4212
4415
  function readVisibleLineNumber(line) {
4213
- const value = line.newLineNumber ?? line.oldLineNumber;
4214
- return typeof value === "number" ? String(value) : "";
4416
+ return String(line.newLineNumber ?? line.oldLineNumber ?? "");
4417
+ }
4418
+ function isTargetLine(line, targetLine) {
4419
+ return typeof targetLine === "number" && (line.newLineNumber ?? line.oldLineNumber) === targetLine;
4215
4420
  }
4216
4421
  function readLineKey(prefix, line, index) {
4217
4422
  return `${prefix}-${index}-${line.oldLineNumber ?? "x"}-${line.newLineNumber ?? "x"}`;
@@ -4256,25 +4461,35 @@ function getCodeRowTone(line) {
4256
4461
  if (line.kind === "add") return "bg-emerald-50 text-emerald-950";
4257
4462
  return "bg-card text-foreground";
4258
4463
  }
4259
- function FileOperationLineNumberCell({ layout, line, lineNumberColumnWidth }) {
4464
+ function FileOperationLineNumberCell({ layout, line, lineNumberColumnWidth, target, targetRef }) {
4260
4465
  return /* @__PURE__ */ jsx("span", {
4466
+ ref: targetRef,
4261
4467
  "data-file-line-number-cell": "true",
4262
4468
  style: layout === "compact" ? {
4263
4469
  width: lineNumberColumnWidth,
4264
4470
  minWidth: lineNumberColumnWidth
4265
4471
  } : void 0,
4266
- className: cn(FILE_LINE_NUMBER_CELL_CLASS_NAME, layout === "compact" ? "sticky left-0 z-[1]" : "w-full shrink-0", getLineNumberTone(line)),
4472
+ 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
4473
  children: readVisibleLineNumber(line)
4268
4474
  });
4269
4475
  }
4270
- function FileOperationCodeCell({ language, line }) {
4476
+ function FileOperationCodeCell({ language, line, target, targetColumn, targetRef }) {
4271
4477
  const highlightedCode = useMemo(() => chatCodeSyntaxHighlighter.highlight(line.text || " ", language), [language, line.text]);
4272
- return /* @__PURE__ */ jsx("span", {
4478
+ const visibleTargetColumn = typeof targetColumn === "number" && Number.isSafeInteger(targetColumn) && targetColumn > 0 ? Math.min(targetColumn, line.text.length + 1) : null;
4479
+ return /* @__PURE__ */ jsxs("span", {
4480
+ ref: visibleTargetColumn ? void 0 : targetRef,
4273
4481
  "data-file-code-row": "true",
4274
4482
  "data-file-code-language": highlightedCode.language,
4275
4483
  "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 }
4484
+ "data-file-target-column": visibleTargetColumn ?? void 0,
4485
+ 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),
4486
+ children: [/* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: highlightedCode.html } }), visibleTargetColumn ? /* @__PURE__ */ jsx("span", {
4487
+ ref: targetRef,
4488
+ "aria-hidden": "true",
4489
+ "data-file-target-caret": "true",
4490
+ className: "pointer-events-none absolute inset-y-0 w-px bg-primary",
4491
+ style: { left: `calc(0.625rem + ${visibleTargetColumn - 1}ch)` }
4492
+ }) : null]
4278
4493
  });
4279
4494
  }
4280
4495
  function FileOperationLineRow({ language, line, showLineNumbers, lineNumberColumnWidth }) {
@@ -4291,11 +4506,22 @@ function FileOperationLineRow({ language, line, showLineNumbers, lineNumberColum
4291
4506
  })]
4292
4507
  });
4293
4508
  }
4294
- function FileOperationWorkspaceSurface({ block }) {
4509
+ function FileOperationWorkspaceSurface({ block, targetColumn, targetLine }) {
4295
4510
  const showLineNumbers = readHasBlockLineNumbers(block);
4296
4511
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
4297
4512
  const codeColumnWidth = readCodeColumnWidth(block);
4298
4513
  const language = readBlockLanguage(block);
4514
+ const targetRef = useRef(null);
4515
+ useLayoutEffect(() => {
4516
+ targetRef.current?.scrollIntoView({
4517
+ block: "center",
4518
+ inline: targetColumn ? "center" : "nearest"
4519
+ });
4520
+ }, [
4521
+ block,
4522
+ targetColumn,
4523
+ targetLine
4524
+ ]);
4299
4525
  return /* @__PURE__ */ jsxs("div", {
4300
4526
  "data-file-code-surface": "true",
4301
4527
  "data-file-code-surface-layout": "workspace",
@@ -4310,7 +4536,9 @@ function FileOperationWorkspaceSurface({ block }) {
4310
4536
  children: [block.lines.map((line, index) => /* @__PURE__ */ jsx(FileOperationLineNumberCell, {
4311
4537
  layout: "workspace",
4312
4538
  line,
4313
- lineNumberColumnWidth
4539
+ lineNumberColumnWidth,
4540
+ target: isTargetLine(line, targetLine),
4541
+ targetRef: !targetColumn && isTargetLine(line, targetLine) ? targetRef : void 0
4314
4542
  }, readLineKey("gutter", line, index))), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 border-r border-border bg-muted" })]
4315
4543
  }) : null, /* @__PURE__ */ jsxs("div", {
4316
4544
  "data-file-code-canvas": "true",
@@ -4319,20 +4547,32 @@ function FileOperationWorkspaceSurface({ block }) {
4319
4547
  "data-file-code-stack": "true",
4320
4548
  className: "min-w-full",
4321
4549
  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)))
4550
+ children: block.lines.map((line, index) => {
4551
+ const target = isTargetLine(line, targetLine);
4552
+ return /* @__PURE__ */ jsx("div", {
4553
+ "data-file-code-canvas-row": "true",
4554
+ "data-file-target-line": target ? "true" : void 0,
4555
+ "aria-current": target ? "location" : void 0,
4556
+ className: FILE_ROW_CLASS_NAME,
4557
+ children: /* @__PURE__ */ jsx(FileOperationCodeCell, {
4558
+ language,
4559
+ line,
4560
+ target,
4561
+ targetColumn: target ? targetColumn : null,
4562
+ targetRef: target && (targetColumn || !showLineNumbers) ? targetRef : void 0
4563
+ })
4564
+ }, readLineKey("code", line, index));
4565
+ })
4330
4566
  }), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-card" })]
4331
4567
  })]
4332
4568
  });
4333
4569
  }
4334
- function FileOperationCodeSurface({ block, layout = "compact" }) {
4335
- if (layout === "workspace") return /* @__PURE__ */ jsx(FileOperationWorkspaceSurface, { block });
4570
+ function FileOperationCodeSurface({ block, layout = "compact", targetColumn, targetLine }) {
4571
+ if (layout === "workspace") return /* @__PURE__ */ jsx(FileOperationWorkspaceSurface, {
4572
+ block,
4573
+ targetColumn,
4574
+ targetLine
4575
+ });
4336
4576
  const showLineNumbers = readHasBlockLineNumbers(block);
4337
4577
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
4338
4578
  const codeColumnWidth = readCodeColumnWidth(block);
@@ -5243,6 +5483,9 @@ const MAX_SUMMARY_SEGMENTS = 3;
5243
5483
  function isToolCardPart(part) {
5244
5484
  return part.type === "tool-card";
5245
5485
  }
5486
+ function isGroupableToolPart(part) {
5487
+ return part?.type === "reasoning" || part?.type === "tool-card" && !part.card.panelApp;
5488
+ }
5246
5489
  function resolveToolActivityFamily(toolName) {
5247
5490
  const lowered = toolName.toLowerCase();
5248
5491
  if (lowered === "list_dir") return "directory";
@@ -5321,17 +5564,29 @@ function formatToolActivityGroupLabel(params) {
5321
5564
  if (hiddenCount > 0) parts.push(`+${hiddenCount}`);
5322
5565
  return parts.join(" · ");
5323
5566
  }
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
- */
5567
+ function collectToolGroupParts(parts, startIndex) {
5568
+ const candidateParts = [];
5569
+ let index = startIndex;
5570
+ let lastToolOffset = -1;
5571
+ while (true) {
5572
+ const candidate = parts[index];
5573
+ if (!isGroupableToolPart(candidate)) break;
5574
+ candidateParts.push(candidate);
5575
+ if (candidate.type === "tool-card") lastToolOffset = candidateParts.length - 1;
5576
+ index += 1;
5577
+ }
5578
+ return {
5579
+ candidateParts,
5580
+ lastToolOffset
5581
+ };
5582
+ }
5583
+ /** Groups tool cards across reasoning; content and interactive panel apps remain stable boundaries. */
5329
5584
  function groupConsecutiveToolParts(parts, labels) {
5330
5585
  const blocks = [];
5331
5586
  let index = 0;
5332
5587
  while (index < parts.length) {
5333
5588
  const part = parts[index];
5334
- if (part.type !== "tool-card") {
5589
+ if (part.type !== "tool-card" || part.card.panelApp) {
5335
5590
  blocks.push({
5336
5591
  kind: "part",
5337
5592
  key: `part-${index}`,
@@ -5342,14 +5597,7 @@ function groupConsecutiveToolParts(parts, labels) {
5342
5597
  continue;
5343
5598
  }
5344
5599
  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
- }
5600
+ const { candidateParts, lastToolOffset } = collectToolGroupParts(parts, startIndex);
5353
5601
  const toolParts = candidateParts.filter(isToolCardPart);
5354
5602
  if (toolParts.length === 1) {
5355
5603
  blocks.push({
@@ -5464,6 +5712,7 @@ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onAtta
5464
5712
  text,
5465
5713
  role,
5466
5714
  texts,
5715
+ isStreaming: isInProgress && isLastPart,
5467
5716
  inlineTokens,
5468
5717
  onFileOpen,
5469
5718
  onInlineTokenClick,
@@ -5666,14 +5915,25 @@ function ChatMessageActionCopy({ message, texts }) {
5666
5915
  }).filter((text) => !!text && text.trim().length > 0).join("\n\n");
5667
5916
  }, [message.parts]);
5668
5917
  const { copied, copy } = useCopyFeedback({ text: messageText });
5918
+ const label = copied ? texts.copiedMessageLabel : texts.copyMessageLabel;
5919
+ const { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } = ChatUiPrimitives;
5669
5920
  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" })
5921
+ return /* @__PURE__ */ jsx(TooltipProvider, {
5922
+ delayDuration: 250,
5923
+ children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
5924
+ asChild: true,
5925
+ children: /* @__PURE__ */ jsx("button", {
5926
+ type: "button",
5927
+ onClick: () => void copy(),
5928
+ 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",
5929
+ "aria-label": label,
5930
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
5931
+ })
5932
+ }), /* @__PURE__ */ jsx(TooltipContent, {
5933
+ side: "top",
5934
+ className: "text-xs",
5935
+ children: label
5936
+ })] })
5677
5937
  });
5678
5938
  }
5679
5939
  //#endregion
@@ -5824,10 +6084,10 @@ function ChatMessageList({ className, isSending, layout = "card", messages, onAt
5824
6084
  " · ",
5825
6085
  message.timestampLabel
5826
6086
  ]
5827
- }), !isUser ? /* @__PURE__ */ jsx(ChatMessageActionCopy, {
6087
+ }), /* @__PURE__ */ jsx(ChatMessageActionCopy, {
5828
6088
  message,
5829
6089
  texts
5830
- }) : null] })
6090
+ })] })
5831
6091
  })]
5832
6092
  }),
5833
6093
  isUser ? /* @__PURE__ */ jsx(ChatMessageAvatar, { role: message.role }) : null