@nextclaw/agent-chat-ui 0.5.4 → 0.6.1

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
@@ -399,6 +399,10 @@ const INPUT_SURFACE_PANEL_DESKTOP_SHRINK_RATIO = .82;
399
399
  const INPUT_SURFACE_PANEL_DESKTOP_MIN_WIDTH = 560;
400
400
  const INPUT_SURFACE_PANEL_MAX_HEIGHT = createChatPopoverAvailableHeightLimit("24rem");
401
401
  const INPUT_SURFACE_PANEL_MIN_HEIGHT = createChatPopoverAvailableHeightLimit("240px");
402
+ function itemMatchesFilter(item, filter) {
403
+ if (!filter.sectionKeys || filter.sectionKeys.length === 0) return true;
404
+ return Boolean(item.sectionKey && filter.sectionKeys.includes(item.sectionKey));
405
+ }
402
406
  const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref) {
403
407
  const { Popover, PopoverAnchor, PopoverContent } = ChatUiPrimitives;
404
408
  const { elementRef: anchorRef, width: panelWidth } = useElementWidth();
@@ -407,12 +411,30 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
407
411
  index: 0,
408
412
  itemsSignature: ""
409
413
  });
410
- const { isOpen, isLoading, items, texts, onSelectItem, onOpenChange, onDetailsPointerDown } = props;
411
- const itemsSignature = useMemo(() => items.map((item) => item.key).join(""), [items]);
412
- const hasItemSections = useMemo(() => items.some((item) => Boolean(item.sectionLabel?.trim())), [items]);
414
+ const { isOpen, isLoading, filterOptions, items, texts, onSelectItem, onOpenChange, onDetailsPointerDown } = props;
415
+ const [activeFilterKey, setActiveFilterKey] = useState(filterOptions?.[0]?.key ?? null);
416
+ const resolvedActiveFilterKey = useMemo(() => {
417
+ if (!filterOptions?.length) return null;
418
+ return filterOptions.some((filter) => filter.key === activeFilterKey) ? activeFilterKey : filterOptions[0].key;
419
+ }, [activeFilterKey, filterOptions]);
420
+ const activeFilter = useMemo(() => filterOptions?.find((filter) => filter.key === resolvedActiveFilterKey) ?? null, [filterOptions, resolvedActiveFilterKey]);
421
+ const filterViews = useMemo(() => filterOptions?.map((filter) => ({
422
+ ...filter,
423
+ count: items.filter((item) => itemMatchesFilter(item, filter)).length
424
+ })) ?? [], [filterOptions, items]);
425
+ const visibleItems = useMemo(() => activeFilter ? items.filter((item) => itemMatchesFilter(item, activeFilter)) : items, [activeFilter, items]);
426
+ const itemsSignature = useMemo(() => visibleItems.map((item) => item.key).join(""), [visibleItems]);
427
+ const hasItemSections = useMemo(() => visibleItems.some((item) => Boolean(item.sectionLabel?.trim())), [visibleItems]);
413
428
  const activeIndex = activeState.itemsSignature === itemsSignature ? activeState.index : 0;
414
- const activeIndexInRange = items.length === 0 ? 0 : Math.min(activeIndex, items.length - 1);
415
- const activeItem = items[activeIndexInRange] ?? null;
429
+ const activeIndexInRange = visibleItems.length === 0 ? 0 : Math.min(activeIndex, visibleItems.length - 1);
430
+ const activeItem = visibleItems[activeIndexInRange] ?? null;
431
+ const handleFilterSelect = useCallback((filterKey) => {
432
+ setActiveFilterKey(filterKey);
433
+ setActiveState({
434
+ index: 0,
435
+ itemsSignature: ""
436
+ });
437
+ }, []);
416
438
  const setActiveIndexForCurrentItems = useCallback((nextIndex) => {
417
439
  setActiveState((currentState) => {
418
440
  const currentIndex = currentState.itemsSignature === itemsSignature ? currentState.index : 0;
@@ -433,35 +455,35 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
433
455
  onOpenChange(false);
434
456
  return true;
435
457
  }
436
- if (items.length === 0) return false;
458
+ if (visibleItems.length === 0) return false;
437
459
  if (event.key === "ArrowDown") {
438
460
  event.preventDefault();
439
- setActiveIndexForCurrentItems((index) => (index + 1) % items.length);
461
+ setActiveIndexForCurrentItems((index) => (index + 1) % visibleItems.length);
440
462
  return true;
441
463
  }
442
464
  if (event.key === "ArrowUp") {
443
465
  event.preventDefault();
444
- setActiveIndexForCurrentItems((index) => (index - 1 + items.length) % items.length);
466
+ setActiveIndexForCurrentItems((index) => (index - 1 + visibleItems.length) % visibleItems.length);
445
467
  return true;
446
468
  }
447
469
  if (event.key === "Enter" && !event.shiftKey || event.key === "Tab") {
448
470
  event.preventDefault();
449
- onSelectItem(items[activeIndexInRange]);
471
+ onSelectItem(visibleItems[activeIndexInRange]);
450
472
  return true;
451
473
  }
452
474
  return false;
453
475
  } }), [
454
476
  activeIndexInRange,
455
477
  isOpen,
456
- items,
457
478
  onOpenChange,
458
479
  onSelectItem,
459
- setActiveIndexForCurrentItems
480
+ setActiveIndexForCurrentItems,
481
+ visibleItems
460
482
  ]);
461
483
  useActiveItemScroll({
462
484
  containerRef: listRef,
463
485
  activeIndex: activeIndexInRange,
464
- itemCount: items.length,
486
+ itemCount: visibleItems.length,
465
487
  isEnabled: isOpen && !isLoading,
466
488
  getItemSelector: (index) => `[data-input-surface-index="${index}"]`
467
489
  });
@@ -487,59 +509,78 @@ const ChatInputSurfaceMenu = forwardRef(function ChatInputSurfaceMenu(props, ref
487
509
  children: /* @__PURE__ */ jsxs("div", {
488
510
  className: "grid min-h-0 flex-1 grid-cols-[minmax(220px,300px)_minmax(0,1fr)]",
489
511
  style: { minHeight: INPUT_SURFACE_PANEL_MIN_HEIGHT },
490
- children: [/* @__PURE__ */ jsx("div", {
491
- ref: listRef,
492
- role: "listbox",
493
- "aria-label": texts.sectionLabel,
494
- className: "custom-scrollbar min-h-0 overflow-y-auto overscroll-contain border-r border-gray-200 p-2.5",
495
- children: isLoading ? /* @__PURE__ */ jsx("div", {
496
- className: "p-2 text-xs text-gray-500",
497
- children: texts.loadingLabel
498
- }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [!hasItemSections ? /* @__PURE__ */ jsx("div", {
499
- className: "mb-2 px-2 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
500
- children: texts.sectionLabel
501
- }) : null, items.length === 0 ? /* @__PURE__ */ jsx("div", {
502
- className: "px-2 text-xs text-gray-400",
503
- children: texts.emptyLabel
504
- }) : /* @__PURE__ */ jsx("div", {
505
- className: "space-y-1",
506
- children: items.map((item, index) => {
507
- const { key, sectionKey, sectionLabel, title, subtitle } = item;
508
- const isActive = index === activeIndexInRange;
509
- const previousItem = items[index - 1];
510
- return /* @__PURE__ */ jsxs("div", {
511
- className: "space-y-1",
512
- children: [hasItemSections && Boolean(sectionLabel?.trim()) && previousItem?.sectionKey !== sectionKey ? /* @__PURE__ */ jsx("div", {
513
- className: "px-2 pb-0.5 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
514
- children: sectionLabel
515
- }) : null, /* @__PURE__ */ jsxs("button", {
516
- type: "button",
517
- role: "option",
518
- "aria-selected": isActive,
519
- "data-input-surface-index": index,
520
- onPointerMove: (event) => {
521
- if (event.pointerType !== "touch") setActiveIndexForCurrentItems(index);
522
- },
523
- onPointerDown: (event) => {
524
- if (event.button > 0) return;
525
- event.preventDefault();
526
- onSelectItem(item);
527
- },
528
- onClick: (event) => {
529
- if (event.detail === 0) onSelectItem(item);
530
- },
531
- className: `flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left transition ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-50"}`,
532
- children: [/* @__PURE__ */ jsx("span", {
533
- className: "truncate text-xs font-semibold",
534
- children: title
535
- }), /* @__PURE__ */ jsx("span", {
536
- className: "truncate text-xs text-gray-500",
537
- children: subtitle
538
- })]
512
+ children: [/* @__PURE__ */ jsxs("div", {
513
+ className: "flex min-h-0 flex-col border-r border-gray-200",
514
+ children: [!isLoading && filterViews.length > 0 ? /* @__PURE__ */ jsx("div", {
515
+ className: "flex shrink-0 gap-1 overflow-x-auto px-2.5 pb-2 pt-2.5",
516
+ children: filterViews.map(({ count, key, label }) => {
517
+ const isActive = key === resolvedActiveFilterKey;
518
+ return /* @__PURE__ */ jsxs("button", {
519
+ type: "button",
520
+ "aria-pressed": isActive,
521
+ onPointerDown: (event) => event.preventDefault(),
522
+ onClick: () => handleFilterSelect(key),
523
+ className: `inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-1 text-[11px] font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${isActive ? "border-primary/30 bg-primary/10 text-primary" : "border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50"}`,
524
+ children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
525
+ className: isActive ? "text-primary/70" : "text-gray-400",
526
+ children: count
539
527
  })]
540
528
  }, key);
541
529
  })
542
- })] })
530
+ }) : null, /* @__PURE__ */ jsx("div", {
531
+ ref: listRef,
532
+ role: "listbox",
533
+ "aria-label": texts.sectionLabel,
534
+ className: `custom-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain ${!isLoading && filterViews.length > 0 ? "px-2.5 pb-2.5" : "p-2.5"}`,
535
+ children: isLoading ? /* @__PURE__ */ jsx("div", {
536
+ className: "p-2 text-xs text-gray-500",
537
+ children: texts.loadingLabel
538
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [!hasItemSections ? /* @__PURE__ */ jsx("div", {
539
+ className: "mb-2 px-2 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
540
+ children: texts.sectionLabel
541
+ }) : null, visibleItems.length === 0 ? /* @__PURE__ */ jsx("div", {
542
+ className: "px-2 text-xs text-gray-400",
543
+ children: texts.emptyLabel
544
+ }) : /* @__PURE__ */ jsx("div", {
545
+ className: "space-y-1",
546
+ children: visibleItems.map((item, index) => {
547
+ const { key, sectionKey, sectionLabel, title, subtitle } = item;
548
+ const isActive = index === activeIndexInRange;
549
+ const previousItem = visibleItems[index - 1];
550
+ return /* @__PURE__ */ jsxs("div", {
551
+ className: "space-y-1",
552
+ children: [hasItemSections && Boolean(sectionLabel?.trim()) && previousItem?.sectionKey !== sectionKey ? /* @__PURE__ */ jsx("div", {
553
+ className: "px-2 pb-0.5 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500",
554
+ children: sectionLabel
555
+ }) : null, /* @__PURE__ */ jsxs("button", {
556
+ type: "button",
557
+ role: "option",
558
+ "aria-selected": isActive,
559
+ "data-input-surface-index": index,
560
+ onPointerMove: (event) => {
561
+ if (event.pointerType !== "touch") setActiveIndexForCurrentItems(index);
562
+ },
563
+ onPointerDown: (event) => {
564
+ if (event.button > 0) return;
565
+ event.preventDefault();
566
+ onSelectItem(item);
567
+ },
568
+ onClick: (event) => {
569
+ if (event.detail === 0) onSelectItem(item);
570
+ },
571
+ className: `flex w-full items-start gap-2 rounded-lg px-2 py-1.5 text-left transition ${isActive ? "bg-gray-100 text-gray-900" : "text-gray-700 hover:bg-gray-50"}`,
572
+ children: [/* @__PURE__ */ jsx("span", {
573
+ className: "truncate text-xs font-semibold",
574
+ children: title
575
+ }), /* @__PURE__ */ jsx("span", {
576
+ className: "truncate text-xs text-gray-500",
577
+ children: subtitle
578
+ })]
579
+ })]
580
+ }, key);
581
+ })
582
+ })] })
583
+ })]
543
584
  }), /* @__PURE__ */ jsx("div", {
544
585
  className: "custom-scrollbar min-h-0 min-w-0 overflow-y-auto overscroll-contain p-2.5",
545
586
  onPointerDown: onDetailsPointerDown,
@@ -634,6 +675,7 @@ function ChatInputSurfaceHost(props) {
634
675
  ref: menuRef,
635
676
  isOpen,
636
677
  isLoading: inputSurface.isLoading,
678
+ filterOptions: inputSurface.filterOptions,
637
679
  items: inputSurface.items,
638
680
  texts: inputSurface.texts,
639
681
  onSelectItem: (item) => {
@@ -775,7 +817,14 @@ function ChatInputBarActions({ sendError, sendErrorDetailsLabel, isSending, canS
775
817
  })] })]
776
818
  }) : null, /* @__PURE__ */ jsxs("div", {
777
819
  className: "flex items-center gap-2",
778
- children: [contextWindow ? /* @__PURE__ */ jsx(ContextWindowIndicator, { contextWindow }) : null, isSending ? canStopGeneration ? /* @__PURE__ */ jsx(ChatButton, {
820
+ children: [contextWindow ? /* @__PURE__ */ jsx(ContextWindowIndicator, { contextWindow }) : null, !isSending || !sendDisabled ? /* @__PURE__ */ jsx(ChatButton, {
821
+ size: "icon",
822
+ className: "h-8 w-8 rounded-full",
823
+ "aria-label": sendButtonLabel,
824
+ onClick: () => void onSend(),
825
+ disabled: sendDisabled,
826
+ children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-5 w-5" })
827
+ }) : canStopGeneration ? /* @__PURE__ */ jsx(ChatButton, {
779
828
  size: "icon",
780
829
  variant: "outline",
781
830
  className: "h-8 w-8 rounded-full",
@@ -799,14 +848,7 @@ function ChatInputBarActions({ sendError, sendErrorDetailsLabel, isSending, canS
799
848
  className: "text-xs",
800
849
  children: stopHint
801
850
  })
802
- })] }) }) : /* @__PURE__ */ jsx(ChatButton, {
803
- size: "icon",
804
- className: "h-8 w-8 rounded-full",
805
- "aria-label": sendButtonLabel,
806
- onClick: () => void onSend(),
807
- disabled: sendDisabled,
808
- children: /* @__PURE__ */ jsx(ArrowUp, { className: "h-5 w-5" })
809
- })]
851
+ })] }) })]
810
852
  })]
811
853
  });
812
854
  }
@@ -1281,11 +1323,11 @@ function buildTokenClassName(tokenKind) {
1281
1323
  "gap-1.5",
1282
1324
  "rounded-lg",
1283
1325
  "border",
1284
- "border-slate-200/80",
1285
- "bg-slate-50",
1326
+ "border-border",
1327
+ "bg-muted",
1286
1328
  "px-2",
1287
1329
  "align-baseline",
1288
- "text-slate-700",
1330
+ "text-foreground",
1289
1331
  "transition-[border-color,background-color,box-shadow,color]",
1290
1332
  "duration-150"
1291
1333
  ].join(" ");
@@ -1310,7 +1352,7 @@ function buildTokenClassName(tokenKind) {
1310
1352
  }
1311
1353
  function ChatComposerTokenChip({ label, tokenKind }) {
1312
1354
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
1313
- className: tokenKind === "file" ? "inline-flex h-4.5 w-4.5 shrink-0 items-center justify-center rounded-md bg-white text-slate-500 ring-1 ring-black/5" : "inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center text-primary/70",
1355
+ 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",
1314
1356
  children: tokenKind === "file" ? /* @__PURE__ */ jsx(ImageIcon, {
1315
1357
  "aria-hidden": "true",
1316
1358
  className: "h-3 w-3"
@@ -1322,7 +1364,7 @@ function ChatComposerTokenChip({ label, tokenKind }) {
1322
1364
  className: "h-3 w-3"
1323
1365
  })
1324
1366
  }), /* @__PURE__ */ jsx("span", {
1325
- className: tokenKind === "file" ? "min-w-0 flex-1 truncate text-[12px] font-medium text-slate-700" : "truncate",
1367
+ className: tokenKind === "file" ? "min-w-0 flex-1 truncate text-[12px] font-medium text-foreground" : "truncate",
1326
1368
  children: label
1327
1369
  })] });
1328
1370
  }
@@ -1786,7 +1828,6 @@ function deleteChatComposerContent(params) {
1786
1828
  //#region src/components/chat/ui/chat-input-bar/lexical/chat-composer-lexical-controller.ts
1787
1829
  function resolveLexicalComposerKeyboardAction(params) {
1788
1830
  const { canStopGeneration, isComposing, isSending, key, shiftKey } = params;
1789
- if (key === "Enter" && !shiftKey && isSending) return { type: "consume" };
1790
1831
  if (key === "Escape") {
1791
1832
  if (isSending && canStopGeneration) return { type: "stop-generation" };
1792
1833
  return { type: "noop" };
@@ -1853,7 +1894,6 @@ function handleLexicalComposerKeyboardCommand(params) {
1853
1894
  });
1854
1895
  if (action.type !== "noop") nativeEvent.preventDefault();
1855
1896
  switch (action.type) {
1856
- case "consume": return true;
1857
1897
  case "stop-generation":
1858
1898
  actions.onStop();
1859
1899
  return true;
@@ -2269,10 +2309,11 @@ function InputBarHint({ hint }) {
2269
2309
  })
2270
2310
  });
2271
2311
  }
2272
- const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSurface, slashMenu, surface, toolbar: toolbarProps }, ref) {
2312
+ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSurface, slashMenu, surface, toolbar: toolbarProps, topSlot }, ref) {
2273
2313
  const composerRef = useRef(null);
2274
2314
  const resolvedInputSurface = inputSurface ?? (slashMenu ? {
2275
2315
  isLoading: slashMenu.isLoading,
2316
+ filterOptions: slashMenu.filterOptions,
2276
2317
  items: slashMenu.items,
2277
2318
  onSelectItem: slashMenu.onSelectItem,
2278
2319
  texts: {
@@ -2309,6 +2350,10 @@ const ChatInputBar = forwardRef(function ChatInputBar({ composer, hint, inputSur
2309
2350
  children: /* @__PURE__ */ jsxs("div", {
2310
2351
  className: "overflow-hidden rounded-2xl border border-border bg-card shadow-card",
2311
2352
  children: [
2353
+ topSlot ? /* @__PURE__ */ jsx("div", {
2354
+ className: "px-3 pb-0 pt-2 sm:px-4 sm:pt-2.5",
2355
+ children: topSlot
2356
+ }) : null,
2312
2357
  /* @__PURE__ */ jsx("div", {
2313
2358
  className: "relative",
2314
2359
  children: /* @__PURE__ */ jsx(ChatInputSurfaceHost, {
@@ -2649,7 +2694,7 @@ var ChatCodeSyntaxHighlighter = class {
2649
2694
  const chatCodeSyntaxHighlighter = new ChatCodeSyntaxHighlighter();
2650
2695
  //#endregion
2651
2696
  //#region src/components/chat/ui/chat-message-list/chat-code-block.tsx
2652
- const CODE_LANGUAGE_REGEX = /language-([a-z0-9-]+)/i;
2697
+ const CODE_LANGUAGE_REGEX$1 = /language-([a-z0-9-]+)/i;
2653
2698
  function flattenNodeText(value) {
2654
2699
  if (typeof value === "string" || typeof value === "number") return String(value);
2655
2700
  if (Array.isArray(value)) return value.map(flattenNodeText).join("");
@@ -2660,7 +2705,7 @@ function normalizeCodeText(value) {
2660
2705
  return content.endsWith("\n") ? content.slice(0, -1) : content;
2661
2706
  }
2662
2707
  function resolveCodeLanguage(className) {
2663
- return (className ? CODE_LANGUAGE_REGEX.exec(className) : null)?.[1]?.toLowerCase() || "text";
2708
+ return (className ? CODE_LANGUAGE_REGEX$1.exec(className) : null)?.[1]?.toLowerCase() || "text";
2664
2709
  }
2665
2710
  function ChatCodeBlock({ className, children, texts }) {
2666
2711
  const language = useMemo(() => resolveCodeLanguage(className), [className]);
@@ -2693,6 +2738,176 @@ function ChatCodeBlock({ className, children, texts }) {
2693
2738
  });
2694
2739
  }
2695
2740
  //#endregion
2741
+ //#region src/components/chat/ui/chat-message-list/utils/chat-inline-display.utils.ts
2742
+ const INLINE_DISPLAY_LANGUAGE = "nextclaw-inline";
2743
+ const CODE_LANGUAGE_REGEX = /language-([a-z0-9-]+)/i;
2744
+ const FILE_PREVIEW_VIEWERS = new Set([
2745
+ "auto",
2746
+ "source",
2747
+ "rendered"
2748
+ ]);
2749
+ function isRecord$1(value) {
2750
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2751
+ }
2752
+ function readString(value) {
2753
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2754
+ }
2755
+ function readPositiveInteger(value) {
2756
+ return Number.isInteger(value) && Number(value) > 0 ? Number(value) : void 0;
2757
+ }
2758
+ function readFilePreviewViewer(value) {
2759
+ const viewer = readString(value);
2760
+ return viewer && FILE_PREVIEW_VIEWERS.has(viewer) ? viewer : void 0;
2761
+ }
2762
+ function readTargetPayload(record) {
2763
+ const { payload } = record;
2764
+ return isRecord$1(payload) ? payload : null;
2765
+ }
2766
+ function readFileTarget(record) {
2767
+ const payload = readTargetPayload(record);
2768
+ if (!payload) return null;
2769
+ const path = readString(payload.path);
2770
+ if (!path) return null;
2771
+ return {
2772
+ type: "file",
2773
+ payload: {
2774
+ path,
2775
+ line: readPositiveInteger(payload.line),
2776
+ column: readPositiveInteger(payload.column),
2777
+ viewer: readFilePreviewViewer(payload.viewer)
2778
+ }
2779
+ };
2780
+ }
2781
+ function readUrlTarget(record) {
2782
+ const payload = readTargetPayload(record);
2783
+ if (!payload) return null;
2784
+ const url = readString(payload.url);
2785
+ if (!url) return null;
2786
+ return {
2787
+ type: "url",
2788
+ payload: { url }
2789
+ };
2790
+ }
2791
+ function readPanelAppTarget(record) {
2792
+ const payload = readTargetPayload(record);
2793
+ if (!payload) return null;
2794
+ const appId = readString(payload.appId);
2795
+ if (!appId) return null;
2796
+ return {
2797
+ type: "panel_app",
2798
+ payload: { appId }
2799
+ };
2800
+ }
2801
+ function readJsonTarget(record) {
2802
+ const payload = readTargetPayload(record);
2803
+ if (!payload || !("value" in payload)) return null;
2804
+ return {
2805
+ type: "json",
2806
+ payload: { value: payload.value }
2807
+ };
2808
+ }
2809
+ function readInlineDisplayTarget(value) {
2810
+ if (!isRecord$1(value)) return null;
2811
+ const type = readString(value.type);
2812
+ if (type === "file") return readFileTarget(value);
2813
+ if (type === "url") return readUrlTarget(value);
2814
+ if (type === "panel_app") return readPanelAppTarget(value);
2815
+ if (type === "json") return readJsonTarget(value);
2816
+ return null;
2817
+ }
2818
+ function isChatInlineDisplayLanguage(className) {
2819
+ return (className ? CODE_LANGUAGE_REGEX.exec(className) : null)?.[1]?.toLowerCase() === INLINE_DISPLAY_LANGUAGE;
2820
+ }
2821
+ function parseChatInlineDisplayDirective(rawText) {
2822
+ const text = rawText.endsWith("\n") ? rawText.slice(0, -1) : rawText;
2823
+ let parsed;
2824
+ try {
2825
+ parsed = JSON.parse(text);
2826
+ } catch {
2827
+ return null;
2828
+ }
2829
+ if (!isRecord$1(parsed)) return null;
2830
+ const target = readInlineDisplayTarget(parsed.target);
2831
+ if (!target) return null;
2832
+ return {
2833
+ target,
2834
+ title: readString(parsed.title),
2835
+ description: readString(parsed.description)
2836
+ };
2837
+ }
2838
+ function getChatInlineDisplayLabel(display) {
2839
+ if (display.title) return display.title;
2840
+ if (display.target.type === "file") {
2841
+ const segments = display.target.payload.path.split("/").filter(Boolean);
2842
+ return segments[segments.length - 1] ?? display.target.payload.path;
2843
+ }
2844
+ if (display.target.type === "url") return display.target.payload.url;
2845
+ if (display.target.type === "panel_app") return display.target.payload.appId;
2846
+ return "JSON";
2847
+ }
2848
+ function getChatInlineDisplayDetail(display) {
2849
+ if (display.target.type === "file") return display.target.payload.path;
2850
+ if (display.target.type === "url") return display.target.payload.url;
2851
+ if (display.target.type === "panel_app") return display.target.payload.appId;
2852
+ return JSON.stringify(display.target.payload.value, null, 2);
2853
+ }
2854
+ //#endregion
2855
+ //#region src/components/chat/ui/chat-message-list/chat-inline-display.tsx
2856
+ function ChatInlineDisplayIcon({ display }) {
2857
+ const className = "h-3.5 w-3.5 shrink-0 text-muted-foreground";
2858
+ if (display.target.type === "file") return /* @__PURE__ */ jsx(FileText, {
2859
+ className,
2860
+ "aria-hidden": true
2861
+ });
2862
+ if (display.target.type === "url") return /* @__PURE__ */ jsx(Globe, {
2863
+ className,
2864
+ "aria-hidden": true
2865
+ });
2866
+ if (display.target.type === "panel_app") return /* @__PURE__ */ jsx(AppWindow, {
2867
+ className,
2868
+ "aria-hidden": true
2869
+ });
2870
+ return /* @__PURE__ */ jsx(Code2, {
2871
+ className,
2872
+ "aria-hidden": true
2873
+ });
2874
+ }
2875
+ function formatJsonDetail(value) {
2876
+ return value.length > 1200 ? `${value.slice(0, 1200)}\n...` : value;
2877
+ }
2878
+ function ChatInlineDisplayFallback({ display }) {
2879
+ const label = getChatInlineDisplayLabel(display);
2880
+ const detail = getChatInlineDisplayDetail(display);
2881
+ const formattedDetail = display.target.type === "json" ? formatJsonDetail(detail) : detail;
2882
+ return /* @__PURE__ */ jsxs("section", {
2883
+ className: cn("my-2 max-w-full border-l-2 border-border bg-muted/30 px-3 py-2", "text-xs leading-relaxed text-foreground"),
2884
+ "data-nextclaw-inline-display": "true",
2885
+ "data-nextclaw-inline-display-type": display.target.type,
2886
+ children: [
2887
+ /* @__PURE__ */ jsxs("div", {
2888
+ className: "flex min-w-0 items-center gap-2",
2889
+ children: [/* @__PURE__ */ jsx(ChatInlineDisplayIcon, { display }), /* @__PURE__ */ jsx("span", {
2890
+ className: "min-w-0 truncate font-medium",
2891
+ children: label
2892
+ })]
2893
+ }),
2894
+ display.description ? /* @__PURE__ */ jsx("p", {
2895
+ className: "mt-1 line-clamp-2 text-[12px] text-muted-foreground",
2896
+ children: display.description
2897
+ }) : null,
2898
+ /* @__PURE__ */ jsx("pre", {
2899
+ className: cn("mt-1 max-h-40 overflow-auto whitespace-pre-wrap break-words", "font-mono text-[11px] leading-relaxed text-muted-foreground"),
2900
+ children: formattedDetail
2901
+ })
2902
+ ]
2903
+ });
2904
+ }
2905
+ function ChatInlineDisplay({ display, renderInlineDisplay }) {
2906
+ const rendered = renderInlineDisplay?.(display);
2907
+ if (rendered !== void 0 && rendered !== null) return /* @__PURE__ */ jsx(Fragment$1, { children: rendered });
2908
+ return /* @__PURE__ */ jsx(ChatInlineDisplayFallback, { display });
2909
+ }
2910
+ //#endregion
2696
2911
  //#region src/components/chat/ui/chat-message-list/chat-message-markdown.tsx
2697
2912
  const MARKDOWN_MAX_CHARS = 14e4;
2698
2913
  const SAFE_LINK_PROTOCOLS = new Set([
@@ -2705,24 +2920,7 @@ const INLINE_TOKEN_KIND_ATTR = "data-chat-inline-token-kind";
2705
2920
  const INLINE_TOKEN_KEY_ATTR = "data-chat-inline-token-key";
2706
2921
  const INLINE_TOKEN_LABEL_ATTR = "data-chat-inline-token-label";
2707
2922
  const INLINE_TOKEN_RAW_TEXT_ATTR = "data-chat-inline-token-raw-text";
2708
- const PROJECT_RELATIVE_FILE_EXTENSIONS = [
2709
- "cjs",
2710
- "css",
2711
- "cts",
2712
- "js",
2713
- "json",
2714
- "jsx",
2715
- "md",
2716
- "mdx",
2717
- "mjs",
2718
- "mts",
2719
- "tsx",
2720
- "ts",
2721
- "txt",
2722
- "yaml",
2723
- "yml"
2724
- ].join("|");
2725
- const PROJECT_RELATIVE_FILE_HREF_PATTERN = new RegExp(`^(?![a-zA-Z][a-zA-Z\\d+.-]*:)(?!//)(?:(?:[^/\\s?#]+/)+[^?#\\s]+\\.[A-Za-z0-9_-]+|[^/\\s?#]+\\.(?:${PROJECT_RELATIVE_FILE_EXTENSIONS}))(?::\\d+(?::\\d+)?)?(?:[?#].*)?$`, "i");
2923
+ const PROJECT_RELATIVE_FILE_HREF_PATTERN = new RegExp(`^(?![a-zA-Z][a-zA-Z\\d+.-]*:)(?!//)(?:(?:[^/\\s?#]+/)+[^?#\\s]+\\.[A-Za-z0-9_-]+|[^/\\s?#]+\\.(?:cjs|css|cts|html?|js|json|jsx|mdx?|mjs|mts|tsx?|txt|ya?ml))(?::\\d+(?::\\d+)?)?(?:[?#].*)?$`, "i");
2726
2924
  function trimMarkdown(value) {
2727
2925
  if (value.length <= MARKDOWN_MAX_CHARS) return value;
2728
2926
  return `${value.slice(0, MARKDOWN_MAX_CHARS)}\n\n...`;
@@ -2744,6 +2942,7 @@ function looksLikeLocalFileHref(href) {
2744
2942
  return href.startsWith("./") || href.startsWith("../") || href.startsWith("/Users/") || href.startsWith("/home/") || href.startsWith("/tmp/") || href.startsWith("/var/") || PROJECT_RELATIVE_FILE_HREF_PATTERN.test(href) || /^\/.+\.[A-Za-z0-9_-]+(?::\d+(?::\d+)?)?$/.test(href);
2745
2943
  }
2746
2944
  function parseLocalFileAction(href) {
2945
+ const viewer = new URLSearchParams(href.split("#")[0]?.split("?")[1] ?? "").get("viewer");
2747
2946
  const normalizedHref = href.split("#")[0]?.split("?")[0] ?? href;
2748
2947
  const decodedHref = decodeURIComponent(normalizedHref);
2749
2948
  if (!looksLikeLocalFileHref(decodedHref)) return null;
@@ -2755,6 +2954,7 @@ function parseLocalFileAction(href) {
2755
2954
  path: rawPath,
2756
2955
  label: rawPath.split("/").filter(Boolean).pop() ?? rawPath,
2757
2956
  viewMode: "preview",
2957
+ ...viewer === "source" || viewer === "rendered" ? { previewViewer: viewer } : {},
2758
2958
  ...typeof line === "number" ? { line } : {},
2759
2959
  ...typeof column === "number" ? { column } : {}
2760
2960
  };
@@ -2848,7 +3048,7 @@ function readStringProp(props, key) {
2848
3048
  const value = props[key];
2849
3049
  return typeof value === "string" && value.length > 0 ? value : null;
2850
3050
  }
2851
- function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen }) {
3051
+ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens, onFileOpen, renderInlineDisplay }) {
2852
3052
  const isUser = role === "user";
2853
3053
  const remarkPlugins = useMemo(() => inlineTokens && inlineTokens.length > 0 ? [remarkGfm, createRemarkInlineTokenPlugin(inlineTokens)] : [remarkGfm], [inlineTokens]);
2854
3054
  const markdownComponents = useMemo(() => ({
@@ -2869,7 +3069,7 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
2869
3069
  children
2870
3070
  });
2871
3071
  },
2872
- a: ({ href, children, ...rest }) => {
3072
+ a: ({ node: _node, href, children, ...rest }) => {
2873
3073
  const safeHref = resolveSafeHref(href);
2874
3074
  if (!safeHref) return /* @__PURE__ */ jsx("span", {
2875
3075
  className: "chat-link-invalid",
@@ -2892,14 +3092,14 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
2892
3092
  children
2893
3093
  });
2894
3094
  },
2895
- table: ({ children, ...rest }) => /* @__PURE__ */ jsx("div", {
3095
+ table: ({ node: _node, children, ...rest }) => /* @__PURE__ */ jsx("div", {
2896
3096
  className: "chat-table-wrap",
2897
3097
  children: /* @__PURE__ */ jsx("table", {
2898
3098
  ...rest,
2899
3099
  children
2900
3100
  })
2901
3101
  }),
2902
- input: ({ type, checked, ...rest }) => {
3102
+ input: ({ node: _node, type, checked, ...rest }) => {
2903
3103
  if (type !== "checkbox") return /* @__PURE__ */ jsx("input", {
2904
3104
  ...rest,
2905
3105
  type
@@ -2913,7 +3113,7 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
2913
3113
  className: "chat-task-checkbox"
2914
3114
  });
2915
3115
  },
2916
- img: ({ src, alt, ...rest }) => {
3116
+ img: ({ node: _node, src, alt, ...rest }) => {
2917
3117
  const safeSrc = resolveSafeHref(src);
2918
3118
  if (!safeSrc) return null;
2919
3119
  return /* @__PURE__ */ jsx("img", {
@@ -2924,12 +3124,18 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
2924
3124
  decoding: "async"
2925
3125
  });
2926
3126
  },
2927
- code: ({ className, children, ...rest }) => {
2928
- if (!className && !String(children ?? "").includes("\n")) return /* @__PURE__ */ jsx("code", {
3127
+ code: ({ node: _node, className, children, ...rest }) => {
3128
+ const plainText = String(children ?? "");
3129
+ if (!className && !plainText.includes("\n")) return /* @__PURE__ */ jsx("code", {
2929
3130
  ...rest,
2930
3131
  className: cn("chat-inline-code", className),
2931
3132
  children
2932
3133
  });
3134
+ const inlineDisplay = isChatInlineDisplayLanguage(className) ? parseChatInlineDisplayDirective(plainText) : null;
3135
+ if (inlineDisplay) return /* @__PURE__ */ jsx(ChatInlineDisplay, {
3136
+ display: inlineDisplay,
3137
+ renderInlineDisplay
3138
+ });
2933
3139
  return /* @__PURE__ */ jsx(ChatCodeBlock, {
2934
3140
  className,
2935
3141
  texts,
@@ -2940,6 +3146,7 @@ function ChatMessageMarkdown({ text, role, texts, inline = false, inlineTokens,
2940
3146
  inline,
2941
3147
  isUser,
2942
3148
  onFileOpen,
3149
+ renderInlineDisplay,
2943
3150
  texts
2944
3151
  ]);
2945
3152
  return /* @__PURE__ */ jsx(inline ? "span" : "div", {
@@ -3371,12 +3578,20 @@ function useReasoningBlockOpenState(params) {
3371
3578
  //#region src/components/chat/hooks/use-sticky-bottom-scroll.ts
3372
3579
  const DEFAULT_STICKY_THRESHOLD_PX = 10;
3373
3580
  function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey, scrollRef, stickyThresholdPx }) {
3581
+ const [isAtBottom, setIsAtBottom] = useState(true);
3374
3582
  const isStickyRef = useRef(true);
3375
3583
  const isProgrammaticScrollRef = useRef(false);
3376
3584
  const previousResetKeyRef = useRef(null);
3377
3585
  const pendingInitialScrollRef = useRef(false);
3378
3586
  const scheduledScrollFrameRef = useRef(null);
3379
- const queueScrollToBottom = useCallback(() => {
3587
+ const updateStickyState = useCallback((nextIsAtBottom) => {
3588
+ isStickyRef.current = nextIsAtBottom;
3589
+ setIsAtBottom(nextIsAtBottom);
3590
+ }, []);
3591
+ const resolveIsAtBottom = useCallback((element) => {
3592
+ return element.scrollHeight - element.scrollTop - element.clientHeight <= (stickyThresholdPx ?? DEFAULT_STICKY_THRESHOLD_PX);
3593
+ }, [stickyThresholdPx]);
3594
+ const queueScrollToBottom = useCallback((behavior = "auto") => {
3380
3595
  if (!scrollRef.current) return;
3381
3596
  if (scheduledScrollFrameRef.current !== null) cancelAnimationFrame(scheduledScrollFrameRef.current);
3382
3597
  scheduledScrollFrameRef.current = requestAnimationFrame(() => {
@@ -3384,24 +3599,36 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3384
3599
  const currentElement = scrollRef.current;
3385
3600
  if (!currentElement) return;
3386
3601
  isProgrammaticScrollRef.current = true;
3387
- currentElement.scrollTop = currentElement.scrollHeight;
3602
+ if (typeof currentElement.scrollTo === "function") currentElement.scrollTo({
3603
+ top: currentElement.scrollHeight,
3604
+ behavior
3605
+ });
3606
+ else currentElement.scrollTop = currentElement.scrollHeight;
3388
3607
  });
3389
3608
  }, [scrollRef]);
3390
- const onScroll = () => {
3609
+ const scrollToBottom = useCallback(() => {
3610
+ updateStickyState(true);
3611
+ queueScrollToBottom();
3612
+ }, [queueScrollToBottom, updateStickyState]);
3613
+ const onScroll = useCallback(() => {
3391
3614
  if (isProgrammaticScrollRef.current) {
3392
3615
  isProgrammaticScrollRef.current = false;
3393
3616
  return;
3394
3617
  }
3395
3618
  const element = scrollRef.current;
3396
3619
  if (!element) return;
3397
- isStickyRef.current = element.scrollHeight - element.scrollTop - element.clientHeight <= (stickyThresholdPx ?? DEFAULT_STICKY_THRESHOLD_PX);
3398
- };
3620
+ updateStickyState(resolveIsAtBottom(element));
3621
+ }, [
3622
+ resolveIsAtBottom,
3623
+ scrollRef,
3624
+ updateStickyState
3625
+ ]);
3399
3626
  useEffect(() => {
3400
3627
  if (previousResetKeyRef.current === resetKey) return;
3401
3628
  previousResetKeyRef.current = resetKey;
3402
- isStickyRef.current = true;
3629
+ updateStickyState(true);
3403
3630
  pendingInitialScrollRef.current = true;
3404
- }, [resetKey]);
3631
+ }, [resetKey, updateStickyState]);
3405
3632
  useEffect(() => {
3406
3633
  return () => {
3407
3634
  const scheduledScrollFrame = scheduledScrollFrameRef.current;
@@ -3432,7 +3659,11 @@ function useStickyBottomScroll({ contentVersion, hasContent, isLoading, resetKey
3432
3659
  queueScrollToBottom,
3433
3660
  scrollRef
3434
3661
  ]);
3435
- return { onScroll };
3662
+ return {
3663
+ isAtBottom,
3664
+ onScroll,
3665
+ scrollToBottom
3666
+ };
3436
3667
  }
3437
3668
  //#endregion
3438
3669
  //#region src/components/chat/ui/chat-message-list/chat-reasoning-block.tsx
@@ -3639,6 +3870,20 @@ function readLineNumberColumnWidth(block) {
3639
3870
  function readCodeColumnWidth(block) {
3640
3871
  return `calc(${block.lines.reduce((currentMax, line) => Math.max(currentMax, Math.max(1, line.text.length)), 1)}ch + 1.25rem)`;
3641
3872
  }
3873
+ function readPathExtension(path) {
3874
+ const fileName = path.split(/[\\/]/).filter(Boolean).pop() ?? path;
3875
+ const cleanName = fileName.split(/[?#]/)[0] ?? fileName;
3876
+ const extensionStart = cleanName.lastIndexOf(".");
3877
+ if (extensionStart <= 0 || extensionStart === cleanName.length - 1) return null;
3878
+ return cleanName.slice(extensionStart + 1).toLowerCase();
3879
+ }
3880
+ function readBlockLanguage(block) {
3881
+ return block.languageHint?.trim() || readPathExtension(block.path) || "text";
3882
+ }
3883
+ function readLanguageClassName(language) {
3884
+ const normalized = language.toLowerCase().replace(/[^a-z0-9-]/g, "-");
3885
+ return normalized ? `language-${normalized}` : "language-text";
3886
+ }
3642
3887
  function getLineNumberTone(line) {
3643
3888
  if (line.kind === "remove") return "border-r border-rose-200 bg-rose-50 text-rose-700";
3644
3889
  if (line.kind === "add") return "border-r border-emerald-200 bg-emerald-50 text-emerald-700";
@@ -3660,14 +3905,17 @@ function FileOperationLineNumberCell({ layout, line, lineNumberColumnWidth }) {
3660
3905
  children: readVisibleLineNumber(line)
3661
3906
  });
3662
3907
  }
3663
- function FileOperationCodeCell({ line }) {
3908
+ function FileOperationCodeCell({ language, line }) {
3909
+ const highlightedCode = useMemo(() => chatCodeSyntaxHighlighter.highlight(line.text || " ", language), [language, line.text]);
3664
3910
  return /* @__PURE__ */ jsx("span", {
3665
3911
  "data-file-code-row": "true",
3666
- className: cn("block min-w-0 flex-1 whitespace-pre px-2.5", getCodeRowTone(line)),
3667
- children: line.text || " "
3912
+ "data-file-code-language": highlightedCode.language,
3913
+ "data-highlighted": highlightedCode.highlighted ? "true" : "false",
3914
+ className: cn("chat-file-code-syntax hljs block min-w-0 flex-1 whitespace-pre px-2.5", readLanguageClassName(highlightedCode.language), getCodeRowTone(line)),
3915
+ dangerouslySetInnerHTML: { __html: highlightedCode.html }
3668
3916
  });
3669
3917
  }
3670
- function FileOperationLineRow({ line, showLineNumbers, lineNumberColumnWidth }) {
3918
+ function FileOperationLineRow({ language, line, showLineNumbers, lineNumberColumnWidth }) {
3671
3919
  return /* @__PURE__ */ jsxs("div", {
3672
3920
  "data-file-line-row": "true",
3673
3921
  className: FILE_ROW_CLASS_NAME,
@@ -3675,13 +3923,17 @@ function FileOperationLineRow({ line, showLineNumbers, lineNumberColumnWidth })
3675
3923
  layout: "compact",
3676
3924
  line,
3677
3925
  lineNumberColumnWidth
3678
- }) : null, /* @__PURE__ */ jsx(FileOperationCodeCell, { line })]
3926
+ }) : null, /* @__PURE__ */ jsx(FileOperationCodeCell, {
3927
+ language,
3928
+ line
3929
+ })]
3679
3930
  });
3680
3931
  }
3681
3932
  function FileOperationWorkspaceSurface({ block }) {
3682
3933
  const showLineNumbers = readHasBlockLineNumbers(block);
3683
3934
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
3684
3935
  const codeColumnWidth = readCodeColumnWidth(block);
3936
+ const language = readBlockLanguage(block);
3685
3937
  return /* @__PURE__ */ jsxs("div", {
3686
3938
  "data-file-code-surface": "true",
3687
3939
  "data-file-code-surface-layout": "workspace",
@@ -3708,7 +3960,10 @@ function FileOperationWorkspaceSurface({ block }) {
3708
3960
  children: block.lines.map((line, index) => /* @__PURE__ */ jsx("div", {
3709
3961
  "data-file-code-canvas-row": "true",
3710
3962
  className: FILE_ROW_CLASS_NAME,
3711
- children: /* @__PURE__ */ jsx(FileOperationCodeCell, { line })
3963
+ children: /* @__PURE__ */ jsx(FileOperationCodeCell, {
3964
+ language,
3965
+ line
3966
+ })
3712
3967
  }, readLineKey("code", line, index)))
3713
3968
  }), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-card" })]
3714
3969
  })]
@@ -3720,6 +3975,7 @@ function FileOperationCodeSurface({ block, layout = "compact" }) {
3720
3975
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
3721
3976
  const codeColumnWidth = readCodeColumnWidth(block);
3722
3977
  const surfaceMinWidth = showLineNumbers ? `calc(${lineNumberColumnWidth} + ${codeColumnWidth})` : codeColumnWidth;
3978
+ const language = readBlockLanguage(block);
3723
3979
  return /* @__PURE__ */ jsx("div", {
3724
3980
  "data-file-code-surface": "true",
3725
3981
  "data-file-code-surface-layout": "compact",
@@ -3729,6 +3985,7 @@ function FileOperationCodeSurface({ block, layout = "compact" }) {
3729
3985
  className: "min-w-full",
3730
3986
  style: { minWidth: surfaceMinWidth },
3731
3987
  children: block.lines.map((line, index) => /* @__PURE__ */ jsx(FileOperationLineRow, {
3988
+ language,
3732
3989
  line,
3733
3990
  showLineNumbers,
3734
3991
  lineNumberColumnWidth
@@ -4039,20 +4296,21 @@ function GenericToolSection({ label, tone, children }) {
4039
4296
  function TerminalExecutionView({ card }) {
4040
4297
  const output = normalizeTerminalOutput(card.output, card.outputData);
4041
4298
  const isRunning = card.statusTone === "running";
4042
- const hasContent = !!(card.summary?.trim() || output.trim().length > 0);
4299
+ const commandPart = card.summary?.replace(/^(command|path|args|query|input):\s*/i, "");
4300
+ const hasOutput = output.trim().length > 0;
4301
+ const canExpand = isRunning || hasOutput || Boolean(commandPart?.trim());
4043
4302
  const { expanded, onToggle } = useToolCardExpandedState({
4044
- canExpand: output.trim().length > 0 || isRunning,
4303
+ canExpand,
4045
4304
  isRunning,
4046
4305
  autoExpandWhileRunning: false,
4047
- expandOnError: hasContent,
4306
+ expandOnError: canExpand,
4048
4307
  statusTone: card.statusTone
4049
4308
  });
4050
- const commandPart = card.summary?.replace(/^(command|path|args|query|input):\s*/i, "");
4051
4309
  return /* @__PURE__ */ jsxs(ToolCardRoot, { children: [/* @__PURE__ */ jsx(ToolCardHeader, {
4052
4310
  card,
4053
4311
  icon: Terminal,
4054
4312
  expanded,
4055
- canExpand: output.trim().length > 0 || isRunning,
4313
+ canExpand,
4056
4314
  onToggle
4057
4315
  }), expanded && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
4058
4316
  className: "px-3 pb-2 font-mono w-full max-h-48 overflow-y-auto custom-scrollbar min-h-0 text-[12px]",
@@ -4069,10 +4327,13 @@ function TerminalExecutionView({ card }) {
4069
4327
  }) : /* @__PURE__ */ jsx("div", { className: "h-3 w-32 bg-muted rounded animate-pulse mt-2" })
4070
4328
  })]
4071
4329
  })
4072
- }), output && /* @__PURE__ */ jsx(ToolCardContent, { children: /* @__PURE__ */ jsxs("pre", {
4073
- className: "font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed px-0",
4074
- children: [output, isRunning && /* @__PURE__ */ jsx("span", { className: "inline-block w-1.5 h-3 ml-1 bg-primary/60 animate-pulse align-middle" })]
4075
- }) })] })] });
4330
+ }), (hasOutput || !isRunning) && /* @__PURE__ */ jsx(ToolCardContent, {
4331
+ className: hasOutput ? void 0 : "bg-muted/20 py-2",
4332
+ children: /* @__PURE__ */ jsxs("pre", {
4333
+ className: cn("font-mono text-[12px] text-muted-foreground whitespace-pre-wrap break-all w-full max-w-full max-h-64 overflow-y-auto overflow-x-hidden min-w-0 custom-scrollbar leading-relaxed", hasOutput ? "px-0" : "rounded-md border border-dashed border-border bg-card/70 px-3 py-2 italic"),
4334
+ children: [hasOutput ? output : card.emptyLabel, isRunning && hasOutput && /* @__PURE__ */ jsx("span", { className: "inline-block w-1.5 h-3 ml-1 bg-primary/60 animate-pulse align-middle" })]
4335
+ })
4336
+ })] })] });
4076
4337
  }
4077
4338
  function FileOperationView({ card, onFileOpen }) {
4078
4339
  const output = card.output?.trim() ?? "";
@@ -4218,75 +4479,130 @@ function ChatToolCard({ card, onToolAction, onFileOpen, renderToolAgent, renderP
4218
4479
  }
4219
4480
  //#endregion
4220
4481
  //#region src/components/chat/ui/chat-message-list/chat-message.tsx
4221
- const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, onFileOpen, renderToolAgent, renderPanelAppCard }) {
4482
+ function isMessageInProgress(status) {
4483
+ return status === "pending" || status === "streaming";
4484
+ }
4485
+ function isProcessPart(part) {
4486
+ return part.type === "reasoning" || part.type === "tool-card";
4487
+ }
4488
+ function splitAssistantProcess(message) {
4489
+ if (message.role !== "assistant" || !message.processSummary || isMessageInProgress(message.status)) return null;
4490
+ let lastProcessPartIndex = -1;
4491
+ for (let index = message.parts.length - 1; index >= 0; index -= 1) if (isProcessPart(message.parts[index])) {
4492
+ lastProcessPartIndex = index;
4493
+ break;
4494
+ }
4495
+ if (lastProcessPartIndex < 0 || lastProcessPartIndex >= message.parts.length - 1) return null;
4496
+ return {
4497
+ processParts: message.parts.slice(0, lastProcessPartIndex + 1),
4498
+ finalParts: message.parts.slice(lastProcessPartIndex + 1)
4499
+ };
4500
+ }
4501
+ function renderChatMessagePart({ index, isInProgress, isLastPart, isUser, onFileOpen, onToolAction, part, renderInlineDisplay, renderPanelAppCard, renderToolAgent, role, texts }) {
4502
+ const { type } = part;
4503
+ if (type === "markdown") {
4504
+ const { inlineTokens, text } = part;
4505
+ return /* @__PURE__ */ jsx(ChatMessageMarkdown, {
4506
+ text,
4507
+ role,
4508
+ texts,
4509
+ inlineTokens,
4510
+ onFileOpen,
4511
+ renderInlineDisplay
4512
+ }, `markdown-${index}`);
4513
+ }
4514
+ if (type === "reasoning") {
4515
+ const { label, text } = part;
4516
+ return /* @__PURE__ */ jsx(ChatReasoningBlock, {
4517
+ label,
4518
+ text,
4519
+ isUser,
4520
+ isInProgress: isInProgress && isLastPart
4521
+ }, `reasoning-${index}`);
4522
+ }
4523
+ if (type === "tool-card") {
4524
+ const { card } = part;
4525
+ return /* @__PURE__ */ jsx("div", {
4526
+ className: "mt-0.5",
4527
+ children: /* @__PURE__ */ jsx(ChatToolCard, {
4528
+ card,
4529
+ onToolAction,
4530
+ onFileOpen,
4531
+ renderToolAgent,
4532
+ renderPanelAppCard
4533
+ })
4534
+ }, `tool-${index}`);
4535
+ }
4536
+ if (type === "file") {
4537
+ const { file } = part;
4538
+ return /* @__PURE__ */ jsx(ChatMessageFile, {
4539
+ file,
4540
+ isUser,
4541
+ texts
4542
+ }, `file-${index}`);
4543
+ }
4544
+ if (type === "unknown") {
4545
+ const { label, rawType, text } = part;
4546
+ return /* @__PURE__ */ jsxs("div", {
4547
+ className: "rounded-lg border border-border bg-muted/60 px-2.5 py-2 text-xs text-muted-foreground",
4548
+ children: [/* @__PURE__ */ jsxs("div", {
4549
+ className: "font-semibold text-foreground",
4550
+ children: [
4551
+ label,
4552
+ ": ",
4553
+ rawType
4554
+ ]
4555
+ }), text ? /* @__PURE__ */ jsx("pre", {
4556
+ className: "mt-1 whitespace-pre-wrap break-words text-[11px] text-muted-foreground",
4557
+ children: text
4558
+ }) : null]
4559
+ }, `unknown-${index}`);
4560
+ }
4561
+ return null;
4562
+ }
4563
+ const ChatMessage = memo(function ChatMessage({ message, texts, onToolAction, onFileOpen, renderInlineDisplay, renderToolAgent, renderPanelAppCard }) {
4222
4564
  const { role } = message;
4223
4565
  const isUser = role === "user";
4224
- const isMessageInProgress = message.status === "pending" || message.status === "streaming";
4566
+ const isInProgress = isMessageInProgress(message.status);
4567
+ const processSplit = splitAssistantProcess(message);
4568
+ const renderPart = (part, index) => renderChatMessagePart({
4569
+ part,
4570
+ index,
4571
+ role,
4572
+ isUser,
4573
+ isInProgress,
4574
+ isLastPart: index === message.parts.length - 1,
4575
+ texts,
4576
+ onToolAction,
4577
+ onFileOpen,
4578
+ renderInlineDisplay,
4579
+ renderToolAgent,
4580
+ renderPanelAppCard
4581
+ });
4225
4582
  return /* @__PURE__ */ jsx("div", {
4226
- className: cn("inline-block w-fit max-w-full rounded-2xl border px-4 shadow-sm", isUser ? "border-primary bg-primary py-3 text-primary-foreground" : role === "assistant" ? "border-border bg-card pb-3 pt-4 text-card-foreground" : "border-border bg-muted/45 py-3 text-foreground"),
4583
+ className: cn("inline-block w-fit max-w-full rounded-2xl border px-4 shadow-sm has-[[data-chat-message-wide-content=true]]:w-full", isUser ? "border-primary bg-primary py-3 text-primary-foreground" : role === "assistant" ? "border-border bg-card pb-3 pt-4 text-card-foreground" : "border-border bg-muted/45 py-3 text-foreground"),
4227
4584
  children: /* @__PURE__ */ jsx("div", {
4228
4585
  className: "space-y-2",
4229
- children: message.parts.map((part, index) => {
4230
- const { type } = part;
4231
- if (type === "markdown") {
4232
- const { inlineTokens, text } = part;
4233
- return /* @__PURE__ */ jsx(ChatMessageMarkdown, {
4234
- text,
4235
- role,
4236
- texts,
4237
- inlineTokens,
4238
- onFileOpen
4239
- }, `markdown-${index}`);
4240
- }
4241
- if (type === "reasoning") {
4242
- const { label, text } = part;
4243
- return /* @__PURE__ */ jsx(ChatReasoningBlock, {
4244
- label,
4245
- text,
4246
- isUser,
4247
- isInProgress: isMessageInProgress && index === message.parts.length - 1
4248
- }, `reasoning-${index}`);
4249
- }
4250
- if (type === "tool-card") {
4251
- const { card } = part;
4252
- return /* @__PURE__ */ jsx("div", {
4253
- className: "mt-0.5",
4254
- children: /* @__PURE__ */ jsx(ChatToolCard, {
4255
- card,
4256
- onToolAction,
4257
- onFileOpen,
4258
- renderToolAgent,
4259
- renderPanelAppCard
4260
- })
4261
- }, `tool-${index}`);
4262
- }
4263
- if (type === "file") {
4264
- const { file } = part;
4265
- return /* @__PURE__ */ jsx(ChatMessageFile, {
4266
- file,
4267
- isUser,
4268
- texts
4269
- }, `file-${index}`);
4270
- }
4271
- if (type === "unknown") {
4272
- const { label, rawType, text } = part;
4273
- return /* @__PURE__ */ jsxs("div", {
4274
- className: "rounded-lg border border-border bg-muted/60 px-2.5 py-2 text-xs text-muted-foreground",
4275
- children: [/* @__PURE__ */ jsxs("div", {
4276
- className: "font-semibold text-foreground",
4277
- children: [
4278
- label,
4279
- ": ",
4280
- rawType
4281
- ]
4282
- }), text ? /* @__PURE__ */ jsx("pre", {
4283
- className: "mt-1 whitespace-pre-wrap break-words text-[11px] text-muted-foreground",
4284
- children: text
4285
- }) : null]
4286
- }, `unknown-${index}`);
4287
- }
4288
- return null;
4289
- })
4586
+ children: processSplit ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("details", {
4587
+ className: "group/process text-xs text-muted-foreground",
4588
+ children: [/* @__PURE__ */ jsxs("summary", {
4589
+ className: "flex cursor-pointer list-none items-center gap-1.5 border-b border-border/60 pb-2 font-medium transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 [&::-webkit-details-marker]:hidden",
4590
+ children: [
4591
+ /* @__PURE__ */ jsx(ChevronRight, {
4592
+ className: "h-3.5 w-3.5 group-open/process:hidden",
4593
+ strokeWidth: 2.5
4594
+ }),
4595
+ /* @__PURE__ */ jsx(ChevronDown, {
4596
+ className: "hidden h-3.5 w-3.5 group-open/process:block",
4597
+ strokeWidth: 2.5
4598
+ }),
4599
+ /* @__PURE__ */ jsx("span", { children: message.processSummary?.label })
4600
+ ]
4601
+ }), /* @__PURE__ */ jsx("div", {
4602
+ className: "space-y-2 pt-2",
4603
+ children: processSplit.processParts.map(renderPart)
4604
+ })]
4605
+ }), processSplit.finalParts.map((part, index) => renderPart(part, processSplit.processParts.length + index))] }) : message.parts.map(renderPart)
4290
4606
  })
4291
4607
  });
4292
4608
  });
@@ -4386,7 +4702,7 @@ function ChatTypingIndicator({ label }) {
4386
4702
  })]
4387
4703
  });
4388
4704
  }
4389
- function ChatMessageList({ className, isSending, messages, onFileOpen, onToolAction, renderPanelAppCard, renderToolAgent, texts }) {
4705
+ function ChatMessageList({ className, isSending, messages, onFileOpen, onToolAction, renderInlineDisplay, renderPanelAppCard, renderToolAgent, texts }) {
4390
4706
  const visibleMessages = messages.filter(hasRenderableMessageContent);
4391
4707
  const hasRenderableAssistantDraft = visibleMessages.some((message) => message.role === "assistant" && (message.status === "streaming" || message.status === "pending"));
4392
4708
  return /* @__PURE__ */ jsxs("div", {
@@ -4399,12 +4715,13 @@ function ChatMessageList({ className, isSending, messages, onFileOpen, onToolAct
4399
4715
  children: [
4400
4716
  !isUser ? /* @__PURE__ */ jsx(ChatMessageAvatar, { role: message.role }) : null,
4401
4717
  /* @__PURE__ */ jsxs("div", {
4402
- className: cn("w-fit max-w-[92%] space-y-2", isUser && "flex flex-col items-end"),
4718
+ className: cn("w-fit max-w-[92%] space-y-2 has-[[data-chat-message-wide-content=true]]:w-full", isUser && "flex flex-col items-end"),
4403
4719
  children: [/* @__PURE__ */ jsx(ChatMessage, {
4404
4720
  message,
4405
4721
  texts,
4406
4722
  onToolAction,
4407
4723
  onFileOpen,
4724
+ renderInlineDisplay,
4408
4725
  renderToolAgent,
4409
4726
  renderPanelAppCard
4410
4727
  }), /* @__PURE__ */ jsx("div", {