opencami 1.5.1 → 1.6.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 (79) hide show
  1. package/README.md +4 -1
  2. package/dist/client/assets/{CSPContext-BBLAL_m_.js → CSPContext-Bq8j4nl9.js} +1 -1
  3. package/dist/client/assets/DirectionContext-BdX86BHP.js +1 -0
  4. package/dist/client/assets/_sessionKey-DsjnpErt.js +14 -0
  5. package/dist/client/assets/agents-DwxKcpP6.js +2 -0
  6. package/dist/client/assets/agents-screen-DwIY8hze.js +1 -0
  7. package/dist/client/assets/bots-CRlm-3-d.js +2 -0
  8. package/dist/client/assets/bots-screen-c78I920d.js +1 -0
  9. package/dist/client/assets/button-Dg7VFQQn.js +1 -0
  10. package/dist/client/assets/composite-DBl8R3ae.js +1 -0
  11. package/dist/client/assets/{connect-DHr3hhUR.js → connect-NYvOqiBJ.js} +1 -1
  12. package/dist/client/assets/file-explorer-screen-BSMbs0vi.js +1 -0
  13. package/dist/client/assets/files-BJbMx0_w.js +2 -0
  14. package/dist/client/assets/{index-B2iG4EM1.js → index-CMATW8VA.js} +1 -1
  15. package/dist/client/assets/{index-GTR-Xzl2.js → index-rOIRO-8E.js} +1 -1
  16. package/dist/client/assets/keyboard-shortcuts-dialog-BTGWdJMl.js +1 -0
  17. package/dist/client/assets/{main-mIHr_ble.js → main-B_dlfHME.js} +9 -9
  18. package/dist/client/assets/markdown-BVzT7z4x.js +87 -0
  19. package/dist/client/assets/memory-S3Yws6a5.js +2 -0
  20. package/dist/client/assets/memory-screen-C-Z9o31m.js +1 -0
  21. package/dist/client/assets/menu-DHNgWk_8.js +1 -0
  22. package/dist/client/assets/{opencami-logo-CRIdKbbZ.js → opencami-logo-BQQETnJG.js} +1 -1
  23. package/dist/client/assets/owner-CpRnf1fI.js +1 -0
  24. package/dist/client/assets/popupStateMapping-BRPDXnjv.js +1 -0
  25. package/dist/client/assets/proxy-BcUh9kMA.js +9 -0
  26. package/dist/client/assets/{react-Cfq4ot0g.js → react-irH8OzhB.js} +1 -1
  27. package/dist/client/assets/search-dialog-B96zx_ng.js +1 -0
  28. package/dist/client/assets/session-export-dialog-DPuHnhgv.js +1 -0
  29. package/dist/client/assets/settings-dialog-DZcRCaPj.js +1 -0
  30. package/dist/client/assets/skills-YZe3I63y.js +2 -0
  31. package/dist/client/assets/{skills-panel-Cv-N_MDk.js → skills-panel-WDUfIwnI.js} +2 -2
  32. package/dist/client/assets/styles-Bwo-K6Y4.css +1 -0
  33. package/dist/client/assets/{switch-Bh9tVOYh.js → switch-DPocNFRG.js} +1 -1
  34. package/dist/client/assets/tabs-B0cro1hL.js +1 -0
  35. package/dist/client/assets/tooltip-Dg9fy-vT.js +1 -0
  36. package/dist/client/assets/use-file-explorer-state-DzT0bksg.js +12 -0
  37. package/dist/client/assets/{useButton-DsMdJPGn.js → useButton-Cbl_9oFG.js} +1 -1
  38. package/dist/client/assets/useCompositeItem-BDAzTxVe.js +1 -0
  39. package/dist/client/assets/{useControlled-wOKVgKF4.js → useControlled-Dscz_s4f.js} +1 -1
  40. package/dist/client/assets/{useMutation-fJnleJAb.js → useMutation-B1FlDsNN.js} +1 -1
  41. package/dist/client/assets/visuallyHidden-ONmQ-0U2.js +1 -0
  42. package/dist/server/assets/{_sessionKey-B5UHBd2U.js → _sessionKey-B0ZlLAjH.js} +172 -567
  43. package/dist/server/assets/_tanstack-start-manifest_v-D5UVTs1o.js +4 -0
  44. package/dist/server/assets/{file-explorer-screen-CVlFiAFu.js → file-explorer-screen-DH4UFK03.js} +3 -2
  45. package/dist/server/assets/{files-BIEcSPGp.js → files-DYdXlQDr.js} +1 -1
  46. package/dist/server/assets/{index--_jH_0mX.js → index-CiUjUD0t.js} +1 -1
  47. package/dist/server/assets/{keyboard-shortcuts-dialog-CsNP85q8.js → keyboard-shortcuts-dialog-Cr6fOqHz.js} +1 -2
  48. package/dist/server/assets/markdown-BFE5y9YH.js +565 -0
  49. package/dist/server/assets/memory-BqZOoD7Q.js +11 -0
  50. package/dist/server/assets/memory-screen-BK5phS8K.js +235 -0
  51. package/dist/server/assets/menu-D90CDTi2.js +45 -0
  52. package/dist/server/assets/{router-DJA7GtMo.js → router-Uuagl6O7.js} +55 -45
  53. package/dist/server/assets/{search-dialog-C2a3OYm_.js → search-dialog-DZTS5SEi.js} +6 -4
  54. package/dist/server/assets/{session-export-dialog-CwclV0Aj.js → session-export-dialog-C53RRAah.js} +1 -2
  55. package/dist/server/assets/{settings-dialog-CHVzrou9.js → settings-dialog-CSYDj2qm.js} +75 -20
  56. package/dist/server/assets/{use-file-explorer-state-Il1LlBAe.js → use-file-explorer-state-s7CS50ho.js} +0 -41
  57. package/dist/server/server.js +2 -2
  58. package/package.json +1 -1
  59. package/dist/client/assets/DirectionContext-DXnZc0zz.js +0 -1
  60. package/dist/client/assets/_sessionKey-BidmO1-D.js +0 -100
  61. package/dist/client/assets/agents-CtZs_u1j.js +0 -2
  62. package/dist/client/assets/agents-screen-Basce5qo.js +0 -1
  63. package/dist/client/assets/bots-C_dWjy3z.js +0 -2
  64. package/dist/client/assets/bots-screen-n_xhYOEE.js +0 -1
  65. package/dist/client/assets/button-BaHefIXU.js +0 -1
  66. package/dist/client/assets/file-explorer-screen-8t6M4Xco.js +0 -1
  67. package/dist/client/assets/files-BdlpK3Cy.js +0 -2
  68. package/dist/client/assets/keyboard-shortcuts-dialog-CcKSlK52.js +0 -1
  69. package/dist/client/assets/search-dialog-D19x_xaG.js +0 -1
  70. package/dist/client/assets/session-export-dialog-DRlJwhMa.js +0 -1
  71. package/dist/client/assets/settings-dialog-BA5FjiyP.js +0 -1
  72. package/dist/client/assets/skills-lmNPZksG.js +0 -2
  73. package/dist/client/assets/styles-JgjN_ZCd.css +0 -1
  74. package/dist/client/assets/tabs-BfaEc9zS.js +0 -1
  75. package/dist/client/assets/tooltip-w9D-e_R-.js +0 -1
  76. package/dist/client/assets/use-file-explorer-state-CLaDuI9X.js +0 -12
  77. package/dist/client/assets/useCompositeItem-CaYygSfB.js +0 -1
  78. package/dist/client/assets/visuallyHidden-CqGRL_Oq.js +0 -9
  79. package/dist/server/assets/_tanstack-start-manifest_v-D11xMFUx.js +0 -4
@@ -1,27 +1,25 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { Link, useNavigate } from "@tanstack/react-router";
3
3
  import * as React from "react";
4
- import React__default, { memo, useDeferredValue, useState, useMemo, useCallback, Suspense, lazy, useRef, useEffect, useId, useLayoutEffect, createContext, useContext } from "react";
4
+ import React__default, { memo, useDeferredValue, useState, useMemo, useCallback, Suspense, lazy, useRef, useEffect, useLayoutEffect, createContext, useContext } from "react";
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-DgsSPocE.js";
7
7
  import { HugeiconsIcon } from "@hugeicons/react";
8
- import { Tick01Icon, Cancel01Icon, MoreHorizontalIcon, Pen01Icon, Upload01Icon, Delete01Icon, BotIcon, Clock01Icon, Chat01Icon, ArrowRight01Icon, SidebarLeft01Icon, PencilEdit02Icon, Folder01Icon, PackageOpenIcon, AiBrain01Icon, SmartPhone01Icon, 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, PackageOpenIcon, 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-Il1LlBAe.js";
10
+ import { D as DialogRoot, a as DialogContent, b as DialogTitle, c as DialogDescription, d as DialogClose } from "./use-file-explorer-state-s7CS50ho.js";
11
11
  import { B as Button, c as cn, b as buttonVariants } from "./button-CwY2OHFj.js";
12
12
  import { AlertDialog } from "@base-ui/react/alert-dialog";
13
13
  import { Collapsible as Collapsible$1 } from "@base-ui/react/collapsible";
14
14
  import { ScrollArea } from "@base-ui/react/scroll-area";
15
+ import { M as MenuRoot, a as MenuTrigger, b as MenuContent, c as MenuItem } from "./menu-D90CDTi2.js";
15
16
  import { O as OpenCamiLogo, a as OpenCamiText } from "./opencami-logo-C-43FL3R.js";
16
- import { marked } from "marked";
17
- import ReactMarkdown from "react-markdown";
18
- import remarkBreaks from "remark-breaks";
19
- import remarkGfm from "remark-gfm";
20
- import { r as resolveLanguage, C as CodeBlock, u as useChatSettings$1 } from "./index-Dl2BOKP7.js";
17
+ import { M as Markdown } from "./markdown-BFE5y9YH.js";
18
+ import { u as useChatSettings$1 } from "./index-Dl2BOKP7.js";
21
19
  import { create } from "zustand";
22
20
  import { persist } from "zustand/middleware";
23
21
  import { createPortal } from "react-dom";
24
- import { a as Route } from "./router-DJA7GtMo.js";
22
+ import { a as Route } from "./router-Uuagl6O7.js";
25
23
  function deriveFriendlyIdFromKey(key) {
26
24
  if (!key) return "main";
27
25
  const trimmed = key.trim();
@@ -1664,10 +1662,10 @@ function useRenameSession() {
1664
1662
  return { renameSession, renaming, error };
1665
1663
  }
1666
1664
  const SettingsDialog = lazy(
1667
- () => import("./settings-dialog-CHVzrou9.js").then((m) => ({ default: m.SettingsDialog }))
1665
+ () => import("./settings-dialog-CSYDj2qm.js").then((m) => ({ default: m.SettingsDialog }))
1668
1666
  );
1669
1667
  const SessionExportDialog = lazy(
1670
- () => import("./session-export-dialog-CwclV0Aj.js").then((m) => ({
1668
+ () => import("./session-export-dialog-C53RRAah.js").then((m) => ({
1671
1669
  default: m.SessionExportDialog
1672
1670
  }))
1673
1671
  );
@@ -1872,7 +1870,14 @@ function ChatSidebarComponent({
1872
1870
  ]
1873
1871
  }
1874
1872
  ),
1875
- /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { children: [
1873
+ (() => {
1874
+ try {
1875
+ const value = localStorage.getItem("opencami-file-explorer");
1876
+ return value === null ? true : value === "true";
1877
+ } catch {
1878
+ return true;
1879
+ }
1880
+ })() && /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { children: [
1876
1881
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
1877
1882
  Link,
1878
1883
  {
@@ -1908,6 +1913,49 @@ function ChatSidebarComponent({
1908
1913
  ) }),
1909
1914
  isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Files" })
1910
1915
  ] }) }),
1916
+ (() => {
1917
+ try {
1918
+ const value = localStorage.getItem("opencami-memory-viewer");
1919
+ return value === null ? true : value === "true";
1920
+ } catch {
1921
+ return true;
1922
+ }
1923
+ })() && /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { children: [
1924
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
1925
+ Link,
1926
+ {
1927
+ to: "/memory",
1928
+ className: cn(
1929
+ buttonVariants({ variant: "ghost", size: "sm" }),
1930
+ "w-full pl-1.5 justify-start"
1931
+ ),
1932
+ onClick: onSelectSession,
1933
+ children: [
1934
+ /* @__PURE__ */ jsx(
1935
+ HugeiconsIcon,
1936
+ {
1937
+ icon: AiBrain01Icon,
1938
+ size: 20,
1939
+ strokeWidth: 1.5,
1940
+ className: "min-w-5"
1941
+ }
1942
+ ),
1943
+ /* @__PURE__ */ jsx(AnimatePresence, { initial: false, mode: "wait", children: !isCollapsed && /* @__PURE__ */ jsx(
1944
+ motion.span,
1945
+ {
1946
+ initial: { opacity: 0 },
1947
+ animate: { opacity: 1 },
1948
+ exit: { opacity: 0 },
1949
+ transition,
1950
+ className: "overflow-hidden whitespace-nowrap",
1951
+ children: "Memory"
1952
+ }
1953
+ ) })
1954
+ ]
1955
+ }
1956
+ ) }),
1957
+ isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Memory" })
1958
+ ] }) }),
1911
1959
  (() => {
1912
1960
  try {
1913
1961
  return localStorage.getItem("opencami-skills-browser") === "true";
@@ -2624,558 +2672,6 @@ function MessageActionsBar({
2624
2672
  }
2625
2673
  );
2626
2674
  }
2627
- const KNOWN_FILE_EXTENSIONS = [
2628
- "md",
2629
- "txt",
2630
- "json",
2631
- "yaml",
2632
- "yml",
2633
- "toml",
2634
- "py",
2635
- "js",
2636
- "ts",
2637
- "tsx",
2638
- "jsx",
2639
- "css",
2640
- "scss",
2641
- "html",
2642
- "xml",
2643
- "sh",
2644
- "bash",
2645
- "go",
2646
- "rs",
2647
- "rb",
2648
- "php",
2649
- "java",
2650
- "kt",
2651
- "swift",
2652
- "c",
2653
- "cpp",
2654
- "h",
2655
- "hpp",
2656
- "sql",
2657
- "graphql",
2658
- "dockerfile",
2659
- "env",
2660
- "conf",
2661
- "cfg",
2662
- "ini",
2663
- "log",
2664
- "csv"
2665
- ];
2666
- const EXTENSION_PATTERN = KNOWN_FILE_EXTENSIONS.join("|");
2667
- const ABSOLUTE_OR_HOME_PATH_PATTERN = "(?:~\\/[A-Za-z0-9._\\-\\/]*[A-Za-z0-9._\\-\\/]|\\/(?:[\\w.\\-]+\\/)*[\\w.\\-]+\\/?)";
2668
- const RELATIVE_PATH_PATTERN = "(?:[A-Za-z0-9._\\-]+(?:\\/[A-Za-z0-9._\\-]+)+\\/?)";
2669
- const BARE_FILENAME_PATTERN = `(?:[A-Za-z0-9_-][A-Za-z0-9._-]*[A-Za-z0-9_-]\\.(?:${EXTENSION_PATTERN})|dockerfile)`;
2670
- const FILE_PATH_REGEX = new RegExp(
2671
- `(^|[\\s"'(,;:])(${ABSOLUTE_OR_HOME_PATH_PATTERN}|${RELATIVE_PATH_PATTERN}|${BARE_FILENAME_PATTERN})(?=$|[\\s"'),;:!?])`,
2672
- "gi"
2673
- );
2674
- function trimTrailingPunctuation(path) {
2675
- if (!path) return { path: path ?? "", trailing: "" };
2676
- const match = path.match(/^(.*?)([),.;:!?]+)?$/);
2677
- if (!match) return { path, trailing: "" };
2678
- return { path: match[1] || path, trailing: match[2] || "" };
2679
- }
2680
- function isLikelyFilePath(text) {
2681
- if (!text) return false;
2682
- FILE_PATH_REGEX.lastIndex = 0;
2683
- const match = FILE_PATH_REGEX.exec(text);
2684
- if (!match) return false;
2685
- const prefix = match[1] || "";
2686
- const value = match[2] || "";
2687
- const matchStart = match.index + prefix.length;
2688
- const matchEnd = matchStart + value.length;
2689
- return matchStart === 0 && matchEnd === text.length;
2690
- }
2691
- function splitTextByFilePaths(text) {
2692
- if (!text) return [{ type: "text", value: text }];
2693
- const segments = [];
2694
- let lastIndex = 0;
2695
- FILE_PATH_REGEX.lastIndex = 0;
2696
- let match;
2697
- while ((match = FILE_PATH_REGEX.exec(text)) !== null) {
2698
- const prefix = match[1] || "";
2699
- const rawPath = match[2] || "";
2700
- const fullMatch = match[0];
2701
- const start = match.index;
2702
- const prefixStart = start;
2703
- const pathStart = start + prefix.length;
2704
- if (prefixStart > lastIndex) {
2705
- segments.push({ type: "text", value: text.slice(lastIndex, prefixStart) });
2706
- }
2707
- if (prefix) {
2708
- segments.push({ type: "text", value: prefix });
2709
- }
2710
- const { path, trailing } = trimTrailingPunctuation(rawPath);
2711
- if (path.length > 1) {
2712
- segments.push({ type: "path", value: path });
2713
- if (trailing) {
2714
- segments.push({ type: "text", value: trailing });
2715
- }
2716
- } else {
2717
- segments.push({ type: "text", value: fullMatch });
2718
- }
2719
- lastIndex = pathStart + rawPath.length;
2720
- }
2721
- if (lastIndex < text.length) {
2722
- segments.push({ type: "text", value: text.slice(lastIndex) });
2723
- }
2724
- return segments.length > 0 ? segments : [{ type: "text", value: text }];
2725
- }
2726
- function filePathToMarkdownHref(path) {
2727
- return `openclaw-file://${encodeURIComponent(path)}`;
2728
- }
2729
- function markdownHrefToFilePath(href) {
2730
- if (!href?.startsWith("openclaw-file://")) return null;
2731
- try {
2732
- return decodeURIComponent(href.slice("openclaw-file://".length));
2733
- } catch {
2734
- return null;
2735
- }
2736
- }
2737
- function remarkFilePathLinks() {
2738
- return (tree) => {
2739
- function visit(node, parent) {
2740
- if (!node) return;
2741
- if (node.type === "text" && parent && parent.type !== "link" && parent.type !== "inlineCode" && parent.type !== "code") {
2742
- const segments = splitTextByFilePaths(String(node.value || ""));
2743
- const hasPaths = segments.some((segment) => segment.type === "path");
2744
- if (!hasPaths) return;
2745
- const replacement = segments.map((segment) => {
2746
- if (segment.type === "text") {
2747
- return { type: "text", value: segment.value };
2748
- }
2749
- return {
2750
- type: "link",
2751
- url: filePathToMarkdownHref(segment.value),
2752
- children: [{ type: "text", value: segment.value }]
2753
- };
2754
- });
2755
- const index = parent.children.indexOf(node);
2756
- if (index >= 0) {
2757
- parent.children.splice(index, 1, ...replacement);
2758
- }
2759
- return;
2760
- }
2761
- if (Array.isArray(node.children)) {
2762
- for (const child of [...node.children]) {
2763
- visit(child, node);
2764
- }
2765
- }
2766
- }
2767
- visit(tree, null);
2768
- };
2769
- }
2770
- const EXTENSION_LANGUAGE_MAP = {
2771
- py: "python",
2772
- ts: "typescript",
2773
- js: "javascript",
2774
- jsx: "jsx",
2775
- tsx: "tsx",
2776
- json: "json",
2777
- md: "markdown",
2778
- yml: "yaml",
2779
- yaml: "yaml",
2780
- sh: "bash",
2781
- bash: "bash",
2782
- zsh: "bash",
2783
- html: "html",
2784
- css: "css",
2785
- sql: "sql",
2786
- xml: "xml",
2787
- toml: "toml",
2788
- rs: "rust",
2789
- go: "go",
2790
- java: "java",
2791
- c: "c",
2792
- cpp: "cpp",
2793
- cs: "csharp",
2794
- php: "php",
2795
- rb: "ruby",
2796
- graphql: "graphql",
2797
- diff: "diff",
2798
- patch: "diff",
2799
- env: "text"
2800
- };
2801
- function languageFromFilePath(path) {
2802
- if (!path) return "text";
2803
- const filename = path.split("/").pop() || "";
2804
- const lower = filename.toLowerCase();
2805
- if (lower === "dockerfile") return "dockerfile";
2806
- if (lower === "makefile") return "text";
2807
- const parts = lower.split(".");
2808
- const extension = parts.length > 1 ? parts.pop() || "" : "";
2809
- const mapped = EXTENSION_LANGUAGE_MAP[extension] || extension || "text";
2810
- return resolveLanguage(mapped);
2811
- }
2812
- const INLINE_PREVIEW_MAX_BYTES = 100 * 1024;
2813
- function normalizeClickedPath(path) {
2814
- if (!path) return "/";
2815
- return path.includes("/") ? path : `/${path}`;
2816
- }
2817
- function toWorkspacePath(path) {
2818
- let p = normalizeClickedPath(path);
2819
- const prefixes = ["/root/clawd/", "/root/"];
2820
- for (const prefix of prefixes) {
2821
- if (p.startsWith(prefix)) {
2822
- p = "/" + p.slice(prefix.length);
2823
- break;
2824
- }
2825
- }
2826
- return p.startsWith("/") ? p : `/${p}`;
2827
- }
2828
- function hasExtension(path) {
2829
- const trimmed = path.replace(/\/+$/, "");
2830
- const name = trimmed.split("/").pop() || "";
2831
- if (!name || name.startsWith(".")) return false;
2832
- return name.includes(".");
2833
- }
2834
- function isDirectoryPathHeuristic(path) {
2835
- if (!path) return false;
2836
- return path.endsWith("/") || !hasExtension(path);
2837
- }
2838
- function isDirectoryError(code, message) {
2839
- const normalizedCode = String(code || "").toUpperCase();
2840
- const normalizedMessage = String(message || "").toLowerCase();
2841
- return normalizedCode.includes("DIRECTORY") || normalizedMessage.includes("is a directory") || normalizedMessage.includes("directory");
2842
- }
2843
- function parseMarkdownIntoBlocks(markdown) {
2844
- const tokens = marked.lexer(markdown);
2845
- return tokens.map((token) => token.raw);
2846
- }
2847
- function extractLanguage(className) {
2848
- if (!className) return "text";
2849
- const match = className.match(/language-([\w-]+)/);
2850
- return match ? match[1] : "text";
2851
- }
2852
- function extractFilenameFromMeta(meta) {
2853
- const value = meta?.trim();
2854
- if (!value) return void 0;
2855
- const firstToken = value.split(/\s+/)[0];
2856
- return firstToken || void 0;
2857
- }
2858
- const BASE_COMPONENTS = {
2859
- code: function CodeComponent({ className, children, node }) {
2860
- const isInline = !className?.includes("language-");
2861
- if (isInline) {
2862
- 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 });
2863
- }
2864
- const language = extractLanguage(className);
2865
- const filename = extractFilenameFromMeta(
2866
- node?.data?.meta
2867
- );
2868
- return /* @__PURE__ */ jsx(
2869
- CodeBlock,
2870
- {
2871
- content: String(children ?? ""),
2872
- language,
2873
- filename,
2874
- className: "w-full"
2875
- }
2876
- );
2877
- },
2878
- pre: function PreComponent({ children }) {
2879
- return /* @__PURE__ */ jsx(Fragment, { children });
2880
- },
2881
- h1: function H1Component({ children }) {
2882
- return /* @__PURE__ */ jsx("h1", { className: "text-xl font-medium text-primary-950", children });
2883
- },
2884
- h2: function H2Component({ children }) {
2885
- return /* @__PURE__ */ jsx("h2", { className: "text-lg font-medium text-primary-900", children });
2886
- },
2887
- h3: function H3Component({ children }) {
2888
- return /* @__PURE__ */ jsx("h3", { className: "font-medium text-primary-900", children });
2889
- },
2890
- p: function PComponent({ children }) {
2891
- return /* @__PURE__ */ jsx("p", { className: "text-primary-950 text-pretty leading-relaxed", children });
2892
- },
2893
- ul: function UlComponent({ children }) {
2894
- return /* @__PURE__ */ jsx("ul", { className: "ml-4 list-disc text-primary-950 marker:text-primary-400", children });
2895
- },
2896
- ol: function OlComponent({ children }) {
2897
- return /* @__PURE__ */ jsx("ol", { className: "ml-4 list-decimal text-primary-950 marker:text-primary-500", children });
2898
- },
2899
- li: function LiComponent({ children }) {
2900
- return /* @__PURE__ */ jsx("li", { className: "leading-relaxed", children });
2901
- },
2902
- blockquote: function BlockquoteComponent({ children }) {
2903
- return /* @__PURE__ */ jsx("blockquote", { className: "border-l-2 border-primary-300 pl-4 text-primary-900 italic", children });
2904
- },
2905
- strong: function StrongComponent({ children }) {
2906
- return /* @__PURE__ */ jsx("strong", { className: "font-medium text-primary-950", children });
2907
- },
2908
- em: function EmComponent({ children }) {
2909
- return /* @__PURE__ */ jsx("em", { className: "italic text-primary-950", children });
2910
- },
2911
- hr: function HrComponent() {
2912
- return /* @__PURE__ */ jsx("hr", { className: "my-3 border-primary-200" });
2913
- },
2914
- table: function TableComponent({ children }) {
2915
- return /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsx("table", { className: "w-full border-collapse text-sm", children }) });
2916
- },
2917
- thead: function TheadComponent({ children }) {
2918
- return /* @__PURE__ */ jsx("thead", { className: "border-b border-primary-200 bg-primary-50", children });
2919
- },
2920
- tbody: function TbodyComponent({ children }) {
2921
- return /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-primary-100", children });
2922
- },
2923
- tr: function TrComponent({ children }) {
2924
- return /* @__PURE__ */ jsx("tr", { className: "transition-colors hover:bg-primary-50/50", children });
2925
- },
2926
- th: function ThComponent({ children }) {
2927
- return /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium text-primary-950", children });
2928
- },
2929
- td: function TdComponent({ children }) {
2930
- return /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-primary-950", children });
2931
- }
2932
- };
2933
- function createDefaultComponents(onOpenFilePreview) {
2934
- return {
2935
- ...BASE_COMPONENTS,
2936
- a: function AComponent({ children, href }) {
2937
- const filePath = markdownHrefToFilePath(href);
2938
- if (filePath) {
2939
- return /* @__PURE__ */ jsx(
2940
- "button",
2941
- {
2942
- type: "button",
2943
- onClick: () => onOpenFilePreview(filePath),
2944
- className: "font-mono text-[var(--opencami-accent)] underline decoration-[var(--opencami-accent-light)] underline-offset-4 hover:opacity-90 cursor-pointer",
2945
- children
2946
- }
2947
- );
2948
- }
2949
- return /* @__PURE__ */ jsx(
2950
- "a",
2951
- {
2952
- href,
2953
- onClick: (event) => {
2954
- if (href?.startsWith("openclaw-file://")) event.preventDefault();
2955
- },
2956
- className: "text-[var(--opencami-accent)] underline decoration-[var(--opencami-accent-light)] underline-offset-4 transition-opacity hover:opacity-90",
2957
- target: "_blank",
2958
- rel: "noopener noreferrer",
2959
- children
2960
- }
2961
- );
2962
- },
2963
- code: function InlineCodeComponent({ children, className, node }) {
2964
- if (className?.includes("language-")) {
2965
- const language = extractLanguage(className);
2966
- const filename = extractFilenameFromMeta(
2967
- node?.data?.meta
2968
- );
2969
- return /* @__PURE__ */ jsx(
2970
- CodeBlock,
2971
- {
2972
- content: String(children ?? ""),
2973
- language,
2974
- filename,
2975
- className: "w-full"
2976
- }
2977
- );
2978
- }
2979
- const text = typeof children === "string" ? children : Array.isArray(children) ? children.filter((c) => typeof c === "string").join("") : String(children ?? "");
2980
- if (text && isLikelyFilePath(text)) {
2981
- return /* @__PURE__ */ jsx(
2982
- "button",
2983
- {
2984
- type: "button",
2985
- onClick: () => onOpenFilePreview(text),
2986
- className: "font-mono text-sm bg-primary-100 rounded px-1.5 py-0.5 text-primary-900 underline decoration-primary-300 underline-offset-4 hover:decoration-primary-600 cursor-pointer",
2987
- children
2988
- }
2989
- );
2990
- }
2991
- return /* @__PURE__ */ jsx("code", { className: "font-mono text-sm bg-primary-100 rounded px-1.5 py-0.5 text-primary-900", children });
2992
- }
2993
- };
2994
- }
2995
- const MemoizedMarkdownBlock = memo(
2996
- function MarkdownBlock({
2997
- content,
2998
- components
2999
- }) {
3000
- return /* @__PURE__ */ jsx(
3001
- ReactMarkdown,
3002
- {
3003
- remarkPlugins: [remarkGfm, remarkBreaks, remarkFilePathLinks],
3004
- components,
3005
- children: content
3006
- }
3007
- );
3008
- },
3009
- function propsAreEqual(prevProps, nextProps) {
3010
- return prevProps.content === nextProps.content;
3011
- }
3012
- );
3013
- MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock";
3014
- function fileErrorMessageFromResponse(status, code) {
3015
- if (code === "NOT_FOUND" || status === 404) return "File not found";
3016
- if (code === "UNSUPPORTED_TYPE") return "Binary file";
3017
- if (code === "FILE_TOO_LARGE" || status === 413) return "File too large";
3018
- return "Failed to load file preview";
3019
- }
3020
- function MarkdownComponent({
3021
- children,
3022
- id,
3023
- className,
3024
- components
3025
- }) {
3026
- const generatedId = useId();
3027
- const blockId = id ?? generatedId;
3028
- const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]);
3029
- const [filePreview, setFilePreview] = useState({ status: "idle" });
3030
- const navigate = useNavigate();
3031
- const openDirectoryInExplorer = useCallback((path) => {
3032
- const workspacePath = toWorkspacePath(path);
3033
- useFileExplorerState.getState().navigateTo(workspacePath);
3034
- setFilePreview({ status: "idle" });
3035
- navigate({ to: "/files" });
3036
- }, [navigate]);
3037
- const onOpenFilePreview = useCallback((path) => {
3038
- const resolvedPath = normalizeClickedPath(path);
3039
- if (isDirectoryPathHeuristic(resolvedPath)) {
3040
- openDirectoryInExplorer(resolvedPath);
3041
- return;
3042
- }
3043
- setFilePreview({ status: "loading", path: resolvedPath });
3044
- }, [openDirectoryInExplorer]);
3045
- const defaultComponents = useMemo(
3046
- () => createDefaultComponents(onOpenFilePreview),
3047
- [onOpenFilePreview]
3048
- );
3049
- const mergedComponents = useMemo(
3050
- () => ({ ...defaultComponents, ...components || {} }),
3051
- [defaultComponents, components]
3052
- );
3053
- useEffect(() => {
3054
- if (filePreview.status !== "loading") return;
3055
- const path = filePreview.path;
3056
- const controller = new AbortController();
3057
- fetch(`/api/files/read?path=${encodeURIComponent(path)}`, {
3058
- signal: controller.signal
3059
- }).then(async (response) => {
3060
- const payload = await response.json().catch(() => ({}));
3061
- if (!response.ok) {
3062
- if (isDirectoryError(payload.code, payload.message)) {
3063
- openDirectoryInExplorer(path);
3064
- return;
3065
- }
3066
- const error = fileErrorMessageFromResponse(response.status, payload.code);
3067
- setFilePreview({ status: "error", path, message: error });
3068
- return;
3069
- }
3070
- const size = Number(payload.size ?? 0);
3071
- if (size > INLINE_PREVIEW_MAX_BYTES) {
3072
- setFilePreview({ status: "error", path, message: "File too large" });
3073
- return;
3074
- }
3075
- const content = String(payload.content ?? "");
3076
- setFilePreview({
3077
- status: "success",
3078
- path,
3079
- content,
3080
- language: languageFromFilePath(path)
3081
- });
3082
- }).catch(() => {
3083
- if (controller.signal.aborted) return;
3084
- setFilePreview({ status: "error", path, message: "Failed to load file preview" });
3085
- });
3086
- return () => controller.abort();
3087
- }, [filePreview, openDirectoryInExplorer]);
3088
- const previewOpen = filePreview.status !== "idle";
3089
- return /* @__PURE__ */ jsxs(Fragment, { children: [
3090
- /* @__PURE__ */ jsx(
3091
- "div",
3092
- {
3093
- className: cn("flex min-w-0 max-w-full flex-col gap-2 overflow-x-hidden", className),
3094
- onClickCapture: (event) => {
3095
- const target = event.target;
3096
- const anchor = target?.closest?.('a[href^="openclaw-file://"]');
3097
- if (!anchor) return;
3098
- const filePath = markdownHrefToFilePath(anchor.getAttribute("href") ?? void 0);
3099
- if (!filePath) return;
3100
- event.preventDefault();
3101
- event.stopPropagation();
3102
- onOpenFilePreview(filePath);
3103
- },
3104
- children: blocks.map((block, index) => /* @__PURE__ */ jsx(
3105
- MemoizedMarkdownBlock,
3106
- {
3107
- content: block,
3108
- components: mergedComponents
3109
- },
3110
- `${blockId}-block-${index}`
3111
- ))
3112
- }
3113
- ),
3114
- /* @__PURE__ */ jsx(
3115
- DialogRoot,
3116
- {
3117
- open: previewOpen,
3118
- onOpenChange: (open) => {
3119
- if (!open) setFilePreview({ status: "idle" });
3120
- },
3121
- children: /* @__PURE__ */ jsxs(DialogContent, { className: "w-[min(1000px,95vw)] max-h-[88vh] overflow-hidden p-0", children: [
3122
- /* @__PURE__ */ jsxs("div", { className: "border-b border-primary-200 px-4 py-3 flex items-start justify-between gap-3", children: [
3123
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
3124
- /* @__PURE__ */ jsx(DialogTitle, { className: "text-base", children: "File Preview" }),
3125
- filePreview.status !== "idle" && /* @__PURE__ */ jsx("p", { className: "text-xs text-primary-600 font-mono truncate", children: filePreview.path })
3126
- ] }),
3127
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3128
- filePreview.status !== "idle" && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: /* @__PURE__ */ jsx(
3129
- Link,
3130
- {
3131
- to: "/files",
3132
- onClick: () => {
3133
- const p = filePreview.status !== "idle" ? toWorkspacePath(filePreview.path) : "";
3134
- if (p) {
3135
- const dir = p.includes("/") ? p.slice(0, p.lastIndexOf("/")) || "/" : "/";
3136
- useFileExplorerState.getState().navigateTo(dir);
3137
- }
3138
- setFilePreview({ status: "idle" });
3139
- },
3140
- children: "Open in Explorer"
3141
- }
3142
- ) }),
3143
- filePreview.status !== "idle" && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: /* @__PURE__ */ jsx(
3144
- Link,
3145
- {
3146
- to: "/files",
3147
- onClick: () => {
3148
- const p = filePreview.status !== "idle" ? toWorkspacePath(filePreview.path) : "";
3149
- if (p) {
3150
- useFileExplorerState.getState().openInEditor(p);
3151
- }
3152
- setFilePreview({ status: "idle" });
3153
- },
3154
- children: "Open in Editor"
3155
- }
3156
- ) }),
3157
- /* @__PURE__ */ jsx(DialogClose, { children: "Close" })
3158
- ] })
3159
- ] }),
3160
- /* @__PURE__ */ jsxs("div", { className: "p-4 overflow-auto max-h-[calc(88vh-72px)]", children: [
3161
- filePreview.status === "loading" && /* @__PURE__ */ jsx("p", { className: "text-sm text-primary-600", children: "Loading preview…" }),
3162
- filePreview.status === "error" && /* @__PURE__ */ jsx("p", { className: "text-sm text-red-600", children: filePreview.message }),
3163
- filePreview.status === "success" && /* @__PURE__ */ jsx(
3164
- CodeBlock,
3165
- {
3166
- content: filePreview.content,
3167
- language: filePreview.language,
3168
- className: "w-full"
3169
- }
3170
- )
3171
- ] })
3172
- ] })
3173
- }
3174
- )
3175
- ] });
3176
- }
3177
- const Markdown = memo(MarkdownComponent);
3178
- Markdown.displayName = "Markdown";
3179
2675
  function Message({ children, className, ...props }) {
3180
2676
  return /* @__PURE__ */ jsx("div", { className: cn("flex gap-3 w-full", className), ...props, children });
3181
2677
  }
@@ -6406,8 +5902,10 @@ function useStreaming(options) {
6406
5902
  const eventSourceRef = useRef(null);
6407
5903
  const onDoneRef = useRef(options.onDone);
6408
5904
  const onErrorRef = useRef(options.onError);
5905
+ const onAssistantDeltaRef = useRef(options.onAssistantDelta);
6409
5906
  onDoneRef.current = options.onDone;
6410
5907
  onErrorRef.current = options.onError;
5908
+ onAssistantDeltaRef.current = options.onAssistantDelta;
6411
5909
  const stop = useCallback((options2) => {
6412
5910
  if (eventSourceRef.current) {
6413
5911
  eventSourceRef.current.close();
@@ -6440,6 +5938,7 @@ function useStreaming(options) {
6440
5938
  ...prev,
6441
5939
  text: prev.text + data.text
6442
5940
  }));
5941
+ onAssistantDeltaRef.current?.({ text: data.text, sessionKey: data.sessionKey });
6443
5942
  } catch {
6444
5943
  }
6445
5944
  });
@@ -6622,13 +6121,109 @@ function useSwipeGesture(options) {
6622
6121
  onTouchEnd: handleTouchEnd
6623
6122
  };
6624
6123
  }
6124
+ const ENABLED_KEY = "opencami-browser-notifications-enabled";
6125
+ const ASKED_KEY = "opencami-browser-notifications-permission-asked";
6126
+ const NOTIFICATION_DEBOUNCE_MS = 5e3;
6127
+ function isSupported() {
6128
+ return typeof window !== "undefined" && "Notification" in window;
6129
+ }
6130
+ function getNotificationsEnabled() {
6131
+ if (typeof window === "undefined") return false;
6132
+ try {
6133
+ return localStorage.getItem(ENABLED_KEY) === "true";
6134
+ } catch {
6135
+ return false;
6136
+ }
6137
+ }
6138
+ function useNotifications() {
6139
+ const navigate = useNavigate();
6140
+ const lastNotificationAtRef = useRef(0);
6141
+ const [enabled, setEnabledState] = useState(() => getNotificationsEnabled());
6142
+ const requestPermission = useCallback(async () => {
6143
+ if (!isSupported()) return "denied";
6144
+ try {
6145
+ return await Notification.requestPermission();
6146
+ } catch {
6147
+ return "denied";
6148
+ }
6149
+ }, []);
6150
+ const setEnabled = useCallback(async (nextEnabled) => {
6151
+ setEnabledState(nextEnabled);
6152
+ try {
6153
+ localStorage.setItem(ENABLED_KEY, String(nextEnabled));
6154
+ } catch {
6155
+ }
6156
+ if (nextEnabled && isSupported() && Notification.permission === "default") {
6157
+ await requestPermission();
6158
+ try {
6159
+ localStorage.setItem(ASKED_KEY, "true");
6160
+ } catch {
6161
+ }
6162
+ }
6163
+ }, [requestPermission]);
6164
+ const maybeNotifyAssistantMessage = useCallback((payload) => {
6165
+ if (!enabled || !isSupported()) return;
6166
+ if (document.hidden !== true) return;
6167
+ if (Notification.permission !== "granted") return;
6168
+ const now = Date.now();
6169
+ if (now - lastNotificationAtRef.current < NOTIFICATION_DEBOUNCE_MS) return;
6170
+ lastNotificationAtRef.current = now;
6171
+ const body = payload.text.slice(0, 100);
6172
+ const notification = new Notification("OpenCami", {
6173
+ body,
6174
+ icon: "/pwa-192x192.png"
6175
+ });
6176
+ notification.onclick = () => {
6177
+ window.focus();
6178
+ void navigate({ to: "/chat/$sessionKey", params: { sessionKey: payload.sessionFriendlyId } });
6179
+ notification.close();
6180
+ };
6181
+ }, [enabled, navigate]);
6182
+ useEffect(() => {
6183
+ if (!isSupported()) return;
6184
+ const onFirstInteraction = () => {
6185
+ if (Notification.permission !== "default") return;
6186
+ let asked = false;
6187
+ try {
6188
+ asked = localStorage.getItem(ASKED_KEY) === "true";
6189
+ } catch {
6190
+ asked = false;
6191
+ }
6192
+ if (asked) return;
6193
+ void requestPermission().finally(() => {
6194
+ try {
6195
+ localStorage.setItem(ASKED_KEY, "true");
6196
+ } catch {
6197
+ }
6198
+ });
6199
+ };
6200
+ const options = { once: true, passive: true };
6201
+ window.addEventListener("pointerdown", onFirstInteraction, options);
6202
+ window.addEventListener("keydown", onFirstInteraction, { once: true });
6203
+ return () => {
6204
+ window.removeEventListener("pointerdown", onFirstInteraction);
6205
+ window.removeEventListener("keydown", onFirstInteraction);
6206
+ };
6207
+ }, [requestPermission]);
6208
+ useEffect(() => {
6209
+ const sync = () => setEnabledState(getNotificationsEnabled());
6210
+ window.addEventListener("storage", sync);
6211
+ return () => window.removeEventListener("storage", sync);
6212
+ }, []);
6213
+ return {
6214
+ notificationsEnabled: enabled,
6215
+ setNotificationsEnabled: setEnabled,
6216
+ requestNotificationPermission: requestPermission,
6217
+ maybeNotifyAssistantMessage
6218
+ };
6219
+ }
6625
6220
  const KeyboardShortcutsDialog = lazy(
6626
- () => import("./keyboard-shortcuts-dialog-CsNP85q8.js").then((m) => ({
6221
+ () => import("./keyboard-shortcuts-dialog-Cr6fOqHz.js").then((m) => ({
6627
6222
  default: m.KeyboardShortcutsDialog
6628
6223
  }))
6629
6224
  );
6630
6225
  const SearchDialog = lazy(
6631
- () => import("./search-dialog-C2a3OYm_.js").then((m) => ({
6226
+ () => import("./search-dialog-DZTS5SEi.js").then((m) => ({
6632
6227
  default: m.SearchDialog
6633
6228
  }))
6634
6229
  );
@@ -6658,6 +6253,7 @@ function ChatScreen({
6658
6253
  const [searchJumpMessageId, setSearchJumpMessageId] = useState(null);
6659
6254
  const [isStreaming, setIsStreaming] = useState(false);
6660
6255
  const thinkingLevel = useThinkingLevelStore((state) => state.level);
6256
+ const { maybeNotifyAssistantMessage } = useNotifications();
6661
6257
  const inputRef = useRef(null);
6662
6258
  const streamTimer = useRef(null);
6663
6259
  const streamIdleTimer = useRef(null);
@@ -6665,6 +6261,7 @@ function ChatScreen({
6665
6261
  const refreshHistoryRef = useRef(() => {
6666
6262
  });
6667
6263
  const pendingStartRef = useRef(false);
6264
+ const streamingNotificationTextRef = useRef("");
6668
6265
  const { isMobile } = useChatMobile(queryClient);
6669
6266
  const {
6670
6267
  sessionsQuery,
@@ -6781,7 +6378,14 @@ function ChatScreen({
6781
6378
  });
6782
6379
  const { streaming, startStream, stopStream } = useStreaming({
6783
6380
  onDone: (sk) => handleStreamDoneRef.current(sk),
6784
- onError: (err) => handleStreamErrorRef.current(err)
6381
+ onError: (err) => handleStreamErrorRef.current(err),
6382
+ onAssistantDelta: ({ text }) => {
6383
+ streamingNotificationTextRef.current += text;
6384
+ maybeNotifyAssistantMessage({
6385
+ text: streamingNotificationTextRef.current,
6386
+ sessionFriendlyId: activeFriendlyId
6387
+ });
6388
+ }
6785
6389
  });
6786
6390
  handleStreamDoneRef.current = useCallback(
6787
6391
  async (_sk) => {
@@ -7084,6 +6688,7 @@ function ChatScreen({
7084
6688
  mimeType: a.file.type,
7085
6689
  content: a.base64
7086
6690
  }));
6691
+ streamingNotificationTextRef.current = "";
7087
6692
  startStream(sessionKey);
7088
6693
  streamStart();
7089
6694
  fetch("/api/send", {