opencami 1.3.1 → 1.4.0

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.
Files changed (59) hide show
  1. package/dist/client/assets/_sessionKey-DB95zj1L.js +97 -0
  2. package/dist/client/assets/agents-LFrWe-HX.js +2 -0
  3. package/dist/client/assets/agents-screen-CEBBk1yO.js +1 -0
  4. package/dist/client/assets/bots-CxDwf_WK.js +2 -0
  5. package/dist/client/assets/bots-screen-D6qma1wK.js +1 -0
  6. package/dist/client/assets/button-Il3CHIzX.js +1 -0
  7. package/dist/client/assets/{connect-B8sfEQyb.js → connect-D3baVDFL.js} +1 -1
  8. package/dist/client/assets/file-explorer-screen-rtV6n-5_.js +1 -0
  9. package/dist/client/assets/files-DMemuq9D.js +2 -0
  10. package/dist/client/assets/{index-BfhiL2JN.js → index-ByIsZcHh.js} +1 -1
  11. package/dist/client/assets/index-CVV4XiZo.js +153 -0
  12. package/dist/client/assets/keyboard-shortcuts-dialog-DXC0YHoy.js +1 -0
  13. package/dist/client/assets/{main-BlX7CS2-.js → main-CkIF0soY.js} +9 -9
  14. package/dist/client/assets/opencami-logo-CIxSO1oo.js +1 -0
  15. package/dist/client/assets/{react-ONC2UaeC.js → react-BhVdgA5r.js} +1 -1
  16. package/dist/client/assets/search-dialog-I1jJplIh.js +1 -0
  17. package/dist/client/assets/session-export-dialog-CH5unryw.js +1 -0
  18. package/dist/client/assets/settings-dialog-B8v-GVJ8.js +1 -0
  19. package/dist/client/assets/styles-BaTVzdPa.css +1 -0
  20. package/dist/client/assets/switch-sQnv1YsK.js +1 -0
  21. package/dist/client/assets/tooltip-j_viC_EE.js +1 -0
  22. package/dist/client/assets/use-file-explorer-state-BUH-u7Jv.js +12 -0
  23. package/dist/client/assets/useButton-9VAzplAB.js +9 -0
  24. package/dist/client/assets/useControlled-Y306krcC.js +1 -0
  25. package/dist/client/assets/useMutation-0WgW4xQJ.js +1 -0
  26. package/dist/server/assets/{_sessionKey-ZF1_Jqbt.js → _sessionKey-BhFH4uWY.js} +360 -62
  27. package/dist/server/assets/_tanstack-start-manifest_v-BaIrL1VQ.js +4 -0
  28. package/dist/server/assets/bots-6ryCIgKh.js +11 -0
  29. package/dist/server/assets/bots-screen-DS_ZF9Ec.js +417 -0
  30. package/dist/server/assets/{connect-CIDOw12K.js → connect-B8jpGQGK.js} +1 -1
  31. package/dist/server/assets/{file-explorer-screen-BzvgvV8m.js → file-explorer-screen-DCfS_Ajx.js} +76 -20
  32. package/dist/server/assets/{files-BxvRDIWU.js → files-D2GIrPF4.js} +1 -1
  33. package/dist/server/assets/{index-CTTNe_Sf.js → index-B2JHn34C.js} +1 -1
  34. package/dist/server/assets/{index-CmbNTqa2.js → index-BNSsDaLb.js} +71 -35
  35. package/dist/server/assets/{keyboard-shortcuts-dialog-7OEtXUlW.js → keyboard-shortcuts-dialog-CqIm8aYF.js} +1 -1
  36. package/dist/server/assets/{router-D5D0udHV.js → router-Dme7USeO.js} +219 -74
  37. package/dist/server/assets/{search-dialog-C8Oy-FBs.js → search-dialog-DG0D9KRN.js} +97 -54
  38. package/dist/server/assets/{session-export-dialog-DRVbC8Q-.js → session-export-dialog-DLPZVlQV.js} +1 -1
  39. package/dist/server/assets/{settings-dialog-CQFuAt9B.js → settings-dialog-BaGT4e5l.js} +36 -7
  40. package/dist/server/assets/{use-file-explorer-state-DMHdtb7D.js → use-file-explorer-state-DfAKF2gZ.js} +12 -0
  41. package/dist/server/server.js +2 -2
  42. package/package.json +1 -1
  43. package/dist/client/assets/_sessionKey-CaDTbjXx.js +0 -97
  44. package/dist/client/assets/agents-CGtqdyBi.js +0 -2
  45. package/dist/client/assets/agents-screen-DzIMcmHe.js +0 -1
  46. package/dist/client/assets/button-BcnCTjUT.js +0 -1
  47. package/dist/client/assets/file-explorer-screen-hjggspl-.js +0 -1
  48. package/dist/client/assets/files-BHXmS1J5.js +0 -2
  49. package/dist/client/assets/index-CxJ4zG_W.js +0 -153
  50. package/dist/client/assets/keyboard-shortcuts-dialog-DEknElTu.js +0 -1
  51. package/dist/client/assets/opencami-logo-5KynvViW.js +0 -1
  52. package/dist/client/assets/search-dialog-40gW31ca.js +0 -1
  53. package/dist/client/assets/session-export-dialog-DSRWU2YO.js +0 -1
  54. package/dist/client/assets/settings-dialog-AsvYzSBc.js +0 -1
  55. package/dist/client/assets/styles-BGTCU8mq.css +0 -1
  56. package/dist/client/assets/switch-dAvZcYA-.js +0 -1
  57. package/dist/client/assets/use-file-explorer-state-C6lX-h0n.js +0 -12
  58. package/dist/client/assets/useButton-DhneLsMA.js +0 -9
  59. package/dist/server/assets/_tanstack-start-manifest_v-C2FijMus.js +0 -4
@@ -5,9 +5,9 @@ import React__default, { memo, useDeferredValue, useState, useMemo, useCallback,
5
5
  import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
6
6
  import { T as TooltipProvider, a as TooltipRoot, b as TooltipTrigger, c as TooltipContent, s as setChatUiState, d as chatUiQueryKey, g as getChatUiState } from "./tooltip-gbV6rEVv.js";
7
7
  import { HugeiconsIcon } from "@hugeicons/react";
8
- import { Tick01Icon, Cancel01Icon, MoreHorizontalIcon, Pen01Icon, Upload01Icon, Delete01Icon, BotIcon, Clock01Icon, Chat01Icon, ArrowRight01Icon, SidebarLeft01Icon, PencilEdit02Icon, Folder01Icon, AiBrain01Icon, Search01Icon, Settings01Icon, Menu01Icon, Tick02Icon, Copy01Icon, Loading02Icon, StopIcon, VolumeHighIcon, ArrowDown01Icon, Loading03Icon, ArtificialIntelligence02Icon, CommandIcon, Attachment01Icon, File01Icon, Mic02Icon, ArrowUp02Icon } from "@hugeicons/core-free-icons";
8
+ import { Tick01Icon, Cancel01Icon, MoreHorizontalIcon, Pen01Icon, Upload01Icon, Delete01Icon, BotIcon, Clock01Icon, Chat01Icon, ArrowRight01Icon, SidebarLeft01Icon, PencilEdit02Icon, Folder01Icon, AiBrain01Icon, SmartPhone01Icon, Search01Icon, Settings01Icon, Menu01Icon, Tick02Icon, Copy01Icon, Loading02Icon, StopIcon, VolumeHighIcon, ArrowDown01Icon, Loading03Icon, ArtificialIntelligence02Icon, CommandIcon, Attachment01Icon, File01Icon, Mic02Icon, ArrowUp02Icon } from "@hugeicons/core-free-icons";
9
9
  import { motion, AnimatePresence } from "motion/react";
10
- import { D as DialogRoot, a as DialogContent, b as DialogTitle, c as DialogDescription, d as DialogClose, M as MenuRoot, e as MenuTrigger, f as MenuContent, g as MenuItem, u as useFileExplorerState } from "./use-file-explorer-state-DMHdtb7D.js";
10
+ import { D as DialogRoot, a as DialogContent, b as DialogTitle, c as DialogDescription, d as DialogClose, M as MenuRoot, e as MenuTrigger, f as MenuContent, g as MenuItem, u as useFileExplorerState } from "./use-file-explorer-state-DfAKF2gZ.js";
11
11
  import { B as Button, c as cn, b as buttonVariants } from "./button-DtQ3rV1m.js";
12
12
  import { AlertDialog } from "@base-ui/react/alert-dialog";
13
13
  import { Collapsible as Collapsible$1 } from "@base-ui/react/collapsible";
@@ -17,11 +17,11 @@ import { marked } from "marked";
17
17
  import ReactMarkdown from "react-markdown";
18
18
  import remarkBreaks from "remark-breaks";
19
19
  import remarkGfm from "remark-gfm";
20
- import { r as resolveLanguage, u as useChatSettingsStore, C as CodeBlock, a as useChatSettings$1 } from "./index-CmbNTqa2.js";
20
+ import { r as resolveLanguage, C as CodeBlock, u as useChatSettings$1 } from "./index-BNSsDaLb.js";
21
21
  import { create } from "zustand";
22
22
  import { persist } from "zustand/middleware";
23
23
  import { createPortal } from "react-dom";
24
- import { a as Route } from "./router-D5D0udHV.js";
24
+ import { a as Route } from "./router-Dme7USeO.js";
25
25
  function deriveFriendlyIdFromKey(key) {
26
26
  if (!key) return "main";
27
27
  const trimmed = key.trim();
@@ -784,7 +784,7 @@ function SessionItemComponent({
784
784
  className: "text-primary-500/70 shrink-0"
785
785
  }
786
786
  ),
787
- /* @__PURE__ */ jsxs("div", { className: "text-sm font-[450] line-clamp-1 min-w-0", children: [
787
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 truncate text-sm font-[450]", title: label, children: [
788
788
  isPinned ? /* @__PURE__ */ jsx("span", { className: "mr-1 text-xs text-primary-700", "aria-hidden": "true", children: "📌" }) : null,
789
789
  label
790
790
  ] }),
@@ -1649,10 +1649,10 @@ function useRenameSession() {
1649
1649
  return { renameSession, renaming, error };
1650
1650
  }
1651
1651
  const SettingsDialog = lazy(
1652
- () => import("./settings-dialog-CQFuAt9B.js").then((m) => ({ default: m.SettingsDialog }))
1652
+ () => import("./settings-dialog-BaGT4e5l.js").then((m) => ({ default: m.SettingsDialog }))
1653
1653
  );
1654
1654
  const SessionExportDialog = lazy(
1655
- () => import("./session-export-dialog-DRVbC8Q-.js").then((m) => ({
1655
+ () => import("./session-export-dialog-DLPZVlQV.js").then((m) => ({
1656
1656
  default: m.SessionExportDialog
1657
1657
  }))
1658
1658
  );
@@ -1934,6 +1934,48 @@ function ChatSidebarComponent({
1934
1934
  }
1935
1935
  ) }),
1936
1936
  isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Agents" })
1937
+ ] }) }),
1938
+ (() => {
1939
+ try {
1940
+ return localStorage.getItem("opencami-cron-manager") === "true";
1941
+ } catch {
1942
+ return false;
1943
+ }
1944
+ })() && /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { children: [
1945
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
1946
+ Link,
1947
+ {
1948
+ to: "/bots",
1949
+ className: cn(
1950
+ buttonVariants({ variant: "ghost", size: "sm" }),
1951
+ "w-full pl-1.5 justify-start"
1952
+ ),
1953
+ onClick: onSelectSession,
1954
+ children: [
1955
+ /* @__PURE__ */ jsx(
1956
+ HugeiconsIcon,
1957
+ {
1958
+ icon: SmartPhone01Icon,
1959
+ size: 20,
1960
+ strokeWidth: 1.5,
1961
+ className: "min-w-5"
1962
+ }
1963
+ ),
1964
+ /* @__PURE__ */ jsx(AnimatePresence, { initial: false, mode: "wait", children: !isCollapsed && /* @__PURE__ */ jsx(
1965
+ motion.span,
1966
+ {
1967
+ initial: { opacity: 0 },
1968
+ animate: { opacity: 1 },
1969
+ exit: { opacity: 0 },
1970
+ transition,
1971
+ className: "overflow-hidden whitespace-nowrap",
1972
+ children: "Cron Jobs"
1973
+ }
1974
+ ) })
1975
+ ]
1976
+ }
1977
+ ) }),
1978
+ isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Cron Jobs" })
1937
1979
  ] }) })
1938
1980
  ]
1939
1981
  }
@@ -2281,7 +2323,7 @@ function ChatHeaderComponent({
2281
2323
  "div",
2282
2324
  {
2283
2325
  ref: wrapperRef,
2284
- className: "border-b border-primary-200 px-4 h-12 flex items-center bg-surface",
2326
+ className: "border-b border-primary-200 px-4 h-12 flex min-w-0 items-center overflow-x-hidden bg-surface",
2285
2327
  children: [
2286
2328
  showSidebarButton ? /* @__PURE__ */ jsx(
2287
2329
  Button,
@@ -2294,7 +2336,7 @@ function ChatHeaderComponent({
2294
2336
  children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: Menu01Icon, size: 18, strokeWidth: 1.6 })
2295
2337
  }
2296
2338
  ) : null,
2297
- /* @__PURE__ */ jsx("div", { className: "text-sm font-medium truncate flex-1 min-w-0", children: activeTitle }),
2339
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-w-0 overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium", children: activeTitle }) }),
2298
2340
  /* @__PURE__ */ jsx(
2299
2341
  MemoizedContextMeter,
2300
2342
  {
@@ -2353,6 +2395,7 @@ function MessageActionsBar({
2353
2395
  const [audioState, setAudioState] = useState("idle");
2354
2396
  const audioRef = useRef(null);
2355
2397
  const objectUrlRef = useRef(null);
2398
+ const ttsAbortControllerRef = useRef(null);
2356
2399
  const [ttsEnabled, setTtsEnabled] = useState(() => {
2357
2400
  if (typeof window === "undefined") return true;
2358
2401
  try {
@@ -2373,6 +2416,7 @@ function MessageActionsBar({
2373
2416
  }, []);
2374
2417
  useEffect(() => {
2375
2418
  return () => {
2419
+ ttsAbortControllerRef.current?.abort();
2376
2420
  if (audioRef.current) {
2377
2421
  audioRef.current.pause();
2378
2422
  audioRef.current = null;
@@ -2394,6 +2438,7 @@ function MessageActionsBar({
2394
2438
  };
2395
2439
  const handleTTS = async () => {
2396
2440
  if (audioState === "playing") {
2441
+ ttsAbortControllerRef.current?.abort();
2397
2442
  if (audioRef.current) {
2398
2443
  audioRef.current.pause();
2399
2444
  audioRef.current = null;
@@ -2405,12 +2450,16 @@ function MessageActionsBar({
2405
2450
  setAudioState("idle");
2406
2451
  return;
2407
2452
  }
2453
+ ttsAbortControllerRef.current?.abort();
2454
+ const controller = new AbortController();
2455
+ ttsAbortControllerRef.current = controller;
2408
2456
  setAudioState("loading");
2409
2457
  try {
2410
2458
  const res = await fetch("/api/tts", {
2411
2459
  method: "POST",
2412
2460
  headers: { "Content-Type": "application/json" },
2413
- body: JSON.stringify({ text })
2461
+ body: JSON.stringify({ text }),
2462
+ signal: controller.signal
2414
2463
  });
2415
2464
  if (!res.ok) throw new Error("TTS failed");
2416
2465
  const blob = await res.blob();
@@ -2436,7 +2485,11 @@ function MessageActionsBar({
2436
2485
  };
2437
2486
  await audio.play();
2438
2487
  setAudioState("playing");
2439
- } catch {
2488
+ } catch (error) {
2489
+ if (error instanceof Error && error.name === "AbortError") {
2490
+ setAudioState("idle");
2491
+ return;
2492
+ }
2440
2493
  setAudioState("idle");
2441
2494
  if (objectUrlRef.current) {
2442
2495
  URL.revokeObjectURL(objectUrlRef.current);
@@ -2512,6 +2565,108 @@ function MessageActionsBar({
2512
2565
  }
2513
2566
  );
2514
2567
  }
2568
+ const KNOWN_FILE_EXTENSIONS = [
2569
+ "md",
2570
+ "txt",
2571
+ "json",
2572
+ "yaml",
2573
+ "yml",
2574
+ "toml",
2575
+ "py",
2576
+ "js",
2577
+ "ts",
2578
+ "tsx",
2579
+ "jsx",
2580
+ "css",
2581
+ "scss",
2582
+ "html",
2583
+ "xml",
2584
+ "sh",
2585
+ "bash",
2586
+ "go",
2587
+ "rs",
2588
+ "rb",
2589
+ "php",
2590
+ "java",
2591
+ "kt",
2592
+ "swift",
2593
+ "c",
2594
+ "cpp",
2595
+ "h",
2596
+ "hpp",
2597
+ "sql",
2598
+ "graphql",
2599
+ "dockerfile",
2600
+ "env",
2601
+ "conf",
2602
+ "cfg",
2603
+ "ini",
2604
+ "log",
2605
+ "csv"
2606
+ ];
2607
+ const EXTENSION_PATTERN = KNOWN_FILE_EXTENSIONS.join("|");
2608
+ const ABSOLUTE_OR_HOME_PATH_PATTERN = "(?:~\\/[A-Za-z0-9._\\-\\/]*[A-Za-z0-9._\\-\\/]|\\/(?:[\\w.\\-]+\\/)*[\\w.\\-]+\\/?)";
2609
+ const RELATIVE_PATH_PATTERN = "(?:[A-Za-z0-9._\\-]+(?:\\/[A-Za-z0-9._\\-]+)+\\/?)";
2610
+ const BARE_FILENAME_PATTERN = `(?:[A-Za-z0-9_-][A-Za-z0-9._-]*[A-Za-z0-9_-]\\.(?:${EXTENSION_PATTERN})|dockerfile)`;
2611
+ const FILE_PATH_REGEX = new RegExp(
2612
+ `(^|[\\s"'(,;:])(${ABSOLUTE_OR_HOME_PATH_PATTERN}|${RELATIVE_PATH_PATTERN}|${BARE_FILENAME_PATTERN})(?=$|[\\s"'),;:!?])`,
2613
+ "gi"
2614
+ );
2615
+ function trimTrailingPunctuation(path) {
2616
+ if (!path) return { path: path ?? "", trailing: "" };
2617
+ const match = path.match(/^(.*?)([),.;:!?]+)?$/);
2618
+ if (!match) return { path, trailing: "" };
2619
+ return { path: match[1] || path, trailing: match[2] || "" };
2620
+ }
2621
+ function isLikelyFilePath(text) {
2622
+ if (!text) return false;
2623
+ FILE_PATH_REGEX.lastIndex = 0;
2624
+ const match = FILE_PATH_REGEX.exec(text);
2625
+ if (!match) return false;
2626
+ const prefix = match[1] || "";
2627
+ const value = match[2] || "";
2628
+ const matchStart = match.index + prefix.length;
2629
+ const matchEnd = matchStart + value.length;
2630
+ return matchStart === 0 && matchEnd === text.length;
2631
+ }
2632
+ function splitTextByFilePaths(text) {
2633
+ if (!text) return [{ type: "text", value: text }];
2634
+ const segments = [];
2635
+ let lastIndex = 0;
2636
+ FILE_PATH_REGEX.lastIndex = 0;
2637
+ let match;
2638
+ while ((match = FILE_PATH_REGEX.exec(text)) !== null) {
2639
+ const prefix = match[1] || "";
2640
+ const rawPath = match[2] || "";
2641
+ const fullMatch = match[0];
2642
+ const start = match.index;
2643
+ const prefixStart = start;
2644
+ const pathStart = start + prefix.length;
2645
+ if (prefixStart > lastIndex) {
2646
+ segments.push({ type: "text", value: text.slice(lastIndex, prefixStart) });
2647
+ }
2648
+ if (prefix) {
2649
+ segments.push({ type: "text", value: prefix });
2650
+ }
2651
+ const { path, trailing } = trimTrailingPunctuation(rawPath);
2652
+ if (path.length > 1) {
2653
+ segments.push({ type: "path", value: path });
2654
+ if (trailing) {
2655
+ segments.push({ type: "text", value: trailing });
2656
+ }
2657
+ } else {
2658
+ segments.push({ type: "text", value: fullMatch });
2659
+ }
2660
+ lastIndex = pathStart + rawPath.length;
2661
+ }
2662
+ if (lastIndex < text.length) {
2663
+ segments.push({ type: "text", value: text.slice(lastIndex) });
2664
+ }
2665
+ return segments.length > 0 ? segments : [{ type: "text", value: text }];
2666
+ }
2667
+ function filePathToMarkdownHref(path) {
2668
+ return `openclaw-file://${encodeURIComponent(path)}`;
2669
+ }
2515
2670
  function markdownHrefToFilePath(href) {
2516
2671
  if (!href?.startsWith("openclaw-file://")) return null;
2517
2672
  try {
@@ -2520,6 +2675,39 @@ function markdownHrefToFilePath(href) {
2520
2675
  return null;
2521
2676
  }
2522
2677
  }
2678
+ function remarkFilePathLinks() {
2679
+ return (tree) => {
2680
+ function visit(node, parent) {
2681
+ if (!node) return;
2682
+ if (node.type === "text" && parent && parent.type !== "link" && parent.type !== "inlineCode" && parent.type !== "code") {
2683
+ const segments = splitTextByFilePaths(String(node.value || ""));
2684
+ const hasPaths = segments.some((segment) => segment.type === "path");
2685
+ if (!hasPaths) return;
2686
+ const replacement = segments.map((segment) => {
2687
+ if (segment.type === "text") {
2688
+ return { type: "text", value: segment.value };
2689
+ }
2690
+ return {
2691
+ type: "link",
2692
+ url: filePathToMarkdownHref(segment.value),
2693
+ children: [{ type: "text", value: segment.value }]
2694
+ };
2695
+ });
2696
+ const index = parent.children.indexOf(node);
2697
+ if (index >= 0) {
2698
+ parent.children.splice(index, 1, ...replacement);
2699
+ }
2700
+ return;
2701
+ }
2702
+ if (Array.isArray(node.children)) {
2703
+ for (const child of [...node.children]) {
2704
+ visit(child, node);
2705
+ }
2706
+ }
2707
+ }
2708
+ visit(tree, null);
2709
+ };
2710
+ }
2523
2711
  const EXTENSION_LANGUAGE_MAP = {
2524
2712
  py: "python",
2525
2713
  ts: "typescript",
@@ -2563,27 +2751,67 @@ function languageFromFilePath(path) {
2563
2751
  return resolveLanguage(mapped);
2564
2752
  }
2565
2753
  const INLINE_PREVIEW_MAX_BYTES = 100 * 1024;
2754
+ function normalizeClickedPath(path) {
2755
+ if (!path) return "/";
2756
+ return path.includes("/") ? path : `/${path}`;
2757
+ }
2758
+ function toWorkspacePath(path) {
2759
+ let p = normalizeClickedPath(path);
2760
+ const prefixes = ["/root/clawd/", "/root/"];
2761
+ for (const prefix of prefixes) {
2762
+ if (p.startsWith(prefix)) {
2763
+ p = "/" + p.slice(prefix.length);
2764
+ break;
2765
+ }
2766
+ }
2767
+ return p.startsWith("/") ? p : `/${p}`;
2768
+ }
2769
+ function hasExtension(path) {
2770
+ const trimmed = path.replace(/\/+$/, "");
2771
+ const name = trimmed.split("/").pop() || "";
2772
+ if (!name || name.startsWith(".")) return false;
2773
+ return name.includes(".");
2774
+ }
2775
+ function isDirectoryPathHeuristic(path) {
2776
+ if (!path) return false;
2777
+ return path.endsWith("/") || !hasExtension(path);
2778
+ }
2779
+ function isDirectoryError(code, message) {
2780
+ const normalizedCode = String(code || "").toUpperCase();
2781
+ const normalizedMessage = String(message || "").toLowerCase();
2782
+ return normalizedCode.includes("DIRECTORY") || normalizedMessage.includes("is a directory") || normalizedMessage.includes("directory");
2783
+ }
2566
2784
  function parseMarkdownIntoBlocks(markdown) {
2567
2785
  const tokens = marked.lexer(markdown);
2568
2786
  return tokens.map((token) => token.raw);
2569
2787
  }
2570
2788
  function extractLanguage(className) {
2571
2789
  if (!className) return "text";
2572
- const match = className.match(/language-(\w+)/);
2790
+ const match = className.match(/language-([\w-]+)/);
2573
2791
  return match ? match[1] : "text";
2574
2792
  }
2793
+ function extractFilenameFromMeta(meta) {
2794
+ const value = meta?.trim();
2795
+ if (!value) return void 0;
2796
+ const firstToken = value.split(/\s+/)[0];
2797
+ return firstToken || void 0;
2798
+ }
2575
2799
  const BASE_COMPONENTS = {
2576
- code: function CodeComponent({ className, children }) {
2800
+ code: function CodeComponent({ className, children, node }) {
2577
2801
  const isInline = !className?.includes("language-");
2578
2802
  if (isInline) {
2579
2803
  return /* @__PURE__ */ jsx("code", { className: "rounded bg-primary-100 px-1.5 py-1 text-sm font-mono text-primary-900 border border-primary-200", children });
2580
2804
  }
2581
2805
  const language = extractLanguage(className);
2806
+ const filename = extractFilenameFromMeta(
2807
+ node?.data?.meta
2808
+ );
2582
2809
  return /* @__PURE__ */ jsx(
2583
2810
  CodeBlock,
2584
2811
  {
2585
2812
  content: String(children ?? ""),
2586
2813
  language,
2814
+ filename,
2587
2815
  className: "w-full"
2588
2816
  }
2589
2817
  );
@@ -2643,13 +2871,12 @@ const BASE_COMPONENTS = {
2643
2871
  return /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-primary-950", children });
2644
2872
  }
2645
2873
  };
2646
- function createDefaultComponents(onOpenFilePreview, inlineFilePreviewEnabled) {
2647
- const FILE_PATH_RE = /^(?:~\/[\w.\-\/]+|\/(?:[\w.\-]+\/)+[\w.\-]+)$/;
2874
+ function createDefaultComponents(onOpenFilePreview) {
2648
2875
  return {
2649
2876
  ...BASE_COMPONENTS,
2650
2877
  a: function AComponent({ children, href }) {
2651
2878
  const filePath = markdownHrefToFilePath(href);
2652
- if (inlineFilePreviewEnabled && filePath) {
2879
+ if (filePath) {
2653
2880
  return /* @__PURE__ */ jsx(
2654
2881
  "button",
2655
2882
  {
@@ -2664,6 +2891,9 @@ function createDefaultComponents(onOpenFilePreview, inlineFilePreviewEnabled) {
2664
2891
  "a",
2665
2892
  {
2666
2893
  href,
2894
+ onClick: (event) => {
2895
+ if (href?.startsWith("openclaw-file://")) event.preventDefault();
2896
+ },
2667
2897
  className: "text-primary-950 underline decoration-primary-300 underline-offset-4 transition-colors hover:text-primary-950 hover:decoration-primary-500",
2668
2898
  target: "_blank",
2669
2899
  rel: "noopener noreferrer",
@@ -2671,10 +2901,24 @@ function createDefaultComponents(onOpenFilePreview, inlineFilePreviewEnabled) {
2671
2901
  }
2672
2902
  );
2673
2903
  },
2674
- code: function InlineCodeComponent({ children, className }) {
2675
- if (className) return /* @__PURE__ */ jsx("code", { className, children });
2904
+ code: function InlineCodeComponent({ children, className, node }) {
2905
+ if (className?.includes("language-")) {
2906
+ const language = extractLanguage(className);
2907
+ const filename = extractFilenameFromMeta(
2908
+ node?.data?.meta
2909
+ );
2910
+ return /* @__PURE__ */ jsx(
2911
+ CodeBlock,
2912
+ {
2913
+ content: String(children ?? ""),
2914
+ language,
2915
+ filename,
2916
+ className: "w-full"
2917
+ }
2918
+ );
2919
+ }
2676
2920
  const text = typeof children === "string" ? children : Array.isArray(children) ? children.filter((c) => typeof c === "string").join("") : String(children ?? "");
2677
- if (inlineFilePreviewEnabled && text && FILE_PATH_RE.test(text)) {
2921
+ if (text && isLikelyFilePath(text)) {
2678
2922
  return /* @__PURE__ */ jsx(
2679
2923
  "button",
2680
2924
  {
@@ -2697,7 +2941,7 @@ const MemoizedMarkdownBlock = memo(
2697
2941
  return /* @__PURE__ */ jsx(
2698
2942
  ReactMarkdown,
2699
2943
  {
2700
- remarkPlugins: [remarkGfm, remarkBreaks],
2944
+ remarkPlugins: [remarkGfm, remarkBreaks, remarkFilePathLinks],
2701
2945
  components,
2702
2946
  children: content
2703
2947
  }
@@ -2724,15 +2968,24 @@ function MarkdownComponent({
2724
2968
  const blockId = id ?? generatedId;
2725
2969
  const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]);
2726
2970
  const [filePreview, setFilePreview] = useState({ status: "idle" });
2727
- const inlineFilePreviewEnabled = useChatSettingsStore(
2728
- (state) => state.settings.inlineFilePreview
2729
- );
2971
+ const navigate = useNavigate();
2972
+ const openDirectoryInExplorer = useCallback((path) => {
2973
+ const workspacePath = toWorkspacePath(path);
2974
+ useFileExplorerState.getState().navigateTo(workspacePath);
2975
+ setFilePreview({ status: "idle" });
2976
+ navigate({ to: "/files" });
2977
+ }, [navigate]);
2978
+ const onOpenFilePreview = useCallback((path) => {
2979
+ const resolvedPath = normalizeClickedPath(path);
2980
+ if (isDirectoryPathHeuristic(resolvedPath)) {
2981
+ openDirectoryInExplorer(resolvedPath);
2982
+ return;
2983
+ }
2984
+ setFilePreview({ status: "loading", path: resolvedPath });
2985
+ }, [openDirectoryInExplorer]);
2730
2986
  const defaultComponents = useMemo(
2731
- () => createDefaultComponents(
2732
- (path) => setFilePreview({ status: "loading", path }),
2733
- inlineFilePreviewEnabled
2734
- ),
2735
- [inlineFilePreviewEnabled]
2987
+ () => createDefaultComponents(onOpenFilePreview),
2988
+ [onOpenFilePreview]
2736
2989
  );
2737
2990
  const mergedComponents = useMemo(
2738
2991
  () => ({ ...defaultComponents, ...components || {} }),
@@ -2747,6 +3000,10 @@ function MarkdownComponent({
2747
3000
  }).then(async (response) => {
2748
3001
  const payload = await response.json().catch(() => ({}));
2749
3002
  if (!response.ok) {
3003
+ if (isDirectoryError(payload.code, payload.message)) {
3004
+ openDirectoryInExplorer(path);
3005
+ return;
3006
+ }
2750
3007
  const error = fileErrorMessageFromResponse(response.status, payload.code);
2751
3008
  setFilePreview({ status: "error", path, message: error });
2752
3009
  return;
@@ -2768,17 +3025,33 @@ function MarkdownComponent({
2768
3025
  setFilePreview({ status: "error", path, message: "Failed to load file preview" });
2769
3026
  });
2770
3027
  return () => controller.abort();
2771
- }, [filePreview]);
3028
+ }, [filePreview, openDirectoryInExplorer]);
2772
3029
  const previewOpen = filePreview.status !== "idle";
2773
3030
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2774
- /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-2", className), children: blocks.map((block, index) => /* @__PURE__ */ jsx(
2775
- MemoizedMarkdownBlock,
3031
+ /* @__PURE__ */ jsx(
3032
+ "div",
2776
3033
  {
2777
- content: block,
2778
- components: mergedComponents
2779
- },
2780
- `${blockId}-block-${index}`
2781
- )) }),
3034
+ className: cn("flex min-w-0 max-w-full flex-col gap-2 overflow-x-hidden", className),
3035
+ onClickCapture: (event) => {
3036
+ const target = event.target;
3037
+ const anchor = target?.closest?.('a[href^="openclaw-file://"]');
3038
+ if (!anchor) return;
3039
+ const filePath = markdownHrefToFilePath(anchor.getAttribute("href") ?? void 0);
3040
+ if (!filePath) return;
3041
+ event.preventDefault();
3042
+ event.stopPropagation();
3043
+ onOpenFilePreview(filePath);
3044
+ },
3045
+ children: blocks.map((block, index) => /* @__PURE__ */ jsx(
3046
+ MemoizedMarkdownBlock,
3047
+ {
3048
+ content: block,
3049
+ components: mergedComponents
3050
+ },
3051
+ `${blockId}-block-${index}`
3052
+ ))
3053
+ }
3054
+ ),
2782
3055
  /* @__PURE__ */ jsx(
2783
3056
  DialogRoot,
2784
3057
  {
@@ -2798,21 +3071,28 @@ function MarkdownComponent({
2798
3071
  {
2799
3072
  to: "/files",
2800
3073
  onClick: () => {
2801
- let p = filePreview.status !== "idle" ? filePreview.path : "";
3074
+ const p = filePreview.status !== "idle" ? toWorkspacePath(filePreview.path) : "";
2802
3075
  if (p) {
2803
- const prefixes = ["/root/clawd/", "/root/"];
2804
- for (const prefix of prefixes) {
2805
- if (p.startsWith(prefix)) {
2806
- p = "/" + p.slice(prefix.length);
2807
- break;
2808
- }
2809
- }
2810
3076
  const dir = p.includes("/") ? p.slice(0, p.lastIndexOf("/")) || "/" : "/";
2811
3077
  useFileExplorerState.getState().navigateTo(dir);
2812
3078
  }
2813
3079
  setFilePreview({ status: "idle" });
2814
3080
  },
2815
- children: "Open in File Explorer"
3081
+ children: "Open in Explorer"
3082
+ }
3083
+ ) }),
3084
+ filePreview.status !== "idle" && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: /* @__PURE__ */ jsx(
3085
+ Link,
3086
+ {
3087
+ to: "/files",
3088
+ onClick: () => {
3089
+ const p = filePreview.status !== "idle" ? toWorkspacePath(filePreview.path) : "";
3090
+ if (p) {
3091
+ useFileExplorerState.getState().openInEditor(p);
3092
+ }
3093
+ setFilePreview({ status: "idle" });
3094
+ },
3095
+ children: "Open in Editor"
2816
3096
  }
2817
3097
  ) }),
2818
3098
  /* @__PURE__ */ jsx(DialogClose, { children: "Close" })
@@ -2847,7 +3127,7 @@ function MessageContent({
2847
3127
  ...props
2848
3128
  }) {
2849
3129
  const classNames = cn(
2850
- "rounded-[12px] break-words whitespace-normal min-w-0",
3130
+ "rounded-[12px] break-words whitespace-normal min-w-0 max-w-full overflow-x-hidden",
2851
3131
  className
2852
3132
  );
2853
3133
  return markdown ? /* @__PURE__ */ jsx(Markdown, { className: classNames, ...props, children }) : /* @__PURE__ */ jsx("div", { className: classNames, ...props, children });
@@ -3214,19 +3494,19 @@ function MessageItemComponent({
3214
3494
  },
3215
3495
  idx
3216
3496
  )) }),
3217
- /* @__PURE__ */ jsx(Message, { className: cn(isUser ? "flex-row-reverse" : ""), children: /* @__PURE__ */ jsx(
3497
+ /* @__PURE__ */ jsx(Message, { className: cn("min-w-0 max-w-full", isUser ? "flex-row-reverse" : ""), children: /* @__PURE__ */ jsx(
3218
3498
  MessageContent,
3219
3499
  {
3220
3500
  markdown: !isUser,
3221
3501
  className: cn(
3222
- "text-primary-900 opencami-text-size",
3502
+ "text-primary-900 opencami-text-size min-w-0 max-w-full",
3223
3503
  !isUser ? "bg-transparent w-full" : "bg-primary-100 px-4 py-2.5 max-w-[85%]",
3224
3504
  !isUser && isStreaming && "stream-fade-in"
3225
3505
  ),
3226
3506
  children: text
3227
3507
  }
3228
3508
  ) }),
3229
- hasToolCalls && settings.showToolMessages && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[900px] mt-2 flex flex-col gap-3", children: toolCalls.map((toolCall) => {
3509
+ hasToolCalls && settings.showToolMessages && /* @__PURE__ */ jsx("div", { className: "mt-2 flex w-full min-w-0 max-w-[900px] flex-col gap-3 overflow-x-hidden", children: toolCalls.map((toolCall) => {
3230
3510
  const resultMessage = toolCall.id ? toolResultsByCallId?.get(toolCall.id) : void 0;
3231
3511
  const toolPart = mapToolCallToToolPart(toolCall, resultMessage);
3232
3512
  return /* @__PURE__ */ jsx(
@@ -3843,12 +4123,12 @@ function ChatContainerShell({
3843
4123
  /* @__PURE__ */ jsx(
3844
4124
  ScrollAreaViewport,
3845
4125
  {
3846
- className: "relative will-change-transform",
4126
+ className: "relative will-change-transform overflow-x-hidden",
3847
4127
  ref: viewportRef,
3848
4128
  ...viewportProps
3849
4129
  }
3850
4130
  ),
3851
- /* @__PURE__ */ jsx("div", { className: "relative mx-auto w-full max-w-full px-5 sm:max-w-[768px] sm:min-w-[400px] ", children: /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute bottom-10 right-10 z-50", children: /* @__PURE__ */ jsx(ScrollButton, { scrollRef }) }) }),
4131
+ /* @__PURE__ */ jsx("div", { className: "relative mx-auto w-full min-w-0 max-w-full px-5 sm:max-w-[768px]", children: /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute bottom-10 right-10 z-50", children: /* @__PURE__ */ jsx(ScrollButton, { scrollRef }) }) }),
3852
4132
  /* @__PURE__ */ jsx(ScrollAreaScrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollAreaThumb, {}) }),
3853
4133
  /* @__PURE__ */ jsx(ScrollAreaCorner, {})
3854
4134
  ]
@@ -3886,7 +4166,7 @@ function ChatContainerPortal({
3886
4166
  }) {
3887
4167
  if (!viewportNode) return null;
3888
4168
  return createPortal(
3889
- /* @__PURE__ */ jsx("div", { className: "relative flex w-full flex-col", children }),
4169
+ /* @__PURE__ */ jsx("div", { className: "relative flex w-full min-w-0 max-w-full flex-col overflow-x-hidden", children }),
3890
4170
  viewportNode
3891
4171
  );
3892
4172
  }
@@ -3935,9 +4215,9 @@ function ChatContainerContent({
3935
4215
  return /* @__PURE__ */ jsx(
3936
4216
  "div",
3937
4217
  {
3938
- className: cn("flex w-full flex-col min-h-full", className),
4218
+ className: cn("flex w-full min-w-0 max-w-full flex-col min-h-full overflow-x-hidden", className),
3939
4219
  ...props,
3940
- children: /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-full px-2 md:px-5 sm:max-w-[768px] sm:min-w-[400px] flex flex-col flex-1 min-h-full", children: /* @__PURE__ */ jsx("div", { className: "flex flex-col space-y-3 md:space-y-6", children }) })
4220
+ children: /* @__PURE__ */ jsx("div", { className: "mx-auto w-full min-w-0 max-w-full px-2 md:px-5 sm:max-w-[768px] flex flex-col flex-1 min-h-full overflow-x-hidden", children: /* @__PURE__ */ jsx("div", { className: "flex min-w-0 max-w-full flex-col space-y-3 md:space-y-6", children }) })
3941
4221
  }
3942
4222
  );
3943
4223
  }
@@ -4448,13 +4728,17 @@ function ModelSelector({ className, onModelChange }) {
4448
4728
  const [selectedModel, setSelectedModel] = useState("");
4449
4729
  const [isLoading, setIsLoading] = useState(true);
4450
4730
  const [error, setError] = useState(null);
4731
+ const abortControllerRef = useRef(null);
4451
4732
  useEffect(() => {
4452
4733
  let mounted = true;
4453
4734
  async function fetchModels() {
4735
+ abortControllerRef.current?.abort();
4736
+ const controller = new AbortController();
4737
+ abortControllerRef.current = controller;
4454
4738
  try {
4455
4739
  setIsLoading(true);
4456
4740
  setError(null);
4457
- const response = await fetch("/api/models");
4741
+ const response = await fetch("/api/models", { signal: controller.signal });
4458
4742
  if (!response.ok) {
4459
4743
  throw new Error("Failed to fetch models");
4460
4744
  }
@@ -4470,6 +4754,7 @@ function ModelSelector({ className, onModelChange }) {
4470
4754
  throw new Error("No models available");
4471
4755
  }
4472
4756
  } catch (err) {
4757
+ if (err instanceof Error && err.name === "AbortError") return;
4473
4758
  if (!mounted) return;
4474
4759
  console.error("[model-selector] Error fetching models:", err);
4475
4760
  setError(err instanceof Error ? err.message : "Failed to load models");
@@ -4484,6 +4769,7 @@ function ModelSelector({ className, onModelChange }) {
4484
4769
  fetchModels();
4485
4770
  return () => {
4486
4771
  mounted = false;
4772
+ abortControllerRef.current?.abort();
4487
4773
  };
4488
4774
  }, [onModelChange]);
4489
4775
  function handleModelSelect(modelId) {
@@ -4598,6 +4884,7 @@ function PersonaPicker({ className, onSelect }) {
4598
4884
  const [isLoading, setIsLoading] = useState(true);
4599
4885
  const [available, setAvailable] = useState(false);
4600
4886
  const [enabled, setEnabled] = useState(isPersonasEnabled);
4887
+ const abortControllerRef = useRef(null);
4601
4888
  useEffect(() => {
4602
4889
  const handleStorage = (e) => {
4603
4890
  if (e.key === PERSONAS_ENABLED_KEY) {
@@ -4610,9 +4897,12 @@ function PersonaPicker({ className, onSelect }) {
4610
4897
  useEffect(() => {
4611
4898
  let mounted = true;
4612
4899
  async function fetchPersonas() {
4900
+ abortControllerRef.current?.abort();
4901
+ const controller = new AbortController();
4902
+ abortControllerRef.current = controller;
4613
4903
  try {
4614
4904
  setIsLoading(true);
4615
- const response = await fetch("/api/personas");
4905
+ const response = await fetch("/api/personas", { signal: controller.signal });
4616
4906
  if (!response.ok) {
4617
4907
  setAvailable(false);
4618
4908
  return;
@@ -4626,7 +4916,8 @@ function PersonaPicker({ className, onSelect }) {
4626
4916
  } else {
4627
4917
  setAvailable(false);
4628
4918
  }
4629
- } catch {
4919
+ } catch (error) {
4920
+ if (error instanceof Error && error.name === "AbortError") return;
4630
4921
  if (!mounted) return;
4631
4922
  setAvailable(false);
4632
4923
  } finally {
@@ -4638,6 +4929,7 @@ function PersonaPicker({ className, onSelect }) {
4638
4929
  fetchPersonas();
4639
4930
  return () => {
4640
4931
  mounted = false;
4932
+ abortControllerRef.current?.abort();
4641
4933
  };
4642
4934
  }, []);
4643
4935
  const handleSelectPersona = useCallback(
@@ -5250,6 +5542,7 @@ function ChatComposerComponent({
5250
5542
  const mediaRecorderRef = useRef(null);
5251
5543
  const recordingChunksRef = useRef([]);
5252
5544
  const recordingTimerRef = useRef(null);
5545
+ const sttAbortControllerRef = useRef(null);
5253
5546
  const webSpeechRef = useRef(null);
5254
5547
  const promptRef = useRef(null);
5255
5548
  const showSlashCommands = useMemo(() => /^\/\S*$/.test(value) && !slashMenuDismissed, [value, slashMenuDismissed]);
@@ -5401,6 +5694,7 @@ function ChatComposerComponent({
5401
5694
  );
5402
5695
  useEffect(() => {
5403
5696
  return () => {
5697
+ sttAbortControllerRef.current?.abort();
5404
5698
  if (recordingTimerRef.current) clearInterval(recordingTimerRef.current);
5405
5699
  if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
5406
5700
  mediaRecorderRef.current.stop();
@@ -5497,7 +5791,10 @@ function ChatComposerComponent({
5497
5791
  const formData = new FormData();
5498
5792
  formData.append("audio", audioBlob, `recording.${mimeType === "audio/webm" ? "webm" : "mp4"}`);
5499
5793
  if (provider !== "auto") formData.append("provider", provider);
5500
- const res = await fetch("/api/stt", { method: "POST", body: formData });
5794
+ sttAbortControllerRef.current?.abort();
5795
+ const controller = new AbortController();
5796
+ sttAbortControllerRef.current = controller;
5797
+ const res = await fetch("/api/stt", { method: "POST", body: formData, signal: controller.signal });
5501
5798
  const data = await res.json();
5502
5799
  if (data.ok && data.text) {
5503
5800
  setValue((prev) => prev + (prev ? " " : "") + data.text);
@@ -5507,6 +5804,7 @@ function ChatComposerComponent({
5507
5804
  alert(data.error || "Speech-to-text failed. Try the Browser provider in Settings.");
5508
5805
  }
5509
5806
  } catch (err) {
5807
+ if (err instanceof Error && err.name === "AbortError") return;
5510
5808
  console.warn("STT request failed:", err);
5511
5809
  alert("Could not reach speech-to-text service.");
5512
5810
  } finally {
@@ -6236,12 +6534,12 @@ function useSwipeGesture(options) {
6236
6534
  };
6237
6535
  }
6238
6536
  const KeyboardShortcutsDialog = lazy(
6239
- () => import("./keyboard-shortcuts-dialog-7OEtXUlW.js").then((m) => ({
6537
+ () => import("./keyboard-shortcuts-dialog-CqIm8aYF.js").then((m) => ({
6240
6538
  default: m.KeyboardShortcutsDialog
6241
6539
  }))
6242
6540
  );
6243
6541
  const SearchDialog = lazy(
6244
- () => import("./search-dialog-C8Oy-FBs.js").then((m) => ({
6542
+ () => import("./search-dialog-DG0D9KRN.js").then((m) => ({
6245
6543
  default: m.SearchDialog
6246
6544
  }))
6247
6545
  );
@@ -6916,7 +7214,7 @@ function ChatScreen({
6916
7214
  {
6917
7215
  className: cn(
6918
7216
  "h-full overflow-hidden",
6919
- isMobile ? "relative" : "grid grid-cols-[auto_1fr]"
7217
+ isMobile ? "relative" : "grid grid-cols-[auto_minmax(0,1fr)]"
6920
7218
  ),
6921
7219
  children: [
6922
7220
  hideUi ? null : isMobile ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -6946,7 +7244,7 @@ function ChatScreen({
6946
7244
  /* @__PURE__ */ jsxs(
6947
7245
  "main",
6948
7246
  {
6949
- className: "flex flex-col h-full min-h-0",
7247
+ className: "flex flex-col h-full min-h-0 min-w-0 overflow-x-hidden",
6950
7248
  ref: mainRef,
6951
7249
  ...sidebarSwipeHandlers,
6952
7250
  children: [