@@ -171,7 +171,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
171171 } )
172172 const valueRef = useRef ( value )
173173 valueRef . current = value
174- const overlayRef = useRef < HTMLDivElement > ( null )
174+ const overlayContentRef = useRef < HTMLDivElement > ( null )
175175 const plusMenuRef = useRef < PlusMenuHandle > ( null )
176176 const skillsMenuRef = useRef < SkillsMenuHandle > ( null )
177177
@@ -477,16 +477,26 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
477477 ]
478478 )
479479
480+ /**
481+ * Mirrors the textarea's scroll offset by translating the overlay content.
482+ * The overlay box itself never scrolls — a scrolled box (even with
483+ * `overflow: hidden`) is a compositor scroll container whose stale textures
484+ * can ghost cleared content; a transform always repaints cleanly and never
485+ * clamps, so the mirror tracks the textarea offset exactly.
486+ */
487+ const syncOverlayScroll = useCallback ( ( scrollTop : number ) => {
488+ const content = overlayContentRef . current
489+ if ( content ) content . style . transform = `translateY(${ - scrollTop } px)`
490+ } , [ ] )
491+
480492 useLayoutEffect ( ( ) => {
481493 const textarea = textareaRef . current
482494 if ( ! textarea ) return
483495 const maxHeight = isInitialView ? window . innerHeight * 0.3 : MAX_CHAT_TEXTAREA_HEIGHT
484496 textarea . style . height = 'auto'
485497 textarea . style . height = `${ Math . min ( textarea . scrollHeight , maxHeight ) } px`
486- if ( overlayRef . current ) {
487- overlayRef . current . scrollTop = textarea . scrollTop
488- }
489- } , [ value , isInitialView , textareaRef ] )
498+ syncOverlayScroll ( textarea . scrollTop )
499+ } , [ value , isInitialView , textareaRef , syncOverlayScroll ] )
490500
491501 const handleResourceSelect = useCallback (
492502 ( resource : MothershipResource ) => {
@@ -1098,12 +1108,9 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
10981108 ( e : React . FormEvent < HTMLTextAreaElement > ) => {
10991109 const maxHeight = isInitialView ? window . innerHeight * 0.3 : MAX_CHAT_TEXTAREA_HEIGHT
11001110 autoResizeTextarea ( e , maxHeight )
1101-
1102- if ( overlayRef . current ) {
1103- overlayRef . current . scrollTop = ( e . target as HTMLTextAreaElement ) . scrollTop
1104- }
1111+ syncOverlayScroll ( ( e . target as HTMLTextAreaElement ) . scrollTop )
11051112 } ,
1106- [ isInitialView ]
1113+ [ isInitialView , syncOverlayScroll ]
11071114 )
11081115
11091116 const handlePaste = useCallback ( ( e : React . ClipboardEvent < HTMLTextAreaElement > ) => {
@@ -1190,11 +1197,12 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
11901197 filesRef . current . processFiles ( dt . files )
11911198 } , [ ] )
11921199
1193- const handleScroll = useCallback ( ( e : React . UIEvent < HTMLTextAreaElement > ) => {
1194- if ( overlayRef . current ) {
1195- overlayRef . current . scrollTop = e . currentTarget . scrollTop
1196- }
1197- } , [ ] )
1200+ const handleScroll = useCallback (
1201+ ( e : React . UIEvent < HTMLTextAreaElement > ) => {
1202+ syncOverlayScroll ( e . currentTarget . scrollTop )
1203+ } ,
1204+ [ syncOverlayScroll ]
1205+ )
11981206
11991207 /**
12001208 * On copy/cut, write a portable representation of the selection to the
@@ -1332,8 +1340,8 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
13321340 />
13331341
13341342 < div className = 'relative' >
1335- < div ref = { overlayRef } className = { OVERLAY_CLASSES } aria-hidden = 'true' >
1336- { overlayContent }
1343+ < div className = { OVERLAY_CLASSES } aria-hidden = 'true' >
1344+ < div ref = { overlayContentRef } > { overlayContent } </ div >
13371345 </ div >
13381346
13391347 < textarea
0 commit comments