mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-12 14:21:01 -04:00
refactor: Improve UX
This commit is contained in:
@@ -4,13 +4,11 @@
|
||||
|
||||
#include "AgentDetailPane.hpp"
|
||||
|
||||
#include "AgentModelDialog.hpp"
|
||||
#include "ModelSelector.hpp"
|
||||
#include "SectionBox.hpp"
|
||||
#include "SettingsTheme.hpp"
|
||||
#include "SettingsUiBuilders.hpp"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
#include <AgentFactory.hpp>
|
||||
@@ -84,6 +82,9 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
|
||||
m_name->setFont(nf);
|
||||
m_name->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
m_sourceBadge = new QLabel(this);
|
||||
m_sourceBadge->setVisible(false);
|
||||
|
||||
m_path = new QLabel(this);
|
||||
m_path->setFont(monospaceFont(11));
|
||||
m_path->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
@@ -91,20 +92,20 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
|
||||
pp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
|
||||
m_path->setPalette(pp);
|
||||
|
||||
m_openBtn = new QPushButton(tr("Open in editor"), this);
|
||||
m_dupBtn = new QPushButton(tr("Customize a copy…"), this);
|
||||
m_dupBtn->setToolTip(
|
||||
tr("Create an editable user agent that inherits from this one — "
|
||||
"override only the fields you want."));
|
||||
m_openBtn = new QPushButton(tr("Open in editor"), this);
|
||||
m_deleteBtn = new QPushButton(tr("Delete"), this);
|
||||
connect(m_openBtn, &QPushButton::clicked, this, [this] {
|
||||
if (m_current)
|
||||
emit openInEditorRequested(*m_current);
|
||||
});
|
||||
connect(m_dupBtn, &QPushButton::clicked, this, [this] {
|
||||
if (m_current)
|
||||
emit customizeRequested(*m_current);
|
||||
});
|
||||
connect(m_openBtn, &QPushButton::clicked, this, [this] {
|
||||
if (m_current)
|
||||
emit openInEditorRequested(*m_current);
|
||||
});
|
||||
connect(m_deleteBtn, &QPushButton::clicked, this, [this] {
|
||||
if (m_current)
|
||||
emit deleteRequested(*m_current);
|
||||
@@ -114,14 +115,15 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
|
||||
auto *actions = new QHBoxLayout(m_actionsHolder);
|
||||
actions->setContentsMargins(0, 0, 0, 0);
|
||||
actions->setSpacing(6);
|
||||
actions->addWidget(m_openBtn);
|
||||
actions->addWidget(m_dupBtn);
|
||||
actions->addWidget(m_openBtn);
|
||||
actions->addWidget(m_deleteBtn);
|
||||
|
||||
auto *titleRow = new QHBoxLayout;
|
||||
titleRow->setContentsMargins(0, 0, 0, 0);
|
||||
titleRow->setSpacing(8);
|
||||
titleRow->addWidget(m_name);
|
||||
titleRow->addWidget(m_sourceBadge, 0, Qt::AlignVCenter);
|
||||
titleRow->addStretch(1);
|
||||
|
||||
auto *headerLeft = new QVBoxLayout;
|
||||
@@ -144,41 +146,32 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
|
||||
m_description->setWordWrap(true);
|
||||
m_description->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
auto *identity = new SectionBox(tr("Identity"), this);
|
||||
m_nameValue = makeReadOnlyLine();
|
||||
m_extendsLabel = new QLabel(tr("Extends:"), this);
|
||||
m_extendsLabel->setMinimumWidth(96);
|
||||
m_extendsLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
m_extendsValue = makeReadOnlyLine();
|
||||
m_descriptionEdit = new QPlainTextEdit(this);
|
||||
m_descriptionEdit->setReadOnly(true);
|
||||
m_descriptionEdit->setMaximumHeight(56);
|
||||
m_tagsValue = makeReadOnlyLine();
|
||||
auto *setup = new SectionBox(tr("Configuration"), this);
|
||||
|
||||
auto *idGrid = new QGridLayout;
|
||||
idGrid->setContentsMargins(0, 0, 0, 0);
|
||||
idGrid->setHorizontalSpacing(8);
|
||||
idGrid->setVerticalSpacing(4);
|
||||
FormBuilder idForm(idGrid);
|
||||
idForm.row(tr("Name:"), m_nameValue);
|
||||
{
|
||||
auto *holder = new QWidget;
|
||||
holder->setLayout(singleField(m_extendsValue));
|
||||
const int row = idForm.currentRow();
|
||||
idGrid->addWidget(m_extendsLabel, row, 0, Qt::AlignTop);
|
||||
idGrid->addWidget(holder, row, 1);
|
||||
m_extendsHolder = holder;
|
||||
idForm = FormBuilder(idGrid, row + 1);
|
||||
}
|
||||
idForm.row(tr("Description:"), m_descriptionEdit);
|
||||
idForm.row(
|
||||
tr("Tags:"),
|
||||
m_tagsValue,
|
||||
tr("Comma-separated. Free-form — used to filter and "
|
||||
"group the agent list."));
|
||||
identity->bodyLayout()->addLayout(idGrid);
|
||||
m_modelSelector = new ModelSelector(this);
|
||||
connect(m_modelSelector, &ModelSelector::modelSubmitted, this, [this](const QString &model) {
|
||||
onModelSubmitted(model);
|
||||
});
|
||||
|
||||
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_modelSelector, 1);
|
||||
modelRow->addWidget(m_modelResetBtn);
|
||||
|
||||
m_modelStatus = makeHintLabel(QString(), this);
|
||||
m_modelStatus->setVisible(false);
|
||||
connect(m_modelSelector, &ModelSelector::statusChanged, this, [this](const QString &status) {
|
||||
m_modelStatus->setText(status);
|
||||
m_modelStatus->setVisible(!status.isEmpty());
|
||||
});
|
||||
|
||||
auto *connection = new SectionBox(tr("Connection"), this);
|
||||
m_providerCombo = new QComboBox(this);
|
||||
m_providerCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
m_providerCombo->setEnabled(false);
|
||||
@@ -199,65 +192,6 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
|
||||
providerRow->addWidget(m_providerCombo, 1);
|
||||
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->setEnabled(false);
|
||||
connect(m_toolsCheck, &QCheckBox::clicked, this, [this](bool on) { onToggleTools(on); });
|
||||
@@ -275,38 +209,46 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
|
||||
toolsRow->addStretch(1);
|
||||
toolsRow->addWidget(m_toolsResetBtn);
|
||||
|
||||
auto *capGrid = new QGridLayout;
|
||||
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);
|
||||
m_thinkingValue = new QLabel(this);
|
||||
|
||||
auto *match = new SectionBox(tr("Match"), this);
|
||||
auto *matchHint = makeHintLabel(
|
||||
tr("When a feature slot has multiple bound agents, the first whose "
|
||||
"match rules satisfy the current context wins."));
|
||||
auto *setupGrid = new QGridLayout;
|
||||
setupGrid->setContentsMargins(0, 0, 0, 0);
|
||||
setupGrid->setHorizontalSpacing(8);
|
||||
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);
|
||||
auto *matchGrid = new QGridLayout;
|
||||
matchGrid->setContentsMargins(0, 0, 0, 0);
|
||||
matchGrid->setHorizontalSpacing(8);
|
||||
matchGrid->setVerticalSpacing(4);
|
||||
FormBuilder(matchGrid).row(
|
||||
tr("File patterns:"),
|
||||
m_filePatternsValue,
|
||||
tr("Globs, comma-separated. Empty matches every file."));
|
||||
match->bodyLayout()->addWidget(matchHint);
|
||||
match->bodyLayout()->addLayout(matchGrid);
|
||||
m_filePatternsValue->setPlaceholderText(tr("(matches every file)"));
|
||||
|
||||
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);
|
||||
|
||||
m_rawToggle = new QToolButton(this);
|
||||
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_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);
|
||||
auto *bodyLayout = new QVBoxLayout(m_body);
|
||||
bodyLayout->setContentsMargins(0, 0, 0, 0);
|
||||
bodyLayout->setSpacing(10);
|
||||
bodyLayout->addWidget(identity);
|
||||
bodyLayout->addWidget(connection);
|
||||
bodyLayout->addWidget(capabilities);
|
||||
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);
|
||||
bodyLayout->addWidget(setup);
|
||||
bodyLayout->addWidget(m_detailsToggle, 0, Qt::AlignLeft);
|
||||
bodyLayout->addWidget(m_detailsBody);
|
||||
|
||||
auto *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(12, 12, 12, 12);
|
||||
@@ -407,25 +380,19 @@ void AgentDetailPane::populateProviderCombo()
|
||||
m_providerComboPopulated = true;
|
||||
}
|
||||
|
||||
void AgentDetailPane::onChangeModel()
|
||||
void AgentDetailPane::onModelSubmitted(const QString &model)
|
||||
{
|
||||
if (!m_agentFactory || !m_current)
|
||||
return;
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
if (const AgentConfig *cfg = m_agentFactory->configByName(name))
|
||||
setAgent(*cfg);
|
||||
}
|
||||
@@ -540,22 +507,11 @@ void AgentDetailPane::setAgent(const AgentConfig &cfg)
|
||||
setDetailsVisible(true);
|
||||
|
||||
m_name->setText(cfg.name);
|
||||
m_sourceBadge->setText(user ? tr("User") : tr("Bundled"));
|
||||
m_path->setText(cfg.sourcePath);
|
||||
m_description->setText(
|
||||
cfg.description.isEmpty() ? tr("No description provided.") : cfg.description);
|
||||
|
||||
m_nameValue->setText(cfg.name);
|
||||
if (cfg.extendsName.isEmpty()) {
|
||||
m_extendsLabel->setVisible(false);
|
||||
m_extendsHolder->setVisible(false);
|
||||
} else {
|
||||
m_extendsLabel->setVisible(true);
|
||||
m_extendsHolder->setVisible(true);
|
||||
m_extendsValue->setText(cfg.extendsName);
|
||||
}
|
||||
m_descriptionEdit->setPlainText(cfg.description);
|
||||
m_tagsValue->setText(cfg.tags.join(QStringLiteral(", ")));
|
||||
|
||||
populateProviderCombo();
|
||||
|
||||
if (m_providerComboHasSentinel) {
|
||||
@@ -589,14 +545,11 @@ void AgentDetailPane::setAgent(const AgentConfig &cfg)
|
||||
? tr("Overridden in settings. Reset to use the agent's default provider.")
|
||||
: QString());
|
||||
|
||||
m_endpointValue->setText(cfg.endpoint);
|
||||
m_endpointValue->setPlaceholderText(tr("(provider default)"));
|
||||
m_modelValue->setText(cfg.model);
|
||||
m_modelChangeBtn->setEnabled(m_agentFactory != nullptr);
|
||||
m_modelSelector->setAgent(m_agentFactory, cfg.name, cfg.providerInstance, cfg.model);
|
||||
const bool hasModelOverride = m_agentFactory
|
||||
&& !m_agentFactory->agentModelOverride(cfg.name).isEmpty();
|
||||
m_modelResetBtn->setVisible(hasModelOverride);
|
||||
m_modelValue->setToolTip(
|
||||
m_modelSelector->setToolTip(
|
||||
hasModelOverride ? tr("Overridden in settings. Reset to use the agent's default model.")
|
||||
: QString());
|
||||
|
||||
@@ -613,14 +566,16 @@ void AgentDetailPane::setAgent(const AgentConfig &cfg)
|
||||
hasToolsOverride ? tr("Overridden in settings. Reset to use the agent's default.")
|
||||
: 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;
|
||||
m_effectiveUrl->setText(
|
||||
eff.isEmpty() ? tr("# effective request line\n(unknown — provider instance not found)")
|
||||
: QStringLiteral("# %1\nPOST %2").arg(tr("effective request line"), eff));
|
||||
|
||||
m_filePatternsValue->setText(cfg.match.filePatterns.join(QStringLiteral(", ")));
|
||||
m_filePatternsValue->setPlaceholderText(tr("(matches every file)"));
|
||||
|
||||
fillRawToml(m_rawToml, cfg.sourcePath);
|
||||
|
||||
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_path->clear();
|
||||
m_description->setText(tr("Pick an agent from the list to see its details."));
|
||||
m_nameValue->clear();
|
||||
m_extendsLabel->setVisible(false);
|
||||
m_extendsHolder->setVisible(false);
|
||||
m_descriptionEdit->clear();
|
||||
m_tagsValue->clear();
|
||||
if (m_providerComboHasSentinel) {
|
||||
m_providerCombo->removeItem(0);
|
||||
m_providerComboHasSentinel = false;
|
||||
@@ -664,11 +614,8 @@ void AgentDetailPane::clear()
|
||||
m_providerCombo->setCurrentIndex(-1);
|
||||
m_providerCombo->setEnabled(false);
|
||||
m_providerResetBtn->setVisible(false);
|
||||
m_endpointValue->clear();
|
||||
m_modelValue->clear();
|
||||
m_modelChangeBtn->setEnabled(false);
|
||||
m_modelSelector->clearAgent();
|
||||
m_modelResetBtn->setVisible(false);
|
||||
m_effectiveUrl->clear();
|
||||
m_thinkingValue->clear();
|
||||
{
|
||||
QSignalBlocker block(m_toolsCheck);
|
||||
@@ -676,7 +623,11 @@ void AgentDetailPane::clear()
|
||||
}
|
||||
m_toolsCheck->setEnabled(false);
|
||||
m_toolsResetBtn->setVisible(false);
|
||||
m_extendsValue->clear();
|
||||
m_tagsValue->clear();
|
||||
m_endpointValue->clear();
|
||||
m_filePatternsValue->clear();
|
||||
m_effectiveUrl->clear();
|
||||
m_rawToml->clear();
|
||||
m_baseRawToml->clear();
|
||||
m_baseRawToggle->setVisible(false);
|
||||
@@ -728,6 +679,7 @@ void AgentDetailPane::fillRawToml(QPlainTextEdit *view, const QString &path)
|
||||
void AgentDetailPane::setDetailsVisible(bool visible)
|
||||
{
|
||||
m_headerSep->setVisible(visible);
|
||||
m_sourceBadge->setVisible(visible);
|
||||
m_path->setVisible(visible);
|
||||
m_actionsHolder->setVisible(visible);
|
||||
m_body->setVisible(visible);
|
||||
@@ -744,6 +696,8 @@ void AgentDetailPane::applyCodePalette()
|
||||
m_effectiveUrl->setStyleSheet(
|
||||
QStringLiteral("QLabel { background:%1; border:1px solid %2; }")
|
||||
.arg(cssColor(codeBg), cssColor(Utils::creatorColor(Utils::Theme::SplitterColor))));
|
||||
if (m_current)
|
||||
styleSourceBadge(m_sourceBadge, m_current->isUserSource());
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -29,6 +29,7 @@ class ProviderInstanceFactory;
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class ModelSelector;
|
||||
class SectionBox;
|
||||
|
||||
class AgentDetailPane : public QWidget
|
||||
@@ -56,7 +57,7 @@ private:
|
||||
void setDetailsVisible(bool visible);
|
||||
void applyCodePalette();
|
||||
void populateProviderCombo();
|
||||
void onChangeModel();
|
||||
void onModelSubmitted(const QString &model);
|
||||
void onResetModel();
|
||||
void onChangeProvider(int index);
|
||||
void onResetProvider();
|
||||
@@ -70,7 +71,11 @@ private:
|
||||
AgentConfig m_currentStorage;
|
||||
const AgentConfig *m_current = nullptr;
|
||||
|
||||
QPointer<Providers::ProviderInstanceFactory> m_instanceFactory;
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
|
||||
QLabel *m_name = nullptr;
|
||||
QLabel *m_sourceBadge = nullptr;
|
||||
QLabel *m_path = nullptr;
|
||||
QFrame *m_headerSep = nullptr;
|
||||
QWidget *m_actionsHolder = nullptr;
|
||||
@@ -80,32 +85,25 @@ private:
|
||||
QPushButton *m_deleteBtn = nullptr;
|
||||
QLabel *m_description = nullptr;
|
||||
|
||||
QLineEdit *m_nameValue = nullptr;
|
||||
QLabel *m_extendsLabel = nullptr;
|
||||
QWidget *m_extendsHolder = nullptr;
|
||||
QLineEdit *m_extendsValue = nullptr;
|
||||
QPlainTextEdit *m_descriptionEdit = nullptr;
|
||||
QLineEdit *m_tagsValue = nullptr;
|
||||
|
||||
ModelSelector *m_modelSelector = nullptr;
|
||||
QPushButton *m_modelResetBtn = nullptr;
|
||||
QLabel *m_modelStatus = nullptr;
|
||||
QComboBox *m_providerCombo = 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;
|
||||
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;
|
||||
|
||||
QToolButton *m_rawToggle = nullptr;
|
||||
QPlainTextEdit *m_rawToml = nullptr;
|
||||
|
||||
QToolButton *m_baseRawToggle = nullptr;
|
||||
QPlainTextEdit *m_baseRawToml = nullptr;
|
||||
};
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
#include "AgentListItem.hpp"
|
||||
|
||||
#include "SettingsTheme.hpp"
|
||||
#include "TagChip.hpp"
|
||||
#include "SettingsUiBuilders.hpp"
|
||||
|
||||
#include <utils/elidinglabel.h>
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
#include <QEvent>
|
||||
@@ -16,73 +17,52 @@
|
||||
#include <QMouseEvent>
|
||||
#include <QPalette>
|
||||
#include <QScopedValueRollback>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
AgentListItem::AgentListItem(const AgentConfig &cfg, QWidget *parent)
|
||||
: QFrame(parent)
|
||||
, m_name(cfg.name)
|
||||
, m_model(cfg.model)
|
||||
{
|
||||
setObjectName(QStringLiteral("AgentListItem"));
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setAutoFillBackground(true);
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
|
||||
auto *dot = new QLabel(QStringLiteral("●"), this);
|
||||
QFont df = dot->font();
|
||||
df.setPixelSize(10);
|
||||
dot->setFont(df);
|
||||
QPalette dp = dot->palette();
|
||||
dp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
|
||||
dot->setPalette(dp);
|
||||
|
||||
auto *nameLbl = new QLabel(cfg.name, this);
|
||||
QFont nf = nameLbl->font();
|
||||
m_nameLabel = new QLabel(this);
|
||||
QFont nf = m_nameLabel->font();
|
||||
nf.setBold(true);
|
||||
nf.setPixelSize(12);
|
||||
nameLbl->setFont(nf);
|
||||
m_nameLabel->setFont(nf);
|
||||
m_nameLabel->setTextFormat(Qt::RichText);
|
||||
|
||||
auto *headerRow = new QHBoxLayout;
|
||||
headerRow->setContentsMargins(0, 0, 0, 0);
|
||||
headerRow->setSpacing(6);
|
||||
headerRow->addWidget(dot, 0, Qt::AlignVCenter);
|
||||
headerRow->addWidget(nameLbl, 1);
|
||||
|
||||
auto *col = new QVBoxLayout;
|
||||
col->setContentsMargins(0, 0, 0, 0);
|
||||
col->setSpacing(2);
|
||||
col->addLayout(headerRow);
|
||||
|
||||
m_modelLabel = new QLabel(cfg.model, this);
|
||||
m_modelLabel = new Utils::ElidingLabel(this);
|
||||
m_modelLabel->setElideMode(Qt::ElideMiddle);
|
||||
m_modelLabel->setFont(monospaceFont(11));
|
||||
m_modelLabel->setContentsMargins(16, 0, 0, 0);
|
||||
m_modelLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
QPalette mp = m_modelLabel->palette();
|
||||
mp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
|
||||
m_modelLabel->setPalette(mp);
|
||||
m_modelLabel->setText(cfg.model);
|
||||
m_modelLabel->setVisible(!cfg.model.isEmpty());
|
||||
col->addWidget(m_modelLabel);
|
||||
|
||||
if (!cfg.tags.isEmpty()) {
|
||||
auto *tagsHolder = new QWidget(this);
|
||||
auto *tagsLay = new QHBoxLayout(tagsHolder);
|
||||
tagsLay->setContentsMargins(16, 2, 0, 0);
|
||||
tagsLay->setSpacing(3);
|
||||
for (const QString &t : cfg.tags) {
|
||||
auto *chip = new TagChip(t, -1, tagsHolder);
|
||||
connect(chip, &TagChip::clicked, this, &AgentListItem::tagClicked);
|
||||
m_chips.append(chip);
|
||||
tagsLay->addWidget(chip);
|
||||
}
|
||||
tagsLay->addStretch(1);
|
||||
col->addWidget(tagsHolder);
|
||||
}
|
||||
auto *row = new QHBoxLayout(this);
|
||||
row->setContentsMargins(8, 4, 8, 4);
|
||||
row->setSpacing(8);
|
||||
row->addWidget(m_nameLabel, 0);
|
||||
row->addWidget(m_modelLabel, 1);
|
||||
|
||||
auto *outer = new QVBoxLayout(this);
|
||||
outer->setContentsMargins(5, 6, 8, 6);
|
||||
outer->setSpacing(0);
|
||||
outer->addLayout(col);
|
||||
QStringList tipLines;
|
||||
if (!cfg.description.isEmpty())
|
||||
tipLines << cfg.description;
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -94,20 +74,26 @@ void AgentListItem::setSelected(bool selected)
|
||||
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)
|
||||
{
|
||||
if (!m_modelLabel)
|
||||
return;
|
||||
m_model = model;
|
||||
m_modelLabel->setText(model);
|
||||
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)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
@@ -133,6 +119,7 @@ void AgentListItem::applyTheme()
|
||||
"#AgentListItem { background:transparent;"
|
||||
" border-top:1px solid %1; border-left:3px solid %2; }")
|
||||
.arg(cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)), accent));
|
||||
updateNameText();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
@@ -15,9 +13,11 @@ QT_BEGIN_NAMESPACE
|
||||
class QLabel;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
namespace Utils {
|
||||
class ElidingLabel;
|
||||
}
|
||||
|
||||
class TagChip;
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class AgentListItem : public QFrame
|
||||
{
|
||||
@@ -27,12 +27,11 @@ public:
|
||||
|
||||
QString agentName() const { return m_name; }
|
||||
void setSelected(bool selected);
|
||||
void setActiveTags(const QSet<QString> &active);
|
||||
void setModel(const QString &model);
|
||||
void setFilterHighlight(const QString &lowerFilter);
|
||||
|
||||
signals:
|
||||
void clicked(const QString &name);
|
||||
void tagClicked(const QString &tag);
|
||||
|
||||
protected:
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
@@ -40,12 +39,15 @@ protected:
|
||||
|
||||
private:
|
||||
void applyTheme();
|
||||
void updateNameText();
|
||||
|
||||
QString m_name;
|
||||
QString m_model;
|
||||
QString m_filter;
|
||||
bool m_selected = false;
|
||||
bool m_inApplyTheme = false;
|
||||
QLabel *m_modelLabel = nullptr;
|
||||
QList<TagChip *> m_chips;
|
||||
QLabel *m_nameLabel = nullptr;
|
||||
Utils::ElidingLabel *m_modelLabel = nullptr;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <QEvent>
|
||||
#include <QFont>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMap>
|
||||
@@ -37,8 +38,13 @@ AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
|
||||
setFrameShape(QFrame::StyledPanel);
|
||||
|
||||
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->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;
|
||||
filterRow->setContentsMargins(6, 6, 6, 6);
|
||||
@@ -48,8 +54,6 @@ AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
|
||||
m_filterHolder->setLayout(filterRow);
|
||||
m_filterHolder->setAutoFillBackground(true);
|
||||
|
||||
m_tagStrip = new TagFilterStrip(this);
|
||||
|
||||
m_listScroll = new QScrollArea(this);
|
||||
m_listScroll->setWidgetResizable(true);
|
||||
m_listScroll->setFrameShape(QFrame::NoFrame);
|
||||
@@ -59,19 +63,22 @@ AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
|
||||
outer->setContentsMargins(0, 0, 0, 0);
|
||||
outer->setSpacing(0);
|
||||
outer->addWidget(m_filterHolder);
|
||||
outer->addWidget(m_tagStrip);
|
||||
outer->addWidget(m_listScroll, 1);
|
||||
|
||||
m_filterDebounce = new QTimer(this);
|
||||
m_filterDebounce->setSingleShot(true);
|
||||
m_filterDebounce->setInterval(100);
|
||||
connect(m_filterDebounce, &QTimer::timeout, this, &AgentListPane::rebuildList);
|
||||
connect(m_filterEdit, &QLineEdit::textChanged, this,
|
||||
[this](const QString &) { m_filterDebounce->start(); });
|
||||
connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &) {
|
||||
m_filterDebounce->start();
|
||||
});
|
||||
|
||||
connect(m_tagStrip, &TagFilterStrip::activeTagsChanged, this,
|
||||
[this](const QSet<QString> &) { rebuildList(); },
|
||||
Qt::QueuedConnection);
|
||||
connect(
|
||||
m_tagStrip,
|
||||
&TagFilterStrip::activeTagsChanged,
|
||||
this,
|
||||
[this](const QSet<QString> &) { rebuildList(); },
|
||||
Qt::QueuedConnection);
|
||||
|
||||
if (m_factory) {
|
||||
connect(m_factory, &AgentFactory::agentModelChanged, this, [this](const QString &name) {
|
||||
@@ -93,7 +100,7 @@ void AgentListPane::selectByName(const QString &name)
|
||||
return;
|
||||
if (m_factory) {
|
||||
if (const AgentConfig *cfg = m_factory->configByName(name))
|
||||
m_expandedGroups.insert(groupKey(*cfg));
|
||||
m_collapsedGroups.remove(groupKey(*cfg));
|
||||
}
|
||||
setCurrentNameInternal(name, false);
|
||||
m_notifyOnRebuild = true;
|
||||
@@ -121,6 +128,12 @@ void AgentListPane::refresh()
|
||||
rebuildList();
|
||||
}
|
||||
|
||||
void AgentListPane::focusFilter()
|
||||
{
|
||||
m_filterEdit->setFocus(Qt::OtherFocusReason);
|
||||
m_filterEdit->selectAll();
|
||||
}
|
||||
|
||||
void AgentListPane::changeEvent(QEvent *event)
|
||||
{
|
||||
QFrame::changeEvent(event);
|
||||
@@ -128,6 +141,41 @@ void AgentListPane::changeEvent(QEvent *event)
|
||||
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()
|
||||
{
|
||||
if (!m_filterHolder)
|
||||
@@ -156,9 +204,14 @@ std::vector<const AgentConfig *> AgentListPane::visibleAgents() const
|
||||
|
||||
bool AgentListPane::matchesFilters(const AgentConfig &a, const QString &lowerFilter) const
|
||||
{
|
||||
if (!lowerFilter.isEmpty()
|
||||
&& !(a.name + QLatin1Char(' ') + a.model).toLower().contains(lowerFilter))
|
||||
return false;
|
||||
if (!lowerFilter.isEmpty()) {
|
||||
const QString haystack = (a.name + QLatin1Char(' ') + a.model + QLatin1Char(' ')
|
||||
+ a.providerInstance + QLatin1Char(' ')
|
||||
+ a.tags.join(QLatin1Char(' ')))
|
||||
.toLower();
|
||||
if (!haystack.contains(lowerFilter))
|
||||
return false;
|
||||
}
|
||||
const QSet<QString> &active = m_tagStrip->activeTags();
|
||||
for (const QString &t : active)
|
||||
if (!a.tags.contains(t))
|
||||
@@ -199,14 +252,8 @@ void AgentListPane::rebuildList()
|
||||
for (const AgentConfig *cfg : agents) {
|
||||
auto *item = new AgentListItem(*cfg, content);
|
||||
item->setSelected(cfg->name == m_currentName);
|
||||
item->setActiveTags(activeTags);
|
||||
item->setFilterHighlight(lowerFilter);
|
||||
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);
|
||||
newRows.append(item);
|
||||
}
|
||||
@@ -231,7 +278,7 @@ void AgentListPane::rebuildList()
|
||||
const std::vector<const AgentConfig *> &group = byProvider[provider];
|
||||
const QString key = sectionKey + QLatin1Char('/') + provider;
|
||||
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);
|
||||
header->setExpanded(expanded);
|
||||
@@ -242,8 +289,8 @@ void AgentListPane::rebuildList()
|
||||
&CollapsibleHeader::toggled,
|
||||
this,
|
||||
[this, key] {
|
||||
if (!m_expandedGroups.remove(key))
|
||||
m_expandedGroups.insert(key);
|
||||
if (!m_collapsedGroups.remove(key))
|
||||
m_collapsedGroups.insert(key);
|
||||
rebuildList();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
@@ -258,7 +305,7 @@ void AgentListPane::rebuildList()
|
||||
addSection(tr("User"), QStringLiteral("user"), userAgents);
|
||||
addSection(tr("Bundled"), QStringLiteral("bundled"), bundledAgents);
|
||||
if (!filtersActive)
|
||||
m_expandedGroups.intersect(liveKeys);
|
||||
m_collapsedGroups.intersect(liveKeys);
|
||||
if (userAgents.empty() && bundledAgents.empty()) {
|
||||
auto *empty = new QLabel(tr("No agents match these filters."), content);
|
||||
empty->setAlignment(Qt::AlignCenter);
|
||||
@@ -273,10 +320,9 @@ void AgentListPane::rebuildList()
|
||||
m_rows = newRows;
|
||||
m_listScroll->setWidget(content);
|
||||
|
||||
const AgentConfig *current
|
||||
= m_currentName.isEmpty() || !m_factory
|
||||
? nullptr
|
||||
: m_factory->configByName(m_currentName);
|
||||
const AgentConfig *current = m_currentName.isEmpty() || !m_factory
|
||||
? nullptr
|
||||
: m_factory->configByName(m_currentName);
|
||||
if (!current && !m_rows.isEmpty()) {
|
||||
const QString fallback = m_rows.front()->agentName();
|
||||
m_rows.front()->setSelected(true);
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <QFrame>
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
|
||||
@@ -35,15 +35,19 @@ public:
|
||||
QString currentName() const { return m_currentName; }
|
||||
void selectByName(const QString &name);
|
||||
void refresh();
|
||||
void focusFilter();
|
||||
TagFilterStrip *tagStrip() const { return m_tagStrip; }
|
||||
|
||||
signals:
|
||||
void currentAgentChanged(const QString &name);
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent *event) override;
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
void rebuildList();
|
||||
void moveSelection(int delta);
|
||||
void applyFilterHolderTheme();
|
||||
bool matchesFilters(const AgentConfig &a, const QString &lowerFilter) const;
|
||||
std::vector<const AgentConfig *> visibleAgents() const;
|
||||
@@ -60,7 +64,7 @@ private:
|
||||
QScrollArea *m_listScroll = nullptr;
|
||||
QList<AgentListItem *> m_rows;
|
||||
QString m_currentName;
|
||||
QSet<QString> m_expandedGroups;
|
||||
QSet<QString> m_collapsedGroups;
|
||||
bool m_notifyOnRebuild = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ¤tModel, 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
|
||||
@@ -8,24 +8,21 @@
|
||||
#include "AgentDuplicator.hpp"
|
||||
#include "AgentListPane.hpp"
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTheme.hpp"
|
||||
#include "TagFilterStrip.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
#include <utils/filepath.h>
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPalette>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
@@ -76,13 +73,7 @@ public:
|
||||
|
||||
m_reload = new QPushButton(tr("Reload from disk"), this);
|
||||
m_openUserDir = new QPushButton(tr("Open agents folder"), this);
|
||||
|
||||
m_userPathLabel = new QLabel(this);
|
||||
m_userPathLabel->setFont(monospaceFont(11));
|
||||
QPalette mutedPal = m_userPathLabel->palette();
|
||||
mutedPal.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
|
||||
m_userPathLabel->setPalette(mutedPal);
|
||||
m_userPathLabel->setMaximumWidth(260);
|
||||
m_openUserDir->setToolTip(QodeAssist::AgentFactory::userAgentsDir());
|
||||
|
||||
auto *headerRow = new QHBoxLayout;
|
||||
headerRow->setContentsMargins(0, 0, 0, 0);
|
||||
@@ -90,7 +81,6 @@ public:
|
||||
headerRow->addWidget(m_titleLabel);
|
||||
headerRow->addStretch(1);
|
||||
headerRow->addWidget(m_reload);
|
||||
headerRow->addWidget(m_userPathLabel);
|
||||
headerRow->addWidget(m_openUserDir);
|
||||
|
||||
auto *headerSep = new QFrame(this);
|
||||
@@ -98,6 +88,7 @@ public:
|
||||
headerSep->setFrameShadow(QFrame::Sunken);
|
||||
|
||||
m_listPane = new AgentListPane(m_agentFactory, this);
|
||||
m_listPane->tagStrip()->setMaxColumns(8);
|
||||
|
||||
m_detail = new AgentDetailPane(this);
|
||||
m_detail->setInstanceFactory(m_agentFactory->instanceFactory());
|
||||
@@ -119,6 +110,7 @@ public:
|
||||
root->setSpacing(6);
|
||||
root->addLayout(headerRow);
|
||||
root->addWidget(headerSep);
|
||||
root->addWidget(m_listPane->tagStrip());
|
||||
root->addWidget(splitter, 1);
|
||||
|
||||
connect(m_reload, &QPushButton::clicked, this, &AgentsWidget::reloadFromDisk);
|
||||
@@ -153,6 +145,8 @@ public:
|
||||
|
||||
reloadFromDisk();
|
||||
|
||||
QTimer::singleShot(0, this, [this] { m_listPane->focusFilter(); });
|
||||
|
||||
if (m_navigator) {
|
||||
QTimer::singleShot(0, this, [this] {
|
||||
if (!m_navigator)
|
||||
@@ -170,18 +164,9 @@ private:
|
||||
void reloadFromDisk()
|
||||
{
|
||||
m_agentFactory->reload();
|
||||
updateUserPathLabel();
|
||||
m_listPane->refresh();
|
||||
}
|
||||
|
||||
void updateUserPathLabel()
|
||||
{
|
||||
const QString dir = QodeAssist::AgentFactory::userAgentsDir();
|
||||
m_userPathLabel->setText(
|
||||
QFontMetrics(m_userPathLabel->font()).elidedText(dir, Qt::ElideLeft, 256));
|
||||
m_userPathLabel->setToolTip(dir);
|
||||
}
|
||||
|
||||
void openAgentInEditor(const AgentConfig &agent)
|
||||
{
|
||||
const QString name = agent.name;
|
||||
@@ -253,7 +238,6 @@ private:
|
||||
QLabel *m_titleLabel = nullptr;
|
||||
QPushButton *m_reload = nullptr;
|
||||
QPushButton *m_openUserDir = nullptr;
|
||||
QLabel *m_userPathLabel = nullptr;
|
||||
|
||||
AgentListPane *m_listPane = nullptr;
|
||||
QScrollArea *m_detailScroll = nullptr;
|
||||
|
||||
@@ -24,7 +24,7 @@ add_library(QodeAssistSettings STATIC
|
||||
UpdateDialog.hpp UpdateDialog.cpp
|
||||
AgentsSettingsPage.hpp AgentsSettingsPage.cpp
|
||||
AgentDetailPane.hpp AgentDetailPane.cpp
|
||||
AgentModelDialog.hpp AgentModelDialog.cpp
|
||||
ModelSelector.hpp ModelSelector.cpp
|
||||
AgentListItem.hpp AgentListItem.cpp
|
||||
AgentListPane.hpp AgentListPane.cpp
|
||||
AgentDuplicator.hpp AgentDuplicator.cpp
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <coreplugin/idocument.h>
|
||||
#include <extensionsystem/pluginmanager.h>
|
||||
#include <extensionsystem/pluginspec.h>
|
||||
#include <utils/infolabel.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QFont>
|
||||
@@ -43,6 +45,16 @@ namespace {
|
||||
|
||||
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
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -261,12 +273,13 @@ GeneralSettings::GeneralSettings()
|
||||
|
||||
requestTimeout.setSettingsKey(Constants::REQUEST_TIMEOUT);
|
||||
requestTimeout.setLabelText(Tr::tr("Request timeout (seconds):"));
|
||||
requestTimeout.setToolTip(Tr::tr(
|
||||
"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. "
|
||||
"The timer resets every time data is received, so this effectively limits the "
|
||||
"time-to-first-token and any stall between tokens. Increase it for slow or local "
|
||||
"models that need a long time to start responding. Set to 0 to disable the timeout."));
|
||||
requestTimeout.setToolTip(
|
||||
Tr::tr(
|
||||
"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. "
|
||||
"The timer resets every time data is received, so this effectively limits the "
|
||||
"time-to-first-token and any stall between tokens. Increase it for slow or local "
|
||||
"models that need a long time to start responding. Set to 0 to disable the timeout."));
|
||||
requestTimeout.setRange(0, 3600);
|
||||
requestTimeout.setDefaultValue(120);
|
||||
|
||||
@@ -282,9 +295,15 @@ GeneralSettings::GeneralSettings()
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
auto networkGroup = Group{
|
||||
title(Tr::tr("Network")),
|
||||
Column{Row{requestTimeout, Stretch{1}}}};
|
||||
auto *versionLabel = new QLabel(pluginVersionText());
|
||||
versionLabel->setEnabled(false);
|
||||
|
||||
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:"));
|
||||
|
||||
@@ -307,16 +326,13 @@ GeneralSettings::GeneralSettings()
|
||||
};
|
||||
|
||||
return Column{
|
||||
Row{supportLabel, supportLinks, Stretch{1}, checkUpdate, resetToDefaults},
|
||||
Space{8},
|
||||
Row{enableQodeAssist, Stretch{1}},
|
||||
Row{enableLogging, Stretch{1}},
|
||||
Row{enableCheckUpdate, Stretch{1}},
|
||||
Space{8},
|
||||
networkGroup,
|
||||
Row{enableQodeAssist, Stretch{1}, versionLabel, checkUpdate, resetToDefaults},
|
||||
Space{12},
|
||||
pipelines,
|
||||
Stretch{1}};
|
||||
Space{12},
|
||||
advancedGroup,
|
||||
Stretch{1},
|
||||
Row{supportLabel, supportLinks, Stretch{1}}};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
176
settings/ModelSelector.cpp
Normal file
176
settings/ModelSelector.cpp
Normal 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
|
||||
@@ -4,52 +4,53 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QComboBox>
|
||||
#include <QFutureWatcher>
|
||||
#include <QList>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace QodeAssist {
|
||||
class AgentFactory;
|
||||
class Agent;
|
||||
class AgentFactory;
|
||||
} // namespace QodeAssist
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class AgentModelDialog : public QDialog
|
||||
class ModelSelector : public QComboBox
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AgentModelDialog(
|
||||
explicit ModelSelector(QWidget *parent = nullptr);
|
||||
|
||||
void setAgent(
|
||||
AgentFactory *factory,
|
||||
const QString &agentName,
|
||||
const QString ¤tModel,
|
||||
QWidget *parent = nullptr);
|
||||
const QString &providerInstance,
|
||||
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:
|
||||
void fetchModels();
|
||||
void onModelsFetched();
|
||||
|
||||
AgentFactory *m_factory = nullptr;
|
||||
QString m_agentName;
|
||||
|
||||
QLineEdit *m_modelEdit = nullptr;
|
||||
QListWidget *m_list = nullptr;
|
||||
QLabel *m_status = nullptr;
|
||||
QPushButton *m_fetchBtn = nullptr;
|
||||
void startFetch();
|
||||
void onFetched();
|
||||
void submitCurrent();
|
||||
|
||||
QPointer<AgentFactory> m_factory;
|
||||
QPointer<Agent> m_probe;
|
||||
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
|
||||
@@ -36,6 +36,9 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
|
||||
nf.setPixelSize(15);
|
||||
m_nameLabel->setFont(nf);
|
||||
|
||||
m_sourceBadge = new QLabel(this);
|
||||
m_sourceBadge->setVisible(false);
|
||||
|
||||
m_sourcePathLabel = new QLabel(this);
|
||||
m_sourcePathLabel->setFont(monospaceFont(11));
|
||||
QPalette spp = m_sourcePathLabel->palette();
|
||||
@@ -66,11 +69,10 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
|
||||
setEditing(false);
|
||||
populate(m_current, m_currentHasStoredKey);
|
||||
});
|
||||
connect(m_saveBtn, &QPushButton::clicked, this, [this] {
|
||||
emit saveRequested(collectEdits());
|
||||
connect(m_saveBtn, &QPushButton::clicked, this, [this] { 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_deleteBtn, &QPushButton::clicked, this, [this] { emit deleteRequested(); });
|
||||
|
||||
@@ -88,6 +90,7 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
|
||||
titleRow->setContentsMargins(0, 0, 0, 0);
|
||||
titleRow->setSpacing(8);
|
||||
titleRow->addWidget(m_nameLabel);
|
||||
titleRow->addWidget(m_sourceBadge, 0, Qt::AlignVCenter);
|
||||
titleRow->addStretch(1);
|
||||
|
||||
auto *headerLeft = new QVBoxLayout;
|
||||
@@ -111,7 +114,7 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
|
||||
m_descriptionLabel->setWordWrap(true);
|
||||
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_descriptionEdit = new QPlainTextEdit(this);
|
||||
m_descriptionEdit->setMaximumHeight(60);
|
||||
@@ -121,29 +124,12 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
|
||||
identityGrid->setHorizontalSpacing(8);
|
||||
identityGrid->setVerticalSpacing(4);
|
||||
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->setFont(monospaceFont(11));
|
||||
auto *endpointGrid = new QGridLayout;
|
||||
endpointGrid->setContentsMargins(0, 0, 0, 0);
|
||||
endpointGrid->setHorizontalSpacing(8);
|
||||
endpointGrid->setVerticalSpacing(4);
|
||||
FormBuilder(endpointGrid).row(tr("URL:"), m_urlEdit,
|
||||
tr("Base URL. Agents append their endpoint path "
|
||||
"(e.g. /chat/completions) to this."));
|
||||
endpointSection->bodyLayout()->addLayout(endpointGrid);
|
||||
|
||||
m_samplePreview = new QLabel(this);
|
||||
m_samplePreview->setFont(monospaceFont(11));
|
||||
m_samplePreview->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
m_samplePreview->setWordWrap(true);
|
||||
m_samplePreview->setContentsMargins(6, 4, 6, 4);
|
||||
m_samplePreview->setAutoFillBackground(true);
|
||||
endpointSection->bodyLayout()->addWidget(m_samplePreview);
|
||||
|
||||
auto *credSection = new SectionBox(tr("Credentials"), this);
|
||||
m_apiKeyEdit = new QLineEdit(this);
|
||||
m_apiKeyEdit->setEchoMode(QLineEdit::Password);
|
||||
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
|
||||
@@ -182,8 +168,7 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
|
||||
emit apiKeySaveRequested(key);
|
||||
m_apiKeyEdit->clear();
|
||||
});
|
||||
connect(m_apiKeyClearBtn, &QPushButton::clicked, this,
|
||||
[this] { emit apiKeyClearRequested(); });
|
||||
connect(m_apiKeyClearBtn, &QPushButton::clicked, this, [this] { emit apiKeyClearRequested(); });
|
||||
m_keyHint = makeHintLabel(QString{}, this);
|
||||
|
||||
auto *keyRow = new QHBoxLayout;
|
||||
@@ -194,15 +179,24 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
|
||||
keyRow->addWidget(m_apiKeySaveBtn);
|
||||
keyRow->addWidget(m_apiKeyClearBtn);
|
||||
|
||||
auto *credGrid = new QGridLayout;
|
||||
credGrid->setContentsMargins(0, 0, 0, 0);
|
||||
credGrid->setHorizontalSpacing(8);
|
||||
credGrid->setVerticalSpacing(4);
|
||||
FormBuilder credForm(credGrid);
|
||||
credForm.row(tr("API key:"), keyRow);
|
||||
credGrid->addWidget(m_legacyKeyBtn, credForm.currentRow(), 1, Qt::AlignLeft);
|
||||
credGrid->addWidget(m_keyHint, credForm.currentRow() + 1, 1);
|
||||
credSection->bodyLayout()->addLayout(credGrid);
|
||||
auto *configGrid = new QGridLayout;
|
||||
configGrid->setContentsMargins(0, 0, 0, 0);
|
||||
configGrid->setHorizontalSpacing(8);
|
||||
configGrid->setVerticalSpacing(4);
|
||||
FormBuilder configForm(configGrid);
|
||||
configForm.row(tr("API key:"), keyRow);
|
||||
{
|
||||
int row = configForm.currentRow();
|
||||
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_launchEmptyHint = new QLabel(this);
|
||||
@@ -218,12 +212,15 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
|
||||
m_startBtn = new QPushButton(tr("Start"), this);
|
||||
m_stopBtn = new QPushButton(tr("Stop"), this);
|
||||
m_restartBtn = new QPushButton(tr("Restart"), this);
|
||||
connect(m_startBtn, &QPushButton::clicked, this,
|
||||
[this] { emit launchStartRequested(m_current.name); });
|
||||
connect(m_stopBtn, &QPushButton::clicked, this,
|
||||
[this] { emit launchStopRequested(m_current.name); });
|
||||
connect(m_restartBtn, &QPushButton::clicked, this,
|
||||
[this] { emit launchRestartRequested(m_current.name); });
|
||||
connect(m_startBtn, &QPushButton::clicked, this, [this] {
|
||||
emit launchStartRequested(m_current.name);
|
||||
});
|
||||
connect(m_stopBtn, &QPushButton::clicked, this, [this] {
|
||||
emit launchStopRequested(m_current.name);
|
||||
});
|
||||
connect(m_restartBtn, &QPushButton::clicked, this, [this] {
|
||||
emit launchRestartRequested(m_current.name);
|
||||
});
|
||||
auto *launchBtnRow = new QHBoxLayout;
|
||||
launchBtnRow->setContentsMargins(0, 0, 0, 0);
|
||||
launchBtnRow->setSpacing(6);
|
||||
@@ -283,9 +280,8 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
|
||||
root->addLayout(headerRow);
|
||||
root->addWidget(headerSep);
|
||||
root->addWidget(m_descriptionLabel);
|
||||
root->addWidget(identitySection);
|
||||
root->addWidget(endpointSection);
|
||||
root->addWidget(credSection);
|
||||
root->addWidget(m_identitySection);
|
||||
root->addWidget(configSection);
|
||||
root->addWidget(m_launchSection);
|
||||
root->addWidget(m_rawToggle, 0, Qt::AlignLeft);
|
||||
root->addWidget(m_rawToml);
|
||||
@@ -302,6 +298,9 @@ void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool
|
||||
const bool needsKey = !inst.apiKeyRef.isEmpty();
|
||||
|
||||
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_descriptionLabel->setText(
|
||||
@@ -323,8 +322,9 @@ void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool
|
||||
m_keyHint->setText(tr("This provider type does not use a key."));
|
||||
} else if (hasStoredKey) {
|
||||
m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it."));
|
||||
m_keyHint->setText(tr("A key is stored. Type a new key and press Save key to "
|
||||
"replace it, or Clear to erase it."));
|
||||
m_keyHint->setText(
|
||||
tr("A key is stored. Type a new key and press Save key to "
|
||||
"replace it, or Clear to erase it."));
|
||||
} else {
|
||||
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
|
||||
m_keyHint->setText(tr("No key stored yet. Type a key and press Save key."));
|
||||
@@ -341,10 +341,6 @@ void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool
|
||||
m_legacyKeyBtn->setVisible(false);
|
||||
}
|
||||
|
||||
m_samplePreview->setText(
|
||||
QStringLiteral("# sample request line\nPOST %1/<agent endpoint>").arg(inst.url));
|
||||
applyPreviewPalette();
|
||||
|
||||
m_deleteBtn->setEnabled(isUser);
|
||||
m_dupBtn->setEnabled(true);
|
||||
m_editBtn->setVisible(isUser);
|
||||
@@ -360,6 +356,7 @@ void ProviderDetailPane::clear()
|
||||
{
|
||||
m_current = {};
|
||||
m_nameLabel->setText(tr("Select a provider"));
|
||||
m_sourceBadge->setVisible(false);
|
||||
m_sourcePathLabel->clear();
|
||||
m_descriptionLabel->clear();
|
||||
m_nameEdit->clear();
|
||||
@@ -373,7 +370,6 @@ void ProviderDetailPane::clear()
|
||||
m_revealKeyBtn->setEnabled(false);
|
||||
m_legacyKeyValue.clear();
|
||||
m_legacyKeyBtn->setVisible(false);
|
||||
m_samplePreview->clear();
|
||||
m_rawToml->clear();
|
||||
m_editBtn->setVisible(false);
|
||||
m_dupBtn->setEnabled(false);
|
||||
@@ -390,8 +386,9 @@ void ProviderDetailPane::refreshKeyStatus(bool hasStoredKey)
|
||||
return;
|
||||
if (hasStoredKey) {
|
||||
m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it."));
|
||||
m_keyHint->setText(tr("A key is stored. Type a new key and press Save key to "
|
||||
"replace it, or Clear to erase it."));
|
||||
m_keyHint->setText(
|
||||
tr("A key is stored. Type a new key and press Save key to "
|
||||
"replace it, or Clear to erase it."));
|
||||
} else {
|
||||
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
|
||||
m_keyHint->setText(tr("No key stored yet. Type a key and press Save key."));
|
||||
@@ -412,11 +409,11 @@ void ProviderDetailPane::setLaunchState(
|
||||
m_launchTerminalToggle->setVisible(hasLaunch);
|
||||
|
||||
if (!hasLaunch) {
|
||||
m_launchEmptyHint->setText(tr(
|
||||
"No [launch] block. This provider is treated as external — "
|
||||
"the plugin will not spawn or supervise any process. "
|
||||
"Add a [launch] block to the TOML to have the plugin manage "
|
||||
"a local server here."));
|
||||
m_launchEmptyHint->setText(
|
||||
tr("No [launch] block. This provider is treated as external — "
|
||||
"the plugin will not spawn or supervise any process. "
|
||||
"Add a [launch] block to the TOML to have the plugin manage "
|
||||
"a local server here."));
|
||||
m_launchCmdLabel->clear();
|
||||
m_launchTerminal->clearContents();
|
||||
return;
|
||||
@@ -429,22 +426,31 @@ void ProviderDetailPane::setLaunchState(
|
||||
Utils::creatorColor(Utils::Theme::PanelTextColorMid).name(),
|
||||
tr("(detached — survives Qt Creator restart)"))
|
||||
: QString();
|
||||
m_launchCmdLabel->setText(
|
||||
QStringLiteral("<b>%1</b> %2%3")
|
||||
.arg(m_current.launch.command.toHtmlEscaped(),
|
||||
m_current.launch.args.join(QLatin1Char(' ')).toHtmlEscaped(),
|
||||
detachedNote));
|
||||
m_launchCmdLabel->setText(QStringLiteral("<b>%1</b> %2%3")
|
||||
.arg(
|
||||
m_current.launch.command.toHtmlEscaped(),
|
||||
m_current.launch.args.join(QLatin1Char(' ')).toHtmlEscaped(),
|
||||
detachedNote));
|
||||
|
||||
QString statusText;
|
||||
switch (st) {
|
||||
case Providers::ProviderLauncher::Idle: statusText = tr("idle"); break;
|
||||
case Providers::ProviderLauncher::Starting: statusText = tr("starting…"); break;
|
||||
case Providers::ProviderLauncher::Probing: statusText = tr("probing…"); break;
|
||||
case Providers::ProviderLauncher::Ready: statusText = tr("ready"); break;
|
||||
case Providers::ProviderLauncher::Stopping: statusText = tr("stopping…"); break;
|
||||
case Providers::ProviderLauncher::Idle:
|
||||
statusText = tr("idle");
|
||||
break;
|
||||
case Providers::ProviderLauncher::Starting:
|
||||
statusText = tr("starting…");
|
||||
break;
|
||||
case Providers::ProviderLauncher::Probing:
|
||||
statusText = tr("probing…");
|
||||
break;
|
||||
case Providers::ProviderLauncher::Ready:
|
||||
statusText = tr("ready");
|
||||
break;
|
||||
case Providers::ProviderLauncher::Stopping:
|
||||
statusText = tr("stopping…");
|
||||
break;
|
||||
case Providers::ProviderLauncher::Failed:
|
||||
statusText = lastError.isEmpty() ? tr("failed")
|
||||
: tr("failed — %1").arg(lastError);
|
||||
statusText = lastError.isEmpty() ? tr("failed") : tr("failed — %1").arg(lastError);
|
||||
break;
|
||||
}
|
||||
m_launchStatusPill->setText(statusText);
|
||||
@@ -473,8 +479,9 @@ void ProviderDetailPane::changeEvent(QEvent *event)
|
||||
{
|
||||
QWidget::changeEvent(event);
|
||||
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) {
|
||||
applyPreviewPalette();
|
||||
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)
|
||||
{
|
||||
m_editing = on;
|
||||
m_identitySection->setVisible(on);
|
||||
m_nameEdit->setReadOnly(!on);
|
||||
m_descriptionEdit->setReadOnly(!on);
|
||||
m_urlEdit->setReadOnly(!on);
|
||||
@@ -510,15 +518,6 @@ Providers::ProviderInstance ProviderDetailPane::collectEdits() const
|
||||
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()
|
||||
{
|
||||
if (!m_launchTerminal)
|
||||
|
||||
@@ -57,7 +57,6 @@ protected:
|
||||
private:
|
||||
void setEditing(bool on);
|
||||
Providers::ProviderInstance collectEdits() const;
|
||||
void applyPreviewPalette();
|
||||
void applyTerminalPalette();
|
||||
|
||||
bool m_editing = false;
|
||||
@@ -65,6 +64,7 @@ private:
|
||||
Providers::ProviderInstance m_current;
|
||||
|
||||
QLabel *m_nameLabel = nullptr;
|
||||
QLabel *m_sourceBadge = nullptr;
|
||||
QLabel *m_sourcePathLabel = nullptr;
|
||||
QPushButton *m_editBtn = nullptr;
|
||||
QPushButton *m_openInEditorBtn = nullptr;
|
||||
@@ -75,11 +75,11 @@ private:
|
||||
|
||||
QLabel *m_descriptionLabel = nullptr;
|
||||
|
||||
SectionBox *m_identitySection = nullptr;
|
||||
QLineEdit *m_nameEdit = nullptr;
|
||||
QLabel *m_protocolLabel = nullptr;
|
||||
QPlainTextEdit *m_descriptionEdit = nullptr;
|
||||
QLineEdit *m_urlEdit = nullptr;
|
||||
QLabel *m_samplePreview = nullptr;
|
||||
|
||||
QLineEdit *m_apiKeyEdit = nullptr;
|
||||
QToolButton *m_revealKeyBtn = nullptr;
|
||||
|
||||
@@ -4,21 +4,22 @@
|
||||
|
||||
#include "ProviderListItem.hpp"
|
||||
|
||||
#include <utils/elidinglabel.h>
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QScopedValueRollback>
|
||||
#include <QVBoxLayout>
|
||||
#include <QStringList>
|
||||
|
||||
#include "ProviderInstance.hpp"
|
||||
#include "SettingsTheme.hpp"
|
||||
#include "SettingsUiBuilders.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
ProviderListItem::ProviderListItem(
|
||||
const Providers::ProviderInstance &inst, QWidget *parent)
|
||||
ProviderListItem::ProviderListItem(const Providers::ProviderInstance &inst, QWidget *parent)
|
||||
: QFrame(parent)
|
||||
, m_name(inst.name)
|
||||
{
|
||||
@@ -27,35 +28,45 @@ ProviderListItem::ProviderListItem(
|
||||
setAutoFillBackground(true);
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
|
||||
auto *headerRow = new QHBoxLayout;
|
||||
headerRow->setContentsMargins(0, 0, 0, 0);
|
||||
headerRow->setSpacing(6);
|
||||
m_statusDot = new QLabel(QStringLiteral("●"), this);
|
||||
QFont df = m_statusDot->font();
|
||||
df.setPixelSize(11);
|
||||
m_statusDot->setFont(df);
|
||||
m_statusDot->setStyleSheet(QStringLiteral("color: %1;").arg(statusColor(Status::Unknown)));
|
||||
m_nameLabel = new QLabel(inst.name, this);
|
||||
|
||||
m_nameLabel = new QLabel(this);
|
||||
QFont nf = m_nameLabel->font();
|
||||
nf.setBold(true);
|
||||
nf.setPixelSize(12);
|
||||
m_nameLabel->setFont(nf);
|
||||
headerRow->addWidget(m_statusDot, 0, Qt::AlignVCenter);
|
||||
headerRow->addWidget(m_nameLabel, 1);
|
||||
m_nameLabel->setTextFormat(Qt::RichText);
|
||||
|
||||
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->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
QPalette up = m_urlLabel->palette();
|
||||
up.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
|
||||
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);
|
||||
outer->setContentsMargins(5, 6, 8, 6);
|
||||
outer->setSpacing(2);
|
||||
outer->addLayout(headerRow);
|
||||
outer->addWidget(m_urlLabel);
|
||||
auto *row = new QHBoxLayout(this);
|
||||
row->setContentsMargins(8, 4, 8, 4);
|
||||
row->setSpacing(6);
|
||||
row->addWidget(m_statusDot, 0, Qt::AlignVCenter);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -73,6 +84,19 @@ void ProviderListItem::setSelected(bool s)
|
||||
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)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
@@ -111,6 +135,7 @@ void ProviderListItem::applyTheme()
|
||||
"#ProvListItem { background:transparent;"
|
||||
" border-top:1px solid %1; border-left:3px solid %2; }")
|
||||
.arg(cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)), accent));
|
||||
updateNameText();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
class QLabel;
|
||||
class QMouseEvent;
|
||||
|
||||
namespace Utils {
|
||||
class ElidingLabel;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
struct ProviderInstance;
|
||||
}
|
||||
@@ -21,11 +25,11 @@ class ProviderListItem : public QFrame
|
||||
public:
|
||||
enum class Status : int { Unknown, Ok, Fail };
|
||||
|
||||
explicit ProviderListItem(
|
||||
const Providers::ProviderInstance &inst, QWidget *parent = nullptr);
|
||||
explicit ProviderListItem(const Providers::ProviderInstance &inst, QWidget *parent = nullptr);
|
||||
|
||||
void setStatus(Status s);
|
||||
void setSelected(bool s);
|
||||
void setFilterHighlight(const QString &lowerFilter);
|
||||
QString providerName() const { return m_name; }
|
||||
|
||||
signals:
|
||||
@@ -38,14 +42,16 @@ protected:
|
||||
private:
|
||||
static QString statusColor(Status s);
|
||||
void applyTheme();
|
||||
void updateNameText();
|
||||
|
||||
QString m_name;
|
||||
QString m_filter;
|
||||
Status m_status = Status::Unknown;
|
||||
bool m_selected = false;
|
||||
bool m_inApplyTheme = false;
|
||||
QLabel *m_statusDot = nullptr;
|
||||
QLabel *m_nameLabel = nullptr;
|
||||
QLabel *m_urlLabel = nullptr;
|
||||
Utils::ElidingLabel *m_urlLabel = nullptr;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
@@ -103,7 +104,10 @@ public:
|
||||
headerSep->setFrameShadow(QFrame::Sunken);
|
||||
|
||||
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->setWidgetResizable(true);
|
||||
@@ -127,55 +131,88 @@ public:
|
||||
leftLay->addWidget(m_listScroll, 1);
|
||||
|
||||
m_detailPane = new ProviderDetailPane(this);
|
||||
connect(m_detailPane, &ProviderDetailPane::saveRequested,
|
||||
this, &ProvidersPageWidget::onSaveEdited);
|
||||
connect(m_detailPane, &ProviderDetailPane::duplicateRequested,
|
||||
this, &ProvidersPageWidget::onDuplicateClicked);
|
||||
connect(m_detailPane, &ProviderDetailPane::deleteRequested,
|
||||
this, &ProvidersPageWidget::onRemoveClicked);
|
||||
connect(m_detailPane, &ProviderDetailPane::apiKeySaveRequested,
|
||||
this, &ProvidersPageWidget::onApiKeySave);
|
||||
connect(m_detailPane, &ProviderDetailPane::apiKeyClearRequested,
|
||||
this, &ProvidersPageWidget::onApiKeyClear);
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::saveRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::onSaveEdited);
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::duplicateRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::onDuplicateClicked);
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::deleteRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::onRemoveClicked);
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::apiKeySaveRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::onApiKeySave);
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::apiKeyClearRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::onApiKeyClear);
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::apiKeyRevealRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::onApiKeyReveal);
|
||||
connect(m_detailPane, &ProviderDetailPane::launchStartRequested,
|
||||
this, &ProvidersPageWidget::onLaunchStart);
|
||||
connect(m_detailPane, &ProviderDetailPane::launchStopRequested,
|
||||
this, &ProvidersPageWidget::onLaunchStop);
|
||||
connect(m_detailPane, &ProviderDetailPane::launchRestartRequested,
|
||||
this, &ProvidersPageWidget::onLaunchRestart);
|
||||
connect(m_detailPane, &ProviderDetailPane::openInEditorRequested,
|
||||
this, [this](const QString &path) {
|
||||
if (path.isEmpty() || path.startsWith(QLatin1String(":/"))) {
|
||||
QMessageBox::information(
|
||||
this, tr("Open in editor"),
|
||||
tr("Bundled providers are read-only. "
|
||||
"Use Duplicate to create an editable user copy first."));
|
||||
return;
|
||||
}
|
||||
Core::EditorManager::openEditor(Utils::FilePath::fromString(path));
|
||||
});
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::launchStartRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::onLaunchStart);
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::launchStopRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::onLaunchStop);
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::launchRestartRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::onLaunchRestart);
|
||||
connect(
|
||||
m_detailPane,
|
||||
&ProviderDetailPane::openInEditorRequested,
|
||||
this,
|
||||
[this](const QString &path) {
|
||||
if (path.isEmpty() || path.startsWith(QLatin1String(":/"))) {
|
||||
QMessageBox::information(
|
||||
this,
|
||||
tr("Open in editor"),
|
||||
tr("Bundled providers are read-only. "
|
||||
"Use Duplicate to create an editable user copy first."));
|
||||
return;
|
||||
}
|
||||
Core::EditorManager::openEditor(Utils::FilePath::fromString(path));
|
||||
});
|
||||
if (m_launcher) {
|
||||
connect(m_launcher.data(), &Providers::ProviderLauncher::stateChanged,
|
||||
this, [this](const QString &name,
|
||||
Providers::ProviderLauncher::State newState) {
|
||||
if (name == m_currentName)
|
||||
refreshDetailLaunch();
|
||||
const ProviderListItem::Status status = rowStatusFromState(newState);
|
||||
for (auto *row : m_rows) {
|
||||
if (row->providerName() == name)
|
||||
row->setStatus(status);
|
||||
}
|
||||
});
|
||||
connect(m_launcher.data(), &Providers::ProviderLauncher::bytesReceived,
|
||||
this, [this](const QString &name, const QByteArray &chunk) {
|
||||
if (name == m_currentName)
|
||||
m_detailPane->appendLaunchBytes(chunk);
|
||||
});
|
||||
connect(
|
||||
m_launcher.data(),
|
||||
&Providers::ProviderLauncher::stateChanged,
|
||||
this,
|
||||
[this](const QString &name, Providers::ProviderLauncher::State newState) {
|
||||
if (name == m_currentName)
|
||||
refreshDetailLaunch();
|
||||
const ProviderListItem::Status status = rowStatusFromState(newState);
|
||||
for (auto *row : m_rows) {
|
||||
if (row->providerName() == name)
|
||||
row->setStatus(status);
|
||||
}
|
||||
});
|
||||
connect(
|
||||
m_launcher.data(),
|
||||
&Providers::ProviderLauncher::bytesReceived,
|
||||
this,
|
||||
[this](const QString &name, const QByteArray &chunk) {
|
||||
if (name == m_currentName)
|
||||
m_detailPane->appendLaunchBytes(chunk);
|
||||
});
|
||||
}
|
||||
m_detailScroll = new QScrollArea(this);
|
||||
m_detailScroll->setWidgetResizable(true);
|
||||
@@ -199,26 +236,34 @@ public:
|
||||
m_filterDebounce = new QTimer(this);
|
||||
m_filterDebounce->setSingleShot(true);
|
||||
m_filterDebounce->setInterval(100);
|
||||
connect(m_filterDebounce, &QTimer::timeout, this,
|
||||
&ProvidersPageWidget::rebuildList);
|
||||
connect(m_filterEdit, &QLineEdit::textChanged, this,
|
||||
[this](const QString &) { m_filterDebounce->start(); });
|
||||
connect(m_filterDebounce, &QTimer::timeout, this, &ProvidersPageWidget::rebuildList);
|
||||
connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &) {
|
||||
m_filterDebounce->start();
|
||||
});
|
||||
|
||||
if (m_factory) {
|
||||
connect(m_factory.data(),
|
||||
&Providers::ProviderInstanceFactory::instancesReloaded,
|
||||
this, &ProvidersPageWidget::rebuildList);
|
||||
connect(
|
||||
m_factory.data(),
|
||||
&Providers::ProviderInstanceFactory::instancesReloaded,
|
||||
this,
|
||||
&ProvidersPageWidget::rebuildList);
|
||||
}
|
||||
if (m_navigator) {
|
||||
connect(m_navigator.data(),
|
||||
&ProvidersPageNavigator::selectInstanceRequested,
|
||||
this, &ProvidersPageWidget::selectInstance);
|
||||
connect(
|
||||
m_navigator.data(),
|
||||
&ProvidersPageNavigator::selectInstanceRequested,
|
||||
this,
|
||||
&ProvidersPageWidget::selectInstance);
|
||||
}
|
||||
|
||||
rebuildList();
|
||||
|
||||
const QString pending
|
||||
= m_navigator ? m_navigator->takePendingSelection() : QString{};
|
||||
QTimer::singleShot(0, this, [this] {
|
||||
m_filterEdit->setFocus(Qt::OtherFocusReason);
|
||||
m_filterEdit->selectAll();
|
||||
});
|
||||
|
||||
const QString pending = m_navigator ? m_navigator->takePendingSelection() : QString{};
|
||||
if (!pending.isEmpty())
|
||||
selectInstance(pending);
|
||||
else if (m_factory && !m_factory->instances().empty())
|
||||
@@ -227,6 +272,23 @@ public:
|
||||
|
||||
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:
|
||||
void rebuildList()
|
||||
{
|
||||
@@ -245,9 +307,9 @@ private slots:
|
||||
auto matches = [&](const Providers::ProviderInstance &inst) {
|
||||
if (filter.isEmpty())
|
||||
return true;
|
||||
return inst.name.toLower().contains(filter)
|
||||
|| inst.clientApi.toLower().contains(filter)
|
||||
|| inst.url.toLower().contains(filter);
|
||||
return inst.name.toLower().contains(filter) || inst.clientApi.toLower().contains(filter)
|
||||
|| inst.url.toLower().contains(filter)
|
||||
|| inst.description.toLower().contains(filter);
|
||||
};
|
||||
|
||||
auto addSection = [&](const QString &title, bool userSection) {
|
||||
@@ -262,17 +324,18 @@ private slots:
|
||||
continue;
|
||||
sorted.push_back(&inst);
|
||||
}
|
||||
std::sort(sorted.begin(), sorted.end(),
|
||||
[](const Providers::ProviderInstance *a,
|
||||
const Providers::ProviderInstance *b) {
|
||||
return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
|
||||
});
|
||||
std::sort(
|
||||
sorted.begin(),
|
||||
sorted.end(),
|
||||
[](const Providers::ProviderInstance *a, const Providers::ProviderInstance *b) {
|
||||
return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
|
||||
});
|
||||
|
||||
int shown = 0;
|
||||
for (const auto *inst : sorted) {
|
||||
auto *row = new ProviderListItem(*inst, m_listContent);
|
||||
connect(row, &ProviderListItem::clicked,
|
||||
this, &ProvidersPageWidget::selectInstance);
|
||||
row->setFilterHighlight(filter);
|
||||
connect(row, &ProviderListItem::clicked, this, &ProvidersPageWidget::selectInstance);
|
||||
if (m_launcher)
|
||||
row->setStatus(rowStatusFromLauncher(inst->name));
|
||||
m_rows.append(row);
|
||||
@@ -281,8 +344,7 @@ private slots:
|
||||
}
|
||||
if (shown == 0) {
|
||||
auto *empty = new QLabel(
|
||||
userSection ? tr("No user instances yet.")
|
||||
: tr("No bundled instances loaded."),
|
||||
userSection ? tr("No user instances yet.") : tr("No bundled instances loaded."),
|
||||
m_listContent);
|
||||
empty->setContentsMargins(10, 6, 10, 6);
|
||||
QPalette ep = empty->palette();
|
||||
@@ -322,21 +384,25 @@ private slots:
|
||||
{
|
||||
if (!m_factory || m_currentName.isEmpty())
|
||||
return;
|
||||
const Providers::ProviderInstance *srcPtr
|
||||
= m_factory->instanceByName(m_currentName);
|
||||
const Providers::ProviderInstance *srcPtr = m_factory->instanceByName(m_currentName);
|
||||
if (!srcPtr)
|
||||
return;
|
||||
const Providers::ProviderInstance srcCopy = *srcPtr;
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
this, tr("Duplicate provider"),
|
||||
tr("Name for the new provider:"), QLineEdit::Normal,
|
||||
QStringLiteral("%1 (copy)").arg(srcCopy.name), &ok);
|
||||
this,
|
||||
tr("Duplicate provider"),
|
||||
tr("Name for the new provider:"),
|
||||
QLineEdit::Normal,
|
||||
QStringLiteral("%1 (copy)").arg(srcCopy.name),
|
||||
&ok);
|
||||
if (!ok || name.trimmed().isEmpty())
|
||||
return;
|
||||
if (m_factory->instanceByName(name.trimmed())) {
|
||||
QMessageBox::warning(this, tr("Duplicate provider"),
|
||||
tr("An instance named '%1' already exists.").arg(name.trimmed()));
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
tr("Duplicate provider"),
|
||||
tr("An instance named '%1' already exists.").arg(name.trimmed()));
|
||||
return;
|
||||
}
|
||||
Providers::ProviderInstance copy = srcCopy;
|
||||
@@ -346,7 +412,8 @@ private slots:
|
||||
copy.overridesBundled = false;
|
||||
QString writeErr;
|
||||
if (Providers::ProviderInstanceWriter::writeToUserDir(
|
||||
copy, /*previousPath=*/QString{}, &writeErr).isEmpty()) {
|
||||
copy, /*previousPath=*/QString{}, &writeErr)
|
||||
.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Duplicate provider"), writeErr);
|
||||
return;
|
||||
}
|
||||
@@ -370,8 +437,7 @@ private slots:
|
||||
{
|
||||
if (!m_factory || m_currentName.isEmpty())
|
||||
return;
|
||||
const Providers::ProviderInstance *instPtr
|
||||
= m_factory->instanceByName(m_currentName);
|
||||
const Providers::ProviderInstance *instPtr = m_factory->instanceByName(m_currentName);
|
||||
if (!instPtr || !instPtr->isUserSource())
|
||||
return;
|
||||
|
||||
@@ -391,8 +457,8 @@ private slots:
|
||||
if (QMessageBox::question(this, tr("Delete provider"), question) != QMessageBox::Yes)
|
||||
return;
|
||||
if (!QFile::remove(sourcePath)) {
|
||||
QMessageBox::warning(this, tr("Delete provider"),
|
||||
tr("Failed to delete file:\n%1").arg(sourcePath));
|
||||
QMessageBox::warning(
|
||||
this, tr("Delete provider"), tr("Failed to delete file:\n%1").arg(sourcePath));
|
||||
return;
|
||||
}
|
||||
if (m_secrets && !apiKeyRef.isEmpty())
|
||||
@@ -418,8 +484,8 @@ private slots:
|
||||
if (e.apiKeyRef.isEmpty() || (nameChanged && e.apiKeyRef == priorRef))
|
||||
e.apiKeyRef = QStringLiteral("qodeassist/providers/%1").arg(e.name);
|
||||
|
||||
const QString validation = Providers::ProviderInstance::validate(
|
||||
e, m_factory->knownClientApis());
|
||||
const QString validation
|
||||
= Providers::ProviderInstance::validate(e, m_factory->knownClientApis());
|
||||
if (!validation.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Save"), validation);
|
||||
return;
|
||||
@@ -427,8 +493,8 @@ private slots:
|
||||
if (nameChanged) {
|
||||
const auto *clash = m_factory->instanceByName(e.name);
|
||||
if (clash) {
|
||||
QMessageBox::warning(this, tr("Save"),
|
||||
tr("An instance named '%1' already exists.").arg(e.name));
|
||||
QMessageBox::warning(
|
||||
this, tr("Save"), tr("An instance named '%1' already exists.").arg(e.name));
|
||||
return;
|
||||
}
|
||||
const QStringList referencing = agentsReferencing(priorName);
|
||||
@@ -449,20 +515,21 @@ private slots:
|
||||
}
|
||||
const QString softWarning = Providers::ProviderInstance::warnings(e);
|
||||
if (!softWarning.isEmpty()) {
|
||||
if (QMessageBox::warning(this, tr("Save"),
|
||||
softWarning + QStringLiteral("\n\n")
|
||||
+ tr("Save anyway?"),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No)
|
||||
if (QMessageBox::warning(
|
||||
this,
|
||||
tr("Save"),
|
||||
softWarning + QStringLiteral("\n\n") + tr("Save anyway?"),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No)
|
||||
!= QMessageBox::Yes)
|
||||
return;
|
||||
}
|
||||
|
||||
const QString previousPath
|
||||
= (prior && prior->isUserSource()) ? prior->sourcePath : QString{};
|
||||
const QString previousPath = (prior && prior->isUserSource()) ? prior->sourcePath
|
||||
: QString{};
|
||||
QString writeErr;
|
||||
const QString writtenPath = Providers::ProviderInstanceWriter::writeToUserDir(
|
||||
e, previousPath, &writeErr);
|
||||
const QString writtenPath
|
||||
= Providers::ProviderInstanceWriter::writeToUserDir(e, previousPath, &writeErr);
|
||||
if (writtenPath.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Save"), writeErr);
|
||||
return;
|
||||
@@ -472,7 +539,8 @@ private slots:
|
||||
!= QFileInfo(previousPath).absoluteFilePath()) {
|
||||
if (!QFile::remove(previousPath)) {
|
||||
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"
|
||||
"Two provider files now describe this instance — delete the "
|
||||
"old file manually to avoid a duplicate-name error.")
|
||||
@@ -515,15 +583,13 @@ private slots:
|
||||
{
|
||||
if (!m_factory || !m_secrets || m_currentName.isEmpty())
|
||||
return;
|
||||
const Providers::ProviderInstance *instPtr
|
||||
= m_factory->instanceByName(m_currentName);
|
||||
const Providers::ProviderInstance *instPtr = m_factory->instanceByName(m_currentName);
|
||||
if (!instPtr || instPtr->apiKeyRef.isEmpty())
|
||||
return;
|
||||
const QString instName = instPtr->name;
|
||||
const QString apiKeyRef = instPtr->apiKeyRef;
|
||||
if (QMessageBox::question(
|
||||
this, tr("Clear API key"),
|
||||
tr("Erase the stored API key for '%1'?").arg(instName))
|
||||
this, tr("Clear API key"), tr("Erase the stored API key for '%1'?").arg(instName))
|
||||
!= QMessageBox::Yes)
|
||||
return;
|
||||
m_secrets->eraseKey(apiKeyRef);
|
||||
@@ -558,6 +624,25 @@ private slots:
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!m_factory)
|
||||
@@ -567,14 +652,13 @@ private:
|
||||
m_detailPane->clear();
|
||||
return;
|
||||
}
|
||||
const bool hasStoredKey
|
||||
= m_secrets && !inst->apiKeyRef.isEmpty() && m_secrets->hasKey(inst->apiKeyRef);
|
||||
const bool hasStoredKey = m_secrets && !inst->apiKeyRef.isEmpty()
|
||||
&& m_secrets->hasKey(inst->apiKeyRef);
|
||||
m_detailPane->populate(*inst, hasStoredKey);
|
||||
|
||||
if (m_launcher) {
|
||||
m_detailPane->setLaunchState(
|
||||
m_launcher->state(inst->name),
|
||||
m_launcher->lastError(inst->name));
|
||||
m_detailPane
|
||||
->setLaunchState(m_launcher->state(inst->name), m_launcher->lastError(inst->name));
|
||||
m_detailPane->resetLaunchTerminal(m_launcher->scrollback(inst->name));
|
||||
} else {
|
||||
m_detailPane->setLaunchState(Providers::ProviderLauncher::Idle, {});
|
||||
@@ -607,13 +691,11 @@ private:
|
||||
{
|
||||
if (!m_launcher || m_currentName.isEmpty())
|
||||
return;
|
||||
m_detailPane->setLaunchState(
|
||||
m_launcher->state(m_currentName),
|
||||
m_launcher->lastError(m_currentName));
|
||||
m_detailPane
|
||||
->setLaunchState(m_launcher->state(m_currentName), m_launcher->lastError(m_currentName));
|
||||
}
|
||||
|
||||
static ProviderListItem::Status rowStatusFromState(
|
||||
Providers::ProviderLauncher::State state)
|
||||
static ProviderListItem::Status rowStatusFromState(Providers::ProviderLauncher::State state)
|
||||
{
|
||||
switch (state) {
|
||||
case Providers::ProviderLauncher::Ready:
|
||||
|
||||
@@ -28,6 +28,49 @@ void applyMutedSmallCaps(QLabel *label)
|
||||
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)
|
||||
{
|
||||
auto *header = new QLabel(title.toUpper(), parent);
|
||||
|
||||
@@ -16,6 +16,10 @@ namespace QodeAssist::Settings {
|
||||
|
||||
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 *makeHintLabel(const QString &text, QWidget *parent = nullptr);
|
||||
|
||||
@@ -58,6 +58,15 @@ void TagFilterStrip::setAvailableTags(
|
||||
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)
|
||||
{
|
||||
for (auto it = m_chipByTag.cbegin(); it != m_chipByTag.cend(); ++it)
|
||||
@@ -161,14 +170,12 @@ void TagFilterStrip::rebuild()
|
||||
sorted.reserve(m_counts.size());
|
||||
for (auto it = m_counts.cbegin(); it != m_counts.cend(); ++it)
|
||||
sorted.emplace_back(it.key(), it.value());
|
||||
std::sort(sorted.begin(), sorted.end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
if (a.second != b.second)
|
||||
return a.second > b.second;
|
||||
return a.first.localeAwareCompare(b.first) < 0;
|
||||
});
|
||||
std::sort(sorted.begin(), sorted.end(), [](const auto &a, const auto &b) {
|
||||
if (a.second != b.second)
|
||||
return a.second > b.second;
|
||||
return a.first.localeAwareCompare(b.first) < 0;
|
||||
});
|
||||
|
||||
constexpr int kMaxColumns = 3;
|
||||
auto *gridHost = new QWidget(this);
|
||||
auto *grid = new QGridLayout(gridHost);
|
||||
grid->setContentsMargins(0, 0, 0, 0);
|
||||
@@ -181,7 +188,7 @@ void TagFilterStrip::rebuild()
|
||||
connect(chip, &TagChip::clicked, this, &TagFilterStrip::toggleTag);
|
||||
grid->addWidget(chip, gridRow, col, Qt::AlignLeft);
|
||||
m_chipByTag.insert(tag, chip);
|
||||
if (++col >= kMaxColumns) {
|
||||
if (++col >= m_maxColumns) {
|
||||
col = 0;
|
||||
++gridRow;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
void setAvailableTags(const QMap<QString, int> &countsByTag);
|
||||
void setAvailableTags(const QMap<QString, int> &countsByTag, const QSet<QString> &activeTags);
|
||||
void setVisibleCounts(const QMap<QString, int> &countsByTag);
|
||||
void setMaxColumns(int columns);
|
||||
const QSet<QString> &activeTags() const { return m_activeTags; }
|
||||
void toggleTag(const QString &tag);
|
||||
|
||||
@@ -42,6 +43,7 @@ private:
|
||||
|
||||
QMap<QString, int> m_counts;
|
||||
QSet<QString> m_activeTags;
|
||||
int m_maxColumns = 3;
|
||||
QVBoxLayout *m_layout = nullptr;
|
||||
QHash<QString, TagChip *> m_chipByTag;
|
||||
QLabel *m_clearLink = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user