refactor: Remove all experimental code

This commit is contained in:
Petr Mironychev
2026-07-13 09:37:04 +02:00
parent 34ce787320
commit adef7972ed
145 changed files with 0 additions and 13468 deletions

View File

@@ -1,476 +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 "AgentDetailPane.hpp"
#include "SectionBox.hpp"
#include "SettingsTheme.hpp"
#include "SettingsUiBuilders.hpp"
#include <ProviderInstance.hpp>
#include <ProviderInstanceFactory.hpp>
#include <QColor>
#include <QComboBox>
#include <QEvent>
#include <QFile>
#include <QFont>
#include <QFrame>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QScopedValueRollback>
#include <QToolButton>
#include <QVBoxLayout>
namespace QodeAssist::Settings {
namespace {
constexpr qint64 kRawTomlMaxBytes = 256 * 1024;
enum class FileReadStatus { Ok, Empty, Truncated, OpenFailed };
struct FileReadResult
{
FileReadStatus status = FileReadStatus::OpenFailed;
QString content;
QString error;
};
FileReadResult readFileTextCapped(const QString &path, qint64 maxBytes)
{
FileReadResult result;
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
result.status = FileReadStatus::OpenFailed;
result.error = f.errorString();
return result;
}
const qint64 size = f.size();
const QByteArray bytes = f.read(maxBytes);
result.content = QString::fromUtf8(bytes);
if (size == 0)
result.status = FileReadStatus::Empty;
else if (size > maxBytes)
result.status = FileReadStatus::Truncated;
else
result.status = FileReadStatus::Ok;
return result;
}
} // namespace
AgentDetailPane::AgentDetailPane(QWidget *parent)
: QWidget(parent)
{
m_name = new QLabel(this);
QFont nf = m_name->font();
nf.setBold(true);
nf.setPixelSize(15);
m_name->setFont(nf);
m_name->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_path = new QLabel(this);
m_path->setFont(monospaceFont(11));
m_path->setTextInteractionFlags(Qt::TextSelectableByMouse);
QPalette pp = m_path->palette();
pp.setColor(QPalette::WindowText, pp.color(QPalette::Mid));
m_path->setPalette(pp);
m_openBtn = new QPushButton(tr("Open in editor"), this);
m_dupBtn = new QPushButton(tr("Duplicate…"), this);
m_deleteBtn = new QPushButton(tr("Delete"), this);
connect(m_openBtn, &QPushButton::clicked, this,
[this] { if (m_current) emit openInEditorRequested(*m_current); });
connect(m_dupBtn, &QPushButton::clicked, this,
[this] { if (m_current) emit customizeRequested(*m_current); });
connect(m_deleteBtn, &QPushButton::clicked, this,
[this] { if (m_current) emit deleteRequested(*m_current); });
auto *actions = new QHBoxLayout;
actions->setContentsMargins(0, 0, 0, 0);
actions->setSpacing(6);
actions->addWidget(m_openBtn);
actions->addWidget(m_dupBtn);
actions->addWidget(m_deleteBtn);
auto *titleRow = new QHBoxLayout;
titleRow->setContentsMargins(0, 0, 0, 0);
titleRow->setSpacing(8);
titleRow->addWidget(m_name);
titleRow->addStretch(1);
auto *headerLeft = new QVBoxLayout;
headerLeft->setContentsMargins(0, 0, 0, 0);
headerLeft->setSpacing(2);
headerLeft->addLayout(titleRow);
headerLeft->addWidget(m_path);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(8);
headerRow->addLayout(headerLeft, 1);
headerRow->addLayout(actions);
auto *headerSep = new QFrame(this);
headerSep->setFrameShape(QFrame::HLine);
headerSep->setFrameShadow(QFrame::Sunken);
m_description = new QLabel(this);
m_description->setWordWrap(true);
m_description->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *identity = new SectionBox(tr("Identity"), this);
m_nameValue = makeReadOnlyLine();
m_extendsLabel = new QLabel(tr("Extends:"), this);
m_extendsLabel->setMinimumWidth(96);
m_extendsLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
m_extendsValue = makeReadOnlyLine();
m_descriptionEdit = new QPlainTextEdit(this);
m_descriptionEdit->setReadOnly(true);
m_descriptionEdit->setMaximumHeight(56);
m_tagsValue = makeReadOnlyLine();
auto *idGrid = new QGridLayout;
idGrid->setContentsMargins(0, 0, 0, 0);
idGrid->setHorizontalSpacing(8);
idGrid->setVerticalSpacing(4);
FormBuilder idForm(idGrid);
idForm.row(tr("Name:"), m_nameValue);
{
auto *holder = new QWidget;
holder->setLayout(singleField(m_extendsValue));
const int row = idForm.currentRow();
idGrid->addWidget(m_extendsLabel, row, 0, Qt::AlignTop);
idGrid->addWidget(holder, row, 1);
m_extendsHolder = holder;
idForm = FormBuilder(idGrid, row + 1);
}
idForm.row(tr("Description:"), m_descriptionEdit);
idForm.row(tr("Tags:"), m_tagsValue,
tr("Comma-separated. Free-form — used to filter and "
"group the agent list."));
identity->bodyLayout()->addLayout(idGrid);
auto *roleSection = new SectionBox(tr("System role"), this);
auto *roleHint = makeHintLabel(
tr("Prepended to every request as the system message."));
m_roleText = new QPlainTextEdit(this);
m_roleText->setReadOnly(true);
m_roleText->setMinimumHeight(120);
roleSection->bodyLayout()->addWidget(roleHint);
roleSection->bodyLayout()->addWidget(m_roleText);
auto *contextSection = new SectionBox(tr("Context"), this);
auto *contextHint = makeHintLabel(
tr("Jinja2 template rendered with ContextManager bindings into the "
"agent.context system-prompt layer. Empty = no context block."));
m_contextText = new QPlainTextEdit(this);
m_contextText->setReadOnly(true);
m_contextText->setFont(monospaceFont(11));
m_contextText->setMinimumHeight(120);
contextSection->bodyLayout()->addWidget(contextHint);
contextSection->bodyLayout()->addWidget(m_contextText);
auto *connection = new SectionBox(tr("Connection"), this);
m_providerCombo = new QComboBox(this);
m_providerCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
m_providerCombo->setEnabled(false);
m_endpointValue = makeReadOnlyLine(true);
m_modelValue = makeReadOnlyLine(true);
auto *connGrid = new QGridLayout;
connGrid->setContentsMargins(0, 0, 0, 0);
connGrid->setHorizontalSpacing(8);
connGrid->setVerticalSpacing(4);
FormBuilder(connGrid)
.row(tr("Provider:"), m_providerCombo,
tr("The provider instance this agent uses. URL is "
"inherited from the instance."))
.row(tr("Endpoint:"), m_endpointValue,
tr("Appended to the provider's URL. Blank uses the "
"provider default."))
.row(tr("Model:"), m_modelValue);
connection->bodyLayout()->addLayout(connGrid);
m_effectiveUrl = new QLabel(this);
m_effectiveUrl->setFont(monospaceFont(11));
m_effectiveUrl->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_effectiveUrl->setWordWrap(true);
m_effectiveUrl->setContentsMargins(6, 4, 6, 4);
m_effectiveUrl->setAutoFillBackground(true);
connection->bodyLayout()->addWidget(m_effectiveUrl);
auto *match = new SectionBox(tr("Match"), this);
auto *matchHint = makeHintLabel(
tr("When a feature slot has multiple bound agents, the first whose "
"match rules satisfy the current context wins."));
m_filePatternsValue = makeReadOnlyLine(true);
auto *matchGrid = new QGridLayout;
matchGrid->setContentsMargins(0, 0, 0, 0);
matchGrid->setHorizontalSpacing(8);
matchGrid->setVerticalSpacing(4);
FormBuilder(matchGrid).row(tr("File patterns:"), m_filePatternsValue,
tr("Globs, comma-separated. Empty matches every file."));
match->bodyLayout()->addWidget(matchHint);
match->bodyLayout()->addLayout(matchGrid);
auto *templ = new SectionBox(tr("Template"), this);
auto *templHint = makeHintLabel(
tr("Jinja2 template (via inja) rendered to the request body. "
"Built-in context: ctx.prefix, ctx.suffix, ctx.history, "
"ctx.system_prompt, agent.model."));
m_messageFormat = new QPlainTextEdit(this);
m_messageFormat->setReadOnly(true);
m_messageFormat->setFont(monospaceFont(11));
m_messageFormat->setMinimumHeight(140);
templ->bodyLayout()->addWidget(templHint);
auto *mfLabel = new QLabel(tr("message_format:"), this);
templ->bodyLayout()->addWidget(mfLabel);
templ->bodyLayout()->addWidget(m_messageFormat);
m_diagnostics = new SectionBox(tr("Load errors"), this);
m_diagnosticsView = new QPlainTextEdit(this);
m_diagnosticsView->setReadOnly(true);
m_diagnosticsView->setMaximumHeight(110);
m_diagnosticsView->setFont(monospaceFont(11));
m_diagnostics->bodyLayout()->addWidget(m_diagnosticsView);
m_diagnostics->setVisible(false);
m_rawToggle = new QToolButton(this);
m_rawToggle->setText(tr("▸ Show raw TOML"));
m_rawToggle->setCursor(Qt::PointingHandCursor);
m_rawToggle->setAutoRaise(true);
m_rawToggle->setCheckable(true);
m_rawToml = new QPlainTextEdit(this);
m_rawToml->setReadOnly(true);
m_rawToml->setFont(monospaceFont(11));
m_rawToml->setMinimumHeight(140);
m_rawToml->setVisible(false);
connect(m_rawToggle, &QToolButton::toggled, this, [this](bool on) {
m_rawToml->setVisible(on);
m_rawToggle->setText(on ? tr("▾ Hide raw TOML") : tr("▸ Show raw TOML"));
});
auto *root = new QVBoxLayout(this);
root->setContentsMargins(12, 12, 12, 12);
root->setSpacing(10);
root->addLayout(headerRow);
root->addWidget(headerSep);
root->addWidget(m_description);
root->addWidget(identity);
root->addWidget(connection);
root->addWidget(match);
root->addWidget(templ);
root->addWidget(roleSection);
root->addWidget(contextSection);
root->addWidget(m_diagnostics);
root->addWidget(m_rawToggle, 0, Qt::AlignLeft);
root->addWidget(m_rawToml);
root->addStretch(1);
clear();
applyCodePalette();
}
void AgentDetailPane::setInstanceFactory(Providers::ProviderInstanceFactory *factory)
{
m_instanceFactory = factory;
m_providerComboPopulated = false;
populateProviderCombo();
}
void AgentDetailPane::populateProviderCombo()
{
if (m_providerComboPopulated)
return;
m_providerCombo->clear();
m_providerComboHasSentinel = false;
if (m_instanceFactory) {
for (const auto &inst : m_instanceFactory->instances()) {
m_providerCombo->addItem(
QStringLiteral("%1 (%2)").arg(inst.name, inst.clientApi), inst.name);
}
}
m_providerComboPopulated = true;
}
void AgentDetailPane::setAgent(const AgentConfig &cfg)
{
m_currentStorage = cfg;
m_current = &m_currentStorage;
const bool user = cfg.isUserSource();
m_name->setText(cfg.name);
m_path->setText(cfg.sourcePath);
m_description->setText(cfg.description.isEmpty()
? tr("No description provided.")
: cfg.description);
m_nameValue->setText(cfg.name);
if (cfg.extendsName.isEmpty()) {
m_extendsLabel->setVisible(false);
m_extendsHolder->setVisible(false);
} else {
m_extendsLabel->setVisible(true);
m_extendsHolder->setVisible(true);
m_extendsValue->setText(cfg.extendsName);
}
m_descriptionEdit->setPlainText(cfg.description);
m_tagsValue->setText(cfg.tags.join(QStringLiteral(", ")));
populateProviderCombo();
if (m_providerComboHasSentinel) {
m_providerCombo->removeItem(0);
m_providerComboHasSentinel = false;
}
QString resolvedUrl;
if (m_instanceFactory) {
if (const auto *inst = m_instanceFactory->instanceByName(cfg.providerInstance))
resolvedUrl = inst->url;
}
const int idx = m_providerCombo->findData(cfg.providerInstance);
if (idx >= 0) {
m_providerCombo->setCurrentIndex(idx);
} else if (!cfg.providerInstance.isEmpty()) {
m_providerCombo->insertItem(
0, tr("%1 (missing — not in provider library)")
.arg(cfg.providerInstance),
cfg.providerInstance);
m_providerCombo->setCurrentIndex(0);
m_providerComboHasSentinel = true;
}
m_endpointValue->setText(cfg.endpoint);
m_endpointValue->setPlaceholderText(tr("(provider default)"));
m_modelValue->setText(cfg.model);
const QString eff = resolvedUrl + cfg.endpoint;
m_effectiveUrl->setText(
eff.isEmpty()
? tr("# effective request line\n(unknown — provider instance not found)")
: QStringLiteral("# %1\nPOST %2")
.arg(tr("effective request line"), eff));
m_roleText->setPlainText(
cfg.role.isEmpty() ? tr("(no system role set)") : cfg.role);
m_contextText->setPlainText(
cfg.context.isEmpty() ? tr("(no context block)") : cfg.context);
m_filePatternsValue->setText(cfg.match.filePatterns.join(QStringLiteral(", ")));
m_filePatternsValue->setPlaceholderText(tr("(matches every file)"));
m_messageFormat->setPlainText(
cfg.messageFormat.isEmpty() ? tr("(inherited from parent / none)")
: cfg.messageFormat);
const FileReadResult raw = readFileTextCapped(cfg.sourcePath, kRawTomlMaxBytes);
switch (raw.status) {
case FileReadStatus::Ok:
m_rawToml->setPlainText(raw.content);
break;
case FileReadStatus::Truncated:
m_rawToml->setPlainText(
raw.content + QStringLiteral("\n\n")
+ tr("(truncated at %1 bytes)").arg(kRawTomlMaxBytes));
break;
case FileReadStatus::Empty:
m_rawToml->setPlainText(tr("(source file is empty)"));
break;
case FileReadStatus::OpenFailed:
m_rawToml->setPlainText(tr("(source file unavailable: %1)").arg(raw.error));
break;
}
m_openBtn->setEnabled(user);
m_openBtn->setToolTip(user ? QString()
: tr("Bundled agents are read-only — "
"duplicate to edit."));
m_deleteBtn->setEnabled(user);
m_deleteBtn->setToolTip(user ? QString()
: tr("Bundled agents cannot be deleted."));
m_dupBtn->setEnabled(true);
applyCodePalette();
}
void AgentDetailPane::clear()
{
m_currentStorage = AgentConfig{};
m_current = nullptr;
m_name->setText(tr("Select an agent"));
m_path->clear();
m_description->setText(tr("Pick an agent from the list to see its details."));
m_nameValue->clear();
m_extendsLabel->setVisible(false);
m_extendsHolder->setVisible(false);
m_descriptionEdit->clear();
m_tagsValue->clear();
if (m_providerComboHasSentinel) {
m_providerCombo->removeItem(0);
m_providerComboHasSentinel = false;
}
m_providerCombo->setCurrentIndex(-1);
m_endpointValue->clear();
m_modelValue->clear();
m_effectiveUrl->clear();
m_roleText->clear();
m_contextText->clear();
m_filePatternsValue->clear();
m_messageFormat->clear();
m_rawToml->clear();
m_openBtn->setEnabled(false);
m_dupBtn->setEnabled(false);
m_deleteBtn->setEnabled(false);
}
void AgentDetailPane::setLoadDiagnostics(const QStringList &errors, const QStringList &warnings)
{
QStringList lines;
for (const QString &e : errors)
lines << tr("error: %1").arg(e);
for (const QString &w : warnings)
lines << tr("warning: %1").arg(w);
m_diagnostics->setVisible(!lines.isEmpty());
m_diagnosticsView->setPlainText(lines.join(QLatin1Char('\n')));
}
void AgentDetailPane::changeEvent(QEvent *event)
{
QWidget::changeEvent(event);
if (m_inApplyPalette)
return;
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
applyCodePalette();
}
QLineEdit *AgentDetailPane::makeReadOnlyLine(bool mono)
{
auto *e = new QLineEdit(this);
e->setReadOnly(true);
if (mono)
e->setFont(monospaceFont(11));
return e;
}
void AgentDetailPane::applyCodePalette()
{
QScopedValueRollback<bool> guard(m_inApplyPalette, true);
const Theme theme = themeFor(palette());
QPalette p = m_effectiveUrl->palette();
p.setColor(QPalette::Window, QColor(theme.codeBg));
p.setColor(QPalette::WindowText, palette().color(QPalette::Text));
m_effectiveUrl->setPalette(p);
m_effectiveUrl->setStyleSheet(QStringLiteral(
"QLabel { background:%1; border:1px solid %2; }")
.arg(theme.codeBg, theme.rowSeparator));
}
} // namespace QodeAssist::Settings

View File

@@ -1,92 +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 <QPointer>
#include <QStringList>
#include <QWidget>
#include <AgentConfig.hpp>
class QComboBox;
class QLabel;
class QLineEdit;
class QPlainTextEdit;
class QPushButton;
class QToolButton;
namespace QodeAssist::Providers {
class ProviderInstanceFactory;
}
namespace QodeAssist::Settings {
class SectionBox;
class AgentDetailPane : public QWidget
{
Q_OBJECT
public:
explicit AgentDetailPane(QWidget *parent = nullptr);
void setInstanceFactory(Providers::ProviderInstanceFactory *factory);
void setAgent(const AgentConfig &cfg);
void clear();
void setLoadDiagnostics(const QStringList &errors, const QStringList &warnings);
signals:
void openInEditorRequested(const AgentConfig &cfg);
void customizeRequested(const AgentConfig &cfg);
void deleteRequested(const AgentConfig &cfg);
protected:
void changeEvent(QEvent *event) override;
private:
QLineEdit *makeReadOnlyLine(bool mono = false);
void applyCodePalette();
void populateProviderCombo();
bool m_inApplyPalette = false;
bool m_providerComboPopulated = false;
bool m_providerComboHasSentinel = false;
AgentConfig m_currentStorage;
const AgentConfig *m_current = nullptr;
QLabel *m_name = nullptr;
QLabel *m_path = nullptr;
QPushButton *m_openBtn = nullptr;
QPushButton *m_dupBtn = nullptr;
QPushButton *m_deleteBtn = nullptr;
QLabel *m_description = nullptr;
QLineEdit *m_nameValue = nullptr;
QLabel *m_extendsLabel = nullptr;
QWidget *m_extendsHolder = nullptr;
QLineEdit *m_extendsValue = nullptr;
QPlainTextEdit *m_descriptionEdit = nullptr;
QLineEdit *m_tagsValue = nullptr;
QComboBox *m_providerCombo = nullptr;
QPointer<Providers::ProviderInstanceFactory> m_instanceFactory;
QLineEdit *m_endpointValue = nullptr;
QLineEdit *m_modelValue = nullptr;
QLabel *m_effectiveUrl = nullptr;
QLineEdit *m_filePatternsValue = nullptr;
QPlainTextEdit *m_roleText = nullptr;
QPlainTextEdit *m_contextText = nullptr;
QPlainTextEdit *m_messageFormat = nullptr;
SectionBox *m_diagnostics = nullptr;
QPlainTextEdit *m_diagnosticsView = nullptr;
QToolButton *m_rawToggle = nullptr;
QPlainTextEdit *m_rawToml = nullptr;
};
} // namespace QodeAssist::Settings

View File

@@ -1,121 +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 "AgentDuplicator.hpp"
#include <Agent.hpp>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <QByteArray>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSaveFile>
namespace QodeAssist::Settings {
namespace {
QString tomlEscape(const QString &s)
{
QString out;
out.reserve(s.size());
for (QChar c : s) {
switch (c.unicode()) {
case '\\': out += QLatin1String("\\\\"); break;
case '"': out += QLatin1String("\\\""); break;
case '\n': out += QLatin1String("\\n"); break;
case '\r': out += QLatin1String("\\r"); break;
case '\t': out += QLatin1String("\\t"); break;
default: out += c;
}
}
return out;
}
constexpr int kMaxUniqueAttempts = 1000;
QString uniqueFilename(const QString &userDir, const QString &parentBasename)
{
QString fileName = parentBasename + QStringLiteral("_custom.toml");
for (int n = 2; n < kMaxUniqueAttempts
&& QFile::exists(QDir(userDir).filePath(fileName));
++n)
fileName = QStringLiteral("%1_custom_%2.toml").arg(parentBasename).arg(n);
return QDir(userDir).filePath(fileName);
}
QString uniqueName(const QString &parentName, const AgentFactory &factory)
{
QString newName = QStringLiteral("%1 (Custom)").arg(parentName);
for (int n = 2; n < kMaxUniqueAttempts && factory.configByName(newName); ++n)
newName = QStringLiteral("%1 (Custom %2)").arg(parentName).arg(n);
return newName;
}
QString trUser(const char *src)
{
return QCoreApplication::translate("QodeAssist::Settings::AgentDuplicator", src);
}
} // namespace
AgentDuplicateResult duplicateAgentInUserDir(
const AgentConfig &parent, const AgentFactory &factory)
{
AgentDuplicateResult result;
if (parent.name.trimmed().isEmpty()) {
result.error = trUser("Parent agent has no name; cannot duplicate.");
return result;
}
const QString userDir = AgentFactory::userAgentsDir();
if (!QDir().mkpath(userDir)) {
result.error = trUser("Cannot create user agents folder: %1").arg(userDir);
return result;
}
const QString parentBasename = QFileInfo(parent.sourcePath).baseName();
result.filePath = uniqueFilename(userDir, parentBasename);
if (QFile::exists(result.filePath)) {
result.error = trUser("Could not find a free filename after %1 attempts.")
.arg(kMaxUniqueAttempts);
return result;
}
result.newName = uniqueName(parent.name, factory);
if (factory.configByName(result.newName)) {
result.error = trUser("Could not find a free agent name after %1 attempts.")
.arg(kMaxUniqueAttempts);
return result;
}
QSaveFile f(result.filePath);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
result.error = trUser("Cannot create %1: %2").arg(result.filePath, f.errorString());
return result;
}
const QString description
= QStringLiteral("User customization of '%1'. Override fields below to taste; "
"values not overridden are inherited from the parent.")
.arg(parent.name);
const QString body = QStringLiteral(
"schema_version = 1\n"
"name = \"%1\"\n"
"extends = \"%2\"\n"
"description = \"%3\"\n")
.arg(tomlEscape(result.newName),
tomlEscape(parent.name),
tomlEscape(description));
const QByteArray payload = body.toUtf8();
if (f.write(payload) != payload.size() || !f.commit()) {
result.error = trUser("Failed to write %1: %2").arg(result.filePath, f.errorString());
return result;
}
result.ok = true;
return result;
}
} // namespace QodeAssist::Settings

View File

@@ -1,27 +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 <QString>
namespace QodeAssist {
class AgentFactory;
struct AgentConfig;
}
namespace QodeAssist::Settings {
struct AgentDuplicateResult
{
bool ok = false;
QString filePath;
QString newName;
QString error;
};
AgentDuplicateResult duplicateAgentInUserDir(
const AgentConfig &parent, const AgentFactory &factory);
} // namespace QodeAssist::Settings

View File

@@ -1,128 +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 "AgentListItem.hpp"
#include "SettingsTheme.hpp"
#include "TagChip.hpp"
#include <QEvent>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QPalette>
#include <QScopedValueRollback>
#include <QVBoxLayout>
namespace QodeAssist::Settings {
AgentListItem::AgentListItem(const AgentConfig &cfg, QWidget *parent)
: QFrame(parent)
, m_name(cfg.name)
{
setObjectName(QStringLiteral("AgentListItem"));
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
setCursor(Qt::PointingHandCursor);
auto *dot = new QLabel(QStringLiteral(""), this);
QFont df = dot->font();
df.setPixelSize(10);
dot->setFont(df);
QPalette dp = dot->palette();
dp.setColor(QPalette::WindowText, dp.color(QPalette::Mid));
dot->setPalette(dp);
auto *nameLbl = new QLabel(cfg.name, this);
QFont nf = nameLbl->font();
nf.setBold(true);
nf.setPixelSize(12);
nameLbl->setFont(nf);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(6);
headerRow->addWidget(dot, 0, Qt::AlignVCenter);
headerRow->addWidget(nameLbl, 1);
auto *col = new QVBoxLayout;
col->setContentsMargins(0, 0, 0, 0);
col->setSpacing(2);
col->addLayout(headerRow);
if (!cfg.model.isEmpty()) {
auto *model = new QLabel(cfg.model, this);
model->setFont(monospaceFont(11));
model->setContentsMargins(16, 0, 0, 0);
QPalette mp = model->palette();
mp.setColor(QPalette::WindowText, mp.color(QPalette::Mid));
model->setPalette(mp);
col->addWidget(model);
}
if (!cfg.tags.isEmpty()) {
auto *tagsHolder = new QWidget(this);
auto *tagsLay = new QHBoxLayout(tagsHolder);
tagsLay->setContentsMargins(16, 2, 0, 0);
tagsLay->setSpacing(3);
for (const QString &t : cfg.tags) {
auto *chip = new TagChip(t, -1, tagsHolder);
connect(chip, &TagChip::clicked, this, &AgentListItem::tagClicked);
m_chips.append(chip);
tagsLay->addWidget(chip);
}
tagsLay->addStretch(1);
col->addWidget(tagsHolder);
}
auto *outer = new QVBoxLayout(this);
outer->setContentsMargins(8, 6, 8, 6);
outer->setSpacing(0);
outer->addLayout(col);
applyTheme();
}
void AgentListItem::setSelected(bool selected)
{
if (m_selected == selected)
return;
m_selected = selected;
applyTheme();
}
void AgentListItem::setActiveTags(const QSet<QString> &active)
{
for (auto *chip : m_chips)
chip->setActive(active.contains(chip->tag()));
}
void AgentListItem::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
emit clicked(m_name);
QFrame::mouseReleaseEvent(event);
}
void AgentListItem::changeEvent(QEvent *event)
{
QFrame::changeEvent(event);
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
applyTheme();
}
void AgentListItem::applyTheme()
{
if (m_inApplyTheme)
return;
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
const Theme theme = themeFor(palette());
setStyleSheet(QStringLiteral(
"#AgentListItem { background:%1; border-top:1px solid %2; }")
.arg(m_selected ? theme.rowSelectedBg : QStringLiteral("transparent"),
theme.rowSeparator));
}
} // namespace QodeAssist::Settings

View File

@@ -1,45 +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 <QFrame>
#include <QList>
#include <QSet>
#include <QString>
#include <AgentConfig.hpp>
namespace QodeAssist::Settings {
class TagChip;
class AgentListItem : public QFrame
{
Q_OBJECT
public:
explicit AgentListItem(const AgentConfig &cfg, QWidget *parent = nullptr);
QString agentName() const { return m_name; }
void setSelected(bool selected);
void setActiveTags(const QSet<QString> &active);
signals:
void clicked(const QString &name);
void tagClicked(const QString &tag);
protected:
void mouseReleaseEvent(QMouseEvent *event) override;
void changeEvent(QEvent *event) override;
private:
void applyTheme();
QString m_name;
bool m_selected = false;
bool m_inApplyTheme = false;
QList<TagChip *> m_chips;
};
} // namespace QodeAssist::Settings

View File

@@ -1,237 +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 "AgentListPane.hpp"
#include "AgentListItem.hpp"
#include "SettingsTheme.hpp"
#include "SettingsUiBuilders.hpp"
#include "TagFilterStrip.hpp"
#include <Agent.hpp>
#include <AgentFactory.hpp>
#include <QEvent>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMap>
#include <QPalette>
#include <QScrollArea>
#include <QTimer>
#include <QVBoxLayout>
#include <algorithm>
namespace QodeAssist::Settings {
AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
: QFrame(parent)
, m_factory(factory)
{
setFrameShape(QFrame::StyledPanel);
m_filterEdit = new QLineEdit(this);
m_filterEdit->setPlaceholderText(tr("Filter agents…"));
m_filterEdit->setClearButtonEnabled(true);
auto *filterRow = new QHBoxLayout;
filterRow->setContentsMargins(6, 6, 6, 6);
filterRow->addWidget(m_filterEdit, 1);
m_filterHolder = new QWidget(this);
m_filterHolder->setObjectName(QStringLiteral("FilterHolder"));
m_filterHolder->setLayout(filterRow);
m_filterHolder->setAutoFillBackground(true);
m_tagStrip = new TagFilterStrip(this);
m_listScroll = new QScrollArea(this);
m_listScroll->setWidgetResizable(true);
m_listScroll->setFrameShape(QFrame::NoFrame);
m_listScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
auto *outer = new QVBoxLayout(this);
outer->setContentsMargins(0, 0, 0, 0);
outer->setSpacing(0);
outer->addWidget(m_filterHolder);
outer->addWidget(m_tagStrip);
outer->addWidget(m_listScroll, 1);
m_filterDebounce = new QTimer(this);
m_filterDebounce->setSingleShot(true);
m_filterDebounce->setInterval(100);
connect(m_filterDebounce, &QTimer::timeout, this, &AgentListPane::rebuildList);
connect(m_filterEdit, &QLineEdit::textChanged, this,
[this](const QString &) { m_filterDebounce->start(); });
connect(m_tagStrip, &TagFilterStrip::activeTagsChanged, this,
[this](const QSet<QString> &) { rebuildList(); },
Qt::QueuedConnection);
applyFilterHolderTheme();
}
void AgentListPane::selectByName(const QString &name)
{
if (name.isEmpty())
return;
setCurrentNameInternal(name, false);
rebuildList();
for (auto *item : m_rows) {
if (item->agentName() == name) {
QTimer::singleShot(0, this, [this, item] {
m_listScroll->ensureWidgetVisible(item, 0, 60);
});
break;
}
}
}
void AgentListPane::refresh()
{
QMap<QString, int> counts;
for (const auto *a : visibleAgents())
for (const QString &t : a->tags)
counts[t] += 1;
m_tagStrip->setAvailableTags(counts);
rebuildList();
}
void AgentListPane::changeEvent(QEvent *event)
{
QFrame::changeEvent(event);
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
applyFilterHolderTheme();
}
void AgentListPane::applyFilterHolderTheme()
{
if (!m_filterHolder)
return;
const Theme theme = themeFor(palette());
m_filterHolder->setStyleSheet(
QStringLiteral("QWidget#FilterHolder { background:%1;"
" border-bottom:1px solid %2; }")
.arg(theme.listHeaderBg, theme.rowSeparator));
}
std::vector<const AgentConfig *> AgentListPane::visibleAgents() const
{
std::vector<const AgentConfig *> out;
if (!m_factory)
return out;
for (const auto &a : m_factory->configs()) {
if (a.hidden)
continue;
out.push_back(&a);
}
return out;
}
bool AgentListPane::matchesFilters(const AgentConfig &a, const QString &lowerFilter) const
{
if (!lowerFilter.isEmpty()
&& !(a.name + QLatin1Char(' ') + a.model).toLower().contains(lowerFilter))
return false;
const QSet<QString> &active = m_tagStrip->activeTags();
for (const QString &t : active)
if (!a.tags.contains(t))
return false;
return true;
}
void AgentListPane::rebuildList()
{
const QString lowerFilter = m_filterEdit->text().trimmed().toLower();
std::vector<const AgentConfig *> userAgents;
std::vector<const AgentConfig *> bundledAgents;
for (const auto *a : visibleAgents()) {
if (!matchesFilters(*a, lowerFilter))
continue;
if (a->isUserSource())
userAgents.push_back(a);
else
bundledAgents.push_back(a);
}
auto byName = [](const AgentConfig *a, const AgentConfig *b) {
return a->name.localeAwareCompare(b->name) < 0;
};
std::sort(userAgents.begin(), userAgents.end(), byName);
std::sort(bundledAgents.begin(), bundledAgents.end(), byName);
QList<AgentListItem *> newRows;
auto *content = new QWidget;
content->setAutoFillBackground(true);
auto *contentLayout = new QVBoxLayout(content);
contentLayout->setContentsMargins(0, 0, 0, 0);
contentLayout->setSpacing(0);
const QSet<QString> &activeTags = m_tagStrip->activeTags();
auto addAgents = [&](const std::vector<const AgentConfig *> &agents) {
for (const AgentConfig *cfg : agents) {
auto *item = new AgentListItem(*cfg, content);
item->setSelected(cfg->name == m_currentName);
item->setActiveTags(activeTags);
connect(item, &AgentListItem::clicked, this, &AgentListPane::onRowClicked);
connect(item, &AgentListItem::tagClicked, this,
[this](const QString &) { refresh(); },
Qt::QueuedConnection);
contentLayout->addWidget(item);
newRows.append(item);
}
};
if (!userAgents.empty()) {
contentLayout->addWidget(makeSectionHeader(tr("User"), content));
addAgents(userAgents);
}
if (!bundledAgents.empty()) {
contentLayout->addWidget(makeSectionHeader(tr("Bundled"), content));
addAgents(bundledAgents);
}
if (newRows.isEmpty()) {
auto *empty = new QLabel(tr("No agents match these filters."), content);
empty->setAlignment(Qt::AlignCenter);
empty->setContentsMargins(10, 16, 10, 16);
QPalette ep = empty->palette();
ep.setColor(QPalette::WindowText, ep.color(QPalette::Mid));
empty->setPalette(ep);
contentLayout->addWidget(empty);
}
contentLayout->addStretch(1);
m_rows = newRows;
m_listScroll->setWidget(content);
const AgentConfig *current
= m_currentName.isEmpty() || !m_factory
? nullptr
: m_factory->configByName(m_currentName);
if (!current && !m_rows.isEmpty()) {
const QString fallback = m_rows.front()->agentName();
m_rows.front()->setSelected(true);
setCurrentNameInternal(fallback, /*emitSignal*/ true);
return;
}
emit currentAgentChanged(m_currentName);
}
void AgentListPane::onRowClicked(const QString &name)
{
setCurrentNameInternal(name, /*emitSignal*/ true);
}
void AgentListPane::setCurrentNameInternal(const QString &name, bool emitSignal)
{
if (name == m_currentName)
return;
m_currentName = name;
for (auto *item : m_rows)
item->setSelected(item->agentName() == name);
if (emitSignal)
emit currentAgentChanged(m_currentName);
}
} // namespace QodeAssist::Settings

View File

@@ -1,63 +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 <QFrame>
#include <QList>
#include <QSet>
#include <QString>
#include <vector>
#include <AgentConfig.hpp>
class QLineEdit;
class QScrollArea;
class QTimer;
class QVBoxLayout;
namespace QodeAssist {
class AgentFactory;
}
namespace QodeAssist::Settings {
class AgentListItem;
class TagFilterStrip;
class AgentListPane : public QFrame
{
Q_OBJECT
public:
explicit AgentListPane(AgentFactory *factory, QWidget *parent = nullptr);
QString currentName() const { return m_currentName; }
void selectByName(const QString &name);
void refresh();
signals:
void currentAgentChanged(const QString &name);
protected:
void changeEvent(QEvent *event) override;
private:
void rebuildList();
void applyFilterHolderTheme();
bool matchesFilters(const AgentConfig &a, const QString &lowerFilter) const;
std::vector<const AgentConfig *> visibleAgents() const;
void setCurrentNameInternal(const QString &name, bool emitSignal);
void onRowClicked(const QString &name);
AgentFactory *m_factory;
QLineEdit *m_filterEdit = nullptr;
QTimer *m_filterDebounce = nullptr;
QWidget *m_filterHolder = nullptr;
TagFilterStrip *m_tagStrip = nullptr;
QScrollArea *m_listScroll = nullptr;
QList<AgentListItem *> m_rows;
QString m_currentName;
};
} // namespace QodeAssist::Settings

View File

@@ -1,281 +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 "AgentsSettingsPage.hpp"
#include "AgentDetailPane.hpp"
#include "AgentDuplicator.hpp"
#include "AgentListPane.hpp"
#include "SettingsTheme.hpp"
#include "SettingsConstants.hpp"
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/editormanager/editormanager.h>
#include <utils/filepath.h>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFont>
#include <QFontMetrics>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPalette>
#include <QPointer>
#include <QPushButton>
#include <QScrollArea>
#include <QSplitter>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
#include <Agent.hpp>
#include <AgentFactory.hpp>
namespace QodeAssist::Settings {
AgentsPageNavigator::AgentsPageNavigator(QObject *parent)
: QObject(parent)
{}
void AgentsPageNavigator::requestSelectAgent(const QString &name)
{
m_pending = name;
emit selectAgentRequested(name);
}
QString AgentsPageNavigator::takePendingSelection()
{
QString p = m_pending;
m_pending.clear();
return p;
}
namespace {
class AgentsWidget : public Core::IOptionsPageWidget
{
Q_OBJECT
public:
explicit AgentsWidget(AgentFactory *agentFactory, AgentsPageNavigator *navigator)
: m_agentFactory(agentFactory)
, m_navigator(navigator)
{
Q_ASSERT(m_agentFactory);
m_titleLabel = new QLabel(tr("Agents"), this);
QFont tf = m_titleLabel->font();
tf.setBold(true);
tf.setPixelSize(13);
m_titleLabel->setFont(tf);
m_reload = new QPushButton(tr("Reload from disk"), this);
m_openUserDir = new QPushButton(tr("Open agents folder"), this);
m_userPathLabel = new QLabel(this);
m_userPathLabel->setFont(monospaceFont(11));
QPalette mutedPal = m_userPathLabel->palette();
mutedPal.setColor(QPalette::WindowText, mutedPal.color(QPalette::Mid));
m_userPathLabel->setPalette(mutedPal);
m_userPathLabel->setMaximumWidth(260);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(8);
headerRow->addWidget(m_titleLabel);
headerRow->addStretch(1);
headerRow->addWidget(m_reload);
headerRow->addWidget(m_userPathLabel);
headerRow->addWidget(m_openUserDir);
auto *headerSep = new QFrame(this);
headerSep->setFrameShape(QFrame::HLine);
headerSep->setFrameShadow(QFrame::Sunken);
m_listPane = new AgentListPane(m_agentFactory, this);
m_detail = new AgentDetailPane(this);
m_detail->setInstanceFactory(m_agentFactory->instanceFactory());
m_detailScroll = new QScrollArea(this);
m_detailScroll->setWidgetResizable(true);
m_detailScroll->setFrameShape(QFrame::StyledPanel);
m_detailScroll->setWidget(m_detail);
auto *splitter = new QSplitter(Qt::Horizontal, this);
splitter->addWidget(m_listPane);
splitter->addWidget(m_detailScroll);
splitter->setStretchFactor(0, 0);
splitter->setStretchFactor(1, 1);
splitter->setSizes({320, 700});
auto *root = new QVBoxLayout(this);
root->setContentsMargins(8, 8, 8, 8);
root->setSpacing(6);
root->addLayout(headerRow);
root->addWidget(headerSep);
root->addWidget(splitter, 1);
connect(m_reload, &QPushButton::clicked, this, &AgentsWidget::reloadFromDisk);
connect(m_openUserDir, &QPushButton::clicked, this, [] {
const QString dir = QodeAssist::AgentFactory::userAgentsDir();
QDir().mkpath(dir);
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
});
connect(m_listPane, &AgentListPane::currentAgentChanged, this,
[this](const QString &name) {
if (const AgentConfig *cfg = m_agentFactory->configByName(name))
m_detail->setAgent(*cfg);
else
m_detail->clear();
});
connect(m_detail, &AgentDetailPane::openInEditorRequested,
this, &AgentsWidget::openAgentInEditor);
connect(m_detail, &AgentDetailPane::customizeRequested,
this, &AgentsWidget::customizeAgent);
connect(m_detail, &AgentDetailPane::deleteRequested,
this, &AgentsWidget::deleteAgent);
if (m_navigator) {
connect(m_navigator, &AgentsPageNavigator::selectAgentRequested,
m_listPane, &AgentListPane::selectByName);
}
reloadFromDisk();
if (m_navigator) {
QTimer::singleShot(0, this, [this] {
if (!m_navigator)
return;
const QString pending = m_navigator->takePendingSelection();
if (!pending.isEmpty())
m_listPane->selectByName(pending);
});
}
}
void apply() final {}
private:
void reloadFromDisk()
{
m_agentFactory->reload();
m_detail->setLoadDiagnostics(
m_agentFactory->lastLoadErrors(), m_agentFactory->lastLoadWarnings());
updateUserPathLabel();
m_listPane->refresh();
}
void updateUserPathLabel()
{
const QString dir = QodeAssist::AgentFactory::userAgentsDir();
m_userPathLabel->setText(
QFontMetrics(m_userPathLabel->font()).elidedText(dir, Qt::ElideLeft, 256));
m_userPathLabel->setToolTip(dir);
}
void openAgentInEditor(const AgentConfig &agent)
{
const QString name = agent.name;
const QString sourcePath = agent.sourcePath;
const bool isUser = agent.isUserSource();
if (!isUser) {
QMessageBox::information(
this, tr("Open agent"),
tr("'%1' is bundled with the plugin and read-only.\n"
"Use Duplicate to create an editable user copy.")
.arg(name));
return;
}
if (sourcePath.isEmpty() || sourcePath.startsWith(QLatin1String(":/"))) {
QMessageBox::warning(
this, tr("Open agent"),
tr("Agent '%1' has no editable source file.").arg(name));
return;
}
if (!Core::EditorManager::openEditor(Utils::FilePath::fromString(sourcePath))) {
QMessageBox::warning(
this, tr("Open agent"),
tr("Could not open %1.").arg(sourcePath));
}
}
void customizeAgent(const AgentConfig &parent)
{
const AgentDuplicateResult res = duplicateAgentInUserDir(parent, *m_agentFactory);
if (!res.ok) {
QMessageBox::warning(this, tr("Duplicate"), res.error);
return;
}
const QString newName = res.newName;
reloadFromDisk();
m_listPane->selectByName(newName);
}
void deleteAgent(const AgentConfig &agent)
{
if (!agent.isUserSource())
return;
const QString name = agent.name;
const QString sourcePath = agent.sourcePath;
if (QMessageBox::question(
this, tr("Delete Agent"),
tr("Delete agent '%1'?\n\nThis will remove the file:\n%2")
.arg(name, sourcePath),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No)
!= QMessageBox::Yes)
return;
if (!QFile::remove(sourcePath)) {
QMessageBox::warning(
this, tr("Delete Agent"),
tr("Could not delete the agent file:\n%1").arg(sourcePath));
return;
}
reloadFromDisk();
}
AgentFactory *m_agentFactory;
QPointer<AgentsPageNavigator> m_navigator;
QLabel *m_titleLabel = nullptr;
QPushButton *m_reload = nullptr;
QPushButton *m_openUserDir = nullptr;
QLabel *m_userPathLabel = nullptr;
AgentListPane *m_listPane = nullptr;
QScrollArea *m_detailScroll = nullptr;
AgentDetailPane *m_detail = nullptr;
};
class AgentsSettingsPage : public Core::IOptionsPage
{
public:
AgentsSettingsPage(AgentFactory *agentFactory, AgentsPageNavigator *navigator)
{
setId(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
setDisplayName(QObject::tr("Agents"));
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
setWidgetCreator([agentFactory, navigator]() {
return new AgentsWidget(agentFactory, navigator);
});
}
};
} // namespace
std::unique_ptr<Core::IOptionsPage> createAgentsSettingsPage(
AgentFactory *agentFactory, AgentsPageNavigator *navigator)
{
return std::make_unique<AgentsSettingsPage>(agentFactory, navigator);
}
} // namespace QodeAssist::Settings
#include "AgentsSettingsPage.moc"

View File

@@ -1,39 +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 : public QObject
{
Q_OBJECT
public:
explicit AgentsPageNavigator(QObject *parent = nullptr);
void requestSelectAgent(const QString &name);
QString takePendingSelection();
signals:
void selectAgentRequested(const QString &name);
private:
QString m_pending;
};
std::unique_ptr<Core::IOptionsPage> createAgentsSettingsPage(
AgentFactory *agentFactory, AgentsPageNavigator *navigator);
} // namespace QodeAssist::Settings

View File

@@ -16,24 +16,11 @@ add_library(QodeAssistSettings STATIC
ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp
ProviderSettings.hpp ProviderSettings.cpp
ProviderNameMigration.hpp
ProvidersSettingsPage.hpp ProvidersSettingsPage.cpp
SettingsTheme.hpp
SettingsUiBuilders.hpp SettingsUiBuilders.cpp
SectionBox.hpp SectionBox.cpp
TagChip.hpp TagChip.cpp
ProviderListItem.hpp ProviderListItem.cpp
ProviderDetailPane.hpp ProviderDetailPane.cpp
PluginUpdater.hpp PluginUpdater.cpp
UpdateDialog.hpp UpdateDialog.cpp
AgentRole.hpp AgentRole.cpp
AgentRoleDialog.hpp AgentRoleDialog.cpp
AgentRolesWidget.hpp AgentRolesWidget.cpp
AgentsSettingsPage.hpp AgentsSettingsPage.cpp
AgentDetailPane.hpp AgentDetailPane.cpp
AgentListItem.hpp AgentListItem.cpp
AgentListPane.hpp AgentListPane.cpp
AgentDuplicator.hpp AgentDuplicator.cpp
TagFilterStrip.hpp TagFilterStrip.cpp
)
target_link_libraries(QodeAssistSettings
@@ -44,8 +31,6 @@ target_link_libraries(QodeAssistSettings
QtCreator::Core
QtCreator::Utils
QodeAssistLogger
ProvidersConfig
Agents
Skills
)
target_include_directories(QodeAssistSettings PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -1,515 +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 "ProviderDetailPane.hpp"
#include <array>
#include <QFrame>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QToolButton>
#include <QVBoxLayout>
#include <solutions/terminal/terminalview.h>
#include "ProviderInstanceWriter.hpp"
#include "SectionBox.hpp"
#include "SettingsTheme.hpp"
#include "SettingsUiBuilders.hpp"
namespace QodeAssist::Settings {
ProviderDetailPane::ProviderDetailPane(QWidget *parent)
: QWidget(parent)
{
m_nameLabel = new QLabel(this);
QFont nf = m_nameLabel->font();
nf.setBold(true);
nf.setPixelSize(15);
m_nameLabel->setFont(nf);
m_sourcePathLabel = new QLabel(this);
m_sourcePathLabel->setFont(monospaceFont(11));
QPalette spp = m_sourcePathLabel->palette();
spp.setColor(QPalette::WindowText, spp.color(QPalette::Mid));
m_sourcePathLabel->setPalette(spp);
m_editBtn = new QPushButton(tr("Edit…"), this);
m_editBtn->setDefault(true);
m_openInEditorBtn = new QPushButton(tr("Open in editor"), this);
m_openInEditorBtn->setToolTip(
tr("Open this provider's TOML file in Qt Creator. "
"Bundled providers are read-only — duplicate first."));
m_dupBtn = new QPushButton(tr("Duplicate…"), this);
m_deleteBtn = new QPushButton(tr("Delete"), this);
m_cancelBtn = new QPushButton(tr("Cancel"), this);
m_saveBtn = new QPushButton(tr("Save"), this);
m_saveBtn->setDefault(true);
m_cancelBtn->hide();
m_saveBtn->hide();
connect(m_editBtn, &QPushButton::clicked, this, [this] { setEditing(true); });
connect(m_cancelBtn, &QPushButton::clicked, this, [this] {
setEditing(false);
populate(m_current, m_currentHasStoredKey);
});
connect(m_saveBtn, &QPushButton::clicked, this, [this] {
emit saveRequested(collectEdits());
});
connect(m_openInEditorBtn, &QPushButton::clicked, this,
[this] { emit openInEditorRequested(m_current.sourcePath); });
connect(m_dupBtn, &QPushButton::clicked, this, [this] { emit duplicateRequested(); });
connect(m_deleteBtn, &QPushButton::clicked, this, [this] { emit deleteRequested(); });
auto *btnBar = new QHBoxLayout;
btnBar->setContentsMargins(0, 0, 0, 0);
btnBar->setSpacing(4);
btnBar->addWidget(m_editBtn);
btnBar->addWidget(m_openInEditorBtn);
btnBar->addWidget(m_dupBtn);
btnBar->addWidget(m_deleteBtn);
btnBar->addWidget(m_cancelBtn);
btnBar->addWidget(m_saveBtn);
auto *titleRow = new QHBoxLayout;
titleRow->setContentsMargins(0, 0, 0, 0);
titleRow->setSpacing(8);
titleRow->addWidget(m_nameLabel);
titleRow->addStretch(1);
auto *headerLeft = new QVBoxLayout;
headerLeft->setContentsMargins(0, 0, 0, 0);
headerLeft->setSpacing(2);
headerLeft->addLayout(titleRow);
headerLeft->addWidget(m_sourcePathLabel);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(8);
headerRow->addLayout(headerLeft, 1);
headerRow->addLayout(btnBar);
auto *headerSep = new QFrame(this);
headerSep->setFrameShape(QFrame::HLine);
headerSep->setFrameShadow(QFrame::Sunken);
m_descriptionLabel = new QLabel(this);
m_descriptionLabel->setWordWrap(true);
m_descriptionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *identitySection = new SectionBox(tr("Identity"), this);
m_nameEdit = new QLineEdit(this);
m_typeEdit = new QLineEdit(this);
m_typeEdit->setReadOnly(true);
m_descriptionEdit = new QPlainTextEdit(this);
m_descriptionEdit->setMaximumHeight(60);
m_descriptionEdit->setReadOnly(true);
auto *identityGrid = new QGridLayout;
identityGrid->setContentsMargins(0, 0, 0, 0);
identityGrid->setHorizontalSpacing(8);
identityGrid->setVerticalSpacing(4);
FormBuilder(identityGrid)
.row(tr("Name:"), m_nameEdit)
.row(tr("Client API:"), m_typeEdit,
tr("The client API this provider speaks. "
"Cannot be changed after creation."))
.row(tr("Description:"), m_descriptionEdit);
identitySection->bodyLayout()->addLayout(identityGrid);
auto *endpointSection = new SectionBox(tr("Endpoint"), this);
m_urlEdit = new QLineEdit(this);
m_urlEdit->setFont(monospaceFont(11));
auto *endpointGrid = new QGridLayout;
endpointGrid->setContentsMargins(0, 0, 0, 0);
endpointGrid->setHorizontalSpacing(8);
endpointGrid->setVerticalSpacing(4);
FormBuilder(endpointGrid).row(tr("URL:"), m_urlEdit,
tr("Base URL. Agents append their endpoint path "
"(e.g. /chat/completions) to this."));
endpointSection->bodyLayout()->addLayout(endpointGrid);
m_samplePreview = new QLabel(this);
m_samplePreview->setFont(monospaceFont(11));
m_samplePreview->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_samplePreview->setWordWrap(true);
m_samplePreview->setContentsMargins(6, 4, 6, 4);
m_samplePreview->setAutoFillBackground(true);
endpointSection->bodyLayout()->addWidget(m_samplePreview);
auto *credSection = new SectionBox(tr("Credentials"), this);
m_apiKeyEdit = new QLineEdit(this);
m_apiKeyEdit->setEchoMode(QLineEdit::Password);
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
m_revealKeyBtn = new QToolButton(this);
m_revealKeyBtn->setText(QStringLiteral("👁"));
m_revealKeyBtn->setCheckable(true);
m_revealKeyBtn->setToolTip(tr("Show / hide API key"));
connect(m_revealKeyBtn, &QToolButton::toggled, this, [this](bool on) {
m_apiKeyEdit->setEchoMode(on ? QLineEdit::Normal : QLineEdit::Password);
});
m_apiKeySaveBtn = new QPushButton(tr("Save key"), this);
m_apiKeySaveBtn->setEnabled(false);
m_apiKeyClearBtn = new QPushButton(tr("Clear"), this);
m_apiKeyClearBtn->setToolTip(tr("Erase the stored API key for this provider"));
connect(m_apiKeyEdit, &QLineEdit::textChanged, this, [this](const QString &t) {
m_apiKeySaveBtn->setEnabled(!t.isEmpty());
});
connect(m_apiKeyEdit, &QLineEdit::returnPressed, this, [this] {
if (!m_apiKeyEdit->text().isEmpty())
m_apiKeySaveBtn->click();
});
connect(m_apiKeySaveBtn, &QPushButton::clicked, this, [this] {
const QString key = m_apiKeyEdit->text();
if (key.isEmpty())
return;
emit apiKeySaveRequested(key);
m_apiKeyEdit->clear();
});
connect(m_apiKeyClearBtn, &QPushButton::clicked, this,
[this] { emit apiKeyClearRequested(); });
m_keyHint = makeHintLabel(QString{}, this);
auto *keyRow = new QHBoxLayout;
keyRow->setContentsMargins(0, 0, 0, 0);
keyRow->setSpacing(4);
keyRow->addWidget(m_apiKeyEdit, 1);
keyRow->addWidget(m_revealKeyBtn);
keyRow->addWidget(m_apiKeySaveBtn);
keyRow->addWidget(m_apiKeyClearBtn);
auto *credGrid = new QGridLayout;
credGrid->setContentsMargins(0, 0, 0, 0);
credGrid->setHorizontalSpacing(8);
credGrid->setVerticalSpacing(4);
FormBuilder credForm(credGrid);
credForm.row(tr("API key:"), keyRow);
credGrid->addWidget(m_keyHint, credForm.currentRow(), 1);
credSection->bodyLayout()->addLayout(credGrid);
m_launchSection = new SectionBox(tr("Launch"), this);
m_launchEmptyHint = new QLabel(this);
m_launchEmptyHint->setWordWrap(true);
QPalette lehp = m_launchEmptyHint->palette();
lehp.setColor(QPalette::WindowText, lehp.color(QPalette::Mid));
m_launchEmptyHint->setPalette(lehp);
m_launchCmdLabel = new QLabel(this);
m_launchCmdLabel->setFont(monospaceFont(11));
m_launchCmdLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_launchCmdLabel->setWordWrap(true);
m_launchStatusPill = new QLabel(tr("idle"), this);
m_startBtn = new QPushButton(tr("Start"), this);
m_stopBtn = new QPushButton(tr("Stop"), this);
m_restartBtn = new QPushButton(tr("Restart"), this);
connect(m_startBtn, &QPushButton::clicked, this,
[this] { emit launchStartRequested(m_current.name); });
connect(m_stopBtn, &QPushButton::clicked, this,
[this] { emit launchStopRequested(m_current.name); });
connect(m_restartBtn, &QPushButton::clicked, this,
[this] { emit launchRestartRequested(m_current.name); });
auto *launchBtnRow = new QHBoxLayout;
launchBtnRow->setContentsMargins(0, 0, 0, 0);
launchBtnRow->setSpacing(6);
launchBtnRow->addWidget(m_launchStatusPill);
launchBtnRow->addStretch(1);
launchBtnRow->addWidget(m_startBtn);
launchBtnRow->addWidget(m_stopBtn);
launchBtnRow->addWidget(m_restartBtn);
m_launchTerminalToggle = new QToolButton(this);
m_launchTerminalToggle->setText(tr("▸ Show launch terminal"));
m_launchTerminalToggle->setCursor(Qt::PointingHandCursor);
m_launchTerminalToggle->setAutoRaise(true);
m_launchTerminalToggle->setCheckable(true);
m_launchTerminal = new TerminalSolution::TerminalView(this);
{
QFont termFont(TerminalSolution::defaultFontFamily());
const int sz = TerminalSolution::defaultFontSize();
if (sz > 0)
termFont.setPixelSize(sz);
termFont.setStyleHint(QFont::Monospace);
m_launchTerminal->setFont(termFont);
applyTerminalPalette();
}
m_launchTerminal->setMinimumHeight(180);
m_launchTerminal->setVisible(false);
connect(m_launchTerminalToggle, &QToolButton::toggled, this, [this](bool on) {
m_launchTerminal->setVisible(on);
m_launchTerminalToggle->setText(
on ? tr("▾ Hide launch terminal") : tr("▸ Show launch terminal"));
});
m_launchSection->bodyLayout()->addWidget(m_launchEmptyHint);
m_launchSection->bodyLayout()->addWidget(m_launchCmdLabel);
m_launchSection->bodyLayout()->addLayout(launchBtnRow);
m_launchSection->bodyLayout()->addWidget(m_launchTerminalToggle, 0, Qt::AlignLeft);
m_launchSection->bodyLayout()->addWidget(m_launchTerminal);
m_rawToggle = new QToolButton(this);
m_rawToggle->setText(tr("▸ Show raw TOML"));
m_rawToggle->setCursor(Qt::PointingHandCursor);
m_rawToggle->setAutoRaise(true);
m_rawToggle->setCheckable(true);
m_rawToml = new QPlainTextEdit(this);
m_rawToml->setReadOnly(true);
m_rawToml->setFont(monospaceFont(11));
m_rawToml->setMinimumHeight(120);
m_rawToml->setVisible(false);
connect(m_rawToggle, &QToolButton::toggled, this, [this](bool on) {
m_rawToml->setVisible(on);
m_rawToggle->setText(on ? tr("▾ Hide raw TOML") : tr("▸ Show raw TOML"));
});
auto *root = new QVBoxLayout(this);
root->setContentsMargins(12, 12, 12, 12);
root->setSpacing(10);
root->addLayout(headerRow);
root->addWidget(headerSep);
root->addWidget(m_descriptionLabel);
root->addWidget(identitySection);
root->addWidget(endpointSection);
root->addWidget(credSection);
root->addWidget(m_launchSection);
root->addWidget(m_rawToggle, 0, Qt::AlignLeft);
root->addWidget(m_rawToml);
root->addStretch(1);
clear();
}
void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool hasStoredKey)
{
m_current = inst;
m_currentHasStoredKey = hasStoredKey;
const bool isUser = inst.isUserSource();
const bool needsKey = !inst.apiKeyRef.isEmpty();
m_nameLabel->setText(inst.name);
m_sourcePathLabel->setText(inst.sourcePath);
m_descriptionLabel->setText(
inst.description.isEmpty() ? tr("No description provided.") : inst.description);
m_nameEdit->setText(inst.name);
m_typeEdit->setText(inst.clientApi);
m_descriptionEdit->setPlainText(inst.description);
m_urlEdit->setText(inst.url);
m_apiKeyEdit->clear();
m_apiKeyEdit->setEnabled(needsKey);
m_apiKeySaveBtn->setEnabled(false);
m_apiKeyClearBtn->setEnabled(needsKey && hasStoredKey);
m_revealKeyBtn->setEnabled(needsKey);
m_revealKeyBtn->setChecked(false);
if (!needsKey) {
m_apiKeyEdit->setPlaceholderText(tr("— not required (local provider)"));
m_keyHint->setText(tr("This provider type does not use a key."));
} else if (hasStoredKey) {
m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it."));
m_keyHint->setText(tr("A key is stored. Type a new key and press Save key to "
"replace it, or Clear to erase it."));
} else {
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
m_keyHint->setText(tr("No key stored yet. Type a key and press Save key."));
}
m_samplePreview->setText(
QStringLiteral("# sample request line\nPOST %1/<agent endpoint>").arg(inst.url));
applyPreviewPalette();
m_deleteBtn->setEnabled(isUser);
m_dupBtn->setEnabled(true);
m_editBtn->setVisible(isUser);
m_openInEditorBtn->setEnabled(isUser);
setEditing(false);
QString toml = QStringLiteral("# %1\n").arg(inst.sourcePath);
toml += Providers::ProviderInstanceWriter::toToml(inst);
m_rawToml->setPlainText(toml);
}
void ProviderDetailPane::clear()
{
m_current = {};
m_nameLabel->setText(tr("Select a provider"));
m_sourcePathLabel->clear();
m_descriptionLabel->clear();
m_nameEdit->clear();
m_typeEdit->clear();
m_descriptionEdit->clear();
m_urlEdit->clear();
m_apiKeyEdit->clear();
m_apiKeyEdit->setEnabled(false);
m_apiKeySaveBtn->setEnabled(false);
m_apiKeyClearBtn->setEnabled(false);
m_revealKeyBtn->setEnabled(false);
m_samplePreview->clear();
m_rawToml->clear();
m_editBtn->setVisible(false);
m_dupBtn->setEnabled(false);
m_deleteBtn->setEnabled(false);
m_openInEditorBtn->setEnabled(false);
}
void ProviderDetailPane::refreshKeyStatus(bool hasStoredKey)
{
m_currentHasStoredKey = hasStoredKey;
const bool needsKey = !m_current.apiKeyRef.isEmpty();
m_apiKeyClearBtn->setEnabled(needsKey && hasStoredKey);
if (!needsKey)
return;
if (hasStoredKey) {
m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it."));
m_keyHint->setText(tr("A key is stored. Type a new key and press Save key to "
"replace it, or Clear to erase it."));
} else {
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
m_keyHint->setText(tr("No key stored yet. Type a key and press Save key."));
}
}
void ProviderDetailPane::setLaunchState(
Providers::ProviderLauncher::State st, const QString &lastError)
{
const bool hasLaunch = !m_current.launch.isEmpty();
m_launchSection->setVisible(true);
m_launchEmptyHint->setVisible(!hasLaunch);
m_launchCmdLabel->setVisible(hasLaunch);
m_startBtn->setVisible(hasLaunch);
m_stopBtn->setVisible(hasLaunch);
m_restartBtn->setVisible(hasLaunch);
m_launchStatusPill->setVisible(hasLaunch);
m_launchTerminalToggle->setVisible(hasLaunch);
if (!hasLaunch) {
m_launchEmptyHint->setText(tr(
"No [launch] block. This provider is treated as external — "
"the plugin will not spawn or supervise any process. "
"Add a [launch] block to the TOML to have the plugin manage "
"a local server here."));
m_launchCmdLabel->clear();
m_launchTerminal->clearContents();
return;
}
const QString detachedNote = m_current.launch.detach
? tr(" <span style='color:gray'>(detached — survives Qt Creator restart)</span>")
: QString();
m_launchCmdLabel->setText(
QStringLiteral("<b>%1</b> %2%3")
.arg(m_current.launch.command.toHtmlEscaped(),
m_current.launch.args.join(QLatin1Char(' ')).toHtmlEscaped(),
detachedNote));
QString statusText;
switch (st) {
case Providers::ProviderLauncher::Idle: statusText = tr("idle"); break;
case Providers::ProviderLauncher::Starting: statusText = tr("starting…"); break;
case Providers::ProviderLauncher::Probing: statusText = tr("probing…"); break;
case Providers::ProviderLauncher::Ready: statusText = tr("ready"); break;
case Providers::ProviderLauncher::Stopping: statusText = tr("stopping…"); break;
case Providers::ProviderLauncher::Failed:
statusText = lastError.isEmpty() ? tr("failed")
: tr("failed — %1").arg(lastError);
break;
}
m_launchStatusPill->setText(statusText);
const bool running = st == Providers::ProviderLauncher::Starting
|| st == Providers::ProviderLauncher::Probing
|| st == Providers::ProviderLauncher::Ready;
m_startBtn->setEnabled(!running && st != Providers::ProviderLauncher::Stopping);
m_stopBtn->setEnabled(running);
m_restartBtn->setEnabled(running || st == Providers::ProviderLauncher::Failed);
}
void ProviderDetailPane::resetLaunchTerminal(const QByteArray &scrollback)
{
m_launchTerminal->clearContents();
if (!scrollback.isEmpty())
m_launchTerminal->writeToTerminal(scrollback, true);
}
void ProviderDetailPane::appendLaunchBytes(const QByteArray &chunk)
{
m_launchTerminal->writeToTerminal(chunk, true);
}
void ProviderDetailPane::changeEvent(QEvent *event)
{
QWidget::changeEvent(event);
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) {
applyPreviewPalette();
applyTerminalPalette();
}
}
void ProviderDetailPane::setEditing(bool on)
{
m_editing = on;
m_nameEdit->setReadOnly(!on);
m_descriptionEdit->setReadOnly(!on);
m_urlEdit->setReadOnly(!on);
m_editBtn->setVisible(!on && m_current.isUserSource());
m_dupBtn->setVisible(!on);
m_deleteBtn->setVisible(!on);
m_cancelBtn->setVisible(on);
m_saveBtn->setVisible(on);
}
Providers::ProviderInstance ProviderDetailPane::collectEdits() const
{
Providers::ProviderInstance out = m_current;
out.name = m_nameEdit->text().trimmed();
out.description = m_descriptionEdit->toPlainText().trimmed();
out.url = m_urlEdit->text().trimmed();
return out;
}
void ProviderDetailPane::applyPreviewPalette()
{
const Theme theme = themeFor(palette());
m_samplePreview->setStyleSheet(QStringLiteral(
"QLabel { background:%1; border:1px solid %2; }")
.arg(theme.codeBg, theme.rowSeparator));
}
void ProviderDetailPane::applyTerminalPalette()
{
if (!m_launchTerminal)
return;
const QPalette pal = palette();
const bool dark = isDarkPalette(pal);
const std::array<QColor, 16> ansi = dark
? std::array<QColor, 16>{
QColor("#000000"), QColor("#cd3131"), QColor("#0dbc79"),
QColor("#e5e510"), QColor("#2472c8"), QColor("#bc3fbc"),
QColor("#11a8cd"), QColor("#e5e5e5"),
QColor("#666666"), QColor("#f14c4c"), QColor("#23d18b"),
QColor("#f5f543"), QColor("#3b8eea"), QColor("#d670d6"),
QColor("#29b8db"), QColor("#ffffff"),
}
: std::array<QColor, 16>{
QColor("#000000"), QColor("#c91b00"), QColor("#00c200"),
QColor("#c7c400"), QColor("#0037da"), QColor("#c930c7"),
QColor("#00c5c7"), QColor("#c7c7c7"),
QColor("#676767"), QColor("#ff6d67"), QColor("#5ff967"),
QColor("#fefb67"), QColor("#6871ff"), QColor("#ff76ff"),
QColor("#5ffdff"), QColor("#ffffff"),
};
std::array<QColor, 20> colors{};
for (int i = 0; i < 16; ++i)
colors[i] = ansi[i];
colors[16] = pal.color(QPalette::Text);
colors[17] = pal.color(QPalette::Base);
colors[18] = pal.color(QPalette::Highlight);
colors[19] = dark ? QColor("#5a5a40") : QColor("#fff59d");
m_launchTerminal->setColors(colors);
}
} // namespace QodeAssist::Settings

View File

@@ -1,102 +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 <QWidget>
#include "ProviderInstance.hpp"
#include "ProviderLauncher.hpp"
class QLabel;
class QLineEdit;
class QPlainTextEdit;
class QPushButton;
class QToolButton;
namespace TerminalSolution {
class TerminalView;
}
namespace QodeAssist::Settings {
class SectionBox;
class ProviderDetailPane : public QWidget
{
Q_OBJECT
public:
explicit ProviderDetailPane(QWidget *parent = nullptr);
void populate(const Providers::ProviderInstance &inst, bool hasStoredKey);
void clear();
void refreshKeyStatus(bool hasStoredKey);
void setLaunchState(Providers::ProviderLauncher::State st, const QString &lastError);
void resetLaunchTerminal(const QByteArray &scrollback);
void appendLaunchBytes(const QByteArray &chunk);
QString currentName() const { return m_current.name; }
signals:
void saveRequested(const Providers::ProviderInstance &edited);
void duplicateRequested();
void deleteRequested();
void apiKeySaveRequested(const QString &newKey);
void apiKeyClearRequested();
void launchStartRequested(const QString &providerName);
void launchStopRequested(const QString &providerName);
void launchRestartRequested(const QString &providerName);
void openInEditorRequested(const QString &sourcePath);
protected:
void changeEvent(QEvent *event) override;
private:
void setEditing(bool on);
Providers::ProviderInstance collectEdits() const;
void applyPreviewPalette();
void applyTerminalPalette();
bool m_editing = false;
bool m_currentHasStoredKey = false;
Providers::ProviderInstance m_current;
QLabel *m_nameLabel = nullptr;
QLabel *m_sourcePathLabel = nullptr;
QPushButton *m_editBtn = nullptr;
QPushButton *m_openInEditorBtn = nullptr;
QPushButton *m_dupBtn = nullptr;
QPushButton *m_deleteBtn = nullptr;
QPushButton *m_cancelBtn = nullptr;
QPushButton *m_saveBtn = nullptr;
QLabel *m_descriptionLabel = nullptr;
QLineEdit *m_nameEdit = nullptr;
QLineEdit *m_typeEdit = nullptr;
QPlainTextEdit *m_descriptionEdit = nullptr;
QLineEdit *m_urlEdit = nullptr;
QLabel *m_samplePreview = nullptr;
QLineEdit *m_apiKeyEdit = nullptr;
QToolButton *m_revealKeyBtn = nullptr;
QLabel *m_keyHint = nullptr;
QPushButton *m_apiKeySaveBtn = nullptr;
QPushButton *m_apiKeyClearBtn = nullptr;
SectionBox *m_launchSection = nullptr;
QLabel *m_launchEmptyHint = nullptr;
QLabel *m_launchCmdLabel = nullptr;
QLabel *m_launchStatusPill = nullptr;
QPushButton *m_startBtn = nullptr;
QPushButton *m_stopBtn = nullptr;
QPushButton *m_restartBtn = nullptr;
QToolButton *m_launchTerminalToggle = nullptr;
TerminalSolution::TerminalView *m_launchTerminal = nullptr;
QToolButton *m_rawToggle = nullptr;
QPlainTextEdit *m_rawToml = nullptr;
};
} // namespace QodeAssist::Settings

View File

@@ -1,110 +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 "ProviderListItem.hpp"
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QScopedValueRollback>
#include <QVBoxLayout>
#include "ProviderInstance.hpp"
#include "SettingsTheme.hpp"
namespace QodeAssist::Settings {
ProviderListItem::ProviderListItem(
const Providers::ProviderInstance &inst, QWidget *parent)
: QFrame(parent)
, m_name(inst.name)
{
setObjectName(QStringLiteral("ProvListItem"));
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
setCursor(Qt::PointingHandCursor);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(6);
m_statusDot = new QLabel(QStringLiteral(""), this);
QFont df = m_statusDot->font();
df.setPixelSize(11);
m_statusDot->setFont(df);
m_statusDot->setStyleSheet(QStringLiteral("color: %1;").arg(statusColor(Status::Unknown)));
m_nameLabel = new QLabel(inst.name, this);
QFont nf = m_nameLabel->font();
nf.setBold(true);
nf.setPixelSize(12);
m_nameLabel->setFont(nf);
headerRow->addWidget(m_statusDot, 0, Qt::AlignVCenter);
headerRow->addWidget(m_nameLabel, 1);
m_urlLabel = new QLabel(inst.url, this);
m_urlLabel->setFont(monospaceFont(10));
QPalette up = m_urlLabel->palette();
up.setColor(QPalette::WindowText, up.color(QPalette::Mid));
m_urlLabel->setPalette(up);
m_urlLabel->setContentsMargins(17, 0, 0, 0);
auto *outer = new QVBoxLayout(this);
outer->setContentsMargins(8, 6, 8, 6);
outer->setSpacing(2);
outer->addLayout(headerRow);
outer->addWidget(m_urlLabel);
applyTheme();
}
void ProviderListItem::setStatus(Status s)
{
m_status = s;
m_statusDot->setStyleSheet(QStringLiteral("color: %1;").arg(statusColor(s)));
}
void ProviderListItem::setSelected(bool s)
{
if (m_selected == s)
return;
m_selected = s;
applyTheme();
}
void ProviderListItem::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
emit clicked(m_name);
QFrame::mouseReleaseEvent(event);
}
void ProviderListItem::changeEvent(QEvent *event)
{
QFrame::changeEvent(event);
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
applyTheme();
}
QString ProviderListItem::statusColor(Status s)
{
switch (s) {
case Status::Ok: return QStringLiteral("#3a8a4f");
case Status::Fail: return QStringLiteral("#c94a4a");
case Status::Unknown: return QStringLiteral("#888888");
}
return QStringLiteral("#888888");
}
void ProviderListItem::applyTheme()
{
if (m_inApplyTheme)
return;
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
const Theme theme = themeFor(palette());
setStyleSheet(QStringLiteral(
"#ProvListItem { background:%1; border-top: 1px solid %2; }")
.arg(m_selected ? theme.rowSelectedBg : QStringLiteral("transparent"),
theme.rowSeparator));
}
} // namespace QodeAssist::Settings

View File

@@ -1,51 +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 <QFrame>
class QLabel;
class QMouseEvent;
namespace QodeAssist::Providers {
struct ProviderInstance;
}
namespace QodeAssist::Settings {
class ProviderListItem : public QFrame
{
Q_OBJECT
public:
enum class Status : int { Unknown, Ok, Fail };
explicit ProviderListItem(
const Providers::ProviderInstance &inst, QWidget *parent = nullptr);
void setStatus(Status s);
void setSelected(bool s);
QString providerName() const { return m_name; }
signals:
void clicked(const QString &name);
protected:
void mouseReleaseEvent(QMouseEvent *event) override;
void changeEvent(QEvent *event) override;
private:
static QString statusColor(Status s);
void applyTheme();
QString m_name;
Status m_status = Status::Unknown;
bool m_selected = false;
bool m_inApplyTheme = false;
QLabel *m_statusDot = nullptr;
QLabel *m_nameLabel = nullptr;
QLabel *m_urlLabel = nullptr;
};
} // namespace QodeAssist::Settings

View File

@@ -256,8 +256,6 @@ public:
}
};
#ifndef QODEASSIST_EXPERIMENTAL
const ProviderSettingsPage providerSettingsPage;
#endif
} // namespace QodeAssist::Settings

View File

@@ -1,618 +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 "ProvidersSettingsPage.hpp"
#include <algorithm>
#include <vector>
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <utils/filepath.h>
#include <QDialog>
#include <QFile>
#include <QFileInfo>
#include <QFrame>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QScrollArea>
#include <QSplitter>
#include <QTimer>
#include <QVBoxLayout>
#include "ProviderDetailPane.hpp"
#include "ProviderInstance.hpp"
#include "ProviderInstanceFactory.hpp"
#include "ProviderInstanceWriter.hpp"
#include "ProviderLauncher.hpp"
#include "ProviderListItem.hpp"
#include "ProviderSecretsStore.hpp"
#include "SettingsConstants.hpp"
#include "SettingsTheme.hpp"
namespace QodeAssist::Settings {
ProvidersPageNavigator::ProvidersPageNavigator(QObject *parent)
: QObject(parent)
{}
void ProvidersPageNavigator::requestSelectInstance(const QString &name)
{
m_pending = name;
emit selectInstanceRequested(name);
}
QString ProvidersPageNavigator::takePendingSelection()
{
QString p = m_pending;
m_pending.clear();
return p;
}
namespace {
class ProvidersPageWidget : public Core::IOptionsPageWidget
{
Q_OBJECT
public:
ProvidersPageWidget(
Providers::ProviderInstanceFactory *factory,
Providers::ProviderSecretsStore *secrets,
Providers::ProviderLauncher *launcher,
ProvidersPageNavigator *navigator)
: m_factory(factory)
, m_secrets(secrets)
, m_launcher(launcher)
, m_navigator(navigator)
{
m_titleLabel = new QLabel(tr("Providers"), this);
QFont tf = m_titleLabel->font();
tf.setBold(true);
tf.setPixelSize(13);
m_titleLabel->setFont(tf);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(8);
headerRow->addWidget(m_titleLabel, 1);
auto *headerSep = new QFrame(this);
headerSep->setFrameShape(QFrame::HLine);
headerSep->setFrameShadow(QFrame::Sunken);
m_filterEdit = new QLineEdit(this);
m_filterEdit->setPlaceholderText(tr("Filter providers…"));
m_listScroll = new QScrollArea(this);
m_listScroll->setWidgetResizable(true);
m_listScroll->setFrameShape(QFrame::NoFrame);
m_listContent = new QWidget(this);
m_listLayout = new QVBoxLayout(m_listContent);
m_listLayout->setContentsMargins(0, 0, 0, 0);
m_listLayout->setSpacing(0);
m_listLayout->addStretch(1);
m_listScroll->setWidget(m_listContent);
auto *leftBox = new QFrame(this);
leftBox->setFrameShape(QFrame::StyledPanel);
auto *leftLay = new QVBoxLayout(leftBox);
leftLay->setContentsMargins(0, 0, 0, 0);
leftLay->setSpacing(0);
auto *filterRow = new QHBoxLayout;
filterRow->setContentsMargins(6, 6, 6, 6);
filterRow->addWidget(m_filterEdit, 1);
leftLay->addLayout(filterRow);
leftLay->addWidget(m_listScroll, 1);
m_detailPane = new ProviderDetailPane(this);
connect(m_detailPane, &ProviderDetailPane::saveRequested,
this, &ProvidersPageWidget::onSaveEdited);
connect(m_detailPane, &ProviderDetailPane::duplicateRequested,
this, &ProvidersPageWidget::onDuplicateClicked);
connect(m_detailPane, &ProviderDetailPane::deleteRequested,
this, &ProvidersPageWidget::onRemoveClicked);
connect(m_detailPane, &ProviderDetailPane::apiKeySaveRequested,
this, &ProvidersPageWidget::onApiKeySave);
connect(m_detailPane, &ProviderDetailPane::apiKeyClearRequested,
this, &ProvidersPageWidget::onApiKeyClear);
connect(m_detailPane, &ProviderDetailPane::launchStartRequested,
this, &ProvidersPageWidget::onLaunchStart);
connect(m_detailPane, &ProviderDetailPane::launchStopRequested,
this, &ProvidersPageWidget::onLaunchStop);
connect(m_detailPane, &ProviderDetailPane::launchRestartRequested,
this, &ProvidersPageWidget::onLaunchRestart);
connect(m_detailPane, &ProviderDetailPane::openInEditorRequested,
this, [this](const QString &path) {
if (path.isEmpty() || path.startsWith(QLatin1String(":/"))) {
QMessageBox::information(
this, tr("Open in editor"),
tr("Bundled providers are read-only. "
"Use Duplicate to create an editable user copy first."));
return;
}
Core::EditorManager::openEditor(Utils::FilePath::fromString(path));
});
if (m_launcher) {
connect(m_launcher.data(), &Providers::ProviderLauncher::stateChanged,
this, [this](const QString &name,
Providers::ProviderLauncher::State newState) {
if (name == m_currentName)
refreshDetailLaunch();
const ProviderListItem::Status status = rowStatusFromState(newState);
for (auto *row : m_rows) {
if (row->providerName() == name)
row->setStatus(status);
}
});
connect(m_launcher.data(), &Providers::ProviderLauncher::bytesReceived,
this, [this](const QString &name, const QByteArray &chunk) {
if (name == m_currentName)
m_detailPane->appendLaunchBytes(chunk);
});
}
m_detailScroll = new QScrollArea(this);
m_detailScroll->setWidgetResizable(true);
m_detailScroll->setFrameShape(QFrame::StyledPanel);
m_detailScroll->setWidget(m_detailPane);
auto *splitter = new QSplitter(Qt::Horizontal, this);
splitter->addWidget(leftBox);
splitter->addWidget(m_detailScroll);
splitter->setStretchFactor(0, 0);
splitter->setStretchFactor(1, 1);
splitter->setSizes({320, 700});
auto *root = new QVBoxLayout(this);
root->setContentsMargins(8, 8, 8, 8);
root->setSpacing(6);
root->addLayout(headerRow);
root->addWidget(headerSep);
root->addWidget(splitter, 1);
m_filterDebounce = new QTimer(this);
m_filterDebounce->setSingleShot(true);
m_filterDebounce->setInterval(100);
connect(m_filterDebounce, &QTimer::timeout, this,
&ProvidersPageWidget::rebuildList);
connect(m_filterEdit, &QLineEdit::textChanged, this,
[this](const QString &) { m_filterDebounce->start(); });
if (m_factory) {
connect(m_factory.data(),
&Providers::ProviderInstanceFactory::instancesReloaded,
this, &ProvidersPageWidget::rebuildList);
}
if (m_navigator) {
connect(m_navigator.data(),
&ProvidersPageNavigator::selectInstanceRequested,
this, &ProvidersPageWidget::selectInstance);
}
rebuildList();
const QString pending
= m_navigator ? m_navigator->takePendingSelection() : QString{};
if (!pending.isEmpty())
selectInstance(pending);
else if (m_factory && !m_factory->instances().empty())
selectInstance(m_factory->instances().front().name);
}
void apply() final {}
private slots:
void rebuildList()
{
if (!m_factory)
return;
while (m_listLayout->count() > 0) {
QLayoutItem *item = m_listLayout->takeAt(0);
if (auto *w = item->widget())
w->deleteLater();
delete item;
}
m_rows.clear();
m_listLayout->addStretch(1); // re-add trailing stretch
const QString filter = m_filterEdit->text().trimmed().toLower();
auto matches = [&](const Providers::ProviderInstance &inst) {
if (filter.isEmpty())
return true;
return inst.name.toLower().contains(filter)
|| inst.clientApi.toLower().contains(filter)
|| inst.url.toLower().contains(filter);
};
auto addSection = [&](const QString &title, bool userSection) {
auto *header = new QLabel(title.toUpper(), m_listContent);
QFont hf = header->font();
hf.setPixelSize(10);
hf.setLetterSpacing(QFont::AbsoluteSpacing, 0.5);
header->setFont(hf);
QPalette hp = header->palette();
hp.setColor(QPalette::WindowText, hp.color(QPalette::Mid));
header->setPalette(hp);
header->setContentsMargins(8, 4, 8, 4);
header->setAutoFillBackground(true);
header->setStyleSheet(
QStringLiteral("QLabel { background:%1; }")
.arg(themeFor(palette()).listHeaderBg));
m_listLayout->insertWidget(m_listLayout->count() - 1, header);
std::vector<const Providers::ProviderInstance *> sorted;
for (const auto &inst : m_factory->instances()) {
if (inst.isUserSource() != userSection)
continue;
if (!matches(inst))
continue;
sorted.push_back(&inst);
}
std::sort(sorted.begin(), sorted.end(),
[](const Providers::ProviderInstance *a,
const Providers::ProviderInstance *b) {
return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
});
int shown = 0;
for (const auto *inst : sorted) {
auto *row = new ProviderListItem(*inst, m_listContent);
connect(row, &ProviderListItem::clicked,
this, &ProvidersPageWidget::selectInstance);
if (m_launcher)
row->setStatus(rowStatusFromLauncher(inst->name));
m_rows.append(row);
m_listLayout->insertWidget(m_listLayout->count() - 1, row);
++shown;
}
if (shown == 0) {
auto *empty = new QLabel(
userSection ? tr("No user instances yet.")
: tr("No bundled instances loaded."),
m_listContent);
empty->setContentsMargins(10, 6, 10, 6);
QPalette ep = empty->palette();
ep.setColor(QPalette::WindowText, ep.color(QPalette::Mid));
empty->setPalette(ep);
m_listLayout->insertWidget(m_listLayout->count() - 1, empty);
}
};
addSection(tr("User"), true);
addSection(tr("Bundled"), false);
for (auto *row : m_rows)
row->setSelected(row->providerName() == m_currentName);
if (!m_currentName.isEmpty())
populateDetail(m_currentName);
else
m_detailPane->clear();
}
void selectInstance(const QString &name)
{
if (name.isEmpty())
return;
const auto *inst = m_factory ? m_factory->instanceByName(name) : nullptr;
if (!inst)
return;
m_currentName = inst->name;
for (auto *row : m_rows)
row->setSelected(row->providerName() == inst->name);
populateDetail(inst->name);
}
void onDuplicateClicked()
{
if (!m_factory || m_currentName.isEmpty())
return;
const Providers::ProviderInstance *srcPtr
= m_factory->instanceByName(m_currentName);
if (!srcPtr)
return;
const Providers::ProviderInstance srcCopy = *srcPtr;
bool ok = false;
const QString name = QInputDialog::getText(
this, tr("Duplicate provider"),
tr("Name for the new provider:"), QLineEdit::Normal,
QStringLiteral("%1 (copy)").arg(srcCopy.name), &ok);
if (!ok || name.trimmed().isEmpty())
return;
if (m_factory->instanceByName(name.trimmed())) {
QMessageBox::warning(this, tr("Duplicate provider"),
tr("An instance named '%1' already exists.").arg(name.trimmed()));
return;
}
Providers::ProviderInstance copy = srcCopy;
copy.name = name.trimmed();
copy.apiKeyRef = QStringLiteral("qodeassist/providers/%1").arg(copy.name);
copy.sourcePath.clear();
copy.overridesBundled = false;
QString writeErr;
if (Providers::ProviderInstanceWriter::writeToUserDir(
copy, /*previousPath=*/QString{}, &writeErr).isEmpty()) {
QMessageBox::warning(this, tr("Duplicate provider"), writeErr);
return;
}
m_factory->reload();
selectInstance(copy.name);
}
void onRemoveClicked()
{
if (!m_factory || m_currentName.isEmpty())
return;
const Providers::ProviderInstance *instPtr
= m_factory->instanceByName(m_currentName);
if (!instPtr || !instPtr->isUserSource())
return;
const QString instName = instPtr->name;
const QString sourcePath = instPtr->sourcePath;
if (QMessageBox::question(
this, tr("Delete provider"),
tr("Delete user provider '%1'?\n\nFile: %2").arg(instName, sourcePath))
!= QMessageBox::Yes)
return;
if (!QFile::remove(sourcePath)) {
QMessageBox::warning(this, tr("Delete provider"),
tr("Failed to delete file:\n%1").arg(sourcePath));
return;
}
m_currentName.clear();
m_factory->reload();
m_detailPane->clear();
}
void onSaveEdited(const Providers::ProviderInstance &edited)
{
if (!m_factory)
return;
Providers::ProviderInstance e = edited;
if (e.name.isEmpty()) {
QMessageBox::warning(this, tr("Save"), tr("Name cannot be empty."));
return;
}
const auto *prior = m_factory->instanceByName(m_currentName);
const QString priorRef = prior ? prior->apiKeyRef : QString{};
const QString priorName = prior ? prior->name : QString{};
const bool nameChanged = !priorName.isEmpty() && priorName != e.name;
if (e.apiKeyRef.isEmpty() || (nameChanged && e.apiKeyRef == priorRef))
e.apiKeyRef = QStringLiteral("qodeassist/providers/%1").arg(e.name);
const QString validation = Providers::ProviderInstance::validate(
e, m_factory->knownClientApis());
if (!validation.isEmpty()) {
QMessageBox::warning(this, tr("Save"), validation);
return;
}
if (nameChanged) {
const auto *clash = m_factory->instanceByName(e.name);
if (clash) {
QMessageBox::warning(this, tr("Save"),
tr("An instance named '%1' already exists.").arg(e.name));
return;
}
}
const QString softWarning = Providers::ProviderInstance::warnings(e);
if (!softWarning.isEmpty()) {
if (QMessageBox::warning(this, tr("Save"),
softWarning + QStringLiteral("\n\n")
+ tr("Save anyway?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No)
!= QMessageBox::Yes)
return;
}
const QString previousPath
= (prior && prior->isUserSource()) ? prior->sourcePath : QString{};
QString writeErr;
const QString writtenPath = Providers::ProviderInstanceWriter::writeToUserDir(
e, previousPath, &writeErr);
if (writtenPath.isEmpty()) {
QMessageBox::warning(this, tr("Save"), writeErr);
return;
}
if (!previousPath.isEmpty()
&& QFileInfo(writtenPath).absoluteFilePath()
!= QFileInfo(previousPath).absoluteFilePath()) {
if (!QFile::remove(previousPath)) {
QMessageBox::warning(
this, tr("Save"),
tr("Saved to:\n%1\n\nbut could not remove the old file:\n%2\n\n"
"Two provider files now describe this instance — delete the "
"old file manually to avoid a duplicate-name error.")
.arg(writtenPath, previousPath));
}
}
if (m_secrets && !priorRef.isEmpty() && priorRef != e.apiKeyRef) {
const QString carried = m_secrets->readKeySync(priorRef);
if (!carried.isEmpty())
m_secrets->writeKey(e.apiKeyRef, carried);
m_secrets->eraseKey(priorRef);
}
m_factory->reload();
selectInstance(e.name);
}
void onApiKeySave(const QString &newKey)
{
if (!m_factory || !m_secrets || m_currentName.isEmpty() || newKey.isEmpty())
return;
const auto *inst = m_factory->instanceByName(m_currentName);
if (!inst || inst->apiKeyRef.isEmpty())
return;
m_secrets->writeKey(inst->apiKeyRef, newKey);
m_detailPane->refreshKeyStatus(true);
}
void onApiKeyClear()
{
if (!m_factory || !m_secrets || m_currentName.isEmpty())
return;
const Providers::ProviderInstance *instPtr
= m_factory->instanceByName(m_currentName);
if (!instPtr || instPtr->apiKeyRef.isEmpty())
return;
const QString instName = instPtr->name;
const QString apiKeyRef = instPtr->apiKeyRef;
if (QMessageBox::question(
this, tr("Clear API key"),
tr("Erase the stored API key for '%1'?").arg(instName))
!= QMessageBox::Yes)
return;
m_secrets->eraseKey(apiKeyRef);
m_detailPane->refreshKeyStatus(false);
}
void onLaunchStart(const QString &name)
{
if (!m_factory || !m_launcher)
return;
const auto *inst = m_factory->instanceByName(name);
if (!inst || inst->launch.isEmpty())
return;
m_launcher->start(name, inst->launch);
}
void onLaunchStop(const QString &name)
{
if (!m_launcher)
return;
m_launcher->stop(name);
}
void onLaunchRestart(const QString &name)
{
if (!m_factory || !m_launcher)
return;
const auto *inst = m_factory->instanceByName(name);
if (!inst || inst->launch.isEmpty())
return;
m_launcher->restart(name, inst->launch);
}
private:
void populateDetail(const QString &name)
{
if (!m_factory)
return;
const auto *inst = m_factory->instanceByName(name);
if (!inst) {
m_detailPane->clear();
return;
}
const bool hasStoredKey
= m_secrets && !inst->apiKeyRef.isEmpty() && m_secrets->hasKey(inst->apiKeyRef);
m_detailPane->populate(*inst, hasStoredKey);
if (m_launcher) {
m_detailPane->setLaunchState(
m_launcher->state(inst->name),
m_launcher->lastError(inst->name));
m_detailPane->resetLaunchTerminal(m_launcher->scrollback(inst->name));
} else {
m_detailPane->setLaunchState(Providers::ProviderLauncher::Idle, {});
m_detailPane->resetLaunchTerminal({});
}
}
QPointer<Providers::ProviderInstanceFactory> m_factory;
QPointer<Providers::ProviderSecretsStore> m_secrets;
QPointer<ProvidersPageNavigator> m_navigator;
QLabel *m_titleLabel = nullptr;
QLineEdit *m_filterEdit = nullptr;
QScrollArea *m_listScroll = nullptr;
QWidget *m_listContent = nullptr;
QVBoxLayout *m_listLayout = nullptr;
QList<ProviderListItem *> m_rows;
QScrollArea *m_detailScroll = nullptr;
ProviderDetailPane *m_detailPane = nullptr;
QString m_currentName;
QPointer<Providers::ProviderLauncher> m_launcher;
QTimer *m_filterDebounce = nullptr;
void refreshDetailLaunch()
{
if (!m_launcher || m_currentName.isEmpty())
return;
m_detailPane->setLaunchState(
m_launcher->state(m_currentName),
m_launcher->lastError(m_currentName));
}
static ProviderListItem::Status rowStatusFromState(
Providers::ProviderLauncher::State state)
{
switch (state) {
case Providers::ProviderLauncher::Ready:
return ProviderListItem::Status::Ok;
case Providers::ProviderLauncher::Failed:
return ProviderListItem::Status::Fail;
case Providers::ProviderLauncher::Idle:
case Providers::ProviderLauncher::Starting:
case Providers::ProviderLauncher::Probing:
case Providers::ProviderLauncher::Stopping:
return ProviderListItem::Status::Unknown;
}
return ProviderListItem::Status::Unknown;
}
ProviderListItem::Status rowStatusFromLauncher(const QString &name) const
{
if (!m_launcher)
return ProviderListItem::Status::Unknown;
return rowStatusFromState(m_launcher->state(name));
}
};
class ProvidersOptionsPage : public Core::IOptionsPage
{
public:
ProvidersOptionsPage(
Providers::ProviderInstanceFactory *factory,
Providers::ProviderSecretsStore *secrets,
Providers::ProviderLauncher *launcher,
ProvidersPageNavigator *navigator)
{
setId(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
setDisplayName(QObject::tr("Providers"));
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
setWidgetCreator([factory, secrets, launcher, navigator] {
return new ProvidersPageWidget(factory, secrets, launcher, navigator);
});
}
};
} // namespace
std::unique_ptr<Core::IOptionsPage> createProvidersSettingsPage(
Providers::ProviderInstanceFactory *instanceFactory,
Providers::ProviderSecretsStore *secrets,
Providers::ProviderLauncher *launcher,
ProvidersPageNavigator *navigator)
{
return std::make_unique<ProvidersOptionsPage>(
instanceFactory, secrets, launcher, navigator);
}
} // namespace QodeAssist::Settings
#include "ProvidersSettingsPage.moc"

View File

@@ -1,44 +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::Providers {
class ProviderInstanceFactory;
class ProviderSecretsStore;
class ProviderLauncher;
}
namespace QodeAssist::Settings {
class ProvidersPageNavigator : public QObject
{
Q_OBJECT
public:
explicit ProvidersPageNavigator(QObject *parent = nullptr);
void requestSelectInstance(const QString &name);
QString takePendingSelection();
signals:
void selectInstanceRequested(const QString &name);
private:
QString m_pending;
};
std::unique_ptr<Core::IOptionsPage> createProvidersSettingsPage(
Providers::ProviderInstanceFactory *instanceFactory,
Providers::ProviderSecretsStore *secrets,
Providers::ProviderLauncher *launcher,
ProvidersPageNavigator *navigator);
} // namespace QodeAssist::Settings

View File

@@ -1,32 +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 "SectionBox.hpp"
#include <QLabel>
#include <QVBoxLayout>
namespace QodeAssist::Settings {
SectionBox::SectionBox(const QString &title, QWidget *parent)
: QWidget(parent)
{
m_title = new QLabel(title, this);
QFont tf = m_title->font();
tf.setBold(true);
m_title->setFont(tf);
m_body = new QWidget(this);
m_bodyLayout = new QVBoxLayout(m_body);
m_bodyLayout->setContentsMargins(0, 0, 0, 0);
m_bodyLayout->setSpacing(4);
auto *outer = new QVBoxLayout(this);
outer->setContentsMargins(0, 4, 0, 4);
outer->setSpacing(4);
outer->addWidget(m_title);
outer->addWidget(m_body, 1);
}
} // namespace QodeAssist::Settings

View File

@@ -1,27 +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 <QWidget>
class QLabel;
class QVBoxLayout;
namespace QodeAssist::Settings {
class SectionBox : public QWidget
{
public:
explicit SectionBox(const QString &title, QWidget *parent = nullptr);
QVBoxLayout *bodyLayout() const { return m_bodyLayout; }
private:
QLabel *m_title = nullptr;
QWidget *m_body = nullptr;
QVBoxLayout *m_bodyLayout = nullptr;
};
} // namespace QodeAssist::Settings

View File

@@ -140,12 +140,6 @@ const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist";
// Provider Settings Page ID
const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.7ProviderSettingsPageId";
// Agents Settings Page ID
const char QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID[] = "QodeAssist.8AgentsSettingsPageId";
// Agent Pipelines (experimental) settings
const char QODE_ASSIST_AGENT_PIPELINES_PAGE_ID[] = "QodeAssist.9AgentPipelinesPageId";
// Provider API Keys
const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey";
const char OPEN_ROUTER_API_KEY_HISTORY[] = "QodeAssist.openRouterApiKeyHistory";

View File

@@ -1,53 +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 <QFont>
#include <QFontDatabase>
#include <QPalette>
#include <QString>
namespace QodeAssist::Settings {
struct Theme
{
bool dark = false;
QString listHeaderBg;
QString rowSeparator;
QString rowSelectedBg;
QString codeBg;
};
inline bool isDarkPalette(const QPalette &p)
{
return p.color(QPalette::Window).lightness() < 128;
}
inline Theme themeFor(const QPalette &p)
{
const bool dark = isDarkPalette(p);
if (dark)
return {true,
QStringLiteral("#262626"),
QStringLiteral("#3a3a3a"),
QStringLiteral("#2c4060"),
QStringLiteral("#1f1f1f")};
return {false,
QStringLiteral("#f0f0f0"),
QStringLiteral("#dcdcdc"),
QStringLiteral("#cfe2ff"),
QStringLiteral("#f4f4f4")};
}
inline QFont monospaceFont(int pixelSize = 11)
{
QFont f = QFontDatabase::systemFont(QFontDatabase::FixedFont);
f.setStyleHint(QFont::Monospace);
if (pixelSize > 0)
f.setPixelSize(pixelSize);
return f;
}
} // namespace QodeAssist::Settings

View File

@@ -1,101 +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 "SettingsUiBuilders.hpp"
#include "SettingsTheme.hpp"
#include <QFont>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPalette>
#include <QWidget>
namespace QodeAssist::Settings {
void applyMutedSmallCaps(QLabel *label)
{
QFont f = label->font();
f.setPixelSize(10);
f.setLetterSpacing(QFont::AbsoluteSpacing, 0.4);
label->setFont(f);
QPalette p = label->palette();
p.setColor(QPalette::WindowText, p.color(QPalette::Mid));
label->setPalette(p);
}
QLabel *makeSectionHeader(const QString &title, QWidget *parent)
{
auto *header = new QLabel(title.toUpper(), parent);
applyMutedSmallCaps(header);
header->setContentsMargins(8, 4, 8, 4);
header->setAutoFillBackground(true);
const Theme theme = themeFor(parent ? parent->palette() : QPalette());
header->setStyleSheet(
QStringLiteral("QLabel { background:%1; border-top:1px solid %2;"
" border-bottom:1px solid %2; }")
.arg(theme.listHeaderBg, theme.rowSeparator));
return header;
}
QLabel *makeHintLabel(const QString &text, QWidget *parent)
{
auto *h = new QLabel(text, parent);
QFont hf = h->font();
hf.setPixelSize(11);
h->setFont(hf);
h->setWordWrap(true);
QPalette p = h->palette();
p.setColor(QPalette::WindowText, p.color(QPalette::Mid));
h->setPalette(p);
return h;
}
QHBoxLayout *singleField(QWidget *w)
{
auto *lay = new QHBoxLayout;
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(4);
lay->addWidget(w, 1);
return lay;
}
namespace {
QLabel *makeFormLabel(const QString &text)
{
auto *l = new QLabel(text);
l->setMinimumWidth(96);
l->setAlignment(Qt::AlignLeft | Qt::AlignTop);
return l;
}
} // namespace
FormBuilder::FormBuilder(QGridLayout *grid, int startRow)
: m_grid(grid)
, m_row(startRow)
{}
FormBuilder &FormBuilder::row(const QString &label, QLayout *value, const QString &hint)
{
m_grid->addWidget(makeFormLabel(label), m_row, 0, Qt::AlignTop);
auto *holder = new QWidget;
holder->setLayout(value);
m_grid->addWidget(holder, m_row, 1);
++m_row;
if (!hint.isEmpty()) {
m_grid->addWidget(makeHintLabel(hint), m_row, 1);
++m_row;
}
return *this;
}
FormBuilder &FormBuilder::row(const QString &label, QWidget *value, const QString &hint)
{
return row(label, singleField(value), hint);
}
} // namespace QodeAssist::Settings

View File

@@ -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 <QString>
class QGridLayout;
class QHBoxLayout;
class QLabel;
class QLayout;
class QWidget;
namespace QodeAssist::Settings {
void applyMutedSmallCaps(QLabel *label);
QLabel *makeSectionHeader(const QString &title, QWidget *parent);
QLabel *makeHintLabel(const QString &text, QWidget *parent = nullptr);
QHBoxLayout *singleField(QWidget *w);
class FormBuilder
{
public:
explicit FormBuilder(QGridLayout *grid, int startRow = 0);
FormBuilder &row(const QString &label, QLayout *value, const QString &hint = {});
FormBuilder &row(const QString &label, QWidget *value, const QString &hint = {});
int currentRow() const { return m_row; }
private:
QGridLayout *m_grid;
int m_row;
};
} // namespace QodeAssist::Settings

View File

@@ -1,89 +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 "TagChip.hpp"
#include "SettingsTheme.hpp"
#include <QEvent>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QPalette>
#include <QScopedValueRollback>
namespace QodeAssist::Settings {
TagChip::TagChip(const QString &tag, int count, QWidget *parent)
: QFrame(parent)
, m_tag(tag)
{
setObjectName(QStringLiteral("TagChip"));
setCursor(Qt::PointingHandCursor);
m_label = new QLabel(tag, this);
m_label->setFont(monospaceFont(11));
auto *row = new QHBoxLayout(this);
row->setContentsMargins(5, 0, 5, 0);
row->setSpacing(4);
row->addWidget(m_label);
if (count >= 0) {
m_count = new QLabel(QString::number(count), this);
QFont cf = m_count->font();
cf.setPixelSize(10);
m_count->setFont(cf);
row->addWidget(m_count);
}
applyTheme();
}
void TagChip::setActive(bool on)
{
if (m_active == on)
return;
m_active = on;
applyTheme();
}
void TagChip::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
emit clicked(m_tag);
QFrame::mouseReleaseEvent(event);
}
void TagChip::changeEvent(QEvent *event)
{
QFrame::changeEvent(event);
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
applyTheme();
}
void TagChip::applyTheme()
{
if (m_inApplyTheme)
return;
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
const Theme theme = themeFor(palette());
const QString text = palette().color(QPalette::WindowText).name();
const QString mute = palette().color(QPalette::Mid).name();
const QString border = m_active ? text : theme.rowSeparator;
const QString bg = m_active ? theme.rowSelectedBg : QStringLiteral("transparent");
setStyleSheet(QStringLiteral(
"#TagChip { background:%1; border:1px solid %2; }")
.arg(bg, border));
QPalette lp = m_label->palette();
lp.setColor(QPalette::WindowText, m_active ? QColor(text) : QColor(mute));
m_label->setPalette(lp);
if (m_count) {
QPalette cp = m_count->palette();
cp.setColor(QPalette::WindowText, QColor(mute));
m_count->setPalette(cp);
}
}
} // namespace QodeAssist::Settings

View File

@@ -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 <QFrame>
#include <QString>
class QLabel;
namespace QodeAssist::Settings {
class TagChip : public QFrame
{
Q_OBJECT
public:
explicit TagChip(const QString &tag, int count, QWidget *parent = nullptr);
void setActive(bool on);
QString tag() const { return m_tag; }
signals:
void clicked(const QString &tag);
protected:
void mouseReleaseEvent(QMouseEvent *event) override;
void changeEvent(QEvent *event) override;
private:
void applyTheme();
QString m_tag;
bool m_active = false;
bool m_inApplyTheme = false;
QLabel *m_label = nullptr;
QLabel *m_count = nullptr;
};
} // namespace QodeAssist::Settings

View File

@@ -1,164 +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 "TagFilterStrip.hpp"
#include "SettingsTheme.hpp"
#include "SettingsUiBuilders.hpp"
#include "TagChip.hpp"
#include <QEvent>
#include <QFont>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLayoutItem>
#include <QPalette>
#include <QScopedValueRollback>
#include <QStringList>
#include <QTimer>
#include <QVBoxLayout>
#include <algorithm>
namespace QodeAssist::Settings {
TagFilterStrip::TagFilterStrip(QWidget *parent)
: QWidget(parent)
{
setObjectName(QStringLiteral("TagStrip"));
setAutoFillBackground(true);
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(8, 6, 8, 6);
m_layout->setSpacing(5);
applyTheme();
}
void TagFilterStrip::setAvailableTags(const QMap<QString, int> &countsByTag)
{
m_counts = countsByTag;
QSet<QString> stillExisting;
for (auto it = m_counts.cbegin(); it != m_counts.cend(); ++it)
stillExisting.insert(it.key());
QSet<QString> trimmed;
for (const QString &t : m_activeTags)
if (stillExisting.contains(t))
trimmed.insert(t);
const bool activeChanged = trimmed != m_activeTags;
if (activeChanged)
m_activeTags = trimmed;
rebuild();
if (activeChanged)
emit activeTagsChanged(m_activeTags);
}
void TagFilterStrip::changeEvent(QEvent *event)
{
QWidget::changeEvent(event);
if (m_inApplyTheme)
return;
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
applyTheme();
}
void TagFilterStrip::toggleTag(const QString &tag)
{
if (m_activeTags.contains(tag))
m_activeTags.remove(tag);
else
m_activeTags.insert(tag);
refreshActiveStates();
emit activeTagsChanged(m_activeTags);
}
void TagFilterStrip::refreshActiveStates()
{
for (auto it = m_chipByTag.cbegin(); it != m_chipByTag.cend(); ++it)
it.value()->setActive(m_activeTags.contains(it.key()));
}
void TagFilterStrip::applyTheme()
{
if (m_inApplyTheme)
return;
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
const Theme theme = themeFor(palette());
setStyleSheet(QStringLiteral("QWidget#TagStrip { background:%1;"
" border-bottom:1px solid %2; }")
.arg(theme.listHeaderBg, theme.rowSeparator));
}
void TagFilterStrip::rebuild()
{
while (auto *item = m_layout->takeAt(0)) {
if (auto *w = item->widget())
w->deleteLater();
if (auto *l = item->layout()) {
while (auto *sub = l->takeAt(0)) {
if (auto *sw = sub->widget())
sw->deleteLater();
delete sub;
}
}
delete item;
}
m_chipByTag.clear();
if (m_counts.isEmpty()) {
setVisible(false);
return;
}
setVisible(true);
auto *headerLine = new QHBoxLayout;
headerLine->setContentsMargins(0, 0, 0, 0);
headerLine->setSpacing(6);
auto *title = new QLabel(tr("FILTER BY TAG"), this);
applyMutedSmallCaps(title);
headerLine->addWidget(title);
headerLine->addStretch(1);
if (!m_activeTags.isEmpty()) {
auto *clear = new QLabel(QStringLiteral("<a href=\"#\">%1</a>").arg(tr("clear")), this);
connect(clear, &QLabel::linkActivated, this, [this](const QString &) {
if (m_activeTags.isEmpty())
return;
m_activeTags.clear();
refreshActiveStates();
emit activeTagsChanged(m_activeTags);
});
headerLine->addWidget(clear);
}
m_layout->addLayout(headerLine);
std::vector<std::pair<QString, int>> sorted;
sorted.reserve(m_counts.size());
for (auto it = m_counts.cbegin(); it != m_counts.cend(); ++it)
sorted.emplace_back(it.key(), it.value());
std::sort(sorted.begin(), sorted.end(),
[](const auto &a, const auto &b) {
if (a.second != b.second)
return a.second > b.second;
return a.first.localeAwareCompare(b.first) < 0;
});
auto *grid = new QGridLayout;
grid->setContentsMargins(0, 0, 0, 0);
grid->setHorizontalSpacing(3);
grid->setVerticalSpacing(3);
int col = 0, gridRow = 0;
for (const auto &[tag, count] : sorted) {
auto *chip = new TagChip(tag, count, this);
chip->setActive(m_activeTags.contains(tag));
connect(chip, &TagChip::clicked, this, &TagFilterStrip::toggleTag);
grid->addWidget(chip, gridRow, col, Qt::AlignLeft);
m_chipByTag.insert(tag, chip);
if (++col >= 4) {
col = 0;
++gridRow;
}
}
grid->setColumnStretch(4, 1);
m_layout->addLayout(grid);
}
} // namespace QodeAssist::Settings

View File

@@ -1,47 +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 <QHash>
#include <QMap>
#include <QSet>
#include <QString>
#include <QWidget>
class QVBoxLayout;
namespace QodeAssist::Settings {
class TagChip;
class TagFilterStrip : public QWidget
{
Q_OBJECT
public:
explicit TagFilterStrip(QWidget *parent = nullptr);
void setAvailableTags(const QMap<QString, int> &countsByTag);
const QSet<QString> &activeTags() const { return m_activeTags; }
signals:
void activeTagsChanged(const QSet<QString> &tags);
protected:
void changeEvent(QEvent *event) override;
private:
void rebuild();
void refreshActiveStates();
void applyTheme();
void toggleTag(const QString &tag);
QMap<QString, int> m_counts;
QSet<QString> m_activeTags;
QVBoxLayout *m_layout = nullptr;
QHash<QString, TagChip *> m_chipByTag;
bool m_inApplyTheme = false;
};
} // namespace QodeAssist::Settings