fix: Found and fix review mistakes

This commit is contained in:
Petr Mironychev
2026-07-02 22:26:07 +02:00
parent c070d65366
commit 35bbaa1af0
139 changed files with 2032 additions and 1573 deletions

View File

@@ -33,12 +33,12 @@ namespace {
enum class PillKind {
Template,
On, // capability on (thinking/tools)
Off, // capability off ("plain")
User, // user-defined agent
Active, // ✓ active
Match, // matched-this-row chip background
Tag, // free-form discoverability tag from AgentConfig::tags
On,
Off,
User,
Active,
Match,
Tag,
Neutral,
};
@@ -192,16 +192,17 @@ class AgentRosterRow : public QFrame
{
Q_OBJECT
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);
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);
signals:
void moveUpRequested(int index);
@@ -221,17 +222,19 @@ private:
int m_index;
};
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)
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)
{
setAutoFillBackground(true);
QPalette pal = palette();
@@ -334,8 +337,8 @@ QWidget *AgentRosterRow::buildIdentityLine(const QString &displayName,
return w;
}
QWidget *AgentRosterRow::buildMetaLine(const AgentConfig *cfg, bool active, bool showMatch,
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);
@@ -352,14 +355,16 @@ QWidget *AgentRosterRow::buildMetaLine(const AgentConfig *cfg, bool active, bool
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());
.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;")
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);
@@ -457,14 +462,14 @@ 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:4px; }")
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 agent selected yet — use \"Add agent…\" below."),
m_rowsFrame);
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);
QFont eh = m_emptyHint->font();
@@ -497,9 +502,7 @@ AgentRosterWidget::AgentRosterWidget(QWidget *parent)
}
void AgentRosterWidget::setSlot(
const QString &title,
const QString &hint,
const QStringList &presetTags)
const QString &title, const QString &hint, const QStringList &presetTags)
{
m_presetTags = presetTags;
m_titleLabel->setText(title);
@@ -514,13 +517,14 @@ void AgentRosterWidget::setRoster(const QStringList &names, AgentFactory *factor
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_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;
@@ -558,6 +562,13 @@ void AgentRosterWidget::applyMode()
m_footerHint->setVisible(!footer.isEmpty());
}
void AgentRosterWidget::setRoutingContext(const AgentRouter::Context &ctx)
{
m_routingCtx = ctx;
if (!m_names.isEmpty())
rebuildRows();
}
void AgentRosterWidget::recomputeActive()
{
if (!m_orderable || !m_factory || m_names.isEmpty()
@@ -571,7 +582,6 @@ void AgentRosterWidget::recomputeActive()
void AgentRosterWidget::rebuildRows()
{
// Tear down existing row widgets (keep the empty hint).
while (m_rowsLayout->count() > 0) {
QLayoutItem *it = m_rowsLayout->takeAt(0);
if (auto *w = it->widget()) {
@@ -598,16 +608,17 @@ void AgentRosterWidget::rebuildRows()
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);
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);
connect(row, &AgentRosterRow::moveDownRequested, this, &AgentRosterWidget::onRowMoveDown);
connect(row, &AgentRosterRow::editRequested, this, &AgentRosterWidget::onRowEdit);
@@ -621,11 +632,12 @@ void AgentRosterWidget::onAddClicked()
if (!m_factory)
return;
AgentSelectionDialog dialog(m_factory->configs(),
/*currentName*/ m_single ? m_names.value(0) : QString{},
m_factory.data(),
m_presetTags,
Core::ICore::dialogParent());
AgentSelectionDialog dialog(
m_factory->configs(),
/*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();

View File

@@ -31,20 +31,12 @@ class AgentRosterWidget : public QWidget
public:
explicit AgentRosterWidget(QWidget *parent = nullptr);
void setSlot(
const QString &title,
const QString &hint,
const QStringList &presetTags = {});
void setSlot(const QString &title, const QString &hint, const QStringList &presetTags = {});
void setRoster(const QStringList &names, AgentFactory *factory);
void setRoutingContext(const AgentRouter::Context &ctx);
// 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);
// 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; }

View File

@@ -4,8 +4,8 @@
#include "AgentSelectionDialog.hpp"
#include "PipelinesConfig.hpp"
#include "Pill.hpp"
#include "PipelinesConfig.hpp"
#include "SettingsTr.hpp"
#include "TagFilterStrip.hpp"
@@ -13,6 +13,7 @@
#include <AgentFactory.hpp>
#include <algorithm>
#include <QDialogButtonBox>
#include <QEnterEvent>
#include <QEvent>
@@ -27,12 +28,9 @@
#include <QSet>
#include <QTimer>
#include <QVBoxLayout>
#include <algorithm>
namespace QodeAssist::Settings {
// -- ListRowCard -------------------------------------------------------
ListRowCard::ListRowCard(QWidget *parent)
: QFrame(parent)
{
@@ -128,16 +126,13 @@ void ListRowCard::applyTheme()
.arg(bg, bd));
}
// -- AgentRowCard ------------------------------------------------------
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.systemPrompt,
cfg.endpoint};
QStringList haystack{
cfg.name, cfg.providerInstance, cfg.model, cfg.description, cfg.systemPrompt, cfg.endpoint};
haystack += cfg.tags;
buildSearchHaystack(haystack);
@@ -249,8 +244,6 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
setToolTip(tooltip.trimmed());
}
// -- ProviderSection ---------------------------------------------------
ProviderSection::ProviderSection(const QString &name, QWidget *parent)
: QWidget(parent)
{
@@ -337,8 +330,6 @@ bool ProviderSection::eventFilter(QObject *watched, QEvent *event)
return QWidget::eventFilter(watched, event);
}
// -- AgentSelectionDialog ----------------------------------------------
AgentSelectionDialog::AgentSelectionDialog(
const std::vector<AgentConfig> &configs,
const QString &currentName,
@@ -380,10 +371,10 @@ AgentSelectionDialog::AgentSelectionDialog(
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 *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);
@@ -425,8 +416,9 @@ AgentSelectionDialog::AgentSelectionDialog(
preset.insert(tag);
m_tagStrip->setAvailableTags(tagCounts, preset);
connect(m_tagStrip, &TagFilterStrip::activeTagsChanged, this,
[this](const QSet<QString> &) { applyFilters(); });
connect(m_tagStrip, &TagFilterStrip::activeTagsChanged, this, [this](const QSet<QString> &) {
applyFilters();
});
connect(expandAll, &QLabel::linkActivated, this, [this](const QString &) {
setAllExpanded(true);
});
@@ -436,8 +428,7 @@ AgentSelectionDialog::AgentSelectionDialog(
rebuild(currentName);
connect(m_filter, &QLineEdit::textChanged, this,
[this](const QString &) { applyFilters(); });
connect(m_filter, &QLineEdit::textChanged, this, [this](const QString &) { applyFilters(); });
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
@@ -486,7 +477,8 @@ void AgentSelectionDialog::rebuild(const QString &currentName)
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
if (cfg.hidden)
continue;
const QString key = cfg.providerInstance.isEmpty()
? Tr::tr("(Unknown provider instance)")
: cfg.providerInstance;
@@ -565,8 +557,7 @@ void AgentSelectionDialog::applyFilters()
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));
m_resultCount->setText(total == 0 ? tr("No matches") : tr("%n agent(s)", nullptr, total));
if (m_tagStrip) {
QMap<QString, int> liveCounts;

View File

@@ -4,6 +4,7 @@
#pragma once
#include <vector>
#include <QDialog>
#include <QFont>
#include <QFrame>
@@ -11,7 +12,6 @@
#include <QPalette>
#include <QSet>
#include <QStringList>
#include <vector>
#include <AgentConfig.hpp>
@@ -142,7 +142,6 @@ public:
void setExpanded(bool expanded);
const QList<ListRowCard *> &cards() const { return m_cards; }
// returns number of visible cards
int applyFilter(const QString &needle, const QSet<QString> &activeTags);
protected:
@@ -189,7 +188,7 @@ private:
QString m_selectedName;
AgentFactory *m_agentFactory = nullptr;
std::vector<AgentConfig> m_localConfigs; // fallback when no factory
std::vector<AgentConfig> m_localConfigs;
QStringList m_presetTags;
};

View File

@@ -87,9 +87,10 @@ void Pill::applyTheme()
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; }")
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)));
}

View File

@@ -9,8 +9,6 @@
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

View File

@@ -18,7 +18,6 @@
#include "Logger.hpp"
#include "TomlWriter.hpp"
#include <AgentFactory.hpp>
namespace QodeAssist::Settings {
@@ -45,8 +44,6 @@ QString toSingleString(const toml::node *node, const QString &slotKey, bool *sch
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()) {
@@ -115,19 +112,53 @@ void fillMissingFromDefaults(PipelineRosters &r, const toml::table &section)
r.quickRefactor = defs.quickRefactor;
}
struct CacheState
{
PipelinesLoadResult result;
QDateTime mtime;
qint64 size = -1;
bool valid = false;
};
CacheState &cacheState()
{
static CacheState s;
return s;
}
QString &filePathOverride()
{
static QString p;
return p;
}
} // namespace
PipelineRosters PipelineRosters::defaults()
{
return PipelineRosters{};
return PipelineRosters{
{QStringLiteral("Ollama Completion — FIM")},
{QStringLiteral("Ollama Chat — Simple"),
QStringLiteral("Ollama Chat — Thinking"),
QStringLiteral("Ollama Chat — Gemma 4")},
QStringLiteral("Ollama Compression — 8 GB"),
QStringLiteral("Ollama Quick Refactor — Simple")};
}
QString PipelinesConfig::filePath()
{
if (!filePathOverride().isEmpty())
return filePathOverride();
return Core::ICore::userResourcePath(QStringLiteral("qodeassist/config/pipelines.toml"))
.toFSPathString();
}
void PipelinesConfig::setFilePathForTests(const QString &path)
{
filePathOverride() = path;
cacheState() = {};
}
PipelinesLoadResult PipelinesConfig::load()
{
PipelinesLoadResult result;
@@ -206,19 +237,18 @@ PipelinesLoadResult PipelinesConfig::load()
PipelinesLoadResult PipelinesConfig::loadCached()
{
static PipelinesLoadResult cached;
static QDateTime cachedMTime;
static bool valid = false;
auto &cache = cacheState();
const QFileInfo info(filePath());
const QDateTime mtime = info.exists() ? info.lastModified() : QDateTime();
if (valid && mtime == cachedMTime)
return cached;
const qint64 size = info.exists() ? info.size() : -1;
if (cache.valid && mtime == cache.mtime && size == cache.size)
return cache.result;
cached = load();
cachedMTime = mtime;
valid = true;
return cached;
cache.result = load();
cache.mtime = mtime;
cache.size = size;
cache.valid = true;
return cache.result;
}
bool PipelinesConfig::save(const PipelineRosters &rosters, QString *errorOut)
@@ -236,15 +266,14 @@ bool PipelinesConfig::save(const PipelineRosters &rosters, QString *errorOut)
TomlSerializer::TomlWriter w;
w.writeComment(QStringLiteral("QodeAssist pipelines — which agent each feature uses."));
w.writeComment(QStringLiteral(
"code_completion: ordered list; the router walks it top-down and uses"));
w.writeComment(QStringLiteral(
" 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("code_completion: ordered list; the router walks it top-down and uses"));
w.writeComment(
QStringLiteral(" 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.writeComment(QStringLiteral("chat_compression / quick_refactor: a single agent name."));
w.writeBlankLine();
w.writeTableHeader(QString::fromUtf8(kSection));
w.writeStringArray(QString::fromUtf8(kCodeCompletion), rosters.codeCompletion);
@@ -276,56 +305,8 @@ bool PipelinesConfig::save(const PipelineRosters &rosters, QString *errorOut)
*errorOut = out.errorString();
return false;
}
return true;
}
bool PipelinesConfig::validate(const QodeAssist::AgentFactory &factory, QString *errorOut)
{
PipelinesLoadResult lr = load();
PipelineRosters &r = lr.rosters;
bool changed = false;
auto fix = [&](QStringList &current) {
QStringList kept;
kept.reserve(current.size());
for (const QString &raw : current) {
const QString name = trimAndCap(raw);
if (name.isEmpty() || kept.contains(name))
continue;
if (factory.configByName(name))
kept.append(name);
}
if (kept != current) {
current = std::move(kept);
changed = true;
}
};
auto fixOne = [&](QString &current) {
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);
fixOne(r.chatCompression);
fixOne(r.quickRefactor);
if (!changed && lr.status == PipelinesLoadStatus::Ok)
return true;
QString saveErr;
if (!save(r, &saveErr)) {
const QString msg = QStringLiteral("failed to persist after validation: %1").arg(saveErr);
LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(msg));
if (errorOut)
*errorOut = msg;
return false;
}
cacheState() = {};
PipelinesNotifier::instance()->notifyChanged();
return true;
}

View File

@@ -4,24 +4,35 @@
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
namespace QodeAssist {
class AgentFactory;
}
namespace QodeAssist::Settings {
class PipelinesNotifier : public QObject
{
Q_OBJECT
public:
static PipelinesNotifier *instance()
{
static PipelinesNotifier notifier;
return &notifier;
}
void notifyChanged() { emit pipelinesChanged(); }
signals:
void pipelinesChanged();
private:
PipelinesNotifier() = default;
};
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;
// Compression and quick refactor each use a single fixed agent.
QString chatCompression;
QString quickRefactor;
@@ -48,8 +59,7 @@ public:
[[nodiscard]] static bool save(const PipelineRosters &rosters, QString *errorOut = nullptr);
[[nodiscard]] static bool validate(
const QodeAssist::AgentFactory &factory, QString *errorOut = nullptr);
static void setFilePathForTests(const QString &path);
};
} // namespace QodeAssist::Settings