opencami 1.3.2 → 1.5.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 (75) hide show
  1. package/README.md +60 -0
  2. package/dist/client/assets/CSPContext-EgWK8bIJ.js +1 -0
  3. package/dist/client/assets/DirectionContext-DXtY05YF.js +1 -0
  4. package/dist/client/assets/_sessionKey-B89e7G3y.js +100 -0
  5. package/dist/client/assets/agents-screen-1BiEZ9od.js +1 -0
  6. package/dist/client/assets/agents-x54ocA9z.js +2 -0
  7. package/dist/client/assets/bots-screen-BNQciUeJ.js +1 -0
  8. package/dist/client/assets/bots-x86ZHG4b.js +2 -0
  9. package/dist/client/assets/button-nDcsaNPl.js +1 -0
  10. package/dist/client/assets/{connect-CtDn993i.js → connect-w4lLOqiJ.js} +1 -1
  11. package/dist/client/assets/file-explorer-screen-CAsjd3w8.js +1 -0
  12. package/dist/client/assets/files-Bype5Mnb.js +2 -0
  13. package/dist/client/assets/{index-N9-2R5hZ.js → index-36G0WCxU.js} +1 -1
  14. package/dist/client/assets/index-BXkRE220.js +153 -0
  15. package/dist/client/assets/keyboard-shortcuts-dialog-BdCeXRjD.js +1 -0
  16. package/dist/client/assets/{main-DrVY5UJU.js → main-B3N0eQFg.js} +129 -17
  17. package/dist/client/assets/opencami-logo-DD0DPFRQ.js +1 -0
  18. package/dist/client/assets/{react-CzqI3gbN.js → react-B16OrBeM.js} +1 -1
  19. package/dist/client/assets/search-dialog-BjTPceEl.js +1 -0
  20. package/dist/client/assets/session-export-dialog-DtHKG2zW.js +1 -0
  21. package/dist/client/assets/settings-dialog-hiqdk_UD.js +1 -0
  22. package/dist/client/assets/skills-DhwyFq3y.js +2 -0
  23. package/dist/client/assets/skills-panel-BLUjzfjJ.js +5 -0
  24. package/dist/client/assets/styles-CHP4l6vZ.css +1 -0
  25. package/dist/client/assets/switch-J6wLIVu2.js +1 -0
  26. package/dist/client/assets/tabs-DvPgTz5I.js +1 -0
  27. package/dist/client/assets/tooltip-C14vdXHK.js +1 -0
  28. package/dist/client/assets/use-file-explorer-state-BnaJEqRP.js +12 -0
  29. package/dist/client/assets/useButton-Bnnac1eR.js +1 -0
  30. package/dist/client/assets/useCompositeItem-BgiEMKAt.js +1 -0
  31. package/dist/client/assets/useControlled-BhUuiHAm.js +1 -0
  32. package/dist/client/assets/useMutation-CFmVaBag.js +1 -0
  33. package/dist/client/assets/visuallyHidden-DCCICp6T.js +9 -0
  34. package/dist/server/assets/{_sessionKey-DVNrEYFh.js → _sessionKey-tRze5NLR.js} +493 -92
  35. package/dist/server/assets/_tanstack-start-manifest_v-CyfoMvUa.js +4 -0
  36. package/dist/server/assets/{agents-Dz_i76VW.js → agents-CmQ4vvXm.js} +1 -1
  37. package/dist/server/assets/{agents-screen-CqQPJndp.js → agents-screen-bmrIyFbk.js} +43 -35
  38. package/dist/server/assets/bots-Byt6jv0a.js +11 -0
  39. package/dist/server/assets/bots-screen-C2TGFv42.js +474 -0
  40. package/dist/server/assets/{button-DtQ3rV1m.js → button-CwY2OHFj.js} +2 -2
  41. package/dist/server/assets/{connect-CIDOw12K.js → connect-d3AqjAqe.js} +2 -2
  42. package/dist/server/assets/{file-explorer-screen-Cx0jiLRU.js → file-explorer-screen-CVlFiAFu.js} +39 -40
  43. package/dist/server/assets/{files-Trs1M5ba.js → files-BIEcSPGp.js} +1 -1
  44. package/dist/server/assets/{index-CmbNTqa2.js → index-CNIATlJ9.js} +93 -38
  45. package/dist/server/assets/{index-Dv2RXDa2.js → index-CRfLKh30.js} +2 -1
  46. package/dist/server/assets/{keyboard-shortcuts-dialog-7OEtXUlW.js → keyboard-shortcuts-dialog-CsNP85q8.js} +2 -2
  47. package/dist/server/assets/{router-OoQe2c20.js → router-rn7pJO_D.js} +515 -78
  48. package/dist/server/assets/{search-dialog-Bq0Pnxdb.js → search-dialog-Bz4Cu0KW.js} +23 -6
  49. package/dist/server/assets/{session-export-dialog-DRVbC8Q-.js → session-export-dialog-CwclV0Aj.js} +2 -2
  50. package/dist/server/assets/{settings-dialog-BsJsnMiu.js → settings-dialog-BBM7jCjE.js} +409 -95
  51. package/dist/server/assets/skills-Cy8xclXY.js +11 -0
  52. package/dist/server/assets/skills-panel-BnRNb7u9.js +762 -0
  53. package/dist/server/assets/{switch-DnX0MjGS.js → switch-BbkUeVDV.js} +1 -1
  54. package/dist/server/assets/tabs-DDFZob0m.js +67 -0
  55. package/dist/server/assets/{tooltip-gbV6rEVv.js → tooltip-DgsSPocE.js} +1 -1
  56. package/dist/server/assets/{use-file-explorer-state-DMHdtb7D.js → use-file-explorer-state-Il1LlBAe.js} +13 -1
  57. package/dist/server/server.js +2 -2
  58. package/package.json +6 -2
  59. package/dist/client/assets/_sessionKey-Z6Wcnj0N.js +0 -97
  60. package/dist/client/assets/agents-D8ZHVQ1Z.js +0 -2
  61. package/dist/client/assets/agents-screen-BVK0QTRH.js +0 -1
  62. package/dist/client/assets/button-CuH8u1uR.js +0 -1
  63. package/dist/client/assets/file-explorer-screen-DMUuR1uG.js +0 -1
  64. package/dist/client/assets/files-CDMLoJ0u.js +0 -2
  65. package/dist/client/assets/index-IGP-Igwt.js +0 -153
  66. package/dist/client/assets/keyboard-shortcuts-dialog-DW--4YLF.js +0 -1
  67. package/dist/client/assets/opencami-logo-ChD2XR2j.js +0 -1
  68. package/dist/client/assets/search-dialog-DYyFYJmw.js +0 -1
  69. package/dist/client/assets/session-export-dialog-BFizBmGb.js +0 -1
  70. package/dist/client/assets/settings-dialog-UvgVi2It.js +0 -1
  71. package/dist/client/assets/styles-BGTCU8mq.css +0 -1
  72. package/dist/client/assets/switch-DxW2OWPG.js +0 -1
  73. package/dist/client/assets/use-file-explorer-state-Cii59H70.js +0 -12
  74. package/dist/client/assets/useButton-hZdvKtl_.js +0 -9
  75. package/dist/server/assets/_tanstack-start-manifest_v-PwRq_yJS.js +0 -4
@@ -3,12 +3,12 @@ import { Link, useNavigate } from "@tanstack/react-router";
3
3
  import * as React from "react";
4
4
  import React__default, { memo, useDeferredValue, useState, useMemo, useCallback, Suspense, lazy, useRef, useEffect, useId, useLayoutEffect, createContext, useContext } from "react";
5
5
  import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
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";
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, 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, PackageOpenIcon, 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";
11
- import { B as Button, c as cn, b as buttonVariants } from "./button-DtQ3rV1m.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-Il1LlBAe.js";
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";
@@ -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-CNIATlJ9.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-OoQe2c20.js";
24
+ import { a as Route } from "./router-rn7pJO_D.js";
25
25
  function deriveFriendlyIdFromKey(key) {
26
26
  if (!key) return "main";
27
27
  const trimmed = key.trim();
@@ -758,7 +758,7 @@ function SessionItemComponent({
758
758
  "group inline-flex items-center justify-between",
759
759
  "w-full text-left pl-1.5 pr-0.5 min-h-8 py-1 rounded-lg transition-colors duration-0",
760
760
  "select-none",
761
- active ? "bg-primary-200 text-primary-950" : "bg-transparent text-primary-950 [&:hover:not(:has(button:hover))]:bg-primary-200"
761
+ active ? "bg-[var(--opencami-accent-light)] text-[var(--opencami-accent)]" : "bg-transparent text-primary-950 [&:hover:not(:has(button:hover))]:bg-primary-200"
762
762
  ),
763
763
  children: [
764
764
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [
@@ -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
  ] }),
@@ -1114,13 +1114,28 @@ const SidebarSessions = memo(function SidebarSessions2({
1114
1114
  `Delete ${selectedSessions.length} sessions? This will archive them.`
1115
1115
  );
1116
1116
  if (!confirmed) return;
1117
- const errors = await runWithConcurrency(
1118
- selectedSessions.map((session) => session.key),
1117
+ const failedSessionLabels = [];
1118
+ await runWithConcurrency(
1119
+ selectedSessions,
1119
1120
  10,
1120
- deleteSessionRequest
1121
+ async (session) => {
1122
+ try {
1123
+ await deleteSessionRequest(session.key);
1124
+ } catch (err) {
1125
+ failedSessionLabels.push(
1126
+ session.label || session.title || session.derivedTitle || session.friendlyId
1127
+ );
1128
+ throw err;
1129
+ }
1130
+ }
1121
1131
  );
1122
- if (errors.length > 0) {
1123
- console.error("[sidebar] Bulk delete failed for some sessions", errors);
1132
+ if (failedSessionLabels.length > 0) {
1133
+ console.error("[sidebar] Bulk delete failed for some sessions", failedSessionLabels);
1134
+ window.alert(
1135
+ `Could not delete ${failedSessionLabels.length} session${failedSessionLabels.length !== 1 ? "s" : ""}:
1136
+
1137
+ ${failedSessionLabels.join("\n")}`
1138
+ );
1124
1139
  }
1125
1140
  setSelectionMode(false);
1126
1141
  setSelectedSessionKeys(/* @__PURE__ */ new Set());
@@ -1649,10 +1664,10 @@ function useRenameSession() {
1649
1664
  return { renameSession, renaming, error };
1650
1665
  }
1651
1666
  const SettingsDialog = lazy(
1652
- () => import("./settings-dialog-BsJsnMiu.js").then((m) => ({ default: m.SettingsDialog }))
1667
+ () => import("./settings-dialog-BBM7jCjE.js").then((m) => ({ default: m.SettingsDialog }))
1653
1668
  );
1654
1669
  const SessionExportDialog = lazy(
1655
- () => import("./session-export-dialog-DRVbC8Q-.js").then((m) => ({
1670
+ () => import("./session-export-dialog-CwclV0Aj.js").then((m) => ({
1656
1671
  default: m.SessionExportDialog
1657
1672
  }))
1658
1673
  );
@@ -1762,7 +1777,7 @@ function ChatSidebarComponent({
1762
1777
  motion.aside,
1763
1778
  {
1764
1779
  initial: false,
1765
- animate: { width: isCollapsed ? 48 : 300 },
1780
+ animate: { width: isCollapsed ? 48 : "var(--opencami-sidebar-width)" },
1766
1781
  transition,
1767
1782
  className: asideProps.className,
1768
1783
  children: [
@@ -1893,6 +1908,48 @@ function ChatSidebarComponent({
1893
1908
  ) }),
1894
1909
  isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Files" })
1895
1910
  ] }) }),
1911
+ (() => {
1912
+ try {
1913
+ return localStorage.getItem("opencami-skills-browser") === "true";
1914
+ } catch {
1915
+ return false;
1916
+ }
1917
+ })() && /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { children: [
1918
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
1919
+ Link,
1920
+ {
1921
+ to: "/skills",
1922
+ className: cn(
1923
+ buttonVariants({ variant: "ghost", size: "sm" }),
1924
+ "w-full pl-1.5 justify-start"
1925
+ ),
1926
+ onClick: onSelectSession,
1927
+ children: [
1928
+ /* @__PURE__ */ jsx(
1929
+ HugeiconsIcon,
1930
+ {
1931
+ icon: PackageOpenIcon,
1932
+ size: 20,
1933
+ strokeWidth: 1.5,
1934
+ className: "min-w-5"
1935
+ }
1936
+ ),
1937
+ /* @__PURE__ */ jsx(AnimatePresence, { initial: false, mode: "wait", children: !isCollapsed && /* @__PURE__ */ jsx(
1938
+ motion.span,
1939
+ {
1940
+ initial: { opacity: 0 },
1941
+ animate: { opacity: 1 },
1942
+ exit: { opacity: 0 },
1943
+ transition,
1944
+ className: "overflow-hidden whitespace-nowrap",
1945
+ children: "Skills"
1946
+ }
1947
+ ) })
1948
+ ]
1949
+ }
1950
+ ) }),
1951
+ isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Skills" })
1952
+ ] }) }),
1896
1953
  (() => {
1897
1954
  try {
1898
1955
  return localStorage.getItem("opencami-agent-manager") === "true";
@@ -1934,6 +1991,48 @@ function ChatSidebarComponent({
1934
1991
  }
1935
1992
  ) }),
1936
1993
  isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Agents" })
1994
+ ] }) }),
1995
+ (() => {
1996
+ try {
1997
+ return localStorage.getItem("opencami-cron-manager") === "true";
1998
+ } catch {
1999
+ return false;
2000
+ }
2001
+ })() && /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { children: [
2002
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
2003
+ Link,
2004
+ {
2005
+ to: "/bots",
2006
+ className: cn(
2007
+ buttonVariants({ variant: "ghost", size: "sm" }),
2008
+ "w-full pl-1.5 justify-start"
2009
+ ),
2010
+ onClick: onSelectSession,
2011
+ children: [
2012
+ /* @__PURE__ */ jsx(
2013
+ HugeiconsIcon,
2014
+ {
2015
+ icon: SmartPhone01Icon,
2016
+ size: 20,
2017
+ strokeWidth: 1.5,
2018
+ className: "min-w-5"
2019
+ }
2020
+ ),
2021
+ /* @__PURE__ */ jsx(AnimatePresence, { initial: false, mode: "wait", children: !isCollapsed && /* @__PURE__ */ jsx(
2022
+ motion.span,
2023
+ {
2024
+ initial: { opacity: 0 },
2025
+ animate: { opacity: 1 },
2026
+ exit: { opacity: 0 },
2027
+ transition,
2028
+ className: "overflow-hidden whitespace-nowrap",
2029
+ children: "Cron Jobs"
2030
+ }
2031
+ ) })
2032
+ ]
2033
+ }
2034
+ ) }),
2035
+ isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Cron Jobs" })
1937
2036
  ] }) })
1938
2037
  ]
1939
2038
  }
@@ -2281,7 +2380,8 @@ function ChatHeaderComponent({
2281
2380
  "div",
2282
2381
  {
2283
2382
  ref: wrapperRef,
2284
- className: "border-b border-primary-200 px-4 h-12 flex items-center bg-surface",
2383
+ "data-tauri-drag-region": true,
2384
+ className: "border-b border-primary-200 px-4 h-12 flex min-w-0 items-center overflow-x-hidden bg-surface tauri-drag-header",
2285
2385
  children: [
2286
2386
  showSidebarButton ? /* @__PURE__ */ jsx(
2287
2387
  Button,
@@ -2290,11 +2390,12 @@ function ChatHeaderComponent({
2290
2390
  variant: "ghost",
2291
2391
  onClick: onOpenSidebar,
2292
2392
  className: "mr-2 text-primary-800 hover:bg-primary-100",
2393
+ style: { WebkitAppRegion: "no-drag" },
2293
2394
  "aria-label": "Open sidebar",
2294
2395
  children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: Menu01Icon, size: 18, strokeWidth: 1.6 })
2295
2396
  }
2296
2397
  ) : null,
2297
- /* @__PURE__ */ jsx("div", { className: "text-sm font-medium truncate flex-1 min-w-0", children: activeTitle }),
2398
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-w-0 overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium", children: activeTitle }) }),
2298
2399
  /* @__PURE__ */ jsx(
2299
2400
  MemoizedContextMeter,
2300
2401
  {
@@ -2523,6 +2624,108 @@ function MessageActionsBar({
2523
2624
  }
2524
2625
  );
2525
2626
  }
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
+ }
2526
2729
  function markdownHrefToFilePath(href) {
2527
2730
  if (!href?.startsWith("openclaw-file://")) return null;
2528
2731
  try {
@@ -2531,6 +2734,39 @@ function markdownHrefToFilePath(href) {
2531
2734
  return null;
2532
2735
  }
2533
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
+ }
2534
2770
  const EXTENSION_LANGUAGE_MAP = {
2535
2771
  py: "python",
2536
2772
  ts: "typescript",
@@ -2574,27 +2810,67 @@ function languageFromFilePath(path) {
2574
2810
  return resolveLanguage(mapped);
2575
2811
  }
2576
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
+ }
2577
2843
  function parseMarkdownIntoBlocks(markdown) {
2578
2844
  const tokens = marked.lexer(markdown);
2579
2845
  return tokens.map((token) => token.raw);
2580
2846
  }
2581
2847
  function extractLanguage(className) {
2582
2848
  if (!className) return "text";
2583
- const match = className.match(/language-(\w+)/);
2849
+ const match = className.match(/language-([\w-]+)/);
2584
2850
  return match ? match[1] : "text";
2585
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
+ }
2586
2858
  const BASE_COMPONENTS = {
2587
- code: function CodeComponent({ className, children }) {
2859
+ code: function CodeComponent({ className, children, node }) {
2588
2860
  const isInline = !className?.includes("language-");
2589
2861
  if (isInline) {
2590
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 });
2591
2863
  }
2592
2864
  const language = extractLanguage(className);
2865
+ const filename = extractFilenameFromMeta(
2866
+ node?.data?.meta
2867
+ );
2593
2868
  return /* @__PURE__ */ jsx(
2594
2869
  CodeBlock,
2595
2870
  {
2596
2871
  content: String(children ?? ""),
2597
2872
  language,
2873
+ filename,
2598
2874
  className: "w-full"
2599
2875
  }
2600
2876
  );
@@ -2654,19 +2930,18 @@ const BASE_COMPONENTS = {
2654
2930
  return /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-primary-950", children });
2655
2931
  }
2656
2932
  };
2657
- function createDefaultComponents(onOpenFilePreview, inlineFilePreviewEnabled) {
2658
- const FILE_PATH_RE = /^(?:~\/[\w.\-\/]+|\/(?:[\w.\-]+\/)+[\w.\-]+)$/;
2933
+ function createDefaultComponents(onOpenFilePreview) {
2659
2934
  return {
2660
2935
  ...BASE_COMPONENTS,
2661
2936
  a: function AComponent({ children, href }) {
2662
2937
  const filePath = markdownHrefToFilePath(href);
2663
- if (inlineFilePreviewEnabled && filePath) {
2938
+ if (filePath) {
2664
2939
  return /* @__PURE__ */ jsx(
2665
2940
  "button",
2666
2941
  {
2667
2942
  type: "button",
2668
2943
  onClick: () => onOpenFilePreview(filePath),
2669
- className: "font-mono text-primary-900 underline decoration-primary-300 underline-offset-4 hover:decoration-primary-600 cursor-pointer",
2944
+ className: "font-mono text-[var(--opencami-accent)] underline decoration-[var(--opencami-accent-light)] underline-offset-4 hover:opacity-90 cursor-pointer",
2670
2945
  children
2671
2946
  }
2672
2947
  );
@@ -2675,17 +2950,34 @@ function createDefaultComponents(onOpenFilePreview, inlineFilePreviewEnabled) {
2675
2950
  "a",
2676
2951
  {
2677
2952
  href,
2678
- className: "text-primary-950 underline decoration-primary-300 underline-offset-4 transition-colors hover:text-primary-950 hover:decoration-primary-500",
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",
2679
2957
  target: "_blank",
2680
2958
  rel: "noopener noreferrer",
2681
2959
  children
2682
2960
  }
2683
2961
  );
2684
2962
  },
2685
- code: function InlineCodeComponent({ children, className }) {
2686
- if (className) return /* @__PURE__ */ jsx("code", { className, children });
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
+ }
2687
2979
  const text = typeof children === "string" ? children : Array.isArray(children) ? children.filter((c) => typeof c === "string").join("") : String(children ?? "");
2688
- if (inlineFilePreviewEnabled && text && FILE_PATH_RE.test(text)) {
2980
+ if (text && isLikelyFilePath(text)) {
2689
2981
  return /* @__PURE__ */ jsx(
2690
2982
  "button",
2691
2983
  {
@@ -2708,7 +3000,7 @@ const MemoizedMarkdownBlock = memo(
2708
3000
  return /* @__PURE__ */ jsx(
2709
3001
  ReactMarkdown,
2710
3002
  {
2711
- remarkPlugins: [remarkGfm, remarkBreaks],
3003
+ remarkPlugins: [remarkGfm, remarkBreaks, remarkFilePathLinks],
2712
3004
  components,
2713
3005
  children: content
2714
3006
  }
@@ -2735,15 +3027,24 @@ function MarkdownComponent({
2735
3027
  const blockId = id ?? generatedId;
2736
3028
  const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]);
2737
3029
  const [filePreview, setFilePreview] = useState({ status: "idle" });
2738
- const inlineFilePreviewEnabled = useChatSettingsStore(
2739
- (state) => state.settings.inlineFilePreview
2740
- );
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]);
2741
3045
  const defaultComponents = useMemo(
2742
- () => createDefaultComponents(
2743
- (path) => setFilePreview({ status: "loading", path }),
2744
- inlineFilePreviewEnabled
2745
- ),
2746
- [inlineFilePreviewEnabled]
3046
+ () => createDefaultComponents(onOpenFilePreview),
3047
+ [onOpenFilePreview]
2747
3048
  );
2748
3049
  const mergedComponents = useMemo(
2749
3050
  () => ({ ...defaultComponents, ...components || {} }),
@@ -2758,6 +3059,10 @@ function MarkdownComponent({
2758
3059
  }).then(async (response) => {
2759
3060
  const payload = await response.json().catch(() => ({}));
2760
3061
  if (!response.ok) {
3062
+ if (isDirectoryError(payload.code, payload.message)) {
3063
+ openDirectoryInExplorer(path);
3064
+ return;
3065
+ }
2761
3066
  const error = fileErrorMessageFromResponse(response.status, payload.code);
2762
3067
  setFilePreview({ status: "error", path, message: error });
2763
3068
  return;
@@ -2779,17 +3084,33 @@ function MarkdownComponent({
2779
3084
  setFilePreview({ status: "error", path, message: "Failed to load file preview" });
2780
3085
  });
2781
3086
  return () => controller.abort();
2782
- }, [filePreview]);
3087
+ }, [filePreview, openDirectoryInExplorer]);
2783
3088
  const previewOpen = filePreview.status !== "idle";
2784
3089
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2785
- /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-2", className), children: blocks.map((block, index) => /* @__PURE__ */ jsx(
2786
- MemoizedMarkdownBlock,
3090
+ /* @__PURE__ */ jsx(
3091
+ "div",
2787
3092
  {
2788
- content: block,
2789
- components: mergedComponents
2790
- },
2791
- `${blockId}-block-${index}`
2792
- )) }),
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
+ ),
2793
3114
  /* @__PURE__ */ jsx(
2794
3115
  DialogRoot,
2795
3116
  {
@@ -2809,21 +3130,28 @@ function MarkdownComponent({
2809
3130
  {
2810
3131
  to: "/files",
2811
3132
  onClick: () => {
2812
- let p = filePreview.status !== "idle" ? filePreview.path : "";
3133
+ const p = filePreview.status !== "idle" ? toWorkspacePath(filePreview.path) : "";
2813
3134
  if (p) {
2814
- const prefixes = ["/root/clawd/", "/root/"];
2815
- for (const prefix of prefixes) {
2816
- if (p.startsWith(prefix)) {
2817
- p = "/" + p.slice(prefix.length);
2818
- break;
2819
- }
2820
- }
2821
3135
  const dir = p.includes("/") ? p.slice(0, p.lastIndexOf("/")) || "/" : "/";
2822
3136
  useFileExplorerState.getState().navigateTo(dir);
2823
3137
  }
2824
3138
  setFilePreview({ status: "idle" });
2825
3139
  },
2826
- children: "Open in File Explorer"
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"
2827
3155
  }
2828
3156
  ) }),
2829
3157
  /* @__PURE__ */ jsx(DialogClose, { children: "Close" })
@@ -2858,7 +3186,7 @@ function MessageContent({
2858
3186
  ...props
2859
3187
  }) {
2860
3188
  const classNames = cn(
2861
- "rounded-[12px] break-words whitespace-normal min-w-0",
3189
+ "rounded-[12px] break-words whitespace-normal min-w-0 max-w-full overflow-x-hidden",
2862
3190
  className
2863
3191
  );
2864
3192
  return markdown ? /* @__PURE__ */ jsx(Markdown, { className: classNames, ...props, children }) : /* @__PURE__ */ jsx("div", { className: classNames, ...props, children });
@@ -3183,7 +3511,9 @@ function MessageItemComponent({
3183
3511
  aggregatedSearchSources,
3184
3512
  wrapperRef,
3185
3513
  wrapperClassName,
3186
- wrapperScrollMarginTop
3514
+ wrapperScrollMarginTop,
3515
+ messageDomId,
3516
+ highlighted = false
3187
3517
  }) {
3188
3518
  const { settings } = useChatSettings$1();
3189
3519
  const role = message.role || "assistant";
@@ -3199,18 +3529,21 @@ function MessageItemComponent({
3199
3529
  "div",
3200
3530
  {
3201
3531
  ref: wrapperRef,
3532
+ id: messageDomId,
3533
+ "data-message-id": messageDomId,
3202
3534
  style: {
3203
3535
  contentVisibility: "auto",
3204
3536
  containIntrinsicSize: "auto 120px",
3205
3537
  ...typeof wrapperScrollMarginTop === "number" ? { scrollMarginTop: `${wrapperScrollMarginTop}px` } : void 0
3206
3538
  },
3207
3539
  className: cn(
3208
- "group flex flex-col gap-1",
3540
+ "opencami-message-item group mx-auto flex w-full max-w-[var(--opencami-chat-width)] flex-col gap-1 py-[var(--opencami-msg-padding-y)]",
3209
3541
  wrapperClassName,
3542
+ highlighted && "opencami-message-highlight",
3210
3543
  isUser ? "items-end" : "items-start"
3211
3544
  ),
3212
3545
  children: [
3213
- thinking && settings.showReasoningBlocks && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[900px]", children: /* @__PURE__ */ jsx(Thinking, { content: thinking }) }),
3546
+ thinking && settings.showReasoningBlocks && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(Thinking, { content: thinking }) }),
3214
3547
  images.length > 0 && /* @__PURE__ */ jsx("div", { className: cn(
3215
3548
  "flex flex-wrap gap-2 mb-2",
3216
3549
  isUser ? "justify-end" : "justify-start"
@@ -3225,19 +3558,19 @@ function MessageItemComponent({
3225
3558
  },
3226
3559
  idx
3227
3560
  )) }),
3228
- /* @__PURE__ */ jsx(Message, { className: cn(isUser ? "flex-row-reverse" : ""), children: /* @__PURE__ */ jsx(
3561
+ /* @__PURE__ */ jsx(Message, { className: cn("min-w-0 max-w-full", isUser ? "flex-row-reverse" : ""), children: /* @__PURE__ */ jsx(
3229
3562
  MessageContent,
3230
3563
  {
3231
3564
  markdown: !isUser,
3232
3565
  className: cn(
3233
- "text-primary-900 opencami-text-size",
3234
- !isUser ? "bg-transparent w-full" : "bg-primary-100 px-4 py-2.5 max-w-[85%]",
3566
+ "text-primary-900 opencami-text-size min-w-0 max-w-full",
3567
+ isUser ? "opencami-message-user bg-primary-100 px-4 py-[var(--opencami-user-bubble-py)] max-w-[85%]" : "opencami-message-assistant bg-transparent w-full",
3235
3568
  !isUser && isStreaming && "stream-fade-in"
3236
3569
  ),
3237
3570
  children: text
3238
3571
  }
3239
3572
  ) }),
3240
- hasToolCalls && settings.showToolMessages && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[900px] mt-2 flex flex-col gap-3", children: toolCalls.map((toolCall) => {
3573
+ hasToolCalls && settings.showToolMessages && /* @__PURE__ */ jsx("div", { className: "mt-2 flex w-full min-w-0 max-w-[var(--opencami-chat-width)] flex-col gap-3 overflow-x-hidden", children: toolCalls.map((toolCall) => {
3241
3574
  const resultMessage = toolCall.id ? toolResultsByCallId?.get(toolCall.id) : void 0;
3242
3575
  const toolPart = mapToolCallToToolPart(toolCall, resultMessage);
3243
3576
  return /* @__PURE__ */ jsx(
@@ -3249,7 +3582,7 @@ function MessageItemComponent({
3249
3582
  toolCall.id || toolCall.name
3250
3583
  );
3251
3584
  }) }),
3252
- searchSources.length > 0 && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[900px]", children: /* @__PURE__ */ jsx(SearchSourcesBadge, { sources: searchSources }) }),
3585
+ searchSources.length > 0 && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(SearchSourcesBadge, { sources: searchSources }) }),
3253
3586
  !hasToolCalls && /* @__PURE__ */ jsx(
3254
3587
  MessageActionsBar,
3255
3588
  {
@@ -3275,6 +3608,8 @@ function areMessagesEqual(prevProps, nextProps) {
3275
3608
  if (prevProps.wrapperScrollMarginTop !== nextProps.wrapperScrollMarginTop) {
3276
3609
  return false;
3277
3610
  }
3611
+ if (prevProps.messageDomId !== nextProps.messageDomId) return false;
3612
+ if (prevProps.highlighted !== nextProps.highlighted) return false;
3278
3613
  if ((prevProps.message.role || "assistant") !== (nextProps.message.role || "assistant")) {
3279
3614
  return false;
3280
3615
  }
@@ -3854,12 +4189,12 @@ function ChatContainerShell({
3854
4189
  /* @__PURE__ */ jsx(
3855
4190
  ScrollAreaViewport,
3856
4191
  {
3857
- className: "relative will-change-transform",
4192
+ className: "relative will-change-transform overflow-x-hidden",
3858
4193
  ref: viewportRef,
3859
4194
  ...viewportProps
3860
4195
  }
3861
4196
  ),
3862
- /* @__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 }) }) }),
4197
+ /* @__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 }) }) }),
3863
4198
  /* @__PURE__ */ jsx(ScrollAreaScrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollAreaThumb, {}) }),
3864
4199
  /* @__PURE__ */ jsx(ScrollAreaCorner, {})
3865
4200
  ]
@@ -3897,7 +4232,7 @@ function ChatContainerPortal({
3897
4232
  }) {
3898
4233
  if (!viewportNode) return null;
3899
4234
  return createPortal(
3900
- /* @__PURE__ */ jsx("div", { className: "relative flex w-full flex-col", children }),
4235
+ /* @__PURE__ */ jsx("div", { className: "relative flex w-full min-w-0 max-w-full flex-col overflow-x-hidden", children }),
3901
4236
  viewportNode
3902
4237
  );
3903
4238
  }
@@ -3946,9 +4281,9 @@ function ChatContainerContent({
3946
4281
  return /* @__PURE__ */ jsx(
3947
4282
  "div",
3948
4283
  {
3949
- className: cn("flex w-full flex-col min-h-full", className),
4284
+ className: cn("flex w-full min-w-0 max-w-full flex-col min-h-full overflow-x-hidden", className),
3950
4285
  ...props,
3951
- 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 }) })
4286
+ 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 }) })
3952
4287
  }
3953
4288
  );
3954
4289
  }
@@ -4022,13 +4357,15 @@ function ChatMessageListComponent({
4022
4357
  pinGroupMinHeight,
4023
4358
  headerHeight,
4024
4359
  contentStyle,
4025
- onFollowUpClick
4360
+ onFollowUpClick,
4361
+ jumpToMessageId
4026
4362
  }) {
4027
4363
  const anchorRef = useRef(null);
4028
4364
  const lastUserRef = useRef(null);
4029
4365
  const programmaticScroll = useRef(false);
4030
4366
  const prevPinRef = useRef(pinToTop);
4031
4367
  const prevUserIndexRef = useRef(void 0);
4368
+ const [highlightedMessageId, setHighlightedMessageId] = useState(null);
4032
4369
  const displayMessages = useMemo(() => {
4033
4370
  return messages.filter((msg) => msg.role !== "toolResult");
4034
4371
  }, [messages]);
@@ -4145,13 +4482,25 @@ function ChatMessageListComponent({
4145
4482
  }, 0);
4146
4483
  }
4147
4484
  }, [loading, displayMessages.length, sessionKey, pinToTop, lastUserIndex]);
4485
+ useEffect(() => {
4486
+ if (!jumpToMessageId || loading) return;
4487
+ const target = document.getElementById(`message-${jumpToMessageId}`);
4488
+ if (!target) return;
4489
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
4490
+ setHighlightedMessageId(jumpToMessageId);
4491
+ const timer = window.setTimeout(() => {
4492
+ setHighlightedMessageId((prev) => prev === jumpToMessageId ? null : prev);
4493
+ }, 1800);
4494
+ return () => window.clearTimeout(timer);
4495
+ }, [jumpToMessageId, loading, displayMessages]);
4148
4496
  return (
4149
4497
  // mt-2 is to fix the prompt-input cut off
4150
4498
  /* @__PURE__ */ jsx(MemoizedChatContainerRoot, { className: "flex-1 min-h-0 -mb-4", children: /* @__PURE__ */ jsxs(ChatContainerContent, { className: "pt-6", style: contentStyle, children: [
4151
4499
  notice && noticePosition === "start" ? notice : null,
4152
4500
  empty && !notice ? emptyState ?? /* @__PURE__ */ jsx("div", { "aria-hidden": true }) : hasGroup ? /* @__PURE__ */ jsxs(Fragment, { children: [
4153
4501
  displayMessages.slice(0, groupStartIndex).map((chatMessage, index) => {
4154
- const messageKey = index;
4502
+ const messageId = typeof chatMessage.id === "string" ? chatMessage.id : void 0;
4503
+ const messageKey = messageId || index;
4155
4504
  const forceActionsVisible = typeof lastAssistantIndex === "number" && index === lastAssistantIndex;
4156
4505
  const isLastAssistant = typeof lastAssistantIndex === "number" && index === lastAssistantIndex;
4157
4506
  const hasToolCalls = chatMessage.role === "assistant" && getToolCallsFromMessage(chatMessage).length > 0;
@@ -4163,7 +4512,9 @@ function ChatMessageListComponent({
4163
4512
  forceActionsVisible,
4164
4513
  isStreaming: isLastAssistant && isStreaming,
4165
4514
  isLastAssistant,
4166
- aggregatedSearchSources: isLastAssistant ? aggregatedSearchSources : void 0
4515
+ aggregatedSearchSources: isLastAssistant ? aggregatedSearchSources : void 0,
4516
+ messageDomId: messageId ? `message-${messageId}` : void 0,
4517
+ highlighted: highlightedMessageId === messageId
4167
4518
  },
4168
4519
  messageKey
4169
4520
  );
@@ -4171,12 +4522,13 @@ function ChatMessageListComponent({
4171
4522
  /* @__PURE__ */ jsxs(
4172
4523
  "div",
4173
4524
  {
4174
- className: "flex flex-col space-y-3 md:space-y-6",
4175
- style: { minHeight: `${Math.max(0, pinGroupMinHeight - 24)}px` },
4525
+ className: "flex flex-col gap-[var(--opencami-msg-gap)]",
4526
+ style: { minHeight: `${Math.max(0, pinGroupMinHeight)}px` },
4176
4527
  children: [
4177
4528
  displayMessages.slice(groupStartIndex).map((chatMessage, index) => {
4178
4529
  const realIndex = groupStartIndex + index;
4179
- const messageKey = realIndex;
4530
+ const messageId = typeof chatMessage.id === "string" ? chatMessage.id : void 0;
4531
+ const messageKey = messageId || realIndex;
4180
4532
  const forceActionsVisible = typeof lastAssistantIndex === "number" && realIndex === lastAssistantIndex;
4181
4533
  const isLastAssistant = typeof lastAssistantIndex === "number" && realIndex === lastAssistantIndex;
4182
4534
  const wrapperRef = realIndex === lastUserIndex ? lastUserRef : void 0;
@@ -4194,7 +4546,9 @@ function ChatMessageListComponent({
4194
4546
  aggregatedSearchSources: isLastAssistant ? aggregatedSearchSources : void 0,
4195
4547
  wrapperRef,
4196
4548
  wrapperClassName,
4197
- wrapperScrollMarginTop
4549
+ wrapperScrollMarginTop,
4550
+ messageDomId: messageId ? `message-${messageId}` : void 0,
4551
+ highlighted: highlightedMessageId === messageId
4198
4552
  },
4199
4553
  messageKey
4200
4554
  );
@@ -4213,7 +4567,8 @@ function ChatMessageListComponent({
4213
4567
  )
4214
4568
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4215
4569
  displayMessages.map((chatMessage, index) => {
4216
- const messageKey = index;
4570
+ const messageId = typeof chatMessage.id === "string" ? chatMessage.id : void 0;
4571
+ const messageKey = messageId || index;
4217
4572
  const forceActionsVisible = typeof lastAssistantIndex === "number" && index === lastAssistantIndex;
4218
4573
  const isLastAssistant = typeof lastAssistantIndex === "number" && index === lastAssistantIndex;
4219
4574
  const hasToolCalls = chatMessage.role === "assistant" && getToolCallsFromMessage(chatMessage).length > 0;
@@ -4225,7 +4580,9 @@ function ChatMessageListComponent({
4225
4580
  forceActionsVisible,
4226
4581
  isStreaming: isLastAssistant && isStreaming,
4227
4582
  isLastAssistant,
4228
- aggregatedSearchSources: isLastAssistant ? aggregatedSearchSources : void 0
4583
+ aggregatedSearchSources: isLastAssistant ? aggregatedSearchSources : void 0,
4584
+ messageDomId: messageId ? `message-${messageId}` : void 0,
4585
+ highlighted: highlightedMessageId === messageId
4229
4586
  },
4230
4587
  messageKey
4231
4588
  );
@@ -4250,7 +4607,7 @@ function ChatMessageListComponent({
4250
4607
  );
4251
4608
  }
4252
4609
  function areChatMessageListEqual(prev, next) {
4253
- return prev.messages === next.messages && prev.loading === next.loading && prev.empty === next.empty && prev.emptyState === next.emptyState && prev.notice === next.notice && prev.noticePosition === next.noticePosition && prev.waitingForResponse === next.waitingForResponse && prev.isStreaming === next.isStreaming && prev.sessionKey === next.sessionKey && prev.pinToTop === next.pinToTop && prev.pinGroupMinHeight === next.pinGroupMinHeight && prev.headerHeight === next.headerHeight && prev.contentStyle === next.contentStyle && prev.onFollowUpClick === next.onFollowUpClick;
4610
+ return prev.messages === next.messages && prev.loading === next.loading && prev.empty === next.empty && prev.emptyState === next.emptyState && prev.notice === next.notice && prev.noticePosition === next.noticePosition && prev.waitingForResponse === next.waitingForResponse && prev.isStreaming === next.isStreaming && prev.sessionKey === next.sessionKey && prev.pinToTop === next.pinToTop && prev.pinGroupMinHeight === next.pinGroupMinHeight && prev.headerHeight === next.headerHeight && prev.contentStyle === next.contentStyle && prev.onFollowUpClick === next.onFollowUpClick && prev.jumpToMessageId === next.jumpToMessageId;
4254
4611
  }
4255
4612
  const MemoizedChatMessageList = memo(
4256
4613
  ChatMessageListComponent,
@@ -5584,7 +5941,7 @@ function ChatComposerComponent({
5584
5941
  return /* @__PURE__ */ jsxs(
5585
5942
  "div",
5586
5943
  {
5587
- className: "mx-auto w-full max-w-full px-2 md:px-5 sm:max-w-[768px] sm:min-w-[400px] relative pb-1 md:pb-3",
5944
+ className: "mx-auto w-full max-w-[var(--opencami-chat-width)] px-2 md:px-5 relative pb-1 md:pb-3",
5588
5945
  ref: wrapperRef,
5589
5946
  onDragOver: handleDragOver,
5590
5947
  onDragLeave: handleDragLeave,
@@ -5687,7 +6044,7 @@ function MessageStatus({
5687
6044
  onAction,
5688
6045
  className
5689
6046
  }) {
5690
- return /* @__PURE__ */ jsx("div", { className: cn("w-full max-w-[900px]", className), children: /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsxs("div", { className: "w-full rounded-xl border border-primary-200 bg-primary-50 p-4 text-primary-900", children: [
6047
+ return /* @__PURE__ */ jsx("div", { className: cn("w-full max-w-[var(--opencami-chat-width)]", className), children: /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsxs("div", { className: "w-full rounded-xl border border-primary-200 bg-primary-50 p-4 text-primary-900", children: [
5691
6048
  /* @__PURE__ */ jsx("div", { className: "text-balance font-medium", children: title }),
5692
6049
  /* @__PURE__ */ jsx("div", { className: "mt-2 text-pretty text-primary-700", children: description }),
5693
6050
  detail ? /* @__PURE__ */ jsx("div", { className: "mt-2 text-xs text-primary-600", children: detail }) : null,
@@ -6265,15 +6622,16 @@ function useSwipeGesture(options) {
6265
6622
  };
6266
6623
  }
6267
6624
  const KeyboardShortcutsDialog = lazy(
6268
- () => import("./keyboard-shortcuts-dialog-7OEtXUlW.js").then((m) => ({
6625
+ () => import("./keyboard-shortcuts-dialog-CsNP85q8.js").then((m) => ({
6269
6626
  default: m.KeyboardShortcutsDialog
6270
6627
  }))
6271
6628
  );
6272
6629
  const SearchDialog = lazy(
6273
- () => import("./search-dialog-Bq0Pnxdb.js").then((m) => ({
6630
+ () => import("./search-dialog-Bz4Cu0KW.js").then((m) => ({
6274
6631
  default: m.SearchDialog
6275
6632
  }))
6276
6633
  );
6634
+ const SEARCH_JUMP_TARGET_KEY = "opencami-search-jump-target";
6277
6635
  function ChatScreen({
6278
6636
  activeFriendlyId,
6279
6637
  isNewChat = false,
@@ -6296,6 +6654,7 @@ function ChatScreen({
6296
6654
  const [showShortcutsHelp, setShowShortcutsHelp] = useState(false);
6297
6655
  const [showSearchDialog, setShowSearchDialog] = useState(false);
6298
6656
  const [searchMode, setSearchMode] = useState("global");
6657
+ const [searchJumpMessageId, setSearchJumpMessageId] = useState(null);
6299
6658
  const [isStreaming, setIsStreaming] = useState(false);
6300
6659
  const thinkingLevel = useThinkingLevelStore((state) => state.level);
6301
6660
  const inputRef = useRef(null);
@@ -6608,6 +6967,10 @@ function ChatScreen({
6608
6967
  useEffect(() => {
6609
6968
  const resetKey = isNewChat ? "new" : activeFriendlyId;
6610
6969
  if (!resetKey) return;
6970
+ streamStop();
6971
+ stopStream();
6972
+ lastAssistantSignature.current = "";
6973
+ setIsStreaming(false);
6611
6974
  if (pendingStartRef.current) {
6612
6975
  pendingStartRef.current = false;
6613
6976
  return;
@@ -6617,13 +6980,34 @@ function ChatScreen({
6617
6980
  setPinToTop(true);
6618
6981
  return;
6619
6982
  }
6620
- streamStop();
6621
- stopStream();
6622
- lastAssistantSignature.current = "";
6623
6983
  setWaitingForResponse(false);
6624
6984
  setPinToTop(false);
6625
- setIsStreaming(false);
6626
6985
  }, [activeFriendlyId, isNewChat, streamStop, stopStream]);
6986
+ useEffect(() => {
6987
+ if (typeof window === "undefined") return;
6988
+ try {
6989
+ const raw = sessionStorage.getItem(SEARCH_JUMP_TARGET_KEY);
6990
+ if (!raw) {
6991
+ setSearchJumpMessageId(null);
6992
+ return;
6993
+ }
6994
+ const parsed = JSON.parse(raw);
6995
+ const isStale = typeof parsed.at === "number" && Date.now() - parsed.at > 5 * 60 * 1e3;
6996
+ if (isStale) {
6997
+ sessionStorage.removeItem(SEARCH_JUMP_TARGET_KEY);
6998
+ setSearchJumpMessageId(null);
6999
+ return;
7000
+ }
7001
+ if (parsed.friendlyId === activeFriendlyId && typeof parsed.messageId === "string" && parsed.messageId.length > 0) {
7002
+ setSearchJumpMessageId(parsed.messageId);
7003
+ sessionStorage.removeItem(SEARCH_JUMP_TARGET_KEY);
7004
+ return;
7005
+ }
7006
+ setSearchJumpMessageId(null);
7007
+ } catch {
7008
+ setSearchJumpMessageId(null);
7009
+ }
7010
+ }, [activeFriendlyId]);
6627
7011
  useLayoutEffect(() => {
6628
7012
  if (isNewChat) return;
6629
7013
  const pending = consumePendingSend(
@@ -6945,7 +7329,7 @@ function ChatScreen({
6945
7329
  {
6946
7330
  className: cn(
6947
7331
  "h-full overflow-hidden",
6948
- isMobile ? "relative" : "grid grid-cols-[auto_1fr]"
7332
+ isMobile ? "relative" : "grid grid-cols-[auto_minmax(0,1fr)]"
6949
7333
  ),
6950
7334
  children: [
6951
7335
  hideUi ? null : isMobile ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -6964,7 +7348,7 @@ function ChatScreen({
6964
7348
  "div",
6965
7349
  {
6966
7350
  className: cn(
6967
- "fixed inset-y-0 left-0 z-50 w-[300px] transition-transform duration-150 safe-area-top",
7351
+ "fixed inset-y-0 left-0 z-50 w-[var(--opencami-sidebar-width)] transition-transform duration-150 safe-area-top",
6968
7352
  isSidebarCollapsed ? "-translate-x-full" : "translate-x-0"
6969
7353
  ),
6970
7354
  ...sidebarCloseSwipeHandlers,
@@ -6975,7 +7359,7 @@ function ChatScreen({
6975
7359
  /* @__PURE__ */ jsxs(
6976
7360
  "main",
6977
7361
  {
6978
- className: "flex flex-col h-full min-h-0",
7362
+ className: "flex flex-col h-full min-h-0 min-w-0 overflow-x-hidden",
6979
7363
  ref: mainRef,
6980
7364
  ...sidebarSwipeHandlers,
6981
7365
  children: [
@@ -7006,7 +7390,8 @@ function ChatScreen({
7006
7390
  pinGroupMinHeight,
7007
7391
  headerHeight,
7008
7392
  contentStyle: stableContentStyle,
7009
- onFollowUpClick: handleFollowUpClick
7393
+ onFollowUpClick: handleFollowUpClick,
7394
+ jumpToMessageId: searchJumpMessageId
7010
7395
  }
7011
7396
  ),
7012
7397
  /* @__PURE__ */ jsx(
@@ -7044,9 +7429,25 @@ function ChatScreen({
7044
7429
  mode: searchMode,
7045
7430
  onJumpToMessage: (result) => {
7046
7431
  setShowSearchDialog(false);
7047
- if (result.friendlyId) {
7048
- navigate({ to: "/chat/$sessionKey", params: { sessionKey: result.friendlyId } });
7432
+ if (!result.friendlyId) return;
7433
+ if (result.messageId && result.friendlyId === activeFriendlyId) {
7434
+ setSearchJumpMessageId(result.messageId);
7435
+ return;
7436
+ }
7437
+ if (result.messageId && typeof window !== "undefined") {
7438
+ try {
7439
+ sessionStorage.setItem(
7440
+ SEARCH_JUMP_TARGET_KEY,
7441
+ JSON.stringify({
7442
+ friendlyId: result.friendlyId,
7443
+ messageId: result.messageId,
7444
+ at: Date.now()
7445
+ })
7446
+ );
7447
+ } catch {
7448
+ }
7049
7449
  }
7450
+ navigate({ to: "/chat/$sessionKey", params: { sessionKey: result.friendlyId } });
7050
7451
  }
7051
7452
  }
7052
7453
  ) })