From 3275e9ec7d2f414fb61d21721179e67c7213eddc Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 21 Jan 2026 14:42:35 -0800 Subject: [PATCH 1/2] fix(messages-input): fix cursor alignment and auto-resize with overlay --- .../messages-input/messages-input.tsx | 196 +++++++++++------- 1 file changed, 123 insertions(+), 73 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx index 6ada0e47cb..30b3fa2e94 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx @@ -1,4 +1,12 @@ -import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' +import { + useCallback, + useEffect, + useImperativeHandle, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react' import { isEqual } from 'lodash' import { ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react' import { Button, Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn' @@ -382,93 +390,138 @@ export function MessagesInput({ textareaRefs.current[fieldId]?.focus() }, []) - const autoResizeTextarea = useCallback((fieldId: string) => { + const syncOverlay = useCallback((fieldId: string) => { const textarea = textareaRefs.current[fieldId] - if (!textarea) return const overlay = overlayRefs.current[fieldId] + if (!textarea || !overlay) return - // If user has manually resized, respect their chosen height and only sync overlay. - if (userResizedRef.current[fieldId]) { - const currentHeight = - textarea.offsetHeight || Number.parseFloat(textarea.style.height) || MIN_TEXTAREA_HEIGHT_PX - const clampedHeight = Math.max(MIN_TEXTAREA_HEIGHT_PX, currentHeight) - textarea.style.height = `${clampedHeight}px` + overlay.style.width = `${textarea.clientWidth}px` + overlay.scrollTop = textarea.scrollTop + overlay.scrollLeft = textarea.scrollLeft + }, []) + + const autoResizeTextarea = useCallback( + (fieldId: string) => { + const textarea = textareaRefs.current[fieldId] + const overlay = overlayRefs.current[fieldId] + if (!textarea) return + + if (!textarea.value.trim()) { + userResizedRef.current[fieldId] = false + } + + if (userResizedRef.current[fieldId]) { + if (overlay) { + overlay.style.height = `${textarea.offsetHeight}px` + } + syncOverlay(fieldId) + return + } + + textarea.style.height = 'auto' + const scrollHeight = textarea.scrollHeight + const height = Math.min( + MAX_TEXTAREA_HEIGHT_PX, + Math.max(MIN_TEXTAREA_HEIGHT_PX, scrollHeight) + ) + + textarea.style.height = `${height}px` if (overlay) { - overlay.style.height = `${clampedHeight}px` + overlay.style.height = `${height}px` } - return - } - textarea.style.height = 'auto' - const naturalHeight = textarea.scrollHeight || MIN_TEXTAREA_HEIGHT_PX - const nextHeight = Math.min( - MAX_TEXTAREA_HEIGHT_PX, - Math.max(MIN_TEXTAREA_HEIGHT_PX, naturalHeight) - ) - textarea.style.height = `${nextHeight}px` + syncOverlay(fieldId) + }, + [syncOverlay] + ) - if (overlay) { - overlay.style.height = `${nextHeight}px` - } - }, []) + const handleResizeStart = useCallback( + (fieldId: string, e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() - const handleResizeStart = useCallback((fieldId: string, e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() + const textarea = textareaRefs.current[fieldId] + if (!textarea) return - const textarea = textareaRefs.current[fieldId] - if (!textarea) return + const startHeight = textarea.offsetHeight || textarea.scrollHeight || MIN_TEXTAREA_HEIGHT_PX - const startHeight = textarea.offsetHeight || textarea.scrollHeight || MIN_TEXTAREA_HEIGHT_PX + isResizingRef.current = true + resizeStateRef.current = { + fieldId, + startY: e.clientY, + startHeight, + } - isResizingRef.current = true - resizeStateRef.current = { - fieldId, - startY: e.clientY, - startHeight, - } + const handleMouseMove = (moveEvent: MouseEvent) => { + if (!isResizingRef.current || !resizeStateRef.current) return - const handleMouseMove = (moveEvent: MouseEvent) => { - if (!isResizingRef.current || !resizeStateRef.current) return + const { fieldId: activeFieldId, startY, startHeight } = resizeStateRef.current + const deltaY = moveEvent.clientY - startY + const nextHeight = Math.max(MIN_TEXTAREA_HEIGHT_PX, startHeight + deltaY) - const { fieldId: activeFieldId, startY, startHeight } = resizeStateRef.current - const deltaY = moveEvent.clientY - startY - const nextHeight = Math.max(MIN_TEXTAREA_HEIGHT_PX, startHeight + deltaY) + const activeTextarea = textareaRefs.current[activeFieldId] + const overlay = overlayRefs.current[activeFieldId] - const activeTextarea = textareaRefs.current[activeFieldId] - if (activeTextarea) { - activeTextarea.style.height = `${nextHeight}px` - } + if (activeTextarea) { + activeTextarea.style.height = `${nextHeight}px` + } - const overlay = overlayRefs.current[activeFieldId] - if (overlay) { - overlay.style.height = `${nextHeight}px` + if (overlay) { + overlay.style.height = `${nextHeight}px` + if (activeTextarea) { + overlay.scrollTop = activeTextarea.scrollTop + overlay.scrollLeft = activeTextarea.scrollLeft + } + } } - } - const handleMouseUp = () => { - if (resizeStateRef.current) { - const { fieldId: activeFieldId } = resizeStateRef.current - userResizedRef.current[activeFieldId] = true - } + const handleMouseUp = () => { + if (resizeStateRef.current) { + const { fieldId: activeFieldId } = resizeStateRef.current + userResizedRef.current[activeFieldId] = true + syncOverlay(activeFieldId) + } - isResizingRef.current = false - resizeStateRef.current = null - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - } + isResizingRef.current = false + resizeStateRef.current = null + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - }, []) + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + }, + [syncOverlay] + ) - useEffect(() => { + useLayoutEffect(() => { currentMessages.forEach((_, index) => { - const fieldId = `message-${index}` - autoResizeTextarea(fieldId) + autoResizeTextarea(`message-${index}`) }) }, [currentMessages, autoResizeTextarea]) + useEffect(() => { + const observers: ResizeObserver[] = [] + + for (let i = 0; i < currentMessages.length; i++) { + const fieldId = `message-${i}` + const textarea = textareaRefs.current[fieldId] + const overlay = overlayRefs.current[fieldId] + + if (textarea && overlay) { + const observer = new ResizeObserver(() => { + overlay.style.width = `${textarea.clientWidth}px` + }) + observer.observe(textarea) + observers.push(observer) + } + } + + return () => { + observers.forEach((observer) => observer.disconnect()) + } + }, [currentMessages.length]) + return (
{currentMessages.map((message, index) => ( @@ -621,19 +674,15 @@ export function MessagesInput({
{/* Content Input with overlay for variable highlighting */} -
+