@@ -169,7 +169,7 @@ const ChatMessage = React.memo(function ChatMessage({ sender, message, timestamp
169169 </ div >
170170 { /* Action buttons for AI only, moved below content */ }
171171 { ! isUser && (
172- < div className = "flex items-center gap-1.5 flex-shrink-0 p-2 pt-0 border-t border-border/50 mt-1 pt-1" >
172+ < div className = "flex items-center gap-1.5 flex-shrink-0 p-2 border-t border-border/50 mt-1 pt-1" >
173173 < button
174174 className = { iconButtonStyle }
175175 title = { copied ? t ( 'InstructionModel:copied' ) : t ( 'InstructionModel:copy' ) }
@@ -525,6 +525,7 @@ export default function InstructionModel(properties) {
525525 const PARAGRAPH_SPACING = 10 ; // Extra space for paragraph breaks (\n\n)
526526 const CODE_BLOCK_PADDING = 40 ; // Padding/margin around code block + copy button space
527527 const CODE_LINE_HEIGHT = 20 ; // Approx height of a line within a code block
528+ const ROW_VERTICAL_PADDING = 16 ; // Buffer to avoid cramped layout after measurement
528529
529530 // --- Helper to estimate code block height ---
530531 const estimateCodeBlockHeight = ( codeContent ) => {
@@ -538,23 +539,26 @@ export default function InstructionModel(properties) {
538539 // Handle typing indicator height
539540 if ( index === chatHistory . length ) {
540541 const showTypingIndicator = isAiResponding && chatHistory . length > 0 && chatHistory [ chatHistory . length - 1 ] ?. sender === 'user' ;
541- return showTypingIndicator ? 60 : 0 ; // Adjusted typing indicator height
542+ return showTypingIndicator ? ( messageHeights . current [ index ] ?? 60 ) : 0 ; // Prefer measured height when available
542543 }
543544
544545 // Handle hidden first AI message
545546 if ( index === 0 && chatHistory [ 0 ] ?. sender === 'ai' ) {
546- // Ensure cache is 0 if it wasn't already
547547 if ( messageHeights . current [ index ] !== 0 ) {
548548 messageHeights . current [ index ] = 0 ;
549- // No need to reset here, height is fixed at 0
550549 }
551550 return 0 ;
552551 }
553552
553+ const cachedHeight = messageHeights . current [ index ] ;
554+ if ( cachedHeight != null ) {
555+ return cachedHeight ;
556+ }
557+
554558 // Estimate height based on content for other messages
555559 const messageData = chatHistory [ index ] ;
556560 if ( ! messageData || ! messageData . message ) {
557- return BASE_MESSAGE_HEIGHT ; // Default minimum
561+ return BASE_MESSAGE_HEIGHT + ROW_VERTICAL_PADDING ; // Default minimum
558562 }
559563
560564 const message = messageData . message ;
@@ -594,35 +598,36 @@ export default function InstructionModel(properties) {
594598 estimatedHeight += totalCodeBlockHeight ;
595599
596600 // Use cached height if available and larger than estimate (measurement is king)
597- const cachedHeight = messageHeights . current [ index ] ;
598- if ( cachedHeight && cachedHeight > estimatedHeight ) {
599- return cachedHeight ;
600- }
601-
602- // Return the estimated height, ensuring a minimum
603- // Add a small safety buffer
604- return Math . max ( estimatedHeight , BASE_MESSAGE_HEIGHT ) + 10 ;
601+ // Return the estimated height, ensuring a minimum, plus padding buffer
602+ return Math . max ( estimatedHeight , BASE_MESSAGE_HEIGHT ) + ROW_VERTICAL_PADDING ;
605603
606604 } , [ chatHistory , isAiResponding ] ) ; // Dependencies: chatHistory and isAiResponding
607605
608606 // --- VariableSizeList row measurer ---
609607 const measureRow = useCallback ( ( index , node ) => {
610- if ( node && node . offsetHeight && messageHeights . current [ index ] !== node . offsetHeight ) {
611- // Ensure height is at least a minimum value to avoid collapse issues
612- const newHeight = Math . max ( node . offsetHeight , 20 ) ; // Ensure a minimum height
613- if ( messageHeights . current [ index ] !== newHeight ) {
614- messageHeights . current [ index ] = newHeight ;
615- if ( chatListRef . current ) {
616- // Use requestAnimationFrame to avoid potential state update loops during measurement
617- requestAnimationFrame ( ( ) => {
618- if ( chatListRef . current ) {
619- chatListRef . current . resetAfterIndex ( index , false ) ; // Use false to avoid immediate scroll jump
620- }
621- } ) ;
622- }
608+ if ( ! node ) return ;
609+
610+ // Keep hidden first AI message collapsed
611+ if ( index === 0 && chatHistory [ 0 ] ?. sender === 'ai' ) {
612+ if ( messageHeights . current [ index ] !== 0 ) {
613+ messageHeights . current [ index ] = 0 ;
614+ }
615+ return ;
616+ }
617+
618+ const rawHeight = node . scrollHeight || node . offsetHeight || 0 ;
619+ if ( ! rawHeight ) return ;
620+
621+ const measuredHeight = Math . max ( rawHeight + ROW_VERTICAL_PADDING , BASE_MESSAGE_HEIGHT ) ;
622+ if ( messageHeights . current [ index ] !== measuredHeight ) {
623+ messageHeights . current [ index ] = measuredHeight ;
624+ if ( chatListRef . current ) {
625+ requestAnimationFrame ( ( ) => {
626+ chatListRef . current ?. resetAfterIndex ( index , false ) ;
627+ } ) ;
623628 }
624629 }
625- } , [ ] ) ; // Dependencies removed
630+ } , [ chatHistory ] ) ;
626631
627632 // --- Scroll to bottom if autoScroll ---
628633 useEffect ( ( ) => {
@@ -795,8 +800,8 @@ export default function InstructionModel(properties) {
795800 // 1. Handle initial loading indicator (Only if the very first message is AI and loading)
796801 if ( index === 0 && isInitialAiResponsePhase ) {
797802 return (
798- < div ref = { node => measureRow ( index , node ) } style = { style } >
799- < div className = "flex text-left justify-center gap-2 p-4 text-muted-foreground" >
803+ < div style = { style } >
804+ < div ref = { node => measureRow ( index , node ) } className = "flex text-left justify-center gap-2 p-4 text-muted-foreground" >
800805 < span > { t ( 'InstructionModel:loading' ) } </ span >
801806 </ div >
802807 </ div >
@@ -808,8 +813,10 @@ export default function InstructionModel(properties) {
808813 if ( index === chatHistory . length && showTypingIndicator ) {
809814 // Use a fixed, known key for the typing indicator
810815 return (
811- < div key = "typing-indicator" ref = { node => measureRow ( index , node ) } style = { style } >
812- < ChatMessage sender = "ai" message = { t ( "InstructionModel:typing" ) } timestamp = { new Date ( ) } />
816+ < div key = "typing-indicator" style = { style } >
817+ < div ref = { node => measureRow ( index , node ) } >
818+ < ChatMessage sender = "ai" message = { t ( "InstructionModel:typing" ) } timestamp = { new Date ( ) } />
819+ </ div >
813820 </ div >
814821 ) ;
815822 }
@@ -838,17 +845,19 @@ export default function InstructionModel(properties) {
838845 const key = `${ msg . sender } -${ msg . timestamp ?. getTime ( ) || index } -${ index } ` ;
839846
840847 return (
841- < div key = { key } ref = { node => measureRow ( index , node ) } style = { style } >
842- < ChatMessage
843- sender = { msg . sender }
844- message = { msg . message }
845- timestamp = { msg . timestamp }
846- canRegenerate = { canRegen }
847- onRegenerate = { ( ) => handleRegenerate ( index ) }
848- onDelete = { ( ) => handleDeleteMessage ( index ) }
849- // Pass isFirstAiMessage prop if needed by ChatMessage, though it's unused currently
850- // isFirstAiMessage={index === firstAiIndex}
851- />
848+ < div key = { key } style = { style } >
849+ < div ref = { node => measureRow ( index , node ) } >
850+ < ChatMessage
851+ sender = { msg . sender }
852+ message = { msg . message }
853+ timestamp = { msg . timestamp }
854+ canRegenerate = { canRegen }
855+ onRegenerate = { ( ) => handleRegenerate ( index ) }
856+ onDelete = { ( ) => handleDeleteMessage ( index ) }
857+ // Pass isFirstAiMessage prop if needed by ChatMessage, though it's unused currently
858+ // isFirstAiMessage={index === firstAiIndex}
859+ />
860+ </ div >
852861 </ div >
853862 ) ;
854863 }
0 commit comments