-
- Revert to checkpoint? This will restore your workflow to the state saved at this
- checkpoint.{' '}
- This action cannot be undone.
-
-
-
-
-
-
+ {
+ if (!message.contentBlocks || message.contentBlocks.length === 0) return false
+ return message.contentBlocks.some((block) => {
+ if (block.type === 'text') {
+ const parsed = parseSpecialTags(block.content)
+ return parsed.cleanContent.trim().length > 0
+ }
+ return block.type === 'thinking' || block.type === 'tool_call'
+ })
+ }, [message.contentBlocks])
+
+ return {
+ hasVisibleContent,
+ }
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts
index bdef4401b..940501ac6 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts
@@ -2,11 +2,18 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
-import type { CopilotMessage } from '@/stores/panel'
+import type { ChatContext, CopilotMessage, MessageFileAttachment } from '@/stores/panel'
import { useCopilotStore } from '@/stores/panel'
const logger = createLogger('useMessageEditing')
+/**
+ * Ref interface for UserInput component
+ */
+interface UserInputRef {
+ focus: () => void
+}
+
/**
* Message truncation height in pixels
*/
@@ -32,8 +39,8 @@ interface UseMessageEditingProps {
setShowCheckpointDiscardModal: (show: boolean) => void
pendingEditRef: React.MutableRefObject<{
message: string
- fileAttachments?: any[]
- contexts?: any[]
+ fileAttachments?: MessageFileAttachment[]
+ contexts?: ChatContext[]
} | null>
/**
* When true, disables the internal document click-outside handler.
@@ -69,7 +76,7 @@ export function useMessageEditing(props: UseMessageEditingProps) {
const editContainerRef = useRef
(null)
const messageContentRef = useRef(null)
- const userInputRef = useRef(null)
+ const userInputRef = useRef(null)
const { sendMessage, isSendingMessage, abortMessage, currentChat } = useCopilotStore()
@@ -121,7 +128,11 @@ export function useMessageEditing(props: UseMessageEditingProps) {
* Truncates messages after edited message and resends with same ID
*/
const performEdit = useCallback(
- async (editedMessage: string, fileAttachments?: any[], contexts?: any[]) => {
+ async (
+ editedMessage: string,
+ fileAttachments?: MessageFileAttachment[],
+ contexts?: ChatContext[]
+ ) => {
const currentMessages = messages
const editIndex = currentMessages.findIndex((m) => m.id === message.id)
@@ -134,7 +145,7 @@ export function useMessageEditing(props: UseMessageEditingProps) {
...message,
content: editedMessage,
fileAttachments: fileAttachments || message.fileAttachments,
- contexts: contexts || (message as any).contexts,
+ contexts: contexts || message.contexts,
}
useCopilotStore.setState({ messages: [...truncatedMessages, updatedMessage] })
@@ -153,7 +164,7 @@ export function useMessageEditing(props: UseMessageEditingProps) {
timestamp: m.timestamp,
...(m.contentBlocks && { contentBlocks: m.contentBlocks }),
...(m.fileAttachments && { fileAttachments: m.fileAttachments }),
- ...((m as any).contexts && { contexts: (m as any).contexts }),
+ ...(m.contexts && { contexts: m.contexts }),
})),
}),
})
@@ -164,7 +175,7 @@ export function useMessageEditing(props: UseMessageEditingProps) {
await sendMessage(editedMessage, {
fileAttachments: fileAttachments || message.fileAttachments,
- contexts: contexts || (message as any).contexts,
+ contexts: contexts || message.contexts,
messageId: message.id,
queueIfBusy: false,
})
@@ -178,7 +189,11 @@ export function useMessageEditing(props: UseMessageEditingProps) {
* Checks for checkpoints and shows confirmation if needed
*/
const handleSubmitEdit = useCallback(
- async (editedMessage: string, fileAttachments?: any[], contexts?: any[]) => {
+ async (
+ editedMessage: string,
+ fileAttachments?: MessageFileAttachment[],
+ contexts?: ChatContext[]
+ ) => {
if (!editedMessage.trim()) return
if (isSendingMessage) {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts
new file mode 100644
index 000000000..d2cf90344
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts
@@ -0,0 +1 @@
+export * from './copilot-message'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts
index 3ac19aac2..632dae142 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts
@@ -1,7 +1,8 @@
-export * from './copilot-message/copilot-message'
-export * from './plan-mode-section/plan-mode-section'
-export * from './queued-messages/queued-messages'
-export * from './todo-list/todo-list'
-export * from './tool-call/tool-call'
-export * from './user-input/user-input'
-export * from './welcome/welcome'
+export * from './chat-history-skeleton'
+export * from './copilot-message'
+export * from './plan-mode-section'
+export * from './queued-messages'
+export * from './todo-list'
+export * from './tool-call'
+export * from './user-input'
+export * from './welcome'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts
new file mode 100644
index 000000000..fb80d1dda
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts
@@ -0,0 +1 @@
+export * from './plan-mode-section'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx
index cadc5f1e8..c4f17704f 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx
@@ -29,7 +29,7 @@ import { Check, GripHorizontal, Pencil, X } from 'lucide-react'
import { Button, Textarea } from '@/components/emcn'
import { Trash } from '@/components/emcn/icons/trash'
import { cn } from '@/lib/core/utils/cn'
-import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
+import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
/**
* Shared border and background styles
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/index.ts
new file mode 100644
index 000000000..498f56dfb
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/index.ts
@@ -0,0 +1 @@
+export * from './queued-messages'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/index.ts
new file mode 100644
index 000000000..4f6ca9852
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/index.ts
@@ -0,0 +1 @@
+export * from './todo-list'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/index.ts
new file mode 100644
index 000000000..0269fca39
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/index.ts
@@ -0,0 +1 @@
+export * from './tool-call'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
index bba129df7..f526b6ac7 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
@@ -15,7 +15,7 @@ import {
hasInterrupt as hasInterruptFromConfig,
isSpecialTool as isSpecialToolFromConfig,
} from '@/lib/copilot/tools/client/ui-config'
-import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
+import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming'
import { ThinkingBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block'
import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/attached-files-display/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/attached-files-display/index.ts
new file mode 100644
index 000000000..ef4e37411
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/attached-files-display/index.ts
@@ -0,0 +1 @@
+export * from './attached-files-display'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx
new file mode 100644
index 000000000..f01b583c8
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx
@@ -0,0 +1,127 @@
+'use client'
+
+import { ArrowUp, Image, Loader2 } from 'lucide-react'
+import { Badge, Button } from '@/components/emcn'
+import { cn } from '@/lib/core/utils/cn'
+import { ModeSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/mode-selector'
+import { ModelSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/model-selector'
+
+interface BottomControlsProps {
+ mode: 'ask' | 'build' | 'plan'
+ onModeChange?: (mode: 'ask' | 'build' | 'plan') => void
+ selectedModel: string
+ onModelSelect: (model: string) => void
+ isNearTop: boolean
+ disabled: boolean
+ hideModeSelector: boolean
+ canSubmit: boolean
+ isLoading: boolean
+ isAborting: boolean
+ showAbortButton: boolean
+ onSubmit: () => void
+ onAbort: () => void
+ onFileSelect: () => void
+}
+
+/**
+ * Bottom controls section of the user input
+ * Contains mode selector, model selector, file attachment button, and submit/abort buttons
+ */
+export function BottomControls({
+ mode,
+ onModeChange,
+ selectedModel,
+ onModelSelect,
+ isNearTop,
+ disabled,
+ hideModeSelector,
+ canSubmit,
+ isLoading,
+ isAborting,
+ showAbortButton,
+ onSubmit,
+ onAbort,
+ onFileSelect,
+}: BottomControlsProps) {
+ return (
+
+ {/* Left side: Mode Selector + Model Selector */}
+
+ {!hideModeSelector && (
+
+ )}
+
+
+
+
+ {/* Right side: Attach Button + Send Button */}
+
+
+
+
+
+ {showAbortButton ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/index.ts
new file mode 100644
index 000000000..7a0d61937
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/index.ts
@@ -0,0 +1 @@
+export * from './bottom-controls'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-pills/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-pills/index.ts
new file mode 100644
index 000000000..09b096342
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-pills/index.ts
@@ -0,0 +1 @@
+export * from './context-pills'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts
index 1d0da42d4..acea9d9b2 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts
@@ -1,6 +1,7 @@
-export { AttachedFilesDisplay } from './attached-files-display/attached-files-display'
-export { ContextPills } from './context-pills/context-pills'
-export { type MentionFolderNav, MentionMenu } from './mention-menu/mention-menu'
-export { ModeSelector } from './mode-selector/mode-selector'
-export { ModelSelector } from './model-selector/model-selector'
-export { type SlashFolderNav, SlashMenu } from './slash-menu/slash-menu'
+export { AttachedFilesDisplay } from './attached-files-display'
+export { BottomControls } from './bottom-controls'
+export { ContextPills } from './context-pills'
+export { type MentionFolderNav, MentionMenu } from './mention-menu'
+export { ModeSelector } from './mode-selector'
+export { ModelSelector } from './model-selector'
+export { type SlashFolderNav, SlashMenu } from './slash-menu'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/index.ts
new file mode 100644
index 000000000..9a6118aee
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/index.ts
@@ -0,0 +1 @@
+export * from './mention-menu'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/index.ts
new file mode 100644
index 000000000..119be9858
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/index.ts
@@ -0,0 +1 @@
+export * from './mode-selector'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/index.ts
new file mode 100644
index 000000000..9bd04ddd9
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/index.ts
@@ -0,0 +1 @@
+export * from './model-selector'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/index.ts
new file mode 100644
index 000000000..7b7f088ca
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/index.ts
@@ -0,0 +1 @@
+export * from './slash-menu'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts
index 858a39c13..af44a7a4e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts
@@ -5,5 +5,6 @@ export { useMentionData } from './use-mention-data'
export { useMentionInsertHandlers } from './use-mention-insert-handlers'
export { useMentionKeyboard } from './use-mention-keyboard'
export { useMentionMenu } from './use-mention-menu'
+export { useMentionSystem } from './use-mention-system'
export { useMentionTokens } from './use-mention-tokens'
export { useTextareaAutoResize } from './use-textarea-auto-resize'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts
index 90b5d6bc9..819d0ef5d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import {
+ escapeRegex,
filterOutContext,
isContextAlreadySelected,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils'
@@ -22,9 +23,6 @@ interface UseContextManagementProps {
export function useContextManagement({ message, initialContexts }: UseContextManagementProps) {
const [selectedContexts, setSelectedContexts] = useState(initialContexts ?? [])
const initializedRef = useRef(false)
- const escapeRegex = useCallback((value: string) => {
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- }, [])
// Initialize with initial contexts when they're first provided (for edit mode)
useEffect(() => {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-system.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-system.ts
new file mode 100644
index 000000000..d67a1d22c
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-system.ts
@@ -0,0 +1,107 @@
+import type { MentionFolderNav } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components'
+import { useContextManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management'
+import { useFileAttachments } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'
+import { useMentionData } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data'
+import { useMentionInsertHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-insert-handlers'
+import { useMentionKeyboard } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-keyboard'
+import { useMentionMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu'
+import { useMentionTokens } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-tokens'
+import { useTextareaAutoResize } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-textarea-auto-resize'
+import type { ChatContext } from '@/stores/panel'
+
+interface UseMentionSystemProps {
+ message: string
+ setMessage: (message: string) => void
+ workflowId: string | null
+ workspaceId: string
+ userId?: string
+ panelWidth: number
+ disabled: boolean
+ isLoading: boolean
+ inputContainerRef: HTMLDivElement | null
+ initialContexts?: ChatContext[]
+ mentionFolderNav: MentionFolderNav | null
+}
+
+/**
+ * Composite hook that combines all mention-related hooks into a single interface.
+ * Reduces import complexity in components that need full mention functionality.
+ *
+ * @param props - Configuration for all mention system hooks
+ * @returns Combined interface for mention system functionality
+ */
+export function useMentionSystem({
+ message,
+ setMessage,
+ workflowId,
+ workspaceId,
+ userId,
+ panelWidth,
+ disabled,
+ isLoading,
+ inputContainerRef,
+ initialContexts,
+ mentionFolderNav,
+}: UseMentionSystemProps) {
+ const contextManagement = useContextManagement({ message, initialContexts })
+
+ const mentionMenu = useMentionMenu({
+ message,
+ selectedContexts: contextManagement.selectedContexts,
+ onContextSelect: contextManagement.addContext,
+ onMessageChange: setMessage,
+ })
+
+ const mentionTokens = useMentionTokens({
+ message,
+ selectedContexts: contextManagement.selectedContexts,
+ mentionMenu,
+ setMessage,
+ setSelectedContexts: contextManagement.setSelectedContexts,
+ })
+
+ const { overlayRef } = useTextareaAutoResize({
+ message,
+ panelWidth,
+ selectedContexts: contextManagement.selectedContexts,
+ textareaRef: mentionMenu.textareaRef,
+ containerRef: inputContainerRef,
+ })
+
+ const mentionData = useMentionData({
+ workflowId,
+ workspaceId,
+ })
+
+ const fileAttachments = useFileAttachments({
+ userId,
+ disabled,
+ isLoading,
+ })
+
+ const insertHandlers = useMentionInsertHandlers({
+ mentionMenu,
+ workflowId,
+ selectedContexts: contextManagement.selectedContexts,
+ onContextAdd: contextManagement.addContext,
+ mentionFolderNav,
+ })
+
+ const mentionKeyboard = useMentionKeyboard({
+ mentionMenu,
+ mentionData,
+ insertHandlers,
+ mentionFolderNav,
+ })
+
+ return {
+ contextManagement,
+ mentionMenu,
+ mentionTokens,
+ overlayRef,
+ mentionData,
+ fileAttachments,
+ insertHandlers,
+ mentionKeyboard,
+ }
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/index.ts
new file mode 100644
index 000000000..20b536415
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/index.ts
@@ -0,0 +1 @@
+export * from './user-input'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx
index 3636c6b29..ea340ce67 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx
@@ -9,19 +9,19 @@ import {
useState,
} from 'react'
import { createLogger } from '@sim/logger'
-import { ArrowUp, AtSign, Image, Loader2 } from 'lucide-react'
+import { AtSign } from 'lucide-react'
import { useParams } from 'next/navigation'
import { createPortal } from 'react-dom'
import { Badge, Button, Textarea } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
+import type { CopilotModelId } from '@/lib/copilot/models'
import { cn } from '@/lib/core/utils/cn'
import {
AttachedFilesDisplay,
+ BottomControls,
ContextPills,
type MentionFolderNav,
MentionMenu,
- ModelSelector,
- ModeSelector,
type SlashFolderNav,
SlashMenu,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components'
@@ -44,6 +44,10 @@ import {
useTextareaAutoResize,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks'
import type { MessageFileAttachment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'
+import {
+ computeMentionHighlightRanges,
+ extractContextTokens,
+} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils'
import type { ChatContext } from '@/stores/panel'
import { useCopilotStore } from '@/stores/panel'
@@ -306,7 +310,7 @@ const UserInput = forwardRef(
size: f.size,
}))
- onSubmit(trimmedMessage, fileAttachmentsForApi, contextManagement.selectedContexts as any)
+ onSubmit(trimmedMessage, fileAttachmentsForApi, contextManagement.selectedContexts)
const shouldClearInput = clearOnSubmit && !options.preserveInput && !overrideMessage
if (shouldClearInput) {
@@ -657,7 +661,7 @@ const UserInput = forwardRef(
const handleModelSelect = useCallback(
(model: string) => {
- setSelectedModel(model as any)
+ setSelectedModel(model as CopilotModelId)
},
[setSelectedModel]
)
@@ -677,15 +681,17 @@ const UserInput = forwardRef(
return {displayText}
}
- const elements: React.ReactNode[] = []
- const ranges = mentionTokensWithContext.computeMentionRanges()
+ const tokens = extractContextTokens(contexts)
+ const ranges = computeMentionHighlightRanges(message, tokens)
if (ranges.length === 0) {
const displayText = message.endsWith('\n') ? `${message}\u200B` : message
return {displayText}
}
+ const elements: React.ReactNode[] = []
let lastIndex = 0
+
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i]
@@ -694,13 +700,12 @@ const UserInput = forwardRef(
elements.push({before})
}
- const mentionText = message.slice(range.start, range.end)
elements.push(
- {mentionText}
+ {range.token}
)
lastIndex = range.end
@@ -713,7 +718,7 @@ const UserInput = forwardRef(
}
return elements.length > 0 ? elements : {'\u00A0'}
- }, [message, contextManagement.selectedContexts, mentionTokensWithContext])
+ }, [message, contextManagement.selectedContexts])
return (
(
{/* Bottom Row: Mode Selector + Model Selector + Attach Button + Send Button */}
-
- {/* Left side: Mode Selector + Model Selector */}
-
- {!hideModeSelector && (
-
- )}
-
-
-
-
- {/* Right side: Attach Button + Send Button */}
-
-
-
-
-
- {showAbortButton ? (
-
- ) : (
-
- )}
-
-
+ void handleSubmit()}
+ onAbort={handleAbort}
+ onFileSelect={fileAttachments.handleFileSelect}
+ />
{/* Hidden File Input - enabled during streaming so users can prepare images for the next message */}
c.kind !== 'current_workflow' && c.label)
+ .map((c) => {
+ const prefix = c.kind === 'slash_command' ? '/' : '@'
+ return `${prefix}${c.label}`
+ })
+}
+
+/**
+ * Mention range for text highlighting
+ */
+export interface MentionHighlightRange {
+ start: number
+ end: number
+ token: string
+}
+
+/**
+ * Computes mention ranges in text for highlighting
+ * @param text - Text to search
+ * @param tokens - Prefixed tokens to find (e.g., "@workflow", "/web")
+ * @returns Array of ranges with start, end, and matched token
+ */
+export function computeMentionHighlightRanges(
+ text: string,
+ tokens: string[]
+): MentionHighlightRange[] {
+ if (!tokens.length || !text) return []
+
+ const pattern = new RegExp(`(${tokens.map(escapeRegex).join('|')})`, 'g')
+ const ranges: MentionHighlightRange[] = []
+ let match: RegExpExecArray | null
+
+ while ((match = pattern.exec(text)) !== null) {
+ ranges.push({
+ start: match.index,
+ end: match.index + match[0].length,
+ token: match[0],
+ })
+ }
+
+ return ranges
+}
+
+/**
+ * Builds React nodes with highlighted mention tokens
+ * @param text - Text to render
+ * @param contexts - Chat contexts to highlight
+ * @param createHighlightSpan - Function to create highlighted span element
+ * @returns Array of React nodes with highlighted mentions
+ */
+export function buildMentionHighlightNodes(
+ text: string,
+ contexts: ChatContext[],
+ createHighlightSpan: (token: string, key: string) => ReactNode
+): ReactNode[] {
+ const tokens = extractContextTokens(contexts)
+ if (!tokens.length) return [text]
+
+ const ranges = computeMentionHighlightRanges(text, tokens)
+ if (!ranges.length) return [text]
+
+ const nodes: ReactNode[] = []
+ let lastIndex = 0
+
+ for (const range of ranges) {
+ if (range.start > lastIndex) {
+ nodes.push(text.slice(lastIndex, range.start))
+ }
+ nodes.push(createHighlightSpan(range.token, `mention-${range.start}-${range.end}`))
+ lastIndex = range.end
+ }
+
+ if (lastIndex < text.length) {
+ nodes.push(text.slice(lastIndex))
+ }
+
+ return nodes
+}
+
/**
* Gets the data array for a folder ID from mentionData.
* Uses FOLDER_CONFIGS as the source of truth for key mapping.
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/index.ts
new file mode 100644
index 000000000..425106f69
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/index.ts
@@ -0,0 +1 @@
+export * from './welcome'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx
index 03053ccf3..19aeadd24 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx
@@ -24,6 +24,7 @@ import {
import { Trash } from '@/components/emcn/icons/trash'
import { cn } from '@/lib/core/utils/cn'
import {
+ ChatHistorySkeleton,
CopilotMessage,
PlanModeSection,
QueuedMessages,
@@ -40,6 +41,7 @@ import {
useTodoManagement,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks'
import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
+import type { ChatContext } from '@/stores/panel'
import { useCopilotStore } from '@/stores/panel'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -74,10 +76,12 @@ export const Copilot = forwardRef(({ panelWidth }, ref
const copilotContainerRef = useRef(null)
const cancelEditCallbackRef = useRef<(() => void) | null>(null)
const [editingMessageId, setEditingMessageId] = useState(null)
- const [isEditingMessage, setIsEditingMessage] = useState(false)
const [revertingMessageId, setRevertingMessageId] = useState(null)
const [isHistoryDropdownOpen, setIsHistoryDropdownOpen] = useState(false)
+ // Derived state - editing when there's an editingMessageId
+ const isEditingMessage = editingMessageId !== null
+
const { activeWorkflowId } = useWorkflowRegistry()
const {
@@ -106,9 +110,9 @@ export const Copilot = forwardRef(({ panelWidth }, ref
areChatsFresh,
workflowId: copilotWorkflowId,
setPlanTodos,
+ closePlanTodos,
clearPlanArtifact,
savePlanArtifact,
- setSelectedModel,
loadAutoAllowedTools,
} = useCopilotStore()
@@ -292,6 +296,15 @@ export const Copilot = forwardRef(({ panelWidth }, ref
}
}, [abortMessage, showPlanTodos])
+ /**
+ * Handles closing the plan todos section
+ * Calls store action and clears the todos
+ */
+ const handleClosePlanTodos = useCallback(() => {
+ closePlanTodos()
+ setPlanTodos([])
+ }, [closePlanTodos, setPlanTodos])
+
/**
* Handles message submission to the copilot
* @param query - The message text to send
@@ -299,13 +312,12 @@ export const Copilot = forwardRef(({ panelWidth }, ref
* @param contexts - Optional context references
*/
const handleSubmit = useCallback(
- async (query: string, fileAttachments?: MessageFileAttachment[], contexts?: any[]) => {
+ async (query: string, fileAttachments?: MessageFileAttachment[], contexts?: ChatContext[]) => {
// Allow submission even when isSendingMessage - store will queue the message
if (!query || !activeWorkflowId) return
if (showPlanTodos) {
- const store = useCopilotStore.getState()
- store.setPlanTodos([])
+ setPlanTodos([])
}
try {
@@ -319,7 +331,7 @@ export const Copilot = forwardRef(({ panelWidth }, ref
logger.error('Failed to send message:', error)
}
},
- [activeWorkflowId, sendMessage, showPlanTodos]
+ [activeWorkflowId, sendMessage, showPlanTodos, setPlanTodos]
)
/**
@@ -330,7 +342,6 @@ export const Copilot = forwardRef(({ panelWidth }, ref
const handleEditModeChange = useCallback(
(messageId: string, isEditing: boolean, cancelCallback?: () => void) => {
setEditingMessageId(isEditing ? messageId : null)
- setIsEditingMessage(isEditing)
cancelEditCallbackRef.current = isEditing ? cancelCallback || null : null
logger.info('Edit mode changed', { messageId, isEditing, willDimMessages: isEditing })
},
@@ -375,24 +386,6 @@ export const Copilot = forwardRef(({ panelWidth }, ref
[handleHistoryDropdownOpenHook]
)
- /**
- * Skeleton loading component for chat history
- */
- const ChatHistorySkeleton = () => (
- <>
-
-
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
- >
- )
-
return (
<>
(({ panelWidth }, ref
{
- const store = useCopilotStore.getState()
- store.closePlanTodos?.()
- useCopilotStore.setState({ planTodos: [] })
- }}
+ onClose={handleClosePlanTodos}
/>
)}
diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts
index d9cd0aa3e..3a3e8ed11 100644
--- a/apps/sim/stores/panel/copilot/store.ts
+++ b/apps/sim/stores/panel/copilot/store.ts
@@ -1736,8 +1736,13 @@ const sseHandlers: Record = {
}
},
done: (_data, context) => {
+ logger.info('[SSE] DONE EVENT RECEIVED', {
+ doneEventCount: context.doneEventCount,
+ data: _data,
+ })
context.doneEventCount++
if (context.doneEventCount >= 1) {
+ logger.info('[SSE] Setting streamComplete = true, stream will terminate')
context.streamComplete = true
}
},