-
{msg.content}
+
+
+ {msg.fileAttachments && msg.fileAttachments.length > 0 && (
+
+
+ {isNarrow ? (
+
+ {msg.fileAttachments.length}
+
+ ) : (
+ <>
+ {msg.fileAttachments[0].filename}
+ {msg.fileAttachments.length > 1 && (
+
+ +{msg.fileAttachments.length - 1}
+
+ )}
+ >
+ )}
+
+ )}
+
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/index.ts
index bcfb231f9ab..7570eb16edc 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/index.ts
@@ -1 +1 @@
-export { UserInput } from './user-input'
+export { UserInput, type UserInputHandle } from './user-input'
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx
index bae0f084183..a243e39055c 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx
@@ -1,7 +1,16 @@
'use client'
import type React from 'react'
-import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
+import {
+ forwardRef,
+ useCallback,
+ useEffect,
+ useImperativeHandle,
+ useLayoutEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
import { useParams } from 'next/navigation'
import { useSession } from '@/lib/auth/auth-client'
import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
@@ -26,6 +35,7 @@ import {
import type {
FileAttachmentForApi,
MothershipResource,
+ QueuedMessage,
} from '@/app/workspace/[workspaceId]/home/types'
import {
useContextManagement,
@@ -91,8 +101,6 @@ function getCaretAnchor(
interface UserInputProps {
defaultValue?: string
- editValue?: string
- onEditValueConsumed?: () => void
onSubmit: (
text: string,
fileAttachments?: FileAttachmentForApi[],
@@ -105,21 +113,28 @@ interface UserInputProps {
onContextAdd?: (context: ChatContext) => void
onContextRemove?: (context: ChatContext) => void
onSendQueuedHead?: () => void
+ onEditQueuedTail?: () => void
+}
+
+export interface UserInputHandle {
+ loadQueuedMessage: (msg: QueuedMessage) => void
}
-export function UserInput({
- defaultValue = '',
- editValue,
- onEditValueConsumed,
- onSubmit,
- isSending,
- onStopGeneration,
- isInitialView = true,
- userId,
- onContextAdd,
- onContextRemove,
- onSendQueuedHead,
-}: UserInputProps) {
+export const UserInput = forwardRef(function UserInput(
+ {
+ defaultValue = '',
+ onSubmit,
+ isSending,
+ onStopGeneration,
+ isInitialView = true,
+ userId,
+ onContextAdd,
+ onContextRemove,
+ onSendQueuedHead,
+ onEditQueuedTail,
+ },
+ ref
+) {
const { workspaceId } = useParams<{ workspaceId: string }>()
const { navigateToSettings } = useSettingsNavigation()
const { data: workflowsById = {} } = useWorkflowMap(workspaceId)
@@ -136,18 +151,6 @@ export function UserInput({
setPrevDefaultValue(defaultValue)
}
- const [prevEditValue, setPrevEditValue] = useState(editValue)
- if (editValue && editValue !== prevEditValue) {
- setPrevEditValue(editValue)
- setValue(editValue)
- } else if (!editValue && prevEditValue) {
- setPrevEditValue(editValue)
- }
-
- useEffect(() => {
- if (editValue) onEditValueConsumed?.()
- }, [editValue, onEditValueConsumed])
-
const files = useFileAttachments({
userId: userId || session?.user?.id,
workspaceId,
@@ -269,6 +272,8 @@ export function UserInput({
contextRef.current = contextManagement
const onSendQueuedHeadRef = useRef(onSendQueuedHead)
onSendQueuedHeadRef.current = onSendQueuedHead
+ const onEditQueuedTailRef = useRef(onEditQueuedTail)
+ onEditQueuedTailRef.current = onEditQueuedTail
const isSendingRef = useRef(isSending)
isSendingRef.current = isSending
@@ -277,6 +282,34 @@ export function UserInput({
const atInsertPosRef = useRef(null)
const pendingCursorRef = useRef(null)
+ useImperativeHandle(
+ ref,
+ () => ({
+ loadQueuedMessage: (msg: QueuedMessage) => {
+ setValue(msg.content)
+ const restored: AttachedFile[] = (msg.fileAttachments ?? []).map((a) => ({
+ id: a.id,
+ name: a.filename,
+ size: a.size,
+ type: a.media_type,
+ path: a.path ?? '',
+ key: a.key,
+ uploading: false,
+ }))
+ files.restoreAttachedFiles(restored)
+ contextManagement.setSelectedContexts(msg.contexts ?? [])
+ requestAnimationFrame(() => {
+ const textarea = textareaRef.current
+ if (!textarea) return
+ textarea.focus()
+ const end = textarea.value.length
+ textarea.setSelectionRange(end, end)
+ })
+ },
+ }),
+ [files.restoreAttachedFiles, contextManagement.setSelectedContexts, textareaRef]
+ )
+
useLayoutEffect(() => {
const textarea = textareaRef.current
if (!textarea) return
@@ -430,6 +463,7 @@ export function UserInput({
filename: f.name,
media_type: f.type,
size: f.size,
+ ...(f.path ? { path: f.path } : {}),
}))
onSubmit(
@@ -452,6 +486,15 @@ export function UserInput({
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
+ if (e.key === 'ArrowUp' && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
+ const isEmpty = valueRef.current.length === 0 && filesRef.current.attachedFiles.length === 0
+ if (isEmpty && onEditQueuedTailRef.current) {
+ e.preventDefault()
+ onEditQueuedTailRef.current()
+ return
+ }
+ }
+
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault()
const hasSubmitPayload =
@@ -763,4 +806,4 @@ export function UserInput({
{files.isDragging && }
)
-}
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx
index d4a3ea7c7c1..5e58f5ca6f4 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx
@@ -2,6 +2,7 @@
import { useMemo } from 'react'
import { useParams } from 'next/navigation'
+import { cn } from '@/lib/core/utils/cn'
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -9,9 +10,17 @@ import { useWorkflows } from '@/hooks/queries/workflows'
const USER_MESSAGE_CLASSES =
'whitespace-pre-wrap break-words [overflow-wrap:anywhere] font-[430] font-[family-name:var(--font-inter)] text-base text-[var(--text-primary)] leading-[23px] tracking-[0] antialiased'
+const COMPACT_CLASSES =
+ 'truncate text-small leading-[20px] font-[430] font-[family-name:var(--font-inter)] text-[var(--text-primary)] tracking-[0] antialiased'
+
interface UserMessageContentProps {
content: string
contexts?: ChatMessageContext[]
+ className?: string
+ /** When true, render mentions as plain inline text (no icon/pill) so truncation flows naturally. */
+ plainMentions?: boolean
+ /** Use compact single-line layout with truncation. */
+ compact?: boolean
}
function escapeRegex(str: string): string {
@@ -64,17 +73,23 @@ function MentionHighlight({ context }: { context: ChatMessageContext }) {
)
}
-export function UserMessageContent({ content, contexts }: UserMessageContentProps) {
+export function UserMessageContent({
+ content,
+ contexts,
+ className,
+ plainMentions = false,
+ compact = false,
+}: UserMessageContentProps) {
const trimmed = content.trim()
+ const classes = cn(compact ? COMPACT_CLASSES : USER_MESSAGE_CLASSES, className)
- if (!contexts || contexts.length === 0) {
- return
{trimmed}
- }
-
- const ranges = computeMentionRanges(content, contexts)
+ const ranges = useMemo(
+ () => (contexts && contexts.length > 0 ? computeMentionRanges(content, contexts) : []),
+ [content, contexts]
+ )
if (ranges.length === 0) {
- return
{trimmed}
+ return
{trimmed}
}
const elements: React.ReactNode[] = []
@@ -88,7 +103,20 @@ export function UserMessageContent({ content, contexts }: UserMessageContentProp
elements.push(
{before})
}
- elements.push(
)
+ if (plainMentions) {
+ elements.push(
+
+ {content.slice(range.start, range.end)}
+
+ )
+ } else {
+ elements.push(
+
+ )
+ }
lastIndex = range.end
}
@@ -97,5 +125,5 @@ export function UserMessageContent({ content, contexts }: UserMessageContentProp
elements.push(
{tail})
}
- return
{elements}
+ return
{elements}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
index 1687a1973d1..5680f8efdd9 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
@@ -159,26 +159,6 @@ export function Home({ chatId }: HomeProps = {}) {
})
)
- const [editingInputValue, setEditingInputValue] = useState('')
- const [prevChatId, setPrevChatId] = useState(chatId)
- const clearEditingValue = useCallback(() => setEditingInputValue(''), [])
-
- // Clear editing value when navigating to a different chat (guarded render-phase update)
- if (chatId !== prevChatId) {
- setPrevChatId(chatId)
- setEditingInputValue('')
- }
-
- const handleEditQueuedMessage = useCallback(
- (id: string) => {
- const msg = editQueuedMessage(id)
- if (msg) {
- setEditingInputValue(msg.content)
- }
- },
- [editQueuedMessage]
- )
-
useEffect(() => {
const url = new URL(window.location.href)
if (activeResourceId) {
@@ -375,13 +355,11 @@ export function Home({ chatId }: HomeProps = {}) {
messageQueue={messageQueue}
onRemoveQueuedMessage={removeFromQueue}
onSendQueuedMessage={sendNow}
- onEditQueuedMessage={handleEditQueuedMessage}
+ onEditQueuedMessage={editQueuedMessage}
userId={session?.user?.id}
chatId={resolvedChatId}
onContextAdd={handleContextAdd}
onWorkspaceResourceSelect={handleWorkspaceResourceSelect}
- editValue={editingInputValue}
- onEditValueConsumed={clearEditingValue}
animateInput={isInputEntering}
onInputAnimationEnd={isInputEntering ? () => setIsInputEntering(false) : undefined}
initialScrollBlocked={resources.length > 0 && isResourceCollapsed}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts
index ce1ef99b203..d41ea9e3d36 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/types.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts
@@ -46,6 +46,7 @@ export interface FileAttachmentForApi {
filename: string
media_type: string
size: number
+ path?: string
}
export interface QueuedMessage {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts
index 7bf0f2079b5..f449413794e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts
@@ -301,6 +301,19 @@ export function useFileAttachments(props: UseFileAttachmentsProps) {
setAttachedFiles([])
}, [attachedFiles])
+ /**
+ * Replaces the current attached files with a given set.
+ * Cleans up preview URLs from the prior set before replacing.
+ */
+ const restoreAttachedFiles = useCallback((files: AttachedFile[]) => {
+ setAttachedFiles((prev) => {
+ prev.forEach((f) => {
+ if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
+ })
+ return files
+ })
+ }, [])
+
return {
// State
attachedFiles,
@@ -321,6 +334,7 @@ export function useFileAttachments(props: UseFileAttachmentsProps) {
handleDragOver,
handleDrop,
clearAttachedFiles,
+ restoreAttachedFiles,
processFiles,
}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
index 8c9fe1ae87d..e881ed300dd 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
@@ -392,17 +392,6 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
wasCopilotSendingRef.current = copilotIsSending
}, [copilotIsSending, loadCopilotChats])
- const [copilotEditingInputValue, setCopilotEditingInputValue] = useState('')
- const clearCopilotEditingValue = useCallback(() => setCopilotEditingInputValue(''), [])
-
- const handleCopilotEditQueuedMessage = useCallback(
- (id: string) => {
- const msg = copilotEditQueuedMessage(id)
- if (msg) setCopilotEditingInputValue(msg.content)
- },
- [copilotEditQueuedMessage]
- )
-
const handleCopilotStopGeneration = useCallback(() => {
captureEvent(posthogRef.current, 'task_generation_aborted', {
workspace_id: workspaceId,
@@ -865,11 +854,9 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
messageQueue={copilotMessageQueue}
onRemoveQueuedMessage={copilotRemoveFromQueue}
onSendQueuedMessage={copilotSendNow}
- onEditQueuedMessage={handleCopilotEditQueuedMessage}
+ onEditQueuedMessage={copilotEditQueuedMessage}
userId={session?.user?.id}
chatId={copilotResolvedChatId}
- editValue={copilotEditingInputValue}
- onEditValueConsumed={clearCopilotEditingValue}
layout='copilot-view'
/>