7676#include < QStyle>
7777#include < QTimer>
7878#include < QMenu>
79+ #include < QActionGroup>
80+ #include < QPointer>
81+ #include < QScreen>
7982#include < QCursor>
8083#include < QAction>
8184#include < QFontMetrics>
@@ -331,6 +334,52 @@ class ChatInputEditCb : public QPlainTextEdit
331334 AcpImageAttachmentList *m_attachments;
332335};
333336
337+ // Combo whose popup is a menu, so leading entries can be submenus (a combo
338+ // list cannot nest). Items, the current index and its signals stay the combo's.
339+ class MenuComboBox : public QComboBox
340+ {
341+ public:
342+ using QComboBox::QComboBox;
343+
344+ // Adds entries above the items, separated from them.
345+ std::function<void (QMenu *)> addLeadingEntries;
346+
347+ void showPopup () override
348+ {
349+ if (count () == 0 )
350+ return ;
351+ QMenu menu (this );
352+ menu.setToolTipsVisible (true );
353+ if (addLeadingEntries)
354+ addLeadingEntries (&menu);
355+ if (!menu.isEmpty ())
356+ menu.addSeparator ();
357+ auto *items = new QActionGroup (&menu);
358+ for (int i = 0 ; i < count (); ++i) {
359+ QAction *a = menu.addAction (itemText (i));
360+ a->setCheckable (true );
361+ a->setChecked (i == currentIndex ());
362+ a->setData (itemData (i));
363+ items->addAction (a);
364+ }
365+ menu.setMinimumWidth (width ());
366+ // Below the combo, or above it when the screen ends first.
367+ QPoint at = mapToGlobal (QPoint (0 , height ()));
368+ const QScreen *screen = this ->screen ();
369+ const int h = menu.sizeHint ().height ();
370+ if (screen && at.y () + h > screen->availableGeometry ().bottom ())
371+ at = mapToGlobal (QPoint (0 , 0 )) - QPoint (0 , h);
372+ const QPointer<MenuComboBox> self (this );
373+ QAction *chosen = menu.exec (at);
374+ // The items may have been rebuilt while the menu was open: go by data.
375+ if (!self || !chosen || chosen->actionGroup () != items)
376+ return ;
377+ const int index = findData (chosen->data ());
378+ if (index >= 0 )
379+ setCurrentIndex (index);
380+ }
381+ };
382+
334383using AcpTurnFolding::messageHasVisibleContent;
335384
336385} // namespace
@@ -513,7 +562,9 @@ void AcpSessionView::buildUi()
513562 const QString kCompactComboCss = QStringLiteral (
514563 " QComboBox { padding: 1px 4px; border: 1px solid palette(mid); border-radius: 3px; }"
515564 " QComboBox::drop-down { width: 14px; }" );
516- m_modelCombo = new QComboBox (this );
565+ auto *modelCombo = new MenuComboBox (this );
566+ modelCombo->addLeadingEntries = [this ](QMenu *menu) { addPrewalkMenu (menu); };
567+ m_modelCombo = modelCombo;
517568 m_modelCombo->setToolTip (tr (" Model" ));
518569 m_modelCombo->setSizeAdjustPolicy (QComboBox::AdjustToContents);
519570 m_modelCombo->setStyleSheet (kCompactComboCss );
@@ -529,20 +580,16 @@ void AcpSessionView::buildUi()
529580 m_effortCombo->setStyleSheet (kCompactComboCss );
530581 m_effortCombo->hide ();
531582
532- // 4. Session options row: Prewalk, New worktree, Advisor, then the ⋯ menu
533- // (Auto-approve) and usage at the right end.
534- m_prewalkCombo = new QComboBox (this );
535- m_prewalkCombo->setSizeAdjustPolicy (QComboBox::AdjustToContents);
536- m_prewalkCombo->setStyleSheet (kCompactComboCss );
537- m_prewalkCombo->hide (); // shown only for Claude Code with a model catalog
583+ // 4. Session options row: New worktree, Advisor, the "More options" menu
584+ // (Auto-approve), then usage at the right end.
538585 m_moreBtn = new QToolButton (this );
539- m_moreBtn->setText (QStringLiteral (" ⋯" ));
540586 m_moreBtn->setAutoRaise (true );
541- m_moreBtn->setToolButtonStyle (Qt::ToolButtonTextOnly);
587+ m_moreBtn->setToolButtonStyle (Qt::ToolButtonIconOnly);
588+ m_moreBtn->setIconSize (QSize (16 , 16 ));
542589 m_moreBtn->setPopupMode (QToolButton::InstantPopup);
543590 m_moreBtn->setToolTip (tr (" More options" ));
544591 m_moreBtn->setStyleSheet (QStringLiteral (
545- " QToolButton { padding: 0px 4px ; border: 1px solid transparent; border-radius: 3px; }"
592+ " QToolButton { padding: 1px ; border: 1px solid transparent; border-radius: 3px; }"
546593 " QToolButton:hover { border: 1px solid palette(mid); }"
547594 " QToolButton::menu-indicator { image: none; width: 0px; }" ));
548595 auto *moreMenu = new QMenu (m_moreBtn);
@@ -563,11 +610,10 @@ void AcpSessionView::buildUi()
563610 auto *optionsRow = new QHBoxLayout ();
564611 optionsRow->setContentsMargins (0 , 0 , 0 , 0 );
565612 optionsRow->setSpacing (6 );
566- optionsRow->addWidget (m_prewalkCombo);
567613 optionsRow->addWidget (m_newWorktreeCheck);
568614 optionsRow->addWidget (m_advisorBtn);
569- optionsRow->addStretch ();
570615 optionsRow->addWidget (m_moreBtn);
616+ optionsRow->addStretch ();
571617 m_usageIndicator = new AcpUsageIndicator (this );
572618 optionsRow->addWidget (m_usageIndicator);
573619 outer->addLayout (optionsRow);
@@ -726,7 +772,7 @@ void AcpSessionView::buildUi()
726772 m_attachBtn->setStyleSheet (QStringLiteral (
727773 " QToolButton { color: palette(placeholder-text); padding: 4px; border: 1px solid transparent; border-radius: 3px; }"
728774 " QToolButton:hover { color: palette(text); border: 1px solid palette(mid); }" ));
729- rebuildAttachIcon ();
775+ rebuildTintedIcons ();
730776 m_cancelBtn = new QPushButton (tr (" Cancel" ), this );
731777 m_sendBtn = new QPushButton (tr (" Send" ), this );
732778 m_cancelBtn->setEnabled (false );
@@ -822,8 +868,6 @@ void AcpSessionView::buildUi()
822868 this , &AcpSessionView::onModeComboChanged);
823869 connect (m_effortCombo, qOverload<int >(&QComboBox::currentIndexChanged),
824870 this , &AcpSessionView::onEffortComboChanged);
825- connect (m_prewalkCombo, qOverload<int >(&QComboBox::currentIndexChanged),
826- this , &AcpSessionView::onPrewalkComboChanged);
827871
828872 // Apply the chat font preference to transcript + input, and follow live
829873 // preference edits. The chat font may be independent of the editor font:
@@ -1320,50 +1364,60 @@ void AcpSessionView::loadPrewalkTarget()
13201364 if (m_model)
13211365 m_model->setPrewalkModel (target);
13221366 }
1323- // refreshPrewalkCombo drops it while the agent's catalog lacks it.
1367+ // refreshPrewalk drops it while the agent's catalog lacks it.
13241368 m_prewalk.setTarget (target);
13251369}
13261370
1327- void AcpSessionView::refreshPrewalkCombo ()
1371+ void AcpSessionView::refreshPrewalk ()
13281372{
1329- if (!m_prewalkCombo)
1330- return ;
13311373 m_prewalkAvailable = prewalkAvailable ();
1332- // Catalog as (id, label) pairs. Rebuild the items only when it changed:
1333- // every config update (Prewalk's own switches included) lands here, and a
1334- // rebuild would close the popup under the user.
1335- QStringList items;
1336- if (m_prewalkAvailable) {
1337- items.reserve (m_modelCombo->count () * 2 );
1338- for (int i = 0 ; i < m_modelCombo->count (); ++i)
1339- items << m_modelCombo->itemData (i).toString () << m_modelCombo->itemText (i);
1340- }
1341- const QString chosen = m_model && m_model->prewalkModel () ? *m_model->prewalkModel () : QString ();
1342- const bool wasUpdating = std::exchange (m_updatingSelectors, true );
1343- if (items != m_prewalkItems) {
1344- m_prewalkItems = items;
1345- m_prewalkCombo->clear ();
1346- if (m_prewalkAvailable) {
1347- m_prewalkCombo->addItem (tr (" Prewalk: Off" ), QString ());
1348- for (qsizetype i = 0 ; i + 1 < items.size (); i += 2 )
1349- m_prewalkCombo->addItem (tr (" Prewalk: %1" ).arg (items.at (i + 1 )), items.at (i));
1350- }
1351- }
13521374 // A model the catalog lacks (another agent's, or gone) is Off here; the
13531375 // session keeps its choice for an agent that has it.
1354- const int chosenIndex = chosen.isEmpty () ? -1 : m_prewalkCombo->findData (chosen);
1355- if (m_prewalkAvailable)
1356- m_prewalkCombo->setCurrentIndex (std::max (0 , chosenIndex));
1357- m_updatingSelectors = wasUpdating;
1358- m_prewalkCombo->setVisible (m_prewalkAvailable);
1359- m_prewalk.setTarget (chosenIndex > 0 ? chosen : QString ());
1376+ const QString chosen = m_model && m_model->prewalkModel () ? *m_model->prewalkModel () : QString ();
1377+ const bool inCatalog = m_prewalkAvailable && !chosen.isEmpty () && m_modelCombo->findData (chosen) >= 0 ;
1378+ m_prewalk.setTarget (inCatalog ? chosen : QString ());
1379+ }
1380+
1381+ void AcpSessionView::addPrewalkMenu (QMenu *menu)
1382+ {
1383+ if (!m_prewalkAvailable)
1384+ return ;
1385+ const QString &target = m_prewalk.target ();
1386+ QMenu *sub = menu->addMenu (target.isEmpty () ? tr (" Prewalk: Off" )
1387+ : tr (" Prewalk: %1" ).arg (modelLabel (target)));
1388+ sub->menuAction ()->setToolTip (prewalkStatus () + QLatin1Char (' \n ' )
1389+ + tr (" Switching models re-reads the conversation without the prompt cache" ));
1390+ auto *group = new QActionGroup (sub);
1391+ const auto add = [&](const QString &label, const QString &id) {
1392+ QAction *a = sub->addAction (label);
1393+ a->setCheckable (true );
1394+ a->setChecked (id == target);
1395+ group->addAction (a);
1396+ connect (a, &QAction::triggered, this , [this , id]() { onPrewalkPicked (id); });
1397+ };
1398+ add (tr (" Off" ), QString ());
1399+ for (int i = 0 ; i < m_modelCombo->count (); ++i)
1400+ add (m_modelCombo->itemText (i), m_modelCombo->itemData (i).toString ());
1401+ }
1402+
1403+ QString AcpSessionView::prewalkStatus () const
1404+ {
1405+ if (const std::optional<AcpPrewalk::Selection> own = m_prewalk.sessionSelection ()) {
1406+ return tr (" Prewalk: on %1 until the turn ends, then back to %2" )
1407+ .arg (modelLabel (currentSelection ().model ), modelLabel (own->model ));
1408+ }
1409+ if (!m_prewalk.target ().isEmpty ()) {
1410+ return tr (" Prewalk on: switching to %1 at the next edit/write; back to %2 when the turn ends" )
1411+ .arg (modelLabel (m_prewalk.target ()), modelLabel (currentSelection ().model ));
1412+ }
1413+ return tr (" Prewalk: switch to a cheaper model once the agent starts editing files, "
1414+ " and back to this one when the turn ends" );
13601415}
13611416
1362- void AcpSessionView::onPrewalkComboChanged ( int index )
1417+ void AcpSessionView::onPrewalkPicked ( const QString &id )
13631418{
1364- if (m_updatingSelectors || index < 0 || !m_model)
1419+ if (!m_model)
13651420 return ;
1366- const QString id = m_prewalkCombo->itemData (index).toString ();
13671421 m_model->setPrewalkModel (id);
13681422 m_prewalk.setTarget (id);
13691423 if (m_registry && m_draft && m_connection)
@@ -2578,7 +2632,7 @@ void AcpSessionView::onMetadataChanged()
25782632 m_effortCombo->setVisible (m_effortCombo->count () > 0 );
25792633
25802634 m_updatingSelectors = false ;
2581- refreshPrewalkCombo ();
2635+ refreshPrewalk ();
25822636 // The restore just landed with an effort the agent reset: not a default.
25832637 const bool prewalkEffortPending = notePrewalkSelectionReported ();
25842638 refreshSelectorTooltips ();
@@ -2858,28 +2912,11 @@ void AcpSessionView::refreshSelectorTooltips()
28582912 // Why a pick no longer becomes the default for new sessions.
28592913 const QString suffix = m_draft ? QString ()
28602914 : QLatin1Char (' \n ' ) + tr (" Applies to this session only" );
2861- const std::optional<AcpPrewalk::Selection> own = m_prewalk.sessionSelection ();
28622915 if (m_modelCombo) {
2863- QString tip = tr (" Model" ) + suffix;
2864- if (own)
2865- tip += QLatin1Char (' \n ' ) + tr (" Prewalk: back to %1 when the turn ends" ).arg (modelLabel (own->model ));
2866- m_modelCombo->setToolTip (tip);
2867- }
2868- if (m_prewalkCombo) {
2869- const QString &target = m_prewalk.target ();
2870- QString tip;
2871- if (own) {
2872- tip = tr (" Prewalk: on %1 until the turn ends, then back to %2" )
2873- .arg (modelLabel (currentSelection ().model ), modelLabel (own->model ));
2874- } else if (!target.isEmpty ()) {
2875- tip = tr (" Prewalk on: switching to %1 at the next edit/write; back to %2 when the turn ends" )
2876- .arg (modelLabel (target), modelLabel (currentSelection ().model ));
2877- } else {
2878- tip = tr (" Prewalk: switch to a cheaper model once the agent starts editing files, "
2879- " and back to this one when the turn ends" );
2880- }
2881- tip += QLatin1Char (' \n ' ) + tr (" Switching models re-reads the conversation without the prompt cache" );
2882- m_prewalkCombo->setToolTip (tip + suffix);
2916+ QString tip = tr (" Model" );
2917+ if (m_prewalkAvailable && (m_prewalk.sessionSelection () || !m_prewalk.target ().isEmpty ()))
2918+ tip += QLatin1Char (' \n ' ) + prewalkStatus ();
2919+ m_modelCombo->setToolTip (tip + suffix);
28832920 }
28842921 if (m_modeCombo)
28852922 m_modeCombo->setToolTip (tr (" Mode" ) + suffix);
@@ -4109,18 +4146,20 @@ void AcpSessionView::changeEvent(QEvent *event)
41094146 case QEvent::PaletteChange:
41104147 case QEvent::StyleChange:
41114148 case QEvent::ApplicationPaletteChange:
4112- rebuildAttachIcon ();
4149+ rebuildTintedIcons ();
41134150 break ;
41144151 default :
41154152 break ;
41164153 }
41174154}
41184155
4119- void AcpSessionView::rebuildAttachIcon ()
4156+ void AcpSessionView::rebuildTintedIcons ()
41204157{
4121- if (!m_attachBtn) return ;
4122- m_attachBtn->setIcon (tintedIcon (QStringLiteral (" :/icons/paperclip.svg" ),
4123- palette ().color (QPalette::WindowText)));
4158+ const QColor color = palette ().color (QPalette::WindowText);
4159+ if (m_attachBtn)
4160+ m_attachBtn->setIcon (tintedIcon (QStringLiteral (" :/icons/paperclip.svg" ), color));
4161+ if (m_moreBtn)
4162+ m_moreBtn->setIcon (tintedIcon (QStringLiteral (" :/icons/more-vertical.svg" ), color));
41244163}
41254164
41264165QFont AcpSessionView::chatFont () const
0 commit comments