33 * Licensed under the MIT License. See License.txt in the project root for license information.
44 *--------------------------------------------------------------------------------------------*/
55
6- import { $ , clearNode , getWindow , hide , scheduleAtNextAnimationFrame } from '../../../../../../base/browser/dom.js' ;
6+ import { $ , clearNode , DisposableResizeObserver , getWindow , hide , scheduleAtNextAnimationFrame } from '../../../../../../base/browser/dom.js' ;
77import { alert } from '../../../../../../base/browser/ui/aria/aria.js' ;
88import { DomScrollableElement } from '../../../../../../base/browser/ui/scrollbar/scrollableElement.js' ;
99import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js' ;
@@ -242,10 +242,12 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
242242 private pendingRemovals : { toolCallId : string ; toolLabel : string } [ ] = [ ] ;
243243 private pendingRemovalFlushDisposable : IDisposable | undefined ;
244244 private pendingScrollDisposable : IDisposable | undefined ;
245- private mutationObserverDisposable : IDisposable | undefined ;
245+ private wrapperResizeObserverDisposable : IDisposable | undefined ;
246+ private viewportResizeObserverDisposable : IDisposable | undefined ;
246247 private isUpdatingDimensions : boolean = false ;
247248 private lastKnownContentHeight : number = 0 ;
248249 private lastKnownScrollTop : number = 0 ;
250+ private _cachedViewportWidth : number = 0 ;
249251 private titleShimmerSpan : HTMLElement | undefined ;
250252 private titleDetailContainer : HTMLElement | undefined ;
251253 private readonly _externalResourceWidget : ChatThinkingExternalResourceWidget ;
@@ -476,19 +478,22 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
476478 } ) ) ;
477479 this . _register ( this . scrollableElement . onScroll ( e => this . handleScroll ( e . scrollTop ) ) ) ;
478480
479- // check for content changes to update scroll dimensions
480- const mutationObserver = new MutationObserver ( ( ) => {
481+ // Use a ResizeObserver on the wrapper to detect content growth and drive scrolling.
482+ const wrapperResizeObserver = this . _register ( new DisposableResizeObserver ( ( ) => {
481483 if ( ! this . streamingCompleted && this . domNode . classList . contains ( 'chat-used-context-collapsed' ) ) {
482- this . syncDimensionsAndScheduleScroll ( ) ;
484+ this . updateScrollDimensionsOnResize ( ) ;
483485 }
484- } ) ;
485- mutationObserver . observe ( this . wrapper , {
486- childList : true ,
487- subtree : true ,
488- characterData : true
489- } ) ;
490- this . mutationObserverDisposable = { dispose : ( ) => mutationObserver . disconnect ( ) } ;
491- this . _register ( this . mutationObserverDisposable ) ;
486+ } ) ) ;
487+ this . wrapperResizeObserverDisposable = this . _register ( wrapperResizeObserver . observe ( this . wrapper ) ) ;
488+
489+ // Track viewport width changes (only changes on panel resize, not during streaming)
490+ const viewportResizeObserver = this . _register ( new DisposableResizeObserver ( ( entries ) => {
491+ const entry = entries [ 0 ] ;
492+ if ( entry ) {
493+ this . _cachedViewportWidth = entry . borderBoxSize [ 0 ] ?. inlineSize ?? entry . contentRect . width ;
494+ }
495+ } ) ) ;
496+ this . viewportResizeObserverDisposable = this . _register ( viewportResizeObserver . observe ( this . scrollableElement . getDomNode ( ) ) ) ;
492497
493498 this . _register ( this . _onDidChangeHeight . event ( ( ) => {
494499 this . syncDimensionsAndScheduleScroll ( ) ;
@@ -533,10 +538,8 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
533538 this . domNode . classList . toggle ( 'chat-thinking-fade-bottom' , maxScrollTop > 0 && currentScrollTop < maxScrollTop - 5 ) ;
534539 }
535540
536- // Schedule a batched scroll dimension update for the next animation frame.
537- // All calls during a single frame (from updateThinking, MutationObserver, etc.)
538- // are coalesced into one layout read, avoiding forced synchronous layouts
539- // during tree splice operations.
541+ // Fallback for non-ResizeObserver updates (onDidChangeHeight, initial setup).
542+ // During streaming, ResizeObserver drives updates via updateScrollDimensionsFromCache().
540543 private syncDimensionsAndScheduleScroll ( ) : void {
541544 if ( this . pendingScrollDisposable ) {
542545 return ;
@@ -555,11 +558,54 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
555558 } finally {
556559 this . isUpdatingDimensions = false ;
557560 }
558- // Use the cached values from updateScrollDimensions to avoid extra layout reads
559561 this . updateFadeClasses ( this . lastKnownScrollTop , this . lastKnownContentHeight ) ;
562+ this . updateDropdownClickability ( ) ;
560563 } ) ;
561564 }
562565
566+ /**
567+ * Update scroll dimensions from ResizeObserver callback (post-layout, so reads are cheap).
568+ */
569+ private updateScrollDimensionsOnResize ( ) : void {
570+ if ( ! this . scrollableElement || this . _store . isDisposed ) {
571+ return ;
572+ }
573+
574+ const isCollapsed = this . domNode . classList . contains ( 'chat-used-context-collapsed' ) ;
575+ if ( ! isCollapsed ) {
576+ return ;
577+ }
578+
579+ const contentHeight = this . wrapper . scrollHeight ;
580+ if ( ! contentHeight ) {
581+ return ;
582+ }
583+
584+ this . lastKnownContentHeight = contentHeight ;
585+ const viewportHeight = Math . min ( contentHeight , THINKING_SCROLL_MAX_HEIGHT ) ;
586+
587+ this . isUpdatingDimensions = true ;
588+ try {
589+ this . scrollableElement . setScrollDimensions ( {
590+ width : this . _cachedViewportWidth || this . scrollableElement . getDomNode ( ) . clientWidth ,
591+ scrollWidth : this . wrapper . scrollWidth ,
592+ height : viewportHeight ,
593+ scrollHeight : contentHeight
594+ } ) ;
595+
596+ this . lastKnownScrollTop = this . scrollableElement . getScrollPosition ( ) . scrollTop ;
597+
598+ if ( this . autoScrollEnabled ) {
599+ this . scrollToBottom ( contentHeight ) ;
600+ }
601+ } finally {
602+ this . isUpdatingDimensions = false ;
603+ }
604+
605+ this . updateFadeClasses ( this . lastKnownScrollTop , this . lastKnownContentHeight ) ;
606+ this . updateDropdownClickability ( contentHeight ) ;
607+ }
608+
563609 private updateScrollDimensions ( ) : number | undefined {
564610 if ( ! this . scrollableElement ) {
565611 return undefined ;
@@ -575,7 +621,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
575621 const viewportHeight = Math . min ( contentHeight , THINKING_SCROLL_MAX_HEIGHT ) ;
576622
577623 this . scrollableElement . setScrollDimensions ( {
578- width : this . scrollableElement . getDomNode ( ) . clientWidth ,
624+ width : this . _cachedViewportWidth || this . scrollableElement . getDomNode ( ) . clientWidth ,
579625 scrollWidth : this . wrapper . scrollWidth ,
580626 height : viewportHeight ,
581627 scrollHeight : contentHeight
@@ -617,7 +663,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
617663 const viewportHeight = Math . min ( contentHeight , THINKING_SCROLL_MAX_HEIGHT ) ;
618664
619665 this . scrollableElement . setScrollDimensions ( {
620- width : this . scrollableElement . getDomNode ( ) . clientWidth ,
666+ width : this . _cachedViewportWidth || this . scrollableElement . getDomNode ( ) . clientWidth ,
621667 scrollWidth : this . wrapper . scrollWidth ,
622668 height : viewportHeight ,
623669 scrollHeight : contentHeight
@@ -882,9 +928,14 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
882928 this . domNode . classList . remove ( 'chat-thinking-fade-top' , 'chat-thinking-fade-bottom' ) ;
883929 this . streamingCompleted = true ;
884930
885- if ( this . mutationObserverDisposable ) {
886- this . mutationObserverDisposable . dispose ( ) ;
887- this . mutationObserverDisposable = undefined ;
931+ if ( this . wrapperResizeObserverDisposable ) {
932+ this . wrapperResizeObserverDisposable . dispose ( ) ;
933+ this . wrapperResizeObserverDisposable = undefined ;
934+ }
935+
936+ if ( this . viewportResizeObserverDisposable ) {
937+ this . viewportResizeObserverDisposable . dispose ( ) ;
938+ this . viewportResizeObserverDisposable = undefined ;
888939 }
889940
890941 if ( this . workingSpinnerElement ) {
0 commit comments