// Copyright (C) 2026 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later #include "AgentSelectionDialog.hpp" #include "AgentSlotWidget.hpp" #include "PipelinesConfig.hpp" #include "SettingsTr.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace QodeAssist::Settings { // -- ListRowCard ------------------------------------------------------- ListRowCard::ListRowCard(QWidget *parent) : QFrame(parent) { setObjectName(QStringLiteral("ListRowCard")); setAttribute(Qt::WA_StyledBackground, true); setCursor(Qt::PointingHandCursor); setFrameShape(QFrame::NoFrame); applyTheme(); } bool ListRowCard::matches(const QString &needle) const { if (needle.isEmpty()) return true; return m_searchHaystack.contains(needle.toLower()); } void ListRowCard::setSelected(bool selected) { if (m_selected == selected) return; m_selected = selected; applyTheme(); } void ListRowCard::buildSearchHaystack(const QStringList &parts) { m_searchHaystack = parts.join(QLatin1Char(' ')).toLower(); } void ListRowCard::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) emit clicked(); QFrame::mousePressEvent(event); } void ListRowCard::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) emit activated(); QFrame::mouseDoubleClickEvent(event); } void ListRowCard::enterEvent(QEnterEvent *event) { QFrame::enterEvent(event); m_hover = true; applyTheme(); } void ListRowCard::leaveEvent(QEvent *event) { QFrame::leaveEvent(event); m_hover = false; applyTheme(); } void ListRowCard::changeEvent(QEvent *event) { QFrame::changeEvent(event); if (m_inApplyTheme) return; if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) applyTheme(); } void ListRowCard::applyTheme() { if (m_inApplyTheme) return; QScopedValueRollback guard(m_inApplyTheme, true); const auto t = CardStyle::toneFor(CardStyle::isDark(palette())); QString bg = t.bg; QString bd = t.cardBd; if (m_selected) { bg = t.selectedBg; bd = t.selectedBd; } else if (m_hover) { bg = t.hoverBg; } setStyleSheet(QStringLiteral( "#ListRowCard { background-color: %1; border: 1px solid %2; }") .arg(bg, bd)); } // -- AgentRowCard ------------------------------------------------------ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent) : ListRowCard(parent) { setItemName(cfg.name); QStringList haystack{cfg.name, cfg.providerInstance, cfg.model, cfg.description, cfg.role, cfg.endpoint}; haystack += cfg.tags; buildSearchHaystack(haystack); auto *name = new QLabel(cfg.name, this); QFont nameFont = name->font(); nameFont.setBold(true); nameFont.setPixelSize(13); name->setFont(nameFont); name->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); QLabel *model = nullptr; if (!cfg.model.isEmpty()) { model = new QLabel(QStringLiteral("· %1").arg(cfg.model), this); model->setFont(CardStyle::monoFont(11)); model->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); model->setMinimumWidth(0); } Pill *sourcePill = nullptr; if (cfg.isUserSource()) { sourcePill = new Pill( Pill::User, cfg.overridesBundled ? Tr::tr("Override") : Tr::tr("User"), this); } auto *description = new QLabel(this); description->setWordWrap(false); QFont descFont = description->font(); descFont.setItalic(true); description->setFont(descFont); description->setText(cfg.description.isEmpty() ? Tr::tr("No description provided.") : cfg.description); description->setMinimumWidth(0); description->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); description->setTextInteractionFlags(Qt::NoTextInteraction); QStringList endpointParts; if (!cfg.endpoint.isEmpty()) endpointParts << cfg.endpoint; endpointParts << (cfg.enableThinking ? Tr::tr("thinking") : Tr::tr("no-thinking")); endpointParts << (cfg.enableTools ? Tr::tr("tools") : Tr::tr("no-tools")); auto *endpoint = new QLabel(endpointParts.join(QStringLiteral(" · ")), this); endpoint->setFont(CardStyle::monoFont(11)); endpoint->setMinimumWidth(0); endpoint->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); endpoint->setTextInteractionFlags(Qt::NoTextInteraction); auto *headerRow = new QHBoxLayout; headerRow->setContentsMargins(0, 0, 0, 0); headerRow->setSpacing(6); headerRow->addWidget(name); if (model) headerRow->addWidget(model, 1); else headerRow->addStretch(1); if (sourcePill) headerRow->addWidget(sourcePill); auto *outer = new QVBoxLayout(this); outer->setContentsMargins(10, 8, 10, 8); outer->setSpacing(3); outer->addLayout(headerRow); outer->addWidget(description); outer->addWidget(endpoint); if (!cfg.tags.isEmpty()) { constexpr int kMaxTagPills = 4; auto *tagsRow = new QHBoxLayout; tagsRow->setContentsMargins(0, 0, 0, 0); tagsRow->setSpacing(4); const int tagCount = cfg.tags.size(); for (int i = 0; i < std::min(tagCount, kMaxTagPills); ++i) tagsRow->addWidget(new Pill(Pill::Tag, cfg.tags.at(i), this)); if (tagCount > kMaxTagPills) { auto *more = new Pill( Pill::Tag, QStringLiteral("+%1").arg(tagCount - kMaxTagPills), this); more->setToolTip(cfg.tags.mid(kMaxTagPills).join(QStringLiteral(", "))); tagsRow->addWidget(more); } tagsRow->addStretch(1); outer->addLayout(tagsRow); } const auto t = CardStyle::toneFor(CardStyle::isDark(palette())); QPalette descPal = description->palette(); descPal.setColor(QPalette::WindowText, QColor(cfg.description.isEmpty() ? t.textFaint : t.textSoft)); description->setPalette(descPal); QPalette endPal = endpoint->palette(); endPal.setColor(QPalette::WindowText, QColor(t.textFaint)); endpoint->setPalette(endPal); if (model) { QPalette mp = model->palette(); mp.setColor(QPalette::WindowText, QColor(t.textMute)); model->setPalette(mp); } QString tooltip; if (!cfg.description.isEmpty()) tooltip += cfg.description + QStringLiteral("\n\n"); if (!cfg.providerInstance.isEmpty()) tooltip += Tr::tr("Provider instance: %1\n").arg(cfg.providerInstance); if (!cfg.role.isEmpty()) tooltip += Tr::tr("Role: %1\n").arg(cfg.role); if (!cfg.endpoint.isEmpty()) tooltip += Tr::tr("Endpoint: %1\n").arg(cfg.endpoint); setToolTip(tooltip.trimmed()); } // -- ProviderSection --------------------------------------------------- ProviderSection::ProviderSection(const QString &name, QWidget *parent) : QWidget(parent) { m_arrow = new QLabel(QStringLiteral("▾")); m_label = new QLabel(name); CardStyle::applySectionFont(m_label); QFont arrowFont = m_label->font(); arrowFont.setCapitalization(QFont::MixedCase); m_arrow->setFont(arrowFont); QPalette ap = m_arrow->palette(); ap.setColor(QPalette::WindowText, ap.color(QPalette::Mid)); m_arrow->setPalette(ap); m_header = new QFrame; m_header->setObjectName(QStringLiteral("ProviderHeader")); m_header->setCursor(Qt::PointingHandCursor); m_header->setFrameShape(QFrame::NoFrame); auto *headerLayout = new QHBoxLayout(m_header); headerLayout->setContentsMargins(2, 4, 2, 2); headerLayout->setSpacing(6); headerLayout->addWidget(m_arrow); headerLayout->addWidget(m_label); headerLayout->addStretch(1); m_header->installEventFilter(this); m_content = new QWidget; m_contentLayout = new QVBoxLayout(m_content); m_contentLayout->setContentsMargins(0, 0, 0, 0); m_contentLayout->setSpacing(4); m_content->setVisible(false); m_arrow->setText(QStringLiteral("▸")); m_expanded = false; auto *outer = new QVBoxLayout(this); outer->setContentsMargins(0, 0, 0, 0); outer->setSpacing(0); outer->addWidget(m_header); outer->addWidget(m_content); } void ProviderSection::addCard(ListRowCard *card) { m_contentLayout->addWidget(card); m_cards.append(card); } int ProviderSection::applyFilter(const QString &needle) { int visible = 0; for (auto *card : m_cards) { const bool show = card->matches(needle); card->setVisible(show); if (show) ++visible; } return visible; } void ProviderSection::setExpanded(bool expanded) { if (m_expanded == expanded) return; m_expanded = expanded; m_content->setVisible(expanded); m_arrow->setText(expanded ? QStringLiteral("▾") : QStringLiteral("▸")); } bool ProviderSection::eventFilter(QObject *watched, QEvent *event) { if (watched == m_header && event->type() == QEvent::MouseButtonRelease) { auto *me = static_cast(event); if (me->button() == Qt::LeftButton) { setExpanded(!m_expanded); return true; } } return QWidget::eventFilter(watched, event); } // -- AgentSelectionDialog ---------------------------------------------- AgentSelectionDialog::AgentSelectionDialog( const std::vector &configs, const QString ¤tName, AgentFactory *agentFactory, QWidget *parent) : QDialog(parent) , m_agentFactory(agentFactory) { setWindowTitle(Tr::tr("Change Agent")); resize(720, 600); setSizeGripEnabled(true); if (!m_agentFactory) m_localConfigs = configs; m_filter = new QLineEdit(this); m_filter->setPlaceholderText( Tr::tr("Filter by name, provider, model, template, description…")); m_filter->setClearButtonEnabled(true); auto *topRow = new QHBoxLayout; topRow->setContentsMargins(0, 0, 0, 0); topRow->setSpacing(6); topRow->addWidget(m_filter, 1); m_scroll = new QScrollArea(this); m_scroll->setWidgetResizable(true); m_scroll->setFrameShape(QFrame::StyledPanel); m_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); m_okButton = buttons->button(QDialogButtonBox::Ok); m_okButton->setText(Tr::tr("Change")); m_okButton->setEnabled(false); auto *layout = new QVBoxLayout(this); layout->addLayout(topRow); layout->addWidget(m_scroll); layout->addWidget(buttons); rebuild(currentName); connect(m_filter, &QLineEdit::textChanged, this, [this](const QString &text) { applyFilter(text); }); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); } void AgentSelectionDialog::selectCard(ListRowCard *card) { if (m_currentCard == card) return; if (m_currentCard) m_currentCard->setSelected(false); m_currentCard = card; if (m_currentCard) { m_currentCard->setSelected(true); m_selectedName = m_currentCard->itemName(); } else { m_selectedName.clear(); } if (m_okButton) m_okButton->setEnabled(!m_selectedName.isEmpty()); } void AgentSelectionDialog::rebuild(const QString ¤tName) { m_sections.clear(); m_currentCard = nullptr; m_selectedName.clear(); if (m_okButton) m_okButton->setEnabled(false); const auto &configs = m_agentFactory ? m_agentFactory->configs() : m_localConfigs; auto *content = new QWidget; auto *contentLayout = new QVBoxLayout(content); contentLayout->setContentsMargins(12, 12, 12, 12); contentLayout->setSpacing(6); QMap> byProvider; for (const auto &cfg : configs) { if (cfg.hidden) continue; // hidden profiles stay loaded but don't surface in the picker const QString key = cfg.providerInstance.isEmpty() ? Tr::tr("(Unknown provider instance)") : cfg.providerInstance; byProvider[key].push_back(&cfg); } AgentRowCard *toSelect = nullptr; ProviderSection *sectionToExpand = nullptr; for (auto it = byProvider.cbegin(); it != byProvider.cend(); ++it) { auto *section = new ProviderSection(it.key()); auto sortedConfigs = it.value(); std::sort(sortedConfigs.begin(), sortedConfigs.end(), [](const AgentConfig *a, const AgentConfig *b) { return a->name < b->name; }); for (const AgentConfig *cfg : sortedConfigs) { auto *card = new AgentRowCard(*cfg); connect(card, &ListRowCard::clicked, this, [this, card]() { selectCard(card); }); connect(card, &ListRowCard::activated, this, [this, card]() { selectCard(card); accept(); }); section->addCard(card); if (cfg->name == currentName) { toSelect = card; sectionToExpand = section; } } contentLayout->addWidget(section); m_sections.append(section); } contentLayout->addStretch(1); m_scroll->setWidget(content); if (sectionToExpand) sectionToExpand->setExpanded(true); if (toSelect) { selectCard(toSelect); QTimer::singleShot(0, this, [this, toSelect]() { m_scroll->ensureWidgetVisible(toSelect, 0, 60); }); } applyFilter(m_filter ? m_filter->text() : QString()); } void AgentSelectionDialog::applyFilter(const QString &needle) { const QString trimmed = needle.trimmed(); for (auto *section : m_sections) { const int visible = section->applyFilter(trimmed); section->setVisible(visible > 0); if (!trimmed.isEmpty()) section->setExpanded(visible > 0); } } } // namespace QodeAssist::Settings