1212import org .apache .commons .lang3 .StringUtils ;
1313import org .eclipse .e4 .ui .services .IStylingEngine ;
1414import org .eclipse .swt .SWT ;
15+ import org .eclipse .swt .custom .ScrolledComposite ;
1516import org .eclipse .swt .events .MouseAdapter ;
1617import org .eclipse .swt .events .MouseEvent ;
1718import org .eclipse .swt .graphics .Cursor ;
@@ -37,16 +38,22 @@ public class ThinkingBlock extends Composite {
3738 private static final Pattern TITLE_PATTERN =
3839 Pattern .compile ("(?:^|\\ n)\\ *\\ *([^*\\ r\\ n]+?)\\ *\\ *(?=\\ r?\\ n|$)" );
3940
41+ private static final int STREAMING_MAX_HEIGHT = 180 ;
42+
4043 private Composite header ;
4144 private Label iconLabel ;
4245 private Label titleLabel ;
4346 private Label chevronLabel ;
4447
45- /** Body container holding one {@link ThinkingSection} per parsed section. Hidden when collapsed. */
48+ /** Scrollable wrapper around {@link #body}; used only during streaming. Disposed on finalized expand. */
49+ private ScrolledComposite bodyScroller ;
50+ /** Body container holding one {@link ThinkingSection} per parsed section. */
4651 private Composite body ;
4752 private final List <ThinkingSection > sections = new ArrayList <>();
4853 private final StringBuilder textBuffer = new StringBuilder ();
4954 private boolean expanded = true ;
55+ /** Auto-scroll to bottom during streaming. Turned off on any user scroll interaction. */
56+ private boolean autoScroll = true ;
5057
5158 /**
5259 * Lifecycle of the block. Transitions are always forward: STREAMING → (SEALED →)? → COMPLETED|CANCELLED.
@@ -123,6 +130,7 @@ public void showCancelled() {
123130 }
124131 setTitleText (Messages .thinking_cancelledTitle );
125132 setExpanded (false );
133+ unwrapBodyFromScroller ();
126134 state = State .CANCELLED ;
127135 }
128136
@@ -139,6 +147,7 @@ public void markSealed() {
139147 stopSpinner ();
140148 setTitleText (Messages .thinking_completedTitle );
141149 setExpanded (false );
150+ unwrapBodyFromScroller ();
142151 }
143152
144153 /** True only while new thinking stream fragments should still be appended to this block. */
@@ -226,14 +235,26 @@ public void mouseUp(MouseEvent e) {
226235 }
227236
228237 private void createBody () {
229- body = new Composite (this , SWT .NONE );
238+ bodyScroller = new ScrolledComposite (this , SWT .V_SCROLL );
239+ bodyScroller .setExpandHorizontal (true );
240+ bodyScroller .setExpandVertical (true );
241+ bodyScroller .setLayoutData (new GridData (SWT .FILL , SWT .FILL , true , false ));
242+ bodyScroller .setAlwaysShowScrollBars (false );
243+
244+ body = new Composite (bodyScroller , SWT .NONE );
230245 GridLayout bodyLayout = new GridLayout (1 , false );
231246 bodyLayout .marginHeight = 4 ;
232247 bodyLayout .marginLeft = 4 ;
233248 bodyLayout .marginWidth = 0 ;
234249 bodyLayout .verticalSpacing = 6 ;
235250 body .setLayout (bodyLayout );
236- body .setLayoutData (new GridData (SWT .FILL , SWT .FILL , true , false ));
251+
252+ bodyScroller .setContent (body );
253+
254+ // Any user scroll interaction disables auto-scroll unconditionally.
255+ Runnable disableAutoScroll = () -> autoScroll = false ;
256+ bodyScroller .getVerticalBar ().addListener (SWT .Selection , e -> disableAutoScroll .run ());
257+ body .addListener (SWT .MouseVerticalWheel , e -> disableAutoScroll .run ());
237258 }
238259
239260 /** Parsed (title?, body) tuple. */
@@ -270,9 +291,41 @@ private void refreshBody() {
270291 }
271292
272293 body .requestLayout ();
294+ updateScrollerDuringStreaming ();
273295 refreshEnclosingScroller ();
274296 }
275297
298+ /** Resize the scroller to fit content (up to max height) and auto-scroll to bottom if enabled. */
299+ private void updateScrollerDuringStreaming () {
300+ if (bodyScroller == null || bodyScroller .isDisposed ()) {
301+ return ;
302+ }
303+ int contentWidth = bodyScroller .getClientArea ().width ;
304+ if (contentWidth <= 0 ) {
305+ contentWidth = SWT .DEFAULT ;
306+ }
307+ int contentHeight = body .computeSize (contentWidth , SWT .DEFAULT ).y ;
308+ bodyScroller .setMinSize (body .computeSize (contentWidth , SWT .DEFAULT ));
309+
310+ // Grow with content up to max; avoids blank space when content is small.
311+ GridData scrollerData = (GridData ) bodyScroller .getLayoutData ();
312+ int newHint = Math .min (contentHeight , STREAMING_MAX_HEIGHT );
313+ if (scrollerData .heightHint != newHint ) {
314+ scrollerData .heightHint = newHint ;
315+ }
316+
317+ if (state == State .STREAMING && autoScroll ) {
318+ bodyScroller .setOrigin (0 , contentHeight );
319+ } else if (state == State .STREAMING && !autoScroll ) {
320+ // Re-enable auto-scroll if user scrolled back to the bottom.
321+ int scrollPos = bodyScroller .getOrigin ().y ;
322+ int viewportHeight = bodyScroller .getClientArea ().height ;
323+ if (scrollPos + viewportHeight >= contentHeight - 10 ) {
324+ autoScroll = true ;
325+ }
326+ }
327+ }
328+
276329 /**
277330 * Split {@code raw} on standalone {@code **Title**} lines (terminator: newline or end of buffer). Text before any
278331 * title becomes a leading section with {@code title == null}.
@@ -361,17 +414,35 @@ private void toggleExpanded() {
361414
362415 private void setExpanded (boolean newExpanded ) {
363416 this .expanded = newExpanded ;
364- if (body != null && !body .isDisposed ()) {
365- GridData data = (GridData ) body .getLayoutData ();
417+
418+ Composite bodyContainer = bodyScroller != null ? bodyScroller : body ;
419+ if (bodyContainer != null && !bodyContainer .isDisposed ()) {
420+ GridData data = (GridData ) bodyContainer .getLayoutData ();
366421 data .exclude = !expanded ;
367- body .setVisible (expanded );
422+ bodyContainer .setVisible (expanded );
368423 }
369424 updateChevron ();
370425 requestLayout ();
371426 // Refresh the enclosing scroller so the revealed/hidden body height is reachable.
372427 refreshEnclosingScroller ();
373428 }
374429
430+ /** Move body from ScrolledComposite to be a direct child of this block. */
431+ private void unwrapBodyFromScroller () {
432+ if (bodyScroller == null || bodyScroller .isDisposed () || body == null || body .isDisposed ()) {
433+ return ;
434+ }
435+ bodyScroller .setContent (null );
436+ body .setParent (this );
437+ GridData bodyData = new GridData (SWT .FILL , SWT .FILL , true , false );
438+ bodyData .exclude = !expanded ;
439+ body .setLayoutData (bodyData );
440+ body .setVisible (expanded );
441+ body .moveBelow (bodyScroller );
442+ bodyScroller .dispose ();
443+ bodyScroller = null ;
444+ }
445+
375446 private void updateChevron () {
376447 if (chevronLabel == null || chevronLabel .isDisposed ()) {
377448 return ;
0 commit comments