@@ -88,6 +88,7 @@ export function LongInput({
8888 const [ cursorPosition , setCursorPosition ] = useState ( 0 )
8989 const textareaRef = useRef < HTMLTextAreaElement > ( null )
9090 const overlayRef = useRef < HTMLDivElement > ( null )
91+ const overlayInnerRef = useRef < HTMLDivElement > ( null )
9192 const [ activeSourceBlockId , setActiveSourceBlockId ] = useState < string | null > ( null )
9293 const containerRef = useRef < HTMLDivElement > ( null )
9394
@@ -140,6 +141,35 @@ export function LongInput({
140141 }
141142 } , [ rows ] )
142143
144+ // Set overlay width to match textarea clientWidth
145+ useLayoutEffect ( ( ) => {
146+ if ( ! textareaRef . current || ! overlayRef . current ) return
147+ const textarea = textareaRef . current
148+ const overlay = overlayRef . current
149+
150+ const applyWidth = ( ) => {
151+ // Match overlay content width to the inner content area of the textarea
152+ overlay . style . width = `${ textarea . clientWidth } px`
153+ }
154+
155+ applyWidth ( )
156+
157+ const resizeObserver = new ResizeObserver ( ( ) => {
158+ applyWidth ( )
159+ } )
160+ resizeObserver . observe ( textarea )
161+
162+ return ( ) => {
163+ resizeObserver . disconnect ( )
164+ }
165+ } , [ ] )
166+
167+ // Initialize overlay transform to current scroll
168+ useLayoutEffect ( ( ) => {
169+ // Initialize overlay transform to current scroll
170+ syncScrollPositions ( )
171+ } , [ ] )
172+
143173 // Handle input changes
144174 const handleChange = ( e : React . ChangeEvent < HTMLTextAreaElement > ) => {
145175 // Don't allow changes if disabled or streaming
@@ -172,19 +202,21 @@ export function LongInput({
172202
173203 // Sync scroll position between textarea and overlay
174204 const handleScroll = ( e : React . UIEvent < HTMLTextAreaElement > ) => {
175- if ( overlayRef . current ) {
176- overlayRef . current . scrollTop = e . currentTarget . scrollTop
177- overlayRef . current . scrollLeft = e . currentTarget . scrollLeft
178- }
205+ if ( ! overlayInnerRef . current ) return
206+ const { scrollTop, scrollLeft } = e . currentTarget
207+ overlayInnerRef . current . style . transform = `translate(${ - scrollLeft } px, ${ - scrollTop } px)`
208+ }
209+
210+ // Force synchronize scroll positions
211+ const syncScrollPositions = ( ) => {
212+ if ( ! textareaRef . current || ! overlayInnerRef . current ) return
213+ const { scrollTop, scrollLeft } = textareaRef . current
214+ overlayInnerRef . current . style . transform = `translate(${ - scrollLeft } px, ${ - scrollTop } px)`
179215 }
180216
181217 // Ensure overlay updates when content changes
182218 useEffect ( ( ) => {
183- if ( textareaRef . current && overlayRef . current ) {
184- // Ensure scrolling is synchronized
185- overlayRef . current . scrollTop = textareaRef . current . scrollTop
186- overlayRef . current . scrollLeft = textareaRef . current . scrollLeft
187- }
219+ syncScrollPositions ( )
188220 } , [ value ] )
189221
190222 // Handle resize functionality
@@ -208,6 +240,8 @@ export function LongInput({
208240 if ( containerRef . current ) {
209241 containerRef . current . style . height = `${ newHeight } px`
210242 }
243+ // Keep overlay aligned with textarea scroll during live resize
244+ syncScrollPositions ( )
211245 }
212246 }
213247
@@ -220,6 +254,8 @@ export function LongInput({
220254 isResizing . current = false
221255 document . removeEventListener ( 'mousemove' , handleMouseMove )
222256 document . removeEventListener ( 'mouseup' , handleMouseUp )
257+ // After resizing completes, re-sync to ensure caret at end remains visually aligned
258+ syncScrollPositions ( )
223259 }
224260
225261 document . addEventListener ( 'mousemove' , handleMouseMove )
@@ -335,9 +371,7 @@ export function LongInput({
335371 }
336372
337373 // For regular scrolling (without Ctrl/Cmd), let the default behavior happen
338- if ( overlayRef . current ) {
339- overlayRef . current . scrollTop = e . currentTarget . scrollTop
340- }
374+ // No overlay scroll; overlay position is synced via transform on scroll handler
341375 }
342376
343377 return (
@@ -365,6 +399,7 @@ export function LongInput({
365399 ref = { textareaRef }
366400 className = { cn (
367401 'allow-scroll min-h-full w-full resize-none text-transparent caret-foreground placeholder:text-muted-foreground/50' ,
402+ '!text-[14px]' , // Force override any responsive text sizes from Textarea component
368403 isConnecting &&
369404 config ?. connectionDroppable !== false &&
370405 'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500' ,
@@ -391,25 +426,69 @@ export function LongInput({
391426 } }
392427 disabled = { isPreview || disabled }
393428 style = { {
394- fontFamily : 'inherit' ,
395- lineHeight : 'inherit' ,
429+ // Explicit font properties for perfect alignment
430+ fontFamily :
431+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' ,
432+ fontSize : '14px' ,
433+ fontWeight : '400' ,
434+ // Match the fixed pixel line-height used on the textarea
435+ lineHeight : '21px' ,
436+ letterSpacing : 'normal' ,
396437 height : `${ height } px` ,
438+ // Text wrapping properties
397439 wordBreak : 'break-word' ,
398440 whiteSpace : 'pre-wrap' ,
441+ overflowWrap : 'break-word' ,
442+ // Box sizing to ensure padding is calculated correctly
443+ boxSizing : 'border-box' ,
444+ // Remove text rendering optimizations that can affect layout
445+ textRendering : 'auto' ,
399446 } }
400447 />
401448 < div
402449 ref = { overlayRef }
403- className = 'pointer-events-none absolute inset-0 whitespace-pre-wrap break-words bg-transparent px-3 py-2 text-sm '
450+ className = 'pointer-events-none absolute bg-transparent'
404451 style = { {
405- fontFamily : 'inherit' ,
406- lineHeight : 'inherit' ,
407- width : '100%' ,
408- height : `${ height } px` ,
452+ // Position exactly over the textarea content area
453+ top : '0' ,
454+ left : '0' ,
455+ // width is set dynamically to match textarea clientWidth to ensure identical wrapping
456+ // right is disabled to avoid conflicts with explicit width
457+ right : 'auto' ,
458+ // Padding: py-2 px-3 = top/bottom: 8px, left/right: 12px
459+ paddingTop : '8px' ,
460+ paddingBottom : '8px' ,
461+ paddingLeft : '12px' ,
462+ paddingRight : '12px' ,
463+ // No border; border would shrink content width under border-box and break wrapping parity
464+ // Exact same font properties as textarea
465+ fontFamily :
466+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' ,
467+ fontSize : '14px' ,
468+ fontWeight : '400' ,
469+ lineHeight : '21px' , // Use fixed pixel line-height to prevent subpixel rounding drift with overlay
470+ letterSpacing : 'normal' ,
471+ // Text wrapping properties - must match textarea exactly
472+ wordBreak : 'break-word' ,
473+ whiteSpace : 'pre-wrap' ,
474+ overflowWrap : 'break-word' ,
475+ // Hide overlay scrolling to avoid dual scroll offsets
409476 overflow : 'hidden' ,
477+ // Box sizing to ensure padding is calculated correctly
478+ boxSizing : 'border-box' ,
479+ // Match text rendering
480+ textRendering : 'auto' ,
410481 } }
411482 >
412- { formatDisplayText ( value ?. toString ( ) ?? '' , true ) }
483+ < div
484+ ref = { overlayInnerRef }
485+ style = { {
486+ willChange : 'transform' ,
487+ lineHeight : '21px' ,
488+ } }
489+ >
490+ { formatDisplayText ( value ?. toString ( ) ?? '' , true ) }
491+ </ div >
413492 </ div >
414493
415494 { /* Wand Button */ }
0 commit comments