@sybilion/uilib 1.3.67 → 1.3.70

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 (24) hide show
  1. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +1 -1
  2. package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +4 -1
  3. package/dist/esm/components/ui/Chat/ChatSheet/chatPanelOpenSync.js +5 -1
  4. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +41 -18
  5. package/dist/esm/components/ui/Tooltip/Tooltip.styl.js +1 -1
  6. package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.js +1 -1
  7. package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.styl.js +2 -2
  8. package/dist/esm/contexts/chat-context.js +18 -0
  9. package/dist/esm/types/src/components/ui/Chat/ChatSheet/chatPanelOpenSync.d.ts +4 -0
  10. package/dist/esm/types/src/contexts/chat-context.d.ts +3 -1
  11. package/dist/esm/types/src/docs/pages/ChatComposerPrefillPage.d.ts +1 -0
  12. package/package.json +1 -1
  13. package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +1 -1
  14. package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +4 -1
  15. package/src/components/ui/Chat/ChatSheet/chatPanelOpenSync.test.ts +26 -0
  16. package/src/components/ui/Chat/ChatSheet/chatPanelOpenSync.ts +16 -0
  17. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +46 -15
  18. package/src/components/ui/Tooltip/Tooltip.styl +4 -4
  19. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl +9 -0
  20. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl.d.ts +1 -0
  21. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.tsx +2 -4
  22. package/src/contexts/chat-context.tsx +29 -0
  23. package/src/docs/pages/ChatComposerPrefillPage.tsx +202 -0
  24. package/src/docs/registry.ts +6 -0
@@ -1,6 +1,6 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = ".ChatMessage_root__6rnsF{background:var(--bg-secondary);display:flex;flex-direction:column;gap:var(--p-1);padding:var(--p-4);padding-bottom:var(--p-1)}.ChatMessage_text__Y1XNR{color:var(--text-secondary);font-size:var(--text-sm);max-width:100%;min-width:0;overflow-wrap:anywhere;-webkit-user-select:text;-moz-user-select:text;user-select:text;width:-moz-fit-content;width:fit-content;word-break:break-word}.ChatMessage_role-assistant__wketE+.ChatMessage_role-assistant__wketE,.ChatMessage_role-system__g13OP+.ChatMessage_role-system__g13OP,.ChatMessage_role-user__u4JPV+.ChatMessage_role-user__u4JPV{padding-top:var(--p-1)}.ChatMessage_role-user__u4JPV{align-items:flex-end;max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_userColumn__cQM6-{align-items:flex-end;display:flex;flex-direction:column;gap:var(--p-2);max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-slate-100);border-radius:var(--p-4);border-bottom-right-radius:0;box-sizing:border-box;overflow:hidden;padding:var(--p-3) var(--p-4);white-space:pre-wrap}.dark .ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-gray-800)}.ChatMessage_role-system__g13OP{align-items:center}.ChatMessage_role-system__g13OP .ChatMessage_text__Y1XNR{color:var(--muted-foreground);width:100%}.ChatMessage_role-assistant__wketE .ChatMessage_text__Y1XNR{width:100%}.ChatMessage_role-assistant__wketE h3{line-height:2.4}.ChatMessage_role-assistant__wketE h4{font-size:1.1em;font-weight:600;line-height:2.2}.ChatMessage_role-assistant__wketE .ChatMessage_bullet__6vAhq{display:inline-block;margin-left:4px;margin-right:6px}.ChatMessage_role-assistant__wketE .ChatMessage_bullet__6vAhq:before{color:var(--text-secondary);content:\"•\";display:inline-block}.ChatMessage_role-assistant__wketE .ChatMessage_scrollHorizontal__Rms9n{max-width:100%}.ChatMessage_role-assistant__wketE table{border:1px solid var(--border);border-collapse:collapse;border-radius:var(--p-2);border-spacing:0;margin:var(--p-4) 0;overflow:hidden}.ChatMessage_role-assistant__wketE table td,.ChatMessage_role-assistant__wketE table th{border:1px solid var(--border);min-width:100px;padding:var(--p-1)}.ChatMessage_role-assistant__wketE table th{text-align:left}.ChatMessage_role-assistant__wketE ol,.ChatMessage_role-assistant__wketE ul{padding-left:var(--p-4)}.ChatMessage_role-assistant__wketE ul{list-style-type:disc}.ChatMessage_role-assistant__wketE ol{list-style-type:decimal}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T{align-items:center;border:1px dashed var(--sb-slate-300);border-radius:8px;color:var(--foreground);display:inline-flex;font-size:var(--text-xs);gap:6px;margin:1px;max-width:100%;padding:2px 6px 2px 4px;text-decoration:none;vertical-align:middle}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T:hover{background-color:var(--sb-slate-50);border-color:var(--sb-slate-400);border-style:solid}.dark .ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T{border-color:var(--sb-gray-600)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T:hover{background-color:var(--sb-gray-900)}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLinkLabel__PMU7e{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ChatMessage_role-assistant__wketE .ChatMessage_quickReplyWrap__1UFyD{display:inline-block;margin:var(--p-1) var(--p-1) var(--p-1) 0;vertical-align:middle}.ChatMessage_role-assistant__wketE .ChatMessage_downloadButtons__RygM-{display:flex;gap:var(--p-2);margin-top:var(--p-4)}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa{align-items:center;background-color:var(--background);border-radius:var(--p-3);box-shadow:0 0 0 1px var(--border);cursor:pointer;display:flex;gap:var(--p-4);margin-top:var(--p-3);padding:var(--p-3);padding-right:var(--p-4);transition:all .15s;width:-moz-fit-content;width:fit-content}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa:hover{background-color:var(--sb-gray-50);border-color:var(--border)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa{background-color:var(--sb-gray-900);border-color:var(--border)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa:hover{background-color:var(--sb-gray-800)}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardIcon__jkxDJ{align-items:center;border-radius:var(--p-2);display:flex;flex-shrink:0;height:32px;justify-content:center;width:32px}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardContent__PTPwz{display:flex;flex:1;flex-direction:column;min-width:0}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardTitle__K1wqr{font-size:var(--text-base);font-weight:600;line-height:1.4}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardSubtitle__fVeF2{color:var(--muted-foreground);font-size:var(--text-sm);line-height:1.4}";
3
+ var css_248z = ".ChatMessage_root__6rnsF{background:var(--bg-secondary);display:flex;flex-direction:column;gap:var(--p-1);padding:var(--p-4);padding-bottom:var(--p-1)}.ChatMessage_text__Y1XNR{color:var(--text-secondary);font-size:var(--text-sm);max-width:100%;min-width:0;overflow-wrap:anywhere;-webkit-user-select:text;-moz-user-select:text;user-select:text;width:-moz-fit-content;width:fit-content;word-break:break-word}.ChatMessage_role-assistant__wketE+.ChatMessage_role-assistant__wketE,.ChatMessage_role-system__g13OP+.ChatMessage_role-system__g13OP,.ChatMessage_role-user__u4JPV+.ChatMessage_role-user__u4JPV{padding-top:var(--p-1)}.ChatMessage_role-user__u4JPV{align-items:flex-end;max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_userColumn__cQM6-{align-items:flex-end;display:flex;flex-direction:column;gap:var(--p-2);max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-slate-100);border-radius:var(--p-4);border-bottom-right-radius:0;box-sizing:border-box;overflow:hidden;padding:var(--p-3) var(--p-4);white-space:pre-wrap}.dark .ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-gray-900)}.ChatMessage_role-system__g13OP{align-items:center}.ChatMessage_role-system__g13OP .ChatMessage_text__Y1XNR{color:var(--muted-foreground);width:100%}.ChatMessage_role-assistant__wketE .ChatMessage_text__Y1XNR{width:100%}.ChatMessage_role-assistant__wketE h3{line-height:2.4}.ChatMessage_role-assistant__wketE h4{font-size:1.1em;font-weight:600;line-height:2.2}.ChatMessage_role-assistant__wketE .ChatMessage_bullet__6vAhq{display:inline-block;margin-left:4px;margin-right:6px}.ChatMessage_role-assistant__wketE .ChatMessage_bullet__6vAhq:before{color:var(--text-secondary);content:\"•\";display:inline-block}.ChatMessage_role-assistant__wketE .ChatMessage_scrollHorizontal__Rms9n{max-width:100%}.ChatMessage_role-assistant__wketE table{border:1px solid var(--border);border-collapse:collapse;border-radius:var(--p-2);border-spacing:0;margin:var(--p-4) 0;overflow:hidden}.ChatMessage_role-assistant__wketE table td,.ChatMessage_role-assistant__wketE table th{border:1px solid var(--border);min-width:100px;padding:var(--p-1)}.ChatMessage_role-assistant__wketE table th{text-align:left}.ChatMessage_role-assistant__wketE ol,.ChatMessage_role-assistant__wketE ul{padding-left:var(--p-4)}.ChatMessage_role-assistant__wketE ul{list-style-type:disc}.ChatMessage_role-assistant__wketE ol{list-style-type:decimal}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T{align-items:center;border:1px dashed var(--sb-slate-300);border-radius:8px;color:var(--foreground);display:inline-flex;font-size:var(--text-xs);gap:6px;margin:1px;max-width:100%;padding:2px 6px 2px 4px;text-decoration:none;vertical-align:middle}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T:hover{background-color:var(--sb-slate-50);border-color:var(--sb-slate-400);border-style:solid}.dark .ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T{border-color:var(--sb-gray-600)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T:hover{background-color:var(--sb-gray-900)}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLinkLabel__PMU7e{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ChatMessage_role-assistant__wketE .ChatMessage_quickReplyWrap__1UFyD{display:inline-block;margin:var(--p-1) var(--p-1) var(--p-1) 0;vertical-align:middle}.ChatMessage_role-assistant__wketE .ChatMessage_downloadButtons__RygM-{display:flex;gap:var(--p-2);margin-top:var(--p-4)}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa{align-items:center;background-color:var(--background);border-radius:var(--p-3);box-shadow:0 0 0 1px var(--border);cursor:pointer;display:flex;gap:var(--p-4);margin-top:var(--p-3);padding:var(--p-3);padding-right:var(--p-4);transition:all .15s;width:-moz-fit-content;width:fit-content}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa:hover{background-color:var(--sb-gray-50);border-color:var(--border)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa{background-color:var(--sb-gray-900);border-color:var(--border)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa:hover{background-color:var(--sb-gray-800)}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardIcon__jkxDJ{align-items:center;border-radius:var(--p-2);display:flex;flex-shrink:0;height:32px;justify-content:center;width:32px}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardContent__PTPwz{display:flex;flex:1;flex-direction:column;min-width:0}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardTitle__K1wqr{font-size:var(--text-base);font-weight:600;line-height:1.4}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardSubtitle__fVeF2{color:var(--muted-foreground);font-size:var(--text-sm);line-height:1.4}";
4
4
  var S = {"root":"ChatMessage_root__6rnsF","text":"ChatMessage_text__Y1XNR","role-user":"ChatMessage_role-user__u4JPV","role-assistant":"ChatMessage_role-assistant__wketE","role-system":"ChatMessage_role-system__g13OP","userColumn":"ChatMessage_userColumn__cQM6-","bullet":"ChatMessage_bullet__6vAhq","scrollHorizontal":"ChatMessage_scrollHorizontal__Rms9n","datasetAppLink":"ChatMessage_datasetAppLink__Pxy-T","datasetAppLinkLabel":"ChatMessage_datasetAppLinkLabel__PMU7e","quickReplyWrap":"ChatMessage_quickReplyWrap__1UFyD","downloadButtons":"ChatMessage_downloadButtons__RygM-","downloadCard":"ChatMessage_downloadCard__NsNRa","downloadCardIcon":"ChatMessage_downloadCardIcon__jkxDJ","downloadCardContent":"ChatMessage_downloadCardContent__PTPwz","downloadCardTitle":"ChatMessage_downloadCardTitle__K1wqr","downloadCardSubtitle":"ChatMessage_downloadCardSubtitle__fVeF2"};
5
5
  styleInject(css_248z);
6
6
 
@@ -143,8 +143,11 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
143
143
  const dom = chatPromptSafeEditorDom(editor);
144
144
  if (dom)
145
145
  syncChatPromptComposerHeight(dom, t);
146
+ if (!disabled && !editor.isDestroyed) {
147
+ editor.chain().focus('end').run();
148
+ }
146
149
  });
147
- }, [editor, prefillMessage]);
150
+ }, [editor, prefillMessage, disabled]);
148
151
  useLayoutEffect(() => {
149
152
  if (!editor)
150
153
  return;
@@ -14,6 +14,10 @@ function shouldHealChatShellDesync(chatOpen, shellChatPanelOpen, layoutDismissed
14
14
  function shouldCloseStaleLocalChatOpen(shellChatPanelOpen, chatOpen, isOpen) {
15
15
  return !shellChatPanelOpen && !chatOpen && isOpen;
16
16
  }
17
+ /** Shell still reserves width but no instance is showing chat (e.g. route change dropped `?chat=`). */
18
+ function shouldCloseOrphanShellChat(shellChatPanelOpen, chatOpen, isOpen) {
19
+ return shellChatPanelOpen && !chatOpen && !isOpen;
20
+ }
17
21
  function shouldDismissChatAfterShellClosed(params) {
18
22
  const { shellChatPanelOpen, wasShellChatPanelOpen, chatOpen, isOpen, openedShellChat, } = params;
19
23
  if (shellChatPanelOpen || !wasShellChatPanelOpen || !chatOpen) {
@@ -25,4 +29,4 @@ function isChatPanelVisible(isOpen, shellChatPanelOpen) {
25
29
  return isOpen && shellChatPanelOpen;
26
30
  }
27
31
 
28
- export { isChatPanelVisible, shouldCloseStaleLocalChatOpen, shouldDismissChatAfterShellClosed, shouldHealChatShellDesync, shouldOpenChatFromUrl };
32
+ export { isChatPanelVisible, shouldCloseOrphanShellChat, shouldCloseStaleLocalChatOpen, shouldDismissChatAfterShellClosed, shouldHealChatShellDesync, shouldOpenChatFromUrl };
@@ -2,7 +2,7 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
3
3
  import { MessageRole } from '../Chat.types.js';
4
4
  import { isGraphIntakeAssistantStepComplete, matchUserTextToQuickReply, isPresetScriptGraph, branchesFromPresetScriptGraph, parseScriptLine, textHasQuickReplyMarkers, branchKeysUsedFromChatHistory, branchKeysUsedByUserMessages, extractQuickReplyLabelKeyPairsFromText, entryBranchKeyBeforeLastAssistant } from '../ChatMessage/presetScript.js';
5
- import { buildChatSendMessagePayload, displayTextFromSendPayload } from '../buildChatSendMessagePayload.js';
5
+ import { displayTextFromSendPayload, buildChatSendMessagePayload } from '../buildChatSendMessagePayload.js';
6
6
  import { usedPresetIdsFromMessages } from '../chat-preset-utils.js';
7
7
  import { useChatsForScopeId, useChat, useChatOutboundPending, useSyncChatPanelBusy, isChatEmpty } from '../../../../contexts/chat-context.js';
8
8
  import { shellFitsSidebarsLayout } from '../../../../hooks/panelWidth.js';
@@ -13,7 +13,7 @@ import logger from '../../../../lib/logger.js';
13
13
  import { mergePresetFreeformDefaults } from '../../../../utils/chatPresetMerge.js';
14
14
  import { useSidebar, DISMISS_CHAT_FOR_LAYOUT_EVENT } from '../../Sidebar/Sidebar.js';
15
15
  import { Chat } from '../Chat.js';
16
- import { shouldOpenChatFromUrl, shouldHealChatShellDesync, shouldCloseStaleLocalChatOpen, shouldDismissChatAfterShellClosed, isChatPanelVisible } from './chatPanelOpenSync.js';
16
+ import { shouldOpenChatFromUrl, shouldHealChatShellDesync, shouldCloseStaleLocalChatOpen, shouldCloseOrphanShellChat, shouldDismissChatAfterShellClosed, isChatPanelVisible } from './chatPanelOpenSync.js';
17
17
 
18
18
  /** Fallback when `scopeId` prop omitted; apps should pass an explicit composite scope (e.g. `${userId}-dashboard`). */
19
19
  const NO_SCOPE_FALLBACK = '__uilib_chat_scope_unset__';
@@ -45,17 +45,20 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
45
45
  /** Nav-forced chat close; blocks URL sync from reopening chat in the same tick. */
46
46
  const layoutDismissedRef = useRef(false);
47
47
  const panelActive = embedAsPage || isOpen;
48
- // Ensure valid currentChatId when chat opens
48
+ // Ensure a renderable session when the panel is active (pick existing or create).
49
49
  useEffect(() => {
50
- if (panelActive && (!currentChatId || currentChatId === '')) {
51
- if (chats.length > 0) {
52
- // Select the first available chat
53
- setCurrentChatId(chats[0].session_id);
54
- }
55
- else {
56
- // Create a new chat if none exists
57
- newChat();
58
- }
50
+ if (!panelActive)
51
+ return;
52
+ const currentValid = currentChatId != null &&
53
+ currentChatId !== '' &&
54
+ chats.some(chat => chat.session_id === currentChatId);
55
+ if (currentValid)
56
+ return;
57
+ if (chats.length > 0) {
58
+ setCurrentChatId(chats[0].session_id);
59
+ }
60
+ else {
61
+ newChat();
59
62
  }
60
63
  }, [panelActive, currentChatId, chats, setCurrentChatId, newChat]);
61
64
  const [scriptByChatId, setScriptByChatId] = useState({});
@@ -294,8 +297,12 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
294
297
  endLocalDemoFlow(chatId);
295
298
  void (async () => {
296
299
  try {
297
- await sendMessage(displayLabel);
298
- onMessage?.(displayLabel);
300
+ let payload = displayLabel;
301
+ if (transformSendPayload) {
302
+ payload = await transformSendPayload(displayLabel, undefined, displayLabel);
303
+ }
304
+ const assistantResponse = await sendMessage(payload);
305
+ onMessage?.(displayTextFromSendPayload(payload), assistantResponse);
299
306
  }
300
307
  catch (error) {
301
308
  logger.error('Error sending chat message:', error);
@@ -312,6 +319,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
312
319
  sendMessage,
313
320
  onMessage,
314
321
  onScriptComplete,
322
+ transformSendPayload,
315
323
  ]);
316
324
  const handlePromptSubmit = useCallback(async (message, attachments) => {
317
325
  const chatId = currentChatId;
@@ -688,6 +696,16 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
688
696
  }
689
697
  setIsOpen(false);
690
698
  }, [embedAsPage, shellChatPanelOpen, chatOpen, isOpen]);
699
+ /** Shell width reserved but no chat UI (e.g. nav dropped `?chat=` while old page still had it). */
700
+ useEffect(() => {
701
+ if (embedAsPage) {
702
+ return;
703
+ }
704
+ if (!shouldCloseOrphanShellChat(shellChatPanelOpen, chatOpen, isOpen)) {
705
+ return;
706
+ }
707
+ setChatPanelOpen(false);
708
+ }, [embedAsPage, shellChatPanelOpen, chatOpen, isOpen, setChatPanelOpen]);
691
709
  /** Shell chat closed while `?chat=` still set (fallback if layout event was missed). */
692
710
  useEffect(() => {
693
711
  if (embedAsPage) {
@@ -706,15 +724,20 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
706
724
  }
707
725
  dismissChatForLayout();
708
726
  }, [embedAsPage, shellChatPanelOpen, chatOpen, dismissChatForLayout, isOpen]);
709
- /** Route change: release shell slot only if this instance opened it (not Strict Mode remount). */
727
+ /** Route change: release shell unless destination URL still has `?chat=` (read live search, not stale closure). */
710
728
  useEffect(() => {
711
729
  return () => {
712
- if (!embedAsPage && openedShellChatRef.current && !chatOpen) {
713
- openedShellChatRef.current = false;
730
+ if (embedAsPage || !openedShellChatRef.current) {
731
+ return;
732
+ }
733
+ openedShellChatRef.current = false;
734
+ const chatStillInUrl = typeof window !== 'undefined' &&
735
+ new URLSearchParams(window.location.search).has(CHAT_QUERY_PARAM);
736
+ if (!chatStillInUrl) {
714
737
  setChatPanelOpen(false);
715
738
  }
716
739
  };
717
- }, [embedAsPage, chatOpen, setChatPanelOpen]);
740
+ }, [embedAsPage, setChatPanelOpen]);
718
741
  useEffect(() => {
719
742
  if (embedAsPage) {
720
743
  return;
@@ -1,6 +1,6 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.Tooltip_tooltipContent__b3pS-{backdrop-filter:blur(10px);background-color:var(--popover);border:1px solid var(--border);border-radius:var(--p-3);box-shadow:0 10px 15px -3px rgba(0,0,0,.1);color:var(--popover-foreground);font-size:12px;min-width:100%;padding:6px 12px;text-wrap:balance;transform-origin:var(--radix-tooltip-content-transform-origin);width:-moz-fit-content;width:fit-content;word-break:break-word;z-index:50}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-[data-state=instant-open],.Tooltip_tooltipContent__b3pS-[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=bottom]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-top-2__8uuS- .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=left]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-right-2__Uu79F .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=right]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-left-2__23kHm .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=top]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-bottom-2__O-Aa8 .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=top]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=instant-open],.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in}.Tooltip_tooltipContentOverTrigger__VQAU3{box-sizing:border-box;height:auto}.Tooltip_tooltipArrow__87DVL{background-color:var(--popover);border-bottom:1px solid var(--border);border-left-width:1px;border-left:0 solid var(--border);border-radius:2px;border-right:1px solid var(--border);border-top-width:1px;border-top:0 solid var(--border);fill:var(--popover);height:10px;transform:translateY(calc(-50% + .5px)) rotate(45deg);width:10px;z-index:50}@keyframes Tooltip_fade-in__ZQqZv{0%{opacity:0}to{opacity:1}}@keyframes Tooltip_fade-out__UOBET{0%{opacity:1}to{opacity:0}}@keyframes Tooltip_zoom-in__SbWQb{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes Tooltip_zoom-out__fodOk{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes Tooltip_slide-in-from-top-2__8uuS-{0%{opacity:0;transform:translateY(-.5rem)}to{opacity:1;transform:translateY(0)}}@keyframes Tooltip_slide-in-from-right-2__Uu79F{0%{opacity:0;transform:translateX(.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-left-2__23kHm{0%{opacity:0;transform:translateX(-.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-bottom-2__O-Aa8{0%{opacity:0;transform:translateY(.5rem)}to{opacity:1;transform:translateY(0)}}";
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.Tooltip_tooltipContent__b3pS-{backdrop-filter:blur(10px);background-color:var(--popover);border:1px solid var(--border);border-radius:var(--p-3);box-shadow:0 10px 15px -3px rgba(0,0,0,.1);color:var(--popover-foreground);font-size:12px;min-width:100%;padding:6px 12px;text-wrap:balance;transform-origin:var(--radix-tooltip-content-transform-origin);width:-moz-fit-content;width:fit-content;word-break:break-word;z-index:50}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-[data-state=instant-open],.Tooltip_tooltipContent__b3pS-[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=bottom]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-top-2__8uuS- .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=left]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-right-2__Uu79F .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=right]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-left-2__23kHm .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=delayed-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=instant-open][data-side=top],.Tooltip_tooltipContent__b3pS-[data-state=open][data-side=top]{animation:Tooltip_fade-in__ZQqZv .15s ease-out,Tooltip_zoom-in__SbWQb .15s ease-out,Tooltip_slide-in-from-bottom-2__O-Aa8 .15s ease-out}.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=bottom],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=left],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=right],.Tooltip_tooltipContent__b3pS-[data-state=closed][data-side=top]{animation:Tooltip_fade-out__UOBET .1s ease-in,Tooltip_zoom-out__fodOk .1s ease-in}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3{box-sizing:border-box;height:auto;text-wrap:wrap}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=delayed-open],.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=instant-open],.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=open]{animation:Tooltip_fade-in__ZQqZv .15s ease-out}.Tooltip_tooltipContent__b3pS-.Tooltip_tooltipContentOverTrigger__VQAU3[data-state=closed]{animation:Tooltip_fade-out__UOBET .1s ease-in}.Tooltip_tooltipArrow__87DVL{background-color:var(--popover);border-bottom:1px solid var(--border);border-left-width:1px;border-left:0 solid var(--border);border-radius:2px;border-right:1px solid var(--border);border-top-width:1px;border-top:0 solid var(--border);fill:var(--popover);height:10px;transform:translateY(calc(-50% + .5px)) rotate(45deg);width:10px;z-index:50}@keyframes Tooltip_fade-in__ZQqZv{0%{opacity:0}to{opacity:1}}@keyframes Tooltip_fade-out__UOBET{0%{opacity:1}to{opacity:0}}@keyframes Tooltip_zoom-in__SbWQb{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes Tooltip_zoom-out__fodOk{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes Tooltip_slide-in-from-top-2__8uuS-{0%{opacity:0;transform:translateY(-.5rem)}to{opacity:1;transform:translateY(0)}}@keyframes Tooltip_slide-in-from-right-2__Uu79F{0%{opacity:0;transform:translateX(.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-left-2__23kHm{0%{opacity:0;transform:translateX(-.5rem)}to{opacity:1;transform:translateX(0)}}@keyframes Tooltip_slide-in-from-bottom-2__O-Aa8{0%{opacity:0;transform:translateY(.5rem)}to{opacity:1;transform:translateY(0)}}";
4
4
  var S = {"tooltipContent":"Tooltip_tooltipContent__b3pS-","fade-in":"Tooltip_fade-in__ZQqZv","zoom-in":"Tooltip_zoom-in__SbWQb","fade-out":"Tooltip_fade-out__UOBET","zoom-out":"Tooltip_zoom-out__fodOk","slide-in-from-top-2":"Tooltip_slide-in-from-top-2__8uuS-","slide-in-from-right-2":"Tooltip_slide-in-from-right-2__Uu79F","slide-in-from-left-2":"Tooltip_slide-in-from-left-2__23kHm","slide-in-from-bottom-2":"Tooltip_slide-in-from-bottom-2__O-Aa8","tooltipContentOverTrigger":"Tooltip_tooltipContentOverTrigger__VQAU3","tooltipArrow":"Tooltip_tooltipArrow__87DVL"};
5
5
  styleInject(css_248z);
6
6
 
@@ -12,7 +12,7 @@ import 'lucide-react';
12
12
  import S from './SybilionAppHeader.styl.js';
13
13
 
14
14
  function SybilionAppHeader({ pageHeaderId, actionsAnchorId = PAGE_HEADER_ACTIONS_ID, actionsAnchorClassName, actionsStart, actionsEnd, pathname, onNavigate, authenticated, defaultApps, appsStorageKey, logo, logoAreaClassName, welcomeBannerOffset, ...navUserHeaderProps }) {
15
- return (jsxs(AppHeaderPortal, { pageHeaderId: pageHeaderId, children: [jsx("div", { className: cn(S.logoArea, welcomeBannerOffset && S.logoAreaWithBanner, logoAreaClassName), children: jsx(Link, { to: "/", className: S.logoLink, children: logo ?? jsx(Logo, { size: "md", "aria-hidden": true }) }) }), jsx(WorkspaceAppSwitcher, { pathname: pathname, onNavigate: onNavigate, authenticated: authenticated, defaultApps: defaultApps, appsStorageKey: appsStorageKey }), jsx(Gap, {}), jsxs("div", { id: actionsAnchorId, className: cn(S.actionsAnchor, actionsAnchorClassName), children: [actionsStart, jsx(NavUserHeader, { ...navUserHeaderProps }), actionsEnd] })] }));
15
+ return (jsxs(AppHeaderPortal, { pageHeaderId: pageHeaderId, children: [jsx("div", { className: cn(S.logoArea, welcomeBannerOffset && S.logoAreaWithBanner, logoAreaClassName), children: jsx(Link, { to: "/", className: S.logoLink, children: logo ?? jsx(Logo, { size: "md", "aria-hidden": true }) }) }), jsx(WorkspaceAppSwitcher, { pathname: pathname, onNavigate: onNavigate, authenticated: authenticated, defaultApps: defaultApps, appsStorageKey: appsStorageKey }), jsx(Gap, {}), jsxs("div", { className: cn(S.actionsAnchor, actionsAnchorClassName), children: [jsx("div", { id: actionsAnchorId, className: S.pageActionsPortal }), actionsStart, jsx(NavUserHeader, { ...navUserHeaderProps }), actionsEnd] })] }));
16
16
  }
17
17
 
18
18
  export { SybilionAppHeader };
@@ -1,7 +1,7 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.SybilionAppHeader_actionsAnchor__ress2{align-items:center;display:flex;flex-shrink:0;gap:var(--p-4)}.SybilionAppHeader_logoArea__3HAhG{align-items:center;display:flex;gap:var(--p-2);left:40px;position:absolute;top:22px;z-index:10}@media (max-width:768px){.SybilionAppHeader_logoArea__3HAhG{left:32px}}@media (min-width:768px){[data-slot=sidebar-wrapper][data-state=expanded] .SybilionAppHeader_logoArea__3HAhG{position:fixed}}.SybilionAppHeader_logoAreaWithBanner__7Iy78{top:22px;top:calc(22px + var(--welcome-banner-height, 0px))}@media (min-width:768px){[data-slot=sidebar-wrapper][data-state=collapsed] .SybilionAppHeader_logoAreaWithBanner__7Iy78{top:22px}}.SybilionAppHeader_logoLink__bH-KX{align-items:center;color:var(--color-foreground);display:flex;font-size:1.5rem;font-weight:400;gap:.5rem;text-decoration:none;white-space:nowrap;width:-moz-fit-content;width:fit-content}.SybilionAppHeader_logoLink__bH-KX svg{display:inline-flex;flex-shrink:0;height:32px;transition:transform .1s ease-in-out;width:auto}.SybilionAppHeader_logoLink__bH-KX:hover svg{transform:scale(1.05)}";
4
- var S = {"actionsAnchor":"SybilionAppHeader_actionsAnchor__ress2","logoArea":"SybilionAppHeader_logoArea__3HAhG","logoAreaWithBanner":"SybilionAppHeader_logoAreaWithBanner__7Iy78","logoLink":"SybilionAppHeader_logoLink__bH-KX"};
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.SybilionAppHeader_actionsAnchor__ress2,.SybilionAppHeader_pageActionsPortal__9C5ww{align-items:center;display:flex;flex-shrink:0;gap:var(--p-4)}.SybilionAppHeader_pageActionsPortal__9C5ww:empty{display:none}.SybilionAppHeader_logoArea__3HAhG{align-items:center;display:flex;gap:var(--p-2);left:40px;position:absolute;top:22px;z-index:10}@media (max-width:768px){.SybilionAppHeader_logoArea__3HAhG{left:32px}}@media (min-width:768px){[data-slot=sidebar-wrapper][data-state=expanded] .SybilionAppHeader_logoArea__3HAhG{position:fixed}}.SybilionAppHeader_logoAreaWithBanner__7Iy78{top:22px;top:calc(22px + var(--welcome-banner-height, 0px))}@media (min-width:768px){[data-slot=sidebar-wrapper][data-state=collapsed] .SybilionAppHeader_logoAreaWithBanner__7Iy78{top:22px}}.SybilionAppHeader_logoLink__bH-KX{align-items:center;color:var(--color-foreground);display:flex;font-size:1.5rem;font-weight:400;gap:.5rem;text-decoration:none;white-space:nowrap;width:-moz-fit-content;width:fit-content}.SybilionAppHeader_logoLink__bH-KX svg{display:inline-flex;flex-shrink:0;height:32px;transition:transform .1s ease-in-out;width:auto}.SybilionAppHeader_logoLink__bH-KX:hover svg{transform:scale(1.05)}";
4
+ var S = {"actionsAnchor":"SybilionAppHeader_actionsAnchor__ress2","pageActionsPortal":"SybilionAppHeader_pageActionsPortal__9C5ww","logoArea":"SybilionAppHeader_logoArea__3HAhG","logoAreaWithBanner":"SybilionAppHeader_logoAreaWithBanner__7Iy78","logoLink":"SybilionAppHeader_logoLink__bH-KX"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
@@ -270,6 +270,23 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
270
270
  return { ...prev, [scopeId]: updatedChats };
271
271
  });
272
272
  }, [userSwitchKey]);
273
+ const setChatMessages = useCallback((scopeId, chatId, messages) => {
274
+ if (userSwitchKey === null)
275
+ return;
276
+ addScopeIdToRegistry(scopeId);
277
+ const cloned = messages.map(message => ({ ...message }));
278
+ setChats(prev => {
279
+ const scopeChats = prev[scopeId] ?? [];
280
+ const updatedChats = scopeChats.map(chat => {
281
+ if (chat.session_id !== chatId)
282
+ return chat;
283
+ return { ...chat, messages: cloned };
284
+ });
285
+ const chatsKey = getChatsKey(scopeId);
286
+ LS.set(chatsKey, updatedChats);
287
+ return { ...prev, [scopeId]: updatedChats };
288
+ });
289
+ }, [userSwitchKey]);
273
290
  const sendMessage = useCallback(async (scopeId, message, chatId) => {
274
291
  const targetChatId = chatId ?? getCurrentChatId(scopeId);
275
292
  if (targetChatId === null || targetChatId === '') {
@@ -347,6 +364,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
347
364
  addMessage,
348
365
  removeMessageById,
349
366
  updateMessageById,
367
+ setChatMessages,
350
368
  sendMessage,
351
369
  getChatsForScopeId,
352
370
  getCurrentChatId,
@@ -2,6 +2,10 @@
2
2
  export declare function shouldOpenChatFromUrl(chatOpen: boolean, layoutDismissed: boolean): boolean;
3
3
  export declare function shouldHealChatShellDesync(chatOpen: boolean, shellChatPanelOpen: boolean, layoutDismissed: boolean, isOpen: boolean): boolean;
4
4
  export declare function shouldCloseStaleLocalChatOpen(shellChatPanelOpen: boolean, chatOpen: boolean, isOpen: boolean): boolean;
5
+ /** Shell still reserves width but no instance is showing chat (e.g. route change dropped `?chat=`). */
6
+ export declare function shouldCloseOrphanShellChat(shellChatPanelOpen: boolean, chatOpen: boolean, isOpen: boolean): boolean;
7
+ /** App shell: collapse chat column when URL has no `?chat=` (no mounted ChatSheet required). */
8
+ export declare function shouldCollapseShellChatWithoutUrlParam(hasChatUrlParam: boolean): boolean;
5
9
  export declare function shouldDismissChatAfterShellClosed(params: {
6
10
  shellChatPanelOpen: boolean;
7
11
  wasShellChatPanelOpen: boolean;
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { type Chat, type ChatSendMessagePayload, MessageRole, type UserTextFileAttachment } from '#uilib/components/ui/Chat/Chat.types';
2
+ import { type Chat, type ChatSendMessagePayload, type Message, MessageRole, type UserTextFileAttachment } from '#uilib/components/ui/Chat/Chat.types';
3
3
  import type { ChatResponse } from '#uilib/types/chat-api.types';
4
4
  export type SendChatMessageFn = (message: string, targetChatId: string) => Promise<ChatResponse>;
5
5
  export type { ChatSendMessagePayload, UserTextFileAttachment, } from '#uilib/components/ui/Chat/Chat.types';
@@ -20,6 +20,8 @@ export interface ChatContextType {
20
20
  addMessage: (scopeId: string, chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string | undefined;
21
21
  removeMessageById: (scopeId: string, chatId: string, messageId: string) => void;
22
22
  updateMessageById: (scopeId: string, chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
23
+ /** Replaces all messages on a session (e.g. seeding from another chat). */
24
+ setChatMessages: (scopeId: string, chatId: string, messages: Message[]) => void;
23
25
  sendMessage: (scopeId: string, message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
24
26
  getChatsForScopeId: (scopeId: string) => Chat[];
25
27
  getCurrentChatId: (scopeId: string) => string | null;
@@ -0,0 +1 @@
1
+ export default function ChatComposerPrefillPage(): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.67",
3
+ "version": "1.3.70",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -48,7 +48,7 @@
48
48
  box-sizing border-box
49
49
 
50
50
  :global(.dark) &
51
- background-color var(--sb-gray-800)
51
+ background-color var(--sb-gray-900)
52
52
 
53
53
  .role-system
54
54
  align-items center
@@ -208,8 +208,11 @@ export function useChatPromptEditor({
208
208
  queueMicrotask(() => {
209
209
  const dom = chatPromptSafeEditorDom(editor);
210
210
  if (dom) syncChatPromptComposerHeight(dom, t);
211
+ if (!disabled && !editor.isDestroyed) {
212
+ editor.chain().focus('end').run();
213
+ }
211
214
  });
212
- }, [editor, prefillMessage]);
215
+ }, [editor, prefillMessage, disabled]);
213
216
 
214
217
  useLayoutEffect(() => {
215
218
  if (!editor) return;
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  isChatPanelVisible,
3
+ shouldCloseOrphanShellChat,
3
4
  shouldCloseStaleLocalChatOpen,
5
+ shouldCollapseShellChatWithoutUrlParam,
4
6
  shouldDismissChatAfterShellClosed,
5
7
  shouldHealChatShellDesync,
6
8
  shouldOpenChatFromUrl,
@@ -36,6 +38,30 @@ describe('shouldCloseStaleLocalChatOpen', () => {
36
38
  });
37
39
  });
38
40
 
41
+ describe('shouldCollapseShellChatWithoutUrlParam', () => {
42
+ it('collapses when chat param absent', () => {
43
+ expect(shouldCollapseShellChatWithoutUrlParam(false)).toBe(true);
44
+ });
45
+
46
+ it('keeps shell when chat param present', () => {
47
+ expect(shouldCollapseShellChatWithoutUrlParam(true)).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('shouldCloseOrphanShellChat', () => {
52
+ it('closes shell slot when URL and local UI are both closed', () => {
53
+ expect(shouldCloseOrphanShellChat(true, false, false)).toBe(true);
54
+ });
55
+
56
+ it('keeps shell while ?chat= requests panel', () => {
57
+ expect(shouldCloseOrphanShellChat(true, true, false)).toBe(false);
58
+ });
59
+
60
+ it('keeps shell while this instance still renders chat', () => {
61
+ expect(shouldCloseOrphanShellChat(true, false, true)).toBe(false);
62
+ });
63
+ });
64
+
39
65
  describe('shouldDismissChatAfterShellClosed', () => {
40
66
  it('dismisses when shell closes with ?chat= and this instance opened chat', () => {
41
67
  expect(
@@ -30,6 +30,22 @@ export function shouldCloseStaleLocalChatOpen(
30
30
  return !shellChatPanelOpen && !chatOpen && isOpen;
31
31
  }
32
32
 
33
+ /** Shell still reserves width but no instance is showing chat (e.g. route change dropped `?chat=`). */
34
+ export function shouldCloseOrphanShellChat(
35
+ shellChatPanelOpen: boolean,
36
+ chatOpen: boolean,
37
+ isOpen: boolean,
38
+ ): boolean {
39
+ return shellChatPanelOpen && !chatOpen && !isOpen;
40
+ }
41
+
42
+ /** App shell: collapse chat column when URL has no `?chat=` (no mounted ChatSheet required). */
43
+ export function shouldCollapseShellChatWithoutUrlParam(
44
+ hasChatUrlParam: boolean,
45
+ ): boolean {
46
+ return !hasChatUrlParam;
47
+ }
48
+
33
49
  export function shouldDismissChatAfterShellClosed(params: {
34
50
  shellChatPanelOpen: boolean;
35
51
  wasShellChatPanelOpen: boolean;
@@ -54,6 +54,7 @@ import type { ChatEmptyStateConfig } from '../ChatEmptyState/ChatEmptyState.type
54
54
  import type { ChatEmptyStateProps } from '../ChatEmptyState/ChatEmptyState.types';
55
55
  import {
56
56
  isChatPanelVisible,
57
+ shouldCloseOrphanShellChat,
57
58
  shouldCloseStaleLocalChatOpen,
58
59
  shouldDismissChatAfterShellClosed,
59
60
  shouldHealChatShellDesync,
@@ -197,16 +198,20 @@ export function useChatPanelChromeModel({
197
198
  const layoutDismissedRef = useRef(false);
198
199
  const panelActive = embedAsPage || isOpen;
199
200
 
200
- // Ensure valid currentChatId when chat opens
201
+ // Ensure a renderable session when the panel is active (pick existing or create).
201
202
  useEffect(() => {
202
- if (panelActive && (!currentChatId || currentChatId === '')) {
203
- if (chats.length > 0) {
204
- // Select the first available chat
205
- setCurrentChatId(chats[0].session_id);
206
- } else {
207
- // Create a new chat if none exists
208
- newChat();
209
- }
203
+ if (!panelActive) return;
204
+
205
+ const currentValid =
206
+ currentChatId != null &&
207
+ currentChatId !== '' &&
208
+ chats.some(chat => chat.session_id === currentChatId);
209
+ if (currentValid) return;
210
+
211
+ if (chats.length > 0) {
212
+ setCurrentChatId(chats[0].session_id);
213
+ } else {
214
+ newChat();
210
215
  }
211
216
  }, [panelActive, currentChatId, chats, setCurrentChatId, newChat]);
212
217
  const [scriptByChatId, setScriptByChatId] = useState<
@@ -496,8 +501,16 @@ export function useChatPanelChromeModel({
496
501
  endLocalDemoFlow(chatId);
497
502
  void (async () => {
498
503
  try {
499
- await sendMessage(displayLabel);
500
- onMessage?.(displayLabel);
504
+ let payload: string | ChatSendMessagePayload = displayLabel;
505
+ if (transformSendPayload) {
506
+ payload = await transformSendPayload(
507
+ displayLabel,
508
+ undefined,
509
+ displayLabel,
510
+ );
511
+ }
512
+ const assistantResponse = await sendMessage(payload);
513
+ onMessage?.(displayTextFromSendPayload(payload), assistantResponse);
501
514
  } catch (error) {
502
515
  logger.error('Error sending chat message:', error);
503
516
  }
@@ -514,6 +527,7 @@ export function useChatPanelChromeModel({
514
527
  sendMessage,
515
528
  onMessage,
516
529
  onScriptComplete,
530
+ transformSendPayload,
517
531
  ],
518
532
  );
519
533
 
@@ -936,6 +950,17 @@ export function useChatPanelChromeModel({
936
950
  setIsOpen(false);
937
951
  }, [embedAsPage, shellChatPanelOpen, chatOpen, isOpen]);
938
952
 
953
+ /** Shell width reserved but no chat UI (e.g. nav dropped `?chat=` while old page still had it). */
954
+ useEffect(() => {
955
+ if (embedAsPage) {
956
+ return;
957
+ }
958
+ if (!shouldCloseOrphanShellChat(shellChatPanelOpen, chatOpen, isOpen)) {
959
+ return;
960
+ }
961
+ setChatPanelOpen(false);
962
+ }, [embedAsPage, shellChatPanelOpen, chatOpen, isOpen, setChatPanelOpen]);
963
+
939
964
  /** Shell chat closed while `?chat=` still set (fallback if layout event was missed). */
940
965
  useEffect(() => {
941
966
  if (embedAsPage) {
@@ -957,15 +982,21 @@ export function useChatPanelChromeModel({
957
982
  dismissChatForLayout();
958
983
  }, [embedAsPage, shellChatPanelOpen, chatOpen, dismissChatForLayout, isOpen]);
959
984
 
960
- /** Route change: release shell slot only if this instance opened it (not Strict Mode remount). */
985
+ /** Route change: release shell unless destination URL still has `?chat=` (read live search, not stale closure). */
961
986
  useEffect(() => {
962
987
  return () => {
963
- if (!embedAsPage && openedShellChatRef.current && !chatOpen) {
964
- openedShellChatRef.current = false;
988
+ if (embedAsPage || !openedShellChatRef.current) {
989
+ return;
990
+ }
991
+ openedShellChatRef.current = false;
992
+ const chatStillInUrl =
993
+ typeof window !== 'undefined' &&
994
+ new URLSearchParams(window.location.search).has(CHAT_QUERY_PARAM);
995
+ if (!chatStillInUrl) {
965
996
  setChatPanelOpen(false);
966
997
  }
967
998
  };
968
- }, [embedAsPage, chatOpen, setChatPanelOpen]);
999
+ }, [embedAsPage, setChatPanelOpen]);
969
1000
 
970
1001
  useEffect(() => {
971
1002
  if (embedAsPage) {
@@ -59,6 +59,10 @@
59
59
  animation fade-out 0.1s ease-in, zoom-out 0.1s ease-in
60
60
 
61
61
  &.tooltipContentOverTrigger
62
+ box-sizing border-box
63
+ height auto
64
+ text-wrap initial
65
+
62
66
  &[data-state="open"],
63
67
  &[data-state="instant-open"],
64
68
  &[data-state="delayed-open"]
@@ -67,10 +71,6 @@
67
71
  &[data-state="closed"]
68
72
  animation fade-out 0.1s ease-in
69
73
 
70
- .tooltipContentOverTrigger
71
- box-sizing border-box
72
- height auto
73
-
74
74
  .tooltipArrow
75
75
  z-index 50
76
76
  width 10px
@@ -6,6 +6,15 @@
6
6
  gap var(--p-4)
7
7
  flex-shrink 0
8
8
 
9
+ .pageActionsPortal
10
+ display flex
11
+ align-items center
12
+ gap var(--p-4)
13
+ flex-shrink 0
14
+
15
+ &:empty
16
+ display none
17
+
9
18
  .logoArea
10
19
  position absolute
11
20
  top 22px
@@ -5,6 +5,7 @@ interface CssExports {
5
5
  'logoArea': string;
6
6
  'logoAreaWithBanner': string;
7
7
  'logoLink': string;
8
+ 'pageActionsPortal': string;
8
9
  }
9
10
  export const cssExports: CssExports;
10
11
  export default cssExports;
@@ -69,10 +69,8 @@ export function SybilionAppHeader({
69
69
  appsStorageKey={appsStorageKey}
70
70
  />
71
71
  <Gap />
72
- <div
73
- id={actionsAnchorId}
74
- className={cn(S.actionsAnchor, actionsAnchorClassName)}
75
- >
72
+ <div className={cn(S.actionsAnchor, actionsAnchorClassName)}>
73
+ <div id={actionsAnchorId} className={S.pageActionsPortal} />
76
74
  {actionsStart}
77
75
  <NavUserHeader {...navUserHeaderProps} />
78
76
  {actionsEnd}
@@ -67,6 +67,12 @@ export interface ChatContextType {
67
67
  messageId: string,
68
68
  patch: UpdateChatMessagePatch,
69
69
  ) => void;
70
+ /** Replaces all messages on a session (e.g. seeding from another chat). */
71
+ setChatMessages: (
72
+ scopeId: string,
73
+ chatId: string,
74
+ messages: Message[],
75
+ ) => void;
70
76
  sendMessage: (
71
77
  scopeId: string,
72
78
  message: string | ChatSendMessagePayload,
@@ -425,6 +431,28 @@ export function ChatProvider({
425
431
  [userSwitchKey],
426
432
  );
427
433
 
434
+ const setChatMessages = useCallback(
435
+ (scopeId: string, chatId: string, messages: Message[]) => {
436
+ if (userSwitchKey === null) return;
437
+ addScopeIdToRegistry(scopeId);
438
+ const cloned = messages.map(message => ({ ...message }));
439
+
440
+ setChats(prev => {
441
+ const scopeChats = prev[scopeId] ?? [];
442
+ const updatedChats = scopeChats.map(chat => {
443
+ if (chat.session_id !== chatId) return chat;
444
+ return { ...chat, messages: cloned };
445
+ });
446
+
447
+ const chatsKey = getChatsKey(scopeId);
448
+ LS.set(chatsKey, updatedChats);
449
+
450
+ return { ...prev, [scopeId]: updatedChats };
451
+ });
452
+ },
453
+ [userSwitchKey],
454
+ );
455
+
428
456
  const sendMessage = useCallback(
429
457
  async (
430
458
  scopeId: string,
@@ -543,6 +571,7 @@ export function ChatProvider({
543
571
  addMessage,
544
572
  removeMessageById,
545
573
  updateMessageById,
574
+ setChatMessages,
546
575
  sendMessage,
547
576
  getChatsForScopeId,
548
577
  getCurrentChatId,
@@ -0,0 +1,202 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import { Button } from '#uilib/components/ui/Button';
4
+ import {
5
+ ChatChrome,
6
+ type ChatSheetActions,
7
+ type Message,
8
+ MessageRole,
9
+ } from '#uilib/components/ui/Chat';
10
+ import { PageContentSection } from '#uilib/components/ui/Page';
11
+ import { ScrollRef } from '@homecode/ui';
12
+ import { MessageSquare } from 'lucide-react';
13
+
14
+ import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
15
+ import { DOCS_CHAT_USER_KEY } from '../docsConstants';
16
+ import { DocsHeaderActions } from '../docsHeaderActions';
17
+
18
+ const NO_QUICK_REPLY_KEYS: ReadonlySet<string> = new Set();
19
+
20
+ const SAMPLE_PREFILL =
21
+ 'Summarize revenue trends for Q4 and highlight outliers.';
22
+
23
+ const ASSISTANT_REPLY_TEXT =
24
+ 'Prefill demo reply — composer should stay editable after send.';
25
+
26
+ function makeMessage(role: MessageRole, text: string): Message {
27
+ return {
28
+ id: crypto.randomUUID(),
29
+ role,
30
+ text,
31
+ timestamp: Date.now(),
32
+ };
33
+ }
34
+
35
+ const DOCS_CHAT_PREFILL_SCOPE_ID = `${DOCS_CHAT_USER_KEY}-docs-composer-prefill`;
36
+
37
+ export default function ChatComposerPrefillPage() {
38
+ const [messages, setMessages] = useState<Message[]>([]);
39
+ const [isLoading, setIsLoading] = useState(false);
40
+ const [inlinePrefill, setInlinePrefill] = useState<string | null>(null);
41
+ const replyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
42
+ const scrollRef = useRef<ScrollRef>(null);
43
+ const sheetActionsRef = useRef<ChatSheetActions | null>(null);
44
+
45
+ useEffect(() => {
46
+ return () => {
47
+ if (replyTimeoutRef.current != null) {
48
+ clearTimeout(replyTimeoutRef.current);
49
+ }
50
+ };
51
+ }, []);
52
+
53
+ const isEmpty = messages.length === 0 && !isLoading;
54
+ const isLastMessageFromUser =
55
+ messages.length > 0 &&
56
+ messages[messages.length - 1]?.role === MessageRole.USER;
57
+
58
+ const onSubmit = useCallback(
59
+ (raw: string) => {
60
+ const text = raw.trim();
61
+ if (!text || isLoading) return;
62
+
63
+ setMessages(prev => [...prev, makeMessage(MessageRole.USER, text)]);
64
+ setIsLoading(true);
65
+ setInlinePrefill(null);
66
+
67
+ if (replyTimeoutRef.current != null) {
68
+ clearTimeout(replyTimeoutRef.current);
69
+ }
70
+ replyTimeoutRef.current = setTimeout(() => {
71
+ replyTimeoutRef.current = null;
72
+ setMessages(prev => [
73
+ ...prev,
74
+ makeMessage(MessageRole.ASSISTANT, ASSISTANT_REPLY_TEXT),
75
+ ]);
76
+ setIsLoading(false);
77
+ }, 900);
78
+ },
79
+ [isLoading],
80
+ );
81
+
82
+ const prefillInlineComposer = useCallback(() => {
83
+ setInlinePrefill(SAMPLE_PREFILL);
84
+ }, []);
85
+
86
+ const openSheetWithPrefill = useCallback(() => {
87
+ sheetActionsRef.current?.openNewChatWithPrefill(SAMPLE_PREFILL);
88
+ }, []);
89
+
90
+ const promptDeepLink = `/docs/chat-composer-prefill?prompt=${encodeURIComponent(SAMPLE_PREFILL)}`;
91
+
92
+ return (
93
+ <>
94
+ <AppPageHeader
95
+ breadcrumbs={[{ label: 'Chat' }, { label: 'Composer prefill' }]}
96
+ title="Chat — composer prefill & focus"
97
+ subheader="Prefill the TipTap composer via prefillMessage, ChatSheetActions.openNewChatWithPrefill, or a ?prompt= deep link. Cursor lands at end; message is not auto-sent."
98
+ actions={
99
+ <DocsHeaderActions
100
+ actionsRef={sheetActionsRef}
101
+ scopeId={DOCS_CHAT_PREFILL_SCOPE_ID}
102
+ triggerLabel={<MessageSquare size={20} />}
103
+ triggerAriaLabel="AI Assistant"
104
+ emptyState={{
105
+ title: 'Start a conversation',
106
+ description:
107
+ 'Use the buttons below or ?prompt= to pre-fill this composer.',
108
+ }}
109
+ />
110
+ }
111
+ />
112
+ <PageContentSection>
113
+ <p style={{ marginBottom: 12, fontSize: 14, lineHeight: 1.5 }}>
114
+ After prefill, the composer receives focus at the end of the text so
115
+ users can edit before sending. Wired through{' '}
116
+ <code style={{ fontSize: 13 }}>useChatPromptEditor</code> (
117
+ <code style={{ fontSize: 13 }}>prefillMessage</code>) and{' '}
118
+ <code style={{ fontSize: 13 }}>useChatPanelChromeModel</code> (
119
+ <code style={{ fontSize: 13 }}>promptPrefill</code> /{' '}
120
+ <code style={{ fontSize: 13 }}>openNewChatWithPrefill</code>).
121
+ </p>
122
+ <p
123
+ style={{
124
+ marginBottom: 16,
125
+ display: 'flex',
126
+ flexWrap: 'wrap',
127
+ gap: 8,
128
+ }}
129
+ >
130
+ <Button
131
+ type="button"
132
+ variant="outline"
133
+ size="sm"
134
+ onClick={prefillInlineComposer}
135
+ >
136
+ Prefill inline composer
137
+ </Button>
138
+ <Button
139
+ type="button"
140
+ variant="outline"
141
+ size="sm"
142
+ onClick={openSheetWithPrefill}
143
+ >
144
+ Open portal chat with prefill
145
+ </Button>
146
+ <Button type="button" variant="outline" size="sm" asChild>
147
+ <a href={promptDeepLink}>Try ?prompt= deep link</a>
148
+ </Button>
149
+ </p>
150
+ <h3 style={{ marginBottom: 8, fontSize: 14, fontWeight: 600 }}>
151
+ What to verify
152
+ </h3>
153
+ <ul
154
+ style={{
155
+ margin: '0 0 16px',
156
+ paddingLeft: 20,
157
+ fontSize: 14,
158
+ lineHeight: 1.6,
159
+ }}
160
+ >
161
+ <li>Composer shows sample text; caret at end (not start)</li>
162
+ <li>User can type or edit before Enter / submit</li>
163
+ <li>Prefill does not send until user submits</li>
164
+ <li>
165
+ Portal path: new session opens, panel visible, same focus behavior
166
+ </li>
167
+ <li>
168
+ Deep link strips <code style={{ fontSize: 13 }}>?prompt=</code> and
169
+ opens chat with prefill once
170
+ </li>
171
+ </ul>
172
+ <ChatChrome
173
+ showResizeHandle={false}
174
+ resizeHandle={undefined}
175
+ onClose={undefined}
176
+ isEmpty={isEmpty}
177
+ renderPresets={() => null}
178
+ messages={messages}
179
+ onQuickReply={() => {}}
180
+ suppressedQuickReplyKeys={NO_QUICK_REPLY_KEYS}
181
+ isLoading={isLoading}
182
+ scriptContinueLabel={undefined}
183
+ onScriptContinue={undefined}
184
+ showSyntheticBranchButtons={false}
185
+ unusedBranchKeys={[]}
186
+ showInlinePresets={false}
187
+ isLastMessageFromUser={isLastMessageFromUser}
188
+ scrollRef={scrollRef}
189
+ effectiveScopeId="docs-chat-composer-prefill-inline"
190
+ onPromptSubmit={onSubmit}
191
+ onChatDeleted={() => {}}
192
+ promptPrefill={inlinePrefill}
193
+ emptyState={{
194
+ title: 'Inline prefill demo',
195
+ description:
196
+ 'Click “Prefill inline composer” or type below. Portal and ?prompt= use the header ChatSheet.',
197
+ }}
198
+ />
199
+ </PageContentSection>
200
+ </>
201
+ );
202
+ }
@@ -139,6 +139,12 @@ export const DOC_REGISTRY: DocEntry[] = [
139
139
  section: 'Chat',
140
140
  load: () => import('./pages/ChatAttachmentsDropzonePage'),
141
141
  },
142
+ {
143
+ slug: 'chat-composer-prefill',
144
+ title: 'Chat composer prefill',
145
+ section: 'Chat',
146
+ load: () => import('./pages/ChatComposerPrefillPage'),
147
+ },
142
148
  {
143
149
  slug: 'checkbox',
144
150
  title: 'Checkbox',