mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 10:59:30 -04:00
feat: Add agents pipelines
This commit is contained in:
@@ -174,6 +174,8 @@ add_qtc_plugin(QodeAssist
|
|||||||
|
|
||||||
if(QODEASSIST_EXPERIMENTAL)
|
if(QODEASSIST_EXPERIMENTAL)
|
||||||
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
|
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
|
||||||
|
add_subdirectory(sources/settings)
|
||||||
|
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
#ifdef QODEASSIST_EXPERIMENTAL
|
#ifdef QODEASSIST_EXPERIMENTAL
|
||||||
#include "settings/AgentsSettingsPage.hpp"
|
#include "settings/AgentsSettingsPage.hpp"
|
||||||
#include "settings/ProvidersSettingsPage.hpp"
|
#include "settings/ProvidersSettingsPage.hpp"
|
||||||
|
#include "sources/settings/AgentPipelinesPage.hpp"
|
||||||
#endif
|
#endif
|
||||||
#include "settings/QuickRefactorSettings.hpp"
|
#include "settings/QuickRefactorSettings.hpp"
|
||||||
#include "settings/SettingsConstants.hpp"
|
#include "settings/SettingsConstants.hpp"
|
||||||
@@ -222,6 +223,10 @@ public:
|
|||||||
m_agentsPageNavigator = new Settings::AgentsPageNavigator(this);
|
m_agentsPageNavigator = new Settings::AgentsPageNavigator(this);
|
||||||
m_agentsOptionsPage = Settings::createAgentsSettingsPage(
|
m_agentsOptionsPage = Settings::createAgentsSettingsPage(
|
||||||
m_agentFactory, m_agentsPageNavigator);
|
m_agentFactory, m_agentsPageNavigator);
|
||||||
|
|
||||||
|
m_agentPipelinesPageNavigator = new Settings::AgentPipelinesPageNavigator(this);
|
||||||
|
m_agentPipelinesOptionsPage = Settings::createAgentPipelinesSettingsPage(
|
||||||
|
m_agentFactory, m_agentPipelinesPageNavigator, m_agentsPageNavigator);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
m_mcpServerManager = new Mcp::McpServerManager(this);
|
m_mcpServerManager = new Mcp::McpServerManager(this);
|
||||||
@@ -527,6 +532,8 @@ private:
|
|||||||
QPointer<AgentFactory> m_agentFactory;
|
QPointer<AgentFactory> m_agentFactory;
|
||||||
QPointer<Settings::AgentsPageNavigator> m_agentsPageNavigator;
|
QPointer<Settings::AgentsPageNavigator> m_agentsPageNavigator;
|
||||||
std::unique_ptr<Core::IOptionsPage> m_agentsOptionsPage;
|
std::unique_ptr<Core::IOptionsPage> m_agentsOptionsPage;
|
||||||
|
QPointer<Settings::AgentPipelinesPageNavigator> m_agentPipelinesPageNavigator;
|
||||||
|
std::unique_ptr<Core::IOptionsPage> m_agentPipelinesOptionsPage;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.7ProviderSettin
|
|||||||
// Agents Settings Page ID
|
// Agents Settings Page ID
|
||||||
const char QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID[] = "QodeAssist.8AgentsSettingsPageId";
|
const char QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID[] = "QodeAssist.8AgentsSettingsPageId";
|
||||||
|
|
||||||
|
// Agent Pipelines (experimental) settings
|
||||||
|
const char QODE_ASSIST_AGENT_PIPELINES_PAGE_ID[] = "QodeAssist.9AgentPipelinesPageId";
|
||||||
|
|
||||||
// Provider API Keys
|
// Provider API Keys
|
||||||
const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey";
|
const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey";
|
||||||
const char OPEN_ROUTER_API_KEY_HISTORY[] = "QodeAssist.openRouterApiKeyHistory";
|
const char OPEN_ROUTER_API_KEY_HISTORY[] = "QodeAssist.openRouterApiKeyHistory";
|
||||||
|
|||||||
@@ -33,6 +33,18 @@ inline const char *CUSTOM_ENDPOINT = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Custo
|
|||||||
inline const char *CODE_COMPLETION = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Code Completion");
|
inline const char *CODE_COMPLETION = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Code Completion");
|
||||||
inline const char *CHAT_ASSISTANT = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Chat Assistant");
|
inline const char *CHAT_ASSISTANT = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Chat Assistant");
|
||||||
inline const char *QUICK_REFACTOR = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Quick Refactor");
|
inline const char *QUICK_REFACTOR = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Quick Refactor");
|
||||||
|
inline const char *CHAT_COMPRESSION = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Chat Compression");
|
||||||
|
inline const char *AGENT_PIPELINES = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Agent Pipelines");
|
||||||
|
inline const char *SLOT_HINT_CODE_COMPLETION = QT_TRANSLATE_NOOP(
|
||||||
|
"QtC::QodeAssist",
|
||||||
|
"Inline completions while you type. Matchers run on every request.");
|
||||||
|
inline const char *SLOT_HINT_CHAT_ASSISTANT = QT_TRANSLATE_NOOP(
|
||||||
|
"QtC::QodeAssist", "Conversational assistant in the QodeAssist panel.");
|
||||||
|
inline const char *SLOT_HINT_CHAT_COMPRESSION = QT_TRANSLATE_NOOP(
|
||||||
|
"QtC::QodeAssist",
|
||||||
|
"Used when a chat conversation needs to be summarised to stay within context.");
|
||||||
|
inline const char *SLOT_HINT_QUICK_REFACTOR = QT_TRANSLATE_NOOP(
|
||||||
|
"QtC::QodeAssist", "Inline editor-driven refactors via the Quick Refactor action.");
|
||||||
inline const char *RESET_SETTINGS = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Reset Settings");
|
inline const char *RESET_SETTINGS = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Reset Settings");
|
||||||
inline const char *CONFIRMATION = QT_TRANSLATE_NOOP(
|
inline const char *CONFIRMATION = QT_TRANSLATE_NOOP(
|
||||||
"QtC::QodeAssist", "Are you sure you want to reset all settings to default values?");
|
"QtC::QodeAssist", "Are you sure you want to reset all settings to default values?");
|
||||||
|
|||||||
273
sources/settings/AgentPipelinesPage.cpp
Normal file
273
sources/settings/AgentPipelinesPage.cpp
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "AgentPipelinesPage.hpp"
|
||||||
|
|
||||||
|
#include <coreplugin/dialogs/ioptionspage.h>
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "../../Version.hpp"
|
||||||
|
#include "AgentRosterWidget.hpp"
|
||||||
|
#include "AgentsSettingsPage.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "PipelinesConfig.hpp"
|
||||||
|
#include "SettingsConstants.hpp"
|
||||||
|
#include "SettingsTr.hpp"
|
||||||
|
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
AgentPipelinesPageNavigator::AgentPipelinesPageNavigator(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kSaveDebounceMs = 300;
|
||||||
|
|
||||||
|
struct SlotMeta
|
||||||
|
{
|
||||||
|
const char *title;
|
||||||
|
const char *hint;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SlotMeta kSlotMeta[] = {
|
||||||
|
{TrConstants::CODE_COMPLETION, TrConstants::SLOT_HINT_CODE_COMPLETION},
|
||||||
|
{TrConstants::CHAT_ASSISTANT, TrConstants::SLOT_HINT_CHAT_ASSISTANT},
|
||||||
|
{TrConstants::CHAT_COMPRESSION, TrConstants::SLOT_HINT_CHAT_COMPRESSION},
|
||||||
|
{TrConstants::QUICK_REFACTOR, TrConstants::SLOT_HINT_QUICK_REFACTOR},
|
||||||
|
};
|
||||||
|
|
||||||
|
class AgentPipelinesPageWidget : public Core::IOptionsPageWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY_MOVE(AgentPipelinesPageWidget)
|
||||||
|
public:
|
||||||
|
AgentPipelinesPageWidget(
|
||||||
|
const QPointer<AgentFactory> &agentFactory,
|
||||||
|
const QPointer<AgentPipelinesPageNavigator> &navigator,
|
||||||
|
const QPointer<AgentsPageNavigator> &agentsNavigator)
|
||||||
|
: m_agentFactory(agentFactory)
|
||||||
|
, m_navigator(navigator)
|
||||||
|
, m_agentsNavigator(agentsNavigator)
|
||||||
|
{
|
||||||
|
m_titleLabel = new QLabel(Tr::tr(TrConstants::AGENT_PIPELINES), this);
|
||||||
|
QFont tf = m_titleLabel->font();
|
||||||
|
tf.setBold(true);
|
||||||
|
tf.setPixelSize(13);
|
||||||
|
m_titleLabel->setFont(tf);
|
||||||
|
|
||||||
|
m_resetBtn = new QPushButton(Tr::tr(TrConstants::RESET_TO_DEFAULTS), this);
|
||||||
|
|
||||||
|
auto *headerRow = new QHBoxLayout;
|
||||||
|
headerRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
headerRow->setSpacing(8);
|
||||||
|
headerRow->addWidget(m_titleLabel);
|
||||||
|
headerRow->addStretch(1);
|
||||||
|
headerRow->addWidget(m_resetBtn);
|
||||||
|
|
||||||
|
auto *headerSep = new QFrame(this);
|
||||||
|
headerSep->setFrameShape(QFrame::HLine);
|
||||||
|
headerSep->setFrameShadow(QFrame::Sunken);
|
||||||
|
|
||||||
|
m_rosters[0] = new AgentRosterWidget(this);
|
||||||
|
m_rosters[1] = new AgentRosterWidget(this);
|
||||||
|
m_rosters[2] = new AgentRosterWidget(this);
|
||||||
|
m_rosters[3] = new AgentRosterWidget(this);
|
||||||
|
|
||||||
|
for (int i = 0; i < kRosterCount; ++i)
|
||||||
|
m_rosters[i]->setSlot(Tr::tr(kSlotMeta[i].title), Tr::tr(kSlotMeta[i].hint), {});
|
||||||
|
|
||||||
|
auto *content = new QWidget(this);
|
||||||
|
auto *contentLay = new QVBoxLayout(content);
|
||||||
|
contentLay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
contentLay->setSpacing(12);
|
||||||
|
for (int i = 0; i < kRosterCount; ++i)
|
||||||
|
contentLay->addWidget(m_rosters[i]);
|
||||||
|
contentLay->addStretch(1);
|
||||||
|
|
||||||
|
auto *scroll = new QScrollArea(this);
|
||||||
|
scroll->setWidgetResizable(true);
|
||||||
|
scroll->setFrameShape(QFrame::NoFrame);
|
||||||
|
scroll->setWidget(content);
|
||||||
|
|
||||||
|
auto *root = new QVBoxLayout(this);
|
||||||
|
root->setContentsMargins(8, 8, 8, 8);
|
||||||
|
root->setSpacing(6);
|
||||||
|
root->addLayout(headerRow);
|
||||||
|
root->addWidget(headerSep);
|
||||||
|
root->addWidget(scroll, 1);
|
||||||
|
|
||||||
|
m_saveDebounce = new QTimer(this);
|
||||||
|
m_saveDebounce->setSingleShot(true);
|
||||||
|
m_saveDebounce->setInterval(kSaveDebounceMs);
|
||||||
|
connect(m_saveDebounce, &QTimer::timeout, this, [this]() { persistRosters(); });
|
||||||
|
|
||||||
|
loadFromSettings();
|
||||||
|
|
||||||
|
connect(m_resetBtn, &QPushButton::clicked, this, &AgentPipelinesPageWidget::onReset);
|
||||||
|
|
||||||
|
for (int i = 0; i < kRosterCount; ++i) {
|
||||||
|
connect(m_rosters[i], &AgentRosterWidget::editAgentRequested, this,
|
||||||
|
&AgentPipelinesPageWidget::onEditAgent);
|
||||||
|
connect(m_rosters[i], &AgentRosterWidget::rosterChanged, this,
|
||||||
|
[this](const QStringList &) { m_saveDebounce->start(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~AgentPipelinesPageWidget() override
|
||||||
|
{
|
||||||
|
if (m_saveDebounce && m_saveDebounce->isActive()) {
|
||||||
|
m_saveDebounce->stop();
|
||||||
|
persistRosters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply() final
|
||||||
|
{
|
||||||
|
if (m_saveDebounce && m_saveDebounce->isActive())
|
||||||
|
m_saveDebounce->stop();
|
||||||
|
persistRosters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int kRosterCount = 4;
|
||||||
|
|
||||||
|
void persistRosters()
|
||||||
|
{
|
||||||
|
PipelineRosters rosters;
|
||||||
|
rosters.codeCompletion = m_rosters[0]->roster();
|
||||||
|
rosters.chatAssistant = m_rosters[1]->roster();
|
||||||
|
rosters.chatCompression = m_rosters[2]->roster();
|
||||||
|
rosters.quickRefactor = m_rosters[3]->roster();
|
||||||
|
QString err;
|
||||||
|
if (!PipelinesConfig::save(rosters, &err)) {
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] save failed (%1): %2")
|
||||||
|
.arg(PipelinesConfig::filePath(), err));
|
||||||
|
if (!m_saveErrorShown) {
|
||||||
|
m_saveErrorShown = true;
|
||||||
|
QMessageBox::warning(
|
||||||
|
Core::ICore::dialogParent(),
|
||||||
|
Tr::tr(TrConstants::AGENT_PIPELINES),
|
||||||
|
tr("Failed to save pipelines.toml:\n%1\n\n"
|
||||||
|
"Further save failures will only be logged.")
|
||||||
|
.arg(err));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m_saveErrorShown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onReset()
|
||||||
|
{
|
||||||
|
const auto reply = QMessageBox::question(
|
||||||
|
Core::ICore::dialogParent(),
|
||||||
|
Tr::tr(TrConstants::RESET_SETTINGS),
|
||||||
|
Tr::tr(TrConstants::CONFIRMATION),
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
|
||||||
|
if (reply != QMessageBox::Yes)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString err;
|
||||||
|
if (!PipelinesConfig::save(PipelineRosters::defaults(), &err))
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] failed to reset rosters: %1").arg(err));
|
||||||
|
|
||||||
|
m_saveErrorShown = false;
|
||||||
|
loadFromSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEditAgent(const QString &name)
|
||||||
|
{
|
||||||
|
if (m_agentsNavigator)
|
||||||
|
m_agentsNavigator->requestSelectAgent(name);
|
||||||
|
if (m_navigator)
|
||||||
|
emit m_navigator->editAgentRequested(name);
|
||||||
|
|
||||||
|
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 83)
|
||||||
|
Core::ICore::showSettings(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
|
||||||
|
#else
|
||||||
|
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadFromSettings()
|
||||||
|
{
|
||||||
|
const PipelinesLoadResult lr = PipelinesConfig::load();
|
||||||
|
if (lr.status == PipelinesLoadStatus::ParseError
|
||||||
|
|| lr.status == PipelinesLoadStatus::SchemaError) {
|
||||||
|
QMessageBox::warning(
|
||||||
|
Core::ICore::dialogParent(),
|
||||||
|
Tr::tr(TrConstants::AGENT_PIPELINES),
|
||||||
|
tr("pipelines.toml has issues — using defaults for affected entries:\n%1\n\n"
|
||||||
|
"Click OK to continue. Changes you make here will overwrite the file.")
|
||||||
|
.arg(lr.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentFactory *factory = m_agentFactory.data();
|
||||||
|
m_rosters[0]->setRoster(lr.rosters.codeCompletion, factory);
|
||||||
|
m_rosters[1]->setRoster(lr.rosters.chatAssistant, factory);
|
||||||
|
m_rosters[2]->setRoster(lr.rosters.chatCompression, factory);
|
||||||
|
m_rosters[3]->setRoster(lr.rosters.quickRefactor, factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointer<AgentFactory> m_agentFactory;
|
||||||
|
QPointer<AgentPipelinesPageNavigator> m_navigator;
|
||||||
|
QPointer<AgentsPageNavigator> m_agentsNavigator;
|
||||||
|
|
||||||
|
QLabel *m_titleLabel = nullptr;
|
||||||
|
QPushButton *m_resetBtn = nullptr;
|
||||||
|
|
||||||
|
AgentRosterWidget *m_rosters[kRosterCount] = {};
|
||||||
|
|
||||||
|
QTimer *m_saveDebounce = nullptr;
|
||||||
|
bool m_saveErrorShown = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AgentPipelinesOptionsPage final : public Core::IOptionsPage
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AgentPipelinesOptionsPage(
|
||||||
|
AgentFactory *agentFactory,
|
||||||
|
AgentPipelinesPageNavigator *navigator,
|
||||||
|
AgentsPageNavigator *agentsNavigator)
|
||||||
|
{
|
||||||
|
setId(Constants::QODE_ASSIST_AGENT_PIPELINES_PAGE_ID);
|
||||||
|
setDisplayName(Tr::tr(TrConstants::AGENT_PIPELINES));
|
||||||
|
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||||
|
const QPointer<AgentFactory> factoryPtr(agentFactory);
|
||||||
|
const QPointer<AgentPipelinesPageNavigator> navPtr(navigator);
|
||||||
|
const QPointer<AgentsPageNavigator> agentsNavPtr(agentsNavigator);
|
||||||
|
setWidgetCreator([factoryPtr, navPtr, agentsNavPtr] {
|
||||||
|
return new AgentPipelinesPageWidget(factoryPtr, navPtr, agentsNavPtr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
|
||||||
|
AgentFactory *agentFactory,
|
||||||
|
AgentPipelinesPageNavigator *navigator,
|
||||||
|
AgentsPageNavigator *agentsNavigator)
|
||||||
|
{
|
||||||
|
return std::make_unique<AgentPipelinesOptionsPage>(
|
||||||
|
agentFactory, navigator, agentsNavigator);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
|
|
||||||
|
#include "AgentPipelinesPage.moc"
|
||||||
39
sources/settings/AgentPipelinesPage.hpp
Normal file
39
sources/settings/AgentPipelinesPage.hpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class IOptionsPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class AgentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
class AgentsPageNavigator;
|
||||||
|
|
||||||
|
class AgentPipelinesPageNavigator : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY_MOVE(AgentPipelinesPageNavigator)
|
||||||
|
public:
|
||||||
|
explicit AgentPipelinesPageNavigator(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void editAgentRequested(const QString &agentName);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
|
||||||
|
AgentFactory *agentFactory,
|
||||||
|
AgentPipelinesPageNavigator *navigator,
|
||||||
|
AgentsPageNavigator *agentsNavigator);
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
636
sources/settings/AgentRosterWidget.cpp
Normal file
636
sources/settings/AgentRosterWidget.cpp
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "AgentRosterWidget.hpp"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QFontDatabase>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QStyle>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <Agent.hpp>
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
#include <AgentRouter.hpp>
|
||||||
|
|
||||||
|
#include "AgentSelectionDialog.hpp"
|
||||||
|
#include "SettingsTr.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class PillKind {
|
||||||
|
Template,
|
||||||
|
On, // capability on (thinking/tools)
|
||||||
|
Off, // capability off ("plain")
|
||||||
|
User, // user-defined agent
|
||||||
|
Active, // ✓ active
|
||||||
|
Match, // matched-this-row chip background
|
||||||
|
Tag, // free-form discoverability tag from AgentConfig::tags
|
||||||
|
Neutral,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Theme
|
||||||
|
{
|
||||||
|
bool dark = false;
|
||||||
|
|
||||||
|
QColor pageBg;
|
||||||
|
QColor cardBg;
|
||||||
|
QColor cardBorder;
|
||||||
|
QColor groupBorder;
|
||||||
|
QColor rowSeparator;
|
||||||
|
QColor rowMatchBg;
|
||||||
|
QColor listHeader;
|
||||||
|
|
||||||
|
QColor text;
|
||||||
|
QColor textSoft;
|
||||||
|
QColor textMute;
|
||||||
|
QColor textFaint;
|
||||||
|
|
||||||
|
QColor matchChipBg;
|
||||||
|
QColor matchChipBorder;
|
||||||
|
QColor matchChipText;
|
||||||
|
|
||||||
|
QColor codeBg;
|
||||||
|
QColor codeBorder;
|
||||||
|
|
||||||
|
struct Pill
|
||||||
|
{
|
||||||
|
QColor bg, fg, border;
|
||||||
|
};
|
||||||
|
Pill pill(PillKind k) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
Theme::Pill Theme::pill(PillKind k) const
|
||||||
|
{
|
||||||
|
if (dark) {
|
||||||
|
switch (k) {
|
||||||
|
case PillKind::Template: return {{0x2c, 0x3f, 0x5a}, {0xcf, 0xe1, 0xf7}, {0x4a, 0x62, 0x86}};
|
||||||
|
case PillKind::On: return {{0x2f, 0x45, 0x30}, {0xbc, 0xe0, 0xbd}, {0x4a, 0x6c, 0x4b}};
|
||||||
|
case PillKind::Off: return {{0x3a, 0x3a, 0x3a}, {0x8a, 0x8a, 0x8a}, {0x4a, 0x4a, 0x4a}};
|
||||||
|
case PillKind::User: return {{0x4a, 0x3f, 0x24}, {0xe6, 0xcd, 0x92}, {0x6a, 0x5a, 0x30}};
|
||||||
|
case PillKind::Active: return {{0x3a, 0x6a, 0x28}, {0xff, 0xff, 0xff}, {0x4a, 0x80, 0x38}};
|
||||||
|
case PillKind::Match: return {{0x27, 0x38, 0x4a}, {0xbd, 0xd7, 0xee}, {0x3f, 0x5a, 0x78}};
|
||||||
|
case PillKind::Tag: return {{0x2e, 0x2e, 0x3a}, {0xb9, 0xb9, 0xcf}, {0x46, 0x46, 0x5a}};
|
||||||
|
case PillKind::Neutral: return {{0x3a, 0x3a, 0x3a}, {0xcf, 0xcf, 0xcf}, {0x4a, 0x4a, 0x4a}};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (k) {
|
||||||
|
case PillKind::Template: return {{0xdb, 0xe7, 0xf6}, {0x1f, 0x3f, 0x73}, {0xa8, 0xc1, 0xe0}};
|
||||||
|
case PillKind::On: return {{0xdb, 0xe9, 0xd3}, {0x2c, 0x5a, 0x1c}, {0xa3, 0xbc, 0x97}};
|
||||||
|
case PillKind::Off: return {{0xec, 0xec, 0xec}, {0x7a, 0x7a, 0x7a}, {0xc8, 0xc8, 0xc8}};
|
||||||
|
case PillKind::User: return {{0xf0, 0xe4, 0xcf}, {0x75, 0x54, 0x1a}, {0xcd, 0xb9, 0x8a}};
|
||||||
|
case PillKind::Active: return {{0x2c, 0x5a, 0x1c}, {0xff, 0xff, 0xff}, {0x2c, 0x5a, 0x1c}};
|
||||||
|
case PillKind::Match: return {{0xd6, 0xe8, 0xf7}, {0x1a, 0x4a, 0x7a}, {0x8a, 0xb1, 0xd5}};
|
||||||
|
case PillKind::Tag: return {{0xe7, 0xe7, 0xf2}, {0x46, 0x46, 0x6e}, {0xc1, 0xc1, 0xd5}};
|
||||||
|
case PillKind::Neutral: return {{0xe3, 0xe3, 0xe3}, {0x3a, 0x3a, 0x3a}, {0xbc, 0xbc, 0xbc}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||||
|
}
|
||||||
|
|
||||||
|
Theme themeFor(const QWidget *w)
|
||||||
|
{
|
||||||
|
const QPalette pal = w ? w->palette() : QApplication::palette();
|
||||||
|
const bool dark = pal.color(QPalette::Window).lightness() < 128;
|
||||||
|
Theme t;
|
||||||
|
t.dark = dark;
|
||||||
|
if (dark) {
|
||||||
|
t.pageBg = {0x2b, 0x2b, 0x2b};
|
||||||
|
t.cardBg = {0x2f, 0x2f, 0x2f};
|
||||||
|
t.cardBorder = {0x4a, 0x4a, 0x4a};
|
||||||
|
t.groupBorder = {0x4a, 0x4a, 0x4a};
|
||||||
|
t.rowSeparator = {0x3a, 0x3a, 0x3a};
|
||||||
|
t.rowMatchBg = {0x2d, 0x3d, 0x24};
|
||||||
|
t.listHeader = {0x25, 0x25, 0x25};
|
||||||
|
t.text = {0xe6, 0xe6, 0xe6};
|
||||||
|
t.textSoft = {0xc2, 0xc2, 0xc2};
|
||||||
|
t.textMute = {0x9a, 0x9a, 0x9a};
|
||||||
|
t.textFaint = {0x7a, 0x7a, 0x7a};
|
||||||
|
t.matchChipBg = {0x26, 0x26, 0x26};
|
||||||
|
t.matchChipBorder = {0x3a, 0x3a, 0x3a};
|
||||||
|
t.matchChipText = {0xc2, 0xc2, 0xc2};
|
||||||
|
t.codeBg = {0x26, 0x26, 0x26};
|
||||||
|
t.codeBorder = {0x3a, 0x3a, 0x3a};
|
||||||
|
} else {
|
||||||
|
t.pageBg = {0xed, 0xed, 0xed};
|
||||||
|
t.cardBg = {0xf8, 0xf8, 0xf8};
|
||||||
|
t.cardBorder = {0xbd, 0xbd, 0xbd};
|
||||||
|
t.groupBorder = {0xb8, 0xb8, 0xb8};
|
||||||
|
t.rowSeparator = {0xe0, 0xe0, 0xe0};
|
||||||
|
t.rowMatchBg = {0xe8, 0xf3, 0xdf};
|
||||||
|
t.listHeader = {0xe8, 0xe8, 0xe8};
|
||||||
|
t.text = {0x1c, 0x1c, 0x1c};
|
||||||
|
t.textSoft = {0x3a, 0x3a, 0x3a};
|
||||||
|
t.textMute = {0x5a, 0x5a, 0x5a};
|
||||||
|
t.textFaint = {0x8a, 0x8a, 0x8a};
|
||||||
|
t.matchChipBg = {0xea, 0xea, 0xea};
|
||||||
|
t.matchChipBorder = {0xcf, 0xcf, 0xcf};
|
||||||
|
t.matchChipText = {0x3a, 0x3a, 0x3a};
|
||||||
|
t.codeBg = {0xea, 0xea, 0xea};
|
||||||
|
t.codeBorder = {0xcf, 0xcf, 0xcf};
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString hex(const QColor &c)
|
||||||
|
{
|
||||||
|
return c.name(QColor::HexRgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString pillStyle(const Theme &t, PillKind k)
|
||||||
|
{
|
||||||
|
const auto p = t.pill(k);
|
||||||
|
return QStringLiteral(
|
||||||
|
"background:%1; color:%2; border:1px solid %3;"
|
||||||
|
"padding:0px 5px; border-radius:2px;"
|
||||||
|
"font-size:10px;")
|
||||||
|
.arg(hex(p.bg), hex(p.fg), hex(p.border));
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel *makePill(const QString &text, PillKind kind, const Theme &t, QWidget *parent = nullptr)
|
||||||
|
{
|
||||||
|
auto *l = new QLabel(text, parent);
|
||||||
|
l->setStyleSheet(pillStyle(t, kind));
|
||||||
|
l->setAlignment(Qt::AlignCenter);
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MatchSummary
|
||||||
|
{
|
||||||
|
QString icon;
|
||||||
|
QString kind;
|
||||||
|
QString value;
|
||||||
|
};
|
||||||
|
|
||||||
|
MatchSummary summarise(const AgentConfig::Match &m)
|
||||||
|
{
|
||||||
|
if (m.isEmpty())
|
||||||
|
return {QStringLiteral("·"), AgentRosterWidget::tr("fallback"),
|
||||||
|
AgentRosterWidget::tr("any file")};
|
||||||
|
if (!m.filePatterns.isEmpty())
|
||||||
|
return {QStringLiteral("*"), AgentRosterWidget::tr("path"),
|
||||||
|
m.filePatterns.join(QStringLiteral(", "))};
|
||||||
|
if (!m.pathPatterns.isEmpty())
|
||||||
|
return {QStringLiteral("*"), AgentRosterWidget::tr("path"),
|
||||||
|
m.pathPatterns.join(QStringLiteral(", "))};
|
||||||
|
if (!m.projectNames.isEmpty())
|
||||||
|
return {QStringLiteral("▣"), AgentRosterWidget::tr("project"),
|
||||||
|
m.projectNames.join(QStringLiteral(", "))};
|
||||||
|
return {QStringLiteral("·"), AgentRosterWidget::tr("any"), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class AgentRosterRow : public QFrame
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
AgentRosterRow(int index,
|
||||||
|
const QString &name,
|
||||||
|
const AgentConfig *cfg,
|
||||||
|
bool active,
|
||||||
|
bool first,
|
||||||
|
bool last,
|
||||||
|
const Theme &theme,
|
||||||
|
QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void moveUpRequested(int index);
|
||||||
|
void moveDownRequested(int index);
|
||||||
|
void editRequested(int index);
|
||||||
|
void removeRequested(int index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QWidget *buildIdentityLine(const QString &displayName,
|
||||||
|
const QString &model,
|
||||||
|
bool active,
|
||||||
|
bool isUser,
|
||||||
|
const Theme &t);
|
||||||
|
QWidget *buildMetaLine(const AgentConfig *cfg, bool active, const Theme &t);
|
||||||
|
QWidget *buildActions(const Theme &t, bool first, bool last);
|
||||||
|
|
||||||
|
int m_index;
|
||||||
|
};
|
||||||
|
|
||||||
|
AgentRosterRow::AgentRosterRow(int index,
|
||||||
|
const QString &name,
|
||||||
|
const AgentConfig *cfg,
|
||||||
|
bool active,
|
||||||
|
bool first,
|
||||||
|
bool last,
|
||||||
|
const Theme &theme,
|
||||||
|
QWidget *parent)
|
||||||
|
: QFrame(parent), m_index(index)
|
||||||
|
{
|
||||||
|
setAutoFillBackground(true);
|
||||||
|
QPalette pal = palette();
|
||||||
|
pal.setColor(QPalette::Window, active ? theme.rowMatchBg : theme.cardBg);
|
||||||
|
setPalette(pal);
|
||||||
|
|
||||||
|
setStyleSheet(QStringLiteral("AgentRosterRow { border:0; border-top:%1; }")
|
||||||
|
.arg(index == 0 ? QStringLiteral("0px")
|
||||||
|
: QStringLiteral("1px solid %1").arg(hex(theme.rowSeparator))));
|
||||||
|
|
||||||
|
auto *outer = new QHBoxLayout(this);
|
||||||
|
outer->setContentsMargins(8, 6, 8, 6);
|
||||||
|
outer->setSpacing(8);
|
||||||
|
|
||||||
|
auto *moveCol = new QVBoxLayout;
|
||||||
|
moveCol->setContentsMargins(0, 0, 0, 0);
|
||||||
|
moveCol->setSpacing(1);
|
||||||
|
auto makeArrow = [&](const QString &glyph, const QString &tip, bool enabled) {
|
||||||
|
auto *b = new QToolButton(this);
|
||||||
|
b->setText(glyph);
|
||||||
|
b->setToolTip(tip);
|
||||||
|
b->setEnabled(enabled);
|
||||||
|
b->setAutoRaise(true);
|
||||||
|
b->setFixedSize(18, 14);
|
||||||
|
QFont f = b->font();
|
||||||
|
f.setPointSizeF(f.pointSizeF() * 0.75);
|
||||||
|
b->setFont(f);
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
auto *upBtn = makeArrow(QStringLiteral("▲"), tr("Move up"), !first);
|
||||||
|
auto *dnBtn = makeArrow(QStringLiteral("▼"), tr("Move down"), !last);
|
||||||
|
moveCol->addWidget(upBtn);
|
||||||
|
moveCol->addWidget(dnBtn);
|
||||||
|
outer->addLayout(moveCol);
|
||||||
|
|
||||||
|
auto *idxLbl = new QLabel(QStringLiteral("%1.").arg(index + 1), this);
|
||||||
|
QFont monoFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||||
|
idxLbl->setFont(monoFont);
|
||||||
|
QPalette idxPal = idxLbl->palette();
|
||||||
|
idxPal.setColor(QPalette::WindowText, active ? theme.text : theme.textFaint);
|
||||||
|
idxLbl->setPalette(idxPal);
|
||||||
|
idxLbl->setFixedWidth(22);
|
||||||
|
idxLbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
|
outer->addWidget(idxLbl);
|
||||||
|
|
||||||
|
auto *body = new QVBoxLayout;
|
||||||
|
body->setContentsMargins(0, 0, 0, 0);
|
||||||
|
body->setSpacing(2);
|
||||||
|
|
||||||
|
const QString displayName = cfg ? cfg->name : tr("%1 (missing)").arg(name);
|
||||||
|
const QString model = cfg ? cfg->model : QString();
|
||||||
|
const bool isUser = cfg && cfg->isUserSource();
|
||||||
|
|
||||||
|
body->addWidget(buildIdentityLine(displayName, model, active, isUser, theme));
|
||||||
|
body->addWidget(buildMetaLine(cfg, active, theme));
|
||||||
|
outer->addLayout(body, /*stretch*/ 1);
|
||||||
|
|
||||||
|
outer->addWidget(buildActions(theme, first, last));
|
||||||
|
|
||||||
|
if (cfg && !cfg->description.isEmpty())
|
||||||
|
setToolTip(cfg->description);
|
||||||
|
|
||||||
|
connect(upBtn, &QToolButton::clicked, this, [this]() { emit moveUpRequested(m_index); });
|
||||||
|
connect(dnBtn, &QToolButton::clicked, this, [this]() { emit moveDownRequested(m_index); });
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget *AgentRosterRow::buildIdentityLine(const QString &displayName,
|
||||||
|
const QString &model,
|
||||||
|
bool active,
|
||||||
|
bool isUser,
|
||||||
|
const Theme &t)
|
||||||
|
{
|
||||||
|
auto *w = new QWidget(this);
|
||||||
|
auto *line = new QHBoxLayout(w);
|
||||||
|
line->setContentsMargins(0, 0, 0, 0);
|
||||||
|
line->setSpacing(6);
|
||||||
|
|
||||||
|
auto *nameLbl = new QLabel(displayName, w);
|
||||||
|
QFont nameFont = nameLbl->font();
|
||||||
|
nameFont.setBold(true);
|
||||||
|
nameLbl->setFont(nameFont);
|
||||||
|
line->addWidget(nameLbl);
|
||||||
|
|
||||||
|
if (!model.isEmpty()) {
|
||||||
|
auto *modelLbl = new QLabel(model, w);
|
||||||
|
modelLbl->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||||
|
QPalette mp = modelLbl->palette();
|
||||||
|
mp.setColor(QPalette::WindowText, t.textSoft);
|
||||||
|
modelLbl->setPalette(mp);
|
||||||
|
line->addWidget(modelLbl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active)
|
||||||
|
line->addWidget(makePill(QStringLiteral("✓ ") + tr("active"), PillKind::Active, t, w));
|
||||||
|
if (isUser)
|
||||||
|
line->addWidget(makePill(tr("user"), PillKind::User, t, w));
|
||||||
|
|
||||||
|
line->addStretch(1);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget *AgentRosterRow::buildMetaLine(const AgentConfig *cfg, bool active, const Theme &t)
|
||||||
|
{
|
||||||
|
auto *w = new QWidget(this);
|
||||||
|
auto *line = new QHBoxLayout(w);
|
||||||
|
line->setContentsMargins(0, 0, 0, 0);
|
||||||
|
line->setSpacing(4);
|
||||||
|
|
||||||
|
if (cfg) {
|
||||||
|
const auto sm = summarise(cfg->match);
|
||||||
|
auto *chip = new QLabel(w);
|
||||||
|
const QColor bg = active ? t.pill(PillKind::Match).bg : t.matchChipBg;
|
||||||
|
const QColor bd = active ? t.pill(PillKind::Match).border : t.matchChipBorder;
|
||||||
|
const QColor fg = active ? t.pill(PillKind::Match).fg : t.matchChipText;
|
||||||
|
QString chipText = QStringLiteral(
|
||||||
|
"<span style='opacity:0.7'>%1</span> "
|
||||||
|
"<span style='color:%2'>%3:</span> %4")
|
||||||
|
.arg(sm.icon,
|
||||||
|
hex(active ? t.pill(PillKind::Match).fg : t.textFaint),
|
||||||
|
sm.kind,
|
||||||
|
sm.value.toHtmlEscaped());
|
||||||
|
chip->setTextFormat(Qt::RichText);
|
||||||
|
chip->setText(chipText);
|
||||||
|
chip->setStyleSheet(QStringLiteral("background:%1; color:%2; border:1px solid %3;"
|
||||||
|
"padding:0px 6px; border-radius:2px; font-size:11px;")
|
||||||
|
.arg(hex(bg), hex(fg), hex(bd)));
|
||||||
|
chip->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||||
|
line->addWidget(chip);
|
||||||
|
|
||||||
|
auto *arrow = new QLabel(QStringLiteral("→"), w);
|
||||||
|
QPalette ap = arrow->palette();
|
||||||
|
ap.setColor(QPalette::WindowText, t.textFaint);
|
||||||
|
arrow->setPalette(ap);
|
||||||
|
line->addWidget(arrow);
|
||||||
|
|
||||||
|
if (!cfg->providerInstance.isEmpty())
|
||||||
|
line->addWidget(makePill(cfg->providerInstance, PillKind::Template, t, w));
|
||||||
|
|
||||||
|
constexpr int kMaxTagPills = 3;
|
||||||
|
const int tagCount = cfg->tags.size();
|
||||||
|
for (int i = 0; i < std::min(tagCount, kMaxTagPills); ++i)
|
||||||
|
line->addWidget(makePill(cfg->tags.at(i), PillKind::Tag, t, w));
|
||||||
|
if (tagCount > kMaxTagPills) {
|
||||||
|
auto *more = makePill(QStringLiteral("+%1").arg(tagCount - kMaxTagPills),
|
||||||
|
PillKind::Tag, t, w);
|
||||||
|
more->setToolTip(cfg->tags.mid(kMaxTagPills).join(QStringLiteral(", ")));
|
||||||
|
line->addWidget(more);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auto *missing = new QLabel(tr("agent configuration is no longer available"), w);
|
||||||
|
QPalette mp = missing->palette();
|
||||||
|
mp.setColor(QPalette::WindowText, t.textMute);
|
||||||
|
missing->setPalette(mp);
|
||||||
|
line->addWidget(missing);
|
||||||
|
}
|
||||||
|
line->addStretch(1);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget *AgentRosterRow::buildActions(const Theme &, bool /*first*/, bool /*last*/)
|
||||||
|
{
|
||||||
|
auto *w = new QWidget(this);
|
||||||
|
auto *l = new QHBoxLayout(w);
|
||||||
|
l->setContentsMargins(0, 0, 0, 0);
|
||||||
|
l->setSpacing(4);
|
||||||
|
|
||||||
|
auto *edit = new QToolButton(w);
|
||||||
|
edit->setText(tr("Edit…"));
|
||||||
|
edit->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
||||||
|
edit->setAutoRaise(false);
|
||||||
|
edit->setFocusPolicy(Qt::TabFocus);
|
||||||
|
edit->setMinimumHeight(22);
|
||||||
|
|
||||||
|
auto *remove = new QToolButton(w);
|
||||||
|
remove->setText(QStringLiteral("✕"));
|
||||||
|
remove->setToolTip(tr("Remove from list"));
|
||||||
|
remove->setAutoRaise(false);
|
||||||
|
remove->setFixedSize(22, 22);
|
||||||
|
|
||||||
|
l->addWidget(edit);
|
||||||
|
l->addWidget(remove);
|
||||||
|
|
||||||
|
connect(edit, &QToolButton::clicked, this, [this]() { emit editRequested(m_index); });
|
||||||
|
connect(remove, &QToolButton::clicked, this, [this]() { emit removeRequested(m_index); });
|
||||||
|
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRosterWidget::AgentRosterWidget(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
const Theme t = themeFor(this);
|
||||||
|
|
||||||
|
auto *outer = new QVBoxLayout(this);
|
||||||
|
outer->setContentsMargins(0, 0, 0, 0);
|
||||||
|
outer->setSpacing(6);
|
||||||
|
|
||||||
|
auto *titleRow = new QHBoxLayout;
|
||||||
|
titleRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
titleRow->setSpacing(6);
|
||||||
|
m_accentDot = new QLabel(this);
|
||||||
|
m_accentDot->setFixedSize(10, 10);
|
||||||
|
m_titleLabel = new QLabel(this);
|
||||||
|
QFont tf = m_titleLabel->font();
|
||||||
|
tf.setBold(true);
|
||||||
|
m_titleLabel->setFont(tf);
|
||||||
|
titleRow->addWidget(m_accentDot);
|
||||||
|
titleRow->addWidget(m_titleLabel);
|
||||||
|
titleRow->addStretch(1);
|
||||||
|
outer->addLayout(titleRow);
|
||||||
|
|
||||||
|
m_hintLabel = new QLabel(this);
|
||||||
|
m_hintLabel->setWordWrap(true);
|
||||||
|
QPalette hp = m_hintLabel->palette();
|
||||||
|
hp.setColor(QPalette::WindowText, t.textMute);
|
||||||
|
m_hintLabel->setPalette(hp);
|
||||||
|
QFont hf = m_hintLabel->font();
|
||||||
|
hf.setPointSizeF(hf.pointSizeF() * 0.9);
|
||||||
|
m_hintLabel->setFont(hf);
|
||||||
|
outer->addWidget(m_hintLabel);
|
||||||
|
|
||||||
|
m_rowsFrame = new QFrame(this);
|
||||||
|
m_rowsFrame->setObjectName(QStringLiteral("rosterCard"));
|
||||||
|
m_rowsFrame->setStyleSheet(
|
||||||
|
QStringLiteral("QFrame#rosterCard { background:%1; border:1px solid %2; border-radius:2px; }")
|
||||||
|
.arg(hex(t.cardBg), hex(t.cardBorder)));
|
||||||
|
m_rowsLayout = new QVBoxLayout(m_rowsFrame);
|
||||||
|
m_rowsLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
m_rowsLayout->setSpacing(0);
|
||||||
|
|
||||||
|
m_emptyHint = new QLabel(tr("No agents in roster. Click \"Add agent…\" to populate."),
|
||||||
|
m_rowsFrame);
|
||||||
|
m_emptyHint->setAlignment(Qt::AlignCenter);
|
||||||
|
m_emptyHint->setContentsMargins(10, 12, 10, 12);
|
||||||
|
QFont eh = m_emptyHint->font();
|
||||||
|
eh.setItalic(true);
|
||||||
|
m_emptyHint->setFont(eh);
|
||||||
|
QPalette ep = m_emptyHint->palette();
|
||||||
|
ep.setColor(QPalette::WindowText, t.textFaint);
|
||||||
|
m_emptyHint->setPalette(ep);
|
||||||
|
m_rowsLayout->addWidget(m_emptyHint);
|
||||||
|
|
||||||
|
outer->addWidget(m_rowsFrame);
|
||||||
|
|
||||||
|
auto *footer = new QHBoxLayout;
|
||||||
|
footer->setContentsMargins(0, 0, 0, 0);
|
||||||
|
footer->setSpacing(6);
|
||||||
|
m_addBtn = new QPushButton(tr("+ Add agent…"), this);
|
||||||
|
footer->addWidget(m_addBtn);
|
||||||
|
footer->addStretch(1);
|
||||||
|
m_footerHint = new QLabel(tr("first matching agent is used"), this);
|
||||||
|
QPalette fp = m_footerHint->palette();
|
||||||
|
fp.setColor(QPalette::WindowText, t.textMute);
|
||||||
|
m_footerHint->setPalette(fp);
|
||||||
|
QFont ff = m_footerHint->font();
|
||||||
|
ff.setPointSizeF(ff.pointSizeF() * 0.9);
|
||||||
|
m_footerHint->setFont(ff);
|
||||||
|
footer->addWidget(m_footerHint);
|
||||||
|
outer->addLayout(footer);
|
||||||
|
|
||||||
|
connect(m_addBtn, &QPushButton::clicked, this, &AgentRosterWidget::onAddClicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::setSlot(const QString &title, const QString &hint, const QColor &accent)
|
||||||
|
{
|
||||||
|
m_titleLabel->setText(title);
|
||||||
|
m_hintLabel->setText(hint);
|
||||||
|
m_hintLabel->setVisible(!hint.isEmpty());
|
||||||
|
if (accent.isValid()) {
|
||||||
|
m_accentDot->setStyleSheet(
|
||||||
|
QStringLiteral("background:%1; border:1px solid %2;")
|
||||||
|
.arg(accent.name(), accent.darker(140).name()));
|
||||||
|
m_accentDot->setVisible(true);
|
||||||
|
} else {
|
||||||
|
m_accentDot->setStyleSheet({});
|
||||||
|
m_accentDot->setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::setRoster(const QStringList &names, AgentFactory *factory)
|
||||||
|
{
|
||||||
|
m_factory = factory;
|
||||||
|
m_names = names;
|
||||||
|
rebuildRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::setRoutingContext(const AgentRouter::Context &ctx)
|
||||||
|
{
|
||||||
|
m_routingCtx = ctx;
|
||||||
|
recomputeActive();
|
||||||
|
rebuildRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::recomputeActive()
|
||||||
|
{
|
||||||
|
if (!m_factory || m_names.isEmpty()
|
||||||
|
|| (m_routingCtx.filePath.isEmpty() && m_routingCtx.projectName.isEmpty())) {
|
||||||
|
m_activeIndex = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString picked = AgentRouter::pickAgent(m_names, m_routingCtx, *m_factory);
|
||||||
|
m_activeIndex = picked.isEmpty() ? -1 : m_names.indexOf(picked);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::rebuildRows()
|
||||||
|
{
|
||||||
|
// Tear down existing row widgets (keep the empty hint).
|
||||||
|
while (m_rowsLayout->count() > 0) {
|
||||||
|
QLayoutItem *it = m_rowsLayout->takeAt(0);
|
||||||
|
if (auto *w = it->widget()) {
|
||||||
|
if (w == m_emptyHint)
|
||||||
|
w->setVisible(false);
|
||||||
|
else
|
||||||
|
w->deleteLater();
|
||||||
|
}
|
||||||
|
delete it;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_names.isEmpty()) {
|
||||||
|
m_emptyHint->setVisible(true);
|
||||||
|
m_rowsLayout->addWidget(m_emptyHint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recomputeActive();
|
||||||
|
const Theme t = themeFor(this);
|
||||||
|
|
||||||
|
for (int i = 0; i < m_names.size(); ++i) {
|
||||||
|
const QString &name = m_names.at(i);
|
||||||
|
const AgentConfig *cfg = m_factory ? m_factory->configByName(name) : nullptr;
|
||||||
|
auto *row = new AgentRosterRow(i,
|
||||||
|
name,
|
||||||
|
cfg,
|
||||||
|
i == m_activeIndex,
|
||||||
|
/*first*/ i == 0,
|
||||||
|
/*last*/ i == m_names.size() - 1,
|
||||||
|
t,
|
||||||
|
m_rowsFrame);
|
||||||
|
connect(row, &AgentRosterRow::moveUpRequested, this, &AgentRosterWidget::onRowMoveUp);
|
||||||
|
connect(row, &AgentRosterRow::moveDownRequested, this, &AgentRosterWidget::onRowMoveDown);
|
||||||
|
connect(row, &AgentRosterRow::editRequested, this, &AgentRosterWidget::onRowEdit);
|
||||||
|
connect(row, &AgentRosterRow::removeRequested, this, &AgentRosterWidget::onRowRemove);
|
||||||
|
m_rowsLayout->addWidget(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::onAddClicked()
|
||||||
|
{
|
||||||
|
if (!m_factory)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AgentSelectionDialog dialog(m_factory->configs(),
|
||||||
|
/*currentName*/ QString{},
|
||||||
|
m_factory.data(),
|
||||||
|
Core::ICore::dialogParent());
|
||||||
|
if (dialog.exec() != QDialog::Accepted)
|
||||||
|
return;
|
||||||
|
const QString picked = dialog.selectedName();
|
||||||
|
if (picked.isEmpty() || m_names.contains(picked))
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_names.append(picked);
|
||||||
|
rebuildRows();
|
||||||
|
emit rosterChanged(m_names);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::onRowMoveUp(int index)
|
||||||
|
{
|
||||||
|
if (index <= 0 || index >= m_names.size())
|
||||||
|
return;
|
||||||
|
m_names.swapItemsAt(index, index - 1);
|
||||||
|
rebuildRows();
|
||||||
|
emit rosterChanged(m_names);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::onRowMoveDown(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_names.size() - 1)
|
||||||
|
return;
|
||||||
|
m_names.swapItemsAt(index, index + 1);
|
||||||
|
rebuildRows();
|
||||||
|
emit rosterChanged(m_names);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::onRowRemove(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_names.size())
|
||||||
|
return;
|
||||||
|
m_names.removeAt(index);
|
||||||
|
rebuildRows();
|
||||||
|
emit rosterChanged(m_names);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRosterWidget::onRowEdit(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_names.size())
|
||||||
|
return;
|
||||||
|
emit editAgentRequested(m_names.at(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
|
|
||||||
|
#include "AgentRosterWidget.moc"
|
||||||
70
sources/settings/AgentRosterWidget.hpp
Normal file
70
sources/settings/AgentRosterWidget.hpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include <AgentRouter.hpp>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
class QToolButton;
|
||||||
|
class QPushButton;
|
||||||
|
class QVBoxLayout;
|
||||||
|
class QFrame;
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class AgentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
class AgentRosterRow;
|
||||||
|
|
||||||
|
class AgentRosterWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit AgentRosterWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setSlot(const QString &title, const QString &hint, const QColor &accent);
|
||||||
|
void setRoster(const QStringList &names, AgentFactory *factory);
|
||||||
|
|
||||||
|
[[nodiscard]] QStringList roster() const { return m_names; }
|
||||||
|
|
||||||
|
void setRoutingContext(const AgentRouter::Context &ctx);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void rosterChanged(const QStringList &names);
|
||||||
|
void editAgentRequested(const QString &agentName);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void rebuildRows();
|
||||||
|
void recomputeActive();
|
||||||
|
|
||||||
|
void onAddClicked();
|
||||||
|
void onRowMoveUp(int index);
|
||||||
|
void onRowMoveDown(int index);
|
||||||
|
void onRowRemove(int index);
|
||||||
|
void onRowEdit(int index);
|
||||||
|
|
||||||
|
QStringList m_names;
|
||||||
|
QPointer<AgentFactory> m_factory;
|
||||||
|
AgentRouter::Context m_routingCtx;
|
||||||
|
int m_activeIndex = -1;
|
||||||
|
|
||||||
|
QLabel *m_accentDot = nullptr;
|
||||||
|
QLabel *m_titleLabel = nullptr;
|
||||||
|
QLabel *m_hintLabel = nullptr;
|
||||||
|
QFrame *m_rowsFrame = nullptr;
|
||||||
|
QVBoxLayout *m_rowsLayout = nullptr;
|
||||||
|
QLabel *m_emptyHint = nullptr;
|
||||||
|
QPushButton *m_addBtn = nullptr;
|
||||||
|
QLabel *m_footerHint = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
471
sources/settings/AgentSelectionDialog.cpp
Normal file
471
sources/settings/AgentSelectionDialog.cpp
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "AgentSelectionDialog.hpp"
|
||||||
|
|
||||||
|
#include "AgentSlotWidget.hpp"
|
||||||
|
#include "PipelinesConfig.hpp"
|
||||||
|
#include "SettingsTr.hpp"
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QEnterEvent>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QScopedValueRollback>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
// -- ListRowCard -------------------------------------------------------
|
||||||
|
|
||||||
|
ListRowCard::ListRowCard(QWidget *parent)
|
||||||
|
: QFrame(parent)
|
||||||
|
{
|
||||||
|
setObjectName(QStringLiteral("ListRowCard"));
|
||||||
|
setAttribute(Qt::WA_StyledBackground, true);
|
||||||
|
setCursor(Qt::PointingHandCursor);
|
||||||
|
setFrameShape(QFrame::NoFrame);
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ListRowCard::matches(const QString &needle) const
|
||||||
|
{
|
||||||
|
if (needle.isEmpty())
|
||||||
|
return true;
|
||||||
|
return m_searchHaystack.contains(needle.toLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListRowCard::setSelected(bool selected)
|
||||||
|
{
|
||||||
|
if (m_selected == selected)
|
||||||
|
return;
|
||||||
|
m_selected = selected;
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListRowCard::buildSearchHaystack(const QStringList &parts)
|
||||||
|
{
|
||||||
|
m_searchHaystack = parts.join(QLatin1Char(' ')).toLower();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListRowCard::mousePressEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (event->button() == Qt::LeftButton)
|
||||||
|
emit clicked();
|
||||||
|
QFrame::mousePressEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListRowCard::mouseDoubleClickEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (event->button() == Qt::LeftButton)
|
||||||
|
emit activated();
|
||||||
|
QFrame::mouseDoubleClickEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListRowCard::enterEvent(QEnterEvent *event)
|
||||||
|
{
|
||||||
|
QFrame::enterEvent(event);
|
||||||
|
m_hover = true;
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListRowCard::leaveEvent(QEvent *event)
|
||||||
|
{
|
||||||
|
QFrame::leaveEvent(event);
|
||||||
|
m_hover = false;
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListRowCard::changeEvent(QEvent *event)
|
||||||
|
{
|
||||||
|
QFrame::changeEvent(event);
|
||||||
|
if (m_inApplyTheme)
|
||||||
|
return;
|
||||||
|
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListRowCard::applyTheme()
|
||||||
|
{
|
||||||
|
if (m_inApplyTheme)
|
||||||
|
return;
|
||||||
|
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
|
||||||
|
|
||||||
|
const auto t = CardStyle::toneFor(CardStyle::isDark(palette()));
|
||||||
|
QString bg = t.bg;
|
||||||
|
QString bd = t.cardBd;
|
||||||
|
if (m_selected) {
|
||||||
|
bg = t.selectedBg;
|
||||||
|
bd = t.selectedBd;
|
||||||
|
} else if (m_hover) {
|
||||||
|
bg = t.hoverBg;
|
||||||
|
}
|
||||||
|
setStyleSheet(QStringLiteral(
|
||||||
|
"#ListRowCard { background-color: %1; border: 1px solid %2; }")
|
||||||
|
.arg(bg, bd));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- AgentRowCard ------------------------------------------------------
|
||||||
|
|
||||||
|
AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
|
||||||
|
: ListRowCard(parent)
|
||||||
|
{
|
||||||
|
setItemName(cfg.name);
|
||||||
|
QStringList haystack{cfg.name, cfg.providerInstance, cfg.model,
|
||||||
|
cfg.description, cfg.role,
|
||||||
|
cfg.endpoint};
|
||||||
|
haystack += cfg.tags;
|
||||||
|
buildSearchHaystack(haystack);
|
||||||
|
|
||||||
|
auto *name = new QLabel(cfg.name, this);
|
||||||
|
QFont nameFont = name->font();
|
||||||
|
nameFont.setBold(true);
|
||||||
|
nameFont.setPixelSize(13);
|
||||||
|
name->setFont(nameFont);
|
||||||
|
name->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
|
||||||
|
|
||||||
|
QLabel *model = nullptr;
|
||||||
|
if (!cfg.model.isEmpty()) {
|
||||||
|
model = new QLabel(QStringLiteral("· %1").arg(cfg.model), this);
|
||||||
|
model->setFont(CardStyle::monoFont(11));
|
||||||
|
model->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||||
|
model->setMinimumWidth(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pill *sourcePill = nullptr;
|
||||||
|
if (cfg.isUserSource()) {
|
||||||
|
sourcePill = new Pill(
|
||||||
|
Pill::User,
|
||||||
|
cfg.overridesBundled ? Tr::tr("Override") : Tr::tr("User"),
|
||||||
|
this);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *description = new QLabel(this);
|
||||||
|
description->setWordWrap(false);
|
||||||
|
QFont descFont = description->font();
|
||||||
|
descFont.setItalic(true);
|
||||||
|
description->setFont(descFont);
|
||||||
|
description->setText(cfg.description.isEmpty()
|
||||||
|
? Tr::tr("No description provided.")
|
||||||
|
: cfg.description);
|
||||||
|
description->setMinimumWidth(0);
|
||||||
|
description->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||||
|
description->setTextInteractionFlags(Qt::NoTextInteraction);
|
||||||
|
|
||||||
|
QStringList endpointParts;
|
||||||
|
if (!cfg.endpoint.isEmpty())
|
||||||
|
endpointParts << cfg.endpoint;
|
||||||
|
endpointParts << (cfg.enableThinking ? Tr::tr("thinking") : Tr::tr("no-thinking"));
|
||||||
|
endpointParts << (cfg.enableTools ? Tr::tr("tools") : Tr::tr("no-tools"));
|
||||||
|
|
||||||
|
auto *endpoint = new QLabel(endpointParts.join(QStringLiteral(" · ")), this);
|
||||||
|
endpoint->setFont(CardStyle::monoFont(11));
|
||||||
|
endpoint->setMinimumWidth(0);
|
||||||
|
endpoint->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||||
|
endpoint->setTextInteractionFlags(Qt::NoTextInteraction);
|
||||||
|
|
||||||
|
auto *headerRow = new QHBoxLayout;
|
||||||
|
headerRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
headerRow->setSpacing(6);
|
||||||
|
headerRow->addWidget(name);
|
||||||
|
if (model)
|
||||||
|
headerRow->addWidget(model, 1);
|
||||||
|
else
|
||||||
|
headerRow->addStretch(1);
|
||||||
|
if (sourcePill)
|
||||||
|
headerRow->addWidget(sourcePill);
|
||||||
|
|
||||||
|
auto *outer = new QVBoxLayout(this);
|
||||||
|
outer->setContentsMargins(10, 8, 10, 8);
|
||||||
|
outer->setSpacing(3);
|
||||||
|
outer->addLayout(headerRow);
|
||||||
|
outer->addWidget(description);
|
||||||
|
outer->addWidget(endpoint);
|
||||||
|
|
||||||
|
if (!cfg.tags.isEmpty()) {
|
||||||
|
constexpr int kMaxTagPills = 4;
|
||||||
|
auto *tagsRow = new QHBoxLayout;
|
||||||
|
tagsRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
tagsRow->setSpacing(4);
|
||||||
|
const int tagCount = cfg.tags.size();
|
||||||
|
for (int i = 0; i < std::min(tagCount, kMaxTagPills); ++i)
|
||||||
|
tagsRow->addWidget(new Pill(Pill::Tag, cfg.tags.at(i), this));
|
||||||
|
if (tagCount > kMaxTagPills) {
|
||||||
|
auto *more = new Pill(
|
||||||
|
Pill::Tag,
|
||||||
|
QStringLiteral("+%1").arg(tagCount - kMaxTagPills),
|
||||||
|
this);
|
||||||
|
more->setToolTip(cfg.tags.mid(kMaxTagPills).join(QStringLiteral(", ")));
|
||||||
|
tagsRow->addWidget(more);
|
||||||
|
}
|
||||||
|
tagsRow->addStretch(1);
|
||||||
|
outer->addLayout(tagsRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto t = CardStyle::toneFor(CardStyle::isDark(palette()));
|
||||||
|
QPalette descPal = description->palette();
|
||||||
|
descPal.setColor(QPalette::WindowText,
|
||||||
|
QColor(cfg.description.isEmpty() ? t.textFaint : t.textSoft));
|
||||||
|
description->setPalette(descPal);
|
||||||
|
QPalette endPal = endpoint->palette();
|
||||||
|
endPal.setColor(QPalette::WindowText, QColor(t.textFaint));
|
||||||
|
endpoint->setPalette(endPal);
|
||||||
|
if (model) {
|
||||||
|
QPalette mp = model->palette();
|
||||||
|
mp.setColor(QPalette::WindowText, QColor(t.textMute));
|
||||||
|
model->setPalette(mp);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString tooltip;
|
||||||
|
if (!cfg.description.isEmpty())
|
||||||
|
tooltip += cfg.description + QStringLiteral("\n\n");
|
||||||
|
if (!cfg.providerInstance.isEmpty())
|
||||||
|
tooltip += Tr::tr("Provider instance: %1\n").arg(cfg.providerInstance);
|
||||||
|
if (!cfg.role.isEmpty())
|
||||||
|
tooltip += Tr::tr("Role: %1\n").arg(cfg.role);
|
||||||
|
if (!cfg.endpoint.isEmpty())
|
||||||
|
tooltip += Tr::tr("Endpoint: %1\n").arg(cfg.endpoint);
|
||||||
|
setToolTip(tooltip.trimmed());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- ProviderSection ---------------------------------------------------
|
||||||
|
|
||||||
|
ProviderSection::ProviderSection(const QString &name, QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
m_arrow = new QLabel(QStringLiteral("▾"));
|
||||||
|
m_label = new QLabel(name);
|
||||||
|
CardStyle::applySectionFont(m_label);
|
||||||
|
|
||||||
|
QFont arrowFont = m_label->font();
|
||||||
|
arrowFont.setCapitalization(QFont::MixedCase);
|
||||||
|
m_arrow->setFont(arrowFont);
|
||||||
|
QPalette ap = m_arrow->palette();
|
||||||
|
ap.setColor(QPalette::WindowText, ap.color(QPalette::Mid));
|
||||||
|
m_arrow->setPalette(ap);
|
||||||
|
|
||||||
|
m_header = new QFrame;
|
||||||
|
m_header->setObjectName(QStringLiteral("ProviderHeader"));
|
||||||
|
m_header->setCursor(Qt::PointingHandCursor);
|
||||||
|
m_header->setFrameShape(QFrame::NoFrame);
|
||||||
|
auto *headerLayout = new QHBoxLayout(m_header);
|
||||||
|
headerLayout->setContentsMargins(2, 4, 2, 2);
|
||||||
|
headerLayout->setSpacing(6);
|
||||||
|
headerLayout->addWidget(m_arrow);
|
||||||
|
headerLayout->addWidget(m_label);
|
||||||
|
headerLayout->addStretch(1);
|
||||||
|
m_header->installEventFilter(this);
|
||||||
|
|
||||||
|
m_content = new QWidget;
|
||||||
|
m_contentLayout = new QVBoxLayout(m_content);
|
||||||
|
m_contentLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
m_contentLayout->setSpacing(4);
|
||||||
|
m_content->setVisible(false);
|
||||||
|
m_arrow->setText(QStringLiteral("▸"));
|
||||||
|
m_expanded = false;
|
||||||
|
|
||||||
|
auto *outer = new QVBoxLayout(this);
|
||||||
|
outer->setContentsMargins(0, 0, 0, 0);
|
||||||
|
outer->setSpacing(0);
|
||||||
|
outer->addWidget(m_header);
|
||||||
|
outer->addWidget(m_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProviderSection::addCard(ListRowCard *card)
|
||||||
|
{
|
||||||
|
m_contentLayout->addWidget(card);
|
||||||
|
m_cards.append(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ProviderSection::applyFilter(const QString &needle)
|
||||||
|
{
|
||||||
|
int visible = 0;
|
||||||
|
for (auto *card : m_cards) {
|
||||||
|
const bool show = card->matches(needle);
|
||||||
|
card->setVisible(show);
|
||||||
|
if (show)
|
||||||
|
++visible;
|
||||||
|
}
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProviderSection::setExpanded(bool expanded)
|
||||||
|
{
|
||||||
|
if (m_expanded == expanded)
|
||||||
|
return;
|
||||||
|
m_expanded = expanded;
|
||||||
|
m_content->setVisible(expanded);
|
||||||
|
m_arrow->setText(expanded ? QStringLiteral("▾") : QStringLiteral("▸"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProviderSection::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
if (watched == m_header && event->type() == QEvent::MouseButtonRelease) {
|
||||||
|
auto *me = static_cast<QMouseEvent *>(event);
|
||||||
|
if (me->button() == Qt::LeftButton) {
|
||||||
|
setExpanded(!m_expanded);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QWidget::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- AgentSelectionDialog ----------------------------------------------
|
||||||
|
|
||||||
|
AgentSelectionDialog::AgentSelectionDialog(
|
||||||
|
const std::vector<AgentConfig> &configs,
|
||||||
|
const QString ¤tName,
|
||||||
|
AgentFactory *agentFactory,
|
||||||
|
QWidget *parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
, m_agentFactory(agentFactory)
|
||||||
|
{
|
||||||
|
setWindowTitle(Tr::tr("Change Agent"));
|
||||||
|
resize(720, 600);
|
||||||
|
setSizeGripEnabled(true);
|
||||||
|
|
||||||
|
if (!m_agentFactory)
|
||||||
|
m_localConfigs = configs;
|
||||||
|
|
||||||
|
m_filter = new QLineEdit(this);
|
||||||
|
m_filter->setPlaceholderText(
|
||||||
|
Tr::tr("Filter by name, provider, model, template, description…"));
|
||||||
|
m_filter->setClearButtonEnabled(true);
|
||||||
|
|
||||||
|
auto *topRow = new QHBoxLayout;
|
||||||
|
topRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
topRow->setSpacing(6);
|
||||||
|
topRow->addWidget(m_filter, 1);
|
||||||
|
|
||||||
|
m_scroll = new QScrollArea(this);
|
||||||
|
m_scroll->setWidgetResizable(true);
|
||||||
|
m_scroll->setFrameShape(QFrame::StyledPanel);
|
||||||
|
m_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
|
||||||
|
auto *buttons
|
||||||
|
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||||
|
m_okButton = buttons->button(QDialogButtonBox::Ok);
|
||||||
|
m_okButton->setText(Tr::tr("Change"));
|
||||||
|
m_okButton->setEnabled(false);
|
||||||
|
|
||||||
|
auto *layout = new QVBoxLayout(this);
|
||||||
|
layout->addLayout(topRow);
|
||||||
|
layout->addWidget(m_scroll);
|
||||||
|
layout->addWidget(buttons);
|
||||||
|
|
||||||
|
rebuild(currentName);
|
||||||
|
|
||||||
|
connect(m_filter, &QLineEdit::textChanged, this,
|
||||||
|
[this](const QString &text) { applyFilter(text); });
|
||||||
|
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSelectionDialog::selectCard(ListRowCard *card)
|
||||||
|
{
|
||||||
|
if (m_currentCard == card)
|
||||||
|
return;
|
||||||
|
if (m_currentCard)
|
||||||
|
m_currentCard->setSelected(false);
|
||||||
|
m_currentCard = card;
|
||||||
|
if (m_currentCard) {
|
||||||
|
m_currentCard->setSelected(true);
|
||||||
|
m_selectedName = m_currentCard->itemName();
|
||||||
|
} else {
|
||||||
|
m_selectedName.clear();
|
||||||
|
}
|
||||||
|
if (m_okButton)
|
||||||
|
m_okButton->setEnabled(!m_selectedName.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSelectionDialog::rebuild(const QString ¤tName)
|
||||||
|
{
|
||||||
|
m_sections.clear();
|
||||||
|
m_currentCard = nullptr;
|
||||||
|
m_selectedName.clear();
|
||||||
|
if (m_okButton)
|
||||||
|
m_okButton->setEnabled(false);
|
||||||
|
|
||||||
|
const auto &configs
|
||||||
|
= m_agentFactory ? m_agentFactory->configs() : m_localConfigs;
|
||||||
|
|
||||||
|
auto *content = new QWidget;
|
||||||
|
auto *contentLayout = new QVBoxLayout(content);
|
||||||
|
contentLayout->setContentsMargins(12, 12, 12, 12);
|
||||||
|
contentLayout->setSpacing(6);
|
||||||
|
|
||||||
|
QMap<QString, std::vector<const AgentConfig *>> byProvider;
|
||||||
|
for (const auto &cfg : configs) {
|
||||||
|
if (cfg.hidden) continue; // hidden profiles stay loaded but don't surface in the picker
|
||||||
|
const QString key = cfg.providerInstance.isEmpty()
|
||||||
|
? Tr::tr("(Unknown provider instance)")
|
||||||
|
: cfg.providerInstance;
|
||||||
|
byProvider[key].push_back(&cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRowCard *toSelect = nullptr;
|
||||||
|
ProviderSection *sectionToExpand = nullptr;
|
||||||
|
|
||||||
|
for (auto it = byProvider.cbegin(); it != byProvider.cend(); ++it) {
|
||||||
|
auto *section = new ProviderSection(it.key());
|
||||||
|
auto sortedConfigs = it.value();
|
||||||
|
std::sort(sortedConfigs.begin(), sortedConfigs.end(),
|
||||||
|
[](const AgentConfig *a, const AgentConfig *b) { return a->name < b->name; });
|
||||||
|
for (const AgentConfig *cfg : sortedConfigs) {
|
||||||
|
auto *card = new AgentRowCard(*cfg);
|
||||||
|
connect(card, &ListRowCard::clicked, this,
|
||||||
|
[this, card]() { selectCard(card); });
|
||||||
|
connect(card, &ListRowCard::activated, this, [this, card]() {
|
||||||
|
selectCard(card);
|
||||||
|
accept();
|
||||||
|
});
|
||||||
|
section->addCard(card);
|
||||||
|
if (cfg->name == currentName) {
|
||||||
|
toSelect = card;
|
||||||
|
sectionToExpand = section;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentLayout->addWidget(section);
|
||||||
|
m_sections.append(section);
|
||||||
|
}
|
||||||
|
contentLayout->addStretch(1);
|
||||||
|
|
||||||
|
m_scroll->setWidget(content);
|
||||||
|
|
||||||
|
if (sectionToExpand)
|
||||||
|
sectionToExpand->setExpanded(true);
|
||||||
|
|
||||||
|
if (toSelect) {
|
||||||
|
selectCard(toSelect);
|
||||||
|
QTimer::singleShot(0, this, [this, toSelect]() {
|
||||||
|
m_scroll->ensureWidgetVisible(toSelect, 0, 60);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter(m_filter ? m_filter->text() : QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSelectionDialog::applyFilter(const QString &needle)
|
||||||
|
{
|
||||||
|
const QString trimmed = needle.trimmed();
|
||||||
|
for (auto *section : m_sections) {
|
||||||
|
const int visible = section->applyFilter(trimmed);
|
||||||
|
section->setVisible(visible > 0);
|
||||||
|
if (!trimmed.isEmpty())
|
||||||
|
section->setExpanded(visible > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
182
sources/settings/AgentSelectionDialog.hpp
Normal file
182
sources/settings/AgentSelectionDialog.hpp
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
|
||||||
|
class QLineEdit;
|
||||||
|
class QPushButton;
|
||||||
|
class QScrollArea;
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class AgentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
namespace CardStyle {
|
||||||
|
|
||||||
|
struct Tone
|
||||||
|
{
|
||||||
|
QString bg;
|
||||||
|
QString hoverBg;
|
||||||
|
QString selectedBg;
|
||||||
|
QString selectedBd;
|
||||||
|
QString cardBd;
|
||||||
|
QString textSoft;
|
||||||
|
QString textMute;
|
||||||
|
QString textFaint;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool isDark(const QPalette &p)
|
||||||
|
{
|
||||||
|
return p.color(QPalette::Window).lightness() < 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Tone toneFor(bool dark)
|
||||||
|
{
|
||||||
|
return dark
|
||||||
|
? Tone{"#333333", "#3a3a3a", "#2a4566", "#3a6fb7", "#4a4a4a",
|
||||||
|
"#c2c2c2", "#9a9a9a", "#7a7a7a"}
|
||||||
|
: Tone{"#f6f6f6", "#ececec", "#cfe1f7", "#3a6fb7", "#bdbdbd",
|
||||||
|
"#3a3a3a", "#5a5a5a", "#8a8a8a"};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline QFont monoFont(int pixelSize)
|
||||||
|
{
|
||||||
|
QFont f;
|
||||||
|
f.setFamilies({QStringLiteral("SF Mono"),
|
||||||
|
QStringLiteral("Cascadia Code"),
|
||||||
|
QStringLiteral("Consolas"),
|
||||||
|
QStringLiteral("Liberation Mono"),
|
||||||
|
QStringLiteral("Menlo"),
|
||||||
|
QStringLiteral("Courier New")});
|
||||||
|
f.setStyleHint(QFont::Monospace);
|
||||||
|
f.setPixelSize(pixelSize);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void applySectionFont(QLabel *l)
|
||||||
|
{
|
||||||
|
QFont f = l->font();
|
||||||
|
f.setPointSizeF(f.pointSizeF() * 0.85);
|
||||||
|
f.setBold(true);
|
||||||
|
f.setCapitalization(QFont::AllUppercase);
|
||||||
|
f.setLetterSpacing(QFont::AbsoluteSpacing, 0.5);
|
||||||
|
l->setFont(f);
|
||||||
|
QPalette p = l->palette();
|
||||||
|
p.setColor(QPalette::WindowText, p.color(QPalette::Mid));
|
||||||
|
l->setPalette(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace CardStyle
|
||||||
|
|
||||||
|
class ListRowCard : public QFrame
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
QString itemName() const { return m_itemName; }
|
||||||
|
bool matches(const QString &needle) const;
|
||||||
|
void setSelected(bool selected);
|
||||||
|
bool isSelected() const { return m_selected; }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void clicked();
|
||||||
|
void activated();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
explicit ListRowCard(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setItemName(const QString &name) { m_itemName = name; }
|
||||||
|
void buildSearchHaystack(const QStringList &parts);
|
||||||
|
|
||||||
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||||
|
void enterEvent(QEnterEvent *event) override;
|
||||||
|
void leaveEvent(QEvent *event) override;
|
||||||
|
void changeEvent(QEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void applyTheme();
|
||||||
|
|
||||||
|
QString m_itemName;
|
||||||
|
QString m_searchHaystack;
|
||||||
|
bool m_selected = false;
|
||||||
|
bool m_hover = false;
|
||||||
|
bool m_inApplyTheme = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AgentRowCard : public ListRowCard
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit AgentRowCard(const AgentConfig &cfg, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
QString agentName() const { return itemName(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProviderSection : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ProviderSection(const QString &name, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void addCard(ListRowCard *card);
|
||||||
|
void setExpanded(bool expanded);
|
||||||
|
bool isExpanded() const { return m_expanded; }
|
||||||
|
const QList<ListRowCard *> &cards() const { return m_cards; }
|
||||||
|
|
||||||
|
int applyFilter(const QString &needle); // returns number of visible cards
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QFrame *m_header = nullptr;
|
||||||
|
QLabel *m_arrow = nullptr;
|
||||||
|
QLabel *m_label = nullptr;
|
||||||
|
QWidget *m_content = nullptr;
|
||||||
|
QVBoxLayout *m_contentLayout = nullptr;
|
||||||
|
QList<ListRowCard *> m_cards;
|
||||||
|
bool m_expanded = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AgentSelectionDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
AgentSelectionDialog(
|
||||||
|
const std::vector<AgentConfig> &configs,
|
||||||
|
const QString ¤tName,
|
||||||
|
AgentFactory *agentFactory = nullptr,
|
||||||
|
QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
QString selectedName() const { return m_selectedName; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void rebuild(const QString ¤tName);
|
||||||
|
void selectCard(ListRowCard *card);
|
||||||
|
void applyFilter(const QString &needle);
|
||||||
|
|
||||||
|
QLineEdit *m_filter = nullptr;
|
||||||
|
QScrollArea *m_scroll = nullptr;
|
||||||
|
QPushButton *m_okButton = nullptr;
|
||||||
|
ListRowCard *m_currentCard = nullptr;
|
||||||
|
QList<ProviderSection *> m_sections;
|
||||||
|
QString m_selectedName;
|
||||||
|
|
||||||
|
AgentFactory *m_agentFactory = nullptr;
|
||||||
|
std::vector<AgentConfig> m_localConfigs; // fallback when no factory
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
361
sources/settings/AgentSlotWidget.cpp
Normal file
361
sources/settings/AgentSlotWidget.cpp
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "AgentSlotWidget.hpp"
|
||||||
|
|
||||||
|
#include "SettingsTr.hpp"
|
||||||
|
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QScopedValueRollback>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool isDarkPalette(const QPalette &p)
|
||||||
|
{
|
||||||
|
return p.color(QPalette::Window).lightness() < 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont monoFont(int pixelSize)
|
||||||
|
{
|
||||||
|
QFont f;
|
||||||
|
f.setFamilies({QStringLiteral("SF Mono"),
|
||||||
|
QStringLiteral("Cascadia Code"),
|
||||||
|
QStringLiteral("Consolas"),
|
||||||
|
QStringLiteral("Liberation Mono"),
|
||||||
|
QStringLiteral("Menlo"),
|
||||||
|
QStringLiteral("Courier New")});
|
||||||
|
f.setStyleHint(QFont::Monospace);
|
||||||
|
f.setPixelSize(pixelSize);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Tone
|
||||||
|
{
|
||||||
|
QString cardBg;
|
||||||
|
QString cardBd;
|
||||||
|
QString textSoft;
|
||||||
|
QString textMute;
|
||||||
|
QString textFaint;
|
||||||
|
};
|
||||||
|
|
||||||
|
Tone toneFor(bool dark)
|
||||||
|
{
|
||||||
|
return dark ? Tone{"#333333", "#4a4a4a", "#c2c2c2", "#9a9a9a", "#7a7a7a"}
|
||||||
|
: Tone{"#f6f6f6", "#bdbdbd", "#3a3a3a", "#5a5a5a", "#8a8a8a"};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PillTone
|
||||||
|
{
|
||||||
|
QString bg;
|
||||||
|
QString fg;
|
||||||
|
QString bd;
|
||||||
|
};
|
||||||
|
|
||||||
|
PillTone pillTone(Pill::Kind kind, bool dark)
|
||||||
|
{
|
||||||
|
switch (kind) {
|
||||||
|
case Pill::Template:
|
||||||
|
return dark ? PillTone{"#2c3f5a", "#cfe1f7", "#4a6286"}
|
||||||
|
: PillTone{"#dbe7f6", "#1f3f73", "#a8c1e0"};
|
||||||
|
case Pill::On:
|
||||||
|
return dark ? PillTone{"#2f4530", "#bce0bd", "#4a6c4b"}
|
||||||
|
: PillTone{"#dbe9d3", "#2c5a1c", "#a3bc97"};
|
||||||
|
case Pill::Off:
|
||||||
|
return dark ? PillTone{"#3a3a3a", "#8a8a8a", "#4a4a4a"}
|
||||||
|
: PillTone{"#ececec", "#7a7a7a", "#c8c8c8"};
|
||||||
|
case Pill::User:
|
||||||
|
return dark ? PillTone{"#4a3f24", "#e6cd92", "#6a5a30"}
|
||||||
|
: PillTone{"#f0e4cf", "#75541a", "#cdb98a"};
|
||||||
|
case Pill::Tag:
|
||||||
|
return dark ? PillTone{"#2e2e3a", "#b9b9cf", "#46465a"}
|
||||||
|
: PillTone{"#e7e7f2", "#46466e", "#c1c1d5"};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// -- Pill --------------------------------------------------------------
|
||||||
|
|
||||||
|
Pill::Pill(Kind kind, const QString &text, QWidget *parent)
|
||||||
|
: QLabel(text, parent)
|
||||||
|
, m_kind(kind)
|
||||||
|
{
|
||||||
|
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
|
||||||
|
setAlignment(Qt::AlignCenter);
|
||||||
|
QFont f = font();
|
||||||
|
f.setPixelSize(11);
|
||||||
|
setFont(f);
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Pill::setKind(Kind kind)
|
||||||
|
{
|
||||||
|
if (m_kind == kind)
|
||||||
|
return;
|
||||||
|
m_kind = kind;
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Pill::changeEvent(QEvent *event)
|
||||||
|
{
|
||||||
|
QLabel::changeEvent(event);
|
||||||
|
if (m_inApplyTheme)
|
||||||
|
return;
|
||||||
|
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Pill::applyTheme()
|
||||||
|
{
|
||||||
|
if (m_inApplyTheme)
|
||||||
|
return;
|
||||||
|
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
|
||||||
|
|
||||||
|
const auto t = pillTone(m_kind, isDarkPalette(palette()));
|
||||||
|
setStyleSheet(QStringLiteral(
|
||||||
|
"QLabel { background-color: %1; color: %2;"
|
||||||
|
" border: 1px solid %3; padding: 1px 7px; }")
|
||||||
|
.arg(t.bg, t.fg, t.bd));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- AgentSlotWidget ---------------------------------------------------
|
||||||
|
|
||||||
|
AgentSlotWidget::AgentSlotWidget(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_StyledBackground, true);
|
||||||
|
setObjectName(QStringLiteral("AgentSlot"));
|
||||||
|
|
||||||
|
m_name = new QLabel(this);
|
||||||
|
QFont nameFont = m_name->font();
|
||||||
|
nameFont.setBold(true);
|
||||||
|
m_name->setFont(nameFont);
|
||||||
|
m_name->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
|
||||||
|
m_sourcePill = new Pill(Pill::User, {}, this);
|
||||||
|
m_sourcePill->hide();
|
||||||
|
|
||||||
|
m_changeBtn = new QPushButton(Tr::tr("Change…"), this);
|
||||||
|
m_changeBtn->setCursor(Qt::PointingHandCursor);
|
||||||
|
connect(m_changeBtn, &QPushButton::clicked, this, &AgentSlotWidget::changeRequested);
|
||||||
|
|
||||||
|
m_editBtn = new QPushButton(Tr::tr("Edit"), this);
|
||||||
|
m_editBtn->setCursor(Qt::PointingHandCursor);
|
||||||
|
m_editBtn->setToolTip(Tr::tr("Open this agent in the Agents settings page."));
|
||||||
|
connect(m_editBtn, &QPushButton::clicked, this, &AgentSlotWidget::editRequested);
|
||||||
|
|
||||||
|
auto makeFieldLabel = [this]() {
|
||||||
|
auto *l = new QLabel(this);
|
||||||
|
l->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
|
return l;
|
||||||
|
};
|
||||||
|
auto makeMonoValue = [this]() {
|
||||||
|
auto *l = new QLabel(this);
|
||||||
|
l->setFont(monoFont(11));
|
||||||
|
l->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
l->setMinimumWidth(0);
|
||||||
|
l->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||||
|
return l;
|
||||||
|
};
|
||||||
|
auto makePlainValue = [this]() {
|
||||||
|
auto *l = new QLabel(this);
|
||||||
|
l->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
l->setMinimumWidth(0);
|
||||||
|
l->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||||
|
return l;
|
||||||
|
};
|
||||||
|
|
||||||
|
m_modelLabel = makeFieldLabel();
|
||||||
|
m_modelLabel->setText(Tr::tr("Model"));
|
||||||
|
m_modelValue = makeMonoValue();
|
||||||
|
|
||||||
|
m_urlLabel = makeFieldLabel();
|
||||||
|
m_urlLabel->setText(Tr::tr("Provider"));
|
||||||
|
m_urlValue = makeMonoValue();
|
||||||
|
|
||||||
|
m_endpointLabel = makeFieldLabel();
|
||||||
|
m_endpointLabel->setText(Tr::tr("Endpoint"));
|
||||||
|
m_endpointValue = makeMonoValue();
|
||||||
|
|
||||||
|
m_templateLabel = makeFieldLabel();
|
||||||
|
m_templateLabel->setText(Tr::tr("Template"));
|
||||||
|
m_templateValue = makePlainValue();
|
||||||
|
|
||||||
|
m_description = new QLabel(this);
|
||||||
|
m_description->setWordWrap(true);
|
||||||
|
m_description->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
QFont descFont = m_description->font();
|
||||||
|
descFont.setItalic(true);
|
||||||
|
m_description->setFont(descFont);
|
||||||
|
|
||||||
|
m_thinkingPill = new Pill(Pill::Off, {}, this);
|
||||||
|
m_toolsPill = new Pill(Pill::Off, {}, this);
|
||||||
|
|
||||||
|
// Header row
|
||||||
|
auto *nameRow = new QHBoxLayout;
|
||||||
|
nameRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
nameRow->setSpacing(6);
|
||||||
|
nameRow->addWidget(m_name);
|
||||||
|
nameRow->addWidget(m_sourcePill);
|
||||||
|
nameRow->addStretch(1);
|
||||||
|
|
||||||
|
auto *buttonsBox = new QHBoxLayout;
|
||||||
|
buttonsBox->setContentsMargins(0, 0, 0, 0);
|
||||||
|
buttonsBox->setSpacing(4);
|
||||||
|
buttonsBox->addWidget(m_editBtn);
|
||||||
|
buttonsBox->addWidget(m_changeBtn);
|
||||||
|
|
||||||
|
auto *headerRow = new QHBoxLayout;
|
||||||
|
headerRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
headerRow->setSpacing(8);
|
||||||
|
headerRow->addLayout(nameRow, 1);
|
||||||
|
headerRow->addLayout(buttonsBox, 0);
|
||||||
|
|
||||||
|
// Two-column field grid
|
||||||
|
auto *grid = new QGridLayout;
|
||||||
|
grid->setContentsMargins(0, 0, 0, 0);
|
||||||
|
grid->setHorizontalSpacing(8);
|
||||||
|
grid->setVerticalSpacing(2);
|
||||||
|
grid->setColumnMinimumWidth(0, 62);
|
||||||
|
grid->setColumnStretch(0, 0);
|
||||||
|
grid->setColumnStretch(1, 1);
|
||||||
|
|
||||||
|
grid->addWidget(m_modelLabel, 0, 0);
|
||||||
|
grid->addWidget(m_modelValue, 0, 1);
|
||||||
|
grid->addWidget(m_urlLabel, 1, 0);
|
||||||
|
grid->addWidget(m_urlValue, 1, 1);
|
||||||
|
grid->addWidget(m_endpointLabel, 2, 0);
|
||||||
|
grid->addWidget(m_endpointValue, 2, 1);
|
||||||
|
grid->addWidget(m_templateLabel, 3, 0);
|
||||||
|
grid->addWidget(m_templateValue, 3, 1);
|
||||||
|
|
||||||
|
auto *pillsRow = new QHBoxLayout;
|
||||||
|
pillsRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
pillsRow->setSpacing(4);
|
||||||
|
pillsRow->addWidget(m_thinkingPill);
|
||||||
|
pillsRow->addWidget(m_toolsPill);
|
||||||
|
pillsRow->addStretch(1);
|
||||||
|
|
||||||
|
auto *outer = new QVBoxLayout(this);
|
||||||
|
outer->setContentsMargins(10, 10, 10, 10);
|
||||||
|
outer->setSpacing(6);
|
||||||
|
outer->addLayout(headerRow);
|
||||||
|
outer->addLayout(grid);
|
||||||
|
outer->addWidget(m_description);
|
||||||
|
outer->addLayout(pillsRow);
|
||||||
|
|
||||||
|
applyTheme();
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSlotWidget::changeEvent(QEvent *event)
|
||||||
|
{
|
||||||
|
QWidget::changeEvent(event);
|
||||||
|
if (m_inApplyTheme)
|
||||||
|
return;
|
||||||
|
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSlotWidget::applyTheme()
|
||||||
|
{
|
||||||
|
if (m_inApplyTheme)
|
||||||
|
return;
|
||||||
|
QScopedValueRollback<bool> guard(m_inApplyTheme, true);
|
||||||
|
|
||||||
|
const Tone t = toneFor(isDarkPalette(palette()));
|
||||||
|
|
||||||
|
setStyleSheet(QStringLiteral(
|
||||||
|
"#AgentSlot { background-color: %1; border: 1px solid %2; }")
|
||||||
|
.arg(t.cardBg, t.cardBd));
|
||||||
|
|
||||||
|
auto applyColor = [](QLabel *label, const QString &color) {
|
||||||
|
QPalette p = label->palette();
|
||||||
|
p.setColor(QPalette::WindowText, QColor(color));
|
||||||
|
label->setPalette(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (QLabel *l : {m_modelLabel, m_urlLabel, m_endpointLabel, m_templateLabel})
|
||||||
|
applyColor(l, t.textMute);
|
||||||
|
|
||||||
|
applyColor(m_description, t.textSoft);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSlotWidget::setAgentConfig(const AgentConfig &cfg)
|
||||||
|
{
|
||||||
|
m_name->setText(cfg.name);
|
||||||
|
|
||||||
|
if (cfg.isUserSource()) {
|
||||||
|
m_sourcePill->setText(cfg.overridesBundled
|
||||||
|
? Tr::tr("User overrides bundled")
|
||||||
|
: Tr::tr("User"));
|
||||||
|
m_sourcePill->show();
|
||||||
|
} else {
|
||||||
|
m_sourcePill->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_modelValue->setText(cfg.model);
|
||||||
|
m_modelValue->setToolTip(cfg.model);
|
||||||
|
// The agent profile no longer carries a URL — the URL belongs to
|
||||||
|
// the referenced provider instance and is editable on the Providers
|
||||||
|
// settings page. Surface the instance name in this row instead.
|
||||||
|
m_urlValue->setText(cfg.providerInstance);
|
||||||
|
m_urlValue->setToolTip(cfg.providerInstance);
|
||||||
|
m_endpointValue->setText(cfg.endpoint);
|
||||||
|
m_endpointValue->setToolTip(cfg.endpoint);
|
||||||
|
// Templates are now inline in the agent profile — no separate name to
|
||||||
|
// surface. Keep the row but show '—' so the layout stays stable.
|
||||||
|
m_templateLabel->hide();
|
||||||
|
m_templateValue->hide();
|
||||||
|
|
||||||
|
m_description->setText(cfg.description.isEmpty()
|
||||||
|
? Tr::tr("No description provided.")
|
||||||
|
: cfg.description);
|
||||||
|
|
||||||
|
const Tone t = toneFor(isDarkPalette(palette()));
|
||||||
|
QPalette descPal = m_description->palette();
|
||||||
|
descPal.setColor(QPalette::WindowText,
|
||||||
|
QColor(cfg.description.isEmpty() ? t.textFaint : t.textSoft));
|
||||||
|
m_description->setPalette(descPal);
|
||||||
|
|
||||||
|
m_thinkingPill->setKind(cfg.enableThinking ? Pill::On : Pill::Off);
|
||||||
|
m_thinkingPill->setText(cfg.enableThinking ? Tr::tr("thinking on")
|
||||||
|
: Tr::tr("thinking off"));
|
||||||
|
m_toolsPill->setKind(cfg.enableTools ? Pill::On : Pill::Off);
|
||||||
|
m_toolsPill->setText(cfg.enableTools ? Tr::tr("tools on")
|
||||||
|
: Tr::tr("tools off"));
|
||||||
|
m_thinkingPill->show();
|
||||||
|
m_toolsPill->show();
|
||||||
|
|
||||||
|
m_editBtn->setEnabled(!cfg.name.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentSlotWidget::clear()
|
||||||
|
{
|
||||||
|
const Tone t = toneFor(isDarkPalette(palette()));
|
||||||
|
|
||||||
|
m_name->setText(QStringLiteral("<i style=\"color:%1\">%2</i>")
|
||||||
|
.arg(t.textFaint, Tr::tr("(no agent selected)")));
|
||||||
|
m_sourcePill->hide();
|
||||||
|
|
||||||
|
for (QLabel *l : {m_modelValue, m_urlValue, m_endpointValue, m_templateValue}) {
|
||||||
|
l->clear();
|
||||||
|
l->setToolTip({});
|
||||||
|
}
|
||||||
|
|
||||||
|
m_description->setText({});
|
||||||
|
m_thinkingPill->hide();
|
||||||
|
m_toolsPill->hide();
|
||||||
|
m_editBtn->setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
77
sources/settings/AgentSlotWidget.hpp
Normal file
77
sources/settings/AgentSlotWidget.hpp
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
class Pill : public QLabel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
enum Kind { Template, On, Off, User, Tag };
|
||||||
|
|
||||||
|
Pill(Kind kind, const QString &text = {}, QWidget *parent = nullptr);
|
||||||
|
void setKind(Kind kind);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void changeEvent(QEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void applyTheme();
|
||||||
|
|
||||||
|
Kind m_kind;
|
||||||
|
bool m_inApplyTheme = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AgentSlotWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit AgentSlotWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setAgentConfig(const AgentConfig &cfg);
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
QPushButton *changeButton() const { return m_changeBtn; }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void changeRequested();
|
||||||
|
void editRequested();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void changeEvent(QEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void applyTheme();
|
||||||
|
|
||||||
|
QLabel *m_name = nullptr;
|
||||||
|
Pill *m_sourcePill = nullptr;
|
||||||
|
QPushButton *m_changeBtn = nullptr;
|
||||||
|
QPushButton *m_editBtn = nullptr;
|
||||||
|
|
||||||
|
QLabel *m_modelLabel = nullptr;
|
||||||
|
QLabel *m_modelValue = nullptr;
|
||||||
|
QLabel *m_urlLabel = nullptr;
|
||||||
|
QLabel *m_urlValue = nullptr;
|
||||||
|
QLabel *m_endpointLabel = nullptr;
|
||||||
|
QLabel *m_endpointValue = nullptr;
|
||||||
|
QLabel *m_templateLabel = nullptr;
|
||||||
|
QLabel *m_templateValue = nullptr;
|
||||||
|
|
||||||
|
QLabel *m_description = nullptr;
|
||||||
|
|
||||||
|
Pill *m_thinkingPill = nullptr;
|
||||||
|
Pill *m_toolsPill = nullptr;
|
||||||
|
|
||||||
|
bool m_inApplyTheme = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
38
sources/settings/CMakeLists.txt
Normal file
38
sources/settings/CMakeLists.txt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
add_library(QodeAssistAgentPipelines OBJECT
|
||||||
|
AgentPipelinesPage.hpp AgentPipelinesPage.cpp
|
||||||
|
PipelinesConfig.hpp PipelinesConfig.cpp
|
||||||
|
AgentRosterWidget.hpp AgentRosterWidget.cpp
|
||||||
|
AgentSlotWidget.hpp AgentSlotWidget.cpp
|
||||||
|
AgentSelectionDialog.hpp AgentSelectionDialog.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(QodeAssistAgentPipelines PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
${CMAKE_SOURCE_DIR}/sources/providers
|
||||||
|
${CMAKE_SOURCE_DIR}/sources/common
|
||||||
|
${CMAKE_SOURCE_DIR}/sources/agents
|
||||||
|
${CMAKE_SOURCE_DIR}/settings
|
||||||
|
${CMAKE_SOURCE_DIR}/logger
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(QodeAssistAgentPipelines PRIVATE
|
||||||
|
Qt::Core
|
||||||
|
Qt::Gui
|
||||||
|
Qt::Widgets
|
||||||
|
Qt::Network
|
||||||
|
QtCreator::Core
|
||||||
|
QtCreator::Utils
|
||||||
|
Agents
|
||||||
|
Providers
|
||||||
|
Common
|
||||||
|
LLMQore
|
||||||
|
QodeAssistLogger
|
||||||
|
TomlSerializer
|
||||||
|
tomlplusplus::tomlplusplus
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(QodeAssistAgentPipelines PRIVATE
|
||||||
|
QODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
|
||||||
|
QODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
|
||||||
|
QODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
||||||
|
)
|
||||||
280
sources/settings/PipelinesConfig.cpp
Normal file
280
sources/settings/PipelinesConfig.cpp
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "PipelinesConfig.hpp"
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QSaveFile>
|
||||||
|
|
||||||
|
#include <toml++/toml.hpp>
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "TomlWriter.hpp"
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char *kSection = "pipelines";
|
||||||
|
constexpr const char *kCodeCompletion = "code_completion";
|
||||||
|
constexpr const char *kChatAssistant = "chat_assistant";
|
||||||
|
constexpr const char *kChatCompression = "chat_compression";
|
||||||
|
constexpr const char *kQuickRefactor = "quick_refactor";
|
||||||
|
constexpr int kMaxAgentNameLen = 256;
|
||||||
|
|
||||||
|
QString trimAndCap(const QString &raw)
|
||||||
|
{
|
||||||
|
QString s = raw.trimmed();
|
||||||
|
if (s.size() > kMaxAgentNameLen)
|
||||||
|
s.truncate(kMaxAgentNameLen);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList toStringList(const toml::node *node, const QString &slotKey, bool *schemaOk)
|
||||||
|
{
|
||||||
|
QStringList out;
|
||||||
|
if (!node)
|
||||||
|
return out;
|
||||||
|
const auto *arr = node->as_array();
|
||||||
|
if (!arr) {
|
||||||
|
LOG_MESSAGE(QStringLiteral(
|
||||||
|
"[Pipelines] schema error: '%1' must be an array of strings, got non-array")
|
||||||
|
.arg(slotKey));
|
||||||
|
if (schemaOk)
|
||||||
|
*schemaOk = false;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
out.reserve(static_cast<qsizetype>(arr->size()));
|
||||||
|
for (size_t i = 0; i < arr->size(); ++i) {
|
||||||
|
const auto &el = (*arr)[i];
|
||||||
|
if (const auto *s = el.as_string()) {
|
||||||
|
const QString name = trimAndCap(QString::fromStdString(s->get()));
|
||||||
|
if (name.isEmpty())
|
||||||
|
continue;
|
||||||
|
if (out.contains(name)) {
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] '%1' element #%2 is a duplicate, dropping")
|
||||||
|
.arg(slotKey)
|
||||||
|
.arg(i));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.append(name);
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] '%1' element #%2 is not a string, dropping")
|
||||||
|
.arg(slotKey)
|
||||||
|
.arg(i));
|
||||||
|
if (schemaOk)
|
||||||
|
*schemaOk = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fillMissingFromDefaults(PipelineRosters &r, const toml::table §ion)
|
||||||
|
{
|
||||||
|
const PipelineRosters defs = PipelineRosters::defaults();
|
||||||
|
if (!section.contains(kCodeCompletion))
|
||||||
|
r.codeCompletion = defs.codeCompletion;
|
||||||
|
if (!section.contains(kChatAssistant))
|
||||||
|
r.chatAssistant = defs.chatAssistant;
|
||||||
|
if (!section.contains(kChatCompression))
|
||||||
|
r.chatCompression = defs.chatCompression;
|
||||||
|
if (!section.contains(kQuickRefactor))
|
||||||
|
r.quickRefactor = defs.quickRefactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PipelineRosters PipelineRosters::defaults()
|
||||||
|
{
|
||||||
|
PipelineRosters r;
|
||||||
|
r.codeCompletion = {QStringLiteral("Ollama Qwen2.5-Coder Completion")};
|
||||||
|
r.chatAssistant = {QStringLiteral("Ollama Chat")};
|
||||||
|
r.chatCompression = {QStringLiteral("Ollama Compression")};
|
||||||
|
r.quickRefactor = {QStringLiteral("Ollama Quick Refactor")};
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PipelinesConfig::filePath()
|
||||||
|
{
|
||||||
|
return Core::ICore::userResourcePath(QStringLiteral("qodeassist/config/pipelines.toml"))
|
||||||
|
.toFSPathString();
|
||||||
|
}
|
||||||
|
|
||||||
|
PipelinesLoadResult PipelinesConfig::load()
|
||||||
|
{
|
||||||
|
PipelinesLoadResult result;
|
||||||
|
const QString path = filePath();
|
||||||
|
QFile f(path);
|
||||||
|
if (!f.exists()) {
|
||||||
|
result.rosters = PipelineRosters::defaults();
|
||||||
|
result.status = PipelinesLoadStatus::FileMissing;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
result.rosters = PipelineRosters::defaults();
|
||||||
|
result.status = PipelinesLoadStatus::ParseError;
|
||||||
|
result.message = QStringLiteral("cannot open %1: %2").arg(path, f.errorString());
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(result.message));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const QByteArray contents = f.readAll();
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
toml::table tbl;
|
||||||
|
try {
|
||||||
|
tbl = toml::parse(std::string_view(contents.constData(),
|
||||||
|
static_cast<size_t>(contents.size())),
|
||||||
|
path.toStdString());
|
||||||
|
} catch (const toml::parse_error &e) {
|
||||||
|
result.rosters = PipelineRosters::defaults();
|
||||||
|
result.status = PipelinesLoadStatus::ParseError;
|
||||||
|
result.message = QStringLiteral("parse error in %1: %2")
|
||||||
|
.arg(path, QString::fromStdString(std::string(e.description())));
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(result.message));
|
||||||
|
return result;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
result.rosters = PipelineRosters::defaults();
|
||||||
|
result.status = PipelinesLoadStatus::ParseError;
|
||||||
|
result.message = QStringLiteral("error reading %1: %2")
|
||||||
|
.arg(path, QString::fromUtf8(e.what()));
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(result.message));
|
||||||
|
return result;
|
||||||
|
} catch (...) {
|
||||||
|
result.rosters = PipelineRosters::defaults();
|
||||||
|
result.status = PipelinesLoadStatus::ParseError;
|
||||||
|
result.message = QStringLiteral("unknown error reading %1").arg(path);
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(result.message));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto *section = tbl[kSection].as_table();
|
||||||
|
if (!section) {
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] no [pipelines] section in %1; using defaults")
|
||||||
|
.arg(path));
|
||||||
|
result.rosters = PipelineRosters::defaults();
|
||||||
|
result.status = PipelinesLoadStatus::SchemaError;
|
||||||
|
result.message = QStringLiteral("missing [pipelines] section");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool schemaOk = true;
|
||||||
|
result.rosters.codeCompletion
|
||||||
|
= toStringList((*section)[kCodeCompletion].node(), kCodeCompletion, &schemaOk);
|
||||||
|
result.rosters.chatAssistant
|
||||||
|
= toStringList((*section)[kChatAssistant].node(), kChatAssistant, &schemaOk);
|
||||||
|
result.rosters.chatCompression
|
||||||
|
= toStringList((*section)[kChatCompression].node(), kChatCompression, &schemaOk);
|
||||||
|
result.rosters.quickRefactor
|
||||||
|
= toStringList((*section)[kQuickRefactor].node(), kQuickRefactor, &schemaOk);
|
||||||
|
|
||||||
|
fillMissingFromDefaults(result.rosters, *section);
|
||||||
|
|
||||||
|
if (!schemaOk) {
|
||||||
|
result.status = PipelinesLoadStatus::SchemaError;
|
||||||
|
result.message = QStringLiteral("some entries had wrong type; see log");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PipelinesConfig::save(const PipelineRosters &rosters, QString *errorOut)
|
||||||
|
{
|
||||||
|
const QString path = filePath();
|
||||||
|
const QFileInfo info(path);
|
||||||
|
if (!QDir().mkpath(info.absolutePath())) {
|
||||||
|
const QString err = QStringLiteral("cannot create configuration directory: %1")
|
||||||
|
.arg(info.absolutePath());
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(err));
|
||||||
|
if (errorOut)
|
||||||
|
*errorOut = err;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TomlSerializer::TomlWriter w;
|
||||||
|
w.writeComment(QStringLiteral(
|
||||||
|
"QodeAssist pipelines — slot → ordered list of agent names."));
|
||||||
|
w.writeComment(QStringLiteral(
|
||||||
|
"The router walks each list top-down at request time and uses"));
|
||||||
|
w.writeComment(QStringLiteral("the first matching agent."));
|
||||||
|
w.writeBlankLine();
|
||||||
|
w.writeTableHeader(QString::fromUtf8(kSection));
|
||||||
|
w.writeStringArray(QString::fromUtf8(kCodeCompletion), rosters.codeCompletion);
|
||||||
|
w.writeStringArray(QString::fromUtf8(kChatAssistant), rosters.chatAssistant);
|
||||||
|
w.writeStringArray(QString::fromUtf8(kChatCompression), rosters.chatCompression);
|
||||||
|
w.writeStringArray(QString::fromUtf8(kQuickRefactor), rosters.quickRefactor);
|
||||||
|
|
||||||
|
QSaveFile out(path);
|
||||||
|
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
if (errorOut)
|
||||||
|
*errorOut = out.errorString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QByteArray payload = w.toUtf8();
|
||||||
|
const qint64 written = out.write(payload);
|
||||||
|
if (written != payload.size()) {
|
||||||
|
const QString err = QStringLiteral("short write (%1/%2): %3")
|
||||||
|
.arg(written)
|
||||||
|
.arg(payload.size())
|
||||||
|
.arg(out.errorString());
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(err));
|
||||||
|
out.cancelWriting();
|
||||||
|
if (errorOut)
|
||||||
|
*errorOut = err;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!out.commit()) {
|
||||||
|
if (errorOut)
|
||||||
|
*errorOut = out.errorString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PipelinesConfig::validate(const QodeAssist::AgentFactory &factory, QString *errorOut)
|
||||||
|
{
|
||||||
|
PipelinesLoadResult lr = load();
|
||||||
|
PipelineRosters &r = lr.rosters;
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
auto fix = [&](QStringList ¤t) {
|
||||||
|
QStringList kept;
|
||||||
|
kept.reserve(current.size());
|
||||||
|
for (const QString &raw : current) {
|
||||||
|
const QString name = trimAndCap(raw);
|
||||||
|
if (name.isEmpty() || kept.contains(name))
|
||||||
|
continue;
|
||||||
|
if (factory.configByName(name))
|
||||||
|
kept.append(name);
|
||||||
|
}
|
||||||
|
if (kept != current) {
|
||||||
|
current = std::move(kept);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fix(r.codeCompletion);
|
||||||
|
fix(r.chatAssistant);
|
||||||
|
fix(r.chatCompression);
|
||||||
|
fix(r.quickRefactor);
|
||||||
|
|
||||||
|
if (!changed && lr.status == PipelinesLoadStatus::Ok)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
QString saveErr;
|
||||||
|
if (!save(r, &saveErr)) {
|
||||||
|
const QString msg = QStringLiteral("failed to persist after validation: %1").arg(saveErr);
|
||||||
|
LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(msg));
|
||||||
|
if (errorOut)
|
||||||
|
*errorOut = msg;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
47
sources/settings/PipelinesConfig.hpp
Normal file
47
sources/settings/PipelinesConfig.hpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class AgentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
struct PipelineRosters
|
||||||
|
{
|
||||||
|
QStringList codeCompletion;
|
||||||
|
QStringList chatAssistant;
|
||||||
|
QStringList chatCompression;
|
||||||
|
QStringList quickRefactor;
|
||||||
|
|
||||||
|
[[nodiscard]] static PipelineRosters defaults();
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class PipelinesLoadStatus { Ok, FileMissing, ParseError, SchemaError };
|
||||||
|
|
||||||
|
struct PipelinesLoadResult
|
||||||
|
{
|
||||||
|
PipelineRosters rosters;
|
||||||
|
PipelinesLoadStatus status = PipelinesLoadStatus::Ok;
|
||||||
|
QString message;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PipelinesConfig
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
[[nodiscard]] static QString filePath();
|
||||||
|
|
||||||
|
[[nodiscard]] static PipelinesLoadResult load();
|
||||||
|
|
||||||
|
[[nodiscard]] static bool save(const PipelineRosters &rosters, QString *errorOut = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] static bool validate(
|
||||||
|
const QodeAssist::AgentFactory &factory, QString *errorOut = nullptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
Reference in New Issue
Block a user