@sybilion/uilib 1.3.37 → 1.3.39

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 (34) hide show
  1. package/dist/esm/components/ui/Chat/Chat.types.js +1 -3
  2. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +7 -10
  3. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +2 -3
  4. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +2 -1
  5. package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +5 -2
  6. package/dist/esm/components/ui/Chat/ChatSheet/ChatSheet.js +2 -2
  7. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +12 -42
  8. package/dist/esm/index.js +1 -1
  9. package/dist/esm/tiptap/slash-mention/createSlashMentionExtension.js +24 -5
  10. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +3 -3
  11. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
  12. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +3 -6
  13. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +1 -1
  14. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.d.ts +3 -2
  15. package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSheet.d.ts +1 -1
  16. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +4 -4
  17. package/dist/esm/types/src/components/ui/Chat/index.d.ts +2 -2
  18. package/dist/esm/types/src/tiptap/slash-mention/types.d.ts +5 -3
  19. package/package.json +1 -1
  20. package/src/components/ui/Chat/Chat.types.ts +6 -4
  21. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +24 -46
  22. package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +6 -8
  23. package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +2 -13
  24. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +2 -0
  25. package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +10 -2
  26. package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +2 -2
  27. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +23 -58
  28. package/src/components/ui/Chat/index.ts +9 -2
  29. package/src/docs/pages/ChatAttachmentsDropzonePage.tsx +0 -5
  30. package/src/docs/pages/ChatPage.tsx +0 -5
  31. package/src/docs/pages/ChatSlashCommandsPage.tsx +40 -16
  32. package/src/docs/pages/ChatUserCsvAttachmentPage.tsx +0 -5
  33. package/src/tiptap/slash-mention/createSlashMentionExtension.ts +24 -5
  34. package/src/tiptap/slash-mention/types.ts +5 -3
@@ -4,7 +4,5 @@ var MessageRole;
4
4
  MessageRole["ASSISTANT"] = "assistant";
5
5
  MessageRole["SYSTEM"] = "system";
6
6
  })(MessageRole || (MessageRole = {}));
7
- /** System placeholder while dashboard generation runs (must match ChatSheet `addMessage` text). */
8
- const GENERATING_DASHBOARD_SYSTEM_TEXT = 'Generating dashboard…';
9
7
 
10
- export { GENERATING_DASHBOARD_SYSTEM_TEXT, MessageRole };
8
+ export { MessageRole };
@@ -4,7 +4,7 @@ import { useMemo, useState, useCallback, useEffect } from 'react';
4
4
  import { displayLabelForBranchKeyFromMessages, humanizeBranchKey } from '../ChatMessage/presetScript.js';
5
5
  import { TextShimmer } from '../../TextShimmer/TextShimmer.js';
6
6
  import { Scroll } from '@homecode/ui';
7
- import { X, PaperPlaneRightIcon, ChartLineIcon } from '@phosphor-icons/react';
7
+ import { X, PaperPlaneRightIcon } from '@phosphor-icons/react';
8
8
  import { Button } from '../../Button/Button.js';
9
9
  import { DropZone } from '../../DropZone/DropZone.js';
10
10
  import { PanelResizeHandle } from '../../Sidebar/Sidebar.js';
@@ -14,7 +14,7 @@ import { filterToTextAttachments, isAttachmentsDropzoneEnabled, buildAcceptAttr
14
14
  import { extractChatAttachmentItems } from '../chatAttachmentExtract.js';
15
15
  import S from './ChatChrome.styl.js';
16
16
 
17
- function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showBranchActionsRow, showSyntheticBranchButtons, unusedBranchKeys, isScriptComplete, onGenerateDashboard, generatingDashboard, onGenerateDashboardClick, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments = false, onAttachmentsDropped, slashCommandItems, promptPlaceholder, }) {
17
+ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments = false, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, }) {
18
18
  const filteredAllowedAttachments = useMemo(() => filterToTextAttachments(allowedAttachments), [allowedAttachments]);
19
19
  const attachmentsDropzoneEnabled = isAttachmentsDropzoneEnabled(allowedAttachments, allowPdfAttachments);
20
20
  const attachmentAccept = useMemo(() => buildAcceptAttr(filteredAllowedAttachments, allowPdfAttachments), [filteredAllowedAttachments, allowPdfAttachments]);
@@ -73,14 +73,11 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
73
73
  : undefined, onScriptContinue: isLast && scriptContinueLabel
74
74
  ? onScriptContinue
75
75
  : undefined, renderMessageChart: renderMessageChart }, msg.id));
76
- }), showBranchActionsRow && (jsxs("div", { className: S.branchRow, children: [showSyntheticBranchButtons
77
- ? unusedBranchKeys.map(key => {
78
- const label = displayLabelForBranchKeyFromMessages(key, messages) ?? humanizeBranchKey(key);
79
- return (jsx("span", { className: S.branchBtnWrap, children: jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: isLoading, onClick: () => onQuickReply(key, label), children: [jsx(PaperPlaneRightIcon, {}), label] }) }, key));
80
- })
81
- : null, isScriptComplete &&
82
- onGenerateDashboard &&
83
- !generatingDashboard ? (jsxs(Button, { type: "button", variant: "default", size: "lg", disabled: isLoading, onClick: onGenerateDashboardClick, children: [jsx(ChartLineIcon, {}), "Generate Dashboard"] })) : null] })), showInlinePresets && renderPresets('inline'), isLoading && isLastMessageFromUser && (jsx(TextShimmer, { duration: 1, spread: 5, className: S.loader, children: "Thinking..." }))] }) })), jsxs("div", { className: cn(S.footer, footerClassName), children: [isEmpty ? (jsx("div", { className: S.notice, children: "Forecast Assistant can make mistakes." })) : null, jsx(Chat.Prompt, { onSubmit: handlePromptSubmitWithAttachments, disabled: promptBusy, attachments: pendingAttachments, onRemoveAttachment: handleRemoveAttachment, prefillMessage: promptPrefill ?? undefined, placeholder: promptPlaceholder, slashCommandItems: slashCommandItems, attachmentAccept: attachmentsDropzoneEnabled ? attachmentAccept : undefined, onAttachmentFiles: attachmentsDropzoneEnabled ? handleAttachmentFiles : undefined })] })] })] })] }));
76
+ }), showSyntheticBranchButtons ? (jsx("div", { className: S.branchRow, children: unusedBranchKeys.map(key => {
77
+ const label = displayLabelForBranchKeyFromMessages(key, messages) ??
78
+ humanizeBranchKey(key);
79
+ return (jsx("span", { className: S.branchBtnWrap, children: jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: isLoading, onClick: () => onQuickReply(key, label), children: [jsx(PaperPlaneRightIcon, {}), label] }) }, key));
80
+ }) })) : null, showInlinePresets && renderPresets('inline'), isLoading && isLastMessageFromUser && (jsx(TextShimmer, { duration: 1, spread: 5, className: S.loader, children: "Thinking..." }))] }) })), jsxs("div", { className: cn(S.footer, footerClassName), children: [isEmpty ? (jsx("div", { className: S.notice, children: "Forecast Assistant can make mistakes." })) : null, jsx(Chat.Prompt, { onSubmit: handlePromptSubmitWithAttachments, disabled: promptBusy, attachments: pendingAttachments, onRemoveAttachment: handleRemoveAttachment, prefillMessage: promptPrefill ?? undefined, placeholder: promptPlaceholder, slashCommandItems: slashCommandItems, onSlashItemCommand: onSlashItemCommand, attachmentAccept: attachmentsDropzoneEnabled ? attachmentAccept : undefined, onAttachmentFiles: attachmentsDropzoneEnabled ? handleAttachmentFiles : undefined })] })] })] })] }));
84
81
  }
85
82
 
86
83
  export { ChatChrome };
@@ -3,8 +3,7 @@ import cn from 'classnames';
3
3
  import { InteractiveContent } from '../../InteractiveContent/InteractiveContent.js';
4
4
  import 'lucide-react';
5
5
  import '../../InteractiveContent/InteractiveContent.styl.js';
6
- import { TextShimmer } from '../../TextShimmer/TextShimmer.js';
7
- import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
6
+ import { MessageRole } from '../Chat.types.js';
8
7
  import { userTextFileAttachmentsFromMessage } from '../userTextFileAttachments.js';
9
8
  import { AgentMessageContent } from './AgentMessageContent.js';
10
9
  import S from './ChatMessage.styl.js';
@@ -16,7 +15,7 @@ function ChatMessage({ role, text, userTextFileAttachments, onQuickReply, suppre
16
15
  });
17
16
  const isAssistant = role === MessageRole.ASSISTANT;
18
17
  const isSystem = role === MessageRole.SYSTEM;
19
- return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: text === GENERATING_DASHBOARD_SYSTEM_TEXT ? (jsx(TextShimmer, { as: "span", children: text })) : (text) })) : isAssistant ? (jsx(AgentMessageContent, { text: text, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: S.text, children: jsx(InteractiveContent, { text: text }) }), fileAttachments.map(attachment => (jsx(UserTextFileAttachmentBubble, { attachment: attachment }, `${attachment.displayName}:${attachment.filename}`)))] })) }));
18
+ return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: text })) : isAssistant ? (jsx(AgentMessageContent, { text: text, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: S.text, children: jsx(InteractiveContent, { text: text }) }), fileAttachments.map(attachment => (jsx(UserTextFileAttachmentBubble, { attachment: attachment }, `${attachment.displayName}:${attachment.filename}`)))] })) }));
20
19
  }
21
20
 
22
21
  export { ChatMessage };
@@ -6,13 +6,14 @@ import { ChatPromptAttachments } from './ChatPromptAttachments.js';
6
6
  import { ChatPromptComposer } from './ChatPromptComposer.js';
7
7
  import { useChatPromptEditor } from './useChatPromptEditor.js';
8
8
 
9
- function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, slashCommandItems, attachments = [], onRemoveAttachment, disabled = false, attachmentAccept, onAttachmentFiles, }) {
9
+ function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, slashCommandItems, onSlashItemCommand, attachments = [], onRemoveAttachment, disabled = false, attachmentAccept, onAttachmentFiles, }) {
10
10
  const attachmentsCount = attachments.length;
11
11
  const emitSubmitRef = useRef(() => { });
12
12
  const { editor, trimmedMessage, resetAfterSend, handleComposerKeyDown } = useChatPromptEditor({
13
13
  disabled,
14
14
  placeholder,
15
15
  slashCommandItems,
16
+ onSlashItemCommand,
16
17
  prefillMessage,
17
18
  attachmentsCount,
18
19
  onEnterSubmit: () => emitSubmitRef.current(),
@@ -7,8 +7,10 @@ import StarterKit from '@tiptap/starter-kit';
7
7
  import { chatPromptSafeEditorDom, syncChatPromptComposerHeight } from './ChatPrompt.helpers.js';
8
8
  import { CHAT_PROMPT_EMPTY_DOC, chatPromptParagraphDoc } from './chatPromptDoc.js';
9
9
 
10
- function useChatPromptEditor({ disabled, placeholder, slashCommandItems, prefillMessage, attachmentsCount = 0, onEnterSubmit, }) {
10
+ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlashItemCommand, prefillMessage, attachmentsCount = 0, onEnterSubmit, }) {
11
11
  const slashOpenRef = useRef(false);
12
+ const onSlashItemCommandRef = useRef(onSlashItemCommand);
13
+ onSlashItemCommandRef.current = onSlashItemCommand;
12
14
  const suggestionActiveUpdater = useCallback((active) => {
13
15
  slashOpenRef.current = active;
14
16
  }, []);
@@ -50,6 +52,7 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, prefill
50
52
  items: slashItemsStable,
51
53
  suggestionPlacement: 'above',
52
54
  onSuggestionUiActiveChange: suggestionActiveUpdater,
55
+ onItemCommand: ctx => onSlashItemCommandRef.current?.(ctx) === true,
53
56
  }));
54
57
  }
55
58
  return exts;
@@ -86,7 +89,7 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, prefill
86
89
  setPlainDraft(ed.getText());
87
90
  },
88
91
  onCreate: bindEditorDom,
89
- }, [extensions, disabled, bindEditorDom, ariaLabelComposer]);
92
+ }, [extensions, bindEditorDom, ariaLabelComposer]);
90
93
  useEffect(() => {
91
94
  if (!editor)
92
95
  return;
@@ -4,20 +4,20 @@ import { Button } from '../../Button/Button.js';
4
4
  import { ChatChrome } from '../ChatChrome/ChatChrome.js';
5
5
  import { useChatPanelChromeModel } from './useChatPanelChromeModel.js';
6
6
 
7
- function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, inline = false, }) {
7
+ function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, inline = false, }) {
8
8
  const model = useChatPanelChromeModel({
9
9
  embedAsPage: inline,
10
10
  presets,
11
11
  scopeId,
12
12
  onMessage,
13
13
  onScriptComplete,
14
- onGenerateDashboard,
15
14
  renderMessageChart,
16
15
  emptyState,
17
16
  allowedAttachments,
18
17
  allowPdfAttachments,
19
18
  onAttachmentsDropped,
20
19
  slashCommandItems,
20
+ onSlashItemCommand,
21
21
  });
22
22
  if (actionsRef) {
23
23
  actionsRef.current = {
@@ -1,9 +1,9 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
3
- import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
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
5
  import { buildChatSendMessagePayload, displayTextFromSendPayload } from '../buildChatSendMessagePayload.js';
6
- import { usedPresetIdsFromMessages, formatChatTranscript } from '../chat-preset-utils.js';
6
+ import { usedPresetIdsFromMessages } from '../chat-preset-utils.js';
7
7
  import { useChatsForScopeId, useChat, useChatOutboundPending, useSyncChatPanelBusy, isChatEmpty } from '../../../../contexts/chat-context.js';
8
8
  import useEvent from '../../../../hooks/useEvent.js';
9
9
  import { useIsMobile } from '../../../../hooks/useIsMobile.js';
@@ -22,7 +22,7 @@ const CHAT_NAV_COLLAPSE_BREAKPOINT_PX = 1400;
22
22
  const CHAT_QUERY_PARAM = 'chat';
23
23
  const CHAT_OPEN_VALUE = 'open';
24
24
  const PROMPT_QUERY_PARAM = 'prompt';
25
- function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, }) {
25
+ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, }) {
26
26
  const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
27
27
  const isMobile = useIsMobile();
28
28
  const { chatPanelContainer, isOpen: sidebarNavOpen, setOpen: setSidebarNavOpen, chatWidthPx, setChatWidthPx, getShellWidth, setChatPanelOpen, } = useSidebar();
@@ -58,9 +58,8 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
58
58
  const [quickReplyBranchesByChat, setQuickReplyBranchesByChat] = useState({});
59
59
  const [usedScriptBranchKeysByChat, setUsedScriptBranchKeysByChat] = useState({});
60
60
  const [intakeByChatId, setIntakeByChatId] = useState({});
61
- /** Preset intake finished for this session; show [Generate Dashboard] when `onGenerateDashboard` is set. */
61
+ /** Preset intake finished for this session (e.g. `onScriptComplete` callback). */
62
62
  const [scriptCompleteByChatId, setScriptCompleteByChatId] = useState({});
63
- const [generatingDashboard, setGeneratingDashboard] = useState(false);
64
63
  const scriptAdvanceLockRef = useRef(false);
65
64
  const quickReplyLockRef = useRef(false);
66
65
  const scrollRef = useRef(null);
@@ -712,37 +711,14 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
712
711
  lastMsg?.role === MessageRole.ASSISTANT &&
713
712
  !linearScriptActive &&
714
713
  (!graphActive || (!lastHasQuickMarkers && unusedBranchKeys.length === 0));
715
- const isScriptComplete = Boolean(currentChatId && scriptCompleteByChatId[currentChatId]);
716
- /** Branch row also when intake is done but all branches used (Generate Dashboard only). */
717
- const showBranchActionsRow = showSyntheticBranchButtons ||
718
- (isScriptComplete && Boolean(onGenerateDashboard) && !generatingDashboard);
719
- const handleGenerateDashboard = useCallback(async () => {
720
- if (!currentChatId || !onGenerateDashboard)
714
+ const onPromptSubmitWithSlashCommands = useCallback(async (message, attachments) => {
715
+ const trimmed = message.trim();
716
+ const slashItem = slashCommandItems?.find(item => trimmed === `/${item.id}`);
717
+ if (slashItem && onSlashItemCommand?.({ item: slashItem }) === true) {
721
718
  return;
722
- const transcript = formatChatTranscript((chat?.messages ?? []).filter(m => m.role !== MessageRole.SYSTEM));
723
- setGeneratingDashboard(true);
724
- const systemPlaceholderId = addMessage(currentChatId, MessageRole.SYSTEM, GENERATING_DASHBOARD_SYSTEM_TEXT);
725
- try {
726
- await Promise.resolve(onGenerateDashboard(transcript));
727
- setScriptCompleteByChatId(prev => {
728
- const next = { ...prev };
729
- delete next[currentChatId];
730
- return next;
731
- });
732
719
  }
733
- finally {
734
- if (systemPlaceholderId) {
735
- removeMessageById(currentChatId, systemPlaceholderId);
736
- }
737
- setGeneratingDashboard(false);
738
- }
739
- }, [
740
- currentChatId,
741
- onGenerateDashboard,
742
- chat?.messages,
743
- addMessage,
744
- removeMessageById,
745
- ]);
720
+ return handlePromptSubmit(message, attachments);
721
+ }, [handlePromptSubmit, slashCommandItems, onSlashItemCommand]);
746
722
  const onDragChatWidth = useCallback((rawPx) => setChatWidthPx(rawPx, { persist: false }), [setChatWidthPx]);
747
723
  const onDragChatComplete = useCallback((finalRawPx) => setChatWidthPx(finalRawPx, { persist: true }), [setChatWidthPx]);
748
724
  const chromeProps = {
@@ -766,20 +742,13 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
766
742
  scriptContinueLabel,
767
743
  onScriptContinue,
768
744
  renderMessageChart,
769
- showBranchActionsRow,
770
745
  showSyntheticBranchButtons,
771
746
  unusedBranchKeys,
772
- isScriptComplete,
773
- onGenerateDashboard,
774
- generatingDashboard,
775
- onGenerateDashboardClick: () => {
776
- void handleGenerateDashboard();
777
- },
778
747
  showInlinePresets,
779
748
  isLastMessageFromUser,
780
749
  scrollRef,
781
750
  effectiveScopeId,
782
- onPromptSubmit: handlePromptSubmit,
751
+ onPromptSubmit: onPromptSubmitWithSlashCommands,
783
752
  onChatDeleted: endLocalDemoFlow,
784
753
  promptPrefill: promptLinkPrefill,
785
754
  emptyState: resolvedEmptyState,
@@ -787,6 +756,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
787
756
  allowPdfAttachments,
788
757
  onAttachmentsDropped,
789
758
  slashCommandItems,
759
+ onSlashItemCommand,
790
760
  };
791
761
  const toggleOpen = () => onOpenChange(!isOpen);
792
762
  return {
package/dist/esm/index.js CHANGED
@@ -22,7 +22,7 @@ export { ChartLegend, ChartTooltip } from './components/ui/Chart/Chart.js';
22
22
  export { THEMES } from './components/ui/Chart/Chart.types.js';
23
23
  export { ChartAreaInteractive, chartConfig } from './components/ui/ChartAreaInteractive/ChartAreaInteractive.js';
24
24
  export { Chat } from './components/ui/Chat/Chat.js';
25
- export { usedPresetIdsFromMessages } from './components/ui/Chat/chat-preset-utils.js';
25
+ export { formatChatTranscript, usedPresetIdsFromMessages } from './components/ui/Chat/chat-preset-utils.js';
26
26
  export { ChatChrome } from './components/ui/Chat/ChatChrome/ChatChrome.js';
27
27
  export { TEXT_ATTACHMENT_ACCEPT_PARTS, filterToTextAttachments } from './components/ui/Chat/chatAttachmentAccept.js';
28
28
  export { buildChatSendMessagePayload, displayTextFromSendPayload, normalizeUserTextFileAttachments } from './components/ui/Chat/buildChatSendMessagePayload.js';
@@ -68,6 +68,28 @@ function slashMentionSuggestionRender(uiRef, placement = 'below') {
68
68
  onKeyDown: ({ event }) => uiRef.current?.onKeyboardEvent(event) ?? false,
69
69
  };
70
70
  }
71
+ function clearSlashTriggerEditor(editor, range) {
72
+ if (editor.isDestroyed)
73
+ return;
74
+ try {
75
+ editor.chain().focus().deleteRange(range).clearContent().run();
76
+ }
77
+ catch {
78
+ // Editor view may be tearing down during suggestion exit.
79
+ }
80
+ }
81
+ function collapseEditorSelectionEnd(editor) {
82
+ if (editor.isDestroyed)
83
+ return;
84
+ try {
85
+ editor.view?.dom?.ownerDocument?.defaultView
86
+ ?.getSelection?.()
87
+ ?.collapseToEnd();
88
+ }
89
+ catch {
90
+ // view.dom throws when editor is not mounted
91
+ }
92
+ }
71
93
  function insertDefaultMention(editor, range, props, slashChar) {
72
94
  const nodeAfter = editor.view.state.selection.$to.nodeAfter;
73
95
  const extend = nodeAfter?.text?.startsWith(' ') ? 1 : 0;
@@ -135,11 +157,8 @@ function createSlashMentionExtension({ items: resolvedItems, slashChar = '/', pl
135
157
  command: ({ editor, range, props }) => {
136
158
  const item = props;
137
159
  if (onItemCommand?.({ editor, range, item }) === true) {
138
- queueMicrotask(() => {
139
- editor.view.dom.ownerDocument?.defaultView
140
- ?.getSelection?.()
141
- ?.collapseToEnd();
142
- });
160
+ clearSlashTriggerEditor(editor, range);
161
+ queueMicrotask(() => collapseEditorSelectionEnd(editor));
143
162
  return null;
144
163
  }
145
164
  insertDefaultMention(editor, range, item, slashChar);
@@ -1,13 +1,11 @@
1
1
  import type { HTMLAttributes, ReactNode } from 'react';
2
- import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
2
+ import type { SlashCommandItem, SlashOnItemCommand } from '#uilib/tiptap/slash-mention/types';
3
3
  import type { PresetScriptGraph } from './ChatMessage/presetScript';
4
4
  export declare enum MessageRole {
5
5
  USER = "user",
6
6
  ASSISTANT = "assistant",
7
7
  SYSTEM = "system"
8
8
  }
9
- /** System placeholder while dashboard generation runs (must match ChatSheet `addMessage` text). */
10
- export declare const GENERATING_DASHBOARD_SYSTEM_TEXT = "Generating dashboard\u2026";
11
9
  /** USER-only: text file attached to a message; shown as downloadable file row(s). */
12
10
  export type UserTextFileAttachment = {
13
11
  displayName: string;
@@ -84,6 +82,8 @@ export interface ChatPromptProps {
84
82
  onAttachmentFiles?: (files: File[]) => void;
85
83
  /** Slash menu (`/`); omit or pass empty to disable Mention trigger (no default items). */
86
84
  slashCommandItems?: SlashCommandItem[];
85
+ /** Custom slash pick handler; return true to skip default mention insert. */
86
+ onSlashItemCommand?: SlashOnItemCommand;
87
87
  }
88
88
  export interface ChatMessageProps {
89
89
  role: MessageRole;
@@ -1,2 +1,2 @@
1
1
  import type { ChatChromeProps } from './ChatChrome.types';
2
- export declare function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showBranchActionsRow, showSyntheticBranchButtons, unusedBranchKeys, isScriptComplete, onGenerateDashboard, generatingDashboard, onGenerateDashboardClick, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, promptPlaceholder, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
@@ -2,7 +2,7 @@ import type { RefObject } from 'react';
2
2
  import type { ChatAttachmentDropItem, Message } from '#uilib/components/ui/Chat/Chat.types';
3
3
  import type { ChatEmptyStateProps } from '#uilib/components/ui/Chat/ChatEmptyState/ChatEmptyState.types';
4
4
  import type { ChatPresetsLayout } from '#uilib/components/ui/Chat/ChatPresets';
5
- import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
5
+ import type { SlashCommandItem, SlashOnItemCommand } from '#uilib/tiptap/slash-mention/types';
6
6
  import type { ScrollRef } from '@homecode/ui';
7
7
  export type ChatChromeResizeHandleConfig = {
8
8
  isActive: boolean;
@@ -26,13 +26,8 @@ export interface ChatChromeProps {
26
26
  scriptContinueLabel: string | undefined;
27
27
  onScriptContinue: (() => void) | undefined;
28
28
  renderMessageChart?: () => React.ReactNode;
29
- showBranchActionsRow: boolean;
30
29
  showSyntheticBranchButtons: boolean;
31
30
  unusedBranchKeys: string[];
32
- isScriptComplete: boolean;
33
- onGenerateDashboard: ((transcript: string) => void | Promise<void>) | undefined;
34
- generatingDashboard: boolean;
35
- onGenerateDashboardClick: () => void;
36
31
  showInlinePresets: boolean;
37
32
  isLastMessageFromUser: boolean;
38
33
  scrollRef: RefObject<ScrollRef | null>;
@@ -51,6 +46,8 @@ export interface ChatChromeProps {
51
46
  onAttachmentsDropped?: (items: ChatAttachmentDropItem[]) => void | Promise<void>;
52
47
  /** Slash menu (`/`), forwarded to `Chat.Prompt`; omit or pass empty list to disable slash palette. */
53
48
  slashCommandItems?: SlashCommandItem[];
49
+ /** Custom slash pick handler; forwarded to `Chat.Prompt`. */
50
+ onSlashItemCommand?: SlashOnItemCommand;
54
51
  /** Composer placeholder forwarded to `Chat.Prompt`. */
55
52
  promptPlaceholder?: string;
56
53
  }
@@ -1,2 +1,2 @@
1
1
  import type { ChatPromptProps } from '../Chat.types';
2
- export declare function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, slashCommandItems, attachments, onRemoveAttachment, disabled, attachmentAccept, onAttachmentFiles, }: ChatPromptProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, slashCommandItems, onSlashItemCommand, attachments, onRemoveAttachment, disabled, attachmentAccept, onAttachmentFiles, }: ChatPromptProps): import("react/jsx-runtime").JSX.Element;
@@ -1,10 +1,11 @@
1
1
  import { type KeyboardEvent as ReactKeyboardEvent } from 'react';
2
- import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
2
+ import type { SlashCommandItem, SlashOnItemCommand } from '#uilib/tiptap/slash-mention/types';
3
3
  import type { Editor } from '@tiptap/core';
4
4
  export type UseChatPromptEditorOptions = {
5
5
  disabled: boolean;
6
6
  placeholder?: string;
7
7
  slashCommandItems?: SlashCommandItem[];
8
+ onSlashItemCommand?: SlashOnItemCommand;
8
9
  prefillMessage?: string | null;
9
10
  /** Staged attachment count — Enter-to-send when text empty but files present. */
10
11
  attachmentsCount?: number;
@@ -17,4 +18,4 @@ export type UseChatPromptEditorResult = {
17
18
  resetAfterSend: () => void;
18
19
  handleComposerKeyDown: (event: ReactKeyboardEvent) => void;
19
20
  };
20
- export declare function useChatPromptEditor({ disabled, placeholder, slashCommandItems, prefillMessage, attachmentsCount, onEnterSubmit, }: UseChatPromptEditorOptions): UseChatPromptEditorResult;
21
+ export declare function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlashItemCommand, prefillMessage, attachmentsCount, onEnterSubmit, }: UseChatPromptEditorOptions): UseChatPromptEditorResult;
@@ -19,4 +19,4 @@ export interface ChatSheetProps extends Omit<UseChatPanelChromeModelInput, 'embe
19
19
  */
20
20
  inline?: boolean;
21
21
  }
22
- export declare function ChatSheet({ triggerLabel, triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function ChatSheet({ triggerLabel, triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import { ChatPreset, type ScriptCompletePayload } from '#uilib/components/ui/Chat/Chat.types';
2
- import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
2
+ import type { SlashCommandItem, SlashOnItemCommand } from '#uilib/tiptap/slash-mention/types';
3
3
  import type { ChatChromeProps } from '../ChatChrome';
4
4
  import type { ChatAttachmentDropItem } from '../ChatChrome/ChatChrome.types';
5
5
  import type { ChatEmptyStateConfig } from '../ChatEmptyState/ChatEmptyState.types';
@@ -12,8 +12,6 @@ export type UseChatPanelChromeModelInput = {
12
12
  onMessage?: (message: string) => void;
13
13
  /** Fires when a preset script has no further `[Label|branchKey]` steps (graph leaf or linear script end). */
14
14
  onScriptComplete?: (payload: ScriptCompletePayload) => void;
15
- /** After intake script completes, user can generate a dashboard from the full transcript (e.g. Decision board). */
16
- onGenerateDashboard?: (transcript: string) => void | Promise<void>;
17
15
  /** Renders `[CHART]` tokens in assistant messages. */
18
16
  renderMessageChart?: () => React.ReactNode;
19
17
  /** Forwarded to `ChatChrome` when the thread is empty. */
@@ -25,6 +23,8 @@ export type UseChatPanelChromeModelInput = {
25
23
  onAttachmentsDropped?: (items: ChatAttachmentDropItem[]) => void | Promise<void>;
26
24
  /** Slash menu (`/`) in the composer; omit or pass empty to disable. */
27
25
  slashCommandItems?: SlashCommandItem[];
26
+ /** Custom slash command handler (palette pick or Enter on `/id`). */
27
+ onSlashItemCommand?: SlashOnItemCommand;
28
28
  };
29
29
  export type UseChatPanelChromeModelResult = {
30
30
  chromeProps: ChatChromeProps;
@@ -34,4 +34,4 @@ export type UseChatPanelChromeModelResult = {
34
34
  newChat: () => void;
35
35
  chatPanelContainer: HTMLElement | null;
36
36
  };
37
- export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
37
+ export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
@@ -1,5 +1,5 @@
1
1
  export { Chat } from './Chat';
2
- export { usedPresetIdsFromMessages } from './chat-preset-utils';
2
+ export { formatChatTranscript, usedPresetIdsFromMessages, } from './chat-preset-utils';
3
3
  export { ChatChrome } from './ChatChrome';
4
4
  export type { ChatChromeProps, ChatChromeResizeHandleConfig, } from './ChatChrome';
5
5
  export { TEXT_ATTACHMENT_ACCEPT_PARTS, filterToTextAttachments, } from './chatAttachmentAccept';
@@ -15,5 +15,5 @@ export { ChatPresets } from './ChatPresets';
15
15
  export type { ChatEmptyStateConfig, ChatEmptyStateContext, ChatEmptyStateProps, } from './ChatEmptyState/ChatEmptyState.types';
16
16
  export type { Chat as ChatType, ChatAttachmentDropItem, ChatSendMessagePayload, ChatProps, ChatPreset as ChatPresetType, Message, UserTextFileAttachment, } from './Chat.types';
17
17
  export { MessageRole } from './Chat.types';
18
- export type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
18
+ export type { SlashCommandItem, SlashItemCommandContext, SlashOnItemCommand, } from '#uilib/tiptap/slash-mention/types';
19
19
  export { CsvIcon } from '../../icons/CsvIcon/CsvIcon';
@@ -5,12 +5,14 @@ export type SlashCommandItem = {
5
5
  description?: string;
6
6
  };
7
7
  export type SlashItemCommandContext = {
8
- editor: Editor;
9
- range: Range;
10
8
  item: SlashCommandItem;
9
+ /** Present when picked from the slash palette; omitted on Enter submit of `/id`. */
10
+ editor?: Editor;
11
+ range?: Range;
11
12
  };
12
13
  /**
13
- * If provided, run before default mention insertion. Return true to skip inserting a mention node.
14
+ * If provided, run when a slash item is picked. Return true to skip mention insert
15
+ * (extension clears the trigger text from the composer).
14
16
  */
15
17
  export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
16
18
  /** Where the slash palette opens relative to the caret. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.37",
3
+ "version": "1.3.39",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,9 @@
1
1
  import type { HTMLAttributes, ReactNode } from 'react';
2
2
 
3
- import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
3
+ import type {
4
+ SlashCommandItem,
5
+ SlashOnItemCommand,
6
+ } from '#uilib/tiptap/slash-mention/types';
4
7
 
5
8
  import type { PresetScriptGraph } from './ChatMessage/presetScript';
6
9
 
@@ -10,9 +13,6 @@ export enum MessageRole {
10
13
  SYSTEM = 'system',
11
14
  }
12
15
 
13
- /** System placeholder while dashboard generation runs (must match ChatSheet `addMessage` text). */
14
- export const GENERATING_DASHBOARD_SYSTEM_TEXT = 'Generating dashboard…';
15
-
16
16
  /** USER-only: text file attached to a message; shown as downloadable file row(s). */
17
17
  export type UserTextFileAttachment = {
18
18
  displayName: string;
@@ -96,6 +96,8 @@ export interface ChatPromptProps {
96
96
  onAttachmentFiles?: (files: File[]) => void;
97
97
  /** Slash menu (`/`); omit or pass empty to disable Mention trigger (no default items). */
98
98
  slashCommandItems?: SlashCommandItem[];
99
+ /** Custom slash pick handler; return true to skip default mention insert. */
100
+ onSlashItemCommand?: SlashOnItemCommand;
99
101
  }
100
102
 
101
103
  export interface ChatMessageProps {
@@ -7,7 +7,7 @@ import {
7
7
  } from '#uilib/components/ui/Chat/ChatMessage/presetScript';
8
8
  import { TextShimmer } from '#uilib/components/ui/TextShimmer';
9
9
  import { Scroll } from '@homecode/ui';
10
- import { ChartLineIcon, PaperPlaneRightIcon, X } from '@phosphor-icons/react';
10
+ import { PaperPlaneRightIcon, X } from '@phosphor-icons/react';
11
11
 
12
12
  import { Button } from '../../Button';
13
13
  import { DropZone } from '../../DropZone/DropZone';
@@ -37,13 +37,8 @@ export function ChatChrome({
37
37
  scriptContinueLabel,
38
38
  onScriptContinue,
39
39
  renderMessageChart,
40
- showBranchActionsRow,
41
40
  showSyntheticBranchButtons,
42
41
  unusedBranchKeys,
43
- isScriptComplete,
44
- onGenerateDashboard,
45
- generatingDashboard,
46
- onGenerateDashboardClick,
47
42
  showInlinePresets,
48
43
  isLastMessageFromUser,
49
44
  scrollRef,
@@ -57,6 +52,7 @@ export function ChatChrome({
57
52
  allowPdfAttachments = false,
58
53
  onAttachmentsDropped,
59
54
  slashCommandItems,
55
+ onSlashItemCommand,
60
56
  promptPlaceholder,
61
57
  }: ChatChromeProps) {
62
58
  const filteredAllowedAttachments = useMemo(
@@ -223,48 +219,29 @@ export function ChatChrome({
223
219
  );
224
220
  })}
225
221
 
226
- {showBranchActionsRow && (
222
+ {showSyntheticBranchButtons ? (
227
223
  <div className={S.branchRow}>
228
- {showSyntheticBranchButtons
229
- ? unusedBranchKeys.map(key => {
230
- const label =
231
- displayLabelForBranchKeyFromMessages(
232
- key,
233
- messages,
234
- ) ?? humanizeBranchKey(key);
235
- return (
236
- <span key={key} className={S.branchBtnWrap}>
237
- <Button
238
- type="button"
239
- variant="outline"
240
- size="sm"
241
- disabled={isLoading}
242
- onClick={() => onQuickReply(key, label)}
243
- >
244
- <PaperPlaneRightIcon />
245
- {label}
246
- </Button>
247
- </span>
248
- );
249
- })
250
- : null}
251
-
252
- {isScriptComplete &&
253
- onGenerateDashboard &&
254
- !generatingDashboard ? (
255
- <Button
256
- type="button"
257
- variant="default"
258
- size="lg"
259
- disabled={isLoading}
260
- onClick={onGenerateDashboardClick}
261
- >
262
- <ChartLineIcon />
263
- Generate Dashboard
264
- </Button>
265
- ) : null}
224
+ {unusedBranchKeys.map(key => {
225
+ const label =
226
+ displayLabelForBranchKeyFromMessages(key, messages) ??
227
+ humanizeBranchKey(key);
228
+ return (
229
+ <span key={key} className={S.branchBtnWrap}>
230
+ <Button
231
+ type="button"
232
+ variant="outline"
233
+ size="sm"
234
+ disabled={isLoading}
235
+ onClick={() => onQuickReply(key, label)}
236
+ >
237
+ <PaperPlaneRightIcon />
238
+ {label}
239
+ </Button>
240
+ </span>
241
+ );
242
+ })}
266
243
  </div>
267
- )}
244
+ ) : null}
268
245
 
269
246
  {showInlinePresets && renderPresets('inline')}
270
247
 
@@ -291,6 +268,7 @@ export function ChatChrome({
291
268
  prefillMessage={promptPrefill ?? undefined}
292
269
  placeholder={promptPlaceholder}
293
270
  slashCommandItems={slashCommandItems}
271
+ onSlashItemCommand={onSlashItemCommand}
294
272
  attachmentAccept={
295
273
  attachmentsDropzoneEnabled ? attachmentAccept : undefined
296
274
  }
@@ -6,7 +6,10 @@ import type {
6
6
  } from '#uilib/components/ui/Chat/Chat.types';
7
7
  import type { ChatEmptyStateProps } from '#uilib/components/ui/Chat/ChatEmptyState/ChatEmptyState.types';
8
8
  import type { ChatPresetsLayout } from '#uilib/components/ui/Chat/ChatPresets';
9
- import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
9
+ import type {
10
+ SlashCommandItem,
11
+ SlashOnItemCommand,
12
+ } from '#uilib/tiptap/slash-mention/types';
10
13
  import type { ScrollRef } from '@homecode/ui';
11
14
 
12
15
  export type ChatChromeResizeHandleConfig = {
@@ -33,15 +36,8 @@ export interface ChatChromeProps {
33
36
  scriptContinueLabel: string | undefined;
34
37
  onScriptContinue: (() => void) | undefined;
35
38
  renderMessageChart?: () => React.ReactNode;
36
- showBranchActionsRow: boolean;
37
39
  showSyntheticBranchButtons: boolean;
38
40
  unusedBranchKeys: string[];
39
- isScriptComplete: boolean;
40
- onGenerateDashboard:
41
- | ((transcript: string) => void | Promise<void>)
42
- | undefined;
43
- generatingDashboard: boolean;
44
- onGenerateDashboardClick: () => void;
45
41
  showInlinePresets: boolean;
46
42
  isLastMessageFromUser: boolean;
47
43
  scrollRef: RefObject<ScrollRef | null>;
@@ -65,6 +61,8 @@ export interface ChatChromeProps {
65
61
  ) => void | Promise<void>;
66
62
  /** Slash menu (`/`), forwarded to `Chat.Prompt`; omit or pass empty list to disable slash palette. */
67
63
  slashCommandItems?: SlashCommandItem[];
64
+ /** Custom slash pick handler; forwarded to `Chat.Prompt`. */
65
+ onSlashItemCommand?: SlashOnItemCommand;
68
66
  /** Composer placeholder forwarded to `Chat.Prompt`. */
69
67
  promptPlaceholder?: string;
70
68
  }
@@ -2,12 +2,7 @@ import cn from 'classnames';
2
2
 
3
3
  import { InteractiveContent } from '#uilib/components/ui/InteractiveContent';
4
4
 
5
- import { TextShimmer } from '../../TextShimmer';
6
- import {
7
- type ChatMessageProps,
8
- GENERATING_DASHBOARD_SYSTEM_TEXT,
9
- MessageRole,
10
- } from '../Chat.types';
5
+ import { type ChatMessageProps, MessageRole } from '../Chat.types';
11
6
  import { userTextFileAttachmentsFromMessage } from '../userTextFileAttachments';
12
7
  import { AgentMessageContent } from './AgentMessageContent';
13
8
  import S from './ChatMessage.styl';
@@ -34,13 +29,7 @@ export function ChatMessage({
34
29
  return (
35
30
  <div className={cn(S.root, S[`role-${role}`])}>
36
31
  {isSystem ? (
37
- <div className={S.text}>
38
- {text === GENERATING_DASHBOARD_SYSTEM_TEXT ? (
39
- <TextShimmer as="span">{text}</TextShimmer>
40
- ) : (
41
- text
42
- )}
43
- </div>
32
+ <div className={S.text}>{text}</div>
44
33
  ) : isAssistant ? (
45
34
  <AgentMessageContent
46
35
  text={text}
@@ -14,6 +14,7 @@ export function ChatPrompt({
14
14
  footer,
15
15
  prefillMessage,
16
16
  slashCommandItems,
17
+ onSlashItemCommand,
17
18
  attachments = [],
18
19
  onRemoveAttachment,
19
20
  disabled = false,
@@ -28,6 +29,7 @@ export function ChatPrompt({
28
29
  disabled,
29
30
  placeholder,
30
31
  slashCommandItems,
32
+ onSlashItemCommand,
31
33
  prefillMessage,
32
34
  attachmentsCount,
33
35
  onEnterSubmit: () => emitSubmitRef.current(),
@@ -12,7 +12,10 @@ import {
12
12
  DEFAULT_CHAT_SLASH_ITEMS,
13
13
  createSlashMentionExtension,
14
14
  } from '#uilib/tiptap/slash-mention';
15
- import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
15
+ import type {
16
+ SlashCommandItem,
17
+ SlashOnItemCommand,
18
+ } from '#uilib/tiptap/slash-mention/types';
16
19
  import type { AnyExtension, Editor } from '@tiptap/core';
17
20
  import { Placeholder } from '@tiptap/extensions';
18
21
  import { useEditor } from '@tiptap/react';
@@ -28,6 +31,7 @@ export type UseChatPromptEditorOptions = {
28
31
  disabled: boolean;
29
32
  placeholder?: string;
30
33
  slashCommandItems?: SlashCommandItem[];
34
+ onSlashItemCommand?: SlashOnItemCommand;
31
35
  prefillMessage?: string | null;
32
36
  /** Staged attachment count — Enter-to-send when text empty but files present. */
33
37
  attachmentsCount?: number;
@@ -46,11 +50,14 @@ export function useChatPromptEditor({
46
50
  disabled,
47
51
  placeholder,
48
52
  slashCommandItems,
53
+ onSlashItemCommand,
49
54
  prefillMessage,
50
55
  attachmentsCount = 0,
51
56
  onEnterSubmit,
52
57
  }: UseChatPromptEditorOptions): UseChatPromptEditorResult {
53
58
  const slashOpenRef = useRef(false);
59
+ const onSlashItemCommandRef = useRef(onSlashItemCommand);
60
+ onSlashItemCommandRef.current = onSlashItemCommand;
54
61
  const suggestionActiveUpdater = useCallback((active: boolean) => {
55
62
  slashOpenRef.current = active;
56
63
  }, []);
@@ -96,6 +103,7 @@ export function useChatPromptEditor({
96
103
  items: slashItemsStable,
97
104
  suggestionPlacement: 'above',
98
105
  onSuggestionUiActiveChange: suggestionActiveUpdater,
106
+ onItemCommand: ctx => onSlashItemCommandRef.current?.(ctx) === true,
99
107
  }),
100
108
  );
101
109
  }
@@ -139,7 +147,7 @@ export function useChatPromptEditor({
139
147
  },
140
148
  onCreate: bindEditorDom,
141
149
  },
142
- [extensions, disabled, bindEditorDom, ariaLabelComposer],
150
+ [extensions, bindEditorDom, ariaLabelComposer],
143
151
  );
144
152
 
145
153
  useEffect(() => {
@@ -41,13 +41,13 @@ export function ChatSheet({
41
41
  scopeId,
42
42
  onMessage,
43
43
  onScriptComplete,
44
- onGenerateDashboard,
45
44
  renderMessageChart,
46
45
  emptyState,
47
46
  allowedAttachments,
48
47
  allowPdfAttachments,
49
48
  onAttachmentsDropped,
50
49
  slashCommandItems,
50
+ onSlashItemCommand,
51
51
  inline = false,
52
52
  }: ChatSheetProps) {
53
53
  const model = useChatPanelChromeModel({
@@ -56,13 +56,13 @@ export function ChatSheet({
56
56
  scopeId,
57
57
  onMessage,
58
58
  onScriptComplete,
59
- onGenerateDashboard,
60
59
  renderMessageChart,
61
60
  emptyState,
62
61
  allowedAttachments,
63
62
  allowPdfAttachments,
64
63
  onAttachmentsDropped,
65
64
  slashCommandItems,
65
+ onSlashItemCommand,
66
66
  });
67
67
 
68
68
  if (actionsRef) {
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
 
3
3
  import {
4
4
  ChatPreset,
5
- GENERATING_DASHBOARD_SYSTEM_TEXT,
6
5
  MessageRole,
7
6
  type ScriptCompletePayload,
8
7
  } from '#uilib/components/ui/Chat/Chat.types';
@@ -23,10 +22,7 @@ import {
23
22
  buildChatSendMessagePayload,
24
23
  displayTextFromSendPayload,
25
24
  } from '#uilib/components/ui/Chat/buildChatSendMessagePayload';
26
- import {
27
- formatChatTranscript,
28
- usedPresetIdsFromMessages,
29
- } from '#uilib/components/ui/Chat/chat-preset-utils';
25
+ import { usedPresetIdsFromMessages } from '#uilib/components/ui/Chat/chat-preset-utils';
30
26
  import {
31
27
  isChatEmpty,
32
28
  useChat,
@@ -38,7 +34,10 @@ import useEvent from '#uilib/hooks/useEvent';
38
34
  import { useIsMobile } from '#uilib/hooks/useIsMobile';
39
35
  import { useQueryParams } from '#uilib/hooks/useQueryParams';
40
36
  import logger from '#uilib/lib/logger';
41
- import type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
37
+ import type {
38
+ SlashCommandItem,
39
+ SlashOnItemCommand,
40
+ } from '#uilib/tiptap/slash-mention/types';
42
41
  import { mergePresetFreeformDefaults } from '#uilib/utils/chatPresetMerge';
43
42
  import { ScrollRef } from '@homecode/ui';
44
43
 
@@ -58,8 +57,6 @@ export type UseChatPanelChromeModelInput = {
58
57
  onMessage?: (message: string) => void;
59
58
  /** Fires when a preset script has no further `[Label|branchKey]` steps (graph leaf or linear script end). */
60
59
  onScriptComplete?: (payload: ScriptCompletePayload) => void;
61
- /** After intake script completes, user can generate a dashboard from the full transcript (e.g. Decision board). */
62
- onGenerateDashboard?: (transcript: string) => void | Promise<void>;
63
60
  /** Renders `[CHART]` tokens in assistant messages. */
64
61
  renderMessageChart?: () => React.ReactNode;
65
62
  /** Forwarded to `ChatChrome` when the thread is empty. */
@@ -73,6 +70,8 @@ export type UseChatPanelChromeModelInput = {
73
70
  ) => void | Promise<void>;
74
71
  /** Slash menu (`/`) in the composer; omit or pass empty to disable. */
75
72
  slashCommandItems?: SlashCommandItem[];
73
+ /** Custom slash command handler (palette pick or Enter on `/id`). */
74
+ onSlashItemCommand?: SlashOnItemCommand;
76
75
  };
77
76
 
78
77
  export type UseChatPanelChromeModelResult = {
@@ -115,13 +114,13 @@ export function useChatPanelChromeModel({
115
114
  scopeId,
116
115
  onMessage,
117
116
  onScriptComplete,
118
- onGenerateDashboard,
119
117
  renderMessageChart,
120
118
  emptyState,
121
119
  allowedAttachments,
122
120
  allowPdfAttachments,
123
121
  onAttachmentsDropped,
124
122
  slashCommandItems,
123
+ onSlashItemCommand,
125
124
  }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult {
126
125
  const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
127
126
  const isMobile = useIsMobile();
@@ -193,11 +192,10 @@ export function useChatPanelChromeModel({
193
192
  const [intakeByChatId, setIntakeByChatId] = useState<
194
193
  Record<string, IntakeScriptState | undefined>
195
194
  >({});
196
- /** Preset intake finished for this session; show [Generate Dashboard] when `onGenerateDashboard` is set. */
195
+ /** Preset intake finished for this session (e.g. `onScriptComplete` callback). */
197
196
  const [scriptCompleteByChatId, setScriptCompleteByChatId] = useState<
198
197
  Record<string, boolean>
199
198
  >({});
200
- const [generatingDashboard, setGeneratingDashboard] = useState(false);
201
199
  const scriptAdvanceLockRef = useRef(false);
202
200
  const quickReplyLockRef = useRef(false);
203
201
  const scrollRef = useRef<ScrollRef>(null);
@@ -956,46 +954,19 @@ export function useChatPanelChromeModel({
956
954
  !linearScriptActive &&
957
955
  (!graphActive || (!lastHasQuickMarkers && unusedBranchKeys.length === 0));
958
956
 
959
- const isScriptComplete = Boolean(
960
- currentChatId && scriptCompleteByChatId[currentChatId],
961
- );
962
-
963
- /** Branch row also when intake is done but all branches used (Generate Dashboard only). */
964
- const showBranchActionsRow =
965
- showSyntheticBranchButtons ||
966
- (isScriptComplete && Boolean(onGenerateDashboard) && !generatingDashboard);
967
-
968
- const handleGenerateDashboard = useCallback(async () => {
969
- if (!currentChatId || !onGenerateDashboard) return;
970
- const transcript = formatChatTranscript(
971
- (chat?.messages ?? []).filter(m => m.role !== MessageRole.SYSTEM),
972
- );
973
- setGeneratingDashboard(true);
974
- const systemPlaceholderId = addMessage(
975
- currentChatId,
976
- MessageRole.SYSTEM,
977
- GENERATING_DASHBOARD_SYSTEM_TEXT,
978
- );
979
- try {
980
- await Promise.resolve(onGenerateDashboard(transcript));
981
- setScriptCompleteByChatId(prev => {
982
- const next = { ...prev };
983
- delete next[currentChatId];
984
- return next;
985
- });
986
- } finally {
987
- if (systemPlaceholderId) {
988
- removeMessageById(currentChatId, systemPlaceholderId);
957
+ const onPromptSubmitWithSlashCommands = useCallback(
958
+ async (message: string, attachments?: ChatAttachmentDropItem[]) => {
959
+ const trimmed = message.trim();
960
+ const slashItem = slashCommandItems?.find(
961
+ item => trimmed === `/${item.id}`,
962
+ );
963
+ if (slashItem && onSlashItemCommand?.({ item: slashItem }) === true) {
964
+ return;
989
965
  }
990
- setGeneratingDashboard(false);
991
- }
992
- }, [
993
- currentChatId,
994
- onGenerateDashboard,
995
- chat?.messages,
996
- addMessage,
997
- removeMessageById,
998
- ]);
966
+ return handlePromptSubmit(message, attachments);
967
+ },
968
+ [handlePromptSubmit, slashCommandItems, onSlashItemCommand],
969
+ );
999
970
 
1000
971
  const onDragChatWidth = useCallback(
1001
972
  (rawPx: number) => setChatWidthPx(rawPx, { persist: false }),
@@ -1028,20 +999,13 @@ export function useChatPanelChromeModel({
1028
999
  scriptContinueLabel,
1029
1000
  onScriptContinue,
1030
1001
  renderMessageChart,
1031
- showBranchActionsRow,
1032
1002
  showSyntheticBranchButtons,
1033
1003
  unusedBranchKeys,
1034
- isScriptComplete,
1035
- onGenerateDashboard,
1036
- generatingDashboard,
1037
- onGenerateDashboardClick: () => {
1038
- void handleGenerateDashboard();
1039
- },
1040
1004
  showInlinePresets,
1041
1005
  isLastMessageFromUser,
1042
1006
  scrollRef,
1043
1007
  effectiveScopeId,
1044
- onPromptSubmit: handlePromptSubmit,
1008
+ onPromptSubmit: onPromptSubmitWithSlashCommands,
1045
1009
  onChatDeleted: endLocalDemoFlow,
1046
1010
  promptPrefill: promptLinkPrefill,
1047
1011
  emptyState: resolvedEmptyState,
@@ -1049,6 +1013,7 @@ export function useChatPanelChromeModel({
1049
1013
  allowPdfAttachments,
1050
1014
  onAttachmentsDropped,
1051
1015
  slashCommandItems,
1016
+ onSlashItemCommand,
1052
1017
  };
1053
1018
 
1054
1019
  const toggleOpen = () => onOpenChange(!isOpen);
@@ -1,5 +1,8 @@
1
1
  export { Chat } from './Chat';
2
- export { usedPresetIdsFromMessages } from './chat-preset-utils';
2
+ export {
3
+ formatChatTranscript,
4
+ usedPresetIdsFromMessages,
5
+ } from './chat-preset-utils';
3
6
  export { ChatChrome } from './ChatChrome';
4
7
  export type {
5
8
  ChatChromeProps,
@@ -40,5 +43,9 @@ export type {
40
43
  UserTextFileAttachment,
41
44
  } from './Chat.types';
42
45
  export { MessageRole } from './Chat.types';
43
- export type { SlashCommandItem } from '#uilib/tiptap/slash-mention/types';
46
+ export type {
47
+ SlashCommandItem,
48
+ SlashItemCommandContext,
49
+ SlashOnItemCommand,
50
+ } from '#uilib/tiptap/slash-mention/types';
44
51
  export { CsvIcon } from '../../icons/CsvIcon/CsvIcon';
@@ -120,13 +120,8 @@ export default function ChatAttachmentsDropzonePage() {
120
120
  isLoading={isLoading}
121
121
  scriptContinueLabel={undefined}
122
122
  onScriptContinue={undefined}
123
- showBranchActionsRow={false}
124
123
  showSyntheticBranchButtons={false}
125
124
  unusedBranchKeys={[]}
126
- isScriptComplete={false}
127
- onGenerateDashboard={undefined}
128
- generatingDashboard={false}
129
- onGenerateDashboardClick={() => {}}
130
125
  showInlinePresets={false}
131
126
  isLastMessageFromUser={isLastMessageFromUser}
132
127
  scrollRef={scrollRef}
@@ -84,13 +84,8 @@ export default function ChatPage() {
84
84
  isLoading={isLoading}
85
85
  scriptContinueLabel={undefined}
86
86
  onScriptContinue={undefined}
87
- showBranchActionsRow={false}
88
87
  showSyntheticBranchButtons={false}
89
88
  unusedBranchKeys={[]}
90
- isScriptComplete={false}
91
- onGenerateDashboard={undefined}
92
- generatingDashboard={false}
93
- onGenerateDashboardClick={() => {}}
94
89
  showInlinePresets={false}
95
90
  isLastMessageFromUser={isLastMessageFromUser}
96
91
  scrollRef={scrollRef}
@@ -8,6 +8,7 @@ import {
8
8
  type SlashCommandItem,
9
9
  } from '#uilib/components/ui/Chat';
10
10
  import { PageContentSection } from '#uilib/components/ui/Page';
11
+ import type { SlashItemCommandContext } from '#uilib/tiptap/slash-mention/types';
11
12
  import { ScrollRef } from '@homecode/ui';
12
13
 
13
14
  import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
@@ -15,18 +16,23 @@ import { DocsHeaderActions } from '../docsHeaderActions';
15
16
 
16
17
  const NO_QUICK_REPLY_KEYS: ReadonlySet<string> = new Set();
17
18
 
19
+ const DOCS_SAMPLE_COMMAND_ID = 'sample-command';
20
+
18
21
  /** Sample items so the docs demo still shows a `/` palette (`DEFAULT_CHAT_SLASH_ITEMS` is empty). */
19
22
  const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
20
23
  {
21
- id: 'sample-command',
22
- label: 'sample-command',
24
+ id: DOCS_SAMPLE_COMMAND_ID,
25
+ label: DOCS_SAMPLE_COMMAND_ID,
23
26
  description:
24
- 'Demo onlydefine `slashCommandItems` in your app to list real commands.',
27
+ 'Demo handlerclears composer and runs `onSlashItemCommand` (no mention insert).',
25
28
  },
26
29
  ];
27
30
 
31
+ const SAMPLE_COMMAND_REPLY_TEXT =
32
+ 'Sample command ran via `onSlashItemCommand` — composer cleared, no mention inserted.';
33
+
28
34
  const ASSISTANT_REPLY_TEXT =
29
- 'Demo reply. Picked slash text is `/sample-command` via TipTap Mention (plain-text round-trip with renderText).';
35
+ 'Demo reply for a normal message. Slash picks with a custom handler skip mention insert.';
30
36
 
31
37
  function makeMessage(role: MessageRole, text: string): Message {
32
38
  return {
@@ -56,11 +62,34 @@ export default function ChatSlashCommandsPage() {
56
62
  messages.length > 0 &&
57
63
  messages[messages.length - 1]?.role === MessageRole.USER;
58
64
 
65
+ const runSampleCommand = useCallback(() => {
66
+ setMessages(prev => [
67
+ ...prev,
68
+ makeMessage(MessageRole.ASSISTANT, SAMPLE_COMMAND_REPLY_TEXT),
69
+ ]);
70
+ }, []);
71
+
72
+ const onSlashItemCommand = useCallback(
73
+ ({ item }: SlashItemCommandContext) => {
74
+ if (item.id !== DOCS_SAMPLE_COMMAND_ID) {
75
+ return false;
76
+ }
77
+ queueMicrotask(() => runSampleCommand());
78
+ return true;
79
+ },
80
+ [runSampleCommand],
81
+ );
82
+
59
83
  const onSubmit = useCallback(
60
84
  (raw: string) => {
61
85
  const text = raw.trim();
62
86
  if (!text || isLoading) return;
63
87
 
88
+ if (text === `/${DOCS_SAMPLE_COMMAND_ID}`) {
89
+ runSampleCommand();
90
+ return;
91
+ }
92
+
64
93
  setMessages(prev => [...prev, makeMessage(MessageRole.USER, text)]);
65
94
  setIsLoading(true);
66
95
 
@@ -76,7 +105,7 @@ export default function ChatSlashCommandsPage() {
76
105
  setIsLoading(false);
77
106
  }, 900);
78
107
  },
79
- [isLoading],
108
+ [isLoading, runSampleCommand],
80
109
  );
81
110
 
82
111
  return (
@@ -84,7 +113,7 @@ export default function ChatSlashCommandsPage() {
84
113
  <AppPageHeader
85
114
  breadcrumbs={[{ label: 'Chat' }, { label: 'Chat slash commands' }]}
86
115
  title="Chat slash commands"
87
- subheader={`Slash palette uses TipTap Mention with "/" as the trigger. Library default item list is empty pass slashCommandItems from the app (this page uses a sample item for the demo).`}
116
+ subheader={`Slash palette uses TipTap Mention with "/" as the trigger. Pass slashCommandItems from the app; optional onSlashItemCommand can clear the composer and run an action instead of inserting a mention.`}
88
117
  actions={
89
118
  <DocsHeaderActions slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS} />
90
119
  }
@@ -97,7 +126,9 @@ export default function ChatSlashCommandsPage() {
97
126
  </Link>{' '}
98
127
  shell below: scrolling history, empty state, disclaimer, composer.
99
128
  Type <kbd className="font-mono">/</kbd> at line start or after a
100
- space; pick with arrows + Enter or click, then send.
129
+ space; pick <kbd className="font-mono">sample-command</kbd> to run the
130
+ custom handler, or send{' '}
131
+ <kbd className="font-mono">/sample-command</kbd> with Enter.
101
132
  </p>
102
133
  <ChatChrome
103
134
  showResizeHandle={false}
@@ -111,13 +142,8 @@ export default function ChatSlashCommandsPage() {
111
142
  isLoading={isLoading}
112
143
  scriptContinueLabel={undefined}
113
144
  onScriptContinue={undefined}
114
- showBranchActionsRow={false}
115
145
  showSyntheticBranchButtons={false}
116
146
  unusedBranchKeys={[]}
117
- isScriptComplete={false}
118
- onGenerateDashboard={undefined}
119
- generatingDashboard={false}
120
- onGenerateDashboardClick={() => {}}
121
147
  showInlinePresets={false}
122
148
  isLastMessageFromUser={isLastMessageFromUser}
123
149
  scrollRef={scrollRef}
@@ -125,14 +151,12 @@ export default function ChatSlashCommandsPage() {
125
151
  onPromptSubmit={onSubmit}
126
152
  onChatDeleted={() => {}}
127
153
  slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS}
154
+ onSlashItemCommand={onSlashItemCommand}
128
155
  promptPlaceholder='Ask something or type "/" for demo commands…'
129
156
  emptyState={{
130
157
  title: 'Try a slash command',
131
158
  description:
132
- 'This demo mounts one sample slash item. Production: pass slashCommandItems to ChatChrome so `/` opens your palette.',
133
- additionalContent: (
134
- <p>Optional empty-state slot via additionalContent.</p>
135
- ),
159
+ 'Pick sample-command from the palette or send /sample-command onSlashItemCommand clears the composer and runs the demo action.',
136
160
  }}
137
161
  />
138
162
  </PageContentSection>
@@ -148,13 +148,8 @@ export default function ChatUserCsvAttachmentPage() {
148
148
  isLoading={isLoading}
149
149
  scriptContinueLabel={undefined}
150
150
  onScriptContinue={undefined}
151
- showBranchActionsRow={false}
152
151
  showSyntheticBranchButtons={false}
153
152
  unusedBranchKeys={[]}
154
- isScriptComplete={false}
155
- onGenerateDashboard={undefined}
156
- generatingDashboard={false}
157
- onGenerateDashboardClick={() => {}}
158
153
  showInlinePresets={false}
159
154
  isLastMessageFromUser={isLastMessageFromUser}
160
155
  scrollRef={scrollRef}
@@ -124,6 +124,28 @@ export function slashMentionSuggestionRender(
124
124
  };
125
125
  }
126
126
 
127
+ function clearSlashTriggerEditor(
128
+ editor: Editor,
129
+ range: { from: number; to: number },
130
+ ): void {
131
+ if (editor.isDestroyed) return;
132
+ try {
133
+ editor.chain().focus().deleteRange(range).clearContent().run();
134
+ } catch {
135
+ // Editor view may be tearing down during suggestion exit.
136
+ }
137
+ }
138
+
139
+ function collapseEditorSelectionEnd(editor: Editor): void {
140
+ if (editor.isDestroyed) return;
141
+ try {
142
+ editor.view?.dom?.ownerDocument?.defaultView
143
+ ?.getSelection?.()
144
+ ?.collapseToEnd();
145
+ } catch {
146
+ // view.dom throws when editor is not mounted
147
+ }
148
+ }
127
149
  function insertDefaultMention(
128
150
  editor: Editor,
129
151
  range: { from: number; to: number },
@@ -223,11 +245,8 @@ export function createSlashMentionExtension({
223
245
  command: ({ editor, range, props }) => {
224
246
  const item = props as SlashCommandItem;
225
247
  if (onItemCommand?.({ editor, range, item }) === true) {
226
- queueMicrotask(() => {
227
- editor.view.dom.ownerDocument?.defaultView
228
- ?.getSelection?.()
229
- ?.collapseToEnd();
230
- });
248
+ clearSlashTriggerEditor(editor, range);
249
+ queueMicrotask(() => collapseEditorSelectionEnd(editor));
231
250
  return null;
232
251
  }
233
252
  insertDefaultMention(editor, range, item, slashChar);
@@ -7,13 +7,15 @@ export type SlashCommandItem = {
7
7
  };
8
8
 
9
9
  export type SlashItemCommandContext = {
10
- editor: Editor;
11
- range: Range;
12
10
  item: SlashCommandItem;
11
+ /** Present when picked from the slash palette; omitted on Enter submit of `/id`. */
12
+ editor?: Editor;
13
+ range?: Range;
13
14
  };
14
15
 
15
16
  /**
16
- * If provided, run before default mention insertion. Return true to skip inserting a mention node.
17
+ * If provided, run when a slash item is picked. Return true to skip mention insert
18
+ * (extension clears the trigger text from the composer).
17
19
  */
18
20
  export type SlashOnItemCommand = (ctx: SlashItemCommandContext) => boolean;
19
21