3838#include < QClipboard>
3939#include < QComboBox>
4040#include < QDialog>
41+ #include < QElapsedTimer>
4142#include < QEvent>
4243#include < QFileDialog>
4344#include < QFont>
@@ -566,6 +567,15 @@ void AcpSessionView::buildUi()
566567 }
567568 });
568569
570+ // Input-placeholder busy clock — repaints the elapsed-time placeholder once
571+ // a second while any agent is working. The underlying span is measured by
572+ // the monotonic m_busyClock; this timer only triggers the text refresh, so
573+ // a missed tick (event-loop stall) self-corrects on the next fire.
574+ m_busyPlaceholderTimer = new QTimer (this );
575+ m_busyPlaceholderTimer->setInterval (1000 );
576+ connect (m_busyPlaceholderTimer, &QTimer::timeout,
577+ this , &AcpSessionView::updateBusyPlaceholderText);
578+
569579 btnRow->addWidget (m_cancelBtn);
570580 btnRow->addWidget (m_sendBtn);
571581
@@ -625,6 +635,8 @@ void AcpSessionView::buildUi()
625635 this , &AcpSessionView::applyChatFont);
626636 connect (settings, &ApplicationSettings::fontSizeChanged,
627637 this , &AcpSessionView::applyChatFont);
638+ connect (settings, &ApplicationSettings::fontHintingChanged,
639+ this , &AcpSessionView::applyChatFont);
628640 }
629641 applyChatFont ();
630642
@@ -722,6 +734,7 @@ void AcpSessionView::hydrateFromModel()
722734 && entry.messageIndex < messages.size ()) {
723735 const AcpMessage &msg = messages.at (entry.messageIndex );
724736 auto *w = new AcpMessageWidget (msg.role , m_transcriptHost);
737+ w->setChatFont (chatFont ()); // styled widget: must set font explicitly
725738 if (msg.fromGoalAgent ) {
726739 w->setFromGoalAgent (true );
727740 }
@@ -732,6 +745,7 @@ void AcpSessionView::hydrateFromModel()
732745 auto it = toolCalls.find (entry.toolCallId );
733746 if (it != toolCalls.end ()) {
734747 auto *card = new AcpToolCallCard (it.value (), m_transcriptHost);
748+ card->setChatFont (chatFont ()); // styled widget: must set font explicitly
735749 insertTimelineWidget (card);
736750 m_toolCallCards.insert (entry.toolCallId , card);
737751 }
@@ -925,6 +939,7 @@ void AcpSessionView::appendMessageWidget(int idx)
925939 const AcpMessage &msg = m_model->messages ().at (idx);
926940
927941 auto *w = new AcpMessageWidget (msg.role , m_transcriptHost);
942+ w->setChatFont (chatFont ()); // styled widget: must set font explicitly
928943 if (msg.fromGoalAgent ) {
929944 w->setFromGoalAgent (true );
930945 }
@@ -1018,6 +1033,7 @@ void AcpSessionView::onToolCallAddedOrUpdated(const QString &toolCallId)
10181033 auto *card = m_toolCallCards.value (toolCallId, nullptr );
10191034 if (!card) {
10201035 card = new AcpToolCallCard (tc, m_transcriptHost);
1036+ card->setChatFont (chatFont ()); // styled widget: must set font explicitly
10211037 insertTimelineWidget (card);
10221038 m_toolCallCards.insert (toolCallId, card);
10231039 m_currentGroupCards.append (card);
@@ -1197,6 +1213,10 @@ void AcpSessionView::onIsProcessingChanged(bool processing)
11971213 }
11981214 }
11991215 if (m_planWidget) m_planWidget->setAgentIdle (!processing);
1216+
1217+ // Recompute the input busy-placeholder against the new ACP state (it also
1218+ // depends on m_goalRunning, so the union is resolved inside).
1219+ refreshBusyPlaceholder ();
12001220}
12011221
12021222void AcpSessionView::onTurnEnded (int groupId)
@@ -1308,6 +1328,8 @@ QStringList AcpSessionView::goalDebugLog() const
13081328void AcpSessionView::setGoalActive (int criterionIndex, int totalCriteria, int iteration, int maxIterations)
13091329{
13101330 if (!m_goalStatusRow) return ;
1331+ m_goalRunning = true ;
1332+ refreshBusyPlaceholder ();
13111333 QString text;
13121334 if (totalCriteria > 1 ) {
13131335 text = tr (" Goal %1/%2 · iter %3/%4" )
@@ -1342,6 +1364,8 @@ void AcpSessionView::setGoalActive(int criterionIndex, int totalCriteria, int it
13421364void AcpSessionView::setGoalTerminal (const QString &statusText)
13431365{
13441366 if (!m_goalStatusRow) return ;
1367+ m_goalRunning = false ;
1368+ refreshBusyPlaceholder ();
13451369 m_goalStatusLabel->setText (statusText);
13461370 m_goalStopBtn->hide ();
13471371 m_goalStatusRow->show ();
@@ -1360,6 +1384,8 @@ void AcpSessionView::setGoalTerminal(const QString &statusText)
13601384void AcpSessionView::clearGoalStatus ()
13611385{
13621386 if (!m_goalStatusRow) return ;
1387+ m_goalRunning = false ;
1388+ refreshBusyPlaceholder ();
13631389 m_goalStatusRow->hide ();
13641390 if (m_goalElapsedTimer) m_goalElapsedTimer->stop ();
13651391 if (m_goalElapsedLabel) m_goalElapsedLabel->hide ();
@@ -1553,6 +1579,63 @@ void AcpSessionView::onElapsedTick()
15531579 }
15541580}
15551581
1582+ void AcpSessionView::refreshBusyPlaceholder ()
1583+ {
1584+ if (!m_input) return ;
1585+
1586+ // Union of every running-agent surface. Either an in-flight ACP turn or an
1587+ // active goal keeps the input "busy"; the placeholder reflects that union,
1588+ // not whichever finished last.
1589+ const bool acpBusy = m_model && m_model->isProcessing ();
1590+ const bool busy = acpBusy || m_goalRunning;
1591+
1592+ if (busy == m_busyPlaceholderActive) {
1593+ // No edge — the other surface was already keeping us busy. Leave the
1594+ // monotonic clock untouched so the elapsed span keeps accumulating.
1595+ return ;
1596+ }
1597+ m_busyPlaceholderActive = busy;
1598+
1599+ if (busy) {
1600+ m_busyClock.start (); // idle→busy edge: anchor the clock once
1601+ updateBusyPlaceholderText (); // paint 0m 0s immediately, don't wait 1 s
1602+ m_busyPlaceholderTimer->start ();
1603+ } else {
1604+ m_busyPlaceholderTimer->stop ();
1605+ // Busy→idle edge. setInputPlaceholder() handles the viewport
1606+ // invalidation so the input reverts to the idle text immediately
1607+ // instead of staying frozen on the last "Agent is working… (Xm Ys)".
1608+ setInputPlaceholder (tr (" Send a message" ));
1609+ }
1610+ }
1611+
1612+ void AcpSessionView::updateBusyPlaceholderText ()
1613+ {
1614+ if (!m_input || !m_busyPlaceholderActive) return ;
1615+ const qint64 elapsedSec = m_busyClock.elapsed () / 1000 ;
1616+ const qint64 minutes = elapsedSec / 60 ;
1617+ const qint64 seconds = elapsedSec % 60 ;
1618+ setInputPlaceholder (tr (" Agent is working… (%1m %2s)" ).arg (minutes).arg (seconds));
1619+ }
1620+
1621+ void AcpSessionView::setInputPlaceholder (const QString &text)
1622+ {
1623+ if (!m_input) return ;
1624+ m_input->setPlaceholderText (text);
1625+ // QPlainTextEdit::setPlaceholderText() does not reliably invalidate the
1626+ // viewport on a pure text change (its internal repaint is conditional and
1627+ // varies across Qt 6.5↔6.10), so without this poke a placeholder transition
1628+ // stays frozen on whatever was last painted by an unrelated relayout. The
1629+ // placeholder is drawn in the viewport's paintEvent — invalidate it
1630+ // directly, but only when it's actually on screen (empty document); when
1631+ // the user has typed text the placeholder is hidden and Qt repaints the
1632+ // content edit itself. Routed through one helper so no transition can skip
1633+ // the poke. At most one repaint per second while busy — negligible cost.
1634+ if (m_input->document ()->isEmpty ()) {
1635+ m_input->viewport ()->update ();
1636+ }
1637+ }
1638+
15561639void AcpSessionView::onShowDebugLogClicked ()
15571640{
15581641 // Find the owning AiAgentDock so we can scope the dialog title and goal
@@ -1787,14 +1870,41 @@ void AcpSessionView::rebuildAttachIcon()
17871870 palette ().color (QPalette::WindowText)));
17881871}
17891872
1790- void AcpSessionView::applyChatFont ()
1873+ QFont AcpSessionView::chatFont () const
17911874{
17921875 auto *settings = appSettings ();
1793- if (!settings) return ;
1876+ QFont f = settings ? QFont (settings->fontName (), settings->fontSize ())
1877+ : QFont ();
1878+ // Mirror the editor's glyph-hinting policy: these are plain Qt widgets, so
1879+ // Scintilla's platform-layer flag doesn't reach them — set it on the QFont
1880+ // directly. Without it, thin fonts (e.g. Lilex) look blurry here while the
1881+ // Scintilla editor looks sharp. See EditorManager / PlatQt for the editor side.
1882+ f.setHintingPreference (settings && !settings->fontHinting ()
1883+ ? QFont::PreferNoHinting
1884+ : QFont::PreferFullHinting);
1885+ return f;
1886+ }
1887+
1888+ void AcpSessionView::applyChatFont ()
1889+ {
1890+ if (!appSettings ()) return ;
17941891
1795- QFont f (settings->fontName (), settings->fontSize ());
1892+ const QFont f = chatFont ();
1893+ // The transcript host carries no stylesheet, so its setFont() is a cheap,
1894+ // harmless default for any unstyled descendants. But the message bubbles
1895+ // (AcpMessageWidget) AND their inner QTextBrowsers are stylesheet'd, and Qt
1896+ // does NOT propagate an inherited font into a styled widget — so we must
1897+ // push the font into each bubble explicitly via setChatFont().
17961898 if (m_transcriptHost) m_transcriptHost->setFont (f);
17971899 if (m_input) m_input->setFont (f);
1900+
1901+ for (AcpMessageWidget *w : m_messageWidgets) {
1902+ if (w) w->setChatFont (f);
1903+ }
1904+ if (m_activeThought) m_activeThought->setChatFont (f);
1905+ for (AcpToolCallCard *c : m_toolCallCards) {
1906+ if (c) c->setChatFont (f);
1907+ }
17981908}
17991909
18001910bool AcpSessionView::inputKeyEventIsSubmit (QKeyEvent *ke) const
0 commit comments