mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-14 02:09:22 -04:00
427 lines
14 KiB
C++
427 lines
14 KiB
C++
// Copyright (C) 2024-2026 Petr Mironychev
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
|
|
#include "GeneralSettings.hpp"
|
|
|
|
#include <coreplugin/dialogs/ioptionspage.h>
|
|
#include <coreplugin/icore.h>
|
|
#include <utils/layoutbuilder.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 "SettingsUtils.hpp"
|
|
#include "UpdateDialog.hpp"
|
|
|
|
#include <AgentFactory.hpp>
|
|
|
|
namespace QodeAssist::Settings {
|
|
|
|
GeneralSettings &generalSettings()
|
|
{
|
|
static GeneralSettings settings;
|
|
return settings;
|
|
}
|
|
|
|
GeneralSettings::GeneralSettings()
|
|
{
|
|
setAutoApply(false);
|
|
|
|
setDisplayName(TrConstants::GENERAL);
|
|
|
|
enableQodeAssist.setSettingsKey(Constants::ENABLE_QODE_ASSIST);
|
|
enableQodeAssist.setLabelText(TrConstants::ENABLE_QODE_ASSIST);
|
|
enableQodeAssist.setDefaultValue(true);
|
|
|
|
enableLogging.setSettingsKey(Constants::ENABLE_LOGGING);
|
|
enableLogging.setLabelText(TrConstants::ENABLE_LOG);
|
|
enableLogging.setToolTip(TrConstants::ENABLE_LOG_TOOLTIP);
|
|
enableLogging.setDefaultValue(false);
|
|
|
|
enableCheckUpdate.setSettingsKey(Constants::ENABLE_CHECK_UPDATE);
|
|
enableCheckUpdate.setLabelText(TrConstants::ENABLE_CHECK_UPDATE_ON_START);
|
|
enableCheckUpdate.setDefaultValue(true);
|
|
|
|
requestTimeout.setSettingsKey(Constants::REQUEST_TIMEOUT);
|
|
requestTimeout.setLabelText(Tr::tr("Request timeout (seconds):"));
|
|
requestTimeout.setToolTip(Tr::tr(
|
|
"Maximum time to wait for the model to send data before a request is aborted. "
|
|
"Applies to all requests — chat, code completion, quick refactor and chat compression. "
|
|
"The timer resets every time data is received, so this effectively limits the "
|
|
"time-to-first-token and any stall between tokens. Increase it for slow or local "
|
|
"models that need a long time to start responding. Set to 0 to disable the timeout."));
|
|
requestTimeout.setRange(0, 3600);
|
|
requestTimeout.setDefaultValue(120);
|
|
|
|
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
|
checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE;
|
|
|
|
readSettings();
|
|
|
|
Logger::instance().setLoggingEnabled(enableLogging());
|
|
|
|
setupConnections();
|
|
|
|
setLayouter([this]() {
|
|
using namespace Layouting;
|
|
|
|
auto networkGroup = Group{
|
|
title(Tr::tr("Network")),
|
|
Column{Row{requestTimeout, Stretch{1}}}};
|
|
|
|
auto *supportLabel = new QLabel(Tr::tr("Support the development of QodeAssist:"));
|
|
|
|
auto *supportLinks = new QLabel(
|
|
"<a href='https://ko-fi.com/qodeassist' style='color: #0066cc;'>Support on Ko-fi ☕</a>"
|
|
" | "
|
|
"<a href='https://github.com/Palm1r/"
|
|
"QodeAssist?tab=readme-ov-file#support-the-development-of-qodeassist' "
|
|
"style='color: #0066cc;'>Support page on GitHub</a>"
|
|
" | "
|
|
"<a href='https://www.paypal.com/paypalme/palm1r' style='color: #0066cc;'>Support via "
|
|
"PayPal 💳</a>");
|
|
supportLinks->setOpenExternalLinks(true);
|
|
supportLinks->setTextFormat(Qt::RichText);
|
|
|
|
return Column{
|
|
Row{supportLabel, supportLinks, Stretch{1}},
|
|
Space{8},
|
|
Row{enableQodeAssist, Stretch{1}},
|
|
Row{enableLogging, Stretch{1}},
|
|
Row{enableCheckUpdate, Stretch{1}},
|
|
Space{8},
|
|
networkGroup,
|
|
Stretch{1}};
|
|
});
|
|
}
|
|
|
|
void GeneralSettings::setupConnections()
|
|
{
|
|
connect(&enableLogging, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
|
Logger::instance().setLoggingEnabled(enableLogging.volatileValue());
|
|
});
|
|
connect(&resetToDefaults, &ButtonAspect::clicked, this, &GeneralSettings::resetPageToDefaults);
|
|
connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() {
|
|
QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent());
|
|
});
|
|
}
|
|
|
|
void GeneralSettings::resetPageToDefaults()
|
|
{
|
|
const QMessageBox::StandardButton reply = QMessageBox::question(
|
|
Core::ICore::dialogParent(),
|
|
TrConstants::RESET_SETTINGS,
|
|
TrConstants::CONFIRMATION,
|
|
QMessageBox::Yes | QMessageBox::No);
|
|
|
|
if (reply == QMessageBox::Yes) {
|
|
resetAspect(enableQodeAssist);
|
|
resetAspect(enableLogging);
|
|
resetAspect(requestTimeout);
|
|
resetAspect(enableCheckUpdate);
|
|
writeSettings();
|
|
}
|
|
}
|
|
|
|
class GeneralSettingsPage : public Core::IOptionsPage
|
|
{
|
|
public:
|
|
GeneralSettingsPage()
|
|
{
|
|
setId(Constants::QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID);
|
|
setDisplayName(TrConstants::GENERAL);
|
|
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
|
#if QODEASSIST_QT_CREATOR_VERSION < QT_VERSION_CHECK(15, 0, 83)
|
|
setDisplayCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY);
|
|
setCategoryIconPath(":/resources/images/qoderassist-icon.png");
|
|
#endif
|
|
setSettingsProvider([] { return &generalSettings(); });
|
|
}
|
|
};
|
|
|
|
const GeneralSettingsPage generalSettingsPage;
|
|
|
|
/*!
|
|
\sa {Core::ICore::showOptionsDialog()}, {Core::ICore::showSettings()}
|
|
\note This function was added to fix Qt Creator API broken changes in v19.0.0-beta2 version
|
|
*/
|
|
void showSettings(const Utils::Id page)
|
|
{
|
|
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 83)
|
|
Core::ICore::showSettings(page);
|
|
#else
|
|
Core::ICore::showOptionsDialog(page);
|
|
#endif
|
|
}
|
|
|
|
/*!
|
|
\sa {Core::ICore::showOptionsDialog()}, {Core::ICore::showSettings()}
|
|
\note This function was added to fix Qt Creator API broken changes in v19.0.0-beta2 version
|
|
*/
|
|
void showSettings(const Utils::Id page, Utils::Id item)
|
|
{
|
|
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 83)
|
|
Core::ICore::showSettings(page, item);
|
|
#else
|
|
Core::ICore::showOptionsDialog(page, item);
|
|
#endif
|
|
}
|
|
|
|
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 "GeneralSettings.moc"
|