opencami 2.0.0 → 2.1.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 (69) hide show
  1. package/dist/client/assets/{CSPContext-CrlIQW7-.js → CSPContext-CxrBvJ99.js} +1 -1
  2. package/dist/client/assets/{DirectionContext-X-0CRn1O.js → DirectionContext-BOL0P_1j.js} +1 -1
  3. package/dist/client/assets/_sessionKey-CTtu2ipR.js +25 -0
  4. package/dist/client/assets/agents-D7JS19Lo.js +2 -0
  5. package/dist/client/assets/{agents-screen-BhOVp_S6.js → agents-screen-CqdzZKaR.js} +1 -1
  6. package/dist/client/assets/bots-DAInzfLx.js +2 -0
  7. package/dist/client/assets/{bots-screen-DLFd0ydi.js → bots-screen-RYovD6cu.js} +1 -1
  8. package/dist/client/assets/{button-D0n2Qsd_.js → button-Bv-7bwZZ.js} +1 -1
  9. package/dist/client/assets/{composite-GtKwZKbV.js → composite-DwQIZFe9.js} +1 -1
  10. package/dist/client/assets/{connect-vQWL0_11.js → connect-DZSrhhNT.js} +1 -1
  11. package/dist/client/assets/{dashboard-Knwc61i1.js → dashboard-y8KVUVF-.js} +1 -1
  12. package/dist/client/assets/{event-CHpdjYFR.js → event-Cnr9OETn.js} +1 -1
  13. package/dist/client/assets/{file-explorer-screen-FoYNs9zK.js → file-explorer-screen-C_mYUJsr.js} +1 -1
  14. package/dist/client/assets/files-CG0jFYdO.js +2 -0
  15. package/dist/client/assets/follow-up-suggestions-C6RzpstA.js +2 -0
  16. package/dist/client/assets/{index-CNPHef4O.js → index-3-ASY4Cl.js} +1 -1
  17. package/dist/client/assets/index-BZxJSF4T.js +3 -0
  18. package/dist/client/assets/{keyboard-shortcuts-dialog-DP8ptQ7N.js → keyboard-shortcuts-dialog-jm4-DftO.js} +1 -1
  19. package/dist/client/assets/{main-xPlWrMhO.js → main-CbISK-pm.js} +2 -2
  20. package/dist/client/assets/{markdown-3Js_RbUp.js → markdown-UD7RzskQ.js} +1 -1
  21. package/dist/client/assets/memory-BYRJ1l_4.js +2 -0
  22. package/dist/client/assets/{memory-screen-nzRra2Qi.js → memory-screen-BRsm-Q8A.js} +1 -1
  23. package/dist/client/assets/{menu-BnSEqetd.js → menu-DRjoHFvG.js} +1 -1
  24. package/dist/client/assets/{opencami-logo-B_hLbomw.js → opencami-logo-D2mv84sX.js} +1 -1
  25. package/dist/client/assets/{proxy-BnlGpgC1.js → proxy-DtxqfBHY.js} +1 -1
  26. package/dist/client/assets/{react-BgjQyJHw.js → react-C_7UlEtd.js} +1 -1
  27. package/dist/client/assets/{search-dialog-Bib2QY9u.js → search-dialog-lsT8JUtT.js} +1 -1
  28. package/dist/client/assets/{search-sources-badge-COHcYFRB.js → search-sources-badge-BnqKxoia.js} +1 -1
  29. package/dist/client/assets/{session-export-dialog-ooPnfHh_.js → session-export-dialog-DHHpvwI8.js} +1 -1
  30. package/dist/client/assets/settings-dialog-vrd0eJGw.js +1 -0
  31. package/dist/client/assets/skills-Svd-7oxQ.js +2 -0
  32. package/dist/client/assets/{skills-panel-D2uMdCHp.js → skills-panel-Dkuo0yyD.js} +1 -1
  33. package/dist/client/assets/{switch-BUQ0qH6r.js → switch-BYFBYPmi.js} +1 -1
  34. package/dist/client/assets/{tabs-BEyU6TjN.js → tabs-1jrPkdUE.js} +1 -1
  35. package/dist/client/assets/{thinking-CA48yhOE.js → thinking-D9BBn5kK.js} +1 -1
  36. package/dist/client/assets/{tooltip-CcIdgcV0.js → tooltip-DzFgDY86.js} +1 -1
  37. package/dist/client/assets/{use-file-explorer-state-CN_IJGcd.js → use-file-explorer-state-Dwhw6Mpg.js} +1 -1
  38. package/dist/client/assets/{useBaseUiId-ClbEYEil.js → useBaseUiId-BdQC9KF3.js} +1 -1
  39. package/dist/client/assets/{useCompositeItem-B_OxfJee.js → useCompositeItem-BI09zNFD.js} +1 -1
  40. package/dist/client/assets/{useControlled-CyT-lqbs.js → useControlled-Bc2Emhlx.js} +1 -1
  41. package/dist/client/assets/{useMutation-eQUrsn-X.js → useMutation-CMp81lDO.js} +1 -1
  42. package/dist/client/assets/{useOnFirstRender-CR_o2MK_.js → useOnFirstRender-DpB3w0o4.js} +1 -1
  43. package/dist/client/assets/{useQuery-k6EMRoMD.js → useQuery-_BEWdUe1.js} +1 -1
  44. package/dist/server/assets/{_sessionKey-CaFqmyhU.js → _sessionKey-COz7RLKC.js} +121 -23
  45. package/dist/server/assets/_tanstack-start-manifest_v-D0f-0Utn.js +4 -0
  46. package/dist/server/assets/{connect-CTVBm0Vc.js → connect-sd_NDvQN.js} +1 -1
  47. package/dist/server/assets/follow-up-suggestions-DRQqO8_S.js +183 -0
  48. package/dist/server/assets/{index-B_F4DTUu.js → index-197lqzv2.js} +2 -0
  49. package/dist/server/assets/{index-C7lmufwX.js → index-sXe2QC5V.js} +1 -1
  50. package/dist/server/assets/{markdown-CFdYXCRQ.js → markdown-Dk1YdosA.js} +1 -1
  51. package/dist/server/assets/{memory-screen-vqXczcVo.js → memory-screen-DA_o-N1Z.js} +2 -2
  52. package/dist/server/assets/{memory-rBB015W-.js → memory-woPzJFfs.js} +1 -1
  53. package/dist/server/assets/{router-X2L0PDPI.js → router-CZhxPkYW.js} +305 -150
  54. package/dist/server/assets/{search-dialog-CXhofdoP.js → search-dialog-DlvWfq1e.js} +5 -5
  55. package/dist/server/assets/{settings-dialog-CPdftvjz.js → settings-dialog-BGtrS2RJ.js} +62 -1
  56. package/dist/server/assets/{thinking-YkRSlXtf.js → thinking-fzwT_aVT.js} +5 -5
  57. package/dist/server/server.js +2 -2
  58. package/package.json +1 -1
  59. package/dist/client/assets/_sessionKey-yNQ57svB.js +0 -23
  60. package/dist/client/assets/agents-WqWjsymD.js +0 -2
  61. package/dist/client/assets/bots-DNqiFT7w.js +0 -2
  62. package/dist/client/assets/files-BtR_gArr.js +0 -2
  63. package/dist/client/assets/follow-up-suggestions-DVXNLqga.js +0 -5
  64. package/dist/client/assets/index-CTT0Y1ya.js +0 -3
  65. package/dist/client/assets/memory-BRGPq5t6.js +0 -2
  66. package/dist/client/assets/settings-dialog-B8mz99u-.js +0 -1
  67. package/dist/client/assets/skills-DA9J_tsC.js +0 -2
  68. package/dist/server/assets/_tanstack-start-manifest_v-P3skSR3R.js +0 -4
  69. package/dist/server/assets/follow-up-suggestions-DHv2_XzB.js +0 -275
@@ -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-D8cKTpmN.js";
16
16
  import { O as OpenCamiLogo, a as OpenCamiText } from "./opencami-logo-C-43FL3R.js";
17
- import { M as Markdown } from "./markdown-CFdYXCRQ.js";
18
- import { u as useChatSettings$1 } from "./index-B_F4DTUu.js";
17
+ import { u as useChatSettings$1 } from "./index-197lqzv2.js";
18
+ import { M as Markdown } from "./markdown-Dk1YdosA.js";
19
19
  import { createPortal } from "react-dom";
20
20
  import { create } from "zustand";
21
21
  import { persist } from "zustand/middleware";
22
- import { a as Route } from "./router-X2L0PDPI.js";
22
+ import { a as Route } from "./router-CZhxPkYW.js";
23
23
  function deriveFriendlyIdFromKey(key) {
24
24
  if (!key) return "main";
25
25
  const trimmed = key.trim();
@@ -51,9 +51,21 @@ function stripInboundMeta(text) {
51
51
  s = s.replace(SUPERMEMORY_CONTEXT_REGEX, "").replace(SUPERMEMORY_CONTAINERS_REGEX, "").replace(INBOUND_SENDER_FENCED_REGEX, "").replace(INBOUND_SENDER_INLINE_REGEX, "").replace(INBOUND_META_TIMESTAMP_REGEX, "").replace(MULTI_BLANK_LINES_REGEX, "\n\n");
52
52
  return s.trim();
53
53
  }
54
+ function joinTextParts(parts) {
55
+ return parts.reduce((joined, part) => {
56
+ if (!part) return joined;
57
+ if (!joined) return part;
58
+ if (/\s$/.test(joined) || /^\s/.test(part)) return `${joined}${part}`;
59
+ if (/[([{"'`„“‚‘]$/.test(joined)) return `${joined}${part}`;
60
+ if (/^[,.;:!?%\])}"'`]/.test(part)) return `${joined}${part}`;
61
+ return `${joined} ${part}`;
62
+ }, "");
63
+ }
54
64
  function textFromMessage(msg) {
55
65
  const parts = Array.isArray(msg.content) ? msg.content : [];
56
- const raw = parts.map((part) => part.type === "text" ? String(part.text ?? "") : "").join("").trim();
66
+ const raw = joinTextParts(
67
+ parts.map((part) => part.type === "text" ? String(part.text ?? "") : "")
68
+ ).trim();
57
69
  return stripInboundMeta(raw);
58
70
  }
59
71
  function getToolCallsFromMessage(msg) {
@@ -233,12 +245,30 @@ function updateHistoryMessages(queryClient, friendlyId, sessionKey, updater) {
233
245
  };
234
246
  });
235
247
  }
248
+ function normalizeId(value) {
249
+ return typeof value === "string" ? value.trim() : "";
250
+ }
251
+ function areSameHistoryMessage(a, b) {
252
+ const aId = normalizeId(a.id);
253
+ const bId = normalizeId(b.id);
254
+ if (aId && bId) return aId === bId;
255
+ const aClientId = normalizeId(a.clientId);
256
+ const bClientId = normalizeId(b.clientId);
257
+ if (aClientId && bClientId) return aClientId === bClientId;
258
+ const aOptimisticId = normalizeId(a.__optimisticId);
259
+ const bOptimisticId = normalizeId(b.__optimisticId);
260
+ if (aOptimisticId && bOptimisticId) return aOptimisticId === bOptimisticId;
261
+ return false;
262
+ }
236
263
  function appendHistoryMessage(queryClient, friendlyId, sessionKey, message) {
237
264
  updateHistoryMessages(
238
265
  queryClient,
239
266
  friendlyId,
240
267
  sessionKey,
241
268
  function append(messages) {
269
+ if (messages.some((existing) => areSameHistoryMessage(existing, message))) {
270
+ return messages;
271
+ }
242
272
  return [...messages, message];
243
273
  }
244
274
  );
@@ -355,7 +385,9 @@ async function updateSessionLabel(queryClient, sessionKey, friendlyId, label) {
355
385
  if (!res.ok) {
356
386
  const errorText = await readError(res);
357
387
  console.error("[updateSessionLabel] Failed to persist:", errorText);
388
+ return;
358
389
  }
390
+ await queryClient.invalidateQueries({ queryKey: chatQueryKeys.sessions });
359
391
  } catch (err) {
360
392
  console.error("[updateSessionLabel] Network error:", err);
361
393
  }
@@ -1686,7 +1718,7 @@ function areSidebarSessionsEqual(prev, next) {
1686
1718
  return true;
1687
1719
  }
1688
1720
  const SettingsDialog = lazy(
1689
- () => import("./settings-dialog-CPdftvjz.js").then((m) => ({ default: m.SettingsDialog }))
1721
+ () => import("./settings-dialog-BGtrS2RJ.js").then((m) => ({ default: m.SettingsDialog }))
1690
1722
  );
1691
1723
  const SessionExportDialog = lazy(
1692
1724
  () => import("./session-export-dialog-CgtlOnwf.js").then((m) => ({
@@ -1734,6 +1766,13 @@ function ChatSidebarComponent({
1734
1766
  const [exportSessionKey, setExportSessionKey] = useState(null);
1735
1767
  const [exportFriendlyId, setExportFriendlyId] = useState(null);
1736
1768
  const [exportSessionTitle, setExportSessionTitle] = useState("");
1769
+ const [showCrons] = useState(() => {
1770
+ try {
1771
+ return typeof window !== "undefined" ? localStorage.getItem("opencami-cron-manager") !== "false" : true;
1772
+ } catch {
1773
+ return true;
1774
+ }
1775
+ });
1737
1776
  const queryClient = useQueryClient();
1738
1777
  function handleOpenRename(session) {
1739
1778
  setRenameSessionKey(session.key);
@@ -2064,13 +2103,7 @@ function ChatSidebarComponent({
2064
2103
  ) }),
2065
2104
  isCollapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Agents" })
2066
2105
  ] }) }),
2067
- (() => {
2068
- try {
2069
- return localStorage.getItem("opencami-cron-manager") === "true";
2070
- } catch {
2071
- return false;
2072
- }
2073
- })() && /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { children: [
2106
+ showCrons && /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { children: [
2074
2107
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
2075
2108
  Link,
2076
2109
  {
@@ -2108,7 +2141,7 @@ function ChatSidebarComponent({
2108
2141
  ] }) }),
2109
2142
  (() => {
2110
2143
  try {
2111
- return localStorage.getItem("feature_dashboard") === "true";
2144
+ return localStorage.getItem("opencami-feature_dashboard") === "true";
2112
2145
  } catch {
2113
2146
  return false;
2114
2147
  }
@@ -2833,7 +2866,7 @@ function Tool({ toolPart, defaultOpen = false }) {
2833
2866
  ] }) });
2834
2867
  }
2835
2868
  const Thinking = lazy(
2836
- () => import("./thinking-YkRSlXtf.js").then((m) => ({
2869
+ () => import("./thinking-fzwT_aVT.js").then((m) => ({
2837
2870
  default: m.Thinking
2838
2871
  }))
2839
2872
  );
@@ -3502,7 +3535,7 @@ function TypingIndicator({ className }) {
3502
3535
  ] });
3503
3536
  }
3504
3537
  const FollowUpSuggestions = lazy(
3505
- () => import("./follow-up-suggestions-DHv2_XzB.js").then((m) => ({
3538
+ () => import("./follow-up-suggestions-DRQqO8_S.js").then((m) => ({
3506
3539
  default: m.FollowUpSuggestions
3507
3540
  }))
3508
3541
  );
@@ -3531,6 +3564,7 @@ function ChatMessageListComponent({
3531
3564
  const prevPinRef = useRef(pinToTop);
3532
3565
  const prevUserIndexRef = useRef(void 0);
3533
3566
  const [highlightedMessageId, setHighlightedMessageId] = useState(null);
3567
+ const { settings } = useChatSettings$1();
3534
3568
  const displayMessages = useMemo(() => {
3535
3569
  return messages.filter((msg) => msg.role !== "toolResult");
3536
3570
  }, [messages]);
@@ -3659,7 +3693,19 @@ function ChatMessageListComponent({
3659
3693
  const hasGroup = pinToTop && groupStartIndex >= 0;
3660
3694
  const lastAssistantMessage = typeof lastTextAssistantIndex === "number" ? displayMessages[lastTextAssistantIndex] : void 0;
3661
3695
  const lastAssistantText = lastAssistantMessage ? textFromMessage(lastAssistantMessage) : "";
3662
- const showFollowUps = !waitingForResponse && !isStreaming && lastAssistantText.length > 0 && onFollowUpClick !== void 0 && (typeof lastUserIndex !== "number" || typeof lastTextAssistantIndex !== "number" || lastTextAssistantIndex > lastUserIndex);
3696
+ const followUpConversationContext = useMemo(() => {
3697
+ if (typeof lastTextAssistantIndex !== "number") {
3698
+ return "";
3699
+ }
3700
+ const recentMessages = displayMessages.slice(Math.max(0, lastTextAssistantIndex - 5), lastTextAssistantIndex + 1).map((message) => {
3701
+ const text = textFromMessage(message).trim();
3702
+ if (!text) return null;
3703
+ const role = message.role === "user" ? "User" : "Assistant";
3704
+ return `${role}: ${text}`;
3705
+ }).filter((line) => Boolean(line));
3706
+ return recentMessages.join("\n\n").slice(0, 4e3);
3707
+ }, [displayMessages, lastTextAssistantIndex]);
3708
+ const showFollowUps = settings.showFollowUps && !waitingForResponse && !isStreaming && lastAssistantText.length > 0 && onFollowUpClick !== void 0 && (typeof lastUserIndex !== "number" || typeof lastTextAssistantIndex !== "number" || lastTextAssistantIndex > lastUserIndex);
3663
3709
  useLayoutEffect(() => {
3664
3710
  if (loading) return;
3665
3711
  if (pinToTop) {
@@ -3767,6 +3813,7 @@ function ChatMessageListComponent({
3767
3813
  FollowUpSuggestions,
3768
3814
  {
3769
3815
  responseText: lastAssistantText,
3816
+ conversationContext: followUpConversationContext,
3770
3817
  onSuggestionClick: onFollowUpClick,
3771
3818
  disabled: waitingForResponse
3772
3819
  }
@@ -3802,6 +3849,7 @@ function ChatMessageListComponent({
3802
3849
  FollowUpSuggestions,
3803
3850
  {
3804
3851
  responseText: lastAssistantText,
3852
+ conversationContext: followUpConversationContext,
3805
3853
  onSuggestionClick: onFollowUpClick,
3806
3854
  disabled: waitingForResponse
3807
3855
  }
@@ -5491,8 +5539,9 @@ function useChatHistory({
5491
5539
  sessionKey: sessionKeyForHistory,
5492
5540
  friendlyId: activeFriendlyId
5493
5541
  });
5542
+ const dedupedServerMessages = dedupeHistoryMessages(serverData.messages);
5494
5543
  const serverMessages = restoreCachedImageParts(
5495
- serverData.messages,
5544
+ dedupedServerMessages,
5496
5545
  cachedMessages
5497
5546
  );
5498
5547
  if (!optimisticMessages.length) {
@@ -5564,6 +5613,39 @@ function findMatchIndex(serverMessages, optimisticMessage) {
5564
5613
  return Math.abs(optimisticTime - serverTime) <= 1e4;
5565
5614
  });
5566
5615
  }
5616
+ function normalizeMessageIdValue(value) {
5617
+ return typeof value === "string" ? value.trim() : "";
5618
+ }
5619
+ function getMessageIdentity(message) {
5620
+ const id = normalizeMessageIdValue(message.id);
5621
+ if (id) return `id:${id}`;
5622
+ const clientId = normalizeMessageIdValue(message.clientId);
5623
+ if (clientId) return `client:${clientId}`;
5624
+ const optimisticId = normalizeMessageIdValue(message.__optimisticId);
5625
+ if (optimisticId) return `optimistic:${optimisticId}`;
5626
+ const toolCallId = normalizeMessageIdValue(message.toolCallId);
5627
+ if (message.role === "toolResult" && toolCallId) {
5628
+ return `tool-result:${toolCallId}`;
5629
+ }
5630
+ const timestampBucket = Math.floor(getMessageTimestamp(message) / 1e3);
5631
+ const text = textFromMessage(message);
5632
+ if (text) {
5633
+ return `fallback:${message.role ?? "unknown"}:${timestampBucket}:${text}`;
5634
+ }
5635
+ return `fallback:${message.role ?? "unknown"}:${timestampBucket}:${JSON.stringify(message.content ?? [])}`;
5636
+ }
5637
+ function dedupeHistoryMessages(messages) {
5638
+ if (!Array.isArray(messages) || messages.length <= 1) return messages;
5639
+ const deduped = [];
5640
+ const seen = /* @__PURE__ */ new Set();
5641
+ for (const message of messages) {
5642
+ const identity = getMessageIdentity(message);
5643
+ if (seen.has(identity)) continue;
5644
+ seen.add(identity);
5645
+ deduped.push(message);
5646
+ }
5647
+ return deduped;
5648
+ }
5567
5649
  function getImageParts(message) {
5568
5650
  if (!Array.isArray(message.content)) return [];
5569
5651
  return message.content.filter((part) => {
@@ -5724,6 +5806,7 @@ function useChatSessions({
5724
5806
  };
5725
5807
  }
5726
5808
  function useSmartTitle() {
5809
+ const { settings } = useChatSettings$1();
5727
5810
  const [isGenerating, setIsGenerating] = useState(false);
5728
5811
  const [lastTitle, setLastTitle] = useState(null);
5729
5812
  const [lastSource, setLastSource] = useState(null);
@@ -5743,7 +5826,8 @@ function useSmartTitle() {
5743
5826
  },
5744
5827
  body: JSON.stringify({
5745
5828
  action: "title",
5746
- message
5829
+ message,
5830
+ model: settings.llmFeaturesModel ? `openclaw/${settings.llmFeaturesModel}` : void 0
5747
5831
  }),
5748
5832
  signal: controller.signal
5749
5833
  });
@@ -5782,7 +5866,7 @@ function useSmartTitle() {
5782
5866
  abortControllerRef.current = null;
5783
5867
  }
5784
5868
  }
5785
- }, []);
5869
+ }, [settings.llmFeaturesModel]);
5786
5870
  return {
5787
5871
  generateTitle,
5788
5872
  isGenerating,
@@ -5834,8 +5918,8 @@ function handleAgentEvent(payload, fallbackSessionKey, options) {
5834
5918
  }
5835
5919
  }
5836
5920
  if (stream === "assistant") {
5837
- const text = normalizeString(streamData?.delta) || normalizeString(streamData?.text);
5838
- if (!text) return;
5921
+ const text = rawString(streamData?.delta) || rawString(streamData?.text);
5922
+ if (text.length === 0) return;
5839
5923
  options.setState((prev) => {
5840
5924
  const blocks = [...prev.contentBlocks];
5841
5925
  const lastBlock = blocks[blocks.length - 1];
@@ -5909,6 +5993,9 @@ function asRecord(value) {
5909
5993
  function normalizeString(value) {
5910
5994
  return typeof value === "string" ? value.trim() : "";
5911
5995
  }
5996
+ function rawString(value) {
5997
+ return typeof value === "string" ? value : "";
5998
+ }
5912
5999
  const INITIAL_STATE = {
5913
6000
  active: false,
5914
6001
  text: "",
@@ -6236,7 +6323,7 @@ const KeyboardShortcutsDialog = lazy(
6236
6323
  }))
6237
6324
  );
6238
6325
  const SearchDialog = lazy(
6239
- () => import("./search-dialog-CXhofdoP.js").then((m) => ({
6326
+ () => import("./search-dialog-DlvWfq1e.js").then((m) => ({
6240
6327
  default: m.SearchDialog
6241
6328
  }))
6242
6329
  );
@@ -6480,9 +6567,10 @@ function ChatScreen({
6480
6567
  }, [streaming.contentBlocks]);
6481
6568
  const messagesWithStreaming = useMemo(() => {
6482
6569
  if (!streamingMessage) return displayMessages;
6570
+ const lastVisibleIndex = [...displayMessages].map((message, index) => ({ message, index })).filter(({ message }) => message.role !== "toolResult").map(({ index }) => index).pop();
6483
6571
  const lastAssistantIndex = [...displayMessages].map((message, index) => ({ message, index })).filter(({ message }) => message.role === "assistant").map(({ index }) => index).pop();
6484
6572
  const lastUserIndex = [...displayMessages].map((message, index) => ({ message, index })).filter(({ message }) => message.role === "user").map(({ index }) => index).pop();
6485
- const assistantIsLatestTurn = typeof lastAssistantIndex === "number" && (typeof lastUserIndex !== "number" || lastAssistantIndex > lastUserIndex);
6573
+ const assistantIsLatestTurn = typeof lastAssistantIndex === "number" && lastAssistantIndex === lastVisibleIndex && (typeof lastUserIndex !== "number" || lastAssistantIndex > lastUserIndex);
6486
6574
  if (!streaming.active && assistantIsLatestTurn) {
6487
6575
  return displayMessages;
6488
6576
  }
@@ -6604,6 +6692,16 @@ function ChatScreen({
6604
6692
  }, 1500);
6605
6693
  }
6606
6694
  }, [historyMessages, streamFinish]);
6695
+ useEffect(() => {
6696
+ if (!isStreaming || streaming.active) return;
6697
+ const lastAssistantIndex = [...historyMessages].map((message, index) => ({ message, index })).filter(({ message }) => message.role === "assistant").map(({ index }) => index).pop();
6698
+ const lastUserIndex = [...historyMessages].map((message, index) => ({ message, index })).filter(({ message }) => message.role === "user").map(({ index }) => index).pop();
6699
+ const hasCompletedAssistantTurn = typeof lastAssistantIndex === "number" && (typeof lastUserIndex !== "number" || lastAssistantIndex > lastUserIndex);
6700
+ if (hasCompletedAssistantTurn) {
6701
+ streamFinish();
6702
+ pollingPhaseRef.current = "fast";
6703
+ }
6704
+ }, [historyMessages, isStreaming, streamFinish, streaming.active]);
6607
6705
  useEffect(() => {
6608
6706
  if (isNewChat || isRedirecting) return;
6609
6707
  const sessionKey = forcedSessionKey || resolvedSessionKey || activeSessionKey;
@@ -0,0 +1,4 @@
1
+ const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "/root/opencami/src/routes/__root.tsx", "children": ["/", "/agents", "/bots", "/connect", "/dashboard", "/files", "/memory", "/new", "/skills", "/api/agents", "/api/cron", "/api/follow-ups", "/api/history", "/api/llm-features", "/api/llm-models", "/api/models", "/api/paths", "/api/personas", "/api/ping", "/api/send", "/api/sessions", "/api/skills", "/api/stream", "/api/stt", "/api/tts", "/chat/$sessionKey", "/api/dashboard/crons", "/api/dashboard/gateway", "/api/dashboard/system", "/api/files/delete", "/api/files/download", "/api/files/info", "/api/files/list", "/api/files/mkdir", "/api/files/read", "/api/files/rename", "/api/files/save", "/api/files/upload"], "preloads": ["/assets/main-CbISK-pm.js"], "assets": [] }, "/": { "filePath": "/root/opencami/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-3-ASY4Cl.js"] }, "/agents": { "filePath": "/root/opencami/src/routes/agents.tsx", "assets": [], "preloads": ["/assets/agents-D7JS19Lo.js"] }, "/bots": { "filePath": "/root/opencami/src/routes/bots.tsx", "assets": [], "preloads": ["/assets/bots-DAInzfLx.js"] }, "/connect": { "filePath": "/root/opencami/src/routes/connect.tsx", "assets": [], "preloads": ["/assets/connect-DZSrhhNT.js", "/assets/index-BZxJSF4T.js", "/assets/button-Bv-7bwZZ.js", "/assets/react-C_7UlEtd.js"] }, "/dashboard": { "filePath": "/root/opencami/src/routes/dashboard.tsx", "assets": [], "preloads": ["/assets/dashboard-y8KVUVF-.js", "/assets/useQuery-_BEWdUe1.js"] }, "/files": { "filePath": "/root/opencami/src/routes/files.tsx", "assets": [], "preloads": ["/assets/files-CG0jFYdO.js"] }, "/memory": { "filePath": "/root/opencami/src/routes/memory.tsx", "assets": [], "preloads": ["/assets/memory-BYRJ1l_4.js"] }, "/new": { "filePath": "/root/opencami/src/routes/new.tsx", "assets": [], "preloads": ["/assets/new-DLlBm66g.js"] }, "/skills": { "filePath": "/root/opencami/src/routes/skills.tsx", "assets": [], "preloads": ["/assets/skills-Svd-7oxQ.js"] }, "/api/agents": { "filePath": "/root/opencami/src/routes/api/agents.ts" }, "/api/cron": { "filePath": "/root/opencami/src/routes/api/cron.ts" }, "/api/follow-ups": { "filePath": "/root/opencami/src/routes/api/follow-ups.ts" }, "/api/history": { "filePath": "/root/opencami/src/routes/api/history.ts" }, "/api/llm-features": { "filePath": "/root/opencami/src/routes/api/llm-features.ts" }, "/api/llm-models": { "filePath": "/root/opencami/src/routes/api/llm-models.ts" }, "/api/models": { "filePath": "/root/opencami/src/routes/api/models.ts" }, "/api/paths": { "filePath": "/root/opencami/src/routes/api/paths.ts" }, "/api/personas": { "filePath": "/root/opencami/src/routes/api/personas.ts" }, "/api/ping": { "filePath": "/root/opencami/src/routes/api/ping.ts" }, "/api/send": { "filePath": "/root/opencami/src/routes/api/send.ts" }, "/api/sessions": { "filePath": "/root/opencami/src/routes/api/sessions.ts" }, "/api/skills": { "filePath": "/root/opencami/src/routes/api/skills.ts" }, "/api/stream": { "filePath": "/root/opencami/src/routes/api/stream.ts" }, "/api/stt": { "filePath": "/root/opencami/src/routes/api/stt.ts" }, "/api/tts": { "filePath": "/root/opencami/src/routes/api/tts.ts" }, "/chat/$sessionKey": { "filePath": "/root/opencami/src/routes/chat/$sessionKey.tsx", "assets": [], "preloads": ["/assets/_sessionKey-CTtu2ipR.js", "/assets/useQuery-_BEWdUe1.js", "/assets/tooltip-DzFgDY86.js", "/assets/button-Bv-7bwZZ.js", "/assets/useMutation-CMp81lDO.js", "/assets/use-file-explorer-state-Dwhw6Mpg.js", "/assets/useBaseUiId-BdQC9KF3.js", "/assets/useControlled-Bc2Emhlx.js", "/assets/useOnFirstRender-DpB3w0o4.js", "/assets/event-Cnr9OETn.js", "/assets/CSPContext-CxrBvJ99.js", "/assets/DirectionContext-BOL0P_1j.js", "/assets/menu-DRjoHFvG.js", "/assets/opencami-logo-D2mv84sX.js", "/assets/proxy-DtxqfBHY.js", "/assets/index-BZxJSF4T.js", "/assets/markdown-UD7RzskQ.js", "/assets/react-C_7UlEtd.js"] }, "/api/dashboard/crons": { "filePath": "/root/opencami/src/routes/api/dashboard/crons.ts" }, "/api/dashboard/gateway": { "filePath": "/root/opencami/src/routes/api/dashboard/gateway.ts" }, "/api/dashboard/system": { "filePath": "/root/opencami/src/routes/api/dashboard/system.ts" }, "/api/files/delete": { "filePath": "/root/opencami/src/routes/api/files/delete.ts" }, "/api/files/download": { "filePath": "/root/opencami/src/routes/api/files/download.ts" }, "/api/files/info": { "filePath": "/root/opencami/src/routes/api/files/info.ts" }, "/api/files/list": { "filePath": "/root/opencami/src/routes/api/files/list.ts" }, "/api/files/mkdir": { "filePath": "/root/opencami/src/routes/api/files/mkdir.ts" }, "/api/files/read": { "filePath": "/root/opencami/src/routes/api/files/read.ts" }, "/api/files/rename": { "filePath": "/root/opencami/src/routes/api/files/rename.ts" }, "/api/files/save": { "filePath": "/root/opencami/src/routes/api/files/save.ts" }, "/api/files/upload": { "filePath": "/root/opencami/src/routes/api/files/upload.ts" } }, "clientEntry": "/assets/main-CbISK-pm.js" });
2
+ export {
3
+ tsrStartManifest
4
+ };
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { C as CodeBlock } from "./index-B_F4DTUu.js";
2
+ import { C as CodeBlock } from "./index-197lqzv2.js";
3
3
  import "react";
4
4
  import "@hugeicons/react";
5
5
  import "@hugeicons/core-free-icons";
@@ -0,0 +1,183 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect, memo } from "react";
3
+ import { HugeiconsIcon } from "@hugeicons/react";
4
+ import { Loading03Icon, ArrowRight01Icon } from "@hugeicons/core-free-icons";
5
+ import { u as useChatSettings } from "./index-197lqzv2.js";
6
+ import { c as cn } from "./button-kI8fEIZQ.js";
7
+ import "zustand";
8
+ import "zustand/middleware";
9
+ import "@base-ui/react/merge-props";
10
+ import "@base-ui/react/use-render";
11
+ import "class-variance-authority";
12
+ import "clsx";
13
+ import "tailwind-merge";
14
+ async function fetchFollowUpsViaOpenclaw(conversationContext, model, signal) {
15
+ const res = await fetch("/api/llm-features", {
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Type": "application/json"
19
+ },
20
+ body: JSON.stringify({
21
+ action: "followups",
22
+ conversationContext,
23
+ model
24
+ }),
25
+ signal
26
+ });
27
+ if (!res.ok) {
28
+ throw new Error(`API error: ${res.status}`);
29
+ }
30
+ const data = await res.json();
31
+ if (data.ok && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
32
+ return data.suggestions;
33
+ }
34
+ return [];
35
+ }
36
+ function useFollowUpSuggestions(responseText, conversationContext, options) {
37
+ const { settings } = useChatSettings();
38
+ const { minResponseLength = 50, heuristicsOnly = false, enabled = true } = options ?? {};
39
+ const [suggestions, setSuggestions] = useState([]);
40
+ const [isLoading, setIsLoading] = useState(false);
41
+ const [error, setError] = useState(null);
42
+ const [source, setSource] = useState(null);
43
+ const lastResponseRef = useRef("");
44
+ const abortControllerRef = useRef(null);
45
+ useEffect(() => {
46
+ if (!enabled) {
47
+ if (abortControllerRef.current) {
48
+ abortControllerRef.current.abort();
49
+ }
50
+ setSuggestions([]);
51
+ setSource(null);
52
+ setIsLoading(false);
53
+ setError(null);
54
+ return;
55
+ }
56
+ if (!responseText || responseText.trim().length < minResponseLength) {
57
+ setSuggestions([]);
58
+ setSource(null);
59
+ setIsLoading(false);
60
+ setError(null);
61
+ return;
62
+ }
63
+ const responseKey = `${settings.llmFeaturesModel}:${responseText.slice(0, 200)}${responseText.length}`;
64
+ if (responseKey === lastResponseRef.current) {
65
+ return;
66
+ }
67
+ lastResponseRef.current = responseKey;
68
+ if (abortControllerRef.current) {
69
+ abortControllerRef.current.abort();
70
+ }
71
+ if (heuristicsOnly) {
72
+ setSuggestions([]);
73
+ setSource(null);
74
+ setIsLoading(false);
75
+ setError(null);
76
+ return;
77
+ }
78
+ const controller = new AbortController();
79
+ abortControllerRef.current = controller;
80
+ setIsLoading(true);
81
+ setError(null);
82
+ setSuggestions([]);
83
+ setSource(null);
84
+ const requestContext = conversationContext?.trim().length ? conversationContext.trim() : `Assistant's response:
85
+ ${responseText.slice(0, 2e3)}`;
86
+ fetchFollowUpsViaOpenclaw(
87
+ requestContext,
88
+ settings.llmFeaturesModel ? `openclaw/${settings.llmFeaturesModel}` : void 0,
89
+ controller.signal
90
+ ).then((openclawSuggestions) => {
91
+ if (controller.signal.aborted) return;
92
+ if (openclawSuggestions.length > 0) {
93
+ setSuggestions(openclawSuggestions);
94
+ setSource("openclaw");
95
+ }
96
+ setIsLoading(false);
97
+ }).catch((err) => {
98
+ if (controller.signal.aborted) return;
99
+ setSuggestions([]);
100
+ setSource(null);
101
+ setError(err instanceof Error ? err.message : String(err));
102
+ setIsLoading(false);
103
+ });
104
+ }, [
105
+ responseText,
106
+ conversationContext,
107
+ enabled,
108
+ minResponseLength,
109
+ heuristicsOnly,
110
+ settings.llmFeaturesModel
111
+ ]);
112
+ return { suggestions, isLoading, error, source };
113
+ }
114
+ function FollowUpSuggestionsComponent({
115
+ responseText,
116
+ conversationContext,
117
+ onSuggestionClick,
118
+ disabled = false,
119
+ className
120
+ }) {
121
+ const { suggestions, isLoading, source } = useFollowUpSuggestions(
122
+ responseText,
123
+ conversationContext,
124
+ {
125
+ minResponseLength: 50
126
+ }
127
+ );
128
+ if (suggestions.length === 0 && !isLoading) {
129
+ return null;
130
+ }
131
+ const showHeader = isLoading || suggestions.length > 0;
132
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-2 mt-3", className), children: [
133
+ showHeader ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-primary-500", children: [
134
+ /* @__PURE__ */ jsx("span", { className: "text-primary-400", children: "✨" }),
135
+ /* @__PURE__ */ jsx("span", { children: isLoading ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
136
+ "Thinking of follow-ups",
137
+ /* @__PURE__ */ jsx(
138
+ HugeiconsIcon,
139
+ {
140
+ icon: Loading03Icon,
141
+ size: 12,
142
+ strokeWidth: 2,
143
+ className: "animate-spin text-primary-400"
144
+ }
145
+ )
146
+ ] }) : source === "openclaw" ? "AI suggestions" : "Follow-up suggestions" })
147
+ ] }) : null,
148
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsxs(
149
+ "button",
150
+ {
151
+ type: "button",
152
+ disabled,
153
+ onClick: () => onSuggestionClick(suggestion),
154
+ className: cn(
155
+ "group inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full",
156
+ "text-sm text-primary-700 bg-primary-50 border border-primary-200",
157
+ "hover:bg-primary-100 hover:border-primary-300 hover:text-primary-900",
158
+ "focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:ring-offset-1",
159
+ "transition-all duration-150 cursor-pointer",
160
+ "disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary-50 disabled:hover:border-primary-200",
161
+ isLoading && "opacity-75"
162
+ ),
163
+ children: [
164
+ /* @__PURE__ */ jsx("span", { children: suggestion }),
165
+ /* @__PURE__ */ jsx(
166
+ HugeiconsIcon,
167
+ {
168
+ icon: ArrowRight01Icon,
169
+ size: 14,
170
+ strokeWidth: 2,
171
+ className: "text-primary-400 group-hover:text-primary-600 group-hover:translate-x-0.5 transition-all duration-150"
172
+ }
173
+ )
174
+ ]
175
+ },
176
+ `${index}-${suggestion.slice(0, 20)}`
177
+ )) })
178
+ ] });
179
+ }
180
+ const MemoizedFollowUpSuggestions = memo(FollowUpSuggestionsComponent);
181
+ export {
182
+ MemoizedFollowUpSuggestions as FollowUpSuggestions
183
+ };
@@ -12,7 +12,9 @@ const useChatSettingsStore = create()(
12
12
  showToolMessages: true,
13
13
  showReasoningBlocks: true,
14
14
  showSearchSources: true,
15
+ showFollowUps: true,
15
16
  inlineFilePreview: false,
17
+ llmFeaturesModel: "gpt54mini",
16
18
  theme: "system",
17
19
  fontFamily: "system",
18
20
  density: "comfortable",
@@ -1,6 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { useEffect } from "react";
3
- import { R as Route } from "./router-X2L0PDPI.js";
3
+ import { R as Route } from "./router-CZhxPkYW.js";
4
4
  import "@tanstack/react-router";
5
5
  import "@tanstack/react-query";
6
6
  import "node:crypto";
@@ -4,7 +4,7 @@ import { memo, useId, useMemo, useState, useCallback, useEffect } from "react";
4
4
  import ReactMarkdown from "react-markdown";
5
5
  import remarkBreaks from "remark-breaks";
6
6
  import remarkGfm from "remark-gfm";
7
- import { r as resolveLanguage, C as CodeBlock } from "./index-B_F4DTUu.js";
7
+ import { r as resolveLanguage, C as CodeBlock } from "./index-197lqzv2.js";
8
8
  import { u as useFileExplorerState, D as DialogRoot, a as DialogContent, b as DialogTitle, d as DialogClose } from "./use-file-explorer-state-E6cUvMva.js";
9
9
  import { c as cn, B as Button } from "./button-kI8fEIZQ.js";
10
10
  import { useNavigate, Link } from "@tanstack/react-router";
@@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from "react";
3
3
  import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
4
4
  import { Link } from "@tanstack/react-router";
5
5
  import { c as cn, B as Button, b as buttonVariants } from "./button-kI8fEIZQ.js";
6
- import { M as Markdown } from "./markdown-CFdYXCRQ.js";
6
+ import { M as Markdown } from "./markdown-Dk1YdosA.js";
7
7
  import "@base-ui/react/merge-props";
8
8
  import "@base-ui/react/use-render";
9
9
  import "class-variance-authority";
@@ -13,7 +13,7 @@ import "marked";
13
13
  import "react-markdown";
14
14
  import "remark-breaks";
15
15
  import "remark-gfm";
16
- import "./index-B_F4DTUu.js";
16
+ import "./index-197lqzv2.js";
17
17
  import "@hugeicons/react";
18
18
  import "@hugeicons/core-free-icons";
19
19
  import "zustand";
@@ -1,6 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { Suspense, lazy } from "react";
3
- const MemoryScreen = lazy(() => import("./memory-screen-vqXczcVo.js").then((m) => ({
3
+ const MemoryScreen = lazy(() => import("./memory-screen-DA_o-N1Z.js").then((m) => ({
4
4
  default: m.MemoryScreen
5
5
  })));
6
6
  function MemoryRoute() {