Skip to content

Commit e144e8b

Browse files
refactor(acp): nest Prewalk in model picker menu
Combo lists cannot nest, so Prewalk lived as a separate control and cluttered the options row. A menu-backed model combo keeps index/signals on the combo while letting Prewalk sit as a leading submenu with related model choices. Also swap the more-options ellipsis text for a tinted vertical-dots icon so the control matches other icon-only toolbar buttons.
1 parent c5d9b97 commit e144e8b

4 files changed

Lines changed: 130 additions & 82 deletions

File tree

‎src/icons/more-vertical.svg‎

Lines changed: 5 additions & 0 deletions
Loading

‎src/resources.qrc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<file>icons/chevron-down.svg</file>
5959
<file>icons/list_with_icons.svg</file>
6060
<file>icons/paperclip.svg</file>
61+
<file>icons/more-vertical.svg</file>
6162
<file>icons/markdown-preview.svg</file>
6263
<file>icons/eye.svg</file>
6364
<file>icons/html-preview.svg</file>

‎src/widgets/AcpSessionView.cpp‎

Lines changed: 114 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@
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+
334383
using 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

41264165
QFont AcpSessionView::chatFont() const

‎src/widgets/AcpSessionView.h‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class QDialog;
6363
class QFrame;
6464
class QLabel;
6565
class QListWidget;
66+
class QMenu;
6667
class QPlainTextEdit;
6768
class QPushButton;
6869
class QScrollArea;
@@ -203,7 +204,7 @@ private slots:
203204
void onModelComboChanged(int index);
204205
void onModeComboChanged(int index);
205206
void onEffortComboChanged(int index);
206-
void onPrewalkComboChanged(int index);
207+
void onPrewalkPicked(const QString &modelId);
207208
void onAdvisorClicked(bool on);
208209
void onAutoApprovePolicyChanged(const QString &policy);
209210
void onJumpToBottomClicked();
@@ -214,7 +215,7 @@ private slots:
214215
private:
215216
void buildUi();
216217
void wireSignals();
217-
void rebuildAttachIcon();
218+
void rebuildTintedIcons();
218219
void hydrateFromModel();
219220
void appendMessageWidget(int idx);
220221
// Insert a widget into the transcript timeline at the tail, just above
@@ -373,7 +374,11 @@ private slots:
373374
bool prewalkAvailable() const;
374375
// The session's prewalk model: its own choice, else the agent default.
375376
void loadPrewalkTarget();
376-
void refreshPrewalkCombo();
377+
void refreshPrewalk();
378+
// The "Prewalk: …" submenu at the top of the model picker's menu.
379+
void addPrewalkMenu(QMenu *menu);
380+
// What Prewalk will do or is doing now.
381+
QString prewalkStatus() const;
377382
void notePrewalkToolCall(const AcpProtocol::AcpToolCall &tc);
378383
void notePrewalkTurnEnded();
379384
// After the agent reported its selection: finish a restore. True when it
@@ -440,13 +445,11 @@ private slots:
440445
// array. Drives whether model changes dispatch via session/set_config.
441446
QString m_modelConfigOptionId;
442447

443-
// Prewalk: "Prewalk: Off" / "Prewalk: <model>"; hidden unless available.
444-
QComboBox *m_prewalkCombo = nullptr;
448+
// Prewalk: a submenu of the model picker, absent unless available.
445449
AcpPrewalk m_prewalk;
446450
bool m_prewalkAvailable = false; // prewalkAvailable() as of the last catalog
447-
QStringList m_prewalkItems; // the combo's catalog: id, label, id, label…
448451

449-
// Auto-approve, in the ⋯ menu at the end of the row.
452+
// Auto-approve, in the "More options" menu (vertical dots) after Advisor.
450453
QToolButton *m_moreBtn = nullptr;
451454
QAction *m_autoApproveAction = nullptr;
452455
// Session-local; never persisted. New views start unchecked.

0 commit comments

Comments
 (0)