mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-03 03:29:11 -04:00
refactor: Move to agent architecture
This commit is contained in:
@@ -1,274 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "AgentPipelinesPage.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "../../Version.hpp"
|
||||
#include "AgentRosterWidget.hpp"
|
||||
#include "AgentsSettingsPage.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "PipelinesConfig.hpp"
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
|
||||
#include <AgentFactory.hpp>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
AgentPipelinesPageNavigator::AgentPipelinesPageNavigator(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kSaveDebounceMs = 300;
|
||||
|
||||
struct SlotMeta
|
||||
{
|
||||
const char *title;
|
||||
const char *hint;
|
||||
};
|
||||
|
||||
const SlotMeta kSlotMeta[] = {
|
||||
{TrConstants::CODE_COMPLETION, TrConstants::SLOT_HINT_CODE_COMPLETION},
|
||||
{TrConstants::CHAT_ASSISTANT, TrConstants::SLOT_HINT_CHAT_ASSISTANT},
|
||||
{TrConstants::CHAT_COMPRESSION, TrConstants::SLOT_HINT_CHAT_COMPRESSION},
|
||||
{TrConstants::QUICK_REFACTOR, TrConstants::SLOT_HINT_QUICK_REFACTOR},
|
||||
};
|
||||
|
||||
class AgentPipelinesPageWidget : public Core::IOptionsPageWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(AgentPipelinesPageWidget)
|
||||
public:
|
||||
AgentPipelinesPageWidget(
|
||||
const QPointer<AgentFactory> &agentFactory,
|
||||
const QPointer<AgentPipelinesPageNavigator> &navigator,
|
||||
const QPointer<AgentsPageNavigator> &agentsNavigator)
|
||||
: m_agentFactory(agentFactory)
|
||||
, m_navigator(navigator)
|
||||
, m_agentsNavigator(agentsNavigator)
|
||||
{
|
||||
m_titleLabel = new QLabel(Tr::tr(TrConstants::AGENT_PIPELINES), this);
|
||||
QFont tf = m_titleLabel->font();
|
||||
tf.setBold(true);
|
||||
tf.setPixelSize(13);
|
||||
m_titleLabel->setFont(tf);
|
||||
|
||||
m_resetBtn = new QPushButton(Tr::tr(TrConstants::RESET_TO_DEFAULTS), this);
|
||||
|
||||
auto *headerRow = new QHBoxLayout;
|
||||
headerRow->setContentsMargins(0, 0, 0, 0);
|
||||
headerRow->setSpacing(8);
|
||||
headerRow->addWidget(m_titleLabel);
|
||||
headerRow->addStretch(1);
|
||||
headerRow->addWidget(m_resetBtn);
|
||||
|
||||
auto *headerSep = new QFrame(this);
|
||||
headerSep->setFrameShape(QFrame::HLine);
|
||||
headerSep->setFrameShadow(QFrame::Sunken);
|
||||
|
||||
m_rosters[0] = new AgentRosterWidget(this);
|
||||
m_rosters[1] = new AgentRosterWidget(this);
|
||||
m_rosters[2] = new AgentRosterWidget(this);
|
||||
m_rosters[3] = new AgentRosterWidget(this);
|
||||
|
||||
for (int i = 0; i < kRosterCount; ++i)
|
||||
m_rosters[i]->setSlot(Tr::tr(kSlotMeta[i].title), Tr::tr(kSlotMeta[i].hint), {});
|
||||
|
||||
auto *content = new QWidget(this);
|
||||
auto *contentLay = new QVBoxLayout(content);
|
||||
contentLay->setContentsMargins(0, 0, 0, 0);
|
||||
contentLay->setSpacing(12);
|
||||
for (int i = 0; i < kRosterCount; ++i)
|
||||
contentLay->addWidget(m_rosters[i]);
|
||||
contentLay->addStretch(1);
|
||||
|
||||
auto *scroll = new QScrollArea(this);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
scroll->setWidget(content);
|
||||
|
||||
auto *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(8, 8, 8, 8);
|
||||
root->setSpacing(6);
|
||||
root->addLayout(headerRow);
|
||||
root->addWidget(headerSep);
|
||||
root->addWidget(scroll, 1);
|
||||
|
||||
m_saveDebounce = new QTimer(this);
|
||||
m_saveDebounce->setSingleShot(true);
|
||||
m_saveDebounce->setInterval(kSaveDebounceMs);
|
||||
connect(m_saveDebounce, &QTimer::timeout, this, [this]() { persistRosters(); });
|
||||
|
||||
loadFromSettings();
|
||||
|
||||
connect(m_resetBtn, &QPushButton::clicked, this, &AgentPipelinesPageWidget::onReset);
|
||||
|
||||
for (int i = 0; i < kRosterCount; ++i) {
|
||||
connect(m_rosters[i], &AgentRosterWidget::editAgentRequested, this,
|
||||
&AgentPipelinesPageWidget::onEditAgent);
|
||||
connect(m_rosters[i], &AgentRosterWidget::rosterChanged, this,
|
||||
[this](const QStringList &) { m_saveDebounce->start(); });
|
||||
}
|
||||
}
|
||||
|
||||
~AgentPipelinesPageWidget() override
|
||||
{
|
||||
if (m_saveDebounce && m_saveDebounce->isActive()) {
|
||||
m_saveDebounce->stop();
|
||||
persistRosters();
|
||||
}
|
||||
}
|
||||
|
||||
void apply() final
|
||||
{
|
||||
if (m_saveDebounce && m_saveDebounce->isActive())
|
||||
m_saveDebounce->stop();
|
||||
persistRosters();
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int kRosterCount = 4;
|
||||
|
||||
void persistRosters()
|
||||
{
|
||||
PipelineRosters rosters;
|
||||
rosters.codeCompletion = m_rosters[0]->roster();
|
||||
rosters.chatAssistant = m_rosters[1]->roster();
|
||||
rosters.chatCompression = m_rosters[2]->roster();
|
||||
rosters.quickRefactor = m_rosters[3]->roster();
|
||||
QString err;
|
||||
if (!PipelinesConfig::save(rosters, &err)) {
|
||||
LOG_MESSAGE(QStringLiteral("[Pipelines] save failed (%1): %2")
|
||||
.arg(PipelinesConfig::filePath(), err));
|
||||
if (!m_saveErrorShown) {
|
||||
m_saveErrorShown = true;
|
||||
QMessageBox::warning(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr(TrConstants::AGENT_PIPELINES),
|
||||
tr("Failed to save pipelines.toml:\n%1\n\n"
|
||||
"Further save failures will only be logged.")
|
||||
.arg(err));
|
||||
}
|
||||
} else {
|
||||
m_saveErrorShown = false;
|
||||
}
|
||||
}
|
||||
|
||||
void onReset()
|
||||
{
|
||||
const auto reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr(TrConstants::RESET_SETTINGS),
|
||||
Tr::tr(TrConstants::CONFIRMATION),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
QString err;
|
||||
if (!PipelinesConfig::save(PipelineRosters::defaults(), &err))
|
||||
LOG_MESSAGE(QStringLiteral("[Pipelines] failed to reset rosters: %1").arg(err));
|
||||
|
||||
m_saveErrorShown = false;
|
||||
loadFromSettings();
|
||||
}
|
||||
|
||||
void onEditAgent(const QString &name)
|
||||
{
|
||||
if (m_agentsNavigator)
|
||||
m_agentsNavigator->requestSelectAgent(name);
|
||||
if (m_navigator)
|
||||
emit m_navigator->editAgentRequested(name);
|
||||
|
||||
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 83)
|
||||
Core::ICore::showSettings(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
|
||||
#else
|
||||
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
|
||||
#endif
|
||||
}
|
||||
|
||||
void loadFromSettings()
|
||||
{
|
||||
const PipelinesLoadResult lr = PipelinesConfig::load();
|
||||
if (lr.status == PipelinesLoadStatus::ParseError
|
||||
|| lr.status == PipelinesLoadStatus::SchemaError) {
|
||||
QMessageBox::warning(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr(TrConstants::AGENT_PIPELINES),
|
||||
tr("pipelines.toml has issues — using defaults for affected entries:\n%1\n\n"
|
||||
"Click OK to continue. Changes you make here will overwrite the file.")
|
||||
.arg(lr.message));
|
||||
}
|
||||
|
||||
AgentFactory *factory = m_agentFactory.data();
|
||||
m_rosters[0]->setRoster(lr.rosters.codeCompletion, factory);
|
||||
m_rosters[1]->setRoster(lr.rosters.chatAssistant, factory);
|
||||
m_rosters[2]->setRoster(lr.rosters.chatCompression, factory);
|
||||
m_rosters[3]->setRoster(lr.rosters.quickRefactor, factory);
|
||||
}
|
||||
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
QPointer<AgentPipelinesPageNavigator> m_navigator;
|
||||
QPointer<AgentsPageNavigator> m_agentsNavigator;
|
||||
|
||||
QLabel *m_titleLabel = nullptr;
|
||||
QPushButton *m_resetBtn = nullptr;
|
||||
|
||||
AgentRosterWidget *m_rosters[kRosterCount] = {};
|
||||
|
||||
QTimer *m_saveDebounce = nullptr;
|
||||
bool m_saveErrorShown = false;
|
||||
};
|
||||
|
||||
class AgentPipelinesOptionsPage final : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
AgentPipelinesOptionsPage(
|
||||
AgentFactory *agentFactory,
|
||||
AgentPipelinesPageNavigator *navigator,
|
||||
AgentsPageNavigator *agentsNavigator)
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_AGENT_PIPELINES_PAGE_ID);
|
||||
setDisplayName(Tr::tr(TrConstants::AGENT_PIPELINES));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
const QPointer<AgentFactory> factoryPtr(agentFactory);
|
||||
const QPointer<AgentPipelinesPageNavigator> navPtr(navigator);
|
||||
const QPointer<AgentsPageNavigator> agentsNavPtr(agentsNavigator);
|
||||
setWidgetCreator([factoryPtr, navPtr, agentsNavPtr] {
|
||||
return new AgentPipelinesPageWidget(factoryPtr, navPtr, agentsNavPtr);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
|
||||
AgentFactory *agentFactory,
|
||||
AgentPipelinesPageNavigator *navigator,
|
||||
AgentsPageNavigator *agentsNavigator)
|
||||
{
|
||||
return std::make_unique<AgentPipelinesOptionsPage>(
|
||||
agentFactory, navigator, agentsNavigator);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
#include "AgentPipelinesPage.moc"
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace Core {
|
||||
class IOptionsPage;
|
||||
}
|
||||
|
||||
namespace QodeAssist {
|
||||
class AgentFactory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class AgentsPageNavigator;
|
||||
|
||||
class AgentPipelinesPageNavigator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(AgentPipelinesPageNavigator)
|
||||
public:
|
||||
explicit AgentPipelinesPageNavigator(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void editAgentRequested(const QString &agentName);
|
||||
};
|
||||
|
||||
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
|
||||
AgentFactory *agentFactory,
|
||||
AgentPipelinesPageNavigator *navigator,
|
||||
AgentsPageNavigator *agentsNavigator);
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
#include <Agent.hpp>
|
||||
#include <AgentConfig.hpp>
|
||||
@@ -23,6 +24,7 @@
|
||||
#include <AgentRouter.hpp>
|
||||
|
||||
#include "AgentSelectionDialog.hpp"
|
||||
#include "SettingsTheme.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
@@ -73,73 +75,67 @@ struct Theme
|
||||
|
||||
Theme::Pill Theme::pill(PillKind k) const
|
||||
{
|
||||
if (dark) {
|
||||
switch (k) {
|
||||
case PillKind::Template: return {{0x2c, 0x3f, 0x5a}, {0xcf, 0xe1, 0xf7}, {0x4a, 0x62, 0x86}};
|
||||
case PillKind::On: return {{0x2f, 0x45, 0x30}, {0xbc, 0xe0, 0xbd}, {0x4a, 0x6c, 0x4b}};
|
||||
case PillKind::Off: return {{0x3a, 0x3a, 0x3a}, {0x8a, 0x8a, 0x8a}, {0x4a, 0x4a, 0x4a}};
|
||||
case PillKind::User: return {{0x4a, 0x3f, 0x24}, {0xe6, 0xcd, 0x92}, {0x6a, 0x5a, 0x30}};
|
||||
case PillKind::Active: return {{0x3a, 0x6a, 0x28}, {0xff, 0xff, 0xff}, {0x4a, 0x80, 0x38}};
|
||||
case PillKind::Match: return {{0x27, 0x38, 0x4a}, {0xbd, 0xd7, 0xee}, {0x3f, 0x5a, 0x78}};
|
||||
case PillKind::Tag: return {{0x2e, 0x2e, 0x3a}, {0xb9, 0xb9, 0xcf}, {0x46, 0x46, 0x5a}};
|
||||
case PillKind::Neutral: return {{0x3a, 0x3a, 0x3a}, {0xcf, 0xcf, 0xcf}, {0x4a, 0x4a, 0x4a}};
|
||||
}
|
||||
} else {
|
||||
switch (k) {
|
||||
case PillKind::Template: return {{0xdb, 0xe7, 0xf6}, {0x1f, 0x3f, 0x73}, {0xa8, 0xc1, 0xe0}};
|
||||
case PillKind::On: return {{0xdb, 0xe9, 0xd3}, {0x2c, 0x5a, 0x1c}, {0xa3, 0xbc, 0x97}};
|
||||
case PillKind::Off: return {{0xec, 0xec, 0xec}, {0x7a, 0x7a, 0x7a}, {0xc8, 0xc8, 0xc8}};
|
||||
case PillKind::User: return {{0xf0, 0xe4, 0xcf}, {0x75, 0x54, 0x1a}, {0xcd, 0xb9, 0x8a}};
|
||||
case PillKind::Active: return {{0x2c, 0x5a, 0x1c}, {0xff, 0xff, 0xff}, {0x2c, 0x5a, 0x1c}};
|
||||
case PillKind::Match: return {{0xd6, 0xe8, 0xf7}, {0x1a, 0x4a, 0x7a}, {0x8a, 0xb1, 0xd5}};
|
||||
case PillKind::Tag: return {{0xe7, 0xe7, 0xf2}, {0x46, 0x46, 0x6e}, {0xc1, 0xc1, 0xd5}};
|
||||
case PillKind::Neutral: return {{0xe3, 0xe3, 0xe3}, {0x3a, 0x3a, 0x3a}, {0xbc, 0xbc, 0xbc}};
|
||||
}
|
||||
const QColor bg = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
||||
const QColor fg = Utils::creatorColor(Utils::Theme::TextColorNormal);
|
||||
const QColor muted = Utils::creatorColor(Utils::Theme::PanelTextColorMid);
|
||||
const QColor faint = Utils::creatorColor(Utils::Theme::TextColorDisabled);
|
||||
const QColor link = Utils::creatorColor(Utils::Theme::TextColorLink);
|
||||
const QColor selBg = Utils::creatorColor(Utils::Theme::BackgroundColorSelected);
|
||||
const QColor line = Utils::creatorColor(Utils::Theme::SplitterColor);
|
||||
const bool isDark = bg.lightness() < 128;
|
||||
|
||||
const auto tint = [&](const QColor &role) -> Pill {
|
||||
return {mix(bg, role, 0.16), isDark ? mix(fg, role, 0.65) : role, mix(bg, role, 0.42)};
|
||||
};
|
||||
|
||||
switch (k) {
|
||||
case PillKind::Template:
|
||||
return tint(link);
|
||||
case PillKind::On:
|
||||
return tint(Utils::creatorColor(Utils::Theme::IconsRunColor));
|
||||
case PillKind::Off:
|
||||
return {mix(bg, faint, 0.12), faint, mix(bg, faint, 0.30)};
|
||||
case PillKind::User:
|
||||
return tint(Utils::creatorColor(Utils::Theme::IconsWarningColor));
|
||||
case PillKind::Active:
|
||||
return {selBg, fg, link};
|
||||
case PillKind::Match:
|
||||
return tint(link);
|
||||
case PillKind::Tag:
|
||||
case PillKind::Neutral:
|
||||
return {mix(bg, muted, 0.14), muted, mix(bg, muted, 0.32)};
|
||||
}
|
||||
return {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||
return {bg, fg, line};
|
||||
}
|
||||
|
||||
Theme themeFor(const QWidget *w)
|
||||
Theme themeFor(const QWidget *)
|
||||
{
|
||||
const QPalette pal = w ? w->palette() : QApplication::palette();
|
||||
const bool dark = pal.color(QPalette::Window).lightness() < 128;
|
||||
const QColor bg = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
||||
const QColor fg = Utils::creatorColor(Utils::Theme::TextColorNormal);
|
||||
const QColor muted = Utils::creatorColor(Utils::Theme::PanelTextColorMid);
|
||||
const QColor faint = Utils::creatorColor(Utils::Theme::TextColorDisabled);
|
||||
const QColor line = Utils::creatorColor(Utils::Theme::SplitterColor);
|
||||
const QColor selBg = Utils::creatorColor(Utils::Theme::BackgroundColorSelected);
|
||||
const bool isDark = bg.lightness() < 128;
|
||||
|
||||
Theme t;
|
||||
t.dark = dark;
|
||||
if (dark) {
|
||||
t.pageBg = {0x2b, 0x2b, 0x2b};
|
||||
t.cardBg = {0x2f, 0x2f, 0x2f};
|
||||
t.cardBorder = {0x4a, 0x4a, 0x4a};
|
||||
t.groupBorder = {0x4a, 0x4a, 0x4a};
|
||||
t.rowSeparator = {0x3a, 0x3a, 0x3a};
|
||||
t.rowMatchBg = {0x2d, 0x3d, 0x24};
|
||||
t.listHeader = {0x25, 0x25, 0x25};
|
||||
t.text = {0xe6, 0xe6, 0xe6};
|
||||
t.textSoft = {0xc2, 0xc2, 0xc2};
|
||||
t.textMute = {0x9a, 0x9a, 0x9a};
|
||||
t.textFaint = {0x7a, 0x7a, 0x7a};
|
||||
t.matchChipBg = {0x26, 0x26, 0x26};
|
||||
t.matchChipBorder = {0x3a, 0x3a, 0x3a};
|
||||
t.matchChipText = {0xc2, 0xc2, 0xc2};
|
||||
t.codeBg = {0x26, 0x26, 0x26};
|
||||
t.codeBorder = {0x3a, 0x3a, 0x3a};
|
||||
} else {
|
||||
t.pageBg = {0xed, 0xed, 0xed};
|
||||
t.cardBg = {0xf8, 0xf8, 0xf8};
|
||||
t.cardBorder = {0xbd, 0xbd, 0xbd};
|
||||
t.groupBorder = {0xb8, 0xb8, 0xb8};
|
||||
t.rowSeparator = {0xe0, 0xe0, 0xe0};
|
||||
t.rowMatchBg = {0xe8, 0xf3, 0xdf};
|
||||
t.listHeader = {0xe8, 0xe8, 0xe8};
|
||||
t.text = {0x1c, 0x1c, 0x1c};
|
||||
t.textSoft = {0x3a, 0x3a, 0x3a};
|
||||
t.textMute = {0x5a, 0x5a, 0x5a};
|
||||
t.textFaint = {0x8a, 0x8a, 0x8a};
|
||||
t.matchChipBg = {0xea, 0xea, 0xea};
|
||||
t.matchChipBorder = {0xcf, 0xcf, 0xcf};
|
||||
t.matchChipText = {0x3a, 0x3a, 0x3a};
|
||||
t.codeBg = {0xea, 0xea, 0xea};
|
||||
t.codeBorder = {0xcf, 0xcf, 0xcf};
|
||||
}
|
||||
t.dark = isDark;
|
||||
t.pageBg = mix(bg, isDark ? QColor(Qt::black) : QColor(Qt::white), 0.04);
|
||||
t.cardBg = bg;
|
||||
t.cardBorder = line;
|
||||
t.groupBorder = line;
|
||||
t.rowSeparator = line;
|
||||
t.rowMatchBg = selBg;
|
||||
t.listHeader = mix(bg, fg, 0.05);
|
||||
t.text = fg;
|
||||
t.textSoft = muted;
|
||||
t.textMute = muted;
|
||||
t.textFaint = faint;
|
||||
t.matchChipBg = mix(bg, fg, 0.06);
|
||||
t.matchChipBorder = line;
|
||||
t.matchChipText = muted;
|
||||
t.codeBg = mix(bg, fg, 0.06);
|
||||
t.codeBorder = line;
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -153,7 +149,7 @@ QString pillStyle(const Theme &t, PillKind k)
|
||||
const auto p = t.pill(k);
|
||||
return QStringLiteral(
|
||||
"background:%1; color:%2; border:1px solid %3;"
|
||||
"padding:0px 5px; border-radius:2px;"
|
||||
"padding:1px 6px; border-radius:3px;"
|
||||
"font-size:10px;")
|
||||
.arg(hex(p.bg), hex(p.fg), hex(p.border));
|
||||
}
|
||||
@@ -199,9 +195,11 @@ public:
|
||||
AgentRosterRow(int index,
|
||||
const QString &name,
|
||||
const AgentConfig *cfg,
|
||||
const QString &model,
|
||||
bool active,
|
||||
bool first,
|
||||
bool last,
|
||||
bool orderable,
|
||||
const Theme &theme,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
@@ -217,7 +215,7 @@ private:
|
||||
bool active,
|
||||
bool isUser,
|
||||
const Theme &t);
|
||||
QWidget *buildMetaLine(const AgentConfig *cfg, bool active, const Theme &t);
|
||||
QWidget *buildMetaLine(const AgentConfig *cfg, bool active, bool showMatch, const Theme &t);
|
||||
QWidget *buildActions(const Theme &t, bool first, bool last);
|
||||
|
||||
int m_index;
|
||||
@@ -226,9 +224,11 @@ private:
|
||||
AgentRosterRow::AgentRosterRow(int index,
|
||||
const QString &name,
|
||||
const AgentConfig *cfg,
|
||||
const QString &model,
|
||||
bool active,
|
||||
bool first,
|
||||
bool last,
|
||||
bool orderable,
|
||||
const Theme &theme,
|
||||
QWidget *parent)
|
||||
: QFrame(parent), m_index(index)
|
||||
@@ -246,56 +246,57 @@ AgentRosterRow::AgentRosterRow(int index,
|
||||
outer->setContentsMargins(8, 6, 8, 6);
|
||||
outer->setSpacing(8);
|
||||
|
||||
auto *moveCol = new QVBoxLayout;
|
||||
moveCol->setContentsMargins(0, 0, 0, 0);
|
||||
moveCol->setSpacing(1);
|
||||
auto makeArrow = [&](const QString &glyph, const QString &tip, bool enabled) {
|
||||
auto *b = new QToolButton(this);
|
||||
b->setText(glyph);
|
||||
b->setToolTip(tip);
|
||||
b->setEnabled(enabled);
|
||||
b->setAutoRaise(true);
|
||||
b->setFixedSize(18, 14);
|
||||
QFont f = b->font();
|
||||
f.setPointSizeF(f.pointSizeF() * 0.75);
|
||||
b->setFont(f);
|
||||
return b;
|
||||
};
|
||||
auto *upBtn = makeArrow(QStringLiteral("▲"), tr("Move up"), !first);
|
||||
auto *dnBtn = makeArrow(QStringLiteral("▼"), tr("Move down"), !last);
|
||||
moveCol->addWidget(upBtn);
|
||||
moveCol->addWidget(dnBtn);
|
||||
outer->addLayout(moveCol);
|
||||
if (orderable) {
|
||||
auto *moveCol = new QVBoxLayout;
|
||||
moveCol->setContentsMargins(0, 0, 0, 0);
|
||||
moveCol->setSpacing(1);
|
||||
auto makeArrow = [&](const QString &glyph, const QString &tip, bool enabled) {
|
||||
auto *b = new QToolButton(this);
|
||||
b->setText(glyph);
|
||||
b->setToolTip(tip);
|
||||
b->setEnabled(enabled);
|
||||
b->setAutoRaise(true);
|
||||
b->setFixedSize(18, 14);
|
||||
QFont f = b->font();
|
||||
f.setPointSizeF(f.pointSizeF() * 0.75);
|
||||
b->setFont(f);
|
||||
return b;
|
||||
};
|
||||
auto *upBtn = makeArrow(QStringLiteral("▲"), tr("Move up"), !first);
|
||||
auto *dnBtn = makeArrow(QStringLiteral("▼"), tr("Move down"), !last);
|
||||
moveCol->addWidget(upBtn);
|
||||
moveCol->addWidget(dnBtn);
|
||||
outer->addLayout(moveCol);
|
||||
|
||||
auto *idxLbl = new QLabel(QStringLiteral("%1.").arg(index + 1), this);
|
||||
QFont monoFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
idxLbl->setFont(monoFont);
|
||||
QPalette idxPal = idxLbl->palette();
|
||||
idxPal.setColor(QPalette::WindowText, active ? theme.text : theme.textFaint);
|
||||
idxLbl->setPalette(idxPal);
|
||||
idxLbl->setFixedWidth(22);
|
||||
idxLbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
outer->addWidget(idxLbl);
|
||||
connect(upBtn, &QToolButton::clicked, this, [this]() { emit moveUpRequested(m_index); });
|
||||
connect(dnBtn, &QToolButton::clicked, this, [this]() { emit moveDownRequested(m_index); });
|
||||
|
||||
auto *idxLbl = new QLabel(QStringLiteral("%1.").arg(index + 1), this);
|
||||
QFont monoFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
idxLbl->setFont(monoFont);
|
||||
QPalette idxPal = idxLbl->palette();
|
||||
idxPal.setColor(QPalette::WindowText, active ? theme.text : theme.textFaint);
|
||||
idxLbl->setPalette(idxPal);
|
||||
idxLbl->setFixedWidth(22);
|
||||
idxLbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
outer->addWidget(idxLbl);
|
||||
}
|
||||
|
||||
auto *body = new QVBoxLayout;
|
||||
body->setContentsMargins(0, 0, 0, 0);
|
||||
body->setSpacing(2);
|
||||
|
||||
const QString displayName = cfg ? cfg->name : tr("%1 (missing)").arg(name);
|
||||
const QString model = cfg ? cfg->model : QString();
|
||||
const bool isUser = cfg && cfg->isUserSource();
|
||||
|
||||
body->addWidget(buildIdentityLine(displayName, model, active, isUser, theme));
|
||||
body->addWidget(buildMetaLine(cfg, active, theme));
|
||||
body->addWidget(buildMetaLine(cfg, active, /*showMatch*/ orderable, theme));
|
||||
outer->addLayout(body, /*stretch*/ 1);
|
||||
|
||||
outer->addWidget(buildActions(theme, first, last));
|
||||
|
||||
if (cfg && !cfg->description.isEmpty())
|
||||
setToolTip(cfg->description);
|
||||
|
||||
connect(upBtn, &QToolButton::clicked, this, [this]() { emit moveUpRequested(m_index); });
|
||||
connect(dnBtn, &QToolButton::clicked, this, [this]() { emit moveDownRequested(m_index); });
|
||||
}
|
||||
|
||||
QWidget *AgentRosterRow::buildIdentityLine(const QString &displayName,
|
||||
@@ -333,7 +334,8 @@ QWidget *AgentRosterRow::buildIdentityLine(const QString &displayName,
|
||||
return w;
|
||||
}
|
||||
|
||||
QWidget *AgentRosterRow::buildMetaLine(const AgentConfig *cfg, bool active, const Theme &t)
|
||||
QWidget *AgentRosterRow::buildMetaLine(const AgentConfig *cfg, bool active, bool showMatch,
|
||||
const Theme &t)
|
||||
{
|
||||
auto *w = new QWidget(this);
|
||||
auto *line = new QHBoxLayout(w);
|
||||
@@ -341,31 +343,33 @@ QWidget *AgentRosterRow::buildMetaLine(const AgentConfig *cfg, bool active, cons
|
||||
line->setSpacing(4);
|
||||
|
||||
if (cfg) {
|
||||
const auto sm = summarise(cfg->match);
|
||||
auto *chip = new QLabel(w);
|
||||
const QColor bg = active ? t.pill(PillKind::Match).bg : t.matchChipBg;
|
||||
const QColor bd = active ? t.pill(PillKind::Match).border : t.matchChipBorder;
|
||||
const QColor fg = active ? t.pill(PillKind::Match).fg : t.matchChipText;
|
||||
QString chipText = QStringLiteral(
|
||||
"<span style='opacity:0.7'>%1</span> "
|
||||
"<span style='color:%2'>%3:</span> %4")
|
||||
.arg(sm.icon,
|
||||
hex(active ? t.pill(PillKind::Match).fg : t.textFaint),
|
||||
sm.kind,
|
||||
sm.value.toHtmlEscaped());
|
||||
chip->setTextFormat(Qt::RichText);
|
||||
chip->setText(chipText);
|
||||
chip->setStyleSheet(QStringLiteral("background:%1; color:%2; border:1px solid %3;"
|
||||
"padding:0px 6px; border-radius:2px; font-size:11px;")
|
||||
.arg(hex(bg), hex(fg), hex(bd)));
|
||||
chip->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
line->addWidget(chip);
|
||||
if (showMatch) {
|
||||
const auto sm = summarise(cfg->match);
|
||||
auto *chip = new QLabel(w);
|
||||
const QColor bg = active ? t.pill(PillKind::Match).bg : t.matchChipBg;
|
||||
const QColor bd = active ? t.pill(PillKind::Match).border : t.matchChipBorder;
|
||||
const QColor fg = active ? t.pill(PillKind::Match).fg : t.matchChipText;
|
||||
QString chipText = QStringLiteral(
|
||||
"<span style='opacity:0.7'>%1</span> "
|
||||
"<span style='color:%2'>%3:</span> %4")
|
||||
.arg(sm.icon,
|
||||
hex(active ? t.pill(PillKind::Match).fg : t.textFaint),
|
||||
sm.kind,
|
||||
sm.value.toHtmlEscaped());
|
||||
chip->setTextFormat(Qt::RichText);
|
||||
chip->setText(chipText);
|
||||
chip->setStyleSheet(QStringLiteral("background:%1; color:%2; border:1px solid %3;"
|
||||
"padding:1px 6px; border-radius:3px; font-size:11px;")
|
||||
.arg(hex(bg), hex(fg), hex(bd)));
|
||||
chip->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
line->addWidget(chip);
|
||||
|
||||
auto *arrow = new QLabel(QStringLiteral("→"), w);
|
||||
QPalette ap = arrow->palette();
|
||||
ap.setColor(QPalette::WindowText, t.textFaint);
|
||||
arrow->setPalette(ap);
|
||||
line->addWidget(arrow);
|
||||
auto *arrow = new QLabel(QStringLiteral("→"), w);
|
||||
QPalette ap = arrow->palette();
|
||||
ap.setColor(QPalette::WindowText, t.textFaint);
|
||||
arrow->setPalette(ap);
|
||||
line->addWidget(arrow);
|
||||
}
|
||||
|
||||
if (!cfg->providerInstance.isEmpty())
|
||||
line->addWidget(makePill(cfg->providerInstance, PillKind::Template, t, w));
|
||||
@@ -432,13 +436,10 @@ AgentRosterWidget::AgentRosterWidget(QWidget *parent)
|
||||
auto *titleRow = new QHBoxLayout;
|
||||
titleRow->setContentsMargins(0, 0, 0, 0);
|
||||
titleRow->setSpacing(6);
|
||||
m_accentDot = new QLabel(this);
|
||||
m_accentDot->setFixedSize(10, 10);
|
||||
m_titleLabel = new QLabel(this);
|
||||
QFont tf = m_titleLabel->font();
|
||||
tf.setBold(true);
|
||||
m_titleLabel->setFont(tf);
|
||||
titleRow->addWidget(m_accentDot);
|
||||
titleRow->addWidget(m_titleLabel);
|
||||
titleRow->addStretch(1);
|
||||
outer->addLayout(titleRow);
|
||||
@@ -456,13 +457,13 @@ AgentRosterWidget::AgentRosterWidget(QWidget *parent)
|
||||
m_rowsFrame = new QFrame(this);
|
||||
m_rowsFrame->setObjectName(QStringLiteral("rosterCard"));
|
||||
m_rowsFrame->setStyleSheet(
|
||||
QStringLiteral("QFrame#rosterCard { background:%1; border:1px solid %2; border-radius:2px; }")
|
||||
QStringLiteral("QFrame#rosterCard { background:%1; border:1px solid %2; border-radius:4px; }")
|
||||
.arg(hex(t.cardBg), hex(t.cardBorder)));
|
||||
m_rowsLayout = new QVBoxLayout(m_rowsFrame);
|
||||
m_rowsLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_rowsLayout->setSpacing(0);
|
||||
|
||||
m_emptyHint = new QLabel(tr("No agents in roster. Click \"Add agent…\" to populate."),
|
||||
m_emptyHint = new QLabel(tr("No agent selected yet — use \"Add agent…\" below."),
|
||||
m_rowsFrame);
|
||||
m_emptyHint->setAlignment(Qt::AlignCenter);
|
||||
m_emptyHint->setContentsMargins(10, 12, 10, 12);
|
||||
@@ -495,39 +496,71 @@ AgentRosterWidget::AgentRosterWidget(QWidget *parent)
|
||||
connect(m_addBtn, &QPushButton::clicked, this, &AgentRosterWidget::onAddClicked);
|
||||
}
|
||||
|
||||
void AgentRosterWidget::setSlot(const QString &title, const QString &hint, const QColor &accent)
|
||||
void AgentRosterWidget::setSlot(
|
||||
const QString &title,
|
||||
const QString &hint,
|
||||
const QStringList &presetTags)
|
||||
{
|
||||
m_presetTags = presetTags;
|
||||
m_titleLabel->setText(title);
|
||||
m_hintLabel->setText(hint);
|
||||
m_hintLabel->setVisible(!hint.isEmpty());
|
||||
if (accent.isValid()) {
|
||||
m_accentDot->setStyleSheet(
|
||||
QStringLiteral("background:%1; border:1px solid %2;")
|
||||
.arg(accent.name(), accent.darker(140).name()));
|
||||
m_accentDot->setVisible(true);
|
||||
} else {
|
||||
m_accentDot->setStyleSheet({});
|
||||
m_accentDot->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRosterWidget::setRoster(const QStringList &names, AgentFactory *factory)
|
||||
{
|
||||
m_factory = factory;
|
||||
if (m_factory != factory) {
|
||||
QObject::disconnect(m_factoryConn);
|
||||
QObject::disconnect(m_modelConn);
|
||||
m_factory = factory;
|
||||
if (m_factory) {
|
||||
m_factoryConn = connect(m_factory, &AgentFactory::agentsChanged, this,
|
||||
[this] { rebuildRows(); });
|
||||
m_modelConn = connect(m_factory, &AgentFactory::agentModelChanged, this,
|
||||
[this](const QString &name) {
|
||||
if (m_names.contains(name))
|
||||
rebuildRows();
|
||||
});
|
||||
}
|
||||
}
|
||||
m_names = names;
|
||||
rebuildRows();
|
||||
}
|
||||
|
||||
void AgentRosterWidget::setRoutingContext(const AgentRouter::Context &ctx)
|
||||
void AgentRosterWidget::setOrderable(bool orderable)
|
||||
{
|
||||
m_routingCtx = ctx;
|
||||
recomputeActive();
|
||||
if (m_orderable == orderable)
|
||||
return;
|
||||
m_orderable = orderable;
|
||||
rebuildRows();
|
||||
}
|
||||
|
||||
void AgentRosterWidget::setSingle(bool single)
|
||||
{
|
||||
if (m_single == single)
|
||||
return;
|
||||
m_single = single;
|
||||
if (single)
|
||||
m_orderable = false;
|
||||
rebuildRows();
|
||||
}
|
||||
|
||||
void AgentRosterWidget::applyMode()
|
||||
{
|
||||
const bool hasEntry = !m_names.isEmpty();
|
||||
m_addBtn->setText((m_single && hasEntry) ? tr("Change agent…") : tr("+ Add agent…"));
|
||||
|
||||
QString footer;
|
||||
if (!m_single)
|
||||
footer = m_orderable ? tr("first matching agent is used")
|
||||
: tr("you pick the active agent in the chat panel");
|
||||
m_footerHint->setText(footer);
|
||||
m_footerHint->setVisible(!footer.isEmpty());
|
||||
}
|
||||
|
||||
void AgentRosterWidget::recomputeActive()
|
||||
{
|
||||
if (!m_factory || m_names.isEmpty()
|
||||
if (!m_orderable || !m_factory || m_names.isEmpty()
|
||||
|| (m_routingCtx.filePath.isEmpty() && m_routingCtx.projectName.isEmpty())) {
|
||||
m_activeIndex = -1;
|
||||
return;
|
||||
@@ -550,6 +583,8 @@ void AgentRosterWidget::rebuildRows()
|
||||
delete it;
|
||||
}
|
||||
|
||||
applyMode();
|
||||
|
||||
if (m_names.isEmpty()) {
|
||||
m_emptyHint->setVisible(true);
|
||||
m_rowsLayout->addWidget(m_emptyHint);
|
||||
@@ -562,12 +597,15 @@ void AgentRosterWidget::rebuildRows()
|
||||
for (int i = 0; i < m_names.size(); ++i) {
|
||||
const QString &name = m_names.at(i);
|
||||
const AgentConfig *cfg = m_factory ? m_factory->configByName(name) : nullptr;
|
||||
const QString model = cfg ? cfg->model : QString();
|
||||
auto *row = new AgentRosterRow(i,
|
||||
name,
|
||||
cfg,
|
||||
model,
|
||||
i == m_activeIndex,
|
||||
/*first*/ i == 0,
|
||||
/*last*/ i == m_names.size() - 1,
|
||||
m_orderable,
|
||||
t,
|
||||
m_rowsFrame);
|
||||
connect(row, &AgentRosterRow::moveUpRequested, this, &AgentRosterWidget::onRowMoveUp);
|
||||
@@ -584,16 +622,25 @@ void AgentRosterWidget::onAddClicked()
|
||||
return;
|
||||
|
||||
AgentSelectionDialog dialog(m_factory->configs(),
|
||||
/*currentName*/ QString{},
|
||||
/*currentName*/ m_single ? m_names.value(0) : QString{},
|
||||
m_factory.data(),
|
||||
m_presetTags,
|
||||
Core::ICore::dialogParent());
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
const QString picked = dialog.selectedName();
|
||||
if (picked.isEmpty() || m_names.contains(picked))
|
||||
if (picked.isEmpty())
|
||||
return;
|
||||
|
||||
m_names.append(picked);
|
||||
if (m_single) {
|
||||
if (m_names.size() == 1 && m_names.first() == picked)
|
||||
return;
|
||||
m_names = {picked};
|
||||
} else {
|
||||
if (m_names.contains(picked))
|
||||
return;
|
||||
m_names.append(picked);
|
||||
}
|
||||
rebuildRows();
|
||||
emit rosterChanged(m_names);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
@@ -32,12 +31,23 @@ class AgentRosterWidget : public QWidget
|
||||
public:
|
||||
explicit AgentRosterWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setSlot(const QString &title, const QString &hint, const QColor &accent);
|
||||
void setSlot(
|
||||
const QString &title,
|
||||
const QString &hint,
|
||||
const QStringList &presetTags = {});
|
||||
void setRoster(const QStringList &names, AgentFactory *factory);
|
||||
|
||||
[[nodiscard]] QStringList roster() const { return m_names; }
|
||||
// When false, the list is an unordered set: no move arrows, no position
|
||||
// numbers, no "first matching" routing hint. Used by pipelines where the
|
||||
// user — not a router — chooses the agent (e.g. the chat picker).
|
||||
void setOrderable(bool orderable);
|
||||
|
||||
void setRoutingContext(const AgentRouter::Context &ctx);
|
||||
// When true, the card holds at most one agent: "Add" becomes "Change",
|
||||
// selecting replaces the current entry, and the routing footer is hidden.
|
||||
// Implies a non-orderable list. Used by single-agent pipelines.
|
||||
void setSingle(bool single);
|
||||
|
||||
[[nodiscard]] QStringList roster() const { return m_names; }
|
||||
|
||||
signals:
|
||||
void rosterChanged(const QStringList &names);
|
||||
@@ -46,6 +56,7 @@ signals:
|
||||
private:
|
||||
void rebuildRows();
|
||||
void recomputeActive();
|
||||
void applyMode();
|
||||
|
||||
void onAddClicked();
|
||||
void onRowMoveUp(int index);
|
||||
@@ -54,11 +65,15 @@ private:
|
||||
void onRowEdit(int index);
|
||||
|
||||
QStringList m_names;
|
||||
QStringList m_presetTags;
|
||||
QPointer<AgentFactory> m_factory;
|
||||
QMetaObject::Connection m_factoryConn;
|
||||
QMetaObject::Connection m_modelConn;
|
||||
AgentRouter::Context m_routingCtx;
|
||||
int m_activeIndex = -1;
|
||||
bool m_orderable = true;
|
||||
bool m_single = false;
|
||||
|
||||
QLabel *m_accentDot = nullptr;
|
||||
QLabel *m_titleLabel = nullptr;
|
||||
QLabel *m_hintLabel = nullptr;
|
||||
QFrame *m_rowsFrame = nullptr;
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
#include "AgentSelectionDialog.hpp"
|
||||
|
||||
#include "AgentSlotWidget.hpp"
|
||||
#include "PipelinesConfig.hpp"
|
||||
#include "Pill.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "TagFilterStrip.hpp"
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
@@ -23,6 +24,7 @@
|
||||
#include <QPushButton>
|
||||
#include <QScopedValueRollback>
|
||||
#include <QScrollArea>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
@@ -56,6 +58,14 @@ void ListRowCard::setSelected(bool selected)
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
bool ListRowCard::hasAllTags(const QSet<QString> &activeTags) const
|
||||
{
|
||||
for (const QString &tag : activeTags)
|
||||
if (!m_itemTags.contains(tag))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ListRowCard::buildSearchHaystack(const QStringList &parts)
|
||||
{
|
||||
m_searchHaystack = parts.join(QLatin1Char(' ')).toLower();
|
||||
@@ -124,8 +134,9 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
|
||||
: ListRowCard(parent)
|
||||
{
|
||||
setItemName(cfg.name);
|
||||
setItemTags(cfg.tags);
|
||||
QStringList haystack{cfg.name, cfg.providerInstance, cfg.model,
|
||||
cfg.description, cfg.role,
|
||||
cfg.description, cfg.systemPrompt,
|
||||
cfg.endpoint};
|
||||
haystack += cfg.tags;
|
||||
buildSearchHaystack(haystack);
|
||||
@@ -147,10 +158,7 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
|
||||
|
||||
Pill *sourcePill = nullptr;
|
||||
if (cfg.isUserSource()) {
|
||||
sourcePill = new Pill(
|
||||
Pill::User,
|
||||
cfg.overridesBundled ? Tr::tr("Override") : Tr::tr("User"),
|
||||
this);
|
||||
sourcePill = new Pill(Pill::User, Tr::tr("User"), this);
|
||||
}
|
||||
|
||||
auto *description = new QLabel(this);
|
||||
@@ -234,8 +242,8 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
|
||||
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.systemPrompt.isEmpty())
|
||||
tooltip += Tr::tr("System prompt: %1\n").arg(cfg.systemPrompt);
|
||||
if (!cfg.endpoint.isEmpty())
|
||||
tooltip += Tr::tr("Endpoint: %1\n").arg(cfg.endpoint);
|
||||
setToolTip(tooltip.trimmed());
|
||||
@@ -257,6 +265,9 @@ ProviderSection::ProviderSection(const QString &name, QWidget *parent)
|
||||
ap.setColor(QPalette::WindowText, ap.color(QPalette::Mid));
|
||||
m_arrow->setPalette(ap);
|
||||
|
||||
m_count = new QLabel;
|
||||
CardStyle::applySectionFont(m_count);
|
||||
|
||||
m_header = new QFrame;
|
||||
m_header->setObjectName(QStringLiteral("ProviderHeader"));
|
||||
m_header->setCursor(Qt::PointingHandCursor);
|
||||
@@ -267,6 +278,7 @@ ProviderSection::ProviderSection(const QString &name, QWidget *parent)
|
||||
headerLayout->addWidget(m_arrow);
|
||||
headerLayout->addWidget(m_label);
|
||||
headerLayout->addStretch(1);
|
||||
headerLayout->addWidget(m_count);
|
||||
m_header->installEventFilter(this);
|
||||
|
||||
m_content = new QWidget;
|
||||
@@ -290,15 +302,17 @@ void ProviderSection::addCard(ListRowCard *card)
|
||||
m_cards.append(card);
|
||||
}
|
||||
|
||||
int ProviderSection::applyFilter(const QString &needle)
|
||||
int ProviderSection::applyFilter(const QString &needle, const QSet<QString> &activeTags)
|
||||
{
|
||||
int visible = 0;
|
||||
for (auto *card : m_cards) {
|
||||
const bool show = card->matches(needle);
|
||||
const bool show = card->matches(needle) && card->hasAllTags(activeTags);
|
||||
card->setVisible(show);
|
||||
if (show)
|
||||
++visible;
|
||||
}
|
||||
if (m_count)
|
||||
m_count->setText(QString::number(visible));
|
||||
return visible;
|
||||
}
|
||||
|
||||
@@ -329,12 +343,16 @@ AgentSelectionDialog::AgentSelectionDialog(
|
||||
const std::vector<AgentConfig> &configs,
|
||||
const QString ¤tName,
|
||||
AgentFactory *agentFactory,
|
||||
const QStringList &presetTags,
|
||||
QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_agentFactory(agentFactory)
|
||||
, m_presetTags(presetTags)
|
||||
{
|
||||
setWindowTitle(Tr::tr("Change Agent"));
|
||||
const bool isChange = !currentName.isEmpty();
|
||||
setWindowTitle(isChange ? Tr::tr("Change Agent") : Tr::tr("Add Agent"));
|
||||
resize(720, 600);
|
||||
setMinimumSize(560, 420);
|
||||
setSizeGripEnabled(true);
|
||||
|
||||
if (!m_agentFactory)
|
||||
@@ -350,6 +368,31 @@ AgentSelectionDialog::AgentSelectionDialog(
|
||||
topRow->setSpacing(6);
|
||||
topRow->addWidget(m_filter, 1);
|
||||
|
||||
m_tagStrip = new TagFilterStrip(this);
|
||||
|
||||
const bool dark = CardStyle::isDark(palette());
|
||||
const auto tone = CardStyle::toneFor(dark);
|
||||
|
||||
m_resultCount = new QLabel(this);
|
||||
{
|
||||
QPalette rp = m_resultCount->palette();
|
||||
rp.setColor(QPalette::WindowText, QColor(tone.textMute));
|
||||
m_resultCount->setPalette(rp);
|
||||
}
|
||||
|
||||
auto *expandAll = new QLabel(
|
||||
QStringLiteral("<a href=\"#\">%1</a>").arg(Tr::tr("Expand all")), this);
|
||||
auto *collapseAll = new QLabel(
|
||||
QStringLiteral("<a href=\"#\">%1</a>").arg(Tr::tr("Collapse all")), this);
|
||||
|
||||
auto *controlsRow = new QHBoxLayout;
|
||||
controlsRow->setContentsMargins(2, 0, 2, 0);
|
||||
controlsRow->setSpacing(8);
|
||||
controlsRow->addWidget(m_resultCount);
|
||||
controlsRow->addStretch(1);
|
||||
controlsRow->addWidget(expandAll);
|
||||
controlsRow->addWidget(collapseAll);
|
||||
|
||||
m_scroll = new QScrollArea(this);
|
||||
m_scroll->setWidgetResizable(true);
|
||||
m_scroll->setFrameShape(QFrame::StyledPanel);
|
||||
@@ -358,20 +401,54 @@ AgentSelectionDialog::AgentSelectionDialog(
|
||||
auto *buttons
|
||||
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
m_okButton = buttons->button(QDialogButtonBox::Ok);
|
||||
m_okButton->setText(Tr::tr("Change"));
|
||||
m_okButton->setText(isChange ? Tr::tr("Change") : Tr::tr("Add"));
|
||||
m_okButton->setEnabled(false);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(topRow);
|
||||
layout->addWidget(m_tagStrip);
|
||||
layout->addLayout(controlsRow);
|
||||
layout->addWidget(m_scroll);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
QMap<QString, int> tagCounts;
|
||||
for (const auto &cfg : (m_agentFactory ? m_agentFactory->configs() : m_localConfigs)) {
|
||||
if (cfg.hidden)
|
||||
continue;
|
||||
for (const QString &tag : cfg.tags)
|
||||
tagCounts[tag] += 1;
|
||||
}
|
||||
|
||||
QSet<QString> preset;
|
||||
for (const QString &tag : m_presetTags)
|
||||
if (tagCounts.contains(tag))
|
||||
preset.insert(tag);
|
||||
m_tagStrip->setAvailableTags(tagCounts, preset);
|
||||
|
||||
connect(m_tagStrip, &TagFilterStrip::activeTagsChanged, this,
|
||||
[this](const QSet<QString> &) { applyFilters(); });
|
||||
connect(expandAll, &QLabel::linkActivated, this, [this](const QString &) {
|
||||
setAllExpanded(true);
|
||||
});
|
||||
connect(collapseAll, &QLabel::linkActivated, this, [this](const QString &) {
|
||||
setAllExpanded(false);
|
||||
});
|
||||
|
||||
rebuild(currentName);
|
||||
|
||||
connect(m_filter, &QLineEdit::textChanged, this,
|
||||
[this](const QString &text) { applyFilter(text); });
|
||||
[this](const QString &) { applyFilters(); });
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
m_filter->setFocus();
|
||||
}
|
||||
|
||||
void AgentSelectionDialog::setAllExpanded(bool expanded)
|
||||
{
|
||||
for (auto *section : m_sections)
|
||||
if (!section->isHidden())
|
||||
section->setExpanded(expanded);
|
||||
}
|
||||
|
||||
void AgentSelectionDialog::selectCard(ListRowCard *card)
|
||||
@@ -441,6 +518,20 @@ void AgentSelectionDialog::rebuild(const QString ¤tName)
|
||||
contentLayout->addWidget(section);
|
||||
m_sections.append(section);
|
||||
}
|
||||
|
||||
m_emptyLabel = new QLabel(tr("No agents match the current filter."), content);
|
||||
m_emptyLabel->setAlignment(Qt::AlignCenter);
|
||||
m_emptyLabel->setContentsMargins(10, 24, 10, 24);
|
||||
{
|
||||
QFont ef = m_emptyLabel->font();
|
||||
ef.setItalic(true);
|
||||
m_emptyLabel->setFont(ef);
|
||||
QPalette ep = m_emptyLabel->palette();
|
||||
ep.setColor(QPalette::WindowText, ep.color(QPalette::Mid));
|
||||
m_emptyLabel->setPalette(ep);
|
||||
}
|
||||
m_emptyLabel->setVisible(false);
|
||||
contentLayout->addWidget(m_emptyLabel);
|
||||
contentLayout->addStretch(1);
|
||||
|
||||
m_scroll->setWidget(content);
|
||||
@@ -455,17 +546,36 @@ void AgentSelectionDialog::rebuild(const QString ¤tName)
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter(m_filter ? m_filter->text() : QString());
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
void AgentSelectionDialog::applyFilter(const QString &needle)
|
||||
void AgentSelectionDialog::applyFilters()
|
||||
{
|
||||
const QString trimmed = needle.trimmed();
|
||||
const QString needle = m_filter ? m_filter->text().trimmed() : QString();
|
||||
const QSet<QString> activeTags = m_tagStrip ? m_tagStrip->activeTags() : QSet<QString>();
|
||||
const bool filtering = !needle.isEmpty() || !activeTags.isEmpty();
|
||||
int total = 0;
|
||||
for (auto *section : m_sections) {
|
||||
const int visible = section->applyFilter(trimmed);
|
||||
const int visible = section->applyFilter(needle, activeTags);
|
||||
section->setVisible(visible > 0);
|
||||
if (!trimmed.isEmpty())
|
||||
if (filtering)
|
||||
section->setExpanded(visible > 0);
|
||||
total += visible;
|
||||
}
|
||||
if (m_emptyLabel)
|
||||
m_emptyLabel->setVisible(total == 0);
|
||||
if (m_resultCount)
|
||||
m_resultCount->setText(total == 0 ? tr("No matches")
|
||||
: tr("%n agent(s)", nullptr, total));
|
||||
|
||||
if (m_tagStrip) {
|
||||
QMap<QString, int> liveCounts;
|
||||
for (auto *section : m_sections)
|
||||
for (auto *card : section->cards())
|
||||
if (card->matches(needle) && card->hasAllTags(activeTags))
|
||||
for (const QString &tag : card->itemTags())
|
||||
liveCounts[tag] += 1;
|
||||
m_tagStrip->setVisibleCounts(liveCounts);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QPalette>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <vector>
|
||||
|
||||
@@ -25,6 +26,8 @@ class AgentFactory;
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class TagFilterStrip;
|
||||
|
||||
namespace CardStyle {
|
||||
|
||||
struct Tone
|
||||
@@ -87,9 +90,10 @@ class ListRowCard : public QFrame
|
||||
Q_OBJECT
|
||||
public:
|
||||
QString itemName() const { return m_itemName; }
|
||||
QStringList itemTags() const { return m_itemTags; }
|
||||
bool matches(const QString &needle) const;
|
||||
bool hasAllTags(const QSet<QString> &activeTags) const;
|
||||
void setSelected(bool selected);
|
||||
bool isSelected() const { return m_selected; }
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
@@ -99,6 +103,7 @@ protected:
|
||||
explicit ListRowCard(QWidget *parent = nullptr);
|
||||
|
||||
void setItemName(const QString &name) { m_itemName = name; }
|
||||
void setItemTags(const QStringList &tags) { m_itemTags = tags; }
|
||||
void buildSearchHaystack(const QStringList &parts);
|
||||
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
@@ -111,6 +116,7 @@ private:
|
||||
void applyTheme();
|
||||
|
||||
QString m_itemName;
|
||||
QStringList m_itemTags;
|
||||
QString m_searchHaystack;
|
||||
bool m_selected = false;
|
||||
bool m_hover = false;
|
||||
@@ -134,10 +140,10 @@ public:
|
||||
|
||||
void addCard(ListRowCard *card);
|
||||
void setExpanded(bool expanded);
|
||||
bool isExpanded() const { return m_expanded; }
|
||||
const QList<ListRowCard *> &cards() const { return m_cards; }
|
||||
|
||||
int applyFilter(const QString &needle); // returns number of visible cards
|
||||
// returns number of visible cards
|
||||
int applyFilter(const QString &needle, const QSet<QString> &activeTags);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
@@ -146,6 +152,7 @@ private:
|
||||
QFrame *m_header = nullptr;
|
||||
QLabel *m_arrow = nullptr;
|
||||
QLabel *m_label = nullptr;
|
||||
QLabel *m_count = nullptr;
|
||||
QWidget *m_content = nullptr;
|
||||
QVBoxLayout *m_contentLayout = nullptr;
|
||||
QList<ListRowCard *> m_cards;
|
||||
@@ -160,6 +167,7 @@ public:
|
||||
const std::vector<AgentConfig> &configs,
|
||||
const QString ¤tName,
|
||||
AgentFactory *agentFactory = nullptr,
|
||||
const QStringList &presetTags = {},
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
QString selectedName() const { return m_selectedName; }
|
||||
@@ -167,17 +175,22 @@ public:
|
||||
private:
|
||||
void rebuild(const QString ¤tName);
|
||||
void selectCard(ListRowCard *card);
|
||||
void applyFilter(const QString &needle);
|
||||
void applyFilters();
|
||||
void setAllExpanded(bool expanded);
|
||||
|
||||
QLineEdit *m_filter = nullptr;
|
||||
TagFilterStrip *m_tagStrip = nullptr;
|
||||
QScrollArea *m_scroll = nullptr;
|
||||
QPushButton *m_okButton = nullptr;
|
||||
QLabel *m_resultCount = nullptr;
|
||||
QLabel *m_emptyLabel = nullptr;
|
||||
ListRowCard *m_currentCard = nullptr;
|
||||
QList<ProviderSection *> m_sections;
|
||||
QString m_selectedName;
|
||||
|
||||
AgentFactory *m_agentFactory = nullptr;
|
||||
std::vector<AgentConfig> m_localConfigs; // fallback when no factory
|
||||
QStringList m_presetTags;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "AgentSlotWidget.hpp"
|
||||
|
||||
#include "SettingsTr.hpp"
|
||||
|
||||
#include <QEvent>
|
||||
#include <QFont>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPalette>
|
||||
#include <QPushButton>
|
||||
#include <QScopedValueRollback>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
namespace {
|
||||
|
||||
bool isDarkPalette(const QPalette &p)
|
||||
{
|
||||
return p.color(QPalette::Window).lightness() < 128;
|
||||
}
|
||||
|
||||
QFont monoFont(int pixelSize)
|
||||
{
|
||||
QFont f;
|
||||
f.setFamilies({QStringLiteral("SF Mono"),
|
||||
QStringLiteral("Cascadia Code"),
|
||||
QStringLiteral("Consolas"),
|
||||
QStringLiteral("Liberation Mono"),
|
||||
QStringLiteral("Menlo"),
|
||||
QStringLiteral("Courier New")});
|
||||
f.setStyleHint(QFont::Monospace);
|
||||
f.setPixelSize(pixelSize);
|
||||
return f;
|
||||
}
|
||||
|
||||
struct Tone
|
||||
{
|
||||
QString cardBg;
|
||||
QString cardBd;
|
||||
QString textSoft;
|
||||
QString textMute;
|
||||
QString textFaint;
|
||||
};
|
||||
|
||||
Tone toneFor(bool dark)
|
||||
{
|
||||
return dark ? Tone{"#333333", "#4a4a4a", "#c2c2c2", "#9a9a9a", "#7a7a7a"}
|
||||
: Tone{"#f6f6f6", "#bdbdbd", "#3a3a3a", "#5a5a5a", "#8a8a8a"};
|
||||
}
|
||||
|
||||
struct PillTone
|
||||
{
|
||||
QString bg;
|
||||
QString fg;
|
||||
QString bd;
|
||||
};
|
||||
|
||||
PillTone pillTone(Pill::Kind kind, bool dark)
|
||||
{
|
||||
switch (kind) {
|
||||
case Pill::Template:
|
||||
return dark ? PillTone{"#2c3f5a", "#cfe1f7", "#4a6286"}
|
||||
: PillTone{"#dbe7f6", "#1f3f73", "#a8c1e0"};
|
||||
case Pill::On:
|
||||
return dark ? PillTone{"#2f4530", "#bce0bd", "#4a6c4b"}
|
||||
: PillTone{"#dbe9d3", "#2c5a1c", "#a3bc97"};
|
||||
case Pill::Off:
|
||||
return dark ? PillTone{"#3a3a3a", "#8a8a8a", "#4a4a4a"}
|
||||
: PillTone{"#ececec", "#7a7a7a", "#c8c8c8"};
|
||||
case Pill::User:
|
||||
return dark ? PillTone{"#4a3f24", "#e6cd92", "#6a5a30"}
|
||||
: PillTone{"#f0e4cf", "#75541a", "#cdb98a"};
|
||||
case Pill::Tag:
|
||||
return dark ? PillTone{"#2e2e3a", "#b9b9cf", "#46465a"}
|
||||
: PillTone{"#e7e7f2", "#46466e", "#c1c1d5"};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// -- Pill --------------------------------------------------------------
|
||||
|
||||
Pill::Pill(Kind kind, const QString &text, QWidget *parent)
|
||||
: QLabel(text, parent)
|
||||
, m_kind(kind)
|
||||
{
|
||||
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
|
||||
setAlignment(Qt::AlignCenter);
|
||||
QFont f = font();
|
||||
f.setPixelSize(11);
|
||||
setFont(f);
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
void Pill::setKind(Kind kind)
|
||||
{
|
||||
if (m_kind == kind)
|
||||
return;
|
||||
m_kind = kind;
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
void Pill::changeEvent(QEvent *event)
|
||||
{
|
||||
QLabel::changeEvent(event);
|
||||
if (m_inApplyTheme)
|
||||
return;
|
||||
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
void Pill::applyTheme()
|
||||
{
|
||||
if (m_inApplyTheme)
|
||||
return;
|
||||
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
|
||||
|
||||
const auto t = pillTone(m_kind, isDarkPalette(palette()));
|
||||
setStyleSheet(QStringLiteral(
|
||||
"QLabel { background-color: %1; color: %2;"
|
||||
" border: 1px solid %3; padding: 1px 7px; }")
|
||||
.arg(t.bg, t.fg, t.bd));
|
||||
}
|
||||
|
||||
// -- AgentSlotWidget ---------------------------------------------------
|
||||
|
||||
AgentSlotWidget::AgentSlotWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_StyledBackground, true);
|
||||
setObjectName(QStringLiteral("AgentSlot"));
|
||||
|
||||
m_name = new QLabel(this);
|
||||
QFont nameFont = m_name->font();
|
||||
nameFont.setBold(true);
|
||||
m_name->setFont(nameFont);
|
||||
m_name->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
m_sourcePill = new Pill(Pill::User, {}, this);
|
||||
m_sourcePill->hide();
|
||||
|
||||
m_changeBtn = new QPushButton(Tr::tr("Change…"), this);
|
||||
m_changeBtn->setCursor(Qt::PointingHandCursor);
|
||||
connect(m_changeBtn, &QPushButton::clicked, this, &AgentSlotWidget::changeRequested);
|
||||
|
||||
m_editBtn = new QPushButton(Tr::tr("Edit"), this);
|
||||
m_editBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_editBtn->setToolTip(Tr::tr("Open this agent in the Agents settings page."));
|
||||
connect(m_editBtn, &QPushButton::clicked, this, &AgentSlotWidget::editRequested);
|
||||
|
||||
auto makeFieldLabel = [this]() {
|
||||
auto *l = new QLabel(this);
|
||||
l->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
return l;
|
||||
};
|
||||
auto makeMonoValue = [this]() {
|
||||
auto *l = new QLabel(this);
|
||||
l->setFont(monoFont(11));
|
||||
l->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
l->setMinimumWidth(0);
|
||||
l->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||
return l;
|
||||
};
|
||||
auto makePlainValue = [this]() {
|
||||
auto *l = new QLabel(this);
|
||||
l->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
l->setMinimumWidth(0);
|
||||
l->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||
return l;
|
||||
};
|
||||
|
||||
m_modelLabel = makeFieldLabel();
|
||||
m_modelLabel->setText(Tr::tr("Model"));
|
||||
m_modelValue = makeMonoValue();
|
||||
|
||||
m_urlLabel = makeFieldLabel();
|
||||
m_urlLabel->setText(Tr::tr("Provider"));
|
||||
m_urlValue = makeMonoValue();
|
||||
|
||||
m_endpointLabel = makeFieldLabel();
|
||||
m_endpointLabel->setText(Tr::tr("Endpoint"));
|
||||
m_endpointValue = makeMonoValue();
|
||||
|
||||
m_templateLabel = makeFieldLabel();
|
||||
m_templateLabel->setText(Tr::tr("Template"));
|
||||
m_templateValue = makePlainValue();
|
||||
|
||||
m_description = new QLabel(this);
|
||||
m_description->setWordWrap(true);
|
||||
m_description->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
QFont descFont = m_description->font();
|
||||
descFont.setItalic(true);
|
||||
m_description->setFont(descFont);
|
||||
|
||||
m_thinkingPill = new Pill(Pill::Off, {}, this);
|
||||
m_toolsPill = new Pill(Pill::Off, {}, this);
|
||||
|
||||
// Header row
|
||||
auto *nameRow = new QHBoxLayout;
|
||||
nameRow->setContentsMargins(0, 0, 0, 0);
|
||||
nameRow->setSpacing(6);
|
||||
nameRow->addWidget(m_name);
|
||||
nameRow->addWidget(m_sourcePill);
|
||||
nameRow->addStretch(1);
|
||||
|
||||
auto *buttonsBox = new QHBoxLayout;
|
||||
buttonsBox->setContentsMargins(0, 0, 0, 0);
|
||||
buttonsBox->setSpacing(4);
|
||||
buttonsBox->addWidget(m_editBtn);
|
||||
buttonsBox->addWidget(m_changeBtn);
|
||||
|
||||
auto *headerRow = new QHBoxLayout;
|
||||
headerRow->setContentsMargins(0, 0, 0, 0);
|
||||
headerRow->setSpacing(8);
|
||||
headerRow->addLayout(nameRow, 1);
|
||||
headerRow->addLayout(buttonsBox, 0);
|
||||
|
||||
// Two-column field grid
|
||||
auto *grid = new QGridLayout;
|
||||
grid->setContentsMargins(0, 0, 0, 0);
|
||||
grid->setHorizontalSpacing(8);
|
||||
grid->setVerticalSpacing(2);
|
||||
grid->setColumnMinimumWidth(0, 62);
|
||||
grid->setColumnStretch(0, 0);
|
||||
grid->setColumnStretch(1, 1);
|
||||
|
||||
grid->addWidget(m_modelLabel, 0, 0);
|
||||
grid->addWidget(m_modelValue, 0, 1);
|
||||
grid->addWidget(m_urlLabel, 1, 0);
|
||||
grid->addWidget(m_urlValue, 1, 1);
|
||||
grid->addWidget(m_endpointLabel, 2, 0);
|
||||
grid->addWidget(m_endpointValue, 2, 1);
|
||||
grid->addWidget(m_templateLabel, 3, 0);
|
||||
grid->addWidget(m_templateValue, 3, 1);
|
||||
|
||||
auto *pillsRow = new QHBoxLayout;
|
||||
pillsRow->setContentsMargins(0, 0, 0, 0);
|
||||
pillsRow->setSpacing(4);
|
||||
pillsRow->addWidget(m_thinkingPill);
|
||||
pillsRow->addWidget(m_toolsPill);
|
||||
pillsRow->addStretch(1);
|
||||
|
||||
auto *outer = new QVBoxLayout(this);
|
||||
outer->setContentsMargins(10, 10, 10, 10);
|
||||
outer->setSpacing(6);
|
||||
outer->addLayout(headerRow);
|
||||
outer->addLayout(grid);
|
||||
outer->addWidget(m_description);
|
||||
outer->addLayout(pillsRow);
|
||||
|
||||
applyTheme();
|
||||
clear();
|
||||
}
|
||||
|
||||
void AgentSlotWidget::changeEvent(QEvent *event)
|
||||
{
|
||||
QWidget::changeEvent(event);
|
||||
if (m_inApplyTheme)
|
||||
return;
|
||||
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
void AgentSlotWidget::applyTheme()
|
||||
{
|
||||
if (m_inApplyTheme)
|
||||
return;
|
||||
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
|
||||
|
||||
const Tone t = toneFor(isDarkPalette(palette()));
|
||||
|
||||
setStyleSheet(QStringLiteral(
|
||||
"#AgentSlot { background-color: %1; border: 1px solid %2; }")
|
||||
.arg(t.cardBg, t.cardBd));
|
||||
|
||||
auto applyColor = [](QLabel *label, const QString &color) {
|
||||
QPalette p = label->palette();
|
||||
p.setColor(QPalette::WindowText, QColor(color));
|
||||
label->setPalette(p);
|
||||
};
|
||||
|
||||
for (QLabel *l : {m_modelLabel, m_urlLabel, m_endpointLabel, m_templateLabel})
|
||||
applyColor(l, t.textMute);
|
||||
|
||||
applyColor(m_description, t.textSoft);
|
||||
}
|
||||
|
||||
void AgentSlotWidget::setAgentConfig(const AgentConfig &cfg)
|
||||
{
|
||||
m_name->setText(cfg.name);
|
||||
|
||||
if (cfg.isUserSource()) {
|
||||
m_sourcePill->setText(cfg.overridesBundled
|
||||
? Tr::tr("User overrides bundled")
|
||||
: Tr::tr("User"));
|
||||
m_sourcePill->show();
|
||||
} else {
|
||||
m_sourcePill->hide();
|
||||
}
|
||||
|
||||
m_modelValue->setText(cfg.model);
|
||||
m_modelValue->setToolTip(cfg.model);
|
||||
// The agent profile no longer carries a URL — the URL belongs to
|
||||
// the referenced provider instance and is editable on the Providers
|
||||
// settings page. Surface the instance name in this row instead.
|
||||
m_urlValue->setText(cfg.providerInstance);
|
||||
m_urlValue->setToolTip(cfg.providerInstance);
|
||||
m_endpointValue->setText(cfg.endpoint);
|
||||
m_endpointValue->setToolTip(cfg.endpoint);
|
||||
// Templates are now inline in the agent profile — no separate name to
|
||||
// surface. Keep the row but show '—' so the layout stays stable.
|
||||
m_templateLabel->hide();
|
||||
m_templateValue->hide();
|
||||
|
||||
m_description->setText(cfg.description.isEmpty()
|
||||
? Tr::tr("No description provided.")
|
||||
: cfg.description);
|
||||
|
||||
const Tone t = toneFor(isDarkPalette(palette()));
|
||||
QPalette descPal = m_description->palette();
|
||||
descPal.setColor(QPalette::WindowText,
|
||||
QColor(cfg.description.isEmpty() ? t.textFaint : t.textSoft));
|
||||
m_description->setPalette(descPal);
|
||||
|
||||
m_thinkingPill->setKind(cfg.enableThinking ? Pill::On : Pill::Off);
|
||||
m_thinkingPill->setText(cfg.enableThinking ? Tr::tr("thinking on")
|
||||
: Tr::tr("thinking off"));
|
||||
m_toolsPill->setKind(cfg.enableTools ? Pill::On : Pill::Off);
|
||||
m_toolsPill->setText(cfg.enableTools ? Tr::tr("tools on")
|
||||
: Tr::tr("tools off"));
|
||||
m_thinkingPill->show();
|
||||
m_toolsPill->show();
|
||||
|
||||
m_editBtn->setEnabled(!cfg.name.isEmpty());
|
||||
}
|
||||
|
||||
void AgentSlotWidget::clear()
|
||||
{
|
||||
const Tone t = toneFor(isDarkPalette(palette()));
|
||||
|
||||
m_name->setText(QStringLiteral("<i style=\"color:%1\">%2</i>")
|
||||
.arg(t.textFaint, Tr::tr("(no agent selected)")));
|
||||
m_sourcePill->hide();
|
||||
|
||||
for (QLabel *l : {m_modelValue, m_urlValue, m_endpointValue, m_templateValue}) {
|
||||
l->clear();
|
||||
l->setToolTip({});
|
||||
}
|
||||
|
||||
m_description->setText({});
|
||||
m_thinkingPill->hide();
|
||||
m_toolsPill->hide();
|
||||
m_editBtn->setEnabled(false);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QWidget>
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
|
||||
class QPushButton;
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class Pill : public QLabel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Kind { Template, On, Off, User, Tag };
|
||||
|
||||
Pill(Kind kind, const QString &text = {}, QWidget *parent = nullptr);
|
||||
void setKind(Kind kind);
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
void applyTheme();
|
||||
|
||||
Kind m_kind;
|
||||
bool m_inApplyTheme = false;
|
||||
};
|
||||
|
||||
class AgentSlotWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AgentSlotWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setAgentConfig(const AgentConfig &cfg);
|
||||
void clear();
|
||||
|
||||
QPushButton *changeButton() const { return m_changeBtn; }
|
||||
|
||||
signals:
|
||||
void changeRequested();
|
||||
void editRequested();
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
void applyTheme();
|
||||
|
||||
QLabel *m_name = nullptr;
|
||||
Pill *m_sourcePill = nullptr;
|
||||
QPushButton *m_changeBtn = nullptr;
|
||||
QPushButton *m_editBtn = nullptr;
|
||||
|
||||
QLabel *m_modelLabel = nullptr;
|
||||
QLabel *m_modelValue = nullptr;
|
||||
QLabel *m_urlLabel = nullptr;
|
||||
QLabel *m_urlValue = nullptr;
|
||||
QLabel *m_endpointLabel = nullptr;
|
||||
QLabel *m_endpointValue = nullptr;
|
||||
QLabel *m_templateLabel = nullptr;
|
||||
QLabel *m_templateValue = nullptr;
|
||||
|
||||
QLabel *m_description = nullptr;
|
||||
|
||||
Pill *m_thinkingPill = nullptr;
|
||||
Pill *m_toolsPill = nullptr;
|
||||
|
||||
bool m_inApplyTheme = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
@@ -1,8 +1,7 @@
|
||||
add_library(QodeAssistAgentPipelines OBJECT
|
||||
AgentPipelinesPage.hpp AgentPipelinesPage.cpp
|
||||
add_library(QodeAssistAgentPipelines STATIC
|
||||
PipelinesConfig.hpp PipelinesConfig.cpp
|
||||
Pill.hpp Pill.cpp
|
||||
AgentRosterWidget.hpp AgentRosterWidget.cpp
|
||||
AgentSlotWidget.hpp AgentSlotWidget.cpp
|
||||
AgentSelectionDialog.hpp AgentSelectionDialog.cpp
|
||||
)
|
||||
|
||||
@@ -29,6 +28,7 @@ target_link_libraries(QodeAssistAgentPipelines PRIVATE
|
||||
QodeAssistLogger
|
||||
TomlSerializer
|
||||
tomlplusplus::tomlplusplus
|
||||
QodeAssistSettings
|
||||
)
|
||||
|
||||
target_compile_definitions(QodeAssistAgentPipelines PRIVATE
|
||||
|
||||
96
sources/settings/Pill.cpp
Normal file
96
sources/settings/Pill.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "Pill.hpp"
|
||||
|
||||
#include "SettingsTheme.hpp"
|
||||
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
#include <QEvent>
|
||||
#include <QFont>
|
||||
#include <QScopedValueRollback>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
namespace {
|
||||
|
||||
struct PillTone
|
||||
{
|
||||
QColor bg;
|
||||
QColor fg;
|
||||
QColor border;
|
||||
};
|
||||
|
||||
PillTone toneFor(Pill::Kind kind)
|
||||
{
|
||||
const QColor cardBg = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
||||
const QColor text = Utils::creatorColor(Utils::Theme::TextColorNormal);
|
||||
const QColor textMuted = Utils::creatorColor(Utils::Theme::PanelTextColorMid);
|
||||
const QColor textFaint = Utils::creatorColor(Utils::Theme::TextColorDisabled);
|
||||
const QColor accent = Utils::creatorColor(Utils::Theme::TextColorLink);
|
||||
const QColor activeBg = Utils::creatorColor(Utils::Theme::BackgroundColorSelected);
|
||||
const QColor border = Utils::creatorColor(Utils::Theme::SplitterColor);
|
||||
const bool dark = cardBg.lightness() < 128;
|
||||
|
||||
const auto tint = [&](const QColor &role) -> PillTone {
|
||||
return {mix(cardBg, role, 0.16), dark ? mix(text, role, 0.65) : role, mix(cardBg, role, 0.42)};
|
||||
};
|
||||
|
||||
switch (kind) {
|
||||
case Pill::On:
|
||||
return tint(Utils::creatorColor(Utils::Theme::IconsRunColor));
|
||||
case Pill::Off:
|
||||
return {mix(cardBg, textFaint, 0.12), textFaint, mix(cardBg, textFaint, 0.30)};
|
||||
case Pill::User:
|
||||
return tint(Utils::creatorColor(Utils::Theme::IconsWarningColor));
|
||||
case Pill::Accent:
|
||||
case Pill::Match:
|
||||
return tint(accent);
|
||||
case Pill::Active:
|
||||
return {activeBg, text, accent};
|
||||
case Pill::Tag:
|
||||
case Pill::Neutral:
|
||||
return {mix(cardBg, textMuted, 0.14), textMuted, mix(cardBg, textMuted, 0.32)};
|
||||
}
|
||||
return {cardBg, text, border};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Pill::Pill(Kind kind, const QString &text, QWidget *parent)
|
||||
: QLabel(text, parent)
|
||||
, m_kind(kind)
|
||||
{
|
||||
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
|
||||
setAlignment(Qt::AlignCenter);
|
||||
QFont f = font();
|
||||
f.setPixelSize(11);
|
||||
setFont(f);
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
void Pill::changeEvent(QEvent *event)
|
||||
{
|
||||
QLabel::changeEvent(event);
|
||||
if (m_inApplyTheme)
|
||||
return;
|
||||
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
void Pill::applyTheme()
|
||||
{
|
||||
if (m_inApplyTheme)
|
||||
return;
|
||||
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
|
||||
|
||||
const PillTone t = toneFor(m_kind);
|
||||
setStyleSheet(QStringLiteral("QLabel { background-color:%1; color:%2;"
|
||||
" border:1px solid %3; border-radius:3px;"
|
||||
" padding:1px 7px; font-size:11px; }")
|
||||
.arg(cssColor(t.bg), cssColor(t.fg), cssColor(t.border)));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
32
sources/settings/Pill.hpp
Normal file
32
sources/settings/Pill.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
// Small rounded chip. Theme-aware: recolors itself on palette/style changes.
|
||||
// All colors derive from the active Qt Creator theme (Utils::Theme).
|
||||
class Pill : public QLabel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Kind { Neutral, Accent, On, Off, User, Tag, Active, Match };
|
||||
|
||||
explicit Pill(Kind kind, const QString &text = {}, QWidget *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
void applyTheme();
|
||||
|
||||
Kind m_kind;
|
||||
bool m_inApplyTheme = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
@@ -38,6 +38,30 @@ QString trimAndCap(const QString &raw)
|
||||
return s;
|
||||
}
|
||||
|
||||
QString toSingleString(const toml::node *node, const QString &slotKey, bool *schemaOk)
|
||||
{
|
||||
if (!node)
|
||||
return {};
|
||||
if (const auto *s = node->as_string())
|
||||
return trimAndCap(QString::fromStdString(s->get()));
|
||||
// Backward compatibility: older pipelines.toml stored these slots as an
|
||||
// ordered array. Collapse to the first usable name.
|
||||
if (const auto *arr = node->as_array()) {
|
||||
for (size_t i = 0; i < arr->size(); ++i) {
|
||||
if (const auto *s = (*arr)[i].as_string()) {
|
||||
const QString name = trimAndCap(QString::fromStdString(s->get()));
|
||||
if (!name.isEmpty())
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
LOG_MESSAGE(QStringLiteral("[Pipelines] schema error: '%1' must be a string").arg(slotKey));
|
||||
if (schemaOk)
|
||||
*schemaOk = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
QStringList toStringList(const toml::node *node, const QString &slotKey, bool *schemaOk)
|
||||
{
|
||||
QStringList out;
|
||||
@@ -94,12 +118,7 @@ void fillMissingFromDefaults(PipelineRosters &r, const toml::table §ion)
|
||||
|
||||
PipelineRosters PipelineRosters::defaults()
|
||||
{
|
||||
PipelineRosters r;
|
||||
r.codeCompletion = {QStringLiteral("Ollama Qwen2.5-Coder Completion")};
|
||||
r.chatAssistant = {QStringLiteral("Ollama Chat")};
|
||||
r.chatCompression = {QStringLiteral("Ollama Compression")};
|
||||
r.quickRefactor = {QStringLiteral("Ollama Quick Refactor")};
|
||||
return r;
|
||||
return PipelineRosters{};
|
||||
}
|
||||
|
||||
QString PipelinesConfig::filePath()
|
||||
@@ -171,9 +190,9 @@ PipelinesLoadResult PipelinesConfig::load()
|
||||
result.rosters.chatAssistant
|
||||
= toStringList((*section)[kChatAssistant].node(), kChatAssistant, &schemaOk);
|
||||
result.rosters.chatCompression
|
||||
= toStringList((*section)[kChatCompression].node(), kChatCompression, &schemaOk);
|
||||
= toSingleString((*section)[kChatCompression].node(), kChatCompression, &schemaOk);
|
||||
result.rosters.quickRefactor
|
||||
= toStringList((*section)[kQuickRefactor].node(), kQuickRefactor, &schemaOk);
|
||||
= toSingleString((*section)[kQuickRefactor].node(), kQuickRefactor, &schemaOk);
|
||||
|
||||
fillMissingFromDefaults(result.rosters, *section);
|
||||
|
||||
@@ -198,17 +217,22 @@ bool PipelinesConfig::save(const PipelineRosters &rosters, QString *errorOut)
|
||||
}
|
||||
|
||||
TomlSerializer::TomlWriter w;
|
||||
w.writeComment(QStringLiteral("QodeAssist pipelines — which agent each feature uses."));
|
||||
w.writeComment(QStringLiteral(
|
||||
"QodeAssist pipelines — slot → ordered list of agent names."));
|
||||
"code_completion: ordered list; the router walks it top-down and uses"));
|
||||
w.writeComment(QStringLiteral(
|
||||
"The router walks each list top-down at request time and uses"));
|
||||
w.writeComment(QStringLiteral("the first matching agent."));
|
||||
" the first agent whose match rules fit the current file/project."));
|
||||
w.writeComment(QStringLiteral(
|
||||
"chat_assistant: agents offered in the chat picker (order irrelevant —"));
|
||||
w.writeComment(QStringLiteral(" you choose one in the UI)."));
|
||||
w.writeComment(QStringLiteral(
|
||||
"chat_compression / quick_refactor: a single agent name."));
|
||||
w.writeBlankLine();
|
||||
w.writeTableHeader(QString::fromUtf8(kSection));
|
||||
w.writeStringArray(QString::fromUtf8(kCodeCompletion), rosters.codeCompletion);
|
||||
w.writeStringArray(QString::fromUtf8(kChatAssistant), rosters.chatAssistant);
|
||||
w.writeStringArray(QString::fromUtf8(kChatCompression), rosters.chatCompression);
|
||||
w.writeStringArray(QString::fromUtf8(kQuickRefactor), rosters.quickRefactor);
|
||||
w.writeString(QString::fromUtf8(kChatCompression), rosters.chatCompression);
|
||||
w.writeString(QString::fromUtf8(kQuickRefactor), rosters.quickRefactor);
|
||||
|
||||
QSaveFile out(path);
|
||||
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
@@ -259,10 +283,19 @@ bool PipelinesConfig::validate(const QodeAssist::AgentFactory &factory, QString
|
||||
}
|
||||
};
|
||||
|
||||
auto fixOne = [&](QString ¤t) {
|
||||
const QString name = trimAndCap(current);
|
||||
const QString next = (!name.isEmpty() && factory.configByName(name)) ? name : QString();
|
||||
if (next != current) {
|
||||
current = next;
|
||||
changed = true;
|
||||
}
|
||||
};
|
||||
|
||||
fix(r.codeCompletion);
|
||||
fix(r.chatAssistant);
|
||||
fix(r.chatCompression);
|
||||
fix(r.quickRefactor);
|
||||
fixOne(r.chatCompression);
|
||||
fixOne(r.quickRefactor);
|
||||
|
||||
if (!changed && lr.status == PipelinesLoadStatus::Ok)
|
||||
return true;
|
||||
|
||||
@@ -15,10 +15,15 @@ namespace QodeAssist::Settings {
|
||||
|
||||
struct PipelineRosters
|
||||
{
|
||||
// Code completion is auto-routed: the router walks this ordered list at
|
||||
// request time and uses the first agent whose match rules fit the file.
|
||||
QStringList codeCompletion;
|
||||
// Chat is user-driven: this is an unordered allow-list of the agents
|
||||
// offered in the chat picker. The user picks; no routing happens.
|
||||
QStringList chatAssistant;
|
||||
QStringList chatCompression;
|
||||
QStringList quickRefactor;
|
||||
// Compression and quick refactor each use a single fixed agent.
|
||||
QString chatCompression;
|
||||
QString quickRefactor;
|
||||
|
||||
[[nodiscard]] static PipelineRosters defaults();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user