refactor: Improve UX

This commit is contained in:
Petr Mironychev
2026-07-07 22:36:06 +02:00
parent bf3a67a1eb
commit 0382cd17a0
23 changed files with 1051 additions and 779 deletions

View File

@@ -4,13 +4,11 @@
#include "AgentDetailPane.hpp" #include "AgentDetailPane.hpp"
#include "AgentModelDialog.hpp" #include "ModelSelector.hpp"
#include "SectionBox.hpp" #include "SectionBox.hpp"
#include "SettingsTheme.hpp" #include "SettingsTheme.hpp"
#include "SettingsUiBuilders.hpp" #include "SettingsUiBuilders.hpp"
#include <optional>
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <AgentFactory.hpp> #include <AgentFactory.hpp>
@@ -84,6 +82,9 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
m_name->setFont(nf); m_name->setFont(nf);
m_name->setTextInteractionFlags(Qt::TextSelectableByMouse); m_name->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_sourceBadge = new QLabel(this);
m_sourceBadge->setVisible(false);
m_path = new QLabel(this); m_path = new QLabel(this);
m_path->setFont(monospaceFont(11)); m_path->setFont(monospaceFont(11));
m_path->setTextInteractionFlags(Qt::TextSelectableByMouse); m_path->setTextInteractionFlags(Qt::TextSelectableByMouse);
@@ -91,20 +92,20 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
pp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid)); pp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_path->setPalette(pp); m_path->setPalette(pp);
m_openBtn = new QPushButton(tr("Open in editor"), this);
m_dupBtn = new QPushButton(tr("Customize a copy…"), this); m_dupBtn = new QPushButton(tr("Customize a copy…"), this);
m_dupBtn->setToolTip( m_dupBtn->setToolTip(
tr("Create an editable user agent that inherits from this one — " tr("Create an editable user agent that inherits from this one — "
"override only the fields you want.")); "override only the fields you want."));
m_openBtn = new QPushButton(tr("Open in editor"), this);
m_deleteBtn = new QPushButton(tr("Delete"), 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] { connect(m_dupBtn, &QPushButton::clicked, this, [this] {
if (m_current) if (m_current)
emit customizeRequested(*m_current); emit customizeRequested(*m_current);
}); });
connect(m_openBtn, &QPushButton::clicked, this, [this] {
if (m_current)
emit openInEditorRequested(*m_current);
});
connect(m_deleteBtn, &QPushButton::clicked, this, [this] { connect(m_deleteBtn, &QPushButton::clicked, this, [this] {
if (m_current) if (m_current)
emit deleteRequested(*m_current); emit deleteRequested(*m_current);
@@ -114,14 +115,15 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
auto *actions = new QHBoxLayout(m_actionsHolder); auto *actions = new QHBoxLayout(m_actionsHolder);
actions->setContentsMargins(0, 0, 0, 0); actions->setContentsMargins(0, 0, 0, 0);
actions->setSpacing(6); actions->setSpacing(6);
actions->addWidget(m_openBtn);
actions->addWidget(m_dupBtn); actions->addWidget(m_dupBtn);
actions->addWidget(m_openBtn);
actions->addWidget(m_deleteBtn); actions->addWidget(m_deleteBtn);
auto *titleRow = new QHBoxLayout; auto *titleRow = new QHBoxLayout;
titleRow->setContentsMargins(0, 0, 0, 0); titleRow->setContentsMargins(0, 0, 0, 0);
titleRow->setSpacing(8); titleRow->setSpacing(8);
titleRow->addWidget(m_name); titleRow->addWidget(m_name);
titleRow->addWidget(m_sourceBadge, 0, Qt::AlignVCenter);
titleRow->addStretch(1); titleRow->addStretch(1);
auto *headerLeft = new QVBoxLayout; auto *headerLeft = new QVBoxLayout;
@@ -144,41 +146,32 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
m_description->setWordWrap(true); m_description->setWordWrap(true);
m_description->setTextInteractionFlags(Qt::TextSelectableByMouse); m_description->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *identity = new SectionBox(tr("Identity"), this); auto *setup = new SectionBox(tr("Configuration"), 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; m_modelSelector = new ModelSelector(this);
idGrid->setContentsMargins(0, 0, 0, 0); connect(m_modelSelector, &ModelSelector::modelSubmitted, this, [this](const QString &model) {
idGrid->setHorizontalSpacing(8); onModelSubmitted(model);
idGrid->setVerticalSpacing(4); });
FormBuilder idForm(idGrid);
idForm.row(tr("Name:"), m_nameValue); m_modelResetBtn = new QPushButton(tr("Reset"), this);
{ m_modelResetBtn->setToolTip(tr("Remove the model override and restore the agent's default"));
auto *holder = new QWidget; m_modelResetBtn->setVisible(false);
holder->setLayout(singleField(m_extendsValue)); connect(m_modelResetBtn, &QPushButton::clicked, this, [this] { onResetModel(); });
const int row = idForm.currentRow();
idGrid->addWidget(m_extendsLabel, row, 0, Qt::AlignTop); auto *modelHolder = new QWidget(this);
idGrid->addWidget(holder, row, 1); auto *modelRow = new QHBoxLayout(modelHolder);
m_extendsHolder = holder; modelRow->setContentsMargins(0, 0, 0, 0);
idForm = FormBuilder(idGrid, row + 1); modelRow->setSpacing(6);
} modelRow->addWidget(m_modelSelector, 1);
idForm.row(tr("Description:"), m_descriptionEdit); modelRow->addWidget(m_modelResetBtn);
idForm.row(
tr("Tags:"), m_modelStatus = makeHintLabel(QString(), this);
m_tagsValue, m_modelStatus->setVisible(false);
tr("Comma-separated. Free-form — used to filter and " connect(m_modelSelector, &ModelSelector::statusChanged, this, [this](const QString &status) {
"group the agent list.")); m_modelStatus->setText(status);
identity->bodyLayout()->addLayout(idGrid); m_modelStatus->setVisible(!status.isEmpty());
});
auto *connection = new SectionBox(tr("Connection"), this);
m_providerCombo = new QComboBox(this); m_providerCombo = new QComboBox(this);
m_providerCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents); m_providerCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
m_providerCombo->setEnabled(false); m_providerCombo->setEnabled(false);
@@ -199,65 +192,6 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
providerRow->addWidget(m_providerCombo, 1); providerRow->addWidget(m_providerCombo, 1);
providerRow->addWidget(m_providerResetBtn); providerRow->addWidget(m_providerResetBtn);
m_endpointValue = makeReadOnlyLine(true);
m_modelValue = makeReadOnlyLine(true);
m_modelValue->setPlaceholderText(tr("(set a model)"));
m_modelChangeBtn = new QPushButton(tr("Change…"), this);
m_modelChangeBtn->setToolTip(tr("Pick a model from the provider or type one"));
m_modelChangeBtn->setEnabled(false);
connect(m_modelChangeBtn, &QPushButton::clicked, this, [this] { onChangeModel(); });
m_modelResetBtn = new QPushButton(tr("Reset"), this);
m_modelResetBtn->setToolTip(tr("Remove the model override and restore the agent's default"));
m_modelResetBtn->setVisible(false);
connect(m_modelResetBtn, &QPushButton::clicked, this, [this] { onResetModel(); });
auto *modelHolder = new QWidget(this);
auto *modelRow = new QHBoxLayout(modelHolder);
modelRow->setContentsMargins(0, 0, 0, 0);
modelRow->setSpacing(6);
modelRow->addWidget(m_modelValue, 1);
modelRow->addWidget(m_modelChangeBtn);
modelRow->addWidget(m_modelResetBtn);
auto *connGrid = new QGridLayout;
connGrid->setContentsMargins(0, 0, 0, 0);
connGrid->setHorizontalSpacing(8);
connGrid->setVerticalSpacing(4);
FormBuilder(connGrid)
.row(
tr("Provider:"),
providerHolder,
tr("The provider instance this agent uses. URL is "
"inherited from the instance. Switching it is saved as a "
"per-agent override; Reset restores the agent's default."))
.row(
tr("Endpoint:"),
m_endpointValue,
tr("Appended to the provider's URL. Blank uses the "
"provider default."))
.row(
tr("Model:"),
modelHolder,
tr("Model sent to the provider. Click Change… to pick from the "
"provider's available models or type one. The choice is saved "
"as a per-agent override in settings; Reset restores the "
"agent's default."));
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 *capabilities = new SectionBox(tr("Capabilities"), this);
m_thinkingValue = new QLabel(this);
m_toolsCheck = new QCheckBox(tr("Allow tool calls"), this); m_toolsCheck = new QCheckBox(tr("Allow tool calls"), this);
m_toolsCheck->setEnabled(false); m_toolsCheck->setEnabled(false);
connect(m_toolsCheck, &QCheckBox::clicked, this, [this](bool on) { onToggleTools(on); }); connect(m_toolsCheck, &QCheckBox::clicked, this, [this](bool on) { onToggleTools(on); });
@@ -275,38 +209,46 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
toolsRow->addStretch(1); toolsRow->addStretch(1);
toolsRow->addWidget(m_toolsResetBtn); toolsRow->addWidget(m_toolsResetBtn);
auto *capGrid = new QGridLayout; m_thinkingValue = new QLabel(this);
capGrid->setContentsMargins(0, 0, 0, 0);
capGrid->setHorizontalSpacing(8);
capGrid->setVerticalSpacing(4);
FormBuilder(capGrid)
.row(
tr("Thinking:"),
m_thinkingValue,
tr("Whether this agent requests extended thinking. Defined by the "
"agent template."))
.row(
tr("Tools:"),
toolsHolder,
tr("Whether the agent may call tools. Toggle to save a per-agent "
"override in settings; Reset restores the agent's default."));
capabilities->bodyLayout()->addLayout(capGrid);
auto *match = new SectionBox(tr("Match"), this); auto *setupGrid = new QGridLayout;
auto *matchHint = makeHintLabel( setupGrid->setContentsMargins(0, 0, 0, 0);
tr("When a feature slot has multiple bound agents, the first whose " setupGrid->setHorizontalSpacing(8);
"match rules satisfy the current context wins.")); setupGrid->setVerticalSpacing(4);
FormBuilder setupForm(setupGrid);
setupForm.row(
tr("Model:"),
modelHolder,
tr("Click to pick from the provider's models, or type a name and press Enter."));
{
const int row = setupForm.currentRow();
setupGrid->addWidget(m_modelStatus, row, 1);
setupForm = FormBuilder(setupGrid, row + 1);
}
setupForm.row(tr("Provider:"), providerHolder)
.row(tr("Tools:"), toolsHolder)
.row(tr("Thinking:"), m_thinkingValue);
setup->bodyLayout()->addLayout(setupGrid);
setup->bodyLayout()->addWidget(makeHintLabel(
tr("Changes apply immediately and are saved as per-agent overrides in settings; "
"Reset restores the value from the agent file."),
this));
m_extendsValue = makeReadOnlyLine();
m_extendsValue->setPlaceholderText(tr("(none)"));
m_tagsValue = makeReadOnlyLine();
m_tagsValue->setPlaceholderText(tr("(none)"));
m_endpointValue = makeReadOnlyLine(true);
m_endpointValue->setPlaceholderText(tr("(provider default)"));
m_filePatternsValue = makeReadOnlyLine(true); m_filePatternsValue = makeReadOnlyLine(true);
auto *matchGrid = new QGridLayout; m_filePatternsValue->setPlaceholderText(tr("(matches every file)"));
matchGrid->setContentsMargins(0, 0, 0, 0);
matchGrid->setHorizontalSpacing(8); m_effectiveUrl = new QLabel(this);
matchGrid->setVerticalSpacing(4); m_effectiveUrl->setFont(monospaceFont(11));
FormBuilder(matchGrid).row( m_effectiveUrl->setTextInteractionFlags(Qt::TextSelectableByMouse);
tr("File patterns:"), m_effectiveUrl->setWordWrap(true);
m_filePatternsValue, m_effectiveUrl->setContentsMargins(6, 4, 6, 4);
tr("Globs, comma-separated. Empty matches every file.")); m_effectiveUrl->setAutoFillBackground(true);
match->bodyLayout()->addWidget(matchHint);
match->bodyLayout()->addLayout(matchGrid);
m_rawToggle = new QToolButton(this); m_rawToggle = new QToolButton(this);
m_rawToggle->setText(tr("▸ Show raw TOML")); m_rawToggle->setText(tr("▸ Show raw TOML"));
@@ -338,18 +280,49 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
m_baseRawToggle->setText(on ? tr("▾ Hide base agent TOML") : tr("▸ Show base agent TOML")); m_baseRawToggle->setText(on ? tr("▾ Hide base agent TOML") : tr("▸ Show base agent TOML"));
}); });
m_detailsBody = new QWidget(this);
auto *detailsLayout = new QVBoxLayout(m_detailsBody);
detailsLayout->setContentsMargins(0, 0, 0, 0);
detailsLayout->setSpacing(10);
auto *detailsGrid = new QGridLayout;
detailsGrid->setContentsMargins(0, 0, 0, 0);
detailsGrid->setHorizontalSpacing(8);
detailsGrid->setVerticalSpacing(4);
FormBuilder(detailsGrid)
.row(tr("Extends:"), m_extendsValue)
.row(tr("Tags:"), m_tagsValue, tr("Free-form — used to filter and group the agent list."))
.row(tr("Endpoint:"), m_endpointValue, tr("Appended to the provider's URL."))
.row(
tr("File patterns:"),
m_filePatternsValue,
tr("Globs, comma-separated. When a feature slot has multiple bound "
"agents, the first whose match rules satisfy the context wins."));
detailsLayout->addLayout(detailsGrid);
detailsLayout->addWidget(m_effectiveUrl);
detailsLayout->addWidget(m_rawToggle, 0, Qt::AlignLeft);
detailsLayout->addWidget(m_rawToml);
detailsLayout->addWidget(m_baseRawToggle, 0, Qt::AlignLeft);
detailsLayout->addWidget(m_baseRawToml);
m_detailsBody->setVisible(false);
m_detailsToggle = new QToolButton(this);
m_detailsToggle->setText(tr("▸ More details"));
m_detailsToggle->setCursor(Qt::PointingHandCursor);
m_detailsToggle->setAutoRaise(true);
m_detailsToggle->setCheckable(true);
connect(m_detailsToggle, &QToolButton::toggled, this, [this](bool on) {
m_detailsBody->setVisible(on);
m_detailsToggle->setText(on ? tr("▾ More details") : tr("▸ More details"));
});
m_body = new QWidget(this); m_body = new QWidget(this);
auto *bodyLayout = new QVBoxLayout(m_body); auto *bodyLayout = new QVBoxLayout(m_body);
bodyLayout->setContentsMargins(0, 0, 0, 0); bodyLayout->setContentsMargins(0, 0, 0, 0);
bodyLayout->setSpacing(10); bodyLayout->setSpacing(10);
bodyLayout->addWidget(identity); bodyLayout->addWidget(setup);
bodyLayout->addWidget(connection); bodyLayout->addWidget(m_detailsToggle, 0, Qt::AlignLeft);
bodyLayout->addWidget(capabilities); bodyLayout->addWidget(m_detailsBody);
bodyLayout->addWidget(match);
bodyLayout->addWidget(m_rawToggle, 0, Qt::AlignLeft);
bodyLayout->addWidget(m_rawToml);
bodyLayout->addWidget(m_baseRawToggle, 0, Qt::AlignLeft);
bodyLayout->addWidget(m_baseRawToml);
auto *root = new QVBoxLayout(this); auto *root = new QVBoxLayout(this);
root->setContentsMargins(12, 12, 12, 12); root->setContentsMargins(12, 12, 12, 12);
@@ -407,25 +380,19 @@ void AgentDetailPane::populateProviderCombo()
m_providerComboPopulated = true; m_providerComboPopulated = true;
} }
void AgentDetailPane::onChangeModel() void AgentDetailPane::onModelSubmitted(const QString &model)
{ {
if (!m_agentFactory || !m_current) if (!m_agentFactory || !m_current)
return; return;
const QString name = m_current->name; const QString name = m_current->name;
AgentModelDialog dialog(m_agentFactory, name, m_current->model, this);
if (dialog.exec() != QDialog::Accepted)
return;
const QString model = dialog.selectedModel();
if (model == m_current->model) if (model == m_current->model)
return; return;
QString err; QString err;
if (!m_agentFactory->setAgentModelOverride(name, model, &err)) { const bool ok = m_agentFactory->setAgentModelOverride(name, model, &err);
if (!ok)
QMessageBox::warning(this, tr("Set model"), err); QMessageBox::warning(this, tr("Set model"), err);
return;
}
if (const AgentConfig *cfg = m_agentFactory->configByName(name)) if (const AgentConfig *cfg = m_agentFactory->configByName(name))
setAgent(*cfg); setAgent(*cfg);
} }
@@ -540,22 +507,11 @@ void AgentDetailPane::setAgent(const AgentConfig &cfg)
setDetailsVisible(true); setDetailsVisible(true);
m_name->setText(cfg.name); m_name->setText(cfg.name);
m_sourceBadge->setText(user ? tr("User") : tr("Bundled"));
m_path->setText(cfg.sourcePath); m_path->setText(cfg.sourcePath);
m_description->setText( m_description->setText(
cfg.description.isEmpty() ? tr("No description provided.") : cfg.description); 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(); populateProviderCombo();
if (m_providerComboHasSentinel) { if (m_providerComboHasSentinel) {
@@ -589,14 +545,11 @@ void AgentDetailPane::setAgent(const AgentConfig &cfg)
? tr("Overridden in settings. Reset to use the agent's default provider.") ? tr("Overridden in settings. Reset to use the agent's default provider.")
: QString()); : QString());
m_endpointValue->setText(cfg.endpoint); m_modelSelector->setAgent(m_agentFactory, cfg.name, cfg.providerInstance, cfg.model);
m_endpointValue->setPlaceholderText(tr("(provider default)"));
m_modelValue->setText(cfg.model);
m_modelChangeBtn->setEnabled(m_agentFactory != nullptr);
const bool hasModelOverride = m_agentFactory const bool hasModelOverride = m_agentFactory
&& !m_agentFactory->agentModelOverride(cfg.name).isEmpty(); && !m_agentFactory->agentModelOverride(cfg.name).isEmpty();
m_modelResetBtn->setVisible(hasModelOverride); m_modelResetBtn->setVisible(hasModelOverride);
m_modelValue->setToolTip( m_modelSelector->setToolTip(
hasModelOverride ? tr("Overridden in settings. Reset to use the agent's default model.") hasModelOverride ? tr("Overridden in settings. Reset to use the agent's default model.")
: QString()); : QString());
@@ -613,14 +566,16 @@ void AgentDetailPane::setAgent(const AgentConfig &cfg)
hasToolsOverride ? tr("Overridden in settings. Reset to use the agent's default.") hasToolsOverride ? tr("Overridden in settings. Reset to use the agent's default.")
: QString()); : QString());
m_extendsValue->setText(cfg.extendsName);
m_tagsValue->setText(cfg.tags.join(QStringLiteral(", ")));
m_endpointValue->setText(cfg.endpoint);
m_filePatternsValue->setText(cfg.match.filePatterns.join(QStringLiteral(", ")));
const QString eff = resolvedUrl + cfg.endpoint; const QString eff = resolvedUrl + cfg.endpoint;
m_effectiveUrl->setText( m_effectiveUrl->setText(
eff.isEmpty() ? tr("# effective request line\n(unknown — provider instance not found)") eff.isEmpty() ? tr("# effective request line\n(unknown — provider instance not found)")
: QStringLiteral("# %1\nPOST %2").arg(tr("effective request line"), eff)); : QStringLiteral("# %1\nPOST %2").arg(tr("effective request line"), eff));
m_filePatternsValue->setText(cfg.match.filePatterns.join(QStringLiteral(", ")));
m_filePatternsValue->setPlaceholderText(tr("(matches every file)"));
fillRawToml(m_rawToml, cfg.sourcePath); fillRawToml(m_rawToml, cfg.sourcePath);
const QString basePath = m_agentFactory ? m_agentFactory->sourcePathForName(cfg.extendsName) const QString basePath = m_agentFactory ? m_agentFactory->sourcePathForName(cfg.extendsName)
@@ -652,11 +607,6 @@ void AgentDetailPane::clear()
m_name->setText(tr("Select an agent")); m_name->setText(tr("Select an agent"));
m_path->clear(); m_path->clear();
m_description->setText(tr("Pick an agent from the list to see its details.")); 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) { if (m_providerComboHasSentinel) {
m_providerCombo->removeItem(0); m_providerCombo->removeItem(0);
m_providerComboHasSentinel = false; m_providerComboHasSentinel = false;
@@ -664,11 +614,8 @@ void AgentDetailPane::clear()
m_providerCombo->setCurrentIndex(-1); m_providerCombo->setCurrentIndex(-1);
m_providerCombo->setEnabled(false); m_providerCombo->setEnabled(false);
m_providerResetBtn->setVisible(false); m_providerResetBtn->setVisible(false);
m_endpointValue->clear(); m_modelSelector->clearAgent();
m_modelValue->clear();
m_modelChangeBtn->setEnabled(false);
m_modelResetBtn->setVisible(false); m_modelResetBtn->setVisible(false);
m_effectiveUrl->clear();
m_thinkingValue->clear(); m_thinkingValue->clear();
{ {
QSignalBlocker block(m_toolsCheck); QSignalBlocker block(m_toolsCheck);
@@ -676,7 +623,11 @@ void AgentDetailPane::clear()
} }
m_toolsCheck->setEnabled(false); m_toolsCheck->setEnabled(false);
m_toolsResetBtn->setVisible(false); m_toolsResetBtn->setVisible(false);
m_extendsValue->clear();
m_tagsValue->clear();
m_endpointValue->clear();
m_filePatternsValue->clear(); m_filePatternsValue->clear();
m_effectiveUrl->clear();
m_rawToml->clear(); m_rawToml->clear();
m_baseRawToml->clear(); m_baseRawToml->clear();
m_baseRawToggle->setVisible(false); m_baseRawToggle->setVisible(false);
@@ -728,6 +679,7 @@ void AgentDetailPane::fillRawToml(QPlainTextEdit *view, const QString &path)
void AgentDetailPane::setDetailsVisible(bool visible) void AgentDetailPane::setDetailsVisible(bool visible)
{ {
m_headerSep->setVisible(visible); m_headerSep->setVisible(visible);
m_sourceBadge->setVisible(visible);
m_path->setVisible(visible); m_path->setVisible(visible);
m_actionsHolder->setVisible(visible); m_actionsHolder->setVisible(visible);
m_body->setVisible(visible); m_body->setVisible(visible);
@@ -744,6 +696,8 @@ void AgentDetailPane::applyCodePalette()
m_effectiveUrl->setStyleSheet( m_effectiveUrl->setStyleSheet(
QStringLiteral("QLabel { background:%1; border:1px solid %2; }") QStringLiteral("QLabel { background:%1; border:1px solid %2; }")
.arg(cssColor(codeBg), cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)))); .arg(cssColor(codeBg), cssColor(Utils::creatorColor(Utils::Theme::SplitterColor))));
if (m_current)
styleSourceBadge(m_sourceBadge, m_current->isUserSource());
} }
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

@@ -29,6 +29,7 @@ class ProviderInstanceFactory;
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
class ModelSelector;
class SectionBox; class SectionBox;
class AgentDetailPane : public QWidget class AgentDetailPane : public QWidget
@@ -56,7 +57,7 @@ private:
void setDetailsVisible(bool visible); void setDetailsVisible(bool visible);
void applyCodePalette(); void applyCodePalette();
void populateProviderCombo(); void populateProviderCombo();
void onChangeModel(); void onModelSubmitted(const QString &model);
void onResetModel(); void onResetModel();
void onChangeProvider(int index); void onChangeProvider(int index);
void onResetProvider(); void onResetProvider();
@@ -70,7 +71,11 @@ private:
AgentConfig m_currentStorage; AgentConfig m_currentStorage;
const AgentConfig *m_current = nullptr; const AgentConfig *m_current = nullptr;
QPointer<Providers::ProviderInstanceFactory> m_instanceFactory;
QPointer<AgentFactory> m_agentFactory;
QLabel *m_name = nullptr; QLabel *m_name = nullptr;
QLabel *m_sourceBadge = nullptr;
QLabel *m_path = nullptr; QLabel *m_path = nullptr;
QFrame *m_headerSep = nullptr; QFrame *m_headerSep = nullptr;
QWidget *m_actionsHolder = nullptr; QWidget *m_actionsHolder = nullptr;
@@ -80,32 +85,25 @@ private:
QPushButton *m_deleteBtn = nullptr; QPushButton *m_deleteBtn = nullptr;
QLabel *m_description = nullptr; QLabel *m_description = nullptr;
QLineEdit *m_nameValue = nullptr; ModelSelector *m_modelSelector = nullptr;
QLabel *m_extendsLabel = nullptr; QPushButton *m_modelResetBtn = nullptr;
QWidget *m_extendsHolder = nullptr; QLabel *m_modelStatus = nullptr;
QLineEdit *m_extendsValue = nullptr;
QPlainTextEdit *m_descriptionEdit = nullptr;
QLineEdit *m_tagsValue = nullptr;
QComboBox *m_providerCombo = nullptr; QComboBox *m_providerCombo = nullptr;
QPushButton *m_providerResetBtn = nullptr; QPushButton *m_providerResetBtn = nullptr;
QPointer<Providers::ProviderInstanceFactory> m_instanceFactory;
QPointer<AgentFactory> m_agentFactory;
QLineEdit *m_endpointValue = nullptr;
QLineEdit *m_modelValue = nullptr;
QPushButton *m_modelChangeBtn = nullptr;
QPushButton *m_modelResetBtn = nullptr;
QLabel *m_effectiveUrl = nullptr;
QLabel *m_thinkingValue = nullptr;
QCheckBox *m_toolsCheck = nullptr; QCheckBox *m_toolsCheck = nullptr;
QPushButton *m_toolsResetBtn = nullptr; QPushButton *m_toolsResetBtn = nullptr;
QLabel *m_thinkingValue = nullptr;
QToolButton *m_detailsToggle = nullptr;
QWidget *m_detailsBody = nullptr;
QLineEdit *m_extendsValue = nullptr;
QLineEdit *m_tagsValue = nullptr;
QLineEdit *m_endpointValue = nullptr;
QLabel *m_effectiveUrl = nullptr;
QLineEdit *m_filePatternsValue = nullptr; QLineEdit *m_filePatternsValue = nullptr;
QToolButton *m_rawToggle = nullptr; QToolButton *m_rawToggle = nullptr;
QPlainTextEdit *m_rawToml = nullptr; QPlainTextEdit *m_rawToml = nullptr;
QToolButton *m_baseRawToggle = nullptr; QToolButton *m_baseRawToggle = nullptr;
QPlainTextEdit *m_baseRawToml = nullptr; QPlainTextEdit *m_baseRawToml = nullptr;
}; };

View File

@@ -5,8 +5,9 @@
#include "AgentListItem.hpp" #include "AgentListItem.hpp"
#include "SettingsTheme.hpp" #include "SettingsTheme.hpp"
#include "TagChip.hpp" #include "SettingsUiBuilders.hpp"
#include <utils/elidinglabel.h>
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <QEvent> #include <QEvent>
@@ -16,73 +17,52 @@
#include <QMouseEvent> #include <QMouseEvent>
#include <QPalette> #include <QPalette>
#include <QScopedValueRollback> #include <QScopedValueRollback>
#include <QVBoxLayout>
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
AgentListItem::AgentListItem(const AgentConfig &cfg, QWidget *parent) AgentListItem::AgentListItem(const AgentConfig &cfg, QWidget *parent)
: QFrame(parent) : QFrame(parent)
, m_name(cfg.name) , m_name(cfg.name)
, m_model(cfg.model)
{ {
setObjectName(QStringLiteral("AgentListItem")); setObjectName(QStringLiteral("AgentListItem"));
setFrameShape(QFrame::NoFrame); setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true); setAutoFillBackground(true);
setCursor(Qt::PointingHandCursor); setCursor(Qt::PointingHandCursor);
auto *dot = new QLabel(QStringLiteral(""), this); m_nameLabel = new QLabel(this);
QFont df = dot->font(); QFont nf = m_nameLabel->font();
df.setPixelSize(10);
dot->setFont(df);
QPalette dp = dot->palette();
dp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
dot->setPalette(dp);
auto *nameLbl = new QLabel(cfg.name, this);
QFont nf = nameLbl->font();
nf.setBold(true); nf.setBold(true);
nf.setPixelSize(12); nf.setPixelSize(12);
nameLbl->setFont(nf); m_nameLabel->setFont(nf);
m_nameLabel->setTextFormat(Qt::RichText);
auto *headerRow = new QHBoxLayout; m_modelLabel = new Utils::ElidingLabel(this);
headerRow->setContentsMargins(0, 0, 0, 0); m_modelLabel->setElideMode(Qt::ElideMiddle);
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);
m_modelLabel = new QLabel(cfg.model, this);
m_modelLabel->setFont(monospaceFont(11)); m_modelLabel->setFont(monospaceFont(11));
m_modelLabel->setContentsMargins(16, 0, 0, 0); m_modelLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
QPalette mp = m_modelLabel->palette(); QPalette mp = m_modelLabel->palette();
mp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid)); mp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_modelLabel->setPalette(mp); m_modelLabel->setPalette(mp);
m_modelLabel->setText(cfg.model);
m_modelLabel->setVisible(!cfg.model.isEmpty()); m_modelLabel->setVisible(!cfg.model.isEmpty());
col->addWidget(m_modelLabel);
if (!cfg.tags.isEmpty()) { auto *row = new QHBoxLayout(this);
auto *tagsHolder = new QWidget(this); row->setContentsMargins(8, 4, 8, 4);
auto *tagsLay = new QHBoxLayout(tagsHolder); row->setSpacing(8);
tagsLay->setContentsMargins(16, 2, 0, 0); row->addWidget(m_nameLabel, 0);
tagsLay->setSpacing(3); row->addWidget(m_modelLabel, 1);
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); QStringList tipLines;
outer->setContentsMargins(5, 6, 8, 6); if (!cfg.description.isEmpty())
outer->setSpacing(0); tipLines << cfg.description;
outer->addLayout(col); if (!cfg.model.isEmpty())
tipLines << tr("Model: %1").arg(cfg.model);
if (!cfg.tags.isEmpty())
tipLines << tr("Tags: %1").arg(cfg.tags.join(QStringLiteral(", ")));
setToolTip(tipLines.join(QStringLiteral("\n")));
updateNameText();
applyTheme(); applyTheme();
} }
@@ -94,20 +74,26 @@ void AgentListItem::setSelected(bool selected)
applyTheme(); applyTheme();
} }
void AgentListItem::setActiveTags(const QSet<QString> &active)
{
for (auto *chip : m_chips)
chip->setActive(active.contains(chip->tag()));
}
void AgentListItem::setModel(const QString &model) void AgentListItem::setModel(const QString &model)
{ {
if (!m_modelLabel) m_model = model;
return;
m_modelLabel->setText(model); m_modelLabel->setText(model);
m_modelLabel->setVisible(!model.isEmpty()); m_modelLabel->setVisible(!model.isEmpty());
} }
void AgentListItem::setFilterHighlight(const QString &lowerFilter)
{
if (m_filter == lowerFilter)
return;
m_filter = lowerFilter;
updateNameText();
}
void AgentListItem::updateNameText()
{
m_nameLabel->setText(filterHighlightedHtml(m_name, m_filter));
}
void AgentListItem::mouseReleaseEvent(QMouseEvent *event) void AgentListItem::mouseReleaseEvent(QMouseEvent *event)
{ {
if (event->button() == Qt::LeftButton) if (event->button() == Qt::LeftButton)
@@ -133,6 +119,7 @@ void AgentListItem::applyTheme()
"#AgentListItem { background:transparent;" "#AgentListItem { background:transparent;"
" border-top:1px solid %1; border-left:3px solid %2; }") " border-top:1px solid %1; border-left:3px solid %2; }")
.arg(cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)), accent)); .arg(cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)), accent));
updateNameText();
} }
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

@@ -5,8 +5,6 @@
#pragma once #pragma once
#include <QFrame> #include <QFrame>
#include <QList>
#include <QSet>
#include <QString> #include <QString>
#include <AgentConfig.hpp> #include <AgentConfig.hpp>
@@ -15,9 +13,11 @@ QT_BEGIN_NAMESPACE
class QLabel; class QLabel;
QT_END_NAMESPACE QT_END_NAMESPACE
namespace QodeAssist::Settings { namespace Utils {
class ElidingLabel;
}
class TagChip; namespace QodeAssist::Settings {
class AgentListItem : public QFrame class AgentListItem : public QFrame
{ {
@@ -27,12 +27,11 @@ public:
QString agentName() const { return m_name; } QString agentName() const { return m_name; }
void setSelected(bool selected); void setSelected(bool selected);
void setActiveTags(const QSet<QString> &active);
void setModel(const QString &model); void setModel(const QString &model);
void setFilterHighlight(const QString &lowerFilter);
signals: signals:
void clicked(const QString &name); void clicked(const QString &name);
void tagClicked(const QString &tag);
protected: protected:
void mouseReleaseEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override;
@@ -40,12 +39,15 @@ protected:
private: private:
void applyTheme(); void applyTheme();
void updateNameText();
QString m_name; QString m_name;
QString m_model;
QString m_filter;
bool m_selected = false; bool m_selected = false;
bool m_inApplyTheme = false; bool m_inApplyTheme = false;
QLabel *m_modelLabel = nullptr; QLabel *m_nameLabel = nullptr;
QList<TagChip *> m_chips; Utils::ElidingLabel *m_modelLabel = nullptr;
}; };
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

@@ -19,6 +19,7 @@
#include <QEvent> #include <QEvent>
#include <QFont> #include <QFont>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMap> #include <QMap>
@@ -37,8 +38,13 @@ AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
setFrameShape(QFrame::StyledPanel); setFrameShape(QFrame::StyledPanel);
m_filterEdit = new QLineEdit(this); m_filterEdit = new QLineEdit(this);
m_filterEdit->setPlaceholderText(tr("Filter agents")); m_filterEdit->setPlaceholderText(tr("Filter by name, model, provider, tag"));
m_filterEdit->setClearButtonEnabled(true); m_filterEdit->setClearButtonEnabled(true);
m_filterEdit->setToolTip(tr("Type to filter; Up/Down moves through the list."));
m_filterEdit->installEventFilter(this);
setFocusProxy(m_filterEdit);
m_tagStrip = new TagFilterStrip(this);
auto *filterRow = new QHBoxLayout; auto *filterRow = new QHBoxLayout;
filterRow->setContentsMargins(6, 6, 6, 6); filterRow->setContentsMargins(6, 6, 6, 6);
@@ -48,8 +54,6 @@ AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
m_filterHolder->setLayout(filterRow); m_filterHolder->setLayout(filterRow);
m_filterHolder->setAutoFillBackground(true); m_filterHolder->setAutoFillBackground(true);
m_tagStrip = new TagFilterStrip(this);
m_listScroll = new QScrollArea(this); m_listScroll = new QScrollArea(this);
m_listScroll->setWidgetResizable(true); m_listScroll->setWidgetResizable(true);
m_listScroll->setFrameShape(QFrame::NoFrame); m_listScroll->setFrameShape(QFrame::NoFrame);
@@ -59,17 +63,20 @@ AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
outer->setContentsMargins(0, 0, 0, 0); outer->setContentsMargins(0, 0, 0, 0);
outer->setSpacing(0); outer->setSpacing(0);
outer->addWidget(m_filterHolder); outer->addWidget(m_filterHolder);
outer->addWidget(m_tagStrip);
outer->addWidget(m_listScroll, 1); outer->addWidget(m_listScroll, 1);
m_filterDebounce = new QTimer(this); m_filterDebounce = new QTimer(this);
m_filterDebounce->setSingleShot(true); m_filterDebounce->setSingleShot(true);
m_filterDebounce->setInterval(100); m_filterDebounce->setInterval(100);
connect(m_filterDebounce, &QTimer::timeout, this, &AgentListPane::rebuildList); connect(m_filterDebounce, &QTimer::timeout, this, &AgentListPane::rebuildList);
connect(m_filterEdit, &QLineEdit::textChanged, this, connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &) {
[this](const QString &) { m_filterDebounce->start(); }); m_filterDebounce->start();
});
connect(m_tagStrip, &TagFilterStrip::activeTagsChanged, this, connect(
m_tagStrip,
&TagFilterStrip::activeTagsChanged,
this,
[this](const QSet<QString> &) { rebuildList(); }, [this](const QSet<QString> &) { rebuildList(); },
Qt::QueuedConnection); Qt::QueuedConnection);
@@ -93,7 +100,7 @@ void AgentListPane::selectByName(const QString &name)
return; return;
if (m_factory) { if (m_factory) {
if (const AgentConfig *cfg = m_factory->configByName(name)) if (const AgentConfig *cfg = m_factory->configByName(name))
m_expandedGroups.insert(groupKey(*cfg)); m_collapsedGroups.remove(groupKey(*cfg));
} }
setCurrentNameInternal(name, false); setCurrentNameInternal(name, false);
m_notifyOnRebuild = true; m_notifyOnRebuild = true;
@@ -121,6 +128,12 @@ void AgentListPane::refresh()
rebuildList(); rebuildList();
} }
void AgentListPane::focusFilter()
{
m_filterEdit->setFocus(Qt::OtherFocusReason);
m_filterEdit->selectAll();
}
void AgentListPane::changeEvent(QEvent *event) void AgentListPane::changeEvent(QEvent *event)
{ {
QFrame::changeEvent(event); QFrame::changeEvent(event);
@@ -128,6 +141,41 @@ void AgentListPane::changeEvent(QEvent *event)
applyFilterHolderTheme(); applyFilterHolderTheme();
} }
bool AgentListPane::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_filterEdit && event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_Down) {
moveSelection(1);
return true;
}
if (ke->key() == Qt::Key_Up) {
moveSelection(-1);
return true;
}
}
return QFrame::eventFilter(watched, event);
}
void AgentListPane::moveSelection(int delta)
{
if (m_rows.isEmpty())
return;
int cur = -1;
for (int i = 0; i < m_rows.size(); ++i) {
if (m_rows.at(i)->agentName() == m_currentName) {
cur = i;
break;
}
}
int next = cur < 0 ? (delta > 0 ? 0 : int(m_rows.size()) - 1) : cur + delta;
next = qBound(0, next, int(m_rows.size()) - 1);
if (next == cur)
return;
setCurrentNameInternal(m_rows.at(next)->agentName(), true);
m_listScroll->ensureWidgetVisible(m_rows.at(next), 0, 40);
}
void AgentListPane::applyFilterHolderTheme() void AgentListPane::applyFilterHolderTheme()
{ {
if (!m_filterHolder) if (!m_filterHolder)
@@ -156,9 +204,14 @@ std::vector<const AgentConfig *> AgentListPane::visibleAgents() const
bool AgentListPane::matchesFilters(const AgentConfig &a, const QString &lowerFilter) const bool AgentListPane::matchesFilters(const AgentConfig &a, const QString &lowerFilter) const
{ {
if (!lowerFilter.isEmpty() if (!lowerFilter.isEmpty()) {
&& !(a.name + QLatin1Char(' ') + a.model).toLower().contains(lowerFilter)) const QString haystack = (a.name + QLatin1Char(' ') + a.model + QLatin1Char(' ')
+ a.providerInstance + QLatin1Char(' ')
+ a.tags.join(QLatin1Char(' ')))
.toLower();
if (!haystack.contains(lowerFilter))
return false; return false;
}
const QSet<QString> &active = m_tagStrip->activeTags(); const QSet<QString> &active = m_tagStrip->activeTags();
for (const QString &t : active) for (const QString &t : active)
if (!a.tags.contains(t)) if (!a.tags.contains(t))
@@ -199,14 +252,8 @@ void AgentListPane::rebuildList()
for (const AgentConfig *cfg : agents) { for (const AgentConfig *cfg : agents) {
auto *item = new AgentListItem(*cfg, content); auto *item = new AgentListItem(*cfg, content);
item->setSelected(cfg->name == m_currentName); item->setSelected(cfg->name == m_currentName);
item->setActiveTags(activeTags); item->setFilterHighlight(lowerFilter);
connect(item, &AgentListItem::clicked, this, &AgentListPane::onRowClicked); connect(item, &AgentListItem::clicked, this, &AgentListPane::onRowClicked);
connect(
item,
&AgentListItem::tagClicked,
this,
[this](const QString &tag) { m_tagStrip->toggleTag(tag); },
Qt::QueuedConnection);
contentLayout->addWidget(item); contentLayout->addWidget(item);
newRows.append(item); newRows.append(item);
} }
@@ -231,7 +278,7 @@ void AgentListPane::rebuildList()
const std::vector<const AgentConfig *> &group = byProvider[provider]; const std::vector<const AgentConfig *> &group = byProvider[provider];
const QString key = sectionKey + QLatin1Char('/') + provider; const QString key = sectionKey + QLatin1Char('/') + provider;
liveKeys.insert(key); liveKeys.insert(key);
const bool expanded = filtersActive || m_expandedGroups.contains(key); const bool expanded = filtersActive || !m_collapsedGroups.contains(key);
auto *header = new CollapsibleHeader(provider, int(group.size()), content); auto *header = new CollapsibleHeader(provider, int(group.size()), content);
header->setExpanded(expanded); header->setExpanded(expanded);
@@ -242,8 +289,8 @@ void AgentListPane::rebuildList()
&CollapsibleHeader::toggled, &CollapsibleHeader::toggled,
this, this,
[this, key] { [this, key] {
if (!m_expandedGroups.remove(key)) if (!m_collapsedGroups.remove(key))
m_expandedGroups.insert(key); m_collapsedGroups.insert(key);
rebuildList(); rebuildList();
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
@@ -258,7 +305,7 @@ void AgentListPane::rebuildList()
addSection(tr("User"), QStringLiteral("user"), userAgents); addSection(tr("User"), QStringLiteral("user"), userAgents);
addSection(tr("Bundled"), QStringLiteral("bundled"), bundledAgents); addSection(tr("Bundled"), QStringLiteral("bundled"), bundledAgents);
if (!filtersActive) if (!filtersActive)
m_expandedGroups.intersect(liveKeys); m_collapsedGroups.intersect(liveKeys);
if (userAgents.empty() && bundledAgents.empty()) { if (userAgents.empty() && bundledAgents.empty()) {
auto *empty = new QLabel(tr("No agents match these filters."), content); auto *empty = new QLabel(tr("No agents match these filters."), content);
empty->setAlignment(Qt::AlignCenter); empty->setAlignment(Qt::AlignCenter);
@@ -273,8 +320,7 @@ void AgentListPane::rebuildList()
m_rows = newRows; m_rows = newRows;
m_listScroll->setWidget(content); m_listScroll->setWidget(content);
const AgentConfig *current const AgentConfig *current = m_currentName.isEmpty() || !m_factory
= m_currentName.isEmpty() || !m_factory
? nullptr ? nullptr
: m_factory->configByName(m_currentName); : m_factory->configByName(m_currentName);
if (!current && !m_rows.isEmpty()) { if (!current && !m_rows.isEmpty()) {

View File

@@ -4,11 +4,11 @@
#pragma once #pragma once
#include <vector>
#include <QFrame> #include <QFrame>
#include <QList> #include <QList>
#include <QSet> #include <QSet>
#include <QString> #include <QString>
#include <vector>
#include <AgentConfig.hpp> #include <AgentConfig.hpp>
@@ -35,15 +35,19 @@ public:
QString currentName() const { return m_currentName; } QString currentName() const { return m_currentName; }
void selectByName(const QString &name); void selectByName(const QString &name);
void refresh(); void refresh();
void focusFilter();
TagFilterStrip *tagStrip() const { return m_tagStrip; }
signals: signals:
void currentAgentChanged(const QString &name); void currentAgentChanged(const QString &name);
protected: protected:
void changeEvent(QEvent *event) override; void changeEvent(QEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;
private: private:
void rebuildList(); void rebuildList();
void moveSelection(int delta);
void applyFilterHolderTheme(); void applyFilterHolderTheme();
bool matchesFilters(const AgentConfig &a, const QString &lowerFilter) const; bool matchesFilters(const AgentConfig &a, const QString &lowerFilter) const;
std::vector<const AgentConfig *> visibleAgents() const; std::vector<const AgentConfig *> visibleAgents() const;
@@ -60,7 +64,7 @@ private:
QScrollArea *m_listScroll = nullptr; QScrollArea *m_listScroll = nullptr;
QList<AgentListItem *> m_rows; QList<AgentListItem *> m_rows;
QString m_currentName; QString m_currentName;
QSet<QString> m_expandedGroups; QSet<QString> m_collapsedGroups;
bool m_notifyOnRebuild = false; bool m_notifyOnRebuild = false;
}; };

View File

@@ -1,139 +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 "AgentModelDialog.hpp"
#include "SettingsTheme.hpp"
#include <utils/theme/theme.h>
#include <Agent.hpp>
#include <AgentFactory.hpp>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPalette>
#include <QPushButton>
#include <QSignalBlocker>
#include <QVBoxLayout>
namespace QodeAssist::Settings {
AgentModelDialog::AgentModelDialog(
AgentFactory *factory, const QString &agentName, const QString &currentModel, QWidget *parent)
: QDialog(parent)
, m_factory(factory)
, m_agentName(agentName)
{
setWindowTitle(tr("Select Model"));
resize(440, 380);
m_modelEdit = new QLineEdit(currentModel, this);
m_modelEdit->setFont(monospaceFont(11));
m_modelEdit->setPlaceholderText(tr("Type a model name or pick one below"));
m_modelEdit->setClearButtonEnabled(true);
m_fetchBtn = new QPushButton(tr("Fetch available models"), this);
m_fetchBtn->setToolTip(tr("Query the agent's provider for its installed models"));
m_status = new QLabel(this);
m_status->setFont(monospaceFont(10));
QPalette sp = m_status->palette();
sp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_status->setPalette(sp);
m_list = new QListWidget(this);
m_list->setFont(monospaceFont(11));
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(m_list, &QListWidget::currentTextChanged, this, [this](const QString &text) {
if (!text.isEmpty())
m_modelEdit->setText(text);
});
connect(m_list, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *) { accept(); });
connect(m_fetchBtn, &QPushButton::clicked, this, [this] { fetchModels(); });
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *fetchRow = new QHBoxLayout;
fetchRow->setContentsMargins(0, 0, 0, 0);
fetchRow->setSpacing(8);
fetchRow->addWidget(m_fetchBtn);
fetchRow->addWidget(m_status, 1);
auto *root = new QVBoxLayout(this);
root->addWidget(new QLabel(tr("Model:"), this));
root->addWidget(m_modelEdit);
root->addLayout(fetchRow);
root->addWidget(m_list, 1);
root->addWidget(buttons);
fetchModels();
}
QString AgentModelDialog::selectedModel() const
{
return m_modelEdit->text().trimmed();
}
void AgentModelDialog::fetchModels()
{
if (!m_factory)
return;
if (m_watcher && m_watcher->isRunning())
return;
QString err;
Agent *probe = m_factory->create(m_agentName, this, &err);
if (!probe) {
m_status->setText(
err.isEmpty() ? tr("Provider is not available.")
: tr("Provider is not available: %1").arg(err));
return;
}
m_probe = probe;
if (!m_watcher) {
m_watcher = new QFutureWatcher<QList<QString>>(this);
connect(m_watcher, &QFutureWatcher<QList<QString>>::finished, this, [this] {
onModelsFetched();
});
}
m_fetchBtn->setEnabled(false);
m_status->setText(tr("Loading models…"));
m_watcher->setFuture(probe->installedModels());
}
void AgentModelDialog::onModelsFetched()
{
QList<QString> models;
if (m_watcher->future().resultCount() > 0)
models = m_watcher->result();
if (m_probe) {
m_probe->deleteLater();
m_probe.clear();
}
m_fetchBtn->setEnabled(true);
const QString keep = m_modelEdit->text();
{
QSignalBlocker block(m_list);
m_list->clear();
m_list->addItems(models);
}
m_modelEdit->setText(keep);
m_status->setText(
models.isEmpty() ? tr("No models returned — type the model name manually.")
: tr("%n model(s) available.", nullptr, static_cast<int>(models.size())));
}
} // namespace QodeAssist::Settings

View File

@@ -8,24 +8,21 @@
#include "AgentDuplicator.hpp" #include "AgentDuplicator.hpp"
#include "AgentListPane.hpp" #include "AgentListPane.hpp"
#include "SettingsConstants.hpp" #include "SettingsConstants.hpp"
#include "SettingsTheme.hpp" #include "TagFilterStrip.hpp"
#include <coreplugin/dialogs/ioptionspage.h> #include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <utils/filepath.h> #include <utils/filepath.h>
#include <utils/theme/theme.h>
#include <QDesktopServices> #include <QDesktopServices>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFont> #include <QFont>
#include <QFontMetrics>
#include <QFrame> #include <QFrame>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <QPalette>
#include <QPointer> #include <QPointer>
#include <QPushButton> #include <QPushButton>
#include <QScrollArea> #include <QScrollArea>
@@ -76,13 +73,7 @@ public:
m_reload = new QPushButton(tr("Reload from disk"), this); m_reload = new QPushButton(tr("Reload from disk"), this);
m_openUserDir = new QPushButton(tr("Open agents folder"), this); m_openUserDir = new QPushButton(tr("Open agents folder"), this);
m_openUserDir->setToolTip(QodeAssist::AgentFactory::userAgentsDir());
m_userPathLabel = new QLabel(this);
m_userPathLabel->setFont(monospaceFont(11));
QPalette mutedPal = m_userPathLabel->palette();
mutedPal.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_userPathLabel->setPalette(mutedPal);
m_userPathLabel->setMaximumWidth(260);
auto *headerRow = new QHBoxLayout; auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0); headerRow->setContentsMargins(0, 0, 0, 0);
@@ -90,7 +81,6 @@ public:
headerRow->addWidget(m_titleLabel); headerRow->addWidget(m_titleLabel);
headerRow->addStretch(1); headerRow->addStretch(1);
headerRow->addWidget(m_reload); headerRow->addWidget(m_reload);
headerRow->addWidget(m_userPathLabel);
headerRow->addWidget(m_openUserDir); headerRow->addWidget(m_openUserDir);
auto *headerSep = new QFrame(this); auto *headerSep = new QFrame(this);
@@ -98,6 +88,7 @@ public:
headerSep->setFrameShadow(QFrame::Sunken); headerSep->setFrameShadow(QFrame::Sunken);
m_listPane = new AgentListPane(m_agentFactory, this); m_listPane = new AgentListPane(m_agentFactory, this);
m_listPane->tagStrip()->setMaxColumns(8);
m_detail = new AgentDetailPane(this); m_detail = new AgentDetailPane(this);
m_detail->setInstanceFactory(m_agentFactory->instanceFactory()); m_detail->setInstanceFactory(m_agentFactory->instanceFactory());
@@ -119,6 +110,7 @@ public:
root->setSpacing(6); root->setSpacing(6);
root->addLayout(headerRow); root->addLayout(headerRow);
root->addWidget(headerSep); root->addWidget(headerSep);
root->addWidget(m_listPane->tagStrip());
root->addWidget(splitter, 1); root->addWidget(splitter, 1);
connect(m_reload, &QPushButton::clicked, this, &AgentsWidget::reloadFromDisk); connect(m_reload, &QPushButton::clicked, this, &AgentsWidget::reloadFromDisk);
@@ -153,6 +145,8 @@ public:
reloadFromDisk(); reloadFromDisk();
QTimer::singleShot(0, this, [this] { m_listPane->focusFilter(); });
if (m_navigator) { if (m_navigator) {
QTimer::singleShot(0, this, [this] { QTimer::singleShot(0, this, [this] {
if (!m_navigator) if (!m_navigator)
@@ -170,18 +164,9 @@ private:
void reloadFromDisk() void reloadFromDisk()
{ {
m_agentFactory->reload(); m_agentFactory->reload();
updateUserPathLabel();
m_listPane->refresh(); 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) void openAgentInEditor(const AgentConfig &agent)
{ {
const QString name = agent.name; const QString name = agent.name;
@@ -253,7 +238,6 @@ private:
QLabel *m_titleLabel = nullptr; QLabel *m_titleLabel = nullptr;
QPushButton *m_reload = nullptr; QPushButton *m_reload = nullptr;
QPushButton *m_openUserDir = nullptr; QPushButton *m_openUserDir = nullptr;
QLabel *m_userPathLabel = nullptr;
AgentListPane *m_listPane = nullptr; AgentListPane *m_listPane = nullptr;
QScrollArea *m_detailScroll = nullptr; QScrollArea *m_detailScroll = nullptr;

View File

@@ -24,7 +24,7 @@ add_library(QodeAssistSettings STATIC
UpdateDialog.hpp UpdateDialog.cpp UpdateDialog.hpp UpdateDialog.cpp
AgentsSettingsPage.hpp AgentsSettingsPage.cpp AgentsSettingsPage.hpp AgentsSettingsPage.cpp
AgentDetailPane.hpp AgentDetailPane.cpp AgentDetailPane.hpp AgentDetailPane.cpp
AgentModelDialog.hpp AgentModelDialog.cpp ModelSelector.hpp ModelSelector.cpp
AgentListItem.hpp AgentListItem.cpp AgentListItem.hpp AgentListItem.cpp
AgentListPane.hpp AgentListPane.cpp AgentListPane.hpp AgentListPane.cpp
AgentDuplicator.hpp AgentDuplicator.cpp AgentDuplicator.hpp AgentDuplicator.cpp

View File

@@ -8,6 +8,8 @@
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <coreplugin/idocument.h> #include <coreplugin/idocument.h>
#include <extensionsystem/pluginmanager.h>
#include <extensionsystem/pluginspec.h>
#include <utils/infolabel.h> #include <utils/infolabel.h>
#include <utils/layoutbuilder.h> #include <utils/layoutbuilder.h>
#include <QFont> #include <QFont>
@@ -43,6 +45,16 @@ namespace {
constexpr int kSaveDebounceMs = 300; constexpr int kSaveDebounceMs = 300;
QString pluginVersionText()
{
const auto pluginSpecs = ExtensionSystem::PluginManager::plugins();
for (const ExtensionSystem::PluginSpec *spec : pluginSpecs) {
if (spec->name() == QLatin1String("QodeAssist"))
return QStringLiteral("QodeAssist %1").arg(spec->version());
}
return QStringLiteral("QodeAssist");
}
class AgentPipelinesWidget : public QWidget class AgentPipelinesWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -261,7 +273,8 @@ GeneralSettings::GeneralSettings()
requestTimeout.setSettingsKey(Constants::REQUEST_TIMEOUT); requestTimeout.setSettingsKey(Constants::REQUEST_TIMEOUT);
requestTimeout.setLabelText(Tr::tr("Request timeout (seconds):")); requestTimeout.setLabelText(Tr::tr("Request timeout (seconds):"));
requestTimeout.setToolTip(Tr::tr( requestTimeout.setToolTip(
Tr::tr(
"Maximum time to wait for the model to send data before a request is aborted. " "Maximum time to wait for the model to send data before a request is aborted. "
"Applies to all requests — chat, code completion, quick refactor and chat compression. " "Applies to all requests — chat, code completion, quick refactor and chat compression. "
"The timer resets every time data is received, so this effectively limits the " "The timer resets every time data is received, so this effectively limits the "
@@ -282,9 +295,15 @@ GeneralSettings::GeneralSettings()
setLayouter([this]() { setLayouter([this]() {
using namespace Layouting; using namespace Layouting;
auto networkGroup = Group{ auto *versionLabel = new QLabel(pluginVersionText());
title(Tr::tr("Network")), versionLabel->setEnabled(false);
Column{Row{requestTimeout, Stretch{1}}}};
auto advancedGroup = Group{
title(Tr::tr("Advanced")),
Column{
Row{enableLogging, Stretch{1}},
Row{enableCheckUpdate, Stretch{1}},
Row{requestTimeout, Stretch{1}}}};
auto *supportLabel = new QLabel(Tr::tr("Support the development of QodeAssist:")); auto *supportLabel = new QLabel(Tr::tr("Support the development of QodeAssist:"));
@@ -307,16 +326,13 @@ GeneralSettings::GeneralSettings()
}; };
return Column{ return Column{
Row{supportLabel, supportLinks, Stretch{1}, checkUpdate, resetToDefaults}, Row{enableQodeAssist, Stretch{1}, versionLabel, checkUpdate, resetToDefaults},
Space{8},
Row{enableQodeAssist, Stretch{1}},
Row{enableLogging, Stretch{1}},
Row{enableCheckUpdate, Stretch{1}},
Space{8},
networkGroup,
Space{12}, Space{12},
pipelines, pipelines,
Stretch{1}}; Space{12},
advancedGroup,
Stretch{1},
Row{supportLabel, supportLinks, Stretch{1}}};
}); });
} }

176
settings/ModelSelector.cpp Normal file
View File

@@ -0,0 +1,176 @@
// 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 "ModelSelector.hpp"
#include "SettingsTheme.hpp"
#include <Agent.hpp>
#include <AgentFactory.hpp>
#include <QAbstractItemView>
#include <QCompleter>
#include <QLineEdit>
#include <QSignalBlocker>
#include <QStandardItemModel>
namespace QodeAssist::Settings {
ModelSelector::ModelSelector(QWidget *parent)
: QComboBox(parent)
{
setEditable(true);
setInsertPolicy(QComboBox::NoInsert);
setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
setMinimumContentsLength(24);
setFont(monospaceFont(11));
lineEdit()->setPlaceholderText(tr("(set a model)"));
lineEdit()->setClearButtonEnabled(true);
setEnabled(false);
auto *completer = new QCompleter(model(), this);
completer->setFilterMode(Qt::MatchContains);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setCompletionMode(QCompleter::PopupCompletion);
setCompleter(completer);
connect(this, &QComboBox::activated, this, [this](int) { submitCurrent(); });
connect(lineEdit(), &QLineEdit::returnPressed, this, [this] { submitCurrent(); });
connect(
completer,
QOverload<const QString &>::of(&QCompleter::activated),
this,
[this](const QString &text) {
setEditText(text);
submitCurrent();
},
Qt::QueuedConnection);
}
void ModelSelector::setAgent(
AgentFactory *factory,
const QString &agentName,
const QString &providerInstance,
const QString &model)
{
const bool sameTarget = factory == m_factory && agentName == m_agentName
&& providerInstance == m_providerInstance;
m_factory = factory;
m_agentName = agentName;
m_providerInstance = providerInstance;
m_model = model;
if (!sameTarget) {
m_fetched = false;
{
QSignalBlocker block(this);
clear();
}
emit statusChanged(QString());
}
{
QSignalBlocker block(this);
setCurrentIndex(findText(model));
setEditText(model);
}
setEnabled(m_factory != nullptr);
}
void ModelSelector::clearAgent()
{
m_factory = nullptr;
m_agentName.clear();
m_providerInstance.clear();
m_model.clear();
m_fetched = false;
{
QSignalBlocker block(this);
clear();
clearEditText();
}
setEnabled(false);
emit statusChanged(QString());
}
void ModelSelector::showPopup()
{
if (!m_fetched && m_factory && !(m_watcher && m_watcher->isRunning()))
startFetch();
QComboBox::showPopup();
}
void ModelSelector::startFetch()
{
QString err;
Agent *probe = m_factory->create(m_agentName, this, &err);
if (!probe) {
emit statusChanged(
err.isEmpty() ? tr("Provider is not available.")
: tr("Provider is not available: %1").arg(err));
return;
}
m_probe = probe;
m_fetchAgent = m_agentName;
m_fetchProvider = m_providerInstance;
if (!m_watcher) {
m_watcher = new QFutureWatcher<QList<QString>>(this);
connect(m_watcher, &QFutureWatcher<QList<QString>>::finished, this, [this] { onFetched(); });
}
const QString keep = currentText();
{
QSignalBlocker block(this);
clear();
addItem(tr("Loading models…"));
if (auto *itemModel = qobject_cast<QStandardItemModel *>(model()))
itemModel->item(0)->setEnabled(false);
setCurrentIndex(-1);
setEditText(keep);
}
emit statusChanged(tr("Loading models…"));
m_watcher->setFuture(probe->installedModels());
}
void ModelSelector::onFetched()
{
QList<QString> models;
if (m_watcher->future().resultCount() > 0)
models = m_watcher->result();
if (m_probe) {
m_probe->deleteLater();
m_probe.clear();
}
if (m_fetchAgent != m_agentName || m_fetchProvider != m_providerInstance)
return;
m_fetched = true;
const QString keep = currentText();
const bool popupOpen = view() && view()->isVisible();
{
QSignalBlocker block(this);
clear();
addItems(models);
setCurrentIndex(findText(keep));
setEditText(keep);
}
emit statusChanged(
models.isEmpty() ? tr("No models returned — type the model name manually.")
: tr("%n model(s) available.", nullptr, static_cast<int>(models.size())));
if (popupOpen) {
hidePopup();
QComboBox::showPopup();
}
}
void ModelSelector::submitCurrent()
{
const QString model = currentText().trimmed();
if (model.isEmpty() || model == m_model)
return;
emit modelSubmitted(model);
}
} // namespace QodeAssist::Settings

View File

@@ -4,52 +4,53 @@
#pragma once #pragma once
#include <QDialog> #include <QComboBox>
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QList> #include <QList>
#include <QPointer> #include <QPointer>
#include <QString> #include <QString>
QT_BEGIN_NAMESPACE
class QLabel;
class QLineEdit;
class QListWidget;
class QPushButton;
QT_END_NAMESPACE
namespace QodeAssist { namespace QodeAssist {
class AgentFactory;
class Agent; class Agent;
class AgentFactory;
} // namespace QodeAssist } // namespace QodeAssist
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
class AgentModelDialog : public QDialog class ModelSelector : public QComboBox
{ {
Q_OBJECT Q_OBJECT
public: public:
AgentModelDialog( explicit ModelSelector(QWidget *parent = nullptr);
void setAgent(
AgentFactory *factory, AgentFactory *factory,
const QString &agentName, const QString &agentName,
const QString &currentModel, const QString &providerInstance,
QWidget *parent = nullptr); const QString &model);
void clearAgent();
[[nodiscard]] QString selectedModel() const; signals:
void modelSubmitted(const QString &model);
void statusChanged(const QString &status);
protected:
void showPopup() override;
private: private:
void fetchModels(); void startFetch();
void onModelsFetched(); void onFetched();
void submitCurrent();
AgentFactory *m_factory = nullptr;
QString m_agentName;
QLineEdit *m_modelEdit = nullptr;
QListWidget *m_list = nullptr;
QLabel *m_status = nullptr;
QPushButton *m_fetchBtn = nullptr;
QPointer<AgentFactory> m_factory;
QPointer<Agent> m_probe; QPointer<Agent> m_probe;
QFutureWatcher<QList<QString>> *m_watcher = nullptr; QFutureWatcher<QList<QString>> *m_watcher = nullptr;
QString m_agentName;
QString m_providerInstance;
QString m_model;
QString m_fetchAgent;
QString m_fetchProvider;
bool m_fetched = false;
}; };
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

@@ -36,6 +36,9 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
nf.setPixelSize(15); nf.setPixelSize(15);
m_nameLabel->setFont(nf); m_nameLabel->setFont(nf);
m_sourceBadge = new QLabel(this);
m_sourceBadge->setVisible(false);
m_sourcePathLabel = new QLabel(this); m_sourcePathLabel = new QLabel(this);
m_sourcePathLabel->setFont(monospaceFont(11)); m_sourcePathLabel->setFont(monospaceFont(11));
QPalette spp = m_sourcePathLabel->palette(); QPalette spp = m_sourcePathLabel->palette();
@@ -66,11 +69,10 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
setEditing(false); setEditing(false);
populate(m_current, m_currentHasStoredKey); populate(m_current, m_currentHasStoredKey);
}); });
connect(m_saveBtn, &QPushButton::clicked, this, [this] { connect(m_saveBtn, &QPushButton::clicked, this, [this] { emit saveRequested(collectEdits()); });
emit saveRequested(collectEdits()); connect(m_openInEditorBtn, &QPushButton::clicked, this, [this] {
emit openInEditorRequested(m_current.sourcePath);
}); });
connect(m_openInEditorBtn, &QPushButton::clicked, this,
[this] { emit openInEditorRequested(m_current.sourcePath); });
connect(m_dupBtn, &QPushButton::clicked, this, [this] { emit duplicateRequested(); }); connect(m_dupBtn, &QPushButton::clicked, this, [this] { emit duplicateRequested(); });
connect(m_deleteBtn, &QPushButton::clicked, this, [this] { emit deleteRequested(); }); connect(m_deleteBtn, &QPushButton::clicked, this, [this] { emit deleteRequested(); });
@@ -88,6 +90,7 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
titleRow->setContentsMargins(0, 0, 0, 0); titleRow->setContentsMargins(0, 0, 0, 0);
titleRow->setSpacing(8); titleRow->setSpacing(8);
titleRow->addWidget(m_nameLabel); titleRow->addWidget(m_nameLabel);
titleRow->addWidget(m_sourceBadge, 0, Qt::AlignVCenter);
titleRow->addStretch(1); titleRow->addStretch(1);
auto *headerLeft = new QVBoxLayout; auto *headerLeft = new QVBoxLayout;
@@ -111,7 +114,7 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
m_descriptionLabel->setWordWrap(true); m_descriptionLabel->setWordWrap(true);
m_descriptionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); m_descriptionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *identitySection = new SectionBox(tr("Identity"), this); m_identitySection = new SectionBox(tr("Identity"), this);
m_nameEdit = new QLineEdit(this); m_nameEdit = new QLineEdit(this);
m_descriptionEdit = new QPlainTextEdit(this); m_descriptionEdit = new QPlainTextEdit(this);
m_descriptionEdit->setMaximumHeight(60); m_descriptionEdit->setMaximumHeight(60);
@@ -121,29 +124,12 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
identityGrid->setHorizontalSpacing(8); identityGrid->setHorizontalSpacing(8);
identityGrid->setVerticalSpacing(4); identityGrid->setVerticalSpacing(4);
FormBuilder(identityGrid).row(tr("Name:"), m_nameEdit).row(tr("Description:"), m_descriptionEdit); FormBuilder(identityGrid).row(tr("Name:"), m_nameEdit).row(tr("Description:"), m_descriptionEdit);
identitySection->bodyLayout()->addLayout(identityGrid); m_identitySection->bodyLayout()->addLayout(identityGrid);
m_identitySection->setVisible(false);
auto *endpointSection = new SectionBox(tr("Endpoint"), this); auto *configSection = new SectionBox(tr("Configuration"), this);
m_urlEdit = new QLineEdit(this); m_urlEdit = new QLineEdit(this);
m_urlEdit->setFont(monospaceFont(11)); 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 = new QLineEdit(this);
m_apiKeyEdit->setEchoMode(QLineEdit::Password); m_apiKeyEdit->setEchoMode(QLineEdit::Password);
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…")); m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
@@ -182,8 +168,7 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
emit apiKeySaveRequested(key); emit apiKeySaveRequested(key);
m_apiKeyEdit->clear(); m_apiKeyEdit->clear();
}); });
connect(m_apiKeyClearBtn, &QPushButton::clicked, this, connect(m_apiKeyClearBtn, &QPushButton::clicked, this, [this] { emit apiKeyClearRequested(); });
[this] { emit apiKeyClearRequested(); });
m_keyHint = makeHintLabel(QString{}, this); m_keyHint = makeHintLabel(QString{}, this);
auto *keyRow = new QHBoxLayout; auto *keyRow = new QHBoxLayout;
@@ -194,15 +179,24 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
keyRow->addWidget(m_apiKeySaveBtn); keyRow->addWidget(m_apiKeySaveBtn);
keyRow->addWidget(m_apiKeyClearBtn); keyRow->addWidget(m_apiKeyClearBtn);
auto *credGrid = new QGridLayout; auto *configGrid = new QGridLayout;
credGrid->setContentsMargins(0, 0, 0, 0); configGrid->setContentsMargins(0, 0, 0, 0);
credGrid->setHorizontalSpacing(8); configGrid->setHorizontalSpacing(8);
credGrid->setVerticalSpacing(4); configGrid->setVerticalSpacing(4);
FormBuilder credForm(credGrid); FormBuilder configForm(configGrid);
credForm.row(tr("API key:"), keyRow); configForm.row(tr("API key:"), keyRow);
credGrid->addWidget(m_legacyKeyBtn, credForm.currentRow(), 1, Qt::AlignLeft); {
credGrid->addWidget(m_keyHint, credForm.currentRow() + 1, 1); int row = configForm.currentRow();
credSection->bodyLayout()->addLayout(credGrid); configGrid->addWidget(m_legacyKeyBtn, row, 1, Qt::AlignLeft);
configGrid->addWidget(m_keyHint, row + 1, 1);
configForm = FormBuilder(configGrid, row + 2);
}
configForm.row(
tr("URL:"),
m_urlEdit,
tr("Base URL. Agents append their endpoint path "
"(e.g. /chat/completions) to this. Editable via Edit…"));
configSection->bodyLayout()->addLayout(configGrid);
m_launchSection = new SectionBox(tr("Launch"), this); m_launchSection = new SectionBox(tr("Launch"), this);
m_launchEmptyHint = new QLabel(this); m_launchEmptyHint = new QLabel(this);
@@ -218,12 +212,15 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
m_startBtn = new QPushButton(tr("Start"), this); m_startBtn = new QPushButton(tr("Start"), this);
m_stopBtn = new QPushButton(tr("Stop"), this); m_stopBtn = new QPushButton(tr("Stop"), this);
m_restartBtn = new QPushButton(tr("Restart"), this); m_restartBtn = new QPushButton(tr("Restart"), this);
connect(m_startBtn, &QPushButton::clicked, this, connect(m_startBtn, &QPushButton::clicked, this, [this] {
[this] { emit launchStartRequested(m_current.name); }); emit launchStartRequested(m_current.name);
connect(m_stopBtn, &QPushButton::clicked, this, });
[this] { emit launchStopRequested(m_current.name); }); connect(m_stopBtn, &QPushButton::clicked, this, [this] {
connect(m_restartBtn, &QPushButton::clicked, this, emit launchStopRequested(m_current.name);
[this] { emit launchRestartRequested(m_current.name); }); });
connect(m_restartBtn, &QPushButton::clicked, this, [this] {
emit launchRestartRequested(m_current.name);
});
auto *launchBtnRow = new QHBoxLayout; auto *launchBtnRow = new QHBoxLayout;
launchBtnRow->setContentsMargins(0, 0, 0, 0); launchBtnRow->setContentsMargins(0, 0, 0, 0);
launchBtnRow->setSpacing(6); launchBtnRow->setSpacing(6);
@@ -283,9 +280,8 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
root->addLayout(headerRow); root->addLayout(headerRow);
root->addWidget(headerSep); root->addWidget(headerSep);
root->addWidget(m_descriptionLabel); root->addWidget(m_descriptionLabel);
root->addWidget(identitySection); root->addWidget(m_identitySection);
root->addWidget(endpointSection); root->addWidget(configSection);
root->addWidget(credSection);
root->addWidget(m_launchSection); root->addWidget(m_launchSection);
root->addWidget(m_rawToggle, 0, Qt::AlignLeft); root->addWidget(m_rawToggle, 0, Qt::AlignLeft);
root->addWidget(m_rawToml); root->addWidget(m_rawToml);
@@ -302,6 +298,9 @@ void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool
const bool needsKey = !inst.apiKeyRef.isEmpty(); const bool needsKey = !inst.apiKeyRef.isEmpty();
m_nameLabel->setText(inst.name); m_nameLabel->setText(inst.name);
m_sourceBadge->setText(isUser ? tr("User") : tr("Bundled"));
m_sourceBadge->setVisible(true);
styleSourceBadge(m_sourceBadge, isUser);
m_sourcePathLabel->setText(inst.sourcePath); m_sourcePathLabel->setText(inst.sourcePath);
m_descriptionLabel->setText( m_descriptionLabel->setText(
@@ -323,7 +322,8 @@ void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool
m_keyHint->setText(tr("This provider type does not use a key.")); m_keyHint->setText(tr("This provider type does not use a key."));
} else if (hasStoredKey) { } else if (hasStoredKey) {
m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it.")); 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 " m_keyHint->setText(
tr("A key is stored. Type a new key and press Save key to "
"replace it, or Clear to erase it.")); "replace it, or Clear to erase it."));
} else { } else {
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…")); m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
@@ -341,10 +341,6 @@ void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool
m_legacyKeyBtn->setVisible(false); m_legacyKeyBtn->setVisible(false);
} }
m_samplePreview->setText(
QStringLiteral("# sample request line\nPOST %1/<agent endpoint>").arg(inst.url));
applyPreviewPalette();
m_deleteBtn->setEnabled(isUser); m_deleteBtn->setEnabled(isUser);
m_dupBtn->setEnabled(true); m_dupBtn->setEnabled(true);
m_editBtn->setVisible(isUser); m_editBtn->setVisible(isUser);
@@ -360,6 +356,7 @@ void ProviderDetailPane::clear()
{ {
m_current = {}; m_current = {};
m_nameLabel->setText(tr("Select a provider")); m_nameLabel->setText(tr("Select a provider"));
m_sourceBadge->setVisible(false);
m_sourcePathLabel->clear(); m_sourcePathLabel->clear();
m_descriptionLabel->clear(); m_descriptionLabel->clear();
m_nameEdit->clear(); m_nameEdit->clear();
@@ -373,7 +370,6 @@ void ProviderDetailPane::clear()
m_revealKeyBtn->setEnabled(false); m_revealKeyBtn->setEnabled(false);
m_legacyKeyValue.clear(); m_legacyKeyValue.clear();
m_legacyKeyBtn->setVisible(false); m_legacyKeyBtn->setVisible(false);
m_samplePreview->clear();
m_rawToml->clear(); m_rawToml->clear();
m_editBtn->setVisible(false); m_editBtn->setVisible(false);
m_dupBtn->setEnabled(false); m_dupBtn->setEnabled(false);
@@ -390,7 +386,8 @@ void ProviderDetailPane::refreshKeyStatus(bool hasStoredKey)
return; return;
if (hasStoredKey) { if (hasStoredKey) {
m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it.")); 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 " m_keyHint->setText(
tr("A key is stored. Type a new key and press Save key to "
"replace it, or Clear to erase it.")); "replace it, or Clear to erase it."));
} else { } else {
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…")); m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
@@ -412,8 +409,8 @@ void ProviderDetailPane::setLaunchState(
m_launchTerminalToggle->setVisible(hasLaunch); m_launchTerminalToggle->setVisible(hasLaunch);
if (!hasLaunch) { if (!hasLaunch) {
m_launchEmptyHint->setText(tr( m_launchEmptyHint->setText(
"No [launch] block. This provider is treated as external — " tr("No [launch] block. This provider is treated as external — "
"the plugin will not spawn or supervise any process. " "the plugin will not spawn or supervise any process. "
"Add a [launch] block to the TOML to have the plugin manage " "Add a [launch] block to the TOML to have the plugin manage "
"a local server here.")); "a local server here."));
@@ -429,22 +426,31 @@ void ProviderDetailPane::setLaunchState(
Utils::creatorColor(Utils::Theme::PanelTextColorMid).name(), Utils::creatorColor(Utils::Theme::PanelTextColorMid).name(),
tr("(detached — survives Qt Creator restart)")) tr("(detached — survives Qt Creator restart)"))
: QString(); : QString();
m_launchCmdLabel->setText( m_launchCmdLabel->setText(QStringLiteral("<b>%1</b> %2%3")
QStringLiteral("<b>%1</b> %2%3") .arg(
.arg(m_current.launch.command.toHtmlEscaped(), m_current.launch.command.toHtmlEscaped(),
m_current.launch.args.join(QLatin1Char(' ')).toHtmlEscaped(), m_current.launch.args.join(QLatin1Char(' ')).toHtmlEscaped(),
detachedNote)); detachedNote));
QString statusText; QString statusText;
switch (st) { switch (st) {
case Providers::ProviderLauncher::Idle: statusText = tr("idle"); break; case Providers::ProviderLauncher::Idle:
case Providers::ProviderLauncher::Starting: statusText = tr("starting…"); break; statusText = tr("idle");
case Providers::ProviderLauncher::Probing: statusText = tr("probing…"); break; break;
case Providers::ProviderLauncher::Ready: statusText = tr("ready"); break; case Providers::ProviderLauncher::Starting:
case Providers::ProviderLauncher::Stopping: statusText = tr("stopping…"); break; 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: case Providers::ProviderLauncher::Failed:
statusText = lastError.isEmpty() ? tr("failed") statusText = lastError.isEmpty() ? tr("failed") : tr("failed — %1").arg(lastError);
: tr("failed — %1").arg(lastError);
break; break;
} }
m_launchStatusPill->setText(statusText); m_launchStatusPill->setText(statusText);
@@ -473,8 +479,9 @@ void ProviderDetailPane::changeEvent(QEvent *event)
{ {
QWidget::changeEvent(event); QWidget::changeEvent(event);
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) { if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) {
applyPreviewPalette();
applyTerminalPalette(); applyTerminalPalette();
if (!m_current.name.isEmpty())
styleSourceBadge(m_sourceBadge, m_current.isUserSource());
} }
} }
@@ -491,6 +498,7 @@ void ProviderDetailPane::showRevealedKey(const QString &key)
void ProviderDetailPane::setEditing(bool on) void ProviderDetailPane::setEditing(bool on)
{ {
m_editing = on; m_editing = on;
m_identitySection->setVisible(on);
m_nameEdit->setReadOnly(!on); m_nameEdit->setReadOnly(!on);
m_descriptionEdit->setReadOnly(!on); m_descriptionEdit->setReadOnly(!on);
m_urlEdit->setReadOnly(!on); m_urlEdit->setReadOnly(!on);
@@ -510,15 +518,6 @@ Providers::ProviderInstance ProviderDetailPane::collectEdits() const
return out; return out;
} }
void ProviderDetailPane::applyPreviewPalette()
{
m_samplePreview->setStyleSheet(
QStringLiteral("QLabel { background:%1; border:1px solid %2; }")
.arg(
cssColor(Utils::creatorColor(Utils::Theme::BackgroundColorNormal)),
cssColor(Utils::creatorColor(Utils::Theme::SplitterColor))));
}
void ProviderDetailPane::applyTerminalPalette() void ProviderDetailPane::applyTerminalPalette()
{ {
if (!m_launchTerminal) if (!m_launchTerminal)

View File

@@ -57,7 +57,6 @@ protected:
private: private:
void setEditing(bool on); void setEditing(bool on);
Providers::ProviderInstance collectEdits() const; Providers::ProviderInstance collectEdits() const;
void applyPreviewPalette();
void applyTerminalPalette(); void applyTerminalPalette();
bool m_editing = false; bool m_editing = false;
@@ -65,6 +64,7 @@ private:
Providers::ProviderInstance m_current; Providers::ProviderInstance m_current;
QLabel *m_nameLabel = nullptr; QLabel *m_nameLabel = nullptr;
QLabel *m_sourceBadge = nullptr;
QLabel *m_sourcePathLabel = nullptr; QLabel *m_sourcePathLabel = nullptr;
QPushButton *m_editBtn = nullptr; QPushButton *m_editBtn = nullptr;
QPushButton *m_openInEditorBtn = nullptr; QPushButton *m_openInEditorBtn = nullptr;
@@ -75,11 +75,11 @@ private:
QLabel *m_descriptionLabel = nullptr; QLabel *m_descriptionLabel = nullptr;
SectionBox *m_identitySection = nullptr;
QLineEdit *m_nameEdit = nullptr; QLineEdit *m_nameEdit = nullptr;
QLabel *m_protocolLabel = nullptr; QLabel *m_protocolLabel = nullptr;
QPlainTextEdit *m_descriptionEdit = nullptr; QPlainTextEdit *m_descriptionEdit = nullptr;
QLineEdit *m_urlEdit = nullptr; QLineEdit *m_urlEdit = nullptr;
QLabel *m_samplePreview = nullptr;
QLineEdit *m_apiKeyEdit = nullptr; QLineEdit *m_apiKeyEdit = nullptr;
QToolButton *m_revealKeyBtn = nullptr; QToolButton *m_revealKeyBtn = nullptr;

View File

@@ -4,21 +4,22 @@
#include "ProviderListItem.hpp" #include "ProviderListItem.hpp"
#include <utils/elidinglabel.h>
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QMouseEvent> #include <QMouseEvent>
#include <QScopedValueRollback> #include <QScopedValueRollback>
#include <QVBoxLayout> #include <QStringList>
#include "ProviderInstance.hpp" #include "ProviderInstance.hpp"
#include "SettingsTheme.hpp" #include "SettingsTheme.hpp"
#include "SettingsUiBuilders.hpp"
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
ProviderListItem::ProviderListItem( ProviderListItem::ProviderListItem(const Providers::ProviderInstance &inst, QWidget *parent)
const Providers::ProviderInstance &inst, QWidget *parent)
: QFrame(parent) : QFrame(parent)
, m_name(inst.name) , m_name(inst.name)
{ {
@@ -27,35 +28,45 @@ ProviderListItem::ProviderListItem(
setAutoFillBackground(true); setAutoFillBackground(true);
setCursor(Qt::PointingHandCursor); setCursor(Qt::PointingHandCursor);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(6);
m_statusDot = new QLabel(QStringLiteral(""), this); m_statusDot = new QLabel(QStringLiteral(""), this);
QFont df = m_statusDot->font(); QFont df = m_statusDot->font();
df.setPixelSize(11); df.setPixelSize(11);
m_statusDot->setFont(df); m_statusDot->setFont(df);
m_statusDot->setStyleSheet(QStringLiteral("color: %1;").arg(statusColor(Status::Unknown))); m_statusDot->setStyleSheet(QStringLiteral("color: %1;").arg(statusColor(Status::Unknown)));
m_nameLabel = new QLabel(inst.name, this);
m_nameLabel = new QLabel(this);
QFont nf = m_nameLabel->font(); QFont nf = m_nameLabel->font();
nf.setBold(true); nf.setBold(true);
nf.setPixelSize(12); nf.setPixelSize(12);
m_nameLabel->setFont(nf); m_nameLabel->setFont(nf);
headerRow->addWidget(m_statusDot, 0, Qt::AlignVCenter); m_nameLabel->setTextFormat(Qt::RichText);
headerRow->addWidget(m_nameLabel, 1);
m_urlLabel = new QLabel(inst.url, this); m_urlLabel = new Utils::ElidingLabel(this);
m_urlLabel->setElideMode(Qt::ElideMiddle);
m_urlLabel->setFont(monospaceFont(10)); m_urlLabel->setFont(monospaceFont(10));
m_urlLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
QPalette up = m_urlLabel->palette(); QPalette up = m_urlLabel->palette();
up.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid)); up.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_urlLabel->setPalette(up); m_urlLabel->setPalette(up);
m_urlLabel->setContentsMargins(17, 0, 0, 0); m_urlLabel->setText(inst.url);
m_urlLabel->setVisible(!inst.url.isEmpty());
auto *outer = new QVBoxLayout(this); auto *row = new QHBoxLayout(this);
outer->setContentsMargins(5, 6, 8, 6); row->setContentsMargins(8, 4, 8, 4);
outer->setSpacing(2); row->setSpacing(6);
outer->addLayout(headerRow); row->addWidget(m_statusDot, 0, Qt::AlignVCenter);
outer->addWidget(m_urlLabel); row->addWidget(m_nameLabel, 0);
row->addWidget(m_urlLabel, 1);
QStringList tipLines;
if (!inst.description.isEmpty())
tipLines << inst.description;
tipLines << tr("API: %1").arg(inst.clientApi);
if (!inst.url.isEmpty())
tipLines << tr("URL: %1").arg(inst.url);
setToolTip(tipLines.join(QStringLiteral("\n")));
updateNameText();
applyTheme(); applyTheme();
} }
@@ -73,6 +84,19 @@ void ProviderListItem::setSelected(bool s)
applyTheme(); applyTheme();
} }
void ProviderListItem::setFilterHighlight(const QString &lowerFilter)
{
if (m_filter == lowerFilter)
return;
m_filter = lowerFilter;
updateNameText();
}
void ProviderListItem::updateNameText()
{
m_nameLabel->setText(filterHighlightedHtml(m_name, m_filter));
}
void ProviderListItem::mouseReleaseEvent(QMouseEvent *event) void ProviderListItem::mouseReleaseEvent(QMouseEvent *event)
{ {
if (event->button() == Qt::LeftButton) if (event->button() == Qt::LeftButton)
@@ -111,6 +135,7 @@ void ProviderListItem::applyTheme()
"#ProvListItem { background:transparent;" "#ProvListItem { background:transparent;"
" border-top:1px solid %1; border-left:3px solid %2; }") " border-top:1px solid %1; border-left:3px solid %2; }")
.arg(cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)), accent)); .arg(cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)), accent));
updateNameText();
} }
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

@@ -9,6 +9,10 @@
class QLabel; class QLabel;
class QMouseEvent; class QMouseEvent;
namespace Utils {
class ElidingLabel;
}
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
struct ProviderInstance; struct ProviderInstance;
} }
@@ -21,11 +25,11 @@ class ProviderListItem : public QFrame
public: public:
enum class Status : int { Unknown, Ok, Fail }; enum class Status : int { Unknown, Ok, Fail };
explicit ProviderListItem( explicit ProviderListItem(const Providers::ProviderInstance &inst, QWidget *parent = nullptr);
const Providers::ProviderInstance &inst, QWidget *parent = nullptr);
void setStatus(Status s); void setStatus(Status s);
void setSelected(bool s); void setSelected(bool s);
void setFilterHighlight(const QString &lowerFilter);
QString providerName() const { return m_name; } QString providerName() const { return m_name; }
signals: signals:
@@ -38,14 +42,16 @@ protected:
private: private:
static QString statusColor(Status s); static QString statusColor(Status s);
void applyTheme(); void applyTheme();
void updateNameText();
QString m_name; QString m_name;
QString m_filter;
Status m_status = Status::Unknown; Status m_status = Status::Unknown;
bool m_selected = false; bool m_selected = false;
bool m_inApplyTheme = false; bool m_inApplyTheme = false;
QLabel *m_statusDot = nullptr; QLabel *m_statusDot = nullptr;
QLabel *m_nameLabel = nullptr; QLabel *m_nameLabel = nullptr;
QLabel *m_urlLabel = nullptr; Utils::ElidingLabel *m_urlLabel = nullptr;
}; };
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

@@ -20,6 +20,7 @@
#include <QFrame> #include <QFrame>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QInputDialog> #include <QInputDialog>
#include <QKeyEvent>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMessageBox> #include <QMessageBox>
@@ -103,7 +104,10 @@ public:
headerSep->setFrameShadow(QFrame::Sunken); headerSep->setFrameShadow(QFrame::Sunken);
m_filterEdit = new QLineEdit(this); m_filterEdit = new QLineEdit(this);
m_filterEdit->setPlaceholderText(tr("Filter providers")); m_filterEdit->setPlaceholderText(tr("Filter by name, API, URL"));
m_filterEdit->setClearButtonEnabled(true);
m_filterEdit->setToolTip(tr("Type to filter; Up/Down moves through the list."));
m_filterEdit->installEventFilter(this);
m_listScroll = new QScrollArea(this); m_listScroll = new QScrollArea(this);
m_listScroll->setWidgetResizable(true); m_listScroll->setWidgetResizable(true);
@@ -127,32 +131,60 @@ public:
leftLay->addWidget(m_listScroll, 1); leftLay->addWidget(m_listScroll, 1);
m_detailPane = new ProviderDetailPane(this); m_detailPane = new ProviderDetailPane(this);
connect(m_detailPane, &ProviderDetailPane::saveRequested, connect(
this, &ProvidersPageWidget::onSaveEdited); m_detailPane,
connect(m_detailPane, &ProviderDetailPane::duplicateRequested, &ProviderDetailPane::saveRequested,
this, &ProvidersPageWidget::onDuplicateClicked); this,
connect(m_detailPane, &ProviderDetailPane::deleteRequested, &ProvidersPageWidget::onSaveEdited);
this, &ProvidersPageWidget::onRemoveClicked); connect(
connect(m_detailPane, &ProviderDetailPane::apiKeySaveRequested, m_detailPane,
this, &ProvidersPageWidget::onApiKeySave); &ProviderDetailPane::duplicateRequested,
connect(m_detailPane, &ProviderDetailPane::apiKeyClearRequested, this,
this, &ProvidersPageWidget::onApiKeyClear); &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( connect(
m_detailPane, m_detailPane,
&ProviderDetailPane::apiKeyRevealRequested, &ProviderDetailPane::apiKeyRevealRequested,
this, this,
&ProvidersPageWidget::onApiKeyReveal); &ProvidersPageWidget::onApiKeyReveal);
connect(m_detailPane, &ProviderDetailPane::launchStartRequested, connect(
this, &ProvidersPageWidget::onLaunchStart); m_detailPane,
connect(m_detailPane, &ProviderDetailPane::launchStopRequested, &ProviderDetailPane::launchStartRequested,
this, &ProvidersPageWidget::onLaunchStop); this,
connect(m_detailPane, &ProviderDetailPane::launchRestartRequested, &ProvidersPageWidget::onLaunchStart);
this, &ProvidersPageWidget::onLaunchRestart); connect(
connect(m_detailPane, &ProviderDetailPane::openInEditorRequested, m_detailPane,
this, [this](const QString &path) { &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(":/"))) { if (path.isEmpty() || path.startsWith(QLatin1String(":/"))) {
QMessageBox::information( QMessageBox::information(
this, tr("Open in editor"), this,
tr("Open in editor"),
tr("Bundled providers are read-only. " tr("Bundled providers are read-only. "
"Use Duplicate to create an editable user copy first.")); "Use Duplicate to create an editable user copy first."));
return; return;
@@ -160,9 +192,11 @@ public:
Core::EditorManager::openEditor(Utils::FilePath::fromString(path)); Core::EditorManager::openEditor(Utils::FilePath::fromString(path));
}); });
if (m_launcher) { if (m_launcher) {
connect(m_launcher.data(), &Providers::ProviderLauncher::stateChanged, connect(
this, [this](const QString &name, m_launcher.data(),
Providers::ProviderLauncher::State newState) { &Providers::ProviderLauncher::stateChanged,
this,
[this](const QString &name, Providers::ProviderLauncher::State newState) {
if (name == m_currentName) if (name == m_currentName)
refreshDetailLaunch(); refreshDetailLaunch();
const ProviderListItem::Status status = rowStatusFromState(newState); const ProviderListItem::Status status = rowStatusFromState(newState);
@@ -171,8 +205,11 @@ public:
row->setStatus(status); row->setStatus(status);
} }
}); });
connect(m_launcher.data(), &Providers::ProviderLauncher::bytesReceived, connect(
this, [this](const QString &name, const QByteArray &chunk) { m_launcher.data(),
&Providers::ProviderLauncher::bytesReceived,
this,
[this](const QString &name, const QByteArray &chunk) {
if (name == m_currentName) if (name == m_currentName)
m_detailPane->appendLaunchBytes(chunk); m_detailPane->appendLaunchBytes(chunk);
}); });
@@ -199,26 +236,34 @@ public:
m_filterDebounce = new QTimer(this); m_filterDebounce = new QTimer(this);
m_filterDebounce->setSingleShot(true); m_filterDebounce->setSingleShot(true);
m_filterDebounce->setInterval(100); m_filterDebounce->setInterval(100);
connect(m_filterDebounce, &QTimer::timeout, this, connect(m_filterDebounce, &QTimer::timeout, this, &ProvidersPageWidget::rebuildList);
&ProvidersPageWidget::rebuildList); connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &) {
connect(m_filterEdit, &QLineEdit::textChanged, this, m_filterDebounce->start();
[this](const QString &) { m_filterDebounce->start(); }); });
if (m_factory) { if (m_factory) {
connect(m_factory.data(), connect(
m_factory.data(),
&Providers::ProviderInstanceFactory::instancesReloaded, &Providers::ProviderInstanceFactory::instancesReloaded,
this, &ProvidersPageWidget::rebuildList); this,
&ProvidersPageWidget::rebuildList);
} }
if (m_navigator) { if (m_navigator) {
connect(m_navigator.data(), connect(
m_navigator.data(),
&ProvidersPageNavigator::selectInstanceRequested, &ProvidersPageNavigator::selectInstanceRequested,
this, &ProvidersPageWidget::selectInstance); this,
&ProvidersPageWidget::selectInstance);
} }
rebuildList(); rebuildList();
const QString pending QTimer::singleShot(0, this, [this] {
= m_navigator ? m_navigator->takePendingSelection() : QString{}; m_filterEdit->setFocus(Qt::OtherFocusReason);
m_filterEdit->selectAll();
});
const QString pending = m_navigator ? m_navigator->takePendingSelection() : QString{};
if (!pending.isEmpty()) if (!pending.isEmpty())
selectInstance(pending); selectInstance(pending);
else if (m_factory && !m_factory->instances().empty()) else if (m_factory && !m_factory->instances().empty())
@@ -227,6 +272,23 @@ public:
void apply() final {} void apply() final {}
protected:
bool eventFilter(QObject *watched, QEvent *event) override
{
if (watched == m_filterEdit && event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_Down) {
moveSelection(1);
return true;
}
if (ke->key() == Qt::Key_Up) {
moveSelection(-1);
return true;
}
}
return Core::IOptionsPageWidget::eventFilter(watched, event);
}
private slots: private slots:
void rebuildList() void rebuildList()
{ {
@@ -245,9 +307,9 @@ private slots:
auto matches = [&](const Providers::ProviderInstance &inst) { auto matches = [&](const Providers::ProviderInstance &inst) {
if (filter.isEmpty()) if (filter.isEmpty())
return true; return true;
return inst.name.toLower().contains(filter) return inst.name.toLower().contains(filter) || inst.clientApi.toLower().contains(filter)
|| inst.clientApi.toLower().contains(filter) || inst.url.toLower().contains(filter)
|| inst.url.toLower().contains(filter); || inst.description.toLower().contains(filter);
}; };
auto addSection = [&](const QString &title, bool userSection) { auto addSection = [&](const QString &title, bool userSection) {
@@ -262,17 +324,18 @@ private slots:
continue; continue;
sorted.push_back(&inst); sorted.push_back(&inst);
} }
std::sort(sorted.begin(), sorted.end(), std::sort(
[](const Providers::ProviderInstance *a, sorted.begin(),
const Providers::ProviderInstance *b) { sorted.end(),
[](const Providers::ProviderInstance *a, const Providers::ProviderInstance *b) {
return a->name.compare(b->name, Qt::CaseInsensitive) < 0; return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
}); });
int shown = 0; int shown = 0;
for (const auto *inst : sorted) { for (const auto *inst : sorted) {
auto *row = new ProviderListItem(*inst, m_listContent); auto *row = new ProviderListItem(*inst, m_listContent);
connect(row, &ProviderListItem::clicked, row->setFilterHighlight(filter);
this, &ProvidersPageWidget::selectInstance); connect(row, &ProviderListItem::clicked, this, &ProvidersPageWidget::selectInstance);
if (m_launcher) if (m_launcher)
row->setStatus(rowStatusFromLauncher(inst->name)); row->setStatus(rowStatusFromLauncher(inst->name));
m_rows.append(row); m_rows.append(row);
@@ -281,8 +344,7 @@ private slots:
} }
if (shown == 0) { if (shown == 0) {
auto *empty = new QLabel( auto *empty = new QLabel(
userSection ? tr("No user instances yet.") userSection ? tr("No user instances yet.") : tr("No bundled instances loaded."),
: tr("No bundled instances loaded."),
m_listContent); m_listContent);
empty->setContentsMargins(10, 6, 10, 6); empty->setContentsMargins(10, 6, 10, 6);
QPalette ep = empty->palette(); QPalette ep = empty->palette();
@@ -322,20 +384,24 @@ private slots:
{ {
if (!m_factory || m_currentName.isEmpty()) if (!m_factory || m_currentName.isEmpty())
return; return;
const Providers::ProviderInstance *srcPtr const Providers::ProviderInstance *srcPtr = m_factory->instanceByName(m_currentName);
= m_factory->instanceByName(m_currentName);
if (!srcPtr) if (!srcPtr)
return; return;
const Providers::ProviderInstance srcCopy = *srcPtr; const Providers::ProviderInstance srcCopy = *srcPtr;
bool ok = false; bool ok = false;
const QString name = QInputDialog::getText( const QString name = QInputDialog::getText(
this, tr("Duplicate provider"), this,
tr("Name for the new provider:"), QLineEdit::Normal, tr("Duplicate provider"),
QStringLiteral("%1 (copy)").arg(srcCopy.name), &ok); tr("Name for the new provider:"),
QLineEdit::Normal,
QStringLiteral("%1 (copy)").arg(srcCopy.name),
&ok);
if (!ok || name.trimmed().isEmpty()) if (!ok || name.trimmed().isEmpty())
return; return;
if (m_factory->instanceByName(name.trimmed())) { if (m_factory->instanceByName(name.trimmed())) {
QMessageBox::warning(this, tr("Duplicate provider"), QMessageBox::warning(
this,
tr("Duplicate provider"),
tr("An instance named '%1' already exists.").arg(name.trimmed())); tr("An instance named '%1' already exists.").arg(name.trimmed()));
return; return;
} }
@@ -346,7 +412,8 @@ private slots:
copy.overridesBundled = false; copy.overridesBundled = false;
QString writeErr; QString writeErr;
if (Providers::ProviderInstanceWriter::writeToUserDir( if (Providers::ProviderInstanceWriter::writeToUserDir(
copy, /*previousPath=*/QString{}, &writeErr).isEmpty()) { copy, /*previousPath=*/QString{}, &writeErr)
.isEmpty()) {
QMessageBox::warning(this, tr("Duplicate provider"), writeErr); QMessageBox::warning(this, tr("Duplicate provider"), writeErr);
return; return;
} }
@@ -370,8 +437,7 @@ private slots:
{ {
if (!m_factory || m_currentName.isEmpty()) if (!m_factory || m_currentName.isEmpty())
return; return;
const Providers::ProviderInstance *instPtr const Providers::ProviderInstance *instPtr = m_factory->instanceByName(m_currentName);
= m_factory->instanceByName(m_currentName);
if (!instPtr || !instPtr->isUserSource()) if (!instPtr || !instPtr->isUserSource())
return; return;
@@ -391,8 +457,8 @@ private slots:
if (QMessageBox::question(this, tr("Delete provider"), question) != QMessageBox::Yes) if (QMessageBox::question(this, tr("Delete provider"), question) != QMessageBox::Yes)
return; return;
if (!QFile::remove(sourcePath)) { if (!QFile::remove(sourcePath)) {
QMessageBox::warning(this, tr("Delete provider"), QMessageBox::warning(
tr("Failed to delete file:\n%1").arg(sourcePath)); this, tr("Delete provider"), tr("Failed to delete file:\n%1").arg(sourcePath));
return; return;
} }
if (m_secrets && !apiKeyRef.isEmpty()) if (m_secrets && !apiKeyRef.isEmpty())
@@ -418,8 +484,8 @@ private slots:
if (e.apiKeyRef.isEmpty() || (nameChanged && e.apiKeyRef == priorRef)) if (e.apiKeyRef.isEmpty() || (nameChanged && e.apiKeyRef == priorRef))
e.apiKeyRef = QStringLiteral("qodeassist/providers/%1").arg(e.name); e.apiKeyRef = QStringLiteral("qodeassist/providers/%1").arg(e.name);
const QString validation = Providers::ProviderInstance::validate( const QString validation
e, m_factory->knownClientApis()); = Providers::ProviderInstance::validate(e, m_factory->knownClientApis());
if (!validation.isEmpty()) { if (!validation.isEmpty()) {
QMessageBox::warning(this, tr("Save"), validation); QMessageBox::warning(this, tr("Save"), validation);
return; return;
@@ -427,8 +493,8 @@ private slots:
if (nameChanged) { if (nameChanged) {
const auto *clash = m_factory->instanceByName(e.name); const auto *clash = m_factory->instanceByName(e.name);
if (clash) { if (clash) {
QMessageBox::warning(this, tr("Save"), QMessageBox::warning(
tr("An instance named '%1' already exists.").arg(e.name)); this, tr("Save"), tr("An instance named '%1' already exists.").arg(e.name));
return; return;
} }
const QStringList referencing = agentsReferencing(priorName); const QStringList referencing = agentsReferencing(priorName);
@@ -449,20 +515,21 @@ private slots:
} }
const QString softWarning = Providers::ProviderInstance::warnings(e); const QString softWarning = Providers::ProviderInstance::warnings(e);
if (!softWarning.isEmpty()) { if (!softWarning.isEmpty()) {
if (QMessageBox::warning(this, tr("Save"), if (QMessageBox::warning(
softWarning + QStringLiteral("\n\n") this,
+ tr("Save anyway?"), tr("Save"),
softWarning + QStringLiteral("\n\n") + tr("Save anyway?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) QMessageBox::No)
!= QMessageBox::Yes) != QMessageBox::Yes)
return; return;
} }
const QString previousPath const QString previousPath = (prior && prior->isUserSource()) ? prior->sourcePath
= (prior && prior->isUserSource()) ? prior->sourcePath : QString{}; : QString{};
QString writeErr; QString writeErr;
const QString writtenPath = Providers::ProviderInstanceWriter::writeToUserDir( const QString writtenPath
e, previousPath, &writeErr); = Providers::ProviderInstanceWriter::writeToUserDir(e, previousPath, &writeErr);
if (writtenPath.isEmpty()) { if (writtenPath.isEmpty()) {
QMessageBox::warning(this, tr("Save"), writeErr); QMessageBox::warning(this, tr("Save"), writeErr);
return; return;
@@ -472,7 +539,8 @@ private slots:
!= QFileInfo(previousPath).absoluteFilePath()) { != QFileInfo(previousPath).absoluteFilePath()) {
if (!QFile::remove(previousPath)) { if (!QFile::remove(previousPath)) {
QMessageBox::warning( QMessageBox::warning(
this, tr("Save"), this,
tr("Save"),
tr("Saved to:\n%1\n\nbut could not remove the old file:\n%2\n\n" 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 " "Two provider files now describe this instance — delete the "
"old file manually to avoid a duplicate-name error.") "old file manually to avoid a duplicate-name error.")
@@ -515,15 +583,13 @@ private slots:
{ {
if (!m_factory || !m_secrets || m_currentName.isEmpty()) if (!m_factory || !m_secrets || m_currentName.isEmpty())
return; return;
const Providers::ProviderInstance *instPtr const Providers::ProviderInstance *instPtr = m_factory->instanceByName(m_currentName);
= m_factory->instanceByName(m_currentName);
if (!instPtr || instPtr->apiKeyRef.isEmpty()) if (!instPtr || instPtr->apiKeyRef.isEmpty())
return; return;
const QString instName = instPtr->name; const QString instName = instPtr->name;
const QString apiKeyRef = instPtr->apiKeyRef; const QString apiKeyRef = instPtr->apiKeyRef;
if (QMessageBox::question( if (QMessageBox::question(
this, tr("Clear API key"), this, tr("Clear API key"), tr("Erase the stored API key for '%1'?").arg(instName))
tr("Erase the stored API key for '%1'?").arg(instName))
!= QMessageBox::Yes) != QMessageBox::Yes)
return; return;
m_secrets->eraseKey(apiKeyRef); m_secrets->eraseKey(apiKeyRef);
@@ -558,6 +624,25 @@ private slots:
} }
private: private:
void moveSelection(int delta)
{
if (m_rows.isEmpty())
return;
int cur = -1;
for (int i = 0; i < m_rows.size(); ++i) {
if (m_rows.at(i)->providerName() == m_currentName) {
cur = i;
break;
}
}
int next = cur < 0 ? (delta > 0 ? 0 : int(m_rows.size()) - 1) : cur + delta;
next = qBound(0, next, int(m_rows.size()) - 1);
if (next == cur)
return;
selectInstance(m_rows.at(next)->providerName());
m_listScroll->ensureWidgetVisible(m_rows.at(next), 0, 40);
}
void populateDetail(const QString &name) void populateDetail(const QString &name)
{ {
if (!m_factory) if (!m_factory)
@@ -567,14 +652,13 @@ private:
m_detailPane->clear(); m_detailPane->clear();
return; return;
} }
const bool hasStoredKey const bool hasStoredKey = m_secrets && !inst->apiKeyRef.isEmpty()
= m_secrets && !inst->apiKeyRef.isEmpty() && m_secrets->hasKey(inst->apiKeyRef); && m_secrets->hasKey(inst->apiKeyRef);
m_detailPane->populate(*inst, hasStoredKey); m_detailPane->populate(*inst, hasStoredKey);
if (m_launcher) { if (m_launcher) {
m_detailPane->setLaunchState( m_detailPane
m_launcher->state(inst->name), ->setLaunchState(m_launcher->state(inst->name), m_launcher->lastError(inst->name));
m_launcher->lastError(inst->name));
m_detailPane->resetLaunchTerminal(m_launcher->scrollback(inst->name)); m_detailPane->resetLaunchTerminal(m_launcher->scrollback(inst->name));
} else { } else {
m_detailPane->setLaunchState(Providers::ProviderLauncher::Idle, {}); m_detailPane->setLaunchState(Providers::ProviderLauncher::Idle, {});
@@ -607,13 +691,11 @@ private:
{ {
if (!m_launcher || m_currentName.isEmpty()) if (!m_launcher || m_currentName.isEmpty())
return; return;
m_detailPane->setLaunchState( m_detailPane
m_launcher->state(m_currentName), ->setLaunchState(m_launcher->state(m_currentName), m_launcher->lastError(m_currentName));
m_launcher->lastError(m_currentName));
} }
static ProviderListItem::Status rowStatusFromState( static ProviderListItem::Status rowStatusFromState(Providers::ProviderLauncher::State state)
Providers::ProviderLauncher::State state)
{ {
switch (state) { switch (state) {
case Providers::ProviderLauncher::Ready: case Providers::ProviderLauncher::Ready:

View File

@@ -28,6 +28,49 @@ void applyMutedSmallCaps(QLabel *label)
label->setPalette(p); label->setPalette(p);
} }
QString filterHighlightedHtml(const QString &text, const QString &lowerFilter)
{
if (lowerFilter.isEmpty())
return text.toHtmlEscaped();
QColor mark = Utils::creatorColor(Utils::Theme::TextColorLink);
mark.setAlphaF(0.30);
const QString markCss
= QStringLiteral("background-color:%1;border-radius:2px;").arg(cssColor(mark));
const QString lowerText = text.toLower();
QString out;
int pos = 0;
while (true) {
const int hit = lowerText.indexOf(lowerFilter, pos);
if (hit < 0) {
out += text.mid(pos).toHtmlEscaped();
break;
}
out += text.mid(pos, hit - pos).toHtmlEscaped();
out += QStringLiteral("<span style=\"%1\">%2</span>")
.arg(markCss, text.mid(hit, lowerFilter.size()).toHtmlEscaped());
pos = hit + int(lowerFilter.size());
}
return out;
}
void styleSourceBadge(QLabel *label, bool user)
{
QFont f = label->font();
f.setBold(true);
f.setPixelSize(10);
label->setFont(f);
const QColor accent = Utils::creatorColor(
user ? Utils::Theme::TextColorLink : Utils::Theme::PanelTextColorMid);
QColor bg = accent;
bg.setAlphaF(0.12);
QColor border = accent;
border.setAlphaF(0.45);
label->setStyleSheet(QStringLiteral(
"QLabel { color:%1; background:%2; border:1px solid %3;"
" border-radius:8px; padding:1px 8px; }")
.arg(cssColor(accent), cssColor(bg), cssColor(border)));
}
QLabel *makeSectionHeader(const QString &title, QWidget *parent) QLabel *makeSectionHeader(const QString &title, QWidget *parent)
{ {
auto *header = new QLabel(title.toUpper(), parent); auto *header = new QLabel(title.toUpper(), parent);

View File

@@ -16,6 +16,10 @@ namespace QodeAssist::Settings {
void applyMutedSmallCaps(QLabel *label); void applyMutedSmallCaps(QLabel *label);
void styleSourceBadge(QLabel *label, bool user);
QString filterHighlightedHtml(const QString &text, const QString &lowerFilter);
QLabel *makeSectionHeader(const QString &title, QWidget *parent); QLabel *makeSectionHeader(const QString &title, QWidget *parent);
QLabel *makeHintLabel(const QString &text, QWidget *parent = nullptr); QLabel *makeHintLabel(const QString &text, QWidget *parent = nullptr);

View File

@@ -58,6 +58,15 @@ void TagFilterStrip::setAvailableTags(
emit activeTagsChanged(m_activeTags); emit activeTagsChanged(m_activeTags);
} }
void TagFilterStrip::setMaxColumns(int columns)
{
const int clamped = qMax(1, columns);
if (clamped == m_maxColumns)
return;
m_maxColumns = clamped;
rebuild();
}
void TagFilterStrip::setVisibleCounts(const QMap<QString, int> &countsByTag) void TagFilterStrip::setVisibleCounts(const QMap<QString, int> &countsByTag)
{ {
for (auto it = m_chipByTag.cbegin(); it != m_chipByTag.cend(); ++it) for (auto it = m_chipByTag.cbegin(); it != m_chipByTag.cend(); ++it)
@@ -161,14 +170,12 @@ void TagFilterStrip::rebuild()
sorted.reserve(m_counts.size()); sorted.reserve(m_counts.size());
for (auto it = m_counts.cbegin(); it != m_counts.cend(); ++it) for (auto it = m_counts.cbegin(); it != m_counts.cend(); ++it)
sorted.emplace_back(it.key(), it.value()); sorted.emplace_back(it.key(), it.value());
std::sort(sorted.begin(), sorted.end(), std::sort(sorted.begin(), sorted.end(), [](const auto &a, const auto &b) {
[](const auto &a, const auto &b) {
if (a.second != b.second) if (a.second != b.second)
return a.second > b.second; return a.second > b.second;
return a.first.localeAwareCompare(b.first) < 0; return a.first.localeAwareCompare(b.first) < 0;
}); });
constexpr int kMaxColumns = 3;
auto *gridHost = new QWidget(this); auto *gridHost = new QWidget(this);
auto *grid = new QGridLayout(gridHost); auto *grid = new QGridLayout(gridHost);
grid->setContentsMargins(0, 0, 0, 0); grid->setContentsMargins(0, 0, 0, 0);
@@ -181,7 +188,7 @@ void TagFilterStrip::rebuild()
connect(chip, &TagChip::clicked, this, &TagFilterStrip::toggleTag); connect(chip, &TagChip::clicked, this, &TagFilterStrip::toggleTag);
grid->addWidget(chip, gridRow, col, Qt::AlignLeft); grid->addWidget(chip, gridRow, col, Qt::AlignLeft);
m_chipByTag.insert(tag, chip); m_chipByTag.insert(tag, chip);
if (++col >= kMaxColumns) { if (++col >= m_maxColumns) {
col = 0; col = 0;
++gridRow; ++gridRow;
} }

View File

@@ -26,6 +26,7 @@ public:
void setAvailableTags(const QMap<QString, int> &countsByTag); void setAvailableTags(const QMap<QString, int> &countsByTag);
void setAvailableTags(const QMap<QString, int> &countsByTag, const QSet<QString> &activeTags); void setAvailableTags(const QMap<QString, int> &countsByTag, const QSet<QString> &activeTags);
void setVisibleCounts(const QMap<QString, int> &countsByTag); void setVisibleCounts(const QMap<QString, int> &countsByTag);
void setMaxColumns(int columns);
const QSet<QString> &activeTags() const { return m_activeTags; } const QSet<QString> &activeTags() const { return m_activeTags; }
void toggleTag(const QString &tag); void toggleTag(const QString &tag);
@@ -42,6 +43,7 @@ private:
QMap<QString, int> m_counts; QMap<QString, int> m_counts;
QSet<QString> m_activeTags; QSet<QString> m_activeTags;
int m_maxColumns = 3;
QVBoxLayout *m_layout = nullptr; QVBoxLayout *m_layout = nullptr;
QHash<QString, TagChip *> m_chipByTag; QHash<QString, TagChip *> m_chipByTag;
QLabel *m_clearLink = nullptr; QLabel *m_clearLink = nullptr;

View File

@@ -7,6 +7,7 @@
#include "Pill.hpp" #include "Pill.hpp"
#include "PipelinesConfig.hpp" #include "PipelinesConfig.hpp"
#include "SettingsTr.hpp" #include "SettingsTr.hpp"
#include "SettingsUiBuilders.hpp"
#include "TagFilterStrip.hpp" #include "TagFilterStrip.hpp"
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
@@ -19,9 +20,11 @@
#include <QEvent> #include <QEvent>
#include <QFont> #include <QFont>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QKeyEvent>
#include <QLineEdit> #include <QLineEdit>
#include <QMap> #include <QMap>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPointer>
#include <QPushButton> #include <QPushButton>
#include <QScopedValueRollback> #include <QScopedValueRollback>
#include <QScrollArea> #include <QScrollArea>
@@ -64,6 +67,11 @@ bool ListRowCard::hasAllTags(const QSet<QString> &activeTags) const
return true; return true;
} }
void ListRowCard::setFilterHighlight(const QString &lowerNeedle)
{
Q_UNUSED(lowerNeedle)
}
void ListRowCard::buildSearchHaystack(const QStringList &parts) void ListRowCard::buildSearchHaystack(const QStringList &parts)
{ {
m_searchHaystack = parts.join(QLatin1Char(' ')).toLower(); m_searchHaystack = parts.join(QLatin1Char(' ')).toLower();
@@ -121,9 +129,8 @@ void ListRowCard::applyTheme()
} else if (m_hover) { } else if (m_hover) {
bg = t.hoverBg; bg = t.hoverBg;
} }
setStyleSheet(QStringLiteral( setStyleSheet(
"#ListRowCard { background-color: %1; border: 1px solid %2; }") QStringLiteral("#ListRowCard { background-color: %1; border: 1px solid %2; }").arg(bg, bd));
.arg(bg, bd));
} }
AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent) AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
@@ -136,19 +143,25 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
haystack += cfg.tags; haystack += cfg.tags;
buildSearchHaystack(haystack); buildSearchHaystack(haystack);
auto *name = new QLabel(cfg.name, this); auto *name = new QLabel(cfg.name.toHtmlEscaped(), this);
QFont nameFont = name->font(); QFont nameFont = name->font();
nameFont.setBold(true); nameFont.setBold(true);
nameFont.setPixelSize(13); nameFont.setPixelSize(13);
name->setFont(nameFont); name->setFont(nameFont);
name->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); name->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
name->setTextFormat(Qt::RichText);
m_nameLabel = name;
QLabel *model = nullptr; QLabel *model = nullptr;
if (!cfg.model.isEmpty()) { if (!cfg.model.isEmpty()) {
model = new QLabel(QStringLiteral("· %1").arg(cfg.model), this); model = new QLabel(cfg.model.toHtmlEscaped(), this);
model->setFont(CardStyle::monoFont(11)); model->setFont(CardStyle::monoFont(11));
model->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); model->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
model->setMinimumWidth(0); model->setMinimumWidth(0);
model->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
model->setTextFormat(Qt::RichText);
m_modelLabel = model;
m_model = cfg.model;
} }
Pill *sourcePill = nullptr; Pill *sourcePill = nullptr;
@@ -156,47 +169,21 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
sourcePill = new Pill(Pill::User, Tr::tr("User"), this); sourcePill = new Pill(Pill::User, Tr::tr("User"), this);
} }
auto *description = new QLabel(this);
description->setWordWrap(false);
QFont descFont = description->font();
descFont.setItalic(true);
description->setFont(descFont);
description->setText(cfg.description.isEmpty()
? Tr::tr("No description provided.")
: cfg.description);
description->setMinimumWidth(0);
description->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
description->setTextInteractionFlags(Qt::NoTextInteraction);
QStringList endpointParts;
if (!cfg.endpoint.isEmpty())
endpointParts << cfg.endpoint;
endpointParts << (cfg.enableThinking ? Tr::tr("thinking") : Tr::tr("no-thinking"));
endpointParts << (cfg.enableTools ? Tr::tr("tools") : Tr::tr("no-tools"));
auto *endpoint = new QLabel(endpointParts.join(QStringLiteral(" · ")), this);
endpoint->setFont(CardStyle::monoFont(11));
endpoint->setMinimumWidth(0);
endpoint->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
endpoint->setTextInteractionFlags(Qt::NoTextInteraction);
auto *headerRow = new QHBoxLayout; auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0); headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(6); headerRow->setSpacing(6);
headerRow->addWidget(name); headerRow->addWidget(name);
if (sourcePill)
headerRow->addWidget(sourcePill);
if (model) if (model)
headerRow->addWidget(model, 1); headerRow->addWidget(model, 1);
else else
headerRow->addStretch(1); headerRow->addStretch(1);
if (sourcePill)
headerRow->addWidget(sourcePill);
auto *outer = new QVBoxLayout(this); auto *outer = new QVBoxLayout(this);
outer->setContentsMargins(10, 8, 10, 8); outer->setContentsMargins(8, 5, 8, 5);
outer->setSpacing(3); outer->setSpacing(2);
outer->addLayout(headerRow); outer->addLayout(headerRow);
outer->addWidget(description);
outer->addWidget(endpoint);
if (!cfg.tags.isEmpty()) { if (!cfg.tags.isEmpty()) {
constexpr int kMaxTagPills = 4; constexpr int kMaxTagPills = 4;
@@ -207,10 +194,8 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
for (int i = 0; i < std::min(tagCount, kMaxTagPills); ++i) for (int i = 0; i < std::min(tagCount, kMaxTagPills); ++i)
tagsRow->addWidget(new Pill(Pill::Tag, cfg.tags.at(i), this)); tagsRow->addWidget(new Pill(Pill::Tag, cfg.tags.at(i), this));
if (tagCount > kMaxTagPills) { if (tagCount > kMaxTagPills) {
auto *more = new Pill( auto *more
Pill::Tag, = new Pill(Pill::Tag, QStringLiteral("+%1").arg(tagCount - kMaxTagPills), this);
QStringLiteral("+%1").arg(tagCount - kMaxTagPills),
this);
more->setToolTip(cfg.tags.mid(kMaxTagPills).join(QStringLiteral(", "))); more->setToolTip(cfg.tags.mid(kMaxTagPills).join(QStringLiteral(", ")));
tagsRow->addWidget(more); tagsRow->addWidget(more);
} }
@@ -218,32 +203,45 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
outer->addLayout(tagsRow); outer->addLayout(tagsRow);
} }
const auto t = CardStyle::toneFor(CardStyle::isDark(palette()));
QPalette descPal = description->palette();
descPal.setColor(QPalette::WindowText,
QColor(cfg.description.isEmpty() ? t.textFaint : t.textSoft));
description->setPalette(descPal);
QPalette endPal = endpoint->palette();
endPal.setColor(QPalette::WindowText, QColor(t.textFaint));
endpoint->setPalette(endPal);
if (model) { if (model) {
const auto t = CardStyle::toneFor(CardStyle::isDark(palette()));
QPalette mp = model->palette(); QPalette mp = model->palette();
mp.setColor(QPalette::WindowText, QColor(t.textMute)); mp.setColor(QPalette::WindowText, QColor(t.textMute));
model->setPalette(mp); model->setPalette(mp);
} }
QStringList capabilityParts;
if (!cfg.endpoint.isEmpty())
capabilityParts << cfg.endpoint;
capabilityParts << (cfg.enableThinking ? Tr::tr("thinking") : Tr::tr("no-thinking"));
capabilityParts << (cfg.enableTools ? Tr::tr("tools") : Tr::tr("no-tools"));
QString tooltip; QString tooltip;
if (!cfg.description.isEmpty()) if (!cfg.description.isEmpty())
tooltip += cfg.description + QStringLiteral("\n\n"); tooltip += cfg.description + QStringLiteral("\n\n");
if (!cfg.model.isEmpty())
tooltip += Tr::tr("Model: %1\n").arg(cfg.model);
if (!cfg.providerInstance.isEmpty()) if (!cfg.providerInstance.isEmpty())
tooltip += Tr::tr("Provider instance: %1\n").arg(cfg.providerInstance); tooltip += Tr::tr("Provider instance: %1\n").arg(cfg.providerInstance);
if (!cfg.systemPrompt.isEmpty()) if (!cfg.systemPrompt.isEmpty())
tooltip += Tr::tr("System prompt: %1\n").arg(cfg.systemPrompt); tooltip += Tr::tr("System prompt: %1\n").arg(cfg.systemPrompt);
if (!cfg.endpoint.isEmpty()) tooltip += capabilityParts.join(QStringLiteral(" · "));
tooltip += Tr::tr("Endpoint: %1\n").arg(cfg.endpoint); if (!cfg.tags.isEmpty())
tooltip += QStringLiteral("\n")
+ Tr::tr("Tags: %1").arg(cfg.tags.join(QStringLiteral(", ")));
setToolTip(tooltip.trimmed()); setToolTip(tooltip.trimmed());
} }
void AgentRowCard::setFilterHighlight(const QString &lowerNeedle)
{
if (m_lowerNeedle == lowerNeedle)
return;
m_lowerNeedle = lowerNeedle;
m_nameLabel->setText(filterHighlightedHtml(itemName(), lowerNeedle));
if (m_modelLabel)
m_modelLabel->setText(filterHighlightedHtml(m_model, lowerNeedle));
}
ProviderSection::ProviderSection(const QString &name, QWidget *parent) ProviderSection::ProviderSection(const QString &name, QWidget *parent)
: QWidget(parent) : QWidget(parent)
{ {
@@ -297,10 +295,12 @@ void ProviderSection::addCard(ListRowCard *card)
int ProviderSection::applyFilter(const QString &needle, const QSet<QString> &activeTags) int ProviderSection::applyFilter(const QString &needle, const QSet<QString> &activeTags)
{ {
const QString lowerNeedle = needle.toLower();
int visible = 0; int visible = 0;
for (auto *card : m_cards) { for (auto *card : m_cards) {
const bool show = card->matches(needle) && card->hasAllTags(activeTags); const bool show = card->matches(needle) && card->hasAllTags(activeTags);
card->setVisible(show); card->setVisible(show);
card->setFilterHighlight(lowerNeedle);
if (show) if (show)
++visible; ++visible;
} }
@@ -350,9 +350,10 @@ AgentSelectionDialog::AgentSelectionDialog(
m_localConfigs = configs; m_localConfigs = configs;
m_filter = new QLineEdit(this); m_filter = new QLineEdit(this);
m_filter->setPlaceholderText( m_filter->setPlaceholderText(Tr::tr("Filter by name, provider, model, template, description…"));
Tr::tr("Filter by name, provider, model, template, description…"));
m_filter->setClearButtonEnabled(true); m_filter->setClearButtonEnabled(true);
m_filter->setToolTip(Tr::tr("Type to filter; Up/Down moves through the list."));
m_filter->installEventFilter(this);
auto *topRow = new QHBoxLayout; auto *topRow = new QHBoxLayout;
topRow->setContentsMargins(0, 0, 0, 0); topRow->setContentsMargins(0, 0, 0, 0);
@@ -360,6 +361,7 @@ AgentSelectionDialog::AgentSelectionDialog(
topRow->addWidget(m_filter, 1); topRow->addWidget(m_filter, 1);
m_tagStrip = new TagFilterStrip(this); m_tagStrip = new TagFilterStrip(this);
m_tagStrip->setMaxColumns(6);
const bool dark = CardStyle::isDark(palette()); const bool dark = CardStyle::isDark(palette());
const auto tone = CardStyle::toneFor(dark); const auto tone = CardStyle::toneFor(dark);
@@ -389,8 +391,7 @@ AgentSelectionDialog::AgentSelectionDialog(
m_scroll->setFrameShape(QFrame::StyledPanel); m_scroll->setFrameShape(QFrame::StyledPanel);
m_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
auto *buttons auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
m_okButton = buttons->button(QDialogButtonBox::Ok); m_okButton = buttons->button(QDialogButtonBox::Ok);
m_okButton->setText(isChange ? Tr::tr("Change") : Tr::tr("Add")); m_okButton->setText(isChange ? Tr::tr("Change") : Tr::tr("Add"));
m_okButton->setEnabled(false); m_okButton->setEnabled(false);
@@ -442,6 +443,59 @@ void AgentSelectionDialog::setAllExpanded(bool expanded)
section->setExpanded(expanded); section->setExpanded(expanded);
} }
bool AgentSelectionDialog::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_filter && event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_Down) {
moveSelection(1);
return true;
}
if (ke->key() == Qt::Key_Up) {
moveSelection(-1);
return true;
}
}
return QDialog::eventFilter(watched, event);
}
QList<ListRowCard *> AgentSelectionDialog::filteredCards() const
{
const QString needle = m_filter ? m_filter->text().trimmed() : QString();
const QSet<QString> activeTags = m_tagStrip ? m_tagStrip->activeTags() : QSet<QString>();
QList<ListRowCard *> out;
for (auto *section : m_sections)
for (auto *card : section->cards())
if (card->matches(needle) && card->hasAllTags(activeTags))
out.append(card);
return out;
}
void AgentSelectionDialog::moveSelection(int delta)
{
const QList<ListRowCard *> cards = filteredCards();
if (cards.isEmpty())
return;
const int cur = m_currentCard ? int(cards.indexOf(m_currentCard)) : -1;
int next = cur < 0 ? (delta > 0 ? 0 : int(cards.size()) - 1) : cur + delta;
next = qBound(0, next, int(cards.size()) - 1);
if (next == cur)
return;
ListRowCard *card = cards.at(next);
for (auto *section : m_sections) {
if (section->cards().contains(card)) {
section->setExpanded(true);
break;
}
}
selectCard(card);
QPointer<ListRowCard> guarded(card);
QTimer::singleShot(0, this, [this, guarded] {
if (guarded)
m_scroll->ensureWidgetVisible(guarded, 0, 40);
});
}
void AgentSelectionDialog::selectCard(ListRowCard *card) void AgentSelectionDialog::selectCard(ListRowCard *card)
{ {
if (m_currentCard == card) if (m_currentCard == card)
@@ -467,8 +521,7 @@ void AgentSelectionDialog::rebuild(const QString &currentName)
if (m_okButton) if (m_okButton)
m_okButton->setEnabled(false); m_okButton->setEnabled(false);
const auto &configs const auto &configs = m_agentFactory ? m_agentFactory->configs() : m_localConfigs;
= m_agentFactory ? m_agentFactory->configs() : m_localConfigs;
auto *content = new QWidget; auto *content = new QWidget;
auto *contentLayout = new QVBoxLayout(content); auto *contentLayout = new QVBoxLayout(content);
@@ -479,8 +532,7 @@ void AgentSelectionDialog::rebuild(const QString &currentName)
for (const auto &cfg : configs) { for (const auto &cfg : configs) {
if (cfg.hidden) if (cfg.hidden)
continue; continue;
const QString key = cfg.providerInstance.isEmpty() const QString key = cfg.providerInstance.isEmpty() ? Tr::tr("(Unknown provider instance)")
? Tr::tr("(Unknown provider instance)")
: cfg.providerInstance; : cfg.providerInstance;
byProvider[key].push_back(&cfg); byProvider[key].push_back(&cfg);
} }
@@ -491,12 +543,13 @@ void AgentSelectionDialog::rebuild(const QString &currentName)
for (auto it = byProvider.cbegin(); it != byProvider.cend(); ++it) { for (auto it = byProvider.cbegin(); it != byProvider.cend(); ++it) {
auto *section = new ProviderSection(it.key()); auto *section = new ProviderSection(it.key());
auto sortedConfigs = it.value(); auto sortedConfigs = it.value();
std::sort(sortedConfigs.begin(), sortedConfigs.end(), std::sort(
sortedConfigs.begin(),
sortedConfigs.end(),
[](const AgentConfig *a, const AgentConfig *b) { return a->name < b->name; }); [](const AgentConfig *a, const AgentConfig *b) { return a->name < b->name; });
for (const AgentConfig *cfg : sortedConfigs) { for (const AgentConfig *cfg : sortedConfigs) {
auto *card = new AgentRowCard(*cfg); auto *card = new AgentRowCard(*cfg);
connect(card, &ListRowCard::clicked, this, connect(card, &ListRowCard::clicked, this, [this, card]() { selectCard(card); });
[this, card]() { selectCard(card); });
connect(card, &ListRowCard::activated, this, [this, card]() { connect(card, &ListRowCard::activated, this, [this, card]() {
selectCard(card); selectCard(card);
accept(); accept();
@@ -554,6 +607,8 @@ void AgentSelectionDialog::applyFilters()
section->setExpanded(visible > 0); section->setExpanded(visible > 0);
total += visible; total += visible;
} }
if (m_currentCard && (!m_currentCard->matches(needle) || !m_currentCard->hasAllTags(activeTags)))
selectCard(nullptr);
if (m_emptyLabel) if (m_emptyLabel)
m_emptyLabel->setVisible(total == 0); m_emptyLabel->setVisible(total == 0);
if (m_resultCount) if (m_resultCount)

View File

@@ -50,16 +50,23 @@ inline bool isDark(const QPalette &p)
inline Tone toneFor(bool dark) inline Tone toneFor(bool dark)
{ {
return dark return dark
? Tone{"#333333", "#3a3a3a", "#2a4566", "#3a6fb7", "#4a4a4a", ? Tone{"#333333", "#3a3a3a", "#2a4566", "#3a6fb7", "#4a4a4a", "#c2c2c2", "#9a9a9a", "#7a7a7a"}
"#c2c2c2", "#9a9a9a", "#7a7a7a"} : Tone{
: Tone{"#f6f6f6", "#ececec", "#cfe1f7", "#3a6fb7", "#bdbdbd", "#f6f6f6",
"#3a3a3a", "#5a5a5a", "#8a8a8a"}; "#ececec",
"#cfe1f7",
"#3a6fb7",
"#bdbdbd",
"#3a3a3a",
"#5a5a5a",
"#8a8a8a"};
} }
inline QFont monoFont(int pixelSize) inline QFont monoFont(int pixelSize)
{ {
QFont f; QFont f;
f.setFamilies({QStringLiteral("SF Mono"), f.setFamilies(
{QStringLiteral("SF Mono"),
QStringLiteral("Cascadia Code"), QStringLiteral("Cascadia Code"),
QStringLiteral("Consolas"), QStringLiteral("Consolas"),
QStringLiteral("Liberation Mono"), QStringLiteral("Liberation Mono"),
@@ -94,6 +101,7 @@ public:
bool matches(const QString &needle) const; bool matches(const QString &needle) const;
bool hasAllTags(const QSet<QString> &activeTags) const; bool hasAllTags(const QSet<QString> &activeTags) const;
void setSelected(bool selected); void setSelected(bool selected);
virtual void setFilterHighlight(const QString &lowerNeedle);
signals: signals:
void clicked(); void clicked();
@@ -130,6 +138,13 @@ public:
explicit AgentRowCard(const AgentConfig &cfg, QWidget *parent = nullptr); explicit AgentRowCard(const AgentConfig &cfg, QWidget *parent = nullptr);
QString agentName() const { return itemName(); } QString agentName() const { return itemName(); }
void setFilterHighlight(const QString &lowerNeedle) override;
private:
QLabel *m_nameLabel = nullptr;
QLabel *m_modelLabel = nullptr;
QString m_model;
QString m_lowerNeedle;
}; };
class ProviderSection : public QWidget class ProviderSection : public QWidget
@@ -171,11 +186,16 @@ public:
QString selectedName() const { return m_selectedName; } QString selectedName() const { return m_selectedName; }
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private: private:
void rebuild(const QString &currentName); void rebuild(const QString &currentName);
void selectCard(ListRowCard *card); void selectCard(ListRowCard *card);
void applyFilters(); void applyFilters();
void setAllExpanded(bool expanded); void setAllExpanded(bool expanded);
void moveSelection(int delta);
QList<ListRowCard *> filteredCards() const;
QLineEdit *m_filter = nullptr; QLineEdit *m_filter = nullptr;
TagFilterStrip *m_tagStrip = nullptr; TagFilterStrip *m_tagStrip = nullptr;