opencami 1.8.6 → 1.8.8

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 (89) hide show
  1. package/README.md +0 -1
  2. package/dist/client/assets/{CSPContext-6t3O1emU.js → CSPContext-nHSyQniZ.js} +1 -1
  3. package/dist/client/assets/{DirectionContext-C6goXEY_.js → DirectionContext-B1cuzwIr.js} +1 -1
  4. package/dist/client/assets/_sessionKey-CNw4O2rY.js +19 -0
  5. package/dist/client/assets/agents-C6Ev94B1.js +2 -0
  6. package/dist/client/assets/agents-screen-DpNSh5Ok.js +1 -0
  7. package/dist/client/assets/bots-B2_u-OmP.js +2 -0
  8. package/dist/client/assets/bots-screen-Qh_IK9hC.js +1 -0
  9. package/dist/client/assets/button-DdG8c-XQ.js +1 -0
  10. package/dist/client/assets/{composite-feK0c-xF.js → composite-Cx-QHT9o.js} +1 -1
  11. package/dist/client/assets/{connect-02tmQV_v.js → connect-CSbeSBTn.js} +1 -1
  12. package/dist/client/assets/{dashboard-DQ0zDQKd.js → dashboard-DA1fTRcH.js} +1 -1
  13. package/dist/client/assets/event-Dwf9IDxK.js +1 -0
  14. package/dist/client/assets/file-explorer-screen-DzDX4HcB.js +1 -0
  15. package/dist/client/assets/files-PsZnGOUx.js +2 -0
  16. package/dist/client/assets/follow-up-suggestions-BYWq-d8P.js +5 -0
  17. package/dist/client/assets/{index-lK3yGoTI.js → index-B551ln24.js} +1 -1
  18. package/dist/client/assets/index-BxsgifDH.js +3 -0
  19. package/dist/client/assets/keyboard-shortcuts-dialog-BX-hH4Wf.js +1 -0
  20. package/dist/client/assets/main-DBmooBKx.js +210 -0
  21. package/dist/client/assets/markdown-Bpat4kTr.js +87 -0
  22. package/dist/client/assets/memory-Bs8hOoEv.js +2 -0
  23. package/dist/client/assets/memory-screen-1DgDLyZf.js +1 -0
  24. package/dist/client/assets/menu-ByR1BVmq.js +1 -0
  25. package/dist/client/assets/{opencami-logo-zuSBm5Br.js → opencami-logo-BV1uPYe6.js} +1 -1
  26. package/dist/client/assets/popupStateMapping-CGDLUl5Y.js +1 -0
  27. package/dist/client/assets/proxy-DYrSkM35.js +9 -0
  28. package/dist/client/assets/{react-BLyCEWpN.js → react-dSDkXQu6.js} +1 -1
  29. package/dist/client/assets/search-dialog-B3XJLq-O.js +1 -0
  30. package/dist/client/assets/search-sources-badge-CTd0gLuz.js +1 -0
  31. package/dist/client/assets/session-export-dialog-Cib97JLm.js +1 -0
  32. package/dist/client/assets/settings-dialog-B4NLq1ZS.js +1 -0
  33. package/dist/client/assets/{skills-panel-BH27r3nC.js → skills-panel-Cj7yHGbX.js} +1 -1
  34. package/dist/client/assets/skills-pueagQNc.js +2 -0
  35. package/dist/client/assets/styles-Ce2xZzc4.css +1 -0
  36. package/dist/client/assets/switch-kXs1I0oW.js +1 -0
  37. package/dist/client/assets/tabs-B6GW7TBf.js +1 -0
  38. package/dist/client/assets/thinking-DTP9JDQl.js +1 -0
  39. package/dist/client/assets/tooltip-CtHpm-sQ.js +1 -0
  40. package/dist/client/assets/use-file-explorer-state-C-D2CShe.js +12 -0
  41. package/dist/client/assets/{useBaseUiId-MgM4ouhx.js → useBaseUiId-Ckx_aJky.js} +1 -1
  42. package/dist/client/assets/useCompositeItem-CkvfeGmG.js +1 -0
  43. package/dist/client/assets/{useControlled-BQxTgsOd.js → useControlled-8D4PSDAL.js} +1 -1
  44. package/dist/client/assets/{useMutation-12DyB3Ox.js → useMutation-DckvFKPC.js} +1 -1
  45. package/dist/client/assets/{useQuery-Ctiljcrr.js → useQuery-CtUiG53w.js} +1 -1
  46. package/dist/server/assets/{_sessionKey-DzsJfprr.js → _sessionKey-LV6xK9IM.js} +548 -947
  47. package/dist/server/assets/{_tanstack-start-manifest_v-C5HBDfQB.js → _tanstack-start-manifest_v-qVhiIEVc.js} +1 -1
  48. package/dist/server/assets/{connect-CbgijWz4.js → connect-BNabuqpW.js} +1 -1
  49. package/dist/server/assets/follow-up-suggestions-CSSc4PDe.js +336 -0
  50. package/dist/server/assets/{index-Dl2BOKP7.js → index-BEWnDAH6.js} +24 -5
  51. package/dist/server/assets/{index-BFHEmXpN.js → index-DMKS4aeI.js} +1 -1
  52. package/dist/server/assets/{markdown-BFE5y9YH.js → markdown-DoX5Q7qh.js} +50 -26
  53. package/dist/server/assets/{memory-BqZOoD7Q.js → memory-Cxu7i8ej.js} +1 -1
  54. package/dist/server/assets/{memory-screen-BK5phS8K.js → memory-screen-B5l1NZRY.js} +2 -2
  55. package/dist/server/assets/{router-BZPatFG9.js → router-Cr2xCvGA.js} +5 -5
  56. package/dist/server/assets/{search-dialog-DQRkARXw.js → search-dialog-DR6zBnui.js} +4 -4
  57. package/dist/server/assets/search-sources-badge-B0rAEDs_.js +106 -0
  58. package/dist/server/assets/{settings-dialog-Bc1ta26X.js → settings-dialog-DEMlCMCP.js} +4 -4
  59. package/dist/server/assets/thinking-BpAc3itF.js +92 -0
  60. package/dist/server/server.js +38 -195
  61. package/package.json +2 -6
  62. package/dist/client/assets/_sessionKey-B5Viv43f.js +0 -23
  63. package/dist/client/assets/agents-BmE6QOwl.js +0 -2
  64. package/dist/client/assets/agents-screen-pHUzJxX5.js +0 -1
  65. package/dist/client/assets/bots-BeOkwrXr.js +0 -2
  66. package/dist/client/assets/bots-screen-B79bAYvf.js +0 -1
  67. package/dist/client/assets/button-CK8tKQ-Z.js +0 -1
  68. package/dist/client/assets/event-BsD1rqGT.js +0 -1
  69. package/dist/client/assets/file-explorer-screen-Ds7LeJTd.js +0 -1
  70. package/dist/client/assets/files-e40B1zFy.js +0 -2
  71. package/dist/client/assets/index-rljDU_1M.js +0 -3
  72. package/dist/client/assets/keyboard-shortcuts-dialog-Bb_GOr9L.js +0 -1
  73. package/dist/client/assets/main-Dq6jpr6-.js +0 -210
  74. package/dist/client/assets/markdown-C7_Aipwd.js +0 -87
  75. package/dist/client/assets/memory-C7UG-1le.js +0 -2
  76. package/dist/client/assets/memory-screen-CUFBWsq5.js +0 -1
  77. package/dist/client/assets/menu-n6L--M9R.js +0 -1
  78. package/dist/client/assets/proxy-BU8Bw1Vt.js +0 -9
  79. package/dist/client/assets/search-dialog-yB4w5ajo.js +0 -1
  80. package/dist/client/assets/session-export-dialog-qbZgd2Zo.js +0 -1
  81. package/dist/client/assets/settings-dialog-CHJbvpgk.js +0 -1
  82. package/dist/client/assets/skills-DoKPPhNY.js +0 -2
  83. package/dist/client/assets/styles-CXV5jZiD.css +0 -1
  84. package/dist/client/assets/switch-BD3a0LRm.js +0 -1
  85. package/dist/client/assets/tabs-DI1e-kzz.js +0 -1
  86. package/dist/client/assets/tooltip-BbH3QWvK.js +0 -1
  87. package/dist/client/assets/use-file-explorer-state-DBfLeAyz.js +0 -12
  88. package/dist/client/assets/useCompositeItem-OhltNFdZ.js +0 -1
  89. package/dist/client/assets/useOnFirstRender-7qoaK5sI.js +0 -1
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import React__default, { useState, useCallback, memo, useDeferredValue, useMemo, 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
- import { Tick01Icon, Cancel01Icon, MoreHorizontalIcon, Pen01Icon, Upload01Icon, Delete01Icon, BotIcon, Clock01Icon, Chat01Icon, ArrowRight01Icon, SidebarLeft01Icon, PencilEdit02Icon, Folder01Icon, AiBrain01Icon, PackageOpenIcon, SmartPhone01Icon, DashboardCircleIcon, Search01Icon, Settings01Icon, Menu01Icon, Tick02Icon, Copy01Icon, Loading02Icon, StopIcon, VolumeHighIcon, ArrowDown01Icon, Loading03Icon, File01Icon, ArtificialIntelligence02Icon, CommandIcon, Attachment01Icon, Mic02Icon, ArrowUp02Icon } from "@hugeicons/core-free-icons";
7
+ import { Tick01Icon, Cancel01Icon, MoreHorizontalIcon, Pen01Icon, Upload01Icon, Delete01Icon, BotIcon, Clock01Icon, Chat01Icon, ArrowRight01Icon, SidebarLeft01Icon, PencilEdit02Icon, Folder01Icon, AiBrain01Icon, PackageOpenIcon, SmartPhone01Icon, DashboardCircleIcon, Search01Icon, Settings01Icon, Menu01Icon, Tick02Icon, Copy01Icon, Loading02Icon, StopIcon, VolumeHighIcon, Loading03Icon, ArrowDown01Icon, File01Icon, CommandIcon, Attachment01Icon, Mic02Icon, ArrowUp02Icon } from "@hugeicons/core-free-icons";
8
8
  import { HugeiconsIcon } from "@hugeicons/react";
9
9
  import { motion, AnimatePresence } from "motion/react";
10
10
  import { AlertDialog } from "@base-ui/react/alert-dialog";
@@ -14,12 +14,12 @@ import { Collapsible as Collapsible$1 } from "@base-ui/react/collapsible";
14
14
  import { ScrollArea } from "@base-ui/react/scroll-area";
15
15
  import { M as MenuRoot, a as MenuTrigger, b as MenuContent, c as MenuItem } from "./menu-D90CDTi2.js";
16
16
  import { O as OpenCamiLogo, a as OpenCamiText } from "./opencami-logo-C-43FL3R.js";
17
- import { M as Markdown } from "./markdown-BFE5y9YH.js";
18
- import { u as useChatSettings$1 } from "./index-Dl2BOKP7.js";
17
+ import { M as Markdown } from "./markdown-DoX5Q7qh.js";
18
+ import { u as useChatSettings$1 } from "./index-BEWnDAH6.js";
19
+ import { createPortal } from "react-dom";
19
20
  import { create } from "zustand";
20
21
  import { persist } from "zustand/middleware";
21
- import { createPortal } from "react-dom";
22
- import { a as Route } from "./router-BZPatFG9.js";
22
+ import { a as Route } from "./router-Cr2xCvGA.js";
23
23
  function deriveFriendlyIdFromKey(key) {
24
24
  if (!key) return "main";
25
25
  const trimmed = key.trim();
@@ -1681,7 +1681,7 @@ function areSidebarSessionsEqual(prev, next) {
1681
1681
  return true;
1682
1682
  }
1683
1683
  const SettingsDialog = lazy(
1684
- () => import("./settings-dialog-Bc1ta26X.js").then((m) => ({ default: m.SettingsDialog }))
1684
+ () => import("./settings-dialog-DEMlCMCP.js").then((m) => ({ default: m.SettingsDialog }))
1685
1685
  );
1686
1686
  const SessionExportDialog = lazy(
1687
1687
  () => import("./session-export-dialog-C53RRAah.js").then((m) => ({
@@ -2748,50 +2748,6 @@ function MessageContent({
2748
2748
  );
2749
2749
  return markdown ? /* @__PURE__ */ jsx(Markdown, { className: classNames, ...props, children }) : /* @__PURE__ */ jsx("div", { className: classNames, ...props, children });
2750
2750
  }
2751
- function useIsMobile() {
2752
- const [isMobile, setIsMobile] = useState(
2753
- () => typeof window !== "undefined" ? window.innerWidth < 768 : false
2754
- );
2755
- useEffect(() => {
2756
- if (typeof window === "undefined") return;
2757
- const mql = window.matchMedia("(max-width: 767px)");
2758
- const handler = (e) => setIsMobile(e.matches);
2759
- mql.addEventListener("change", handler);
2760
- setIsMobile(mql.matches);
2761
- return () => mql.removeEventListener("change", handler);
2762
- }, []);
2763
- return isMobile;
2764
- }
2765
- function Thinking({ content }) {
2766
- const isMobile = useIsMobile();
2767
- return /* @__PURE__ */ jsx("div", { className: "inline-flex flex-col", children: /* @__PURE__ */ jsxs(Collapsible, { defaultOpen: !isMobile, children: [
2768
- /* @__PURE__ */ jsxs(
2769
- CollapsibleTrigger,
2770
- {
2771
- render: /* @__PURE__ */ jsx(
2772
- Button,
2773
- {
2774
- variant: "ghost",
2775
- className: "h-auto gap-1.5 px-1.5 py-0.5 -mx-2"
2776
- }
2777
- ),
2778
- children: [
2779
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-primary-900", children: "Thinking" }),
2780
- /* @__PURE__ */ jsx(
2781
- HugeiconsIcon,
2782
- {
2783
- icon: ArrowDown01Icon,
2784
- size: 14,
2785
- strokeWidth: 1.5,
2786
- className: "text-primary-900 transition-transform duration-150 group-data-panel-open:rotate-180"
2787
- }
2788
- )
2789
- ]
2790
- }
2791
- ),
2792
- /* @__PURE__ */ jsx(CollapsiblePanel, { children: /* @__PURE__ */ jsx("div", { className: "pt-1 mb-3", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-primary-700 whitespace-pre-wrap", children: content }) }) })
2793
- ] }) });
2794
- }
2795
2751
  function Tool({ toolPart, defaultOpen = false }) {
2796
2752
  const { state, input, output, toolCallId } = toolPart;
2797
2753
  const serialize = (value, maxLength = 3200) => {
@@ -2871,99 +2827,16 @@ function Tool({ toolPart, defaultOpen = false }) {
2871
2827
  ] }) })
2872
2828
  ] }) });
2873
2829
  }
2874
- function getDomain(url) {
2875
- try {
2876
- return new URL(url).hostname.replace("www.", "");
2877
- } catch {
2878
- return url;
2879
- }
2880
- }
2881
- function FaviconCircle({ domain }) {
2882
- return /* @__PURE__ */ jsx(
2883
- "div",
2884
- {
2885
- className: "flex items-center justify-center w-5 h-5 rounded-full bg-white dark:bg-zinc-800 border border-cyan-500/20 overflow-hidden",
2886
- title: domain,
2887
- children: /* @__PURE__ */ jsx(
2888
- "img",
2889
- {
2890
- src: `https://www.google.com/s2/favicons?domain=${domain}&sz=32`,
2891
- alt: "",
2892
- className: "w-4 h-4 object-contain",
2893
- loading: "lazy",
2894
- onError: (e) => {
2895
- const target = e.target;
2896
- target.style.display = "none";
2897
- const parent = target.parentElement;
2898
- if (parent) {
2899
- parent.textContent = domain.charAt(0).toUpperCase();
2900
- parent.classList.add("text-[10px]", "font-medium", "text-cyan-400");
2901
- }
2902
- }
2903
- }
2904
- )
2905
- }
2906
- );
2907
- }
2908
- function SearchSourcesBadge({ sources }) {
2909
- const [expanded, setExpanded] = useState(false);
2910
- if (!sources.length) return null;
2911
- const uniqueDomains = [...new Set(sources.map((s) => getDomain(s.url)))];
2912
- return /* @__PURE__ */ jsxs("div", { className: "mt-2 w-full", children: [
2913
- /* @__PURE__ */ jsxs(
2914
- "button",
2915
- {
2916
- onClick: () => setExpanded(!expanded),
2917
- className: cn(
2918
- "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs",
2919
- "bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30",
2920
- "text-cyan-300 transition-colors"
2921
- ),
2922
- children: [
2923
- /* @__PURE__ */ jsx(HugeiconsIcon, { icon: Search01Icon, size: 14 }),
2924
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Sources" }),
2925
- /* @__PURE__ */ jsx("span", { className: "px-1.5 py-0.5 rounded-full bg-cyan-500/20 text-cyan-400 font-semibold", children: sources.length }),
2926
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 ml-1", children: [
2927
- uniqueDomains.slice(0, 3).map((domain) => /* @__PURE__ */ jsx(FaviconCircle, { domain }, domain)),
2928
- uniqueDomains.length > 3 && /* @__PURE__ */ jsxs("span", { className: "text-cyan-500/70 text-[10px] ml-0.5", children: [
2929
- "+",
2930
- uniqueDomains.length - 3
2931
- ] })
2932
- ] }),
2933
- /* @__PURE__ */ jsx(
2934
- HugeiconsIcon,
2935
- {
2936
- icon: ArrowRight01Icon,
2937
- size: 12,
2938
- className: cn("transition-transform", expanded && "rotate-90")
2939
- }
2940
- )
2941
- ]
2942
- }
2943
- ),
2944
- expanded && /* @__PURE__ */ jsx("div", { className: "mt-2 rounded-lg border border-cyan-500/20 bg-cyan-500/5 max-h-80 overflow-y-auto", children: sources.map((source, i) => {
2945
- const domain = getDomain(source.url);
2946
- return /* @__PURE__ */ jsxs(
2947
- "a",
2948
- {
2949
- href: source.url,
2950
- target: "_blank",
2951
- rel: "noopener noreferrer",
2952
- className: "flex items-start gap-2 p-2.5 border-b border-cyan-500/10 last:border-b-0 hover:bg-cyan-500/5 transition-colors",
2953
- children: [
2954
- /* @__PURE__ */ jsx(FaviconCircle, { domain }),
2955
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
2956
- /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-primary-900 hover:underline line-clamp-1", children: source.title || domain }),
2957
- /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: domain }),
2958
- source.snippet && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground/80 line-clamp-2 mt-0.5", children: source.snippet })
2959
- ] })
2960
- ]
2961
- },
2962
- `${source.url}-${i}`
2963
- );
2964
- }) })
2965
- ] });
2966
- }
2830
+ const Thinking = lazy(
2831
+ () => import("./thinking-BpAc3itF.js").then((m) => ({
2832
+ default: m.Thinking
2833
+ }))
2834
+ );
2835
+ const SearchSourcesBadge = lazy(
2836
+ () => import("./search-sources-badge-B0rAEDs_.js").then((m) => ({
2837
+ default: m.SearchSourcesBadge
2838
+ }))
2839
+ );
2967
2840
  function mapToolCallToToolPart(toolCall, resultMessage) {
2968
2841
  const hasResult = resultMessage !== void 0;
2969
2842
  const isError = resultMessage?.isError ?? false;
@@ -3145,10 +3018,19 @@ function MessageItemComponent({
3145
3018
  const thinking = thinkingFromMessage(message);
3146
3019
  const images = imagesFromMessage(message);
3147
3020
  const isUser = role === "user";
3021
+ const imageDataUris = useMemo(
3022
+ () => images.map(
3023
+ (img) => `data:${img.source.media_type};base64,${img.source.data}`
3024
+ ),
3025
+ [images]
3026
+ );
3148
3027
  const timestamp = getMessageTimestamp(message);
3149
3028
  const navigate = useNavigate();
3150
3029
  const openInEditor = useFileExplorerState((state) => state.openInEditor);
3151
- const uploadedFileRefs = useMemo(() => parseUploadedFileReferences(text), [text]);
3030
+ const uploadedFileRefs = useMemo(
3031
+ () => parseUploadedFileReferences(text),
3032
+ [text]
3033
+ );
3152
3034
  const [fileSizes, setFileSizes] = useState({});
3153
3035
  const displayText = useMemo(() => stripUploadedFileLines(text), [text]);
3154
3036
  useEffect(() => {
@@ -3158,7 +3040,9 @@ function MessageItemComponent({
3158
3040
  await Promise.all(
3159
3041
  uploadedFileRefs.map(async (ref) => {
3160
3042
  try {
3161
- const response = await fetch(`/api/files/info?path=${encodeURIComponent(ref.path)}`);
3043
+ const response = await fetch(
3044
+ `/api/files/info?path=${encodeURIComponent(ref.path)}`
3045
+ );
3162
3046
  if (!response.ok) {
3163
3047
  nextSizes[ref.path] = null;
3164
3048
  return;
@@ -3208,51 +3092,81 @@ function MessageItemComponent({
3208
3092
  isUser ? "items-end" : "items-start"
3209
3093
  ),
3210
3094
  children: [
3211
- thinking && settings.showReasoningBlocks && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(Thinking, { content: thinking }) }),
3212
- images.length > 0 && /* @__PURE__ */ jsx("div", { className: cn(
3213
- "flex flex-wrap gap-2 mb-2",
3214
- isUser ? "justify-end" : "justify-start"
3215
- ), children: images.map((img, idx) => /* @__PURE__ */ jsx(
3216
- "img",
3217
- {
3218
- src: `data:${img.source.media_type};base64,${img.source.data}`,
3219
- alt: `Attachment ${idx + 1}`,
3220
- loading: "lazy",
3221
- decoding: "async",
3222
- className: "max-w-[300px] max-h-[300px] rounded-lg object-cover"
3223
- },
3224
- idx
3225
- )) }),
3226
- uploadedFileRefs.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("mb-2 flex w-full flex-col gap-2", isUser ? "items-end" : "items-start"), children: uploadedFileRefs.map((fileRef) => /* @__PURE__ */ jsxs(
3227
- "button",
3095
+ thinking && settings.showReasoningBlocks && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Thinking, { content: thinking }) }) }),
3096
+ images.length > 0 && /* @__PURE__ */ jsx(
3097
+ "div",
3228
3098
  {
3229
- type: "button",
3230
- onClick: () => {
3231
- void handleOpenFile(fileRef.path);
3232
- },
3233
- className: "flex max-w-full items-center gap-3 rounded-xl border border-primary-200 bg-primary-50 px-3 py-2 text-left hover:bg-primary-100",
3234
- children: [
3235
- /* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-primary-100", children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: File01Icon, size: 18, className: "text-primary-600" }) }),
3236
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
3237
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-primary-900", children: fileRef.filename }),
3238
- /* @__PURE__ */ jsx("p", { className: "text-xs text-primary-600", children: formatFileSize$1(fileSizes[fileRef.path] ?? null) })
3239
- ] })
3240
- ]
3241
- },
3242
- fileRef.path
3243
- )) }),
3244
- /* @__PURE__ */ jsx(Message, { className: cn("min-w-0 max-w-full", isUser ? "flex-row-reverse" : ""), children: /* @__PURE__ */ jsx(
3245
- MessageContent,
3099
+ className: cn(
3100
+ "flex flex-wrap gap-2 mb-2",
3101
+ isUser ? "justify-end" : "justify-start"
3102
+ ),
3103
+ children: images.map((img, idx) => /* @__PURE__ */ jsx(
3104
+ "img",
3105
+ {
3106
+ src: imageDataUris[idx],
3107
+ alt: `Attachment ${idx + 1}`,
3108
+ loading: "lazy",
3109
+ decoding: "async",
3110
+ fetchPriority: "low",
3111
+ className: "max-w-[300px] max-h-[300px] rounded-lg object-cover"
3112
+ },
3113
+ idx
3114
+ ))
3115
+ }
3116
+ ),
3117
+ uploadedFileRefs.length > 0 && /* @__PURE__ */ jsx(
3118
+ "div",
3246
3119
  {
3247
- markdown: !isUser,
3248
3120
  className: cn(
3249
- "text-primary-900 opencami-text-size min-w-0 max-w-full",
3250
- 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",
3251
- !isUser && isStreaming && "stream-fade-in"
3121
+ "mb-2 flex w-full flex-col gap-2",
3122
+ isUser ? "items-end" : "items-start"
3252
3123
  ),
3253
- children: displayText
3124
+ children: uploadedFileRefs.map((fileRef) => /* @__PURE__ */ jsxs(
3125
+ "button",
3126
+ {
3127
+ type: "button",
3128
+ onClick: () => {
3129
+ void handleOpenFile(fileRef.path);
3130
+ },
3131
+ className: "flex max-w-full items-center gap-3 rounded-xl border border-primary-200 bg-primary-50 px-3 py-2 text-left hover:bg-primary-100",
3132
+ children: [
3133
+ /* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-primary-100", children: /* @__PURE__ */ jsx(
3134
+ HugeiconsIcon,
3135
+ {
3136
+ icon: File01Icon,
3137
+ size: 18,
3138
+ className: "text-primary-600"
3139
+ }
3140
+ ) }),
3141
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
3142
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-primary-900", children: fileRef.filename }),
3143
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-primary-600", children: formatFileSize$1(fileSizes[fileRef.path] ?? null) })
3144
+ ] })
3145
+ ]
3146
+ },
3147
+ fileRef.path
3148
+ ))
3254
3149
  }
3255
- ) }),
3150
+ ),
3151
+ /* @__PURE__ */ jsx(
3152
+ Message,
3153
+ {
3154
+ className: cn("min-w-0 max-w-full", isUser ? "flex-row-reverse" : ""),
3155
+ children: /* @__PURE__ */ jsx(
3156
+ MessageContent,
3157
+ {
3158
+ markdown: !isUser,
3159
+ isStreaming: !isUser && isStreaming,
3160
+ className: cn(
3161
+ "text-primary-900 opencami-text-size min-w-0 max-w-full",
3162
+ 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",
3163
+ !isUser && isStreaming && "stream-fade-in"
3164
+ ),
3165
+ children: displayText
3166
+ }
3167
+ )
3168
+ }
3169
+ ),
3256
3170
  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) => {
3257
3171
  const resultMessage = toolCall.id ? toolResultsByCallId?.get(toolCall.id) : void 0;
3258
3172
  const toolPart = mapToolCallToToolPart(toolCall, resultMessage);
@@ -3265,7 +3179,7 @@ function MessageItemComponent({
3265
3179
  toolCall.id || toolCall.name
3266
3180
  );
3267
3181
  }) }),
3268
- searchSources.length > 0 && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(SearchSourcesBadge, { sources: searchSources }) }),
3182
+ searchSources.length > 0 && /* @__PURE__ */ jsx("div", { className: "w-full max-w-[var(--opencami-chat-width)]", children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(SearchSourcesBadge, { sources: searchSources }) }) }),
3269
3183
  !hasToolCalls && /* @__PURE__ */ jsx(
3270
3184
  MessageActionsBar,
3271
3185
  {
@@ -3285,7 +3199,8 @@ function areMessagesEqual(prevProps, nextProps) {
3285
3199
  }
3286
3200
  if (prevProps.isStreaming !== nextProps.isStreaming) return false;
3287
3201
  if (prevProps.isLastAssistant !== nextProps.isLastAssistant) return false;
3288
- if (prevProps.aggregatedSearchSources !== nextProps.aggregatedSearchSources) return false;
3202
+ if (prevProps.aggregatedSearchSources !== nextProps.aggregatedSearchSources)
3203
+ return false;
3289
3204
  if (prevProps.wrapperClassName !== nextProps.wrapperClassName) return false;
3290
3205
  if (prevProps.wrapperRef !== nextProps.wrapperRef) return false;
3291
3206
  if (prevProps.wrapperScrollMarginTop !== nextProps.wrapperScrollMarginTop) {
@@ -3314,581 +3229,102 @@ function areMessagesEqual(prevProps, nextProps) {
3314
3229
  return true;
3315
3230
  }
3316
3231
  const MemoizedMessageItem = memo(MessageItemComponent, areMessagesEqual);
3317
- const CODE_PATTERNS = [
3318
- /```[\s\S]*?```/g,
3319
- /`[^`]+`/g,
3320
- /\b(function|const|let|var|class|import|export|return|async|await)\b/g
3321
- ];
3322
- function extractTopics(text) {
3323
- const topics = [];
3324
- const quoted = text.match(/"([^"]+)"|'([^']+)'/g);
3325
- if (quoted) {
3326
- topics.push(...quoted.map((q) => q.replace(/['"]/g, "")));
3327
- }
3328
- const capitalized = text.match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g);
3329
- if (capitalized) {
3330
- topics.push(
3331
- ...capitalized.filter((t) => !["I", "The", "A", "An", "This"].includes(t))
3332
- );
3333
- }
3334
- const backticked = text.match(/`([^`]+)`/g);
3335
- if (backticked) {
3336
- topics.push(...backticked.map((t) => t.replace(/`/g, "")));
3337
- }
3338
- return [...new Set(topics)].slice(0, 5);
3339
- }
3340
- function hasCode(text) {
3341
- return CODE_PATTERNS.some((pattern) => pattern.test(text));
3342
- }
3343
- function hasList(text) {
3344
- return /^[\s]*[-*•\d]+[.)\s]/m.test(text) || /\b(first|second|third|1\.|2\.|3\.)/i.test(text);
3345
- }
3346
- function hasAlternatives(text) {
3347
- return /\b(alternatively|another|other option|could also|you might|consider)\b/i.test(text);
3348
- }
3349
- function isExplanatory(text) {
3350
- return /\b(because|since|due to|reason|why|how|what|when|where)\b/i.test(text);
3351
- }
3352
- function hasCaveats(text) {
3353
- return /\b(however|but|note|warning|caution|careful|important|keep in mind|be aware)\b/i.test(text);
3354
- }
3355
- function generateHeuristicFollowUps(responseText) {
3356
- const suggestions = [];
3357
- const topics = extractTopics(responseText);
3358
- if (hasCode(responseText)) {
3359
- suggestions.push({
3360
- text: "Can you explain this code step by step?",
3361
- type: "clarify"
3362
- });
3363
- suggestions.push({
3364
- text: "How would I modify this for my use case?",
3365
- type: "actionable"
3366
- });
3367
- }
3368
- if (hasList(responseText)) {
3369
- suggestions.push({
3370
- text: "Can you elaborate on the first point?",
3371
- type: "expand"
3372
- });
3373
- suggestions.push({
3374
- text: "Which of these is most important to start with?",
3375
- type: "actionable"
3376
- });
3377
- }
3378
- if (hasAlternatives(responseText)) {
3379
- suggestions.push({
3380
- text: "What are the pros and cons of each approach?",
3381
- type: "alternative"
3382
- });
3383
- suggestions.push({
3384
- text: "Which option would you recommend and why?",
3385
- type: "clarify"
3386
- });
3387
- }
3388
- if (isExplanatory(responseText) && suggestions.length < 3) {
3389
- suggestions.push({
3390
- text: "Can you give me a practical example?",
3391
- type: "example"
3392
- });
3393
- }
3394
- if (hasCaveats(responseText) && suggestions.length < 3) {
3395
- suggestions.push({
3396
- text: "What potential issues should I watch out for?",
3397
- type: "clarify"
3398
- });
3399
- }
3400
- if (topics.length > 0 && suggestions.length < 3) {
3401
- const topic = topics[0];
3402
- suggestions.push({
3403
- text: `Tell me more about ${topic}`,
3404
- type: "expand"
3232
+ function ScrollButton({
3233
+ className,
3234
+ variant = "outline",
3235
+ scrollRef,
3236
+ ...props
3237
+ }) {
3238
+ const [isAtBottom, setIsAtBottom] = useState(true);
3239
+ const [showButton, setShowButton] = useState(false);
3240
+ const lastScrollTopRef = useRef(0);
3241
+ const checkIsAtBottom = useCallback(() => {
3242
+ const element = scrollRef.current;
3243
+ if (!element) return;
3244
+ const isBottom = Math.abs(
3245
+ element.scrollHeight - element.scrollTop - element.clientHeight
3246
+ ) < 100;
3247
+ setIsAtBottom(isBottom);
3248
+ }, [scrollRef]);
3249
+ useLayoutEffect(() => {
3250
+ const element = scrollRef.current;
3251
+ if (!element) return;
3252
+ const handleScroll = () => {
3253
+ lastScrollTopRef.current = element.scrollTop;
3254
+ checkIsAtBottom();
3255
+ };
3256
+ const observer = new MutationObserver(() => {
3257
+ if (!element) return;
3258
+ if (element.scrollTop !== lastScrollTopRef.current) {
3259
+ lastScrollTopRef.current = element.scrollTop;
3260
+ }
3261
+ checkIsAtBottom();
3405
3262
  });
3406
- }
3407
- const fallbacks = [
3408
- { text: "Can you give me a concrete example?", type: "example" },
3409
- { text: "What would be the next steps?", type: "actionable" },
3410
- { text: "How does this work in practice?", type: "expand" },
3411
- { text: "Are there any common mistakes to avoid?", type: "clarify" },
3412
- { text: "Can you simplify this explanation?", type: "clarify" }
3413
- ];
3414
- const usedTypes = new Set(suggestions.map((s) => s.type));
3415
- for (const fallback of fallbacks) {
3416
- if (suggestions.length >= 3) break;
3417
- if (!usedTypes.has(fallback.type)) {
3418
- suggestions.push(fallback);
3419
- usedTypes.add(fallback.type);
3263
+ checkIsAtBottom();
3264
+ element.addEventListener("scroll", handleScroll);
3265
+ observer.observe(element, { childList: true, subtree: true });
3266
+ return () => {
3267
+ element.removeEventListener("scroll", handleScroll);
3268
+ observer.disconnect();
3269
+ };
3270
+ }, [checkIsAtBottom, scrollRef]);
3271
+ useLayoutEffect(() => {
3272
+ if (isAtBottom) {
3273
+ setShowButton(false);
3274
+ return;
3420
3275
  }
3421
- }
3422
- for (const fallback of fallbacks) {
3423
- if (suggestions.length >= 3) break;
3424
- if (!suggestions.some((s) => s.text === fallback.text)) {
3425
- suggestions.push(fallback);
3276
+ const timer = window.setTimeout(() => {
3277
+ setShowButton(true);
3278
+ }, 200);
3279
+ return () => window.clearTimeout(timer);
3280
+ }, [isAtBottom]);
3281
+ return /* @__PURE__ */ jsx(
3282
+ Button,
3283
+ {
3284
+ variant: "secondary",
3285
+ size: "icon-sm",
3286
+ className: cn(
3287
+ "pointer-events-auto rounded-full shadow-md",
3288
+ "transition-all duration-100 ease-in-out",
3289
+ !isAtBottom && showButton ? "translate-y-0 scale-100 opacity-100" : "pointer-events-none translate-y-4 scale-98 opacity-0",
3290
+ className
3291
+ ),
3292
+ onClick: () => {
3293
+ const element = scrollRef.current;
3294
+ if (!element) return;
3295
+ element.scrollTop = element.scrollHeight;
3296
+ setIsAtBottom(true);
3297
+ },
3298
+ ...props,
3299
+ children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArrowDown01Icon, size: 18, strokeWidth: 1.8 })
3426
3300
  }
3427
- }
3428
- return suggestions.slice(0, 3);
3301
+ );
3429
3302
  }
3430
- function getHeuristicFollowUpTexts(responseText) {
3431
- return generateHeuristicFollowUps(responseText).map((s) => s.text);
3432
- }
3433
- const LLM_PROVIDER_DEFAULTS = {
3434
- openai: {
3435
- baseUrl: "https://api.openai.com/v1",
3436
- model: "gpt-4.1-nano"
3437
- },
3438
- openrouter: {
3439
- baseUrl: "https://openrouter.ai/api/v1",
3440
- model: "openai/gpt-oss-120b"
3441
- },
3442
- kilocode: {
3443
- baseUrl: "https://api.kilo.ai/api/gateway",
3444
- model: "google/gemini-2.5-flash"
3445
- },
3446
- ollama: {
3447
- baseUrl: "http://localhost:11434/v1",
3448
- model: "llama3.2"
3449
- },
3450
- custom: {
3451
- baseUrl: "",
3452
- model: ""
3453
- }
3454
- };
3455
- const DEFAULT_LLM_SETTINGS = {
3456
- useLlmTitles: true,
3457
- useLlmFollowUps: true,
3458
- llmProvider: "openai",
3459
- llmBaseUrl: "",
3460
- llmModel: "",
3461
- llmApiKey: ""
3462
- };
3463
- function getLlmProviderDefaults(provider) {
3464
- return LLM_PROVIDER_DEFAULTS[provider];
3465
- }
3466
- function getEffectiveLlmBaseUrl(settings) {
3467
- if (settings.llmBaseUrl.trim()) return settings.llmBaseUrl.trim();
3468
- return getLlmProviderDefaults(settings.llmProvider).baseUrl;
3469
- }
3470
- function getEffectiveLlmModel(settings) {
3471
- if (settings.llmModel.trim()) return settings.llmModel.trim();
3472
- return getLlmProviderDefaults(settings.llmProvider).model;
3473
- }
3474
- function getAvailability(settings, hasEnvKey) {
3475
- if (settings.llmProvider === "ollama") return true;
3476
- if (settings.llmProvider === "custom") {
3477
- return Boolean(settings.llmApiKey.trim()) || Boolean(settings.llmBaseUrl.trim() && settings.llmModel.trim());
3478
- }
3479
- return hasEnvKey || Boolean(settings.llmApiKey.trim());
3480
- }
3481
- function migratePersistedState(persistedState) {
3482
- if (!persistedState || typeof persistedState !== "object") {
3483
- return { settings: DEFAULT_LLM_SETTINGS };
3484
- }
3485
- const { settings } = persistedState;
3486
- if (!settings) {
3487
- return { settings: DEFAULT_LLM_SETTINGS };
3488
- }
3489
- const { openaiApiKey, ...rest } = settings;
3490
- const llmApiKey = rest.llmApiKey ?? openaiApiKey ?? "";
3491
- return {
3492
- settings: {
3493
- ...DEFAULT_LLM_SETTINGS,
3494
- ...rest,
3495
- llmApiKey
3496
- }
3497
- };
3498
- }
3499
- const useLlmSettingsStore = create()(
3500
- persist(
3501
- (set) => ({
3502
- settings: {
3503
- ...DEFAULT_LLM_SETTINGS
3504
- },
3505
- updateSettings: (updates) => set((state) => ({
3506
- settings: { ...state.settings, ...updates }
3507
- })),
3508
- clearApiKey: () => set((state) => ({
3509
- settings: { ...state.settings, llmApiKey: "" }
3510
- }))
3511
- }),
3512
- {
3513
- name: "llm-settings",
3514
- version: 2,
3515
- migrate: (persistedState) => migratePersistedState(persistedState)
3516
- }
3517
- )
3518
- );
3519
- function useLlmSettings() {
3520
- const settings = useLlmSettingsStore((state) => state.settings);
3521
- const updateSettings = useLlmSettingsStore((state) => state.updateSettings);
3522
- const clearApiKey = useLlmSettingsStore((state) => state.clearApiKey);
3523
- const [status, setStatus] = useState({
3524
- hasEnvKey: false,
3525
- hasOpenRouterKey: false,
3526
- hasKilocodeKey: false,
3527
- hasUserKey: Boolean(settings.llmApiKey),
3528
- isAvailable: getAvailability(settings, false),
3529
- isLoading: true,
3530
- error: null
3531
- });
3532
- useEffect(() => {
3533
- let cancelled = false;
3534
- async function checkStatus() {
3535
- try {
3536
- const res = await fetch("/api/llm-features");
3537
- if (!res.ok) throw new Error("Failed to check LLM status");
3538
- const data = await res.json();
3539
- if (cancelled) return;
3540
- const hasUserKey = Boolean(settings.llmApiKey);
3541
- const hasProviderKey = settings.llmProvider === "openrouter" ? Boolean(data.hasOpenRouterKey) : settings.llmProvider === "kilocode" ? Boolean(data.hasKilocodeKey) : data.hasEnvKey;
3542
- setStatus({
3543
- hasEnvKey: data.hasEnvKey,
3544
- hasOpenRouterKey: Boolean(data.hasOpenRouterKey),
3545
- hasKilocodeKey: Boolean(data.hasKilocodeKey),
3546
- hasUserKey,
3547
- isAvailable: getAvailability(settings, hasProviderKey),
3548
- isLoading: false,
3549
- error: null
3550
- });
3551
- } catch (err) {
3552
- if (cancelled) return;
3553
- setStatus((prev) => ({
3554
- ...prev,
3555
- isLoading: false,
3556
- error: err instanceof Error ? err.message : "Failed to check status"
3557
- }));
3558
- }
3559
- }
3560
- void checkStatus();
3561
- return () => {
3562
- cancelled = true;
3563
- };
3564
- }, [
3565
- settings.llmApiKey,
3566
- settings.llmProvider,
3567
- settings.llmBaseUrl,
3568
- settings.llmModel
3569
- ]);
3570
- const testApiKey = useCallback(async (key) => {
3571
- try {
3572
- const headers = buildLlmHeaders({
3573
- ...settings,
3574
- llmApiKey: key
3575
- });
3576
- const res = await fetch("/api/llm-features", {
3577
- method: "POST",
3578
- headers: {
3579
- "Content-Type": "application/json",
3580
- ...headers
3581
- },
3582
- body: JSON.stringify({ action: "test" })
3583
- });
3584
- const data = await res.json();
3585
- if (!data.ok) {
3586
- return { valid: false, error: data.error || "Test failed" };
3587
- }
3588
- return { valid: data.valid ?? false, error: data.error };
3589
- } catch (err) {
3590
- return {
3591
- valid: false,
3592
- error: err instanceof Error ? err.message : "Network error"
3593
- };
3594
- }
3595
- }, [settings]);
3596
- return {
3597
- settings,
3598
- updateSettings,
3599
- clearApiKey,
3600
- status,
3601
- testApiKey
3602
- };
3603
- }
3604
- function buildLlmHeaders(settings) {
3605
- const apiKey = settings.llmApiKey;
3606
- const baseUrl = getEffectiveLlmBaseUrl(settings);
3607
- const model = getEffectiveLlmModel(settings);
3608
- if (apiKey) {
3609
- return {
3610
- "X-OpenAI-API-Key": apiKey,
3611
- ...baseUrl ? { "X-LLM-Base-URL": baseUrl } : {},
3612
- ...model ? { "X-LLM-Model": model } : {}
3613
- };
3614
- }
3615
- return {
3616
- ...baseUrl ? { "X-LLM-Base-URL": baseUrl } : {},
3617
- ...model ? { "X-LLM-Model": model } : {}
3618
- };
3619
- }
3620
- function getLlmHeaders() {
3621
- const settings = useLlmSettingsStore.getState().settings;
3622
- return buildLlmHeaders(settings);
3623
- }
3624
- async function fetchLlmFollowUps(conversationContext, signal) {
3625
- const headers = {
3626
- "Content-Type": "application/json",
3627
- ...getLlmHeaders()
3628
- };
3629
- const res = await fetch("/api/llm-features", {
3630
- method: "POST",
3631
- headers,
3632
- body: JSON.stringify({
3633
- action: "followups",
3634
- conversationContext
3635
- }),
3636
- signal
3637
- });
3638
- if (!res.ok) {
3639
- throw new Error(`API error: ${res.status}`);
3640
- }
3641
- const data = await res.json();
3642
- if (data.ok && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
3643
- return data.suggestions;
3644
- }
3645
- return [];
3646
- }
3647
- function useFollowUpSuggestions(responseText, contextSummary, options) {
3648
- const llmSettings = useLlmSettingsStore((state) => state.settings);
3649
- const useLlmFollowUps = llmSettings.useLlmFollowUps;
3650
- const {
3651
- minResponseLength = 50,
3652
- timeoutMs = 8e3,
3653
- // Use heuristics only if explicitly set OR if LLM follow-ups are disabled
3654
- heuristicsOnly: forceHeuristicsOnly
3655
- } = options ?? {};
3656
- const heuristicsOnly = forceHeuristicsOnly ?? !useLlmFollowUps;
3657
- const [suggestions, setSuggestions] = useState([]);
3658
- const [isLoading, setIsLoading] = useState(false);
3659
- const [error, setError] = useState(null);
3660
- const [source, setSource] = useState(null);
3661
- const lastResponseRef = useRef("");
3662
- const abortControllerRef = useRef(null);
3663
- useEffect(() => {
3664
- if (!responseText || responseText.trim().length < minResponseLength) {
3665
- setSuggestions([]);
3666
- setSource(null);
3667
- setIsLoading(false);
3668
- setError(null);
3669
- return;
3670
- }
3671
- const responseKey = responseText.slice(0, 200) + responseText.length;
3672
- if (responseKey === lastResponseRef.current) {
3673
- return;
3674
- }
3675
- lastResponseRef.current = responseKey;
3676
- if (abortControllerRef.current) {
3677
- abortControllerRef.current.abort();
3678
- }
3679
- if (heuristicsOnly) {
3680
- const heuristicSuggestions2 = getHeuristicFollowUpTexts(responseText);
3681
- setSuggestions(heuristicSuggestions2);
3682
- setSource("heuristic");
3683
- setIsLoading(false);
3684
- setError(null);
3685
- return;
3686
- }
3687
- const controller = new AbortController();
3688
- abortControllerRef.current = controller;
3689
- setIsLoading(true);
3690
- setError(null);
3691
- const heuristicSuggestions = getHeuristicFollowUpTexts(responseText);
3692
- setSuggestions(heuristicSuggestions);
3693
- setSource("heuristic");
3694
- const conversationContext = contextSummary ? `Context: ${contextSummary}
3695
-
3696
- Assistant's response:
3697
- ${responseText.slice(0, 2e3)}` : `Assistant's response:
3698
- ${responseText.slice(0, 2e3)}`;
3699
- fetchLlmFollowUps(conversationContext, controller.signal).then((llmSuggestions) => {
3700
- if (controller.signal.aborted) {
3701
- return;
3702
- }
3703
- if (llmSuggestions.length > 0) {
3704
- setSuggestions(llmSuggestions);
3705
- setSource("llm");
3706
- }
3707
- setIsLoading(false);
3708
- }).catch((err) => {
3709
- if (controller.signal.aborted) return;
3710
- setError(err instanceof Error ? err.message : String(err));
3711
- setIsLoading(false);
3712
- });
3713
- return () => {
3714
- };
3715
- }, [
3716
- responseText,
3717
- contextSummary,
3718
- minResponseLength,
3719
- timeoutMs,
3720
- heuristicsOnly,
3721
- llmSettings.llmApiKey,
3722
- llmSettings.llmBaseUrl,
3723
- llmSettings.llmModel,
3724
- llmSettings.llmProvider
3725
- ]);
3726
- return { suggestions, isLoading, error, source };
3727
- }
3728
- function FollowUpSuggestionsComponent({
3729
- responseText,
3730
- contextSummary,
3731
- onSuggestionClick,
3732
- disabled = false,
3733
- className
3734
- }) {
3735
- const { suggestions, isLoading, source } = useFollowUpSuggestions(
3736
- responseText,
3737
- contextSummary,
3738
- {
3739
- minResponseLength: 50,
3740
- timeoutMs: 8e3
3741
- }
3742
- );
3743
- if (suggestions.length === 0 && !isLoading) {
3744
- return null;
3745
- }
3746
- return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-2 mt-3", className), children: [
3747
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-primary-500", children: [
3748
- /* @__PURE__ */ jsx("span", { className: "text-primary-400", children: "✨" }),
3749
- /* @__PURE__ */ jsx("span", { children: isLoading ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
3750
- "Thinking of follow-ups",
3751
- /* @__PURE__ */ jsx(
3752
- HugeiconsIcon,
3753
- {
3754
- icon: Loading03Icon,
3755
- size: 12,
3756
- strokeWidth: 2,
3757
- className: "animate-spin text-primary-400"
3758
- }
3759
- )
3760
- ] }) : source === "llm" ? "AI suggestions" : "Follow-up suggestions" })
3761
- ] }),
3762
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsxs(
3763
- "button",
3764
- {
3765
- type: "button",
3766
- disabled,
3767
- onClick: () => onSuggestionClick(suggestion),
3768
- className: cn(
3769
- "group inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full",
3770
- "text-sm text-primary-700 bg-primary-50 border border-primary-200",
3771
- "hover:bg-primary-100 hover:border-primary-300 hover:text-primary-900",
3772
- "focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:ring-offset-1",
3773
- "transition-all duration-150 cursor-pointer",
3774
- "disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary-50 disabled:hover:border-primary-200",
3775
- // Subtle animation when suggestions update from heuristic to LLM
3776
- isLoading && "opacity-75"
3777
- ),
3778
- children: [
3779
- /* @__PURE__ */ jsx("span", { children: suggestion }),
3780
- /* @__PURE__ */ jsx(
3781
- HugeiconsIcon,
3782
- {
3783
- icon: ArrowRight01Icon,
3784
- size: 14,
3785
- strokeWidth: 2,
3786
- className: "text-primary-400 group-hover:text-primary-600 group-hover:translate-x-0.5 transition-all duration-150"
3787
- }
3788
- )
3789
- ]
3790
- },
3791
- `${index}-${suggestion.slice(0, 20)}`
3792
- )) })
3793
- ] });
3794
- }
3795
- const MemoizedFollowUpSuggestions = memo(FollowUpSuggestionsComponent);
3796
- function ScrollButton({
3797
- className,
3798
- variant = "outline",
3799
- scrollRef,
3800
- ...props
3801
- }) {
3802
- const [isAtBottom, setIsAtBottom] = useState(true);
3803
- const [showButton, setShowButton] = useState(false);
3804
- const lastScrollTopRef = useRef(0);
3805
- const checkIsAtBottom = useCallback(() => {
3806
- const element = scrollRef.current;
3807
- if (!element) return;
3808
- const isBottom = Math.abs(
3809
- element.scrollHeight - element.scrollTop - element.clientHeight
3810
- ) < 100;
3811
- setIsAtBottom(isBottom);
3812
- }, [scrollRef]);
3813
- useLayoutEffect(() => {
3814
- const element = scrollRef.current;
3815
- if (!element) return;
3816
- const handleScroll = () => {
3817
- lastScrollTopRef.current = element.scrollTop;
3818
- checkIsAtBottom();
3819
- };
3820
- const observer = new MutationObserver(() => {
3821
- if (!element) return;
3822
- if (element.scrollTop !== lastScrollTopRef.current) {
3823
- lastScrollTopRef.current = element.scrollTop;
3824
- }
3825
- checkIsAtBottom();
3826
- });
3827
- checkIsAtBottom();
3828
- element.addEventListener("scroll", handleScroll);
3829
- observer.observe(element, { childList: true, subtree: true });
3830
- return () => {
3831
- element.removeEventListener("scroll", handleScroll);
3832
- observer.disconnect();
3833
- };
3834
- }, [checkIsAtBottom, scrollRef]);
3835
- useLayoutEffect(() => {
3836
- if (isAtBottom) {
3837
- setShowButton(false);
3838
- return;
3839
- }
3840
- const timer = window.setTimeout(() => {
3841
- setShowButton(true);
3842
- }, 200);
3843
- return () => window.clearTimeout(timer);
3844
- }, [isAtBottom]);
3845
- return /* @__PURE__ */ jsx(
3846
- Button,
3847
- {
3848
- variant: "secondary",
3849
- size: "icon-sm",
3850
- className: cn(
3851
- "pointer-events-auto rounded-full shadow-md",
3852
- "transition-all duration-100 ease-in-out",
3853
- !isAtBottom && showButton ? "translate-y-0 scale-100 opacity-100" : "pointer-events-none translate-y-4 scale-98 opacity-0",
3854
- className
3855
- ),
3856
- onClick: () => {
3857
- const element = scrollRef.current;
3858
- if (!element) return;
3859
- element.scrollTop = element.scrollHeight;
3860
- setIsAtBottom(true);
3861
- },
3862
- ...props,
3863
- children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArrowDown01Icon, size: 18, strokeWidth: 1.8 })
3864
- }
3865
- );
3866
- }
3867
- function ChatContainerShell({
3868
- className,
3869
- viewportRef,
3870
- scrollRef,
3871
- viewportProps
3872
- }) {
3873
- return /* @__PURE__ */ jsxs(
3874
- ScrollAreaRoot,
3875
- {
3876
- className: cn("relative flex flex-1 min-h-0 flex-col", className),
3877
- children: [
3878
- /* @__PURE__ */ jsx(
3879
- ScrollAreaViewport,
3880
- {
3881
- className: "relative will-change-transform overflow-x-hidden",
3882
- ref: viewportRef,
3883
- ...viewportProps
3884
- }
3885
- ),
3886
- /* @__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 }) }) }),
3887
- /* @__PURE__ */ jsx(ScrollAreaScrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollAreaThumb, {}) }),
3888
- /* @__PURE__ */ jsx(ScrollAreaCorner, {})
3889
- ]
3890
- }
3891
- );
3303
+ function ChatContainerShell({
3304
+ className,
3305
+ viewportRef,
3306
+ scrollRef,
3307
+ viewportProps
3308
+ }) {
3309
+ return /* @__PURE__ */ jsxs(
3310
+ ScrollAreaRoot,
3311
+ {
3312
+ className: cn("relative flex flex-1 min-h-0 flex-col", className),
3313
+ children: [
3314
+ /* @__PURE__ */ jsx(
3315
+ ScrollAreaViewport,
3316
+ {
3317
+ className: "relative will-change-transform overflow-x-hidden",
3318
+ ref: viewportRef,
3319
+ ...viewportProps
3320
+ }
3321
+ ),
3322
+ /* @__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 }) }) }),
3323
+ /* @__PURE__ */ jsx(ScrollAreaScrollbar, { orientation: "vertical", children: /* @__PURE__ */ jsx(ScrollAreaThumb, {}) }),
3324
+ /* @__PURE__ */ jsx(ScrollAreaCorner, {})
3325
+ ]
3326
+ }
3327
+ );
3892
3328
  }
3893
3329
  function areViewportPropsEqual(prevProps, nextProps) {
3894
3330
  if (prevProps === nextProps) return true;
@@ -4032,6 +3468,11 @@ function TypingIndicator({ className }) {
4032
3468
  /* @__PURE__ */ jsx(TextShimmer, { className: "text-sm", duration: 2, children: "Generating..." })
4033
3469
  ] });
4034
3470
  }
3471
+ const FollowUpSuggestions = lazy(
3472
+ () => import("./follow-up-suggestions-CSSc4PDe.js").then((m) => ({
3473
+ default: m.FollowUpSuggestions
3474
+ }))
3475
+ );
4035
3476
  function ChatMessageListComponent({
4036
3477
  messages,
4037
3478
  loading,
@@ -4058,6 +3499,7 @@ function ChatMessageListComponent({
4058
3499
  const displayMessages = useMemo(() => {
4059
3500
  return messages.filter((msg) => msg.role !== "toolResult");
4060
3501
  }, [messages]);
3502
+ const deferredDisplayMessages = useDeferredValue(displayMessages);
4061
3503
  const toolResultsByCallId = useMemo(() => {
4062
3504
  const map = /* @__PURE__ */ new Map();
4063
3505
  for (const message of messages) {
@@ -4069,10 +3511,40 @@ function ChatMessageListComponent({
4069
3511
  }
4070
3512
  return map;
4071
3513
  }, [messages]);
3514
+ const deferredToolResultsByCallId = useDeferredValue(toolResultsByCallId);
3515
+ const aggregatedSearchSourcesSignature = useMemo(() => {
3516
+ const parts = [];
3517
+ for (const msg of deferredDisplayMessages) {
3518
+ if (msg.role !== "assistant") continue;
3519
+ const toolCalls = getToolCallsFromMessage(msg);
3520
+ for (const tc of toolCalls) {
3521
+ if (!tc.id) continue;
3522
+ const isSearch = tc.name === "web_search";
3523
+ const isFetch = tc.name === "web_fetch";
3524
+ const isExec = tc.name === "exec";
3525
+ if (!isSearch && !isFetch && !isExec) continue;
3526
+ const result = deferredToolResultsByCallId.get(tc.id);
3527
+ const text = result?.content?.map((p) => p.type === "text" ? String(p.text ?? "") : "").join("").trim() ?? "";
3528
+ parts.push(
3529
+ [
3530
+ tc.id,
3531
+ tc.name ?? "",
3532
+ typeof tc.arguments?.url === "string" ? tc.arguments.url : "",
3533
+ result?.isError ? "1" : "0",
3534
+ text
3535
+ ].join("::")
3536
+ );
3537
+ }
3538
+ }
3539
+ return parts.join("||");
3540
+ }, [deferredDisplayMessages, deferredToolResultsByCallId]);
4072
3541
  const aggregatedSearchSources = useMemo(() => {
4073
3542
  const strip = (s) => {
4074
3543
  if (!s) return "";
4075
- return s.replace(/SECURITY NOTICE:[\s\S]*?<<<EXTERNAL_UNTRUSTED_CONTENT>>>/g, "").replace(/<<<\/?EXTERNAL_UNTRUSTED_CONTENT>>>/g, "").replace(/<<<\/?END_EXTERNAL_UNTRUSTED_CONTENT>>>/g, "").replace(/Source: Web (?:Search|Fetch)\n---/g, "").replace(/\n{2,}/g, "\n").trim();
3544
+ return s.replace(
3545
+ /SECURITY NOTICE:[\s\S]*?<<<EXTERNAL_UNTRUSTED_CONTENT>>>/g,
3546
+ ""
3547
+ ).replace(/<<<\/?EXTERNAL_UNTRUSTED_CONTENT>>>/g, "").replace(/<<<\/?END_EXTERNAL_UNTRUSTED_CONTENT>>>/g, "").replace(/Source: Web (?:Search|Fetch)\n---/g, "").replace(/\n{2,}/g, "\n").trim();
4076
3548
  };
4077
3549
  const extractResults = (text, sources2, seenUrls2) => {
4078
3550
  try {
@@ -4086,7 +3558,9 @@ function ChatMessageListComponent({
4086
3558
  sources2.push({
4087
3559
  title: strip(item.title),
4088
3560
  url: item.url,
4089
- snippet: strip(item.description || item.snippet || item.content || "")
3561
+ snippet: strip(
3562
+ item.description || item.snippet || item.content || ""
3563
+ )
4090
3564
  });
4091
3565
  found = true;
4092
3566
  }
@@ -4098,7 +3572,7 @@ function ChatMessageListComponent({
4098
3572
  };
4099
3573
  const sources = [];
4100
3574
  const seenUrls = /* @__PURE__ */ new Set();
4101
- for (const msg of displayMessages) {
3575
+ for (const msg of deferredDisplayMessages) {
4102
3576
  if (msg.role !== "assistant") continue;
4103
3577
  const toolCalls = getToolCallsFromMessage(msg);
4104
3578
  for (const tc of toolCalls) {
@@ -4107,7 +3581,7 @@ function ChatMessageListComponent({
4107
3581
  const isFetch = tc.name === "web_fetch";
4108
3582
  const isExec = tc.name === "exec";
4109
3583
  if (!isSearch && !isFetch && !isExec) continue;
4110
- const result = toolResultsByCallId.get(tc.id);
3584
+ const result = deferredToolResultsByCallId.get(tc.id);
4111
3585
  if (!result) continue;
4112
3586
  const text = result.content?.map((p) => p.type === "text" ? String(p.text ?? "") : "").join("").trim();
4113
3587
  if (!text) continue;
@@ -4137,7 +3611,11 @@ function ChatMessageListComponent({
4137
3611
  }
4138
3612
  }
4139
3613
  return sources;
4140
- }, [displayMessages, toolResultsByCallId]);
3614
+ }, [
3615
+ aggregatedSearchSourcesSignature,
3616
+ deferredDisplayMessages,
3617
+ deferredToolResultsByCallId
3618
+ ]);
4141
3619
  const lastAssistantIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role !== "user").map(({ index }) => index).pop();
4142
3620
  const lastTextAssistantIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role === "assistant").map(({ index }) => index).pop();
4143
3621
  const lastUserIndex = displayMessages.map((message, index) => ({ message, index })).filter(({ message }) => message.role === "user").map(({ index }) => index).pop();
@@ -4179,7 +3657,9 @@ function ChatMessageListComponent({
4179
3657
  target.scrollIntoView({ behavior: "smooth", block: "center" });
4180
3658
  setHighlightedMessageId(jumpToMessageId);
4181
3659
  const timer = window.setTimeout(() => {
4182
- setHighlightedMessageId((prev) => prev === jumpToMessageId ? null : prev);
3660
+ setHighlightedMessageId(
3661
+ (prev) => prev === jumpToMessageId ? null : prev
3662
+ );
4183
3663
  }, 1800);
4184
3664
  return () => window.clearTimeout(timer);
4185
3665
  }, [jumpToMessageId, loading, displayMessages]);
@@ -4244,14 +3724,14 @@ function ChatMessageListComponent({
4244
3724
  );
4245
3725
  }),
4246
3726
  showTypingIndicator ? /* @__PURE__ */ jsx("div", { className: "py-2", children: /* @__PURE__ */ jsx(TypingIndicator, {}) }) : null,
4247
- showFollowUps && onFollowUpClick ? /* @__PURE__ */ jsx(
4248
- MemoizedFollowUpSuggestions,
3727
+ showFollowUps && onFollowUpClick ? /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
3728
+ FollowUpSuggestions,
4249
3729
  {
4250
3730
  responseText: lastAssistantText,
4251
3731
  onSuggestionClick: onFollowUpClick,
4252
3732
  disabled: waitingForResponse
4253
3733
  }
4254
- ) : null
3734
+ ) }) : null
4255
3735
  ]
4256
3736
  }
4257
3737
  )
@@ -4277,14 +3757,14 @@ function ChatMessageListComponent({
4277
3757
  messageKey
4278
3758
  );
4279
3759
  }),
4280
- showFollowUps && onFollowUpClick ? /* @__PURE__ */ jsx(
4281
- MemoizedFollowUpSuggestions,
3760
+ showFollowUps && onFollowUpClick ? /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
3761
+ FollowUpSuggestions,
4282
3762
  {
4283
3763
  responseText: lastAssistantText,
4284
3764
  onSuggestionClick: onFollowUpClick,
4285
3765
  disabled: waitingForResponse
4286
3766
  }
4287
- ) : null
3767
+ ) }) : null
4288
3768
  ] }),
4289
3769
  notice && noticePosition === "end" ? notice : null,
4290
3770
  /* @__PURE__ */ jsx(
@@ -4462,159 +3942,27 @@ function PromptInputActions({
4462
3942
  children,
4463
3943
  className,
4464
3944
  ...props
4465
- }) {
4466
- return /* @__PURE__ */ jsx("div", { className: cn("flex items-center gap-2", className), ...props, children });
4467
- }
4468
- function PromptInputAction({
4469
- tooltip,
4470
- children,
4471
- className,
4472
- side = "top",
4473
- ...props
4474
- }) {
4475
- const { disabled } = usePromptInput();
4476
- return /* @__PURE__ */ jsxs(TooltipRoot, { ...props, children: [
4477
- /* @__PURE__ */ jsx(
4478
- TooltipTrigger,
4479
- {
4480
- disabled,
4481
- onClick: (event) => event.stopPropagation(),
4482
- children
4483
- }
4484
- ),
4485
- /* @__PURE__ */ jsx(TooltipContent, { side, className, children: tooltip })
4486
- ] });
4487
- }
4488
- const STORAGE_KEY = "opencami-selected-model";
4489
- function getStoredModel() {
4490
- if (typeof window === "undefined") return null;
4491
- try {
4492
- return localStorage.getItem(STORAGE_KEY);
4493
- } catch {
4494
- return null;
4495
- }
4496
- }
4497
- function setStoredModel(modelId) {
4498
- if (typeof window === "undefined") return;
4499
- try {
4500
- localStorage.setItem(STORAGE_KEY, modelId);
4501
- } catch {
4502
- }
4503
- }
4504
- function ModelSelector({ className, onModelChange }) {
4505
- const [models, setModels] = useState([]);
4506
- const [selectedModel, setSelectedModel] = useState("");
4507
- const [isLoading, setIsLoading] = useState(true);
4508
- const [error, setError] = useState(null);
4509
- const abortControllerRef = useRef(null);
4510
- useEffect(() => {
4511
- let mounted = true;
4512
- async function fetchModels() {
4513
- abortControllerRef.current?.abort();
4514
- const controller = new AbortController();
4515
- abortControllerRef.current = controller;
4516
- try {
4517
- setIsLoading(true);
4518
- setError(null);
4519
- const response = await fetch("/api/models", { signal: controller.signal });
4520
- if (!response.ok) {
4521
- throw new Error("Failed to fetch models");
4522
- }
4523
- const data = await response.json();
4524
- if (!mounted) return;
4525
- if (data.ok && data.models.length > 0) {
4526
- setModels(data.models);
4527
- const storedModel = getStoredModel();
4528
- const initialModel = storedModel && data.models.some((m) => m.id === storedModel) ? storedModel : data.defaultModel;
4529
- setSelectedModel(initialModel);
4530
- onModelChange?.(initialModel);
4531
- } else {
4532
- throw new Error("No models available");
4533
- }
4534
- } catch (err) {
4535
- if (err instanceof Error && err.name === "AbortError") return;
4536
- if (!mounted) return;
4537
- console.error("[model-selector] Error fetching models:", err);
4538
- setError(err instanceof Error ? err.message : "Failed to load models");
4539
- setModels([{ id: "default", name: "Default Model" }]);
4540
- setSelectedModel("default");
4541
- } finally {
4542
- if (mounted) {
4543
- setIsLoading(false);
4544
- }
4545
- }
4546
- }
4547
- fetchModels();
4548
- return () => {
4549
- mounted = false;
4550
- abortControllerRef.current?.abort();
4551
- };
4552
- }, [onModelChange]);
4553
- function handleModelSelect(modelId) {
4554
- setSelectedModel(modelId);
4555
- setStoredModel(modelId);
4556
- onModelChange?.(modelId);
4557
- }
4558
- if (isLoading) {
4559
- return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2 text-xs text-primary-500", className), children: [
4560
- /* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArtificialIntelligence02Icon, size: 14 }),
4561
- /* @__PURE__ */ jsx("span", { children: "Loading..." })
4562
- ] });
4563
- }
4564
- if (error || models.length === 0) {
4565
- return null;
4566
- }
4567
- const selectedModelInfo = models.find((m) => m.id === selectedModel);
4568
- const displayName = selectedModelInfo?.name || "Select Model";
4569
- const shortDisplayName = (() => {
4570
- if (!selectedModelInfo?.name) return displayName;
4571
- const name = selectedModelInfo.name;
4572
- const clean = name.replace(/\s*\([^)]*\)\s*$/, "").trim();
4573
- const words = clean.split(/\s+/);
4574
- if (words.length > 3) return words.slice(0, 3).join(" ");
4575
- return clean;
4576
- })();
4577
- if (models.length === 1) {
4578
- return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2 text-xs text-primary-500", className), children: [
4579
- /* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArtificialIntelligence02Icon, size: 14 }),
4580
- /* @__PURE__ */ jsx("span", { className: "font-[450] md:hidden", children: shortDisplayName }),
4581
- /* @__PURE__ */ jsx("span", { className: "font-[450] hidden md:inline", children: displayName })
4582
- ] });
4583
- }
4584
- return /* @__PURE__ */ jsxs(MenuRoot, { children: [
4585
- /* @__PURE__ */ jsxs(
4586
- MenuTrigger,
3945
+ }) {
3946
+ return /* @__PURE__ */ jsx("div", { className: cn("flex items-center gap-2", className), ...props, children });
3947
+ }
3948
+ function PromptInputAction({
3949
+ tooltip,
3950
+ children,
3951
+ className,
3952
+ side = "top",
3953
+ ...props
3954
+ }) {
3955
+ const { disabled } = usePromptInput();
3956
+ return /* @__PURE__ */ jsxs(TooltipRoot, { ...props, children: [
3957
+ /* @__PURE__ */ jsx(
3958
+ TooltipTrigger,
4587
3959
  {
4588
- className: cn(
4589
- "inline-flex h-7 items-center gap-2 rounded-md px-2 text-xs font-[450] text-primary-600 hover:text-primary-900 hover:bg-primary-100",
4590
- className
4591
- ),
4592
- children: [
4593
- /* @__PURE__ */ jsx(HugeiconsIcon, { icon: ArtificialIntelligence02Icon, size: 14 }),
4594
- /* @__PURE__ */ jsx("span", { className: "md:hidden", children: shortDisplayName }),
4595
- /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: displayName })
4596
- ]
3960
+ disabled,
3961
+ onClick: (event) => event.stopPropagation(),
3962
+ children
4597
3963
  }
4598
3964
  ),
4599
- /* @__PURE__ */ jsx(MenuContent, { side: "top", align: "start", children: models.map((model) => /* @__PURE__ */ jsxs(
4600
- MenuItem,
4601
- {
4602
- onClick: () => handleModelSelect(model.id),
4603
- className: "justify-between min-w-[180px]",
4604
- children: [
4605
- /* @__PURE__ */ jsx("span", { children: model.name }),
4606
- selectedModel === model.id && /* @__PURE__ */ jsx(
4607
- HugeiconsIcon,
4608
- {
4609
- icon: Tick02Icon,
4610
- size: 14,
4611
- className: "text-primary-600"
4612
- }
4613
- )
4614
- ]
4615
- },
4616
- model.id
4617
- )) })
3965
+ /* @__PURE__ */ jsx(TooltipContent, { side, className, children: tooltip })
4618
3966
  ] });
4619
3967
  }
4620
3968
  const PERSONAS_ENABLED_KEY = "opencami-personas-enabled";
@@ -5374,6 +4722,7 @@ function ChatComposerComponent({
5374
4722
  const [isRecording, setIsRecording] = useState(false);
5375
4723
  const [recordingTime, setRecordingTime] = useState(0);
5376
4724
  const [sttLoading, setSttLoading] = useState(false);
4725
+ const [micError, setMicError] = useState(null);
5377
4726
  const mediaRecorderRef = useRef(null);
5378
4727
  const recordingChunksRef = useRef([]);
5379
4728
  const recordingTimerRef = useRef(null);
@@ -5406,6 +4755,9 @@ function ChatComposerComponent({
5406
4755
  promptRef.current?.focus();
5407
4756
  });
5408
4757
  }, []);
4758
+ const showMicError = useCallback((message) => {
4759
+ setMicError(message);
4760
+ }, []);
5409
4761
  const reset = useCallback(() => {
5410
4762
  setValue("");
5411
4763
  setAttachments([]);
@@ -5629,11 +4981,12 @@ ${template}`;
5629
4981
  setRecordingTime(0);
5630
4982
  }, []);
5631
4983
  const startRecording = useCallback(async () => {
4984
+ setMicError(null);
5632
4985
  const provider = getSttProvider();
5633
4986
  if (provider === "browser") {
5634
4987
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
5635
4988
  if (!SpeechRecognition) {
5636
- alert("Web Speech API is not supported in this browser.");
4989
+ showMicError("Web Speech API is not supported in this browser.");
5637
4990
  return;
5638
4991
  }
5639
4992
  const recognition = new SpeechRecognition();
@@ -5665,6 +5018,7 @@ ${template}`;
5665
5018
  setRecordingTime(0);
5666
5019
  if (recordingTimerRef.current) clearInterval(recordingTimerRef.current);
5667
5020
  if (transcript.trim()) {
5021
+ setMicError(null);
5668
5022
  setValue((prev) => prev + (prev ? " " : "") + transcript.trim());
5669
5023
  focusPrompt();
5670
5024
  }
@@ -5673,6 +5027,7 @@ ${template}`;
5673
5027
  setIsRecording(false);
5674
5028
  setRecordingTime(0);
5675
5029
  if (recordingTimerRef.current) clearInterval(recordingTimerRef.current);
5030
+ showMicError("Speech recognition failed. Try the Browser provider again or switch providers in Settings.");
5676
5031
  };
5677
5032
  recognition.start();
5678
5033
  return;
@@ -5701,16 +5056,17 @@ ${template}`;
5701
5056
  const res = await fetch("/api/stt", { method: "POST", body: formData, signal: controller.signal });
5702
5057
  const data = await res.json();
5703
5058
  if (data.ok && data.text) {
5059
+ setMicError(null);
5704
5060
  setValue((prev) => prev + (prev ? " " : "") + data.text);
5705
5061
  focusPrompt();
5706
5062
  } else if (!data.ok) {
5707
5063
  console.warn("STT failed:", data.error);
5708
- alert(data.error || "Speech-to-text failed. Try the Browser provider in Settings.");
5064
+ showMicError(data.error || "Speech-to-text failed. Try the Browser provider in Settings.");
5709
5065
  }
5710
5066
  } catch (err) {
5711
5067
  if (err instanceof Error && err.name === "AbortError") return;
5712
5068
  console.warn("STT request failed:", err);
5713
- alert("Could not reach speech-to-text service.");
5069
+ showMicError("Could not reach speech-to-text service.");
5714
5070
  } finally {
5715
5071
  setSttLoading(false);
5716
5072
  }
@@ -5733,18 +5089,18 @@ ${template}`;
5733
5089
  try {
5734
5090
  const status = await navigator.permissions.query({ name: "microphone" });
5735
5091
  if (status.state === "denied") {
5736
- alert("Microphone access is blocked. Please enable it in your browser/app settings.");
5092
+ showMicError("Microphone access is blocked. Please enable it in your browser/app settings.");
5737
5093
  } else {
5738
- alert("Microphone permission was not granted. Please try again and allow access when prompted.");
5094
+ showMicError("Microphone permission was not granted. Please try again and allow access when prompted.");
5739
5095
  }
5740
5096
  } catch {
5741
- alert("Could not access microphone. Please check your browser settings and allow microphone access for this site.");
5097
+ showMicError("Could not access microphone. Please check your browser settings and allow microphone access for this site.");
5742
5098
  }
5743
5099
  } else {
5744
- alert("Could not access microphone: " + msg);
5100
+ showMicError("Could not access microphone: " + msg);
5745
5101
  }
5746
5102
  }
5747
- }, [getSttProvider, stopRecording, focusPrompt]);
5103
+ }, [getSttProvider, stopRecording, focusPrompt, showMicError]);
5748
5104
  const handleMicClick = useCallback(() => {
5749
5105
  if (isRecording) {
5750
5106
  stopRecording();
@@ -5780,6 +5136,15 @@ ${template}`;
5780
5136
  onRemove: handleRemoveAttachment
5781
5137
  }
5782
5138
  ),
5139
+ micError && /* @__PURE__ */ jsx(
5140
+ "div",
5141
+ {
5142
+ className: "mx-3 mt-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900/40 dark:bg-red-950/40 dark:text-red-200",
5143
+ role: "alert",
5144
+ "aria-live": "polite",
5145
+ children: micError
5146
+ }
5147
+ ),
5783
5148
  showSlashCommands && filteredSlashCommands.length > 0 && /* @__PURE__ */ jsx(
5784
5149
  SlashCommandMenu,
5785
5150
  {
@@ -5798,7 +5163,6 @@ ${template}`;
5798
5163
  ),
5799
5164
  /* @__PURE__ */ jsxs(PromptInputActions, { className: "justify-between px-3", children: [
5800
5165
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
5801
- /* @__PURE__ */ jsx(ModelSelector, { onModelChange: setSelectedModel }),
5802
5166
  /* @__PURE__ */ jsx(ThinkingLevelSelector, {}),
5803
5167
  /* @__PURE__ */ jsx(PersonaPicker, { onSelect: handlePersonaSelect }),
5804
5168
  /* @__PURE__ */ jsx(CommandHelp, { onCommandSelect: (cmd) => handleValueChange(cmd + " ") })
@@ -6066,10 +5430,25 @@ function useChatSessions({
6066
5430
  isNewChat,
6067
5431
  forcedSessionKey
6068
5432
  }) {
5433
+ const [isDocumentVisible, setIsDocumentVisible] = useState(() => {
5434
+ if (typeof document === "undefined") return true;
5435
+ return document.visibilityState === "visible";
5436
+ });
5437
+ useEffect(() => {
5438
+ if (typeof document === "undefined") return void 0;
5439
+ const handleVisibilityChange = () => {
5440
+ setIsDocumentVisible(document.visibilityState === "visible");
5441
+ };
5442
+ handleVisibilityChange();
5443
+ document.addEventListener("visibilitychange", handleVisibilityChange);
5444
+ return () => {
5445
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
5446
+ };
5447
+ }, []);
6069
5448
  const sessionsQuery = useQuery({
6070
5449
  queryKey: chatQueryKeys.sessions,
6071
5450
  queryFn: fetchSessions,
6072
- refetchInterval: 3e4
5451
+ refetchInterval: isDocumentVisible ? 3e4 : false
6073
5452
  });
6074
5453
  const sessions = useMemo(() => {
6075
5454
  const rawSessions = sessionsQuery.data ?? [];
@@ -6109,6 +5488,197 @@ function useChatSessions({
6109
5488
  sessionsError
6110
5489
  };
6111
5490
  }
5491
+ const LLM_PROVIDER_DEFAULTS = {
5492
+ openai: {
5493
+ baseUrl: "https://api.openai.com/v1",
5494
+ model: "gpt-4.1-nano"
5495
+ },
5496
+ openrouter: {
5497
+ baseUrl: "https://openrouter.ai/api/v1",
5498
+ model: "openai/gpt-oss-120b"
5499
+ },
5500
+ kilocode: {
5501
+ baseUrl: "https://api.kilo.ai/api/gateway",
5502
+ model: "google/gemini-2.5-flash"
5503
+ },
5504
+ ollama: {
5505
+ baseUrl: "http://localhost:11434/v1",
5506
+ model: "llama3.2"
5507
+ },
5508
+ custom: {
5509
+ baseUrl: "",
5510
+ model: ""
5511
+ }
5512
+ };
5513
+ const DEFAULT_LLM_SETTINGS = {
5514
+ useLlmTitles: true,
5515
+ useLlmFollowUps: true,
5516
+ llmProvider: "openai",
5517
+ llmBaseUrl: "",
5518
+ llmModel: "",
5519
+ llmApiKey: ""
5520
+ };
5521
+ function getLlmProviderDefaults(provider) {
5522
+ return LLM_PROVIDER_DEFAULTS[provider];
5523
+ }
5524
+ function getEffectiveLlmBaseUrl(settings) {
5525
+ if (settings.llmBaseUrl.trim()) return settings.llmBaseUrl.trim();
5526
+ return getLlmProviderDefaults(settings.llmProvider).baseUrl;
5527
+ }
5528
+ function getEffectiveLlmModel(settings) {
5529
+ if (settings.llmModel.trim()) return settings.llmModel.trim();
5530
+ return getLlmProviderDefaults(settings.llmProvider).model;
5531
+ }
5532
+ function getAvailability(settings, hasEnvKey) {
5533
+ if (settings.llmProvider === "ollama") return true;
5534
+ if (settings.llmProvider === "custom") {
5535
+ return Boolean(settings.llmApiKey.trim()) || Boolean(settings.llmBaseUrl.trim() && settings.llmModel.trim());
5536
+ }
5537
+ return hasEnvKey || Boolean(settings.llmApiKey.trim());
5538
+ }
5539
+ function migratePersistedState(persistedState) {
5540
+ if (!persistedState || typeof persistedState !== "object") {
5541
+ return { settings: DEFAULT_LLM_SETTINGS };
5542
+ }
5543
+ const { settings } = persistedState;
5544
+ if (!settings) {
5545
+ return { settings: DEFAULT_LLM_SETTINGS };
5546
+ }
5547
+ const { openaiApiKey, ...rest } = settings;
5548
+ const llmApiKey = rest.llmApiKey ?? openaiApiKey ?? "";
5549
+ return {
5550
+ settings: {
5551
+ ...DEFAULT_LLM_SETTINGS,
5552
+ ...rest,
5553
+ llmApiKey
5554
+ }
5555
+ };
5556
+ }
5557
+ const useLlmSettingsStore = create()(
5558
+ persist(
5559
+ (set) => ({
5560
+ settings: {
5561
+ ...DEFAULT_LLM_SETTINGS
5562
+ },
5563
+ updateSettings: (updates) => set((state) => ({
5564
+ settings: { ...state.settings, ...updates }
5565
+ })),
5566
+ clearApiKey: () => set((state) => ({
5567
+ settings: { ...state.settings, llmApiKey: "" }
5568
+ }))
5569
+ }),
5570
+ {
5571
+ name: "llm-settings",
5572
+ version: 2,
5573
+ migrate: (persistedState) => migratePersistedState(persistedState)
5574
+ }
5575
+ )
5576
+ );
5577
+ function useLlmSettings() {
5578
+ const settings = useLlmSettingsStore((state) => state.settings);
5579
+ const updateSettings = useLlmSettingsStore((state) => state.updateSettings);
5580
+ const clearApiKey = useLlmSettingsStore((state) => state.clearApiKey);
5581
+ const [status, setStatus] = useState({
5582
+ hasEnvKey: false,
5583
+ hasOpenRouterKey: false,
5584
+ hasKilocodeKey: false,
5585
+ hasUserKey: Boolean(settings.llmApiKey),
5586
+ isAvailable: getAvailability(settings, false),
5587
+ isLoading: true,
5588
+ error: null
5589
+ });
5590
+ useEffect(() => {
5591
+ let cancelled = false;
5592
+ async function checkStatus() {
5593
+ try {
5594
+ const res = await fetch("/api/llm-features");
5595
+ if (!res.ok) throw new Error("Failed to check LLM status");
5596
+ const data = await res.json();
5597
+ if (cancelled) return;
5598
+ const hasUserKey = Boolean(settings.llmApiKey);
5599
+ const hasProviderKey = settings.llmProvider === "openrouter" ? Boolean(data.hasOpenRouterKey) : settings.llmProvider === "kilocode" ? Boolean(data.hasKilocodeKey) : data.hasEnvKey;
5600
+ setStatus({
5601
+ hasEnvKey: data.hasEnvKey,
5602
+ hasOpenRouterKey: Boolean(data.hasOpenRouterKey),
5603
+ hasKilocodeKey: Boolean(data.hasKilocodeKey),
5604
+ hasUserKey,
5605
+ isAvailable: getAvailability(settings, hasProviderKey),
5606
+ isLoading: false,
5607
+ error: null
5608
+ });
5609
+ } catch (err) {
5610
+ if (cancelled) return;
5611
+ setStatus((prev) => ({
5612
+ ...prev,
5613
+ isLoading: false,
5614
+ error: err instanceof Error ? err.message : "Failed to check status"
5615
+ }));
5616
+ }
5617
+ }
5618
+ void checkStatus();
5619
+ return () => {
5620
+ cancelled = true;
5621
+ };
5622
+ }, [
5623
+ settings.llmApiKey,
5624
+ settings.llmProvider,
5625
+ settings.llmBaseUrl,
5626
+ settings.llmModel
5627
+ ]);
5628
+ const testApiKey = useCallback(async (key) => {
5629
+ try {
5630
+ const headers = buildLlmHeaders({
5631
+ ...settings,
5632
+ llmApiKey: key
5633
+ });
5634
+ const res = await fetch("/api/llm-features", {
5635
+ method: "POST",
5636
+ headers: {
5637
+ "Content-Type": "application/json",
5638
+ ...headers
5639
+ },
5640
+ body: JSON.stringify({ action: "test" })
5641
+ });
5642
+ const data = await res.json();
5643
+ if (!data.ok) {
5644
+ return { valid: false, error: data.error || "Test failed" };
5645
+ }
5646
+ return { valid: data.valid ?? false, error: data.error };
5647
+ } catch (err) {
5648
+ return {
5649
+ valid: false,
5650
+ error: err instanceof Error ? err.message : "Network error"
5651
+ };
5652
+ }
5653
+ }, [settings]);
5654
+ return {
5655
+ settings,
5656
+ updateSettings,
5657
+ clearApiKey,
5658
+ status,
5659
+ testApiKey
5660
+ };
5661
+ }
5662
+ function buildLlmHeaders(settings) {
5663
+ const apiKey = settings.llmApiKey;
5664
+ const baseUrl = getEffectiveLlmBaseUrl(settings);
5665
+ const model = getEffectiveLlmModel(settings);
5666
+ if (apiKey) {
5667
+ return {
5668
+ "X-OpenAI-API-Key": apiKey,
5669
+ ...baseUrl ? { "X-LLM-Base-URL": baseUrl } : {},
5670
+ ...model ? { "X-LLM-Model": model } : {}
5671
+ };
5672
+ }
5673
+ return {
5674
+ ...baseUrl ? { "X-LLM-Base-URL": baseUrl } : {},
5675
+ ...model ? { "X-LLM-Model": model } : {}
5676
+ };
5677
+ }
5678
+ function getLlmHeaders() {
5679
+ const settings = useLlmSettingsStore.getState().settings;
5680
+ return buildLlmHeaders(settings);
5681
+ }
6112
5682
  function useSmartTitle() {
6113
5683
  const llmSettings = useLlmSettingsStore((state) => state.settings);
6114
5684
  const [isGenerating, setIsGenerating] = useState(false);
@@ -6596,7 +6166,7 @@ const KeyboardShortcutsDialog = lazy(
6596
6166
  }))
6597
6167
  );
6598
6168
  const SearchDialog = lazy(
6599
- () => import("./search-dialog-DQRkARXw.js").then((m) => ({
6169
+ () => import("./search-dialog-DR6zBnui.js").then((m) => ({
6600
6170
  default: m.SearchDialog
6601
6171
  }))
6602
6172
  );
@@ -6623,7 +6193,9 @@ function ChatScreen({
6623
6193
  const [showShortcutsHelp, setShowShortcutsHelp] = useState(false);
6624
6194
  const [showSearchDialog, setShowSearchDialog] = useState(false);
6625
6195
  const [searchMode, setSearchMode] = useState("global");
6626
- const [searchJumpMessageId, setSearchJumpMessageId] = useState(null);
6196
+ const [searchJumpMessageId, setSearchJumpMessageId] = useState(
6197
+ null
6198
+ );
6627
6199
  const [isStreaming, setIsStreaming] = useState(false);
6628
6200
  const thinkingLevel = useThinkingLevelStore((state) => state.level);
6629
6201
  const { maybeNotifyAssistantMessage } = useNotifications();
@@ -6735,8 +6307,8 @@ function ChatScreen({
6735
6307
  const llmTitlesEnabled = useLlmTitlesEnabled();
6736
6308
  const { generateTitle } = useSmartTitle();
6737
6309
  const titleGeneratedRef = useRef(/* @__PURE__ */ new Set());
6738
- const FAST_POLL_MS = 150;
6739
- const NORMAL_POLL_MS = 600;
6310
+ const FAST_POLL_MS = 2e3;
6311
+ const NORMAL_POLL_MS = 2e3;
6740
6312
  const streamStart = useCallback(() => {
6741
6313
  if (!activeFriendlyId || isNewChat) return;
6742
6314
  if (streamTimer.current) window.clearInterval(streamTimer.current);
@@ -6769,12 +6341,9 @@ function ChatScreen({
6769
6341
  },
6770
6342
  [historyQuery, queryClient, stopStream, streamFinish]
6771
6343
  );
6772
- handleStreamErrorRef.current = useCallback(
6773
- (_err) => {
6774
- console.warn("[stream] SSE error, falling back to polling");
6775
- },
6776
- []
6777
- );
6344
+ handleStreamErrorRef.current = useCallback((_err) => {
6345
+ console.warn("[stream] SSE error, falling back to polling");
6346
+ }, []);
6778
6347
  const streamingMessage = useMemo(() => {
6779
6348
  if (!streaming.text) return null;
6780
6349
  const content = [];
@@ -6917,7 +6486,9 @@ function ChatScreen({
6917
6486
  if (!sessionKey) return;
6918
6487
  if (titleGeneratedRef.current.has(sessionKey)) return;
6919
6488
  const userMessages = historyMessages.filter((m) => m.role === "user");
6920
- const assistantMessages = historyMessages.filter((m) => m.role === "assistant");
6489
+ const assistantMessages = historyMessages.filter(
6490
+ (m) => m.role === "assistant"
6491
+ );
6921
6492
  if (userMessages.length === 0 || assistantMessages.length === 0) return;
6922
6493
  if (userMessages.length !== 1) return;
6923
6494
  const firstUserMessage = textFromMessage(userMessages[0]);
@@ -6927,7 +6498,12 @@ function ChatScreen({
6927
6498
  try {
6928
6499
  const result = await generateTitle(firstUserMessage);
6929
6500
  if (result.title) {
6930
- await updateSessionLabel(queryClient, sessionKey, activeFriendlyId, result.title);
6501
+ await updateSessionLabel(
6502
+ queryClient,
6503
+ sessionKey,
6504
+ activeFriendlyId,
6505
+ result.title
6506
+ );
6931
6507
  }
6932
6508
  } catch (err) {
6933
6509
  console.error("[smart-title] Error generating title:", err);
@@ -7025,7 +6601,13 @@ function ChatScreen({
7025
6601
  }
7026
6602
  setWaitingForResponse(true);
7027
6603
  setPinToTop(true);
7028
- sendMessage(pending.sessionKey, pending.friendlyId, pending.message, true, pending.attachments);
6604
+ sendMessage(
6605
+ pending.sessionKey,
6606
+ pending.friendlyId,
6607
+ pending.message,
6608
+ true,
6609
+ pending.attachments
6610
+ );
7029
6611
  }, [
7030
6612
  activeFriendlyId,
7031
6613
  activeSessionKey,
@@ -7037,7 +6619,10 @@ function ChatScreen({
7037
6619
  function sendMessage(sessionKey, friendlyId, body, skipOptimistic = false, attachments, model) {
7038
6620
  let optimisticClientId = "";
7039
6621
  if (!skipOptimistic) {
7040
- const { clientId, optimisticMessage } = createOptimisticMessage(body, attachments);
6622
+ const { clientId, optimisticMessage } = createOptimisticMessage(
6623
+ body,
6624
+ attachments
6625
+ );
7041
6626
  optimisticClientId = clientId;
7042
6627
  appendHistoryMessage(
7043
6628
  queryClient,
@@ -7129,7 +6714,8 @@ function ChatScreen({
7129
6714
  const send = useCallback(
7130
6715
  (body, helpers) => {
7131
6716
  const attachments = helpers.attachments;
7132
- if (body.length === 0 && (!attachments || attachments.length === 0)) return;
6717
+ if (body.length === 0 && (!attachments || attachments.length === 0))
6718
+ return;
7133
6719
  helpers.reset();
7134
6720
  if (isNewChat) {
7135
6721
  const { clientId, optimisticId, optimisticMessage } = createOptimisticMessage(body, attachments);
@@ -7176,7 +6762,14 @@ function ChatScreen({
7176
6762
  return;
7177
6763
  }
7178
6764
  const sessionKeyForSend = forcedSessionKey || resolvedSessionKey || activeSessionKey;
7179
- sendMessage(sessionKeyForSend, activeFriendlyId, body, false, attachments, helpers.model);
6765
+ sendMessage(
6766
+ sessionKeyForSend,
6767
+ activeFriendlyId,
6768
+ body,
6769
+ false,
6770
+ attachments,
6771
+ helpers.model
6772
+ );
7180
6773
  },
7181
6774
  [
7182
6775
  activeFriendlyId,
@@ -7431,7 +7024,10 @@ function ChatScreen({
7431
7024
  } catch {
7432
7025
  }
7433
7026
  }
7434
- navigate({ to: "/chat/$sessionKey", params: { sessionKey: result.friendlyId } });
7027
+ navigate({
7028
+ to: "/chat/$sessionKey",
7029
+ params: { sessionKey: result.friendlyId }
7030
+ });
7435
7031
  }
7436
7032
  }
7437
7033
  ) })
@@ -7467,7 +7063,12 @@ const $sessionKey = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineP
7467
7063
  }, Symbol.toStringTag, { value: "Module" }));
7468
7064
  export {
7469
7065
  $sessionKey as $,
7066
+ Collapsible as C,
7067
+ useLlmSettingsStore as a,
7068
+ getLlmHeaders as b,
7470
7069
  chatQueryKeys as c,
7070
+ CollapsibleTrigger as d,
7071
+ CollapsiblePanel as e,
7471
7072
  getLlmProviderDefaults as g,
7472
7073
  useLlmSettings as u
7473
7074
  };