feat: Add agents pipelines

This commit is contained in:
Petr Mironychev
2026-05-26 16:44:45 +02:00
parent 97236c6069
commit fb887967ed
15 changed files with 2498 additions and 0 deletions

View 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 &currentName,
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 &currentName)
{
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