@@ -166,13 +166,29 @@ export const ChatRowContent = ({
166166
167167 // Handle edit button click
168168 const handleEditClick = useCallback ( ( ) => {
169- // Pre-scroll the bubble container into view so the textarea can mount fully
169+ // Pre-scroll the bubble container into view so the textarea can mount fully.
170+ // Use center to give Virtuoso more room to render around the target.
170171 try {
171- editAreaRef . current ?. scrollIntoView ( {
172- behavior : "auto" ,
173- block : "nearest" ,
174- inline : "nearest" ,
175- } )
172+ const el = editAreaRef . current
173+ if ( el ) {
174+ el . scrollIntoView ( {
175+ behavior : "auto" ,
176+ block : "center" ,
177+ inline : "nearest" ,
178+ } )
179+ // Safety re-center on the next frame in case virtualization/layout shifts after state updates.
180+ requestAnimationFrame ( ( ) => {
181+ try {
182+ el . scrollIntoView ( {
183+ behavior : "auto" ,
184+ block : "center" ,
185+ inline : "nearest" ,
186+ } )
187+ } catch {
188+ // no-op
189+ }
190+ } )
191+ }
176192 } catch {
177193 // no-op
178194 }
@@ -195,7 +211,7 @@ export const ChatRowContent = ({
195211 } , [ isEditing , message . text , message . images , mode ] )
196212
197213 // Ensure the edit textarea is focused and scrolled into view when entering edit mode.
198- // Uses a short delay and a few animation frames to allow virtualization reflow before scrolling.
214+ // Uses a short delay and repeated frames to allow virtualization reflow before scrolling.
199215 useEffect ( ( ) => {
200216 if ( ! isEditing ) return
201217
@@ -205,8 +221,7 @@ export const ChatRowContent = ({
205221 const getScrollContainer = ( el : HTMLElement | null ) : HTMLElement | null => {
206222 if ( ! el ) return null
207223 // ChatView sets the Virtuoso scroller with class "scrollable"
208- const scroller = el . closest ( ".scrollable" ) as HTMLElement | null
209- return scroller
224+ return el . closest ( ".scrollable" ) as HTMLElement | null
210225 }
211226
212227 const isFullyVisible = ( el : HTMLElement ) : boolean => {
@@ -220,6 +235,22 @@ export const ChatRowContent = ({
220235 return topVisible && bottomVisible
221236 }
222237
238+ const centerInScroller = ( el : HTMLElement ) => {
239+ const scroller = getScrollContainer ( el )
240+ if ( ! scroller ) {
241+ // Fallback to element's own scroll logic
242+ el . scrollIntoView ( { behavior : "auto" , block : "center" , inline : "nearest" } )
243+ return
244+ }
245+ const rect = el . getBoundingClientRect ( )
246+ const containerRect = scroller . getBoundingClientRect ( )
247+ const elCenter = rect . top + rect . height / 2
248+ const containerCenter = containerRect . top + containerRect . height / 2
249+ const delta = elCenter - containerCenter
250+ // Adjust scrollTop by the delta between element center and container center
251+ scroller . scrollTop += delta
252+ }
253+
223254 const focusTextarea = ( ) => {
224255 if ( editTextAreaRef . current ) {
225256 try {
@@ -231,23 +262,35 @@ export const ChatRowContent = ({
231262 }
232263
233264 let attempts = 0
234- const maxAttempts = 10
265+ const maxAttempts = 20 // a bit more robust across topic switches
235266
236267 const step = ( ) => {
237268 if ( cancelled ) return
238269 attempts += 1
239270
240- const targetEl = ( editTextAreaRef . current as HTMLTextAreaElement | null ) ?? editAreaRef . current
271+ const targetEl =
272+ ( editTextAreaRef . current as HTMLTextAreaElement | null ) ?? ( editAreaRef . current as HTMLElement | null )
241273
242274 // Focus first so caret is visible; preventScroll avoids double-jump
243275 focusTextarea ( )
244276
245- if ( targetEl && ! isFullyVisible ( targetEl ) ) {
246- targetEl . scrollIntoView ( {
247- behavior : "smooth" ,
248- block : "center" ,
249- inline : "nearest" ,
250- } )
277+ if ( targetEl ) {
278+ if ( ! isFullyVisible ( targetEl ) ) {
279+ // Prefer centering within the Virtuoso scroller; fallback to native scrollIntoView
280+ try {
281+ centerInScroller ( targetEl )
282+ } catch {
283+ try {
284+ targetEl . scrollIntoView ( {
285+ behavior : "auto" ,
286+ block : "center" ,
287+ inline : "nearest" ,
288+ } )
289+ } catch {
290+ // no-op
291+ }
292+ }
293+ }
251294 }
252295
253296 // Continue for a few frames to account for virtualization/layout reflows
@@ -256,12 +299,12 @@ export const ChatRowContent = ({
256299 }
257300 }
258301
259- // Defer until after the textarea has mounted
302+ // Defer until after the textarea has mounted and Virtuoso has had a moment to lay out
260303 const timeoutId = window . setTimeout ( ( ) => {
261304 if ( ! cancelled ) {
262305 step ( )
263306 }
264- } , 50 )
307+ } , 80 )
265308
266309 return ( ) => {
267310 cancelled = true
0 commit comments