mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 02:49:12 -04:00
feat: Add agents pipelines
This commit is contained in:
471
sources/settings/AgentSelectionDialog.cpp
Normal file
471
sources/settings/AgentSelectionDialog.cpp
Normal file
@@ -0,0 +1,471 @@
|
||||
// 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 <coreplugin/icore.h>
|
||||
|
||||
#include <AgentFactory.hpp>
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QEnterEvent>
|
||||
#include <QEvent>
|
||||
#include <QFont>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QMap>
|
||||
#include <QMouseEvent>
|
||||
#include <QPushButton>
|
||||
#include <QScopedValueRollback>
|
||||
#include <QScrollArea>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
|
||||
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<bool> 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<QMouseEvent *>(event);
|
||||
if (me->button() == Qt::LeftButton) {
|
||||
setExpanded(!m_expanded);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
// -- AgentSelectionDialog ----------------------------------------------
|
||||
|
||||
AgentSelectionDialog::AgentSelectionDialog(
|
||||
const std::vector<AgentConfig> &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<QString, std::vector<const AgentConfig *>> 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
|
||||
Reference in New Issue
Block a user