3131#include < QMouseEvent>
3232#include < QPainter>
3333#include < QPixmap>
34+ #include < QPointer>
3435#include < QRegularExpression>
3536#include < QResizeEvent>
3637#include < QScrollBar>
38+ #include < QShowEvent>
3739#include < QStyle>
3840#include < QTextBlock>
3941#include < QTextBrowser>
@@ -156,6 +158,28 @@ QString ensureHardBreaks(const QString &md)
156158 return out;
157159}
158160
161+ // QTextDocument lays out lazily: after setHtml()/setPlainText() the document
162+ // reports a stale (often single-line) size until something forces the layout
163+ // engine to run for the current text width. Measuring height before that pass
164+ // clips multi-line thought/assistant bodies to ~one line. Touching the layout's
165+ // documentSize() after pinning the text width forces the full pass, so the
166+ // subsequent doc->size() read is authoritative. Same helper as AcpToolCallCard.
167+ qreal layoutDocumentHeight (QTextDocument *doc, int textWidth)
168+ {
169+ if (!doc) return 0.0 ;
170+ doc->setTextWidth (textWidth);
171+ QAbstractTextDocumentLayout *layout = doc->documentLayout ();
172+ if (!layout) return doc->size ().height ();
173+ qreal h = layout->documentSize ().height ();
174+ const QTextBlock last = doc->lastBlock ();
175+ if (last.isValid ()) {
176+ const QRectF r = layout->blockBoundingRect (last);
177+ if (r.isValid ())
178+ h = qMax (h, r.bottom ());
179+ }
180+ return qMax (h, doc->size ().height ());
181+ }
182+
159183} // namespace
160184
161185AcpMessageWidget::AcpMessageWidget (QString role, QWidget *parent)
@@ -203,8 +227,9 @@ AcpMessageWidget::AcpMessageWidget(QString role, QWidget *parent)
203227 m_layout->addWidget (m_browser);
204228
205229 connect (m_thoughtHeader, &QToolButton::toggled, this , [this ](bool checked) {
206- if (m_browser) m_browser->setVisible (checked);
207- refitBrowserHeight ();
230+ // Header checked = expanded. Keep m_collapsed in lockstep so height
231+ // fitting can gate on the logical flag, not QWidget::isVisible().
232+ applyCollapsed (!checked);
208233 });
209234 } else {
210235 // assistant + any other roles
@@ -449,6 +474,7 @@ void AcpMessageWidget::rerender()
449474 normalizeBlockMargins (m_browser->document ());
450475 }
451476 refitBrowserHeight ();
477+ scheduleRefit ();
452478}
453479
454480void AcpMessageWidget::refitBrowserHeight ()
@@ -461,14 +487,6 @@ void AcpMessageWidget::refitBrowserHeight()
461487 if (m_layout) {
462488 m_layout->getContentsMargins (&marginL, &marginT, &marginR, &marginB);
463489 }
464- const int w = width () - marginL - marginR;
465- if (w <= 0 ) {
466- return ;
467- }
468- QTextDocument *doc = m_browser->document ();
469- doc->setTextWidth (w);
470- const int browserH = qMax (0 , static_cast <int >(std::ceil (doc->size ().height ())));
471- m_browser->setFixedHeight (browserH);
472490
473491 // Pin the bubble's own height too. setFixedHeight on the inner browser
474492 // only clamps the browser — QFrame's sizeHint cascades through QBoxLayout
@@ -480,15 +498,49 @@ void AcpMessageWidget::refitBrowserHeight()
480498 // adds style-derived button margins even with stylesheet padding:0,
481499 // which adds phantom vertical space inside the bubble.
482500 bubbleH += m_thoughtHeader->fontMetrics ().height ();
483- if (m_browser->isVisible ()) {
484- bubbleH += m_layout->spacing () + browserH;
501+ // Gate on the LOGICAL expand state, not m_browser->isVisible(): a
502+ // thought on a hidden QStackedWidget page (inactive session tab) —
503+ // or a just-inserted widget not yet painted — reads isVisible()==false
504+ // even though the body WILL paint once the tab is shown. That under-
505+ // pins the frame to header-only height while the body keeps its
506+ // measured height, clipping the thinking text. m_collapsed is
507+ // independent of show timing.
508+ if (m_collapsed) {
509+ setFixedHeight (bubbleH);
510+ return ;
485511 }
486- } else {
487- bubbleH += browserH;
512+ bubbleH += m_layout->spacing ();
488513 }
514+
515+ const int w = width () - marginL - marginR;
516+ if (w <= 0 ) {
517+ // Width not settled (never-shown stack page). Keep whatever height we
518+ // already have unless we just collapsed to header-only above.
519+ return ;
520+ }
521+ QTextDocument *doc = m_browser->document ();
522+ // Force a full layout pass for the current width before measuring —
523+ // QTextDocument under-reports height for freshly-set multi-line text
524+ // until the layout engine has run, which clips an expanded thought down
525+ // to roughly its first line.
526+ const int browserH = qMax (0 , static_cast <int >(std::ceil (layoutDocumentHeight (doc, w))));
527+ m_browser->setFixedHeight (browserH);
528+ bubbleH += browserH;
489529 setFixedHeight (bubbleH);
490530}
491531
532+ void AcpMessageWidget::scheduleRefit ()
533+ {
534+ if (m_refitScheduled) return ;
535+ m_refitScheduled = true ;
536+ QPointer<AcpMessageWidget> guard (this );
537+ QTimer::singleShot (0 , this , [guard]() {
538+ if (!guard) return ;
539+ guard->m_refitScheduled = false ;
540+ guard->refitBrowserHeight ();
541+ });
542+ }
543+
492544void AcpMessageWidget::resizeEvent (QResizeEvent *event)
493545{
494546 QFrame::resizeEvent (event);
@@ -501,11 +553,22 @@ void AcpMessageWidget::resizeEvent(QResizeEvent *event)
501553 }
502554}
503555
556+ void AcpMessageWidget::showEvent (QShowEvent *event)
557+ {
558+ QFrame::showEvent (event);
559+ // Session-tab switch shows this page without a size change, so resizeEvent
560+ // may not run. Re-measure now that we are actually visible and the stacked
561+ // layout has given us a real width.
562+ refitBrowserHeight ();
563+ scheduleRefit ();
564+ }
565+
504566void AcpMessageWidget::changeEvent (QEvent *event)
505567{
506568 QFrame::changeEvent (event);
507569 if (event->type () == QEvent::FontChange) {
508570 refitBrowserHeight ();
571+ scheduleRefit ();
509572 } else if (event->type () == QEvent::PaletteChange
510573 || event->type () == QEvent::ApplicationPaletteChange) {
511574 // Re-tint the copy glyph and re-skin code surfaces for the new theme.
@@ -867,6 +930,7 @@ void AcpMessageWidget::setChatFont(const QFont &font)
867930 rerender ();
868931 } else {
869932 refitBrowserHeight ();
933+ scheduleRefit ();
870934 }
871935}
872936
@@ -886,4 +950,8 @@ void AcpMessageWidget::applyCollapsed(bool collapsed)
886950 m_browser->setVisible (!collapsed);
887951 }
888952 refitBrowserHeight ();
953+ // Body may have been laid out at a stale width while hidden; re-measure
954+ // once the expanded geometry settles so the thought doesn't clip on
955+ // first expand / tab show.
956+ if (!collapsed) scheduleRefit ();
889957}
0 commit comments