opencami 1.5.0 → 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 (118) hide show
  1. package/README.md +4 -1
  2. package/dist/client/assets/{CSPContext-EgWK8bIJ.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/c-BIGW1oBm.js +1 -0
  11. package/dist/client/assets/composite-DBl8R3ae.js +1 -0
  12. package/dist/client/assets/{connect-w4lLOqiJ.js → connect-NYvOqiBJ.js} +1 -1
  13. package/dist/client/assets/core-BrHBc0Zp.js +12 -0
  14. package/dist/client/assets/cpp-B-lmv-kZ.js +1 -0
  15. package/dist/client/assets/csharp-K5feNrxe.js +1 -0
  16. package/dist/client/assets/css-DPfMkruS.js +1 -0
  17. package/dist/client/assets/diff-D97Zzqfu.js +1 -0
  18. package/dist/client/assets/dockerfile-BcOcwvcX.js +1 -0
  19. package/dist/client/assets/engine-javascript-PwbX1GN8.js +141 -0
  20. package/dist/client/assets/file-explorer-screen-BSMbs0vi.js +1 -0
  21. package/dist/client/assets/files-BJbMx0_w.js +2 -0
  22. package/dist/client/assets/go-Dn2_MT6a.js +1 -0
  23. package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
  24. package/dist/client/assets/html-GMplVEZG.js +1 -0
  25. package/dist/client/assets/index-CMATW8VA.js +3 -0
  26. package/dist/client/assets/index-T4TOjvD0.js +1 -0
  27. package/dist/client/assets/{index-36G0WCxU.js → index-rOIRO-8E.js} +1 -1
  28. package/dist/client/assets/java-CylS5w8V.js +1 -0
  29. package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
  30. package/dist/client/assets/json-Cp-IABpG.js +1 -0
  31. package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
  32. package/dist/client/assets/keyboard-shortcuts-dialog-BTGWdJMl.js +1 -0
  33. package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
  34. package/dist/client/assets/{main-B3N0eQFg.js → main-B_dlfHME.js} +9 -9
  35. package/dist/client/assets/markdown-BVzT7z4x.js +87 -0
  36. package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
  37. package/dist/client/assets/memory-S3Yws6a5.js +2 -0
  38. package/dist/client/assets/memory-screen-C-Z9o31m.js +1 -0
  39. package/dist/client/assets/menu-DHNgWk_8.js +1 -0
  40. package/dist/client/assets/{opencami-logo-DD0DPFRQ.js → opencami-logo-BQQETnJG.js} +1 -1
  41. package/dist/client/assets/owner-CpRnf1fI.js +1 -0
  42. package/dist/client/assets/php-CDn_0X-4.js +1 -0
  43. package/dist/client/assets/popupStateMapping-BRPDXnjv.js +1 -0
  44. package/dist/client/assets/proxy-BcUh9kMA.js +9 -0
  45. package/dist/client/assets/python-B6aJPvgy.js +1 -0
  46. package/dist/client/assets/react-irH8OzhB.js +1 -0
  47. package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
  48. package/dist/client/assets/ruby-FDmvQDUv.js +1 -0
  49. package/dist/client/assets/rust-B1yitclQ.js +1 -0
  50. package/dist/client/assets/search-dialog-B96zx_ng.js +1 -0
  51. package/dist/client/assets/session-export-dialog-DPuHnhgv.js +1 -0
  52. package/dist/client/assets/settings-dialog-DZcRCaPj.js +1 -0
  53. package/dist/client/assets/shell-DfDnw5Jg.js +1 -0
  54. package/dist/client/assets/skills-YZe3I63y.js +2 -0
  55. package/dist/client/assets/{skills-panel-BLUjzfjJ.js → skills-panel-WDUfIwnI.js} +2 -2
  56. package/dist/client/assets/sql-BLtJtn59.js +1 -0
  57. package/dist/client/assets/styles-Bwo-K6Y4.css +1 -0
  58. package/dist/client/assets/swift-Dg5xB15N.js +1 -0
  59. package/dist/client/assets/switch-DPocNFRG.js +1 -0
  60. package/dist/client/assets/tabs-B0cro1hL.js +1 -0
  61. package/dist/client/assets/toml-vGWfd6FD.js +1 -0
  62. package/dist/client/assets/tooltip-Dg9fy-vT.js +1 -0
  63. package/dist/client/assets/tsx-COt5Ahok.js +1 -0
  64. package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
  65. package/dist/client/assets/use-file-explorer-state-DzT0bksg.js +12 -0
  66. package/dist/client/assets/useButton-Cbl_9oFG.js +1 -0
  67. package/dist/client/assets/useCompositeItem-BDAzTxVe.js +1 -0
  68. package/dist/client/assets/{useControlled-BhUuiHAm.js → useControlled-Dscz_s4f.js} +1 -1
  69. package/dist/client/assets/{useMutation-CFmVaBag.js → useMutation-B1FlDsNN.js} +1 -1
  70. package/dist/client/assets/visuallyHidden-ONmQ-0U2.js +1 -0
  71. package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
  72. package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
  73. package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
  74. package/dist/client/assets/yaml-Buea-lGh.js +1 -0
  75. package/dist/server/assets/{_sessionKey-tRze5NLR.js → _sessionKey-B0ZlLAjH.js} +184 -575
  76. package/dist/server/assets/_tanstack-start-manifest_v-D5UVTs1o.js +4 -0
  77. package/dist/server/assets/{connect-d3AqjAqe.js → connect-CbgijWz4.js} +1 -34
  78. package/dist/server/assets/{file-explorer-screen-CVlFiAFu.js → file-explorer-screen-DH4UFK03.js} +3 -2
  79. package/dist/server/assets/{files-BIEcSPGp.js → files-DYdXlQDr.js} +1 -1
  80. package/dist/server/assets/{index-CRfLKh30.js → index-CiUjUD0t.js} +1 -1
  81. package/dist/server/assets/{index-CNIATlJ9.js → index-Dl2BOKP7.js} +94 -79
  82. package/dist/server/assets/{keyboard-shortcuts-dialog-CsNP85q8.js → keyboard-shortcuts-dialog-Cr6fOqHz.js} +1 -2
  83. package/dist/server/assets/markdown-BFE5y9YH.js +565 -0
  84. package/dist/server/assets/memory-BqZOoD7Q.js +11 -0
  85. package/dist/server/assets/memory-screen-BK5phS8K.js +235 -0
  86. package/dist/server/assets/menu-D90CDTi2.js +45 -0
  87. package/dist/server/assets/{router-rn7pJO_D.js → router-Uuagl6O7.js} +56 -46
  88. package/dist/server/assets/{search-dialog-Bz4Cu0KW.js → search-dialog-DZTS5SEi.js} +49 -83
  89. package/dist/server/assets/{session-export-dialog-CwclV0Aj.js → session-export-dialog-C53RRAah.js} +1 -2
  90. package/dist/server/assets/{settings-dialog-BBM7jCjE.js → settings-dialog-CSYDj2qm.js} +82 -56
  91. package/dist/server/assets/{use-file-explorer-state-Il1LlBAe.js → use-file-explorer-state-s7CS50ho.js} +0 -41
  92. package/dist/server/server.js +2 -2
  93. package/package.json +2 -1
  94. package/dist/client/assets/DirectionContext-DXtY05YF.js +0 -1
  95. package/dist/client/assets/_sessionKey-B89e7G3y.js +0 -100
  96. package/dist/client/assets/agents-screen-1BiEZ9od.js +0 -1
  97. package/dist/client/assets/agents-x54ocA9z.js +0 -2
  98. package/dist/client/assets/bots-screen-BNQciUeJ.js +0 -1
  99. package/dist/client/assets/bots-x86ZHG4b.js +0 -2
  100. package/dist/client/assets/button-nDcsaNPl.js +0 -1
  101. package/dist/client/assets/file-explorer-screen-CAsjd3w8.js +0 -1
  102. package/dist/client/assets/files-Bype5Mnb.js +0 -2
  103. package/dist/client/assets/index-BXkRE220.js +0 -153
  104. package/dist/client/assets/keyboard-shortcuts-dialog-BdCeXRjD.js +0 -1
  105. package/dist/client/assets/react-B16OrBeM.js +0 -1
  106. package/dist/client/assets/search-dialog-BjTPceEl.js +0 -1
  107. package/dist/client/assets/session-export-dialog-DtHKG2zW.js +0 -1
  108. package/dist/client/assets/settings-dialog-hiqdk_UD.js +0 -1
  109. package/dist/client/assets/skills-DhwyFq3y.js +0 -2
  110. package/dist/client/assets/styles-CHP4l6vZ.css +0 -1
  111. package/dist/client/assets/switch-J6wLIVu2.js +0 -1
  112. package/dist/client/assets/tabs-DvPgTz5I.js +0 -1
  113. package/dist/client/assets/tooltip-C14vdXHK.js +0 -1
  114. package/dist/client/assets/use-file-explorer-state-BnaJEqRP.js +0 -12
  115. package/dist/client/assets/useButton-Bnnac1eR.js +0 -1
  116. package/dist/client/assets/useCompositeItem-BgiEMKAt.js +0 -1
  117. package/dist/client/assets/visuallyHidden-DCCICp6T.js +0 -9
  118. package/dist/server/assets/_tanstack-start-manifest_v-CyfoMvUa.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-CNIATlJ9.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-rn7pJO_D.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-BBM7jCjE.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
  }
@@ -5988,13 +5484,13 @@ function ChatComposerComponent({
5988
5484
  /* @__PURE__ */ jsx(CommandHelp, { onCommandSelect: (cmd) => handleValueChange(cmd + " ") })
5989
5485
  ] }),
5990
5486
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
5991
- /* @__PURE__ */ jsx(PromptInputAction, { tooltip: "Attach file", children: /* @__PURE__ */ jsx(
5487
+ /* @__PURE__ */ jsx(
5992
5488
  AttachmentButton,
5993
5489
  {
5994
5490
  onFileSelect: handleFileSelect,
5995
5491
  disabled
5996
5492
  }
5997
- ) }),
5493
+ ),
5998
5494
  /* @__PURE__ */ jsx(PromptInputAction, { tooltip: isRecording ? "Stop recording" : "Voice input", children: /* @__PURE__ */ jsx(
5999
5495
  Button,
6000
5496
  {
@@ -6328,7 +5824,8 @@ function useSmartTitle() {
6328
5824
  if (controller.signal.aborted) {
6329
5825
  throw new Error("Aborted");
6330
5826
  }
6331
- const title = data.title || message.slice(0, 50);
5827
+ const rawTitle = data.title || message.slice(0, 50);
5828
+ const title = rawTitle.length > 64 ? rawTitle.slice(0, 61) + "..." : rawTitle;
6332
5829
  const source = data.source || "heuristic";
6333
5830
  setLastTitle(title);
6334
5831
  setLastSource(source);
@@ -6405,8 +5902,10 @@ function useStreaming(options) {
6405
5902
  const eventSourceRef = useRef(null);
6406
5903
  const onDoneRef = useRef(options.onDone);
6407
5904
  const onErrorRef = useRef(options.onError);
5905
+ const onAssistantDeltaRef = useRef(options.onAssistantDelta);
6408
5906
  onDoneRef.current = options.onDone;
6409
5907
  onErrorRef.current = options.onError;
5908
+ onAssistantDeltaRef.current = options.onAssistantDelta;
6410
5909
  const stop = useCallback((options2) => {
6411
5910
  if (eventSourceRef.current) {
6412
5911
  eventSourceRef.current.close();
@@ -6439,6 +5938,7 @@ function useStreaming(options) {
6439
5938
  ...prev,
6440
5939
  text: prev.text + data.text
6441
5940
  }));
5941
+ onAssistantDeltaRef.current?.({ text: data.text, sessionKey: data.sessionKey });
6442
5942
  } catch {
6443
5943
  }
6444
5944
  });
@@ -6621,13 +6121,109 @@ function useSwipeGesture(options) {
6621
6121
  onTouchEnd: handleTouchEnd
6622
6122
  };
6623
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
+ }
6624
6220
  const KeyboardShortcutsDialog = lazy(
6625
- () => import("./keyboard-shortcuts-dialog-CsNP85q8.js").then((m) => ({
6221
+ () => import("./keyboard-shortcuts-dialog-Cr6fOqHz.js").then((m) => ({
6626
6222
  default: m.KeyboardShortcutsDialog
6627
6223
  }))
6628
6224
  );
6629
6225
  const SearchDialog = lazy(
6630
- () => import("./search-dialog-Bz4Cu0KW.js").then((m) => ({
6226
+ () => import("./search-dialog-DZTS5SEi.js").then((m) => ({
6631
6227
  default: m.SearchDialog
6632
6228
  }))
6633
6229
  );
@@ -6657,6 +6253,7 @@ function ChatScreen({
6657
6253
  const [searchJumpMessageId, setSearchJumpMessageId] = useState(null);
6658
6254
  const [isStreaming, setIsStreaming] = useState(false);
6659
6255
  const thinkingLevel = useThinkingLevelStore((state) => state.level);
6256
+ const { maybeNotifyAssistantMessage } = useNotifications();
6660
6257
  const inputRef = useRef(null);
6661
6258
  const streamTimer = useRef(null);
6662
6259
  const streamIdleTimer = useRef(null);
@@ -6664,6 +6261,7 @@ function ChatScreen({
6664
6261
  const refreshHistoryRef = useRef(() => {
6665
6262
  });
6666
6263
  const pendingStartRef = useRef(false);
6264
+ const streamingNotificationTextRef = useRef("");
6667
6265
  const { isMobile } = useChatMobile(queryClient);
6668
6266
  const {
6669
6267
  sessionsQuery,
@@ -6780,7 +6378,14 @@ function ChatScreen({
6780
6378
  });
6781
6379
  const { streaming, startStream, stopStream } = useStreaming({
6782
6380
  onDone: (sk) => handleStreamDoneRef.current(sk),
6783
- 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
+ }
6784
6389
  });
6785
6390
  handleStreamDoneRef.current = useCallback(
6786
6391
  async (_sk) => {
@@ -6818,12 +6423,15 @@ function ChatScreen({
6818
6423
  }, [streaming.text, streaming.tools]);
6819
6424
  const messagesWithStreaming = useMemo(() => {
6820
6425
  if (!streamingMessage) return displayMessages;
6821
- const lastMsg = displayMessages[displayMessages.length - 1];
6822
- if (lastMsg?.role === "assistant") {
6823
- const lastText = textFromMessage(lastMsg);
6824
- if (lastText.length >= streaming.text.length) return displayMessages;
6426
+ const lastAssistantIndex = [...displayMessages].map((message, index) => ({ message, index })).filter(({ message }) => message.role === "assistant").map(({ index }) => index).pop();
6427
+ const lastUserIndex = [...displayMessages].map((message, index) => ({ message, index })).filter(({ message }) => message.role === "user").map(({ index }) => index).pop();
6428
+ const assistantIsLatestTurn = typeof lastAssistantIndex === "number" && (typeof lastUserIndex !== "number" || lastAssistantIndex > lastUserIndex);
6429
+ if (assistantIsLatestTurn && typeof lastAssistantIndex === "number") {
6430
+ const historyAssistant = displayMessages[lastAssistantIndex];
6431
+ const historyText = textFromMessage(historyAssistant);
6432
+ if (historyText.length >= streaming.text.length) return displayMessages;
6825
6433
  const msgs = [...displayMessages];
6826
- msgs[msgs.length - 1] = streamingMessage;
6434
+ msgs[lastAssistantIndex] = streamingMessage;
6827
6435
  return msgs;
6828
6436
  }
6829
6437
  return [...displayMessages, streamingMessage];
@@ -7080,6 +6688,7 @@ function ChatScreen({
7080
6688
  mimeType: a.file.type,
7081
6689
  content: a.base64
7082
6690
  }));
6691
+ streamingNotificationTextRef.current = "";
7083
6692
  startStream(sessionKey);
7084
6693
  streamStart();
7085
6694
  fetch("/api/send", {