mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-20 02:01:23 -04:00
refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore
This commit is contained in:
211
sources/settings/AgentRole.cpp
Normal file
211
sources/settings/AgentRole.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
// 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 "AgentRole.hpp"
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
QString AgentRolesManager::getConfigurationDirectory()
|
||||
{
|
||||
QString path = QString("%1/qodeassist/agent_roles")
|
||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
||||
QDir().mkpath(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
QList<AgentRole> AgentRolesManager::loadAllRoles()
|
||||
{
|
||||
QList<AgentRole> roles;
|
||||
QString configDir = getConfigurationDirectory();
|
||||
QDir dir(configDir);
|
||||
|
||||
ensureDefaultRoles();
|
||||
|
||||
const QStringList jsonFiles = dir.entryList({"*.json"}, QDir::Files);
|
||||
for (const QString &fileName : jsonFiles) {
|
||||
AgentRole role = loadRoleFromFile(dir.absoluteFilePath(fileName));
|
||||
if (!role.id.isEmpty()) {
|
||||
roles.append(role);
|
||||
}
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::loadRole(const QString &roleId)
|
||||
{
|
||||
if (roleId.isEmpty())
|
||||
return {};
|
||||
|
||||
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
|
||||
if (!QFile::exists(filePath))
|
||||
return {};
|
||||
|
||||
return loadRoleFromFile(filePath);
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::loadRoleFromFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
if (doc.isNull() || !doc.isObject())
|
||||
return {};
|
||||
|
||||
return AgentRole::fromJson(doc.object());
|
||||
}
|
||||
|
||||
bool AgentRolesManager::saveRole(const AgentRole &role)
|
||||
{
|
||||
if (role.id.isEmpty())
|
||||
return false;
|
||||
|
||||
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(role.id + ".json");
|
||||
QFile file(filePath);
|
||||
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
|
||||
QJsonDocument doc(role.toJson());
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AgentRolesManager::deleteRole(const QString &roleId)
|
||||
{
|
||||
if (roleId.isEmpty())
|
||||
return false;
|
||||
|
||||
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
|
||||
return QFile::remove(filePath);
|
||||
}
|
||||
|
||||
bool AgentRolesManager::roleExists(const QString &roleId)
|
||||
{
|
||||
if (roleId.isEmpty())
|
||||
return false;
|
||||
|
||||
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
|
||||
return QFile::exists(filePath);
|
||||
}
|
||||
|
||||
void AgentRolesManager::ensureDefaultRoles()
|
||||
{
|
||||
QDir dir(getConfigurationDirectory());
|
||||
|
||||
if (!dir.exists("developer.json"))
|
||||
saveRole(getDefaultDeveloperRole());
|
||||
|
||||
if (!dir.exists("reviewer.json"))
|
||||
saveRole(getDefaultReviewerRole());
|
||||
|
||||
if (!dir.exists("researcher.json"))
|
||||
saveRole(getDefaultResearcherRole());
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::getDefaultDeveloperRole()
|
||||
{
|
||||
return AgentRole{
|
||||
"developer",
|
||||
"Developer",
|
||||
"Experienced Qt/C++ developer for implementation tasks",
|
||||
"You are an experienced Qt/C++ developer working on a Qt Creator plugin.\n\n"
|
||||
"Your workflow:\n"
|
||||
"1. **Analyze** - understand the problem and what needs to be done\n"
|
||||
"2. **Propose solution** - explain your approach in 2-3 sentences\n"
|
||||
"3. **Wait for approval** - don't write code until the solution is confirmed\n"
|
||||
"4. **Implement** - write clean, minimal code that solves the task\n\n"
|
||||
"When analyzing:\n"
|
||||
"- Ask clarifying questions if requirements are unclear\n"
|
||||
"- Check existing code for similar patterns\n"
|
||||
"- Consider edge cases and potential issues\n\n"
|
||||
"When proposing:\n"
|
||||
"- Explain what you'll change and why\n"
|
||||
"- Mention files you'll modify\n"
|
||||
"- Note any architectural implications\n\n"
|
||||
"When implementing:\n"
|
||||
"- Use C++20, Qt6, follow existing codebase style\n"
|
||||
"- Write only what's needed (MVP approach)\n"
|
||||
"- Include file paths and necessary changes\n"
|
||||
"- Handle errors properly\n"
|
||||
"- Make sure it compiles\n\n"
|
||||
"Keep it practical:\n"
|
||||
"- Short explanations, let code speak\n"
|
||||
"- No over-engineering or unnecessary refactoring\n"
|
||||
"- No TODOs, debug code, or unfinished work\n"
|
||||
"- Point out non-obvious things\n\n"
|
||||
"You're a pragmatic team member who thinks before coding.",
|
||||
true};
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::getDefaultReviewerRole()
|
||||
{
|
||||
return AgentRole{
|
||||
"reviewer",
|
||||
"Code Reviewer",
|
||||
"Expert C++/QML code reviewer for quality assurance",
|
||||
"You are an expert C++/QML code reviewer specializing in C++20 and Qt6.\n\n"
|
||||
"What you check:\n"
|
||||
"- Bugs, memory leaks, undefined behavior\n"
|
||||
"- C++20 compliance and Qt6 patterns\n"
|
||||
"- RAII, move semantics, smart pointers\n"
|
||||
"- Qt parent-child ownership and signal/slot correctness\n"
|
||||
"- Thread safety and Qt concurrent usage\n"
|
||||
"- const-correctness and Qt container usage\n"
|
||||
"- Performance bottlenecks\n"
|
||||
"- Production readiness: error handling, no debug leftovers\n\n"
|
||||
"What you do:\n"
|
||||
"- Point out problems with clear explanations\n"
|
||||
"- Suggest specific fixes with code examples\n"
|
||||
"- Remove unnecessary comments, keep essential docs only\n"
|
||||
"- Flag anything that's not production-ready\n"
|
||||
"- Recommend optimizations when you spot them\n\n"
|
||||
"Focus on: correctness, performance, maintainability, Qt idioms.\n\n"
|
||||
"Be direct and specific. Show, don't just tell.",
|
||||
true};
|
||||
}
|
||||
|
||||
AgentRole AgentRolesManager::getDefaultResearcherRole()
|
||||
{
|
||||
return AgentRole{
|
||||
"researcher",
|
||||
"Researcher",
|
||||
"Research-oriented developer for exploring solutions",
|
||||
"You are a research-oriented Qt/C++ developer who investigates problems and explores "
|
||||
"solutions.\n\n"
|
||||
"Your job is to think, not to code:\n"
|
||||
"- Deep dive into the problem before suggesting anything\n"
|
||||
"- Research Qt docs, patterns, and best practices\n"
|
||||
"- Find multiple ways to solve it\n"
|
||||
"- Compare trade-offs: performance, complexity, maintainability\n"
|
||||
"- Look for relevant Qt APIs and modules\n"
|
||||
"- Think about architectural consequences\n\n"
|
||||
"How you work:\n"
|
||||
"1. **Problem Analysis** - what exactly needs solving\n"
|
||||
"2. **Research Findings** - what you learned about this problem space\n"
|
||||
"3. **Solution Options** - present 2-3 approaches with honest pros/cons\n"
|
||||
"4. **Recommendation** - which one fits best and why\n"
|
||||
"5. **Next Steps** - what to consider before implementing\n\n"
|
||||
"What you provide:\n"
|
||||
"- Clear comparison of different approaches\n"
|
||||
"- Code snippets as examples (not ready-to-use patches)\n"
|
||||
"- Links to docs, examples, similar implementations\n"
|
||||
"- Questions to clarify requirements\n"
|
||||
"- Warning about potential problems\n\n"
|
||||
"You DO NOT write implementation code. You explore options and let the developer choose.\n\n"
|
||||
"Think like a consultant: research thoroughly, present clearly, stay objective.",
|
||||
true};
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
67
sources/settings/AgentRole.hpp
Normal file
67
sources/settings/AgentRole.hpp
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
struct AgentRole
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString description;
|
||||
QString systemPrompt;
|
||||
bool isBuiltin = false;
|
||||
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
return QJsonObject{
|
||||
{"id", id},
|
||||
{"name", name},
|
||||
{"description", description},
|
||||
{"systemPrompt", systemPrompt},
|
||||
{"isBuiltin", isBuiltin}};
|
||||
}
|
||||
|
||||
static AgentRole fromJson(const QJsonObject &json)
|
||||
{
|
||||
return AgentRole{
|
||||
json["id"].toString(),
|
||||
json["name"].toString(),
|
||||
json["description"].toString(),
|
||||
json["systemPrompt"].toString(),
|
||||
json["isBuiltin"].toBool(false)};
|
||||
}
|
||||
|
||||
bool operator==(const AgentRole &other) const { return id == other.id; }
|
||||
};
|
||||
|
||||
class AgentRolesManager
|
||||
{
|
||||
public:
|
||||
static QString getConfigurationDirectory();
|
||||
static QList<AgentRole> loadAllRoles();
|
||||
static AgentRole loadRole(const QString &roleId);
|
||||
static AgentRole loadRoleFromFile(const QString &filePath);
|
||||
static bool saveRole(const AgentRole &role);
|
||||
static bool deleteRole(const QString &roleId);
|
||||
static bool roleExists(const QString &roleId);
|
||||
static void ensureDefaultRoles();
|
||||
|
||||
static AgentRole getNoRole()
|
||||
{
|
||||
return AgentRole{"", "No Role", "Use base system prompt without role specialization", "", false};
|
||||
}
|
||||
|
||||
private:
|
||||
static AgentRole getDefaultDeveloperRole();
|
||||
static AgentRole getDefaultReviewerRole();
|
||||
static AgentRole getDefaultResearcherRole();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
110
sources/settings/AgentRoleDialog.cpp
Normal file
110
sources/settings/AgentRoleDialog.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
// 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 "AgentRoleDialog.hpp"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
AgentRoleDialog::AgentRoleDialog(Action action, QWidget *parent)
|
||||
: QDialog{parent}
|
||||
, m_action{action}
|
||||
{
|
||||
auto getTitle = [](Action action) {
|
||||
switch(action)
|
||||
{
|
||||
case Action::Add:
|
||||
return tr("Add Agent Role");
|
||||
case Action::Duplicate:
|
||||
return tr("Duplicate Agent Role");
|
||||
case Action::Edit:
|
||||
return tr("Edit Agent Role");
|
||||
}
|
||||
};
|
||||
|
||||
setWindowTitle(getTitle(action));
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void AgentRoleDialog::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
auto *formLayout = new QFormLayout();
|
||||
|
||||
m_nameEdit = new QLineEdit(this);
|
||||
m_nameEdit->setPlaceholderText(tr("e.g., Developer, Code Reviewer"));
|
||||
formLayout->addRow(tr("Name:"), m_nameEdit);
|
||||
|
||||
m_idEdit = new QLineEdit(this);
|
||||
m_idEdit->setPlaceholderText(tr("e.g., developer, code_reviewer"));
|
||||
formLayout->addRow(tr("ID:"), m_idEdit);
|
||||
|
||||
m_descriptionEdit = new QTextEdit(this);
|
||||
m_descriptionEdit->setPlaceholderText(tr("Brief description of this role..."));
|
||||
m_descriptionEdit->setMaximumHeight(80);
|
||||
formLayout->addRow(tr("Description:"), m_descriptionEdit);
|
||||
|
||||
mainLayout->addLayout(formLayout);
|
||||
|
||||
auto *promptLabel = new QLabel(tr("System Prompt:"), this);
|
||||
mainLayout->addWidget(promptLabel);
|
||||
|
||||
m_systemPromptEdit = new QTextEdit(this);
|
||||
m_systemPromptEdit->setPlaceholderText(
|
||||
tr("You are an expert in...\n\nYour role is to:\n- Task 1\n- Task 2\n- Task 3"));
|
||||
mainLayout->addWidget(m_systemPromptEdit);
|
||||
|
||||
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
mainLayout->addWidget(m_buttonBox);
|
||||
|
||||
connect(m_buttonBox, &QDialogButtonBox::accepted, this, &AgentRoleDialog::accept);
|
||||
connect(m_buttonBox, &QDialogButtonBox::rejected, this, &AgentRoleDialog::reject);
|
||||
connect(m_nameEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput);
|
||||
connect(m_idEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput);
|
||||
connect(m_systemPromptEdit, &QTextEdit::textChanged, this, &AgentRoleDialog::validateInput);
|
||||
|
||||
if (m_action == Action::Edit) {
|
||||
m_idEdit->setEnabled(false);
|
||||
m_idEdit->setToolTip(tr("ID cannot be changed for existing roles"));
|
||||
}
|
||||
|
||||
setMinimumSize(600, 500);
|
||||
validateInput();
|
||||
}
|
||||
|
||||
void AgentRoleDialog::validateInput()
|
||||
{
|
||||
bool valid = !m_nameEdit->text().trimmed().isEmpty()
|
||||
&& !m_idEdit->text().trimmed().isEmpty()
|
||||
&& !m_systemPromptEdit->toPlainText().trimmed().isEmpty();
|
||||
|
||||
m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid);
|
||||
}
|
||||
|
||||
AgentRole AgentRoleDialog::getRole() const
|
||||
{
|
||||
return AgentRole{
|
||||
m_idEdit->text().trimmed(),
|
||||
m_nameEdit->text().trimmed(),
|
||||
m_descriptionEdit->toPlainText().trimmed(),
|
||||
m_systemPromptEdit->toPlainText().trimmed(),
|
||||
false};
|
||||
}
|
||||
|
||||
void AgentRoleDialog::setRole(const AgentRole &role)
|
||||
{
|
||||
m_idEdit->setText(role.id);
|
||||
m_nameEdit->setText(role.name);
|
||||
m_descriptionEdit->setPlainText(role.description);
|
||||
m_systemPromptEdit->setPlainText(role.systemPrompt);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
50
sources/settings/AgentRoleDialog.hpp
Normal file
50
sources/settings/AgentRoleDialog.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
|
||||
class QLineEdit;
|
||||
class QTextEdit;
|
||||
class QDialogButtonBox;
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class AgentRoleDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Action {
|
||||
Add,
|
||||
Duplicate,
|
||||
Edit,
|
||||
};
|
||||
|
||||
explicit AgentRoleDialog(Action action, QWidget *parent = nullptr);
|
||||
explicit AgentRoleDialog(const AgentRole &role, Action action, QWidget *parent = nullptr)
|
||||
: AgentRoleDialog{action, parent}
|
||||
{
|
||||
setRole(role);
|
||||
}
|
||||
|
||||
AgentRole getRole() const;
|
||||
void setRole(const AgentRole &role);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void validateInput();
|
||||
|
||||
QLineEdit *m_nameEdit = nullptr;
|
||||
QLineEdit *m_idEdit = nullptr;
|
||||
QTextEdit *m_descriptionEdit = nullptr;
|
||||
QTextEdit *m_systemPromptEdit = nullptr;
|
||||
QDialogButtonBox *m_buttonBox = nullptr;
|
||||
Action m_action;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
242
sources/settings/AgentRolesWidget.cpp
Normal file
242
sources/settings/AgentRolesWidget.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
// 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 "AgentRolesWidget.hpp"
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
#include "AgentRoleDialog.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
void AgentRolesWidget::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
auto *headerLayout = new QHBoxLayout();
|
||||
|
||||
auto *infoLabel = new QLabel(
|
||||
Tr::tr("Agent roles define different system prompts for specific tasks."), this);
|
||||
infoLabel->setWordWrap(true);
|
||||
headerLayout->addWidget(infoLabel, 1);
|
||||
|
||||
auto *openFolderButton = new QPushButton(Tr::tr("Open Roles Folder..."), this);
|
||||
connect(openFolderButton, &QPushButton::clicked, this, &AgentRolesWidget::onOpenRolesFolder);
|
||||
headerLayout->addWidget(openFolderButton);
|
||||
|
||||
mainLayout->addLayout(headerLayout);
|
||||
|
||||
auto *contentLayout = new QHBoxLayout();
|
||||
|
||||
m_rolesList = new QListWidget(this);
|
||||
m_rolesList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
connect(m_rolesList, &QListWidget::itemSelectionChanged, this, &AgentRolesWidget::updateButtons);
|
||||
connect(m_rolesList, &QListWidget::itemDoubleClicked, this, &AgentRolesWidget::onEditRole);
|
||||
contentLayout->addWidget(m_rolesList, 1);
|
||||
|
||||
auto *buttonsLayout = new QVBoxLayout();
|
||||
|
||||
m_addButton = new QPushButton(Tr::tr("Add..."), this);
|
||||
connect(m_addButton, &QPushButton::clicked, this, &AgentRolesWidget::onAddRole);
|
||||
buttonsLayout->addWidget(m_addButton);
|
||||
|
||||
m_editButton = new QPushButton(Tr::tr("Edit..."), this);
|
||||
connect(m_editButton, &QPushButton::clicked, this, &AgentRolesWidget::onEditRole);
|
||||
buttonsLayout->addWidget(m_editButton);
|
||||
|
||||
m_duplicateButton = new QPushButton(Tr::tr("Duplicate..."), this);
|
||||
connect(m_duplicateButton, &QPushButton::clicked, this, &AgentRolesWidget::onDuplicateRole);
|
||||
buttonsLayout->addWidget(m_duplicateButton);
|
||||
|
||||
m_deleteButton = new QPushButton(Tr::tr("Delete"), this);
|
||||
connect(m_deleteButton, &QPushButton::clicked, this, &AgentRolesWidget::onDeleteRole);
|
||||
buttonsLayout->addWidget(m_deleteButton);
|
||||
|
||||
buttonsLayout->addStretch();
|
||||
|
||||
contentLayout->addLayout(buttonsLayout);
|
||||
mainLayout->addLayout(contentLayout);
|
||||
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
void AgentRolesWidget::loadRoles()
|
||||
{
|
||||
m_rolesList->clear();
|
||||
|
||||
const QList<AgentRole> roles = AgentRolesManager::loadAllRoles();
|
||||
for (const AgentRole &role : roles) {
|
||||
auto *item = new QListWidgetItem(role.name, m_rolesList);
|
||||
item->setData(Qt::UserRole, role.id);
|
||||
|
||||
QString tooltip = role.description;
|
||||
if (role.isBuiltin) {
|
||||
tooltip += "\n\n" + Tr::tr("(Built-in role)");
|
||||
item->setForeground(Qt::darkGray);
|
||||
}
|
||||
item->setToolTip(tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::updateButtons()
|
||||
{
|
||||
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||
bool hasSelection = selectedItem != nullptr;
|
||||
bool isBuiltin = false;
|
||||
|
||||
if (hasSelection) {
|
||||
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||
isBuiltin = role.isBuiltin;
|
||||
}
|
||||
|
||||
m_editButton->setEnabled(hasSelection);
|
||||
m_duplicateButton->setEnabled(hasSelection);
|
||||
m_deleteButton->setEnabled(hasSelection && !isBuiltin);
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onAddRole()
|
||||
{
|
||||
AgentRoleDialog dialog{AgentRoleDialog::Action::Add, this};
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
AgentRole newRole = dialog.getRole();
|
||||
|
||||
if (AgentRolesManager::roleExists(newRole.id)) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
Tr::tr("Role Already Exists"),
|
||||
Tr::tr("A role with ID '%1' already exists. Please use a different ID.")
|
||||
.arg(newRole.id));
|
||||
return;
|
||||
}
|
||||
|
||||
if (AgentRolesManager::saveRole(newRole)) {
|
||||
loadRoles();
|
||||
} else {
|
||||
QMessageBox::critical(
|
||||
this, Tr::tr("Error"), Tr::tr("Failed to save role '%1'.").arg(newRole.name));
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onEditRole()
|
||||
{
|
||||
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||
if (!selectedItem)
|
||||
return;
|
||||
|
||||
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||
|
||||
if (role.isBuiltin) {
|
||||
QMessageBox::information(
|
||||
this,
|
||||
Tr::tr("Cannot Edit Built-in Role"),
|
||||
Tr::tr(
|
||||
"Built-in roles cannot be edited. You can duplicate this role and modify the copy."));
|
||||
return;
|
||||
}
|
||||
|
||||
AgentRoleDialog dialog{role, AgentRoleDialog::Action::Edit, this};
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
AgentRole updatedRole = dialog.getRole();
|
||||
|
||||
if (AgentRolesManager::saveRole(updatedRole)) {
|
||||
loadRoles();
|
||||
} else {
|
||||
QMessageBox::critical(
|
||||
this, Tr::tr("Error"), Tr::tr("Failed to update role '%1'.").arg(updatedRole.name));
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onDuplicateRole()
|
||||
{
|
||||
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||
if (!selectedItem)
|
||||
return;
|
||||
|
||||
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||
|
||||
role.name += " (Copy)";
|
||||
role.id += "_copy";
|
||||
role.isBuiltin = false;
|
||||
|
||||
int counter = 1;
|
||||
QString baseId = role.id;
|
||||
while (AgentRolesManager::roleExists(role.id)) {
|
||||
role.id = baseId + QString::number(counter++);
|
||||
}
|
||||
|
||||
AgentRoleDialog dialog{role, AgentRoleDialog::Action::Duplicate, this};
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
AgentRole newRole = dialog.getRole();
|
||||
|
||||
if (AgentRolesManager::roleExists(newRole.id)) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
Tr::tr("Role Already Exists"),
|
||||
Tr::tr("A role with ID '%1' already exists. Please use a different ID.")
|
||||
.arg(newRole.id));
|
||||
return;
|
||||
}
|
||||
|
||||
if (AgentRolesManager::saveRole(newRole)) {
|
||||
loadRoles();
|
||||
} else {
|
||||
QMessageBox::critical(this, Tr::tr("Error"), Tr::tr("Failed to duplicate role."));
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onDeleteRole()
|
||||
{
|
||||
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||
if (!selectedItem)
|
||||
return;
|
||||
|
||||
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||
|
||||
if (role.isBuiltin) {
|
||||
QMessageBox::information(
|
||||
this, Tr::tr("Cannot Delete Built-in Role"), Tr::tr("Built-in roles cannot be deleted."));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||
this,
|
||||
Tr::tr("Delete Role"),
|
||||
Tr::tr("Are you sure you want to delete the role '%1'?").arg(role.name),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
if (AgentRolesManager::deleteRole(roleId)) {
|
||||
loadRoles();
|
||||
} else {
|
||||
QMessageBox::critical(
|
||||
this, Tr::tr("Error"), Tr::tr("Failed to delete role '%1'.").arg(role.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRolesWidget::onOpenRolesFolder()
|
||||
{
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(AgentRolesManager::getConfigurationDirectory()));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
43
sources/settings/AgentRolesWidget.hpp
Normal file
43
sources/settings/AgentRolesWidget.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class AgentRolesWidget : public Core::IOptionsPageWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AgentRolesWidget()
|
||||
{
|
||||
setupUI();
|
||||
loadRoles();
|
||||
}
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void loadRoles();
|
||||
void updateButtons();
|
||||
|
||||
void onAddRole();
|
||||
void onEditRole();
|
||||
void onDuplicateRole();
|
||||
void onDeleteRole();
|
||||
void onOpenRolesFolder();
|
||||
|
||||
QListWidget *m_rolesList = nullptr;
|
||||
QPushButton *m_addButton = nullptr;
|
||||
QPushButton *m_editButton = nullptr;
|
||||
QPushButton *m_duplicateButton = nullptr;
|
||||
QPushButton *m_deleteButton = nullptr;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
16
sources/settings/Assisttr.h
Normal file
16
sources/settings/Assisttr.h
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
struct Tr
|
||||
{
|
||||
Q_DECLARE_TR_FUNCTIONS(QtC::QodeAssist)
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
69
sources/settings/ButtonAspect.hpp
Normal file
69
sources/settings/ButtonAspect.hpp
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QIcon>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
|
||||
class ButtonAspect : public Utils::BaseAspect
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ButtonAspect(Utils::AspectContainer *container = nullptr)
|
||||
: Utils::BaseAspect(container)
|
||||
{}
|
||||
|
||||
void addToLayoutImpl(Layouting::Layout &parent) override
|
||||
{
|
||||
auto button = new QPushButton(m_buttonText);
|
||||
button->setVisible(m_visible);
|
||||
button->setAutoDefault(false);
|
||||
button->setDefault(false);
|
||||
button->setFocusPolicy(Qt::TabFocus);
|
||||
|
||||
QTimer::singleShot(0, button, [button] {
|
||||
button->setAutoDefault(false);
|
||||
button->setDefault(false);
|
||||
});
|
||||
|
||||
if (!m_icon.isNull()) {
|
||||
button->setIcon(m_icon);
|
||||
button->setText("");
|
||||
}
|
||||
|
||||
if (m_isCompact) {
|
||||
button->setMaximumWidth(30);
|
||||
button->setToolTip(m_tooltip.isEmpty() ? m_buttonText : m_tooltip);
|
||||
}
|
||||
|
||||
connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked);
|
||||
connect(this, &ButtonAspect::visibleChanged, button, &QPushButton::setVisible);
|
||||
parent.addItem(button);
|
||||
}
|
||||
|
||||
void updateVisibility(bool visible)
|
||||
{
|
||||
if (m_visible == visible)
|
||||
return;
|
||||
m_visible = visible;
|
||||
emit visibleChanged(visible);
|
||||
}
|
||||
|
||||
QString m_buttonText;
|
||||
QIcon m_icon;
|
||||
QString m_tooltip;
|
||||
bool m_isCompact = false;
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
void visibleChanged(bool state);
|
||||
|
||||
private:
|
||||
bool m_visible = true;
|
||||
};
|
||||
39
sources/settings/CMakeLists.txt
Normal file
39
sources/settings/CMakeLists.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
add_library(QodeAssistSettings STATIC
|
||||
GeneralSettings.hpp GeneralSettings.cpp
|
||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||
SettingsUtils.hpp
|
||||
SettingsConstants.hpp
|
||||
ButtonAspect.hpp
|
||||
SettingsTr.hpp
|
||||
CodeCompletionSettings.hpp CodeCompletionSettings.cpp
|
||||
ChatAssistantSettings.hpp ChatAssistantSettings.cpp
|
||||
QuickRefactorSettings.hpp QuickRefactorSettings.cpp
|
||||
ToolsSettings.hpp ToolsSettings.cpp
|
||||
SkillsSettings.hpp SkillsSettings.cpp
|
||||
McpSettings.hpp McpSettings.cpp
|
||||
SettingsDialog.hpp SettingsDialog.cpp
|
||||
ProjectSettings.hpp ProjectSettings.cpp
|
||||
ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp
|
||||
ProviderSettings.hpp ProviderSettings.cpp
|
||||
ProviderNameMigration.hpp
|
||||
PluginUpdater.hpp PluginUpdater.cpp
|
||||
UpdateDialog.hpp UpdateDialog.cpp
|
||||
AgentRole.hpp AgentRole.cpp
|
||||
AgentRoleDialog.hpp AgentRoleDialog.cpp
|
||||
AgentRolesWidget.hpp AgentRolesWidget.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistSettings
|
||||
PUBLIC
|
||||
Qt::Core
|
||||
Qt::Widgets
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
QodeAssistLogger
|
||||
Skills
|
||||
)
|
||||
target_include_directories(QodeAssistSettings
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PRIVATE ${CMAKE_SOURCE_DIR}/sources
|
||||
)
|
||||
414
sources/settings/ChatAssistantSettings.cpp
Normal file
414
sources/settings/ChatAssistantSettings.cpp
Normal file
@@ -0,0 +1,414 @@
|
||||
// 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 "ChatAssistantSettings.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "SettingsUtils.hpp"
|
||||
#include "AgentRolesWidget.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
ChatAssistantSettings &chatAssistantSettings()
|
||||
{
|
||||
static ChatAssistantSettings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
ChatAssistantSettings::ChatAssistantSettings()
|
||||
{
|
||||
setAutoApply(false);
|
||||
|
||||
setDisplayName(Tr::tr("Chat Assistant"));
|
||||
|
||||
linkOpenFiles.setSettingsKey(Constants::CA_LINK_OPEN_FILES);
|
||||
linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default"));
|
||||
linkOpenFiles.setDefaultValue(false);
|
||||
|
||||
autosave.setSettingsKey(Constants::CA_AUTOSAVE);
|
||||
autosave.setDefaultValue(true);
|
||||
autosave.setLabelText(Tr::tr("Enable autosave when message received"));
|
||||
|
||||
enableChatInBottomToolBar.setSettingsKey(Constants::CA_ENABLE_CHAT_IN_BOTTOM_TOOLBAR);
|
||||
enableChatInBottomToolBar.setLabelText(Tr::tr("Enable chat in bottom toolbar"));
|
||||
enableChatInBottomToolBar.setDefaultValue(false);
|
||||
|
||||
enableChatInNavigationPanel.setSettingsKey(Constants::CA_ENABLE_CHAT_IN_NAVIGATION_PANEL);
|
||||
enableChatInNavigationPanel.setLabelText(Tr::tr("Enable chat in navigation panel"));
|
||||
enableChatInNavigationPanel.setDefaultValue(false);
|
||||
|
||||
enableChatTools.setSettingsKey(Constants::CA_ENABLE_CHAT_TOOLS);
|
||||
enableChatTools.setLabelText(Tr::tr("Enable tools/function calling"));
|
||||
enableChatTools.setToolTip(Tr::tr("When enabled, AI can use tools to read files, search project, and build code"));
|
||||
enableChatTools.setDefaultValue(false);
|
||||
|
||||
autoCompress.setSettingsKey(Constants::CA_AUTO_COMPRESS);
|
||||
autoCompress.setLabelText(Tr::tr("Auto-compress chat when session tokens exceed:"));
|
||||
autoCompress.setToolTip(Tr::tr(
|
||||
"After each assistant response, if the running session token total exceeds the "
|
||||
"threshold, the chat is summarized and a new compressed chat is started "
|
||||
"automatically. The original chat is preserved on disk."));
|
||||
autoCompress.setDefaultValue(false);
|
||||
|
||||
autoCompressThreshold.setSettingsKey(Constants::CA_AUTO_COMPRESS_THRESHOLD);
|
||||
autoCompressThreshold.setRange(1000, 99999999);
|
||||
autoCompressThreshold.setDefaultValue(40000);
|
||||
|
||||
// General Parameters Settings
|
||||
temperature.setSettingsKey(Constants::CA_TEMPERATURE);
|
||||
temperature.setLabelText(Tr::tr("Temperature:"));
|
||||
temperature.setDefaultValue(0.5);
|
||||
temperature.setRange(0.0, 2.0);
|
||||
temperature.setSingleStep(0.1);
|
||||
|
||||
maxTokens.setSettingsKey(Constants::CA_MAX_TOKENS);
|
||||
maxTokens.setLabelText(Tr::tr("Max Tokens:"));
|
||||
maxTokens.setRange(-1, 200000); // -1 for unlimited, 200k max for extended output
|
||||
maxTokens.setDefaultValue(2000);
|
||||
|
||||
// Advanced Parameters
|
||||
useTopP.setSettingsKey(Constants::CA_USE_TOP_P);
|
||||
useTopP.setDefaultValue(false);
|
||||
useTopP.setLabelText(Tr::tr("Top P:"));
|
||||
|
||||
topP.setSettingsKey(Constants::CA_TOP_P);
|
||||
topP.setDefaultValue(0.9);
|
||||
topP.setRange(0.0, 1.0);
|
||||
topP.setSingleStep(0.1);
|
||||
|
||||
useTopK.setSettingsKey(Constants::CA_USE_TOP_K);
|
||||
useTopK.setDefaultValue(false);
|
||||
useTopK.setLabelText(Tr::tr("Top K:"));
|
||||
|
||||
topK.setSettingsKey(Constants::CA_TOP_K);
|
||||
topK.setDefaultValue(50);
|
||||
topK.setRange(1, 1000);
|
||||
|
||||
usePresencePenalty.setSettingsKey(Constants::CA_USE_PRESENCE_PENALTY);
|
||||
usePresencePenalty.setDefaultValue(false);
|
||||
usePresencePenalty.setLabelText(Tr::tr("Presence Penalty:"));
|
||||
|
||||
presencePenalty.setSettingsKey(Constants::CA_PRESENCE_PENALTY);
|
||||
presencePenalty.setDefaultValue(0.0);
|
||||
presencePenalty.setRange(-2.0, 2.0);
|
||||
presencePenalty.setSingleStep(0.1);
|
||||
|
||||
useFrequencyPenalty.setSettingsKey(Constants::CA_USE_FREQUENCY_PENALTY);
|
||||
useFrequencyPenalty.setDefaultValue(false);
|
||||
useFrequencyPenalty.setLabelText(Tr::tr("Frequency Penalty:"));
|
||||
|
||||
frequencyPenalty.setSettingsKey(Constants::CA_FREQUENCY_PENALTY);
|
||||
frequencyPenalty.setDefaultValue(0.0);
|
||||
frequencyPenalty.setRange(-2.0, 2.0);
|
||||
frequencyPenalty.setSingleStep(0.1);
|
||||
|
||||
// Context Settings
|
||||
useSystemPrompt.setSettingsKey(Constants::CA_USE_SYSTEM_PROMPT);
|
||||
useSystemPrompt.setDefaultValue(true);
|
||||
useSystemPrompt.setLabelText(Tr::tr("Use System Prompt"));
|
||||
|
||||
systemPrompt.setSettingsKey(Constants::CA_SYSTEM_PROMPT);
|
||||
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
systemPrompt.setDefaultValue(
|
||||
"You are an advanced AI assistant specializing in C++, Qt, and QML development. Your role "
|
||||
"is to provide helpful, accurate, and detailed responses to questions about coding, "
|
||||
"debugging, "
|
||||
"and best practices in these technologies.");
|
||||
|
||||
// Ollama Settings
|
||||
ollamaLivetime.setSettingsKey(Constants::CA_OLLAMA_LIVETIME);
|
||||
ollamaLivetime.setToolTip(
|
||||
Tr::tr("Time to suspend Ollama after completion request (in minutes), "
|
||||
"Only Ollama, -1 to disable"));
|
||||
ollamaLivetime.setLabelText("Livetime:");
|
||||
ollamaLivetime.setDefaultValue("5m");
|
||||
ollamaLivetime.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
|
||||
contextWindow.setSettingsKey(Constants::CA_OLLAMA_CONTEXT_WINDOW);
|
||||
contextWindow.setLabelText(Tr::tr("Context Window:"));
|
||||
contextWindow.setRange(-1, 10000);
|
||||
contextWindow.setDefaultValue(2048);
|
||||
|
||||
// Extended Thinking Settings
|
||||
enableThinkingMode.setSettingsKey(Constants::CA_ENABLE_THINKING_MODE);
|
||||
enableThinkingMode.setLabelText(Tr::tr("Enable extended thinking mode."));
|
||||
enableThinkingMode.setToolTip(
|
||||
Tr::tr("Enable extended thinking mode for complex reasoning tasks."
|
||||
"This provides step-by-step reasoning before the final answer."
|
||||
"Temperature is 1.0 accordingly API requirement"));
|
||||
enableThinkingMode.setDefaultValue(false);
|
||||
|
||||
thinkingBudgetTokens.setSettingsKey(Constants::CA_THINKING_BUDGET_TOKENS);
|
||||
thinkingBudgetTokens.setLabelText(Tr::tr("Thinking budget tokens:"));
|
||||
thinkingBudgetTokens.setToolTip(
|
||||
Tr::tr("Maximum number of tokens Claude can use for internal reasoning. "
|
||||
"Larger budgets improve quality but increase latency. Minimum: 1024, Recommended: 10000-16000."));
|
||||
thinkingBudgetTokens.setRange(1024, 100000);
|
||||
thinkingBudgetTokens.setDefaultValue(10000);
|
||||
|
||||
thinkingMaxTokens.setSettingsKey(Constants::CA_THINKING_MAX_TOKENS);
|
||||
thinkingMaxTokens.setLabelText(Tr::tr("Thinking mode max output tokens:"));
|
||||
thinkingMaxTokens.setToolTip(
|
||||
Tr::tr("Maximum number of tokens for the final response when thinking mode is enabled. "
|
||||
"Set to -1 to use the default max tokens setting. Recommended: 4096-16000."));
|
||||
thinkingMaxTokens.setRange(-1, 200000);
|
||||
thinkingMaxTokens.setDefaultValue(16000);
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
openAIResponsesReasoningEffort.setSettingsKey(Constants::CA_OPENAI_RESPONSES_REASONING_EFFORT);
|
||||
openAIResponsesReasoningEffort.setLabelText(Tr::tr("Reasoning effort:"));
|
||||
openAIResponsesReasoningEffort.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
openAIResponsesReasoningEffort.addOption("None");
|
||||
openAIResponsesReasoningEffort.addOption("Minimal");
|
||||
openAIResponsesReasoningEffort.addOption("Low");
|
||||
openAIResponsesReasoningEffort.addOption("Medium");
|
||||
openAIResponsesReasoningEffort.addOption("High");
|
||||
openAIResponsesReasoningEffort.setDefaultValue("Medium");
|
||||
openAIResponsesReasoningEffort.setToolTip(
|
||||
Tr::tr("Constrains effort on reasoning for OpenAI gpt-5 and o-series models:\n\n"
|
||||
"None: No reasoning (gpt-5.1 only)\n"
|
||||
"Minimal: Minimal reasoning effort (o-series only)\n"
|
||||
"Low: Low reasoning effort\n"
|
||||
"Medium: Balanced reasoning (default for most models)\n"
|
||||
"High: Maximum reasoning effort (gpt-5-pro only supports this)\n\n"
|
||||
"Note: Reducing effort = faster responses + fewer tokens"));
|
||||
|
||||
autosave.setDefaultValue(true);
|
||||
autosave.setLabelText(Tr::tr("Enable autosave when message received"));
|
||||
|
||||
textFontFamily.setSettingsKey(Constants::CA_TEXT_FONT_FAMILY);
|
||||
textFontFamily.setLabelText(Tr::tr("Text Font:"));
|
||||
textFontFamily.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
const QStringList families = QFontDatabase::families();
|
||||
for (const QString &family : families) {
|
||||
textFontFamily.addOption(family);
|
||||
}
|
||||
textFontFamily.setDefaultValue(QApplication::font().family());
|
||||
|
||||
textFontSize.setSettingsKey(Constants::CA_TEXT_FONT_SIZE);
|
||||
textFontSize.setLabelText(Tr::tr("Text Font Size:"));
|
||||
textFontSize.setDefaultValue(QApplication::font().pointSize());
|
||||
|
||||
codeFontFamily.setSettingsKey(Constants::CA_CODE_FONT_FAMILY);
|
||||
codeFontFamily.setLabelText(Tr::tr("Code Font:"));
|
||||
codeFontFamily.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
const QStringList monospaceFamilies = QFontDatabase::families(QFontDatabase::Latin);
|
||||
for (const QString &family : monospaceFamilies) {
|
||||
if (QFontDatabase::isFixedPitch(family)) {
|
||||
codeFontFamily.addOption(family);
|
||||
}
|
||||
}
|
||||
|
||||
QString defaultMonoFont;
|
||||
QStringList fixedPitchFamilies;
|
||||
|
||||
for (const QString &family : monospaceFamilies) {
|
||||
if (QFontDatabase::isFixedPitch(family)) {
|
||||
fixedPitchFamilies.append(family);
|
||||
}
|
||||
}
|
||||
|
||||
if (fixedPitchFamilies.contains("Consolas")) {
|
||||
defaultMonoFont = "Consolas";
|
||||
} else if (fixedPitchFamilies.contains("Courier New")) {
|
||||
defaultMonoFont = "Courier New";
|
||||
} else if (fixedPitchFamilies.contains("Monospace")) {
|
||||
defaultMonoFont = "Monospace";
|
||||
} else if (!fixedPitchFamilies.isEmpty()) {
|
||||
defaultMonoFont = fixedPitchFamilies.first();
|
||||
} else {
|
||||
defaultMonoFont = QApplication::font().family();
|
||||
}
|
||||
codeFontFamily.setDefaultValue(defaultMonoFont);
|
||||
codeFontSize.setSettingsKey(Constants::CA_CODE_FONT_SIZE);
|
||||
codeFontSize.setLabelText(Tr::tr("Code Font Size:"));
|
||||
codeFontSize.setDefaultValue(QApplication::font().pointSize());
|
||||
|
||||
textFormat.setSettingsKey(Constants::CA_TEXT_FORMAT);
|
||||
textFormat.setLabelText(Tr::tr("Text Format:"));
|
||||
textFormat.setDefaultValue(0);
|
||||
textFormat.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
textFormat.addOption("Markdown");
|
||||
textFormat.addOption("HTML");
|
||||
textFormat.addOption("Plain Text");
|
||||
|
||||
chatRenderer.setSettingsKey(Constants::CA_CHAT_RENDERER);
|
||||
chatRenderer.setLabelText(Tr::tr("Chat Renderer:"));
|
||||
chatRenderer.addOption("rhi");
|
||||
chatRenderer.addOption("software");
|
||||
chatRenderer.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
#ifdef Q_OS_WIN
|
||||
chatRenderer.setDefaultValue("software");
|
||||
#else
|
||||
chatRenderer.setDefaultValue("rhi");
|
||||
#endif
|
||||
|
||||
lastUsedRoleId.setSettingsKey(Constants::CA_LAST_USED_ROLE);
|
||||
lastUsedRoleId.setDefaultValue("");
|
||||
|
||||
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
||||
|
||||
readSettings();
|
||||
|
||||
setupConnections();
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
auto genGrid = Grid{};
|
||||
genGrid.addRow({Row{temperature}});
|
||||
genGrid.addRow({Row{maxTokens}});
|
||||
|
||||
auto advancedGrid = Grid{};
|
||||
advancedGrid.addRow({useTopP, topP});
|
||||
advancedGrid.addRow({useTopK, topK});
|
||||
advancedGrid.addRow({usePresencePenalty, presencePenalty});
|
||||
advancedGrid.addRow({useFrequencyPenalty, frequencyPenalty});
|
||||
|
||||
auto ollamaGrid = Grid{};
|
||||
ollamaGrid.addRow({ollamaLivetime});
|
||||
ollamaGrid.addRow({contextWindow});
|
||||
|
||||
auto thinkingGrid = Grid{};
|
||||
thinkingGrid.addRow({thinkingBudgetTokens});
|
||||
thinkingGrid.addRow({thinkingMaxTokens});
|
||||
|
||||
auto openAIResponsesGrid = Grid{};
|
||||
openAIResponsesGrid.addRow({openAIResponsesReasoningEffort});
|
||||
|
||||
auto chatViewSettingsGrid = Grid{};
|
||||
chatViewSettingsGrid.addRow({textFontFamily, textFontSize});
|
||||
chatViewSettingsGrid.addRow({codeFontFamily, codeFontSize});
|
||||
chatViewSettingsGrid.addRow({textFormat});
|
||||
chatViewSettingsGrid.addRow({chatRenderer});
|
||||
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Chat Settings")),
|
||||
Column{
|
||||
linkOpenFiles,
|
||||
autosave,
|
||||
Row{autoCompress, autoCompressThreshold, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Tools")),
|
||||
Column{enableChatTools}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Extended Thinking (Claude)")),
|
||||
Column{enableThinkingMode, Row{thinkingGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("OpenAI Responses API")),
|
||||
Column{Row{openAIResponsesGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("General Parameters")),
|
||||
Row{genGrid, Stretch{1}},
|
||||
},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Context Settings")),
|
||||
Column{
|
||||
Row{useSystemPrompt, Stretch{1}},
|
||||
systemPrompt,
|
||||
}},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Group{title(Tr::tr("Chat Settings")), Row{chatViewSettingsGrid, Stretch{1}}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
void ChatAssistantSettings::setupConnections()
|
||||
{
|
||||
connect(
|
||||
&resetToDefaults,
|
||||
&ButtonAspect::clicked,
|
||||
this,
|
||||
&ChatAssistantSettings::resetSettingsToDefaults);
|
||||
}
|
||||
|
||||
void ChatAssistantSettings::resetSettingsToDefaults()
|
||||
{
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr("Reset Settings"),
|
||||
Tr::tr("Are you sure you want to reset all settings to default values?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(autoCompress);
|
||||
resetAspect(autoCompressThreshold);
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
resetAspect(useTopP);
|
||||
resetAspect(topP);
|
||||
resetAspect(useTopK);
|
||||
resetAspect(topK);
|
||||
resetAspect(usePresencePenalty);
|
||||
resetAspect(presencePenalty);
|
||||
resetAspect(useFrequencyPenalty);
|
||||
resetAspect(frequencyPenalty);
|
||||
resetAspect(useSystemPrompt);
|
||||
resetAspect(systemPrompt);
|
||||
resetAspect(ollamaLivetime);
|
||||
resetAspect(contextWindow);
|
||||
resetAspect(enableThinkingMode);
|
||||
resetAspect(thinkingBudgetTokens);
|
||||
resetAspect(thinkingMaxTokens);
|
||||
resetAspect(openAIResponsesReasoningEffort);
|
||||
resetAspect(linkOpenFiles);
|
||||
resetAspect(enableChatTools);
|
||||
resetAspect(textFontFamily);
|
||||
resetAspect(codeFontFamily);
|
||||
resetAspect(textFontSize);
|
||||
resetAspect(codeFontSize);
|
||||
resetAspect(textFormat);
|
||||
resetAspect(chatRenderer);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
class ChatAssistantSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
ChatAssistantSettingsPage()
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID);
|
||||
setDisplayName(Tr::tr("Chat Assistant"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setSettingsProvider([] { return &chatAssistantSettings(); });
|
||||
}
|
||||
};
|
||||
|
||||
const ChatAssistantSettingsPage chatAssistantSettingsPage;
|
||||
|
||||
class AgentRolesSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
AgentRolesSettingsPage()
|
||||
{
|
||||
setId("QodeAssist.AgentRoles");
|
||||
setDisplayName(Tr::tr("Agent Roles"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setWidgetCreator([]() { return new AgentRolesWidget(); });
|
||||
}
|
||||
};
|
||||
|
||||
const AgentRolesSettingsPage agentRolesSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
81
sources/settings/ChatAssistantSettings.hpp
Normal file
81
sources/settings/ChatAssistantSettings.hpp
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
#include "ButtonAspect.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class ChatAssistantSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
ChatAssistantSettings();
|
||||
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
// Chat settings
|
||||
Utils::BoolAspect linkOpenFiles{this};
|
||||
Utils::BoolAspect autosave{this};
|
||||
Utils::BoolAspect enableChatInBottomToolBar{this};
|
||||
Utils::BoolAspect enableChatInNavigationPanel{this};
|
||||
Utils::BoolAspect enableChatTools{this};
|
||||
Utils::BoolAspect autoCompress{this};
|
||||
Utils::IntegerAspect autoCompressThreshold{this};
|
||||
|
||||
// General Parameters Settings
|
||||
Utils::DoubleAspect temperature{this};
|
||||
Utils::IntegerAspect maxTokens{this};
|
||||
|
||||
// Advanced Parameters
|
||||
Utils::BoolAspect useTopP{this};
|
||||
Utils::DoubleAspect topP{this};
|
||||
|
||||
Utils::BoolAspect useTopK{this};
|
||||
Utils::IntegerAspect topK{this};
|
||||
|
||||
Utils::BoolAspect usePresencePenalty{this};
|
||||
Utils::DoubleAspect presencePenalty{this};
|
||||
|
||||
Utils::BoolAspect useFrequencyPenalty{this};
|
||||
Utils::DoubleAspect frequencyPenalty{this};
|
||||
|
||||
// Context Settings
|
||||
Utils::BoolAspect useSystemPrompt{this};
|
||||
Utils::StringAspect systemPrompt{this};
|
||||
|
||||
// Ollama Settings
|
||||
Utils::StringAspect ollamaLivetime{this};
|
||||
Utils::IntegerAspect contextWindow{this};
|
||||
|
||||
// Extended Thinking Settings (Claude only)
|
||||
Utils::BoolAspect enableThinkingMode{this};
|
||||
Utils::IntegerAspect thinkingBudgetTokens{this};
|
||||
Utils::IntegerAspect thinkingMaxTokens{this};
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
Utils::SelectionAspect openAIResponsesReasoningEffort{this};
|
||||
|
||||
// Visuals settings
|
||||
Utils::SelectionAspect textFontFamily{this};
|
||||
Utils::IntegerAspect textFontSize{this};
|
||||
Utils::SelectionAspect codeFontFamily{this};
|
||||
Utils::IntegerAspect codeFontSize{this};
|
||||
Utils::SelectionAspect textFormat{this};
|
||||
|
||||
Utils::SelectionAspect chatRenderer{this};
|
||||
|
||||
Utils::StringAspect lastUsedRoleId{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
};
|
||||
|
||||
ChatAssistantSettings &chatAssistantSettings();
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
552
sources/settings/CodeCompletionSettings.cpp
Normal file
552
sources/settings/CodeCompletionSettings.cpp
Normal file
@@ -0,0 +1,552 @@
|
||||
// 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 "CodeCompletionSettings.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "SettingsUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
CodeCompletionSettings &codeCompletionSettings()
|
||||
{
|
||||
static CodeCompletionSettings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
CodeCompletionSettings::CodeCompletionSettings()
|
||||
{
|
||||
setAutoApply(false);
|
||||
|
||||
setDisplayName(Tr::tr("Code Completion"));
|
||||
|
||||
// Auto Completion Settings
|
||||
autoCompletion.setSettingsKey(Constants::CC_AUTO_COMPLETION);
|
||||
autoCompletion.setLabelText(Tr::tr("Enable Auto Complete"));
|
||||
autoCompletion.setDefaultValue(true);
|
||||
|
||||
multiLineCompletion.setSettingsKey(Constants::CC_MULTILINE_COMPLETION);
|
||||
multiLineCompletion.setDefaultValue(true);
|
||||
multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion"));
|
||||
|
||||
modelOutputHandler.setLabelText(Tr::tr("Text output proccessing mode:"));
|
||||
modelOutputHandler.setSettingsKey(Constants::CC_MODEL_OUTPUT_HANDLER);
|
||||
modelOutputHandler.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
modelOutputHandler.addOption("Auto");
|
||||
modelOutputHandler.addOption("Force processing");
|
||||
modelOutputHandler.addOption("Raw text");
|
||||
modelOutputHandler.setDefaultValue("Auto");
|
||||
modelOutputHandler.setToolTip(
|
||||
Tr::tr("Auto: Automatically detects codeblock and applies processing when found, other "
|
||||
"text as comments\n"
|
||||
"Force Processing: Always processes text with codeblock formatting and other text "
|
||||
"as comments\n"
|
||||
"Raw Text: Shows unprocessed text without any formatting"));
|
||||
|
||||
completionTriggerMode.setLabelText(Tr::tr("Completion trigger mode:"));
|
||||
completionTriggerMode.setSettingsKey(Constants::CC_COMPLETION_TRIGGER_MODE);
|
||||
completionTriggerMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
completionTriggerMode.addOption("Hint-based (Tab to trigger)");
|
||||
completionTriggerMode.addOption("Automatic");
|
||||
completionTriggerMode.setDefaultValue("Automatic");
|
||||
completionTriggerMode.setToolTip(
|
||||
Tr::tr("Hint-based: Shows a hint when typing, press Tab to request completion\n"
|
||||
"Automatic: Automatically requests completion after typing threshold"));
|
||||
|
||||
completionMode.setLabelText(Tr::tr("Completion mode:"));
|
||||
completionMode.setSettingsKey(Constants::CC_COMPLETION_MODE);
|
||||
completionMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
completionMode.addOption("Automatic");
|
||||
completionMode.addOption("Manual");
|
||||
completionMode.setDefaultValue("Automatic");
|
||||
completionMode.setToolTip(
|
||||
Tr::tr("Automatic: requests completion while typing (with smart context gates).\n"
|
||||
"Manual: no auto-triggering; invoke via the 'Request QodeAssist Suggestion' "
|
||||
"shortcut (default Ctrl+Alt+Q, reconfigurable in Preferences > Keyboard)."));
|
||||
|
||||
smartContextTrigger.setSettingsKey(Constants::CC_SMART_CONTEXT_TRIGGER);
|
||||
smartContextTrigger.setLabelText(Tr::tr("Smart context-aware triggering"));
|
||||
smartContextTrigger.setDefaultValue(true);
|
||||
smartContextTrigger.setToolTip(
|
||||
Tr::tr("When enabled, auto-completion is suppressed in places where Qt Creator's built-in "
|
||||
"completion is usually stronger (middle of an identifier, right after '.', '->', "
|
||||
"'::') and is triggered more eagerly after structural characters like '(', ',', "
|
||||
"'{', '=' and on fresh indented lines."));
|
||||
|
||||
respectQtcPopup.setSettingsKey(Constants::CC_RESPECT_QTC_POPUP);
|
||||
respectQtcPopup.setLabelText(Tr::tr("Don't dismiss Qt Creator's completion popup"));
|
||||
respectQtcPopup.setDefaultValue(true);
|
||||
respectQtcPopup.setToolTip(
|
||||
Tr::tr("When enabled, an AI completion arriving while Qt Creator's own completion popup "
|
||||
"is already visible will not force it closed. The LLM suggestion still appears "
|
||||
"inline."));
|
||||
|
||||
cancelOnInput.setSettingsKey(Constants::CC_CANCEL_ON_INPUT);
|
||||
cancelOnInput.setLabelText(Tr::tr("Cancel in-flight request on new input"));
|
||||
cancelOnInput.setDefaultValue(false);
|
||||
cancelOnInput.setToolTip(
|
||||
Tr::tr("When enabled, every new keystroke cancels any completion request already in "
|
||||
"flight and restarts the debounce timer. Useful for slow local models where an "
|
||||
"outdated answer is rarely worth waiting for.\n"
|
||||
"When disabled (default), the in-flight request is kept; when the answer arrives, "
|
||||
"the plugin compares it with characters typed in the meantime and either trims "
|
||||
"the matching prefix or drops the answer."));
|
||||
|
||||
startSuggestionTimer.setSettingsKey(Constants::СС_START_SUGGESTION_TIMER);
|
||||
startSuggestionTimer.setLabelText(Tr::tr("with delay(ms)"));
|
||||
startSuggestionTimer.setToolTip(
|
||||
Tr::tr("Delay before sending the completion request.\n"
|
||||
"(Only for Automatic trigger mode)"));
|
||||
startSuggestionTimer.setRange(10, 10000);
|
||||
startSuggestionTimer.setDefaultValue(350);
|
||||
|
||||
autoCompletionCharThreshold.setSettingsKey(Constants::СС_AUTO_COMPLETION_CHAR_THRESHOLD);
|
||||
autoCompletionCharThreshold.setLabelText(Tr::tr("AI suggestion triggers after typing"));
|
||||
autoCompletionCharThreshold.setToolTip(
|
||||
Tr::tr("The number of characters that need to be typed within the typing interval "
|
||||
"before an AI suggestion request is sent automatically.\n"
|
||||
"(Only for Automatic trigger mode)"));
|
||||
autoCompletionCharThreshold.setRange(0, 10);
|
||||
autoCompletionCharThreshold.setDefaultValue(1);
|
||||
|
||||
autoCompletionTypingInterval.setSettingsKey(Constants::СС_AUTO_COMPLETION_TYPING_INTERVAL);
|
||||
autoCompletionTypingInterval.setLabelText(Tr::tr("character(s) within(ms)"));
|
||||
autoCompletionTypingInterval.setToolTip(
|
||||
Tr::tr("The time window (in milliseconds) during which the character threshold "
|
||||
"must be met to trigger an AI suggestion request automatically.\n"
|
||||
"(Only for Automatic trigger mode)"));
|
||||
autoCompletionTypingInterval.setRange(500, 5000);
|
||||
autoCompletionTypingInterval.setDefaultValue(1200);
|
||||
|
||||
hintCharThreshold.setSettingsKey(Constants::CC_HINT_CHAR_THRESHOLD);
|
||||
hintCharThreshold.setLabelText(Tr::tr("Hint shows after typing"));
|
||||
hintCharThreshold.setToolTip(
|
||||
Tr::tr("The number of characters that need to be typed before the hint widget appears "
|
||||
"(only for Hint-based trigger mode)."));
|
||||
hintCharThreshold.setRange(1, 10);
|
||||
hintCharThreshold.setDefaultValue(3);
|
||||
|
||||
hintHideTimeout.setSettingsKey(Constants::CC_HINT_HIDE_TIMEOUT);
|
||||
hintHideTimeout.setLabelText(Tr::tr("Hint auto-hide timeout (ms)"));
|
||||
hintHideTimeout.setToolTip(
|
||||
Tr::tr("Time in milliseconds after which the hint widget will automatically hide "
|
||||
"(only for Hint-based trigger mode)."));
|
||||
hintHideTimeout.setRange(500, 10000);
|
||||
hintHideTimeout.setDefaultValue(4000);
|
||||
|
||||
hintTriggerKey.setLabelText(Tr::tr("Trigger key:"));
|
||||
hintTriggerKey.setSettingsKey(Constants::CC_HINT_TRIGGER_KEY);
|
||||
hintTriggerKey.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
hintTriggerKey.addOption("Space");
|
||||
hintTriggerKey.addOption("Ctrl+Space");
|
||||
hintTriggerKey.addOption("Alt+Space");
|
||||
hintTriggerKey.addOption("Ctrl+Enter");
|
||||
hintTriggerKey.addOption("Tab");
|
||||
hintTriggerKey.addOption("Enter");
|
||||
hintTriggerKey.setDefaultValue("Tab");
|
||||
hintTriggerKey.setToolTip(
|
||||
Tr::tr("Key to press for requesting completion when hint is visible.\n"
|
||||
"Space is recommended as least conflicting with context menu.\n"
|
||||
"(Only for Hint-based trigger mode)"));
|
||||
|
||||
ignoreWhitespaceInCharCount.setSettingsKey(Constants::CC_IGNORE_WHITESPACE_IN_CHAR_COUNT);
|
||||
ignoreWhitespaceInCharCount.setLabelText(
|
||||
Tr::tr("Ignore spaces and tabs in character count"));
|
||||
ignoreWhitespaceInCharCount.setDefaultValue(true);
|
||||
ignoreWhitespaceInCharCount.setToolTip(
|
||||
Tr::tr("When enabled, spaces and tabs are not counted towards the character threshold "
|
||||
"for triggering completions. This helps trigger completions based on actual code "
|
||||
"characters only."));
|
||||
|
||||
// General Parameters Settings
|
||||
temperature.setSettingsKey(Constants::CC_TEMPERATURE);
|
||||
temperature.setLabelText(Tr::tr("Temperature:"));
|
||||
temperature.setDefaultValue(0.2);
|
||||
temperature.setRange(0.0, 2.0);
|
||||
temperature.setSingleStep(0.1);
|
||||
|
||||
maxTokens.setSettingsKey(Constants::CC_MAX_TOKENS);
|
||||
maxTokens.setLabelText(Tr::tr("Max Tokens:"));
|
||||
maxTokens.setRange(-1, 900000);
|
||||
maxTokens.setDefaultValue(500);
|
||||
|
||||
// Advanced Parameters
|
||||
useTopP.setSettingsKey(Constants::CC_USE_TOP_P);
|
||||
useTopP.setDefaultValue(false);
|
||||
useTopP.setLabelText(Tr::tr("Top P:"));
|
||||
|
||||
topP.setSettingsKey(Constants::CC_TOP_P);
|
||||
topP.setDefaultValue(0.9);
|
||||
topP.setRange(0.0, 1.0);
|
||||
topP.setSingleStep(0.1);
|
||||
|
||||
useTopK.setSettingsKey(Constants::CC_USE_TOP_K);
|
||||
useTopK.setDefaultValue(false);
|
||||
useTopK.setLabelText(Tr::tr("Top K:"));
|
||||
|
||||
topK.setSettingsKey(Constants::CC_TOP_K);
|
||||
topK.setDefaultValue(50);
|
||||
topK.setRange(1, 1000);
|
||||
|
||||
usePresencePenalty.setSettingsKey(Constants::CC_USE_PRESENCE_PENALTY);
|
||||
usePresencePenalty.setDefaultValue(false);
|
||||
usePresencePenalty.setLabelText(Tr::tr("Presence Penalty:"));
|
||||
|
||||
presencePenalty.setSettingsKey(Constants::CC_PRESENCE_PENALTY);
|
||||
presencePenalty.setDefaultValue(0.0);
|
||||
presencePenalty.setRange(-2.0, 2.0);
|
||||
presencePenalty.setSingleStep(0.1);
|
||||
|
||||
useFrequencyPenalty.setSettingsKey(Constants::CC_USE_FREQUENCY_PENALTY);
|
||||
useFrequencyPenalty.setDefaultValue(false);
|
||||
useFrequencyPenalty.setLabelText(Tr::tr("Frequency Penalty:"));
|
||||
|
||||
frequencyPenalty.setSettingsKey(Constants::CC_FREQUENCY_PENALTY);
|
||||
frequencyPenalty.setDefaultValue(0.0);
|
||||
frequencyPenalty.setRange(-2.0, 2.0);
|
||||
frequencyPenalty.setSingleStep(0.1);
|
||||
|
||||
// Context Settings
|
||||
readFullFile.setSettingsKey(Constants::CC_READ_FULL_FILE);
|
||||
readFullFile.setLabelText(Tr::tr("Read Full File"));
|
||||
readFullFile.setDefaultValue(false);
|
||||
|
||||
readFileParts.setLabelText(Tr::tr("Read Strings Before Cursor:"));
|
||||
readFileParts.setDefaultValue(true);
|
||||
|
||||
readStringsBeforeCursor.setSettingsKey(Constants::CC_READ_STRINGS_BEFORE_CURSOR);
|
||||
readStringsBeforeCursor.setRange(0, 10000);
|
||||
readStringsBeforeCursor.setDefaultValue(50);
|
||||
|
||||
readStringsAfterCursor.setSettingsKey(Constants::CC_READ_STRINGS_AFTER_CURSOR);
|
||||
readStringsAfterCursor.setLabelText(Tr::tr("Read Strings After Cursor:"));
|
||||
readStringsAfterCursor.setRange(0, 10000);
|
||||
readStringsAfterCursor.setDefaultValue(30);
|
||||
|
||||
useSystemPrompt.setSettingsKey(Constants::CC_USE_SYSTEM_PROMPT);
|
||||
useSystemPrompt.setDefaultValue(true);
|
||||
useSystemPrompt.setLabelText(Tr::tr("Use System Prompt"));
|
||||
|
||||
systemPrompt.setSettingsKey(Constants::CC_SYSTEM_PROMPT);
|
||||
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
systemPrompt.setDefaultValue(
|
||||
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
|
||||
"precise and contextually appropriate code completions.\n\n");
|
||||
|
||||
useUserMessageTemplateForCC.setSettingsKey(Constants::CC_USE_USER_TEMPLATE);
|
||||
useUserMessageTemplateForCC.setDefaultValue(true);
|
||||
useUserMessageTemplateForCC.setLabelText(
|
||||
Tr::tr("Use special system prompt and user message for non FIM models"));
|
||||
|
||||
systemPromptForNonFimModels.setSettingsKey(Constants::CC_SYSTEM_PROMPT_FOR_NON_FIM);
|
||||
systemPromptForNonFimModels.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
systemPromptForNonFimModels.setLabelText(Tr::tr("System prompt for non FIM models:"));
|
||||
systemPromptForNonFimModels.setDefaultValue(
|
||||
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
|
||||
"precise and contextually appropriate code completions.\n\n"
|
||||
"Core Requirements:\n"
|
||||
"1. Continue code exactly from the cursor position, ensuring it properly connects with any "
|
||||
"existing code after the cursor\n"
|
||||
"2. Never repeat existing code before or after the cursor\n"
|
||||
"Specific Guidelines:\n"
|
||||
"- For function calls: Complete parameters with appropriate types and names\n"
|
||||
"- For class members: Respect access modifiers and class conventions\n"
|
||||
"- Respect existing indentation and formatting\n"
|
||||
"- Consider scope and visibility of referenced symbols\n"
|
||||
"- Ensure seamless integration with code both before and after the cursor\n\n"
|
||||
"Context Format:\n"
|
||||
"<code_context>\n"
|
||||
"{{code before cursor}}<cursor>{{code after cursor}}\n"
|
||||
"</code_context>\n\n"
|
||||
"Response Format:\n"
|
||||
"- No explanations or comments\n"
|
||||
"- Only include new characters needed to create valid code\n"
|
||||
"- Should be codeblock with language\n");
|
||||
|
||||
userMessageTemplateForCC.setSettingsKey(Constants::CC_USER_TEMPLATE);
|
||||
userMessageTemplateForCC.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
userMessageTemplateForCC.setLabelText(Tr::tr("User message for non FIM models:"));
|
||||
userMessageTemplateForCC.setDefaultValue(
|
||||
"Here is the code context with insertion points:\n"
|
||||
"<code_context>\n${prefix}<cursor>${suffix}\n</code_context>\n\n");
|
||||
|
||||
customLanguages.setSettingsKey(Constants::CC_CUSTOM_LANGUAGES);
|
||||
customLanguages.setLabelText(
|
||||
Tr::tr("Additional Programming Languages for handling: Example: rust,//,rust rs,rs"));
|
||||
customLanguages.setToolTip(Tr::tr("Specify additional programming languages in format: "
|
||||
"name,comment_style,model_names,extensions\n"
|
||||
"Example: rust,//,rust rs,rs\n"
|
||||
"Fields: language name, comment prefix, names from LLM "
|
||||
"(space-separated), file extensions (space-separated)"));
|
||||
customLanguages.setDefaultValue({{"cmake,#,cmake,CMakeLists.txt"}, {"qmake,#,qmake,pro pri"}});
|
||||
|
||||
showProgressWidget.setSettingsKey(Constants::CC_SHOW_PROGRESS_WIDGET);
|
||||
showProgressWidget.setLabelText(Tr::tr("Show progress indicator during code completion"));
|
||||
showProgressWidget.setDefaultValue(true);
|
||||
|
||||
abortAssistOnRequest.setSettingsKey(Constants::CC_ABORT_ASSIST_ON_REQUEST);
|
||||
abortAssistOnRequest.setLabelText(Tr::tr("Abort existing assist on new completion request"));
|
||||
abortAssistOnRequest.setToolTip(
|
||||
Tr::tr("When enabled, cancels any active Qt Creator code assist popup "
|
||||
"before requesting LLM completion.\n"
|
||||
"(Only for Automatic trigger mode)"));
|
||||
abortAssistOnRequest.setDefaultValue(true);
|
||||
|
||||
useOpenFilesContext.setSettingsKey(Constants::CC_USE_OPEN_FILES_CONTEXT);
|
||||
useOpenFilesContext.setLabelText(Tr::tr("Include context from open files"));
|
||||
useOpenFilesContext.setDefaultValue(false);
|
||||
|
||||
useProjectChangesCache.setSettingsKey(Constants::CC_USE_PROJECT_CHANGES_CACHE);
|
||||
useProjectChangesCache.setDefaultValue(true);
|
||||
useProjectChangesCache.setLabelText(Tr::tr("Max Changes Cache Size:"));
|
||||
|
||||
maxChangesCacheSize.setSettingsKey(Constants::CC_MAX_CHANGES_CACHE_SIZE);
|
||||
maxChangesCacheSize.setRange(2, 1000);
|
||||
maxChangesCacheSize.setDefaultValue(10);
|
||||
|
||||
// Ollama Settings
|
||||
ollamaLivetime.setSettingsKey(Constants::CC_OLLAMA_LIVETIME);
|
||||
ollamaLivetime.setToolTip(
|
||||
Tr::tr("Time to suspend Ollama after completion request (in minutes), "
|
||||
"Only Ollama, -1 to disable"));
|
||||
ollamaLivetime.setLabelText("Livetime:");
|
||||
ollamaLivetime.setDefaultValue("5m");
|
||||
ollamaLivetime.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
|
||||
contextWindow.setSettingsKey(Constants::CC_OLLAMA_CONTEXT_WINDOW);
|
||||
contextWindow.setLabelText(Tr::tr("Context Window:"));
|
||||
contextWindow.setRange(-1, 10000);
|
||||
contextWindow.setDefaultValue(2048);
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
openAIResponsesReasoningEffort.setSettingsKey(Constants::CC_OPENAI_RESPONSES_REASONING_EFFORT);
|
||||
openAIResponsesReasoningEffort.setLabelText(Tr::tr("Reasoning effort:"));
|
||||
openAIResponsesReasoningEffort.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
openAIResponsesReasoningEffort.addOption("None");
|
||||
openAIResponsesReasoningEffort.addOption("Minimal");
|
||||
openAIResponsesReasoningEffort.addOption("Low");
|
||||
openAIResponsesReasoningEffort.addOption("Medium");
|
||||
openAIResponsesReasoningEffort.addOption("High");
|
||||
openAIResponsesReasoningEffort.setDefaultValue("Medium");
|
||||
openAIResponsesReasoningEffort.setToolTip(
|
||||
Tr::tr("Constrains effort on reasoning for OpenAI gpt-5 and o-series models:\n\n"
|
||||
"None: No reasoning (gpt-5.1 only)\n"
|
||||
"Minimal: Minimal reasoning effort (o-series only)\n"
|
||||
"Low: Low reasoning effort\n"
|
||||
"Medium: Balanced reasoning (default for most models)\n"
|
||||
"High: Maximum reasoning effort (gpt-5-pro only supports this)\n\n"
|
||||
"Note: Reducing effort = faster responses + fewer tokens"));
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
|
||||
readSettings();
|
||||
|
||||
migrateCompletionMode();
|
||||
|
||||
readFileParts.setValue(!readFullFile.value());
|
||||
|
||||
setupConnections();
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
auto genGrid = Grid{};
|
||||
genGrid.addRow({Row{temperature}});
|
||||
genGrid.addRow({Row{maxTokens}});
|
||||
|
||||
auto advancedGrid = Grid{};
|
||||
advancedGrid.addRow({useTopP, topP});
|
||||
advancedGrid.addRow({useTopK, topK});
|
||||
advancedGrid.addRow({usePresencePenalty, presencePenalty});
|
||||
advancedGrid.addRow({useFrequencyPenalty, frequencyPenalty});
|
||||
|
||||
auto ollamaGrid = Grid{};
|
||||
ollamaGrid.addRow({ollamaLivetime});
|
||||
ollamaGrid.addRow({contextWindow});
|
||||
|
||||
auto openAIResponsesGrid = Grid{};
|
||||
openAIResponsesGrid.addRow({openAIResponsesReasoningEffort});
|
||||
|
||||
auto contextGrid = Grid{};
|
||||
contextGrid.addRow({Row{readFullFile}});
|
||||
contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}});
|
||||
|
||||
auto contextItem = Column{
|
||||
Row{contextGrid, Stretch{1}},
|
||||
Row{useSystemPrompt, Stretch{1}},
|
||||
Group{title(Tr::tr("Prompts for FIM models")), Column{systemPrompt}},
|
||||
Group{
|
||||
title(Tr::tr("Prompts for Non FIM models")),
|
||||
Column{
|
||||
Row{useUserMessageTemplateForCC, Stretch{1}},
|
||||
systemPromptForNonFimModels,
|
||||
userMessageTemplateForCC,
|
||||
customLanguages,
|
||||
}},
|
||||
Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}};
|
||||
|
||||
auto generalSettings = Column{
|
||||
autoCompletion,
|
||||
multiLineCompletion,
|
||||
Row{modelOutputHandler, Stretch{1}},
|
||||
Row{completionMode, Stretch{1}},
|
||||
showProgressWidget,
|
||||
useOpenFilesContext,
|
||||
respectQtcPopup,
|
||||
cancelOnInput,
|
||||
abortAssistOnRequest,
|
||||
ignoreWhitespaceInCharCount};
|
||||
|
||||
auto autoTriggerSettings = Column{
|
||||
smartContextTrigger,
|
||||
Row{autoCompletionCharThreshold,
|
||||
autoCompletionTypingInterval,
|
||||
startSuggestionTimer,
|
||||
Stretch{1}}};
|
||||
|
||||
return Column{Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{title(TrConstants::AUTO_COMPLETION_SETTINGS),
|
||||
Column{Group{title(Tr::tr("General Settings")), generalSettings},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Automatic Trigger Mode")), autoTriggerSettings}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("General Parameters")),
|
||||
Column{
|
||||
Row{genGrid, Stretch{1}},
|
||||
}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")),
|
||||
Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")), contextItem},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("OpenAI Responses API")), Column{Row{openAIResponsesGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
void CodeCompletionSettings::setupConnections()
|
||||
{
|
||||
connect(
|
||||
&resetToDefaults,
|
||||
&ButtonAspect::clicked,
|
||||
this,
|
||||
&CodeCompletionSettings::resetSettingsToDefaults);
|
||||
|
||||
connect(&readFullFile, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
if (readFullFile.volatileValue()) {
|
||||
readFileParts.setValue(false);
|
||||
writeSettings();
|
||||
}
|
||||
});
|
||||
|
||||
connect(&readFileParts, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
if (readFileParts.volatileValue()) {
|
||||
readFullFile.setValue(false);
|
||||
writeSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CodeCompletionSettings::resetSettingsToDefaults()
|
||||
{
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr("Reset Settings"),
|
||||
Tr::tr("Are you sure you want to reset all settings to default values?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(autoCompletion);
|
||||
resetAspect(multiLineCompletion);
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
resetAspect(useTopP);
|
||||
resetAspect(topP);
|
||||
resetAspect(useTopK);
|
||||
resetAspect(topK);
|
||||
resetAspect(usePresencePenalty);
|
||||
resetAspect(presencePenalty);
|
||||
resetAspect(useFrequencyPenalty);
|
||||
resetAspect(frequencyPenalty);
|
||||
resetAspect(readFullFile);
|
||||
resetAspect(readFileParts);
|
||||
resetAspect(readStringsBeforeCursor);
|
||||
resetAspect(readStringsAfterCursor);
|
||||
resetAspect(useSystemPrompt);
|
||||
resetAspect(systemPrompt);
|
||||
resetAspect(useProjectChangesCache);
|
||||
resetAspect(maxChangesCacheSize);
|
||||
resetAspect(ollamaLivetime);
|
||||
resetAspect(contextWindow);
|
||||
resetAspect(openAIResponsesReasoningEffort);
|
||||
resetAspect(useUserMessageTemplateForCC);
|
||||
resetAspect(userMessageTemplateForCC);
|
||||
resetAspect(systemPromptForNonFimModels);
|
||||
resetAspect(customLanguages);
|
||||
resetAspect(showProgressWidget);
|
||||
resetAspect(useOpenFilesContext);
|
||||
resetAspect(modelOutputHandler);
|
||||
resetAspect(completionTriggerMode);
|
||||
resetAspect(completionMode);
|
||||
resetAspect(smartContextTrigger);
|
||||
resetAspect(respectQtcPopup);
|
||||
resetAspect(cancelOnInput);
|
||||
resetAspect(hintCharThreshold);
|
||||
resetAspect(hintHideTimeout);
|
||||
resetAspect(hintTriggerKey);
|
||||
resetAspect(ignoreWhitespaceInCharCount);
|
||||
resetAspect(abortAssistOnRequest);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void CodeCompletionSettings::migrateCompletionMode()
|
||||
{
|
||||
auto *qtcSettings = Core::ICore::settings();
|
||||
if (!qtcSettings || qtcSettings->contains(Constants::CC_COMPLETION_MODE))
|
||||
return;
|
||||
|
||||
const QString oldMode = completionTriggerMode.stringValue();
|
||||
if (oldMode.startsWith("Hint"))
|
||||
completionMode.setStringValue("Manual");
|
||||
else
|
||||
completionMode.setStringValue("Automatic");
|
||||
|
||||
writeSettings();
|
||||
}
|
||||
|
||||
QString CodeCompletionSettings::processMessageToFIM(const QString &prefix, const QString &suffix) const
|
||||
{
|
||||
QString result = userMessageTemplateForCC();
|
||||
result.replace("${prefix}", prefix);
|
||||
result.replace("${suffix}", suffix);
|
||||
return result;
|
||||
}
|
||||
|
||||
class CodeCompletionSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
CodeCompletionSettingsPage()
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_CODE_COMPLETION_SETTINGS_PAGE_ID);
|
||||
setDisplayName(Tr::tr("Code Completion"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setSettingsProvider([] { return &codeCompletionSettings(); });
|
||||
}
|
||||
};
|
||||
|
||||
const CodeCompletionSettingsPage codeCompletionSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
91
sources/settings/CodeCompletionSettings.hpp
Normal file
91
sources/settings/CodeCompletionSettings.hpp
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ButtonAspect.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class CodeCompletionSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
CodeCompletionSettings();
|
||||
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
// Auto Completion Settings
|
||||
Utils::BoolAspect autoCompletion{this};
|
||||
Utils::BoolAspect multiLineCompletion{this};
|
||||
Utils::SelectionAspect modelOutputHandler{this};
|
||||
Utils::SelectionAspect completionTriggerMode{this};
|
||||
Utils::SelectionAspect completionMode{this};
|
||||
Utils::BoolAspect smartContextTrigger{this};
|
||||
Utils::BoolAspect respectQtcPopup{this};
|
||||
Utils::BoolAspect cancelOnInput{this};
|
||||
|
||||
Utils::IntegerAspect startSuggestionTimer{this};
|
||||
Utils::IntegerAspect autoCompletionCharThreshold{this};
|
||||
Utils::IntegerAspect autoCompletionTypingInterval{this};
|
||||
Utils::IntegerAspect hintCharThreshold{this};
|
||||
Utils::IntegerAspect hintHideTimeout{this};
|
||||
Utils::SelectionAspect hintTriggerKey{this};
|
||||
Utils::BoolAspect ignoreWhitespaceInCharCount{this};
|
||||
|
||||
Utils::StringListAspect customLanguages{this};
|
||||
|
||||
Utils::BoolAspect showProgressWidget{this};
|
||||
Utils::BoolAspect abortAssistOnRequest{this};
|
||||
Utils::BoolAspect useOpenFilesContext{this};
|
||||
|
||||
// General Parameters Settings
|
||||
Utils::DoubleAspect temperature{this};
|
||||
Utils::IntegerAspect maxTokens{this};
|
||||
|
||||
// Advanced Parameters
|
||||
Utils::BoolAspect useTopP{this};
|
||||
Utils::DoubleAspect topP{this};
|
||||
|
||||
Utils::BoolAspect useTopK{this};
|
||||
Utils::IntegerAspect topK{this};
|
||||
|
||||
Utils::BoolAspect usePresencePenalty{this};
|
||||
Utils::DoubleAspect presencePenalty{this};
|
||||
|
||||
Utils::BoolAspect useFrequencyPenalty{this};
|
||||
Utils::DoubleAspect frequencyPenalty{this};
|
||||
|
||||
// Context Settings
|
||||
Utils::BoolAspect readFullFile{this};
|
||||
Utils::BoolAspect readFileParts{this};
|
||||
Utils::IntegerAspect readStringsBeforeCursor{this};
|
||||
Utils::IntegerAspect readStringsAfterCursor{this};
|
||||
Utils::BoolAspect useSystemPrompt{this};
|
||||
Utils::StringAspect systemPrompt{this};
|
||||
Utils::BoolAspect useUserMessageTemplateForCC{this};
|
||||
Utils::StringAspect systemPromptForNonFimModels{this};
|
||||
Utils::StringAspect userMessageTemplateForCC{this};
|
||||
Utils::BoolAspect useProjectChangesCache{this};
|
||||
Utils::IntegerAspect maxChangesCacheSize{this};
|
||||
|
||||
// Ollama Settings
|
||||
Utils::StringAspect ollamaLivetime{this};
|
||||
Utils::IntegerAspect contextWindow{this};
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
Utils::SelectionAspect openAIResponsesReasoningEffort{this};
|
||||
|
||||
QString processMessageToFIM(const QString &prefix, const QString &suffix) const;
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
void migrateCompletionMode();
|
||||
};
|
||||
|
||||
CodeCompletionSettings &codeCompletionSettings();
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
373
sources/settings/ConfigurationManager.cpp
Normal file
373
sources/settings/ConfigurationManager.cpp
Normal file
@@ -0,0 +1,373 @@
|
||||
// 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 "ConfigurationManager.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QUuid>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include "Logger.hpp"
|
||||
#include "ProviderNameMigration.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
ConfigurationManager &ConfigurationManager::instance()
|
||||
{
|
||||
static ConfigurationManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
QVector<AIConfiguration> ConfigurationManager::getPredefinedConfigurations(
|
||||
ConfigurationType type)
|
||||
{
|
||||
QVector<AIConfiguration> presets;
|
||||
|
||||
AIConfiguration claudeOpus;
|
||||
claudeOpus.id = "preset_claude_opus";
|
||||
claudeOpus.name = "Claude Opus 4.7";
|
||||
claudeOpus.provider = "Claude";
|
||||
claudeOpus.model = "claude-opus-4-7";
|
||||
claudeOpus.url = "https://api.anthropic.com";
|
||||
claudeOpus.customEndpoint = "";
|
||||
claudeOpus.templateName = "Claude";
|
||||
claudeOpus.type = type;
|
||||
claudeOpus.isPredefined = true;
|
||||
|
||||
AIConfiguration claudeSonnet;
|
||||
claudeSonnet.id = "preset_claude_sonnet";
|
||||
claudeSonnet.name = "Claude Sonnet 4.6";
|
||||
claudeSonnet.provider = "Claude";
|
||||
claudeSonnet.model = "claude-sonnet-4-6";
|
||||
claudeSonnet.url = "https://api.anthropic.com";
|
||||
claudeSonnet.customEndpoint = "";
|
||||
claudeSonnet.templateName = "Claude";
|
||||
claudeSonnet.type = type;
|
||||
claudeSonnet.isPredefined = true;
|
||||
|
||||
AIConfiguration claudeHaiku;
|
||||
claudeHaiku.id = "preset_claude_haiku";
|
||||
claudeHaiku.name = "Claude Haiku 4.5";
|
||||
claudeHaiku.provider = "Claude";
|
||||
claudeHaiku.model = "claude-haiku-4-5-20251001";
|
||||
claudeHaiku.url = "https://api.anthropic.com";
|
||||
claudeHaiku.customEndpoint = "";
|
||||
claudeHaiku.templateName = "Claude";
|
||||
claudeHaiku.type = type;
|
||||
claudeHaiku.isPredefined = true;
|
||||
|
||||
AIConfiguration codestral;
|
||||
codestral.id = "preset_codestral";
|
||||
codestral.name = "Codestral";
|
||||
codestral.provider = "Codestral";
|
||||
codestral.model = "codestral-latest";
|
||||
codestral.url = "https://codestral.mistral.ai";
|
||||
codestral.customEndpoint = "";
|
||||
codestral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
|
||||
codestral.type = type;
|
||||
codestral.isPredefined = true;
|
||||
|
||||
AIConfiguration mistral;
|
||||
mistral.id = "preset_mistral";
|
||||
mistral.name = "Mistral";
|
||||
mistral.provider = "Mistral AI";
|
||||
mistral.model = type == ConfigurationType::CodeCompletion ? "codestral-latest" : "mistral-large-latest";
|
||||
mistral.url = "https://api.mistral.ai";
|
||||
mistral.customEndpoint = "";
|
||||
mistral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
|
||||
mistral.type = type;
|
||||
mistral.isPredefined = true;
|
||||
|
||||
AIConfiguration geminiFlash;
|
||||
geminiFlash.id = "preset_gemini_flash";
|
||||
geminiFlash.name = "Gemini 2.5 Flash";
|
||||
geminiFlash.provider = "Google AI";
|
||||
geminiFlash.model = "gemini-2.5-flash";
|
||||
geminiFlash.url = "https://generativelanguage.googleapis.com/v1beta";
|
||||
geminiFlash.customEndpoint = "";
|
||||
geminiFlash.templateName = "Google AI";
|
||||
geminiFlash.type = type;
|
||||
geminiFlash.isPredefined = true;
|
||||
|
||||
AIConfiguration qwenPlus;
|
||||
qwenPlus.id = "preset_qwen_plus";
|
||||
qwenPlus.name = "Qwen3.6 Plus";
|
||||
qwenPlus.provider = "Qwen (OpenAI Response)";
|
||||
qwenPlus.model = "qwen3.6-plus";
|
||||
qwenPlus.url = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
qwenPlus.customEndpoint = "";
|
||||
qwenPlus.templateName = "OpenAI Responses";
|
||||
qwenPlus.type = type;
|
||||
qwenPlus.isPredefined = true;
|
||||
|
||||
AIConfiguration qwenMax;
|
||||
qwenMax.id = "preset_qwen_max";
|
||||
qwenMax.name = "Qwen3.7 Max";
|
||||
qwenMax.provider = "Qwen (OpenAI Response)";
|
||||
qwenMax.model = "qwen3.7-max";
|
||||
qwenMax.url = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
qwenMax.customEndpoint = "";
|
||||
qwenMax.templateName = "OpenAI Responses";
|
||||
qwenMax.type = type;
|
||||
qwenMax.isPredefined = true;
|
||||
|
||||
AIConfiguration deepSeekFlash;
|
||||
deepSeekFlash.id = "preset_deepseek_flash";
|
||||
deepSeekFlash.name = "DeepSeek V4 Flash";
|
||||
deepSeekFlash.provider = "DeepSeek";
|
||||
deepSeekFlash.model = "deepseek-v4-flash";
|
||||
deepSeekFlash.url = "https://api.deepseek.com";
|
||||
deepSeekFlash.customEndpoint = "";
|
||||
deepSeekFlash.templateName = "OpenAI Compatible";
|
||||
deepSeekFlash.type = type;
|
||||
deepSeekFlash.isPredefined = true;
|
||||
|
||||
AIConfiguration deepSeekPro;
|
||||
deepSeekPro.id = "preset_deepseek_pro";
|
||||
deepSeekPro.name = "DeepSeek V4 Pro";
|
||||
deepSeekPro.provider = "DeepSeek";
|
||||
deepSeekPro.model = "deepseek-v4-pro";
|
||||
deepSeekPro.url = "https://api.deepseek.com";
|
||||
deepSeekPro.customEndpoint = "";
|
||||
deepSeekPro.templateName = "OpenAI Compatible";
|
||||
deepSeekPro.type = type;
|
||||
deepSeekPro.isPredefined = true;
|
||||
|
||||
AIConfiguration gpt;
|
||||
gpt.id = "preset_gpt";
|
||||
gpt.name = "gpt-5.5";
|
||||
gpt.provider = "OpenAI (Responses API)";
|
||||
gpt.model = "gpt-5.5";
|
||||
gpt.url = "https://api.openai.com/v1";
|
||||
gpt.customEndpoint = "";
|
||||
gpt.templateName = "OpenAI Responses";
|
||||
gpt.type = type;
|
||||
gpt.isPredefined = true;
|
||||
|
||||
presets.append(claudeSonnet);
|
||||
presets.append(claudeHaiku);
|
||||
presets.append(claudeOpus);
|
||||
presets.append(gpt);
|
||||
presets.append(codestral);
|
||||
presets.append(mistral);
|
||||
presets.append(geminiFlash);
|
||||
presets.append(qwenPlus);
|
||||
presets.append(qwenMax);
|
||||
presets.append(deepSeekFlash);
|
||||
presets.append(deepSeekPro);
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const
|
||||
{
|
||||
switch (type) {
|
||||
case ConfigurationType::CodeCompletion:
|
||||
return "code_completion";
|
||||
case ConfigurationType::Chat:
|
||||
return "chat";
|
||||
case ConfigurationType::QuickRefactor:
|
||||
return "quick_refactor";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
QString ConfigurationManager::getConfigurationDirectory(ConfigurationType type) const
|
||||
{
|
||||
QString path = QString("%1/qodeassist/configurations/%2")
|
||||
.arg(Core::ICore::userResourcePath().toFSPathString(),
|
||||
configurationTypeToString(type));
|
||||
return path;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::ensureDirectoryExists(ConfigurationType type) const
|
||||
{
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
if (!dir.exists()) {
|
||||
return dir.mkpath(".");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::loadConfigurations(ConfigurationType type)
|
||||
{
|
||||
QVector<AIConfiguration> *configs = nullptr;
|
||||
switch (type) {
|
||||
case ConfigurationType::CodeCompletion:
|
||||
configs = &m_ccConfigurations;
|
||||
break;
|
||||
case ConfigurationType::Chat:
|
||||
configs = &m_caConfigurations;
|
||||
break;
|
||||
case ConfigurationType::QuickRefactor:
|
||||
configs = &m_qrConfigurations;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!configs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
configs->clear();
|
||||
|
||||
QVector<AIConfiguration> predefinedConfigs = getPredefinedConfigurations(type);
|
||||
configs->append(predefinedConfigs);
|
||||
|
||||
if (!ensureDirectoryExists(type)) {
|
||||
LOG_MESSAGE("Failed to create configuration directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
QStringList filters;
|
||||
filters << "*.json";
|
||||
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
|
||||
|
||||
for (const QFileInfo &fileInfo : files) {
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to open configuration file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
file.close();
|
||||
|
||||
if (!doc.isObject()) {
|
||||
LOG_MESSAGE(QString("Invalid configuration file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
AIConfiguration config;
|
||||
config.id = obj["id"].toString();
|
||||
config.name = obj["name"].toString();
|
||||
config.provider = migrateProviderName(obj["provider"].toString());
|
||||
config.model = obj["model"].toString();
|
||||
config.templateName = obj["template"].toString();
|
||||
config.url = obj["url"].toString();
|
||||
config.customEndpoint = obj["customEndpoint"].toString();
|
||||
config.type = type;
|
||||
config.formatVersion = obj.value("formatVersion").toInt(1);
|
||||
|
||||
config.isPredefined = false;
|
||||
|
||||
if (config.id.isEmpty() || config.name.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Invalid configuration data in file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
configs->append(config);
|
||||
}
|
||||
|
||||
emit configurationsChanged(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::saveConfiguration(const AIConfiguration &config)
|
||||
{
|
||||
if (!ensureDirectoryExists(config.type)) {
|
||||
LOG_MESSAGE("Failed to create configuration directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject obj;
|
||||
obj["formatVersion"] = config.formatVersion;
|
||||
obj["id"] = config.id;
|
||||
obj["name"] = config.name;
|
||||
obj["provider"] = config.provider;
|
||||
obj["model"] = config.model;
|
||||
obj["template"] = config.templateName;
|
||||
obj["url"] = config.url;
|
||||
obj["customEndpoint"] = config.customEndpoint;
|
||||
|
||||
QString sanitizedName = config.name;
|
||||
sanitizedName.replace(" ", "_");
|
||||
sanitizedName.replace(QRegularExpression("[^a-zA-Z0-9_-]"), "");
|
||||
|
||||
QString fileName = QString("%1/%2_%3.json")
|
||||
.arg(getConfigurationDirectory(config.type), sanitizedName, config.id);
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to create configuration file: %1").arg(fileName));
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc(obj);
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
|
||||
loadConfigurations(config.type);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::deleteConfiguration(const QString &id, ConfigurationType type)
|
||||
{
|
||||
AIConfiguration config = getConfigurationById(id, type);
|
||||
if (config.isPredefined) {
|
||||
LOG_MESSAGE(QString("Cannot delete predefined configuration: %1").arg(id));
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
QStringList filters;
|
||||
filters << QString("*_%1.json").arg(id);
|
||||
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
|
||||
|
||||
if (files.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Configuration file not found for id: %1").arg(id));
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const QFileInfo &fileInfo : files) {
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (!file.remove()) {
|
||||
LOG_MESSAGE(QString("Failed to delete configuration file: %1")
|
||||
.arg(fileInfo.absoluteFilePath()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
loadConfigurations(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<AIConfiguration> ConfigurationManager::configurations(ConfigurationType type) const
|
||||
{
|
||||
switch (type) {
|
||||
case ConfigurationType::CodeCompletion:
|
||||
return m_ccConfigurations;
|
||||
case ConfigurationType::Chat:
|
||||
return m_caConfigurations;
|
||||
case ConfigurationType::QuickRefactor:
|
||||
return m_qrConfigurations;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
AIConfiguration ConfigurationManager::getConfigurationById(const QString &id,
|
||||
ConfigurationType type) const
|
||||
{
|
||||
const QVector<AIConfiguration> &configs = configurations(type);
|
||||
for (const AIConfiguration &config : configs) {
|
||||
if (config.id == id) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
return AIConfiguration();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
66
sources/settings/ConfigurationManager.hpp
Normal file
66
sources/settings/ConfigurationManager.hpp
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
enum class ConfigurationType { CodeCompletion, Chat, QuickRefactor };
|
||||
|
||||
inline constexpr int CONFIGURATION_FORMAT_VERSION = 1;
|
||||
|
||||
struct AIConfiguration
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString provider;
|
||||
QString model;
|
||||
QString templateName;
|
||||
QString url;
|
||||
// Empty = use the template's endpoint; non-empty = override path.
|
||||
QString customEndpoint;
|
||||
ConfigurationType type;
|
||||
int formatVersion = CONFIGURATION_FORMAT_VERSION;
|
||||
bool isPredefined = false;
|
||||
};
|
||||
|
||||
class ConfigurationManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ConfigurationManager &instance();
|
||||
|
||||
bool loadConfigurations(ConfigurationType type);
|
||||
bool saveConfiguration(const AIConfiguration &config);
|
||||
bool deleteConfiguration(const QString &id, ConfigurationType type);
|
||||
|
||||
QVector<AIConfiguration> configurations(ConfigurationType type) const;
|
||||
AIConfiguration getConfigurationById(const QString &id, ConfigurationType type) const;
|
||||
|
||||
QString getConfigurationDirectory(ConfigurationType type) const;
|
||||
|
||||
static QVector<AIConfiguration> getPredefinedConfigurations(ConfigurationType type);
|
||||
|
||||
signals:
|
||||
void configurationsChanged(ConfigurationType type);
|
||||
|
||||
private:
|
||||
explicit ConfigurationManager(QObject *parent = nullptr);
|
||||
~ConfigurationManager() override = default;
|
||||
|
||||
bool ensureDirectoryExists(ConfigurationType type) const;
|
||||
QString configurationTypeToString(ConfigurationType type) const;
|
||||
|
||||
QVector<AIConfiguration> m_ccConfigurations;
|
||||
QVector<AIConfiguration> m_caConfigurations;
|
||||
QVector<AIConfiguration> m_qrConfigurations;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
1040
sources/settings/GeneralSettings.cpp
Normal file
1040
sources/settings/GeneralSettings.cpp
Normal file
File diff suppressed because it is too large
Load Diff
176
sources/settings/GeneralSettings.hpp
Normal file
176
sources/settings/GeneralSettings.hpp
Normal file
@@ -0,0 +1,176 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
#include <QPointer>
|
||||
|
||||
#include "ButtonAspect.hpp"
|
||||
#include "ConfigurationManager.hpp"
|
||||
|
||||
namespace Utils {
|
||||
class DetailsWidget;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
class Provider;
|
||||
}
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class GeneralSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
GeneralSettings();
|
||||
|
||||
Utils::BoolAspect enableQodeAssist{this};
|
||||
Utils::BoolAspect enableLogging{this};
|
||||
Utils::BoolAspect enableCheckUpdate{this};
|
||||
|
||||
Utils::IntegerAspect requestTimeout{this};
|
||||
|
||||
ButtonAspect checkUpdate{this};
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
// code completion setttings
|
||||
Utils::SelectionAspect ccPresetConfig{this};
|
||||
ButtonAspect ccConfigureApiKey{this};
|
||||
|
||||
Utils::StringAspect ccProvider{this};
|
||||
ButtonAspect ccSelectProvider{this};
|
||||
|
||||
Utils::StringAspect ccModel{this};
|
||||
ButtonAspect ccSelectModel{this};
|
||||
|
||||
Utils::StringAspect ccTemplate{this};
|
||||
ButtonAspect ccSelectTemplate{this};
|
||||
|
||||
Utils::StringAspect ccUrl{this};
|
||||
ButtonAspect ccSetUrl{this};
|
||||
|
||||
Utils::StringAspect ccCustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect ccStatus{this};
|
||||
ButtonAspect ccTest{this};
|
||||
|
||||
Utils::StringAspect ccTemplateDescription{this};
|
||||
|
||||
ButtonAspect ccSaveConfig{this};
|
||||
ButtonAspect ccLoadConfig{this};
|
||||
ButtonAspect ccOpenConfigFolder{this};
|
||||
|
||||
// TODO create dynamic presets system
|
||||
// preset1 for code completion settings
|
||||
Utils::BoolAspect specifyPreset1{this};
|
||||
Utils::SelectionAspect preset1Language{this};
|
||||
|
||||
Utils::StringAspect ccPreset1Provider{this};
|
||||
ButtonAspect ccPreset1SelectProvider{this};
|
||||
|
||||
Utils::StringAspect ccPreset1Url{this};
|
||||
ButtonAspect ccPreset1SetUrl{this};
|
||||
|
||||
Utils::StringAspect ccPreset1CustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect ccPreset1Model{this};
|
||||
ButtonAspect ccPreset1SelectModel{this};
|
||||
|
||||
Utils::StringAspect ccPreset1Template{this};
|
||||
ButtonAspect ccPreset1SelectTemplate{this};
|
||||
|
||||
// chat assistant settings
|
||||
Utils::SelectionAspect caPresetConfig{this};
|
||||
ButtonAspect caConfigureApiKey{this};
|
||||
|
||||
Utils::StringAspect caProvider{this};
|
||||
ButtonAspect caSelectProvider{this};
|
||||
|
||||
Utils::StringAspect caModel{this};
|
||||
ButtonAspect caSelectModel{this};
|
||||
|
||||
Utils::StringAspect caTemplate{this};
|
||||
ButtonAspect caSelectTemplate{this};
|
||||
|
||||
Utils::StringAspect caUrl{this};
|
||||
ButtonAspect caSetUrl{this};
|
||||
|
||||
Utils::StringAspect caCustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect caStatus{this};
|
||||
ButtonAspect caTest{this};
|
||||
|
||||
Utils::StringAspect caTemplateDescription{this};
|
||||
|
||||
ButtonAspect caSaveConfig{this};
|
||||
ButtonAspect caLoadConfig{this};
|
||||
ButtonAspect caOpenConfigFolder{this};
|
||||
|
||||
// quick refactor settings
|
||||
Utils::SelectionAspect qrPresetConfig{this};
|
||||
ButtonAspect qrConfigureApiKey{this};
|
||||
|
||||
Utils::StringAspect qrProvider{this};
|
||||
ButtonAspect qrSelectProvider{this};
|
||||
|
||||
Utils::StringAspect qrModel{this};
|
||||
ButtonAspect qrSelectModel{this};
|
||||
|
||||
Utils::StringAspect qrTemplate{this};
|
||||
ButtonAspect qrSelectTemplate{this};
|
||||
|
||||
Utils::StringAspect qrUrl{this};
|
||||
ButtonAspect qrSetUrl{this};
|
||||
|
||||
Utils::StringAspect qrCustomEndpoint{this};
|
||||
|
||||
Utils::StringAspect qrStatus{this};
|
||||
ButtonAspect qrTest{this};
|
||||
|
||||
Utils::StringAspect qrTemplateDescription{this};
|
||||
|
||||
ButtonAspect qrSaveConfig{this};
|
||||
ButtonAspect qrLoadConfig{this};
|
||||
ButtonAspect qrOpenConfigFolder{this};
|
||||
|
||||
ButtonAspect ccShowTemplateInfo{this};
|
||||
ButtonAspect caShowTemplateInfo{this};
|
||||
ButtonAspect qrShowTemplateInfo{this};
|
||||
|
||||
void showSelectionDialog(
|
||||
const QStringList &data,
|
||||
Utils::StringAspect &aspect,
|
||||
const QString &title = {},
|
||||
const QString &text = {});
|
||||
|
||||
void showModelsNotFoundDialog(Utils::StringAspect &aspect);
|
||||
|
||||
void showModelsNotSupportedDialog(Utils::StringAspect &aspect);
|
||||
|
||||
void showUrlSelectionDialog(Utils::StringAspect &aspect, const QStringList &predefinedUrls);
|
||||
|
||||
void showTemplateInfoDialog(const Utils::StringAspect &descriptionAspect, const QString &templateName);
|
||||
|
||||
void updatePreset1Visiblity(bool state);
|
||||
|
||||
void onSaveConfiguration(const QString &prefix);
|
||||
void onLoadConfiguration(const QString &prefix);
|
||||
|
||||
void loadPresetConfigurations(Utils::SelectionAspect &aspect, ConfigurationType type);
|
||||
void applyPresetConfiguration(int index, ConfigurationType type);
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetPageToDefaults();
|
||||
|
||||
QVector<AIConfiguration> m_ccPresets;
|
||||
QVector<AIConfiguration> m_caPresets;
|
||||
QVector<AIConfiguration> m_qrPresets;
|
||||
};
|
||||
|
||||
GeneralSettings &generalSettings();
|
||||
|
||||
void showSettings(const Utils::Id page);
|
||||
void showSettings(const Utils::Id page, Utils::Id item);
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
512
sources/settings/McpClientsListAspect.cpp
Normal file
512
sources/settings/McpClientsListAspect.cpp
Normal file
@@ -0,0 +1,512 @@
|
||||
// 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 "McpClientsListAspect.hpp"
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <utils/filepath.h>
|
||||
#include <utils/theme/theme.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDir>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPalette>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "McpSettings.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "StatusDot.hpp"
|
||||
#include "mcp/McpClientsManager.hpp"
|
||||
#include "mcp/McpServerConnection.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
namespace {
|
||||
|
||||
QString mutedColorHex()
|
||||
{
|
||||
if (auto *t = Utils::creatorTheme())
|
||||
return t->color(Utils::Theme::TextColorDisabled).name();
|
||||
return qApp->palette().color(QPalette::PlaceholderText).name();
|
||||
}
|
||||
|
||||
QString errorColorHex()
|
||||
{
|
||||
if (auto *t = Utils::creatorTheme())
|
||||
return t->color(Utils::Theme::TextColorError).name();
|
||||
return QStringLiteral("#d8444d");
|
||||
}
|
||||
|
||||
QColor dotColorFor(Mcp::McpConnectionState state)
|
||||
{
|
||||
auto *t = Utils::creatorTheme();
|
||||
switch (state) {
|
||||
case Mcp::McpConnectionState::Connected:
|
||||
return t ? t->color(Utils::Theme::IconsRunColor) : QColor(0x3fb950);
|
||||
case Mcp::McpConnectionState::Failed:
|
||||
return t ? t->color(Utils::Theme::TextColorError) : QColor(0xd8444d);
|
||||
case Mcp::McpConnectionState::Connecting:
|
||||
return t ? t->color(Utils::Theme::IconsWarningColor) : QColor(0xd29922);
|
||||
case Mcp::McpConnectionState::Disabled:
|
||||
break;
|
||||
}
|
||||
return t ? t->color(Utils::Theme::TextColorDisabled) : QColor(0x888888);
|
||||
}
|
||||
|
||||
QString statusTooltip(Mcp::McpConnectionState state, const QString &detail)
|
||||
{
|
||||
switch (state) {
|
||||
case Mcp::McpConnectionState::Connected:
|
||||
return detail.isEmpty() ? McpClientsListAspect::tr("Connected.") : detail;
|
||||
case Mcp::McpConnectionState::Connecting:
|
||||
return McpClientsListAspect::tr("Connecting…");
|
||||
case Mcp::McpConnectionState::Failed:
|
||||
return detail.isEmpty() ? McpClientsListAspect::tr("Failed.")
|
||||
: McpClientsListAspect::tr("Failed: %1").arg(detail);
|
||||
case Mcp::McpConnectionState::Disabled:
|
||||
break;
|
||||
}
|
||||
return McpClientsListAspect::tr("Disabled.");
|
||||
}
|
||||
|
||||
QString formatDetails(const Mcp::McpServerConfig &cfg)
|
||||
{
|
||||
const QString muted = mutedColorHex();
|
||||
const QString type = cfg.transport == Mcp::McpTransportKind::Http
|
||||
? QStringLiteral("sse")
|
||||
: QStringLiteral("stdio");
|
||||
|
||||
QString details;
|
||||
if (cfg.transport == Mcp::McpTransportKind::Http) {
|
||||
details = QString("<span style=\"color:%1\">%2</span>")
|
||||
.arg(muted, cfg.url.toString().toHtmlEscaped());
|
||||
} else {
|
||||
QStringList parts;
|
||||
if (!cfg.command.isEmpty())
|
||||
parts << cfg.command.toHtmlEscaped();
|
||||
for (const QString &a : cfg.args)
|
||||
parts << a.toHtmlEscaped();
|
||||
details = QString("<span style=\"color:%1\">%2</span>").arg(muted, parts.join(' '));
|
||||
}
|
||||
|
||||
if (!cfg.env.isEmpty()) {
|
||||
QStringList envKeys;
|
||||
for (auto it = cfg.env.begin(); it != cfg.env.end(); ++it)
|
||||
envKeys << it.key().toHtmlEscaped();
|
||||
details
|
||||
+= QString(" <span style=\"color:%1\">env: %2</span>")
|
||||
.arg(muted, envKeys.join(", "));
|
||||
}
|
||||
|
||||
return QString("<b>%1</b> <span style=\"color:%2\">[%3]</span><br><tt>%4</tt>")
|
||||
.arg(cfg.name.toHtmlEscaped(), muted, type, details);
|
||||
}
|
||||
|
||||
struct ExamplePreset
|
||||
{
|
||||
QString label;
|
||||
QString defaultName;
|
||||
QJsonObject body;
|
||||
};
|
||||
|
||||
QList<ExamplePreset> buildExamplePresets()
|
||||
{
|
||||
QList<ExamplePreset> out;
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("everything (reference test server)"),
|
||||
QStringLiteral("everything"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "npx"},
|
||||
{"args", QJsonArray{"-y", "@modelcontextprotocol/server-everything"}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("filesystem (local files)"),
|
||||
QStringLiteral("filesystem"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "npx"},
|
||||
{"args",
|
||||
QJsonArray{
|
||||
"-y", "@modelcontextprotocol/server-filesystem", QDir::homePath()}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("memory (in-memory key-value)"),
|
||||
QStringLiteral("memory"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "npx"},
|
||||
{"args", QJsonArray{"-y", "@modelcontextprotocol/server-memory"}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("git (local git ops)"),
|
||||
QStringLiteral("git"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "uvx"},
|
||||
{"args", QJsonArray{"mcp-server-git"}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("time (system clock)"),
|
||||
QStringLiteral("time"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "stdio"},
|
||||
{"command", "uvx"},
|
||||
{"args", QJsonArray{"mcp-server-time"}}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("qtcreator (Qt Creator's built-in MCP server)"),
|
||||
QStringLiteral("qtcreator"),
|
||||
QJsonObject{
|
||||
{"enable", false},
|
||||
{"type", "sse"},
|
||||
{"url", "http://127.0.0.1:3001/sse"},
|
||||
{"spec", "2024-11-05"}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("qt-docs (Qt documentation)"),
|
||||
QStringLiteral("qt-docs"),
|
||||
QJsonObject{
|
||||
{"enable", true},
|
||||
{"type", "sse"},
|
||||
{"url", "https://qt-docs-mcp.qt.io/mcp"}}});
|
||||
|
||||
out.append(
|
||||
{McpClientsListAspect::tr("remote (SSE / HTTP)"),
|
||||
QStringLiteral("remote"),
|
||||
QJsonObject{
|
||||
{"enable", false},
|
||||
{"type", "sse"},
|
||||
{"url", "https://example.com/mcp"},
|
||||
{"headers", QJsonObject{{"Authorization", "Bearer <token>"}}}}});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
struct RowWidgets
|
||||
{
|
||||
QPointer<StatusDot> dot;
|
||||
QPointer<QLabel> status;
|
||||
QPointer<QLabel> tools;
|
||||
};
|
||||
|
||||
void applyState(const RowWidgets &w, Mcp::McpServerConnection *conn)
|
||||
{
|
||||
if (!conn)
|
||||
return;
|
||||
if (w.dot) {
|
||||
w.dot->setColor(dotColorFor(conn->state()));
|
||||
w.dot->setToolTip(statusTooltip(conn->state(), conn->statusText()));
|
||||
}
|
||||
if (w.status) {
|
||||
w.status->setText(
|
||||
QString("<span style=\"color:%1\">%2</span>")
|
||||
.arg(mutedColorHex(),
|
||||
statusTooltip(conn->state(), conn->statusText()).toHtmlEscaped()));
|
||||
}
|
||||
if (w.tools) {
|
||||
const QStringList names = conn->toolNames();
|
||||
if (names.isEmpty()) {
|
||||
if (conn->state() == Mcp::McpConnectionState::Connected) {
|
||||
w.tools->setText(
|
||||
QString("<i style=\"color:%1\">%2</i>")
|
||||
.arg(mutedColorHex(),
|
||||
McpClientsListAspect::tr("Server reports no tools.")));
|
||||
w.tools->show();
|
||||
} else {
|
||||
w.tools->clear();
|
||||
w.tools->hide();
|
||||
}
|
||||
} else {
|
||||
QStringList escaped;
|
||||
escaped.reserve(names.size());
|
||||
for (const QString &n : names)
|
||||
escaped << n.toHtmlEscaped();
|
||||
w.tools->setText(
|
||||
QString("<b>%1</b> (%2): %3")
|
||||
.arg(McpClientsListAspect::tr("Tools"))
|
||||
.arg(names.size())
|
||||
.arg(escaped.join(", ")));
|
||||
w.tools->show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QWidget *makeRow(Mcp::McpServerConnection *conn, QHash<QString, RowWidgets> *widgets, QWidget *host)
|
||||
{
|
||||
auto *entry = new QFrame(host);
|
||||
entry->setFrameShape(QFrame::StyledPanel);
|
||||
auto *outer = new QVBoxLayout(entry);
|
||||
outer->setContentsMargins(8, 6, 8, 6);
|
||||
outer->setSpacing(2);
|
||||
|
||||
auto *row = new QWidget(entry);
|
||||
auto *rowLayout = new QHBoxLayout(row);
|
||||
rowLayout->setContentsMargins(0, 0, 0, 0);
|
||||
rowLayout->setSpacing(6);
|
||||
|
||||
auto *dot = new StatusDot(row);
|
||||
rowLayout->addWidget(dot, 0, Qt::AlignTop);
|
||||
|
||||
auto *check = new QCheckBox(row);
|
||||
check->setChecked(conn->config().enabled);
|
||||
check->setToolTip(McpClientsListAspect::tr("Enable / disable this MCP server"));
|
||||
rowLayout->addWidget(check, 0, Qt::AlignTop);
|
||||
|
||||
auto *info = new QLabel(formatDetails(conn->config()), row);
|
||||
info->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
info->setWordWrap(true);
|
||||
rowLayout->addWidget(info, 1);
|
||||
|
||||
auto *statusLabel = new QLabel(row);
|
||||
statusLabel->setMinimumWidth(120);
|
||||
statusLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
rowLayout->addWidget(statusLabel, 0, Qt::AlignTop);
|
||||
|
||||
auto *removeBtn = new QPushButton(QStringLiteral("✕"), row);
|
||||
removeBtn->setToolTip(McpClientsListAspect::tr("Remove this server from the config."));
|
||||
removeBtn->setFlat(true);
|
||||
removeBtn->setFixedWidth(24);
|
||||
removeBtn->setCursor(Qt::PointingHandCursor);
|
||||
removeBtn->setStyleSheet(
|
||||
QString("QPushButton:hover { color: %1; }").arg(errorColorHex()));
|
||||
rowLayout->addWidget(removeBtn, 0, Qt::AlignTop);
|
||||
|
||||
outer->addWidget(row);
|
||||
|
||||
auto *tools = new QLabel(entry);
|
||||
tools->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
tools->setWordWrap(true);
|
||||
tools->setContentsMargins(38, 0, 0, 0);
|
||||
tools->hide();
|
||||
outer->addWidget(tools);
|
||||
|
||||
RowWidgets w{dot, statusLabel, tools};
|
||||
widgets->insert(conn->config().name, w);
|
||||
applyState(w, conn);
|
||||
|
||||
QObject::connect(check, &QCheckBox::toggled, row, [name = conn->config().name](bool on) {
|
||||
Mcp::McpClientsManager::instance().setServerEnabled(name, on);
|
||||
});
|
||||
|
||||
QObject::connect(removeBtn, &QPushButton::clicked, row, [name = conn->config().name, host]() {
|
||||
const auto reply = QMessageBox::question(
|
||||
host,
|
||||
McpClientsListAspect::tr("Remove server"),
|
||||
McpClientsListAspect::tr("Remove server '%1' from the config?").arg(name),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No);
|
||||
if (reply == QMessageBox::Yes)
|
||||
Mcp::McpClientsManager::instance().removeServer(name);
|
||||
});
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
void clearLayout(QVBoxLayout *lay)
|
||||
{
|
||||
while (auto *item = lay->takeAt(0)) {
|
||||
if (auto *w = item->widget())
|
||||
w->deleteLater();
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
McpClientsListAspect::McpClientsListAspect(Utils::AspectContainer *container)
|
||||
: Utils::BaseAspect(container)
|
||||
{}
|
||||
|
||||
void McpClientsListAspect::addToLayoutImpl(Layouting::Layout &parent)
|
||||
{
|
||||
auto *outer = new QWidget();
|
||||
auto *outerLayout = new QVBoxLayout(outer);
|
||||
outerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
outerLayout->setSpacing(4);
|
||||
|
||||
auto *openBtn = new QPushButton(tr("Open Config"), outer);
|
||||
openBtn->setToolTip(Mcp::McpClientsManager::configFilePath());
|
||||
auto *refreshBtn = new QPushButton(tr("Refresh MCP List"), outer);
|
||||
|
||||
auto *buttonsRow = new QHBoxLayout();
|
||||
buttonsRow->setContentsMargins(0, 0, 0, 0);
|
||||
buttonsRow->addWidget(openBtn);
|
||||
buttonsRow->addWidget(refreshBtn);
|
||||
buttonsRow->addStretch(1);
|
||||
|
||||
auto *restartHint = new QLabel(outer);
|
||||
restartHint->setWordWrap(true);
|
||||
restartHint->setText(
|
||||
tr("Note: restart Qt Creator to apply MCP changes to already-opened chats "
|
||||
"and running sessions."));
|
||||
|
||||
auto *summaryLabel = new QLabel(outer);
|
||||
summaryLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
auto *serversHost = new QWidget(outer);
|
||||
auto *serversLayout = new QVBoxLayout(serversHost);
|
||||
serversLayout->setContentsMargins(0, 0, 0, 0);
|
||||
serversLayout->setSpacing(4);
|
||||
|
||||
auto *scroll = new QScrollArea(outer);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setMinimumHeight(160);
|
||||
scroll->setFrameShape(QFrame::StyledPanel);
|
||||
scroll->setWidget(serversHost);
|
||||
|
||||
auto *quickSetupLabel = new QLabel(tr("Quick Setup"), outer);
|
||||
auto *presetsCombo = new QComboBox(outer);
|
||||
presetsCombo->setToolTip(
|
||||
tr("Pick a preset to append a ready-made server entry to the config "
|
||||
"(auto-suffixed if the name is taken)."));
|
||||
presetsCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
presetsCombo->addItem(tr("-- Select Preset --"));
|
||||
const auto presets = buildExamplePresets();
|
||||
for (const auto &p : presets)
|
||||
presetsCombo->addItem(p.label);
|
||||
|
||||
auto *btnRow = new QHBoxLayout();
|
||||
btnRow->setContentsMargins(0, 0, 0, 0);
|
||||
btnRow->addWidget(quickSetupLabel);
|
||||
btnRow->addWidget(presetsCombo);
|
||||
btnRow->addStretch(1);
|
||||
|
||||
outerLayout->addLayout(buttonsRow);
|
||||
outerLayout->addWidget(restartHint);
|
||||
outerLayout->addWidget(summaryLabel);
|
||||
outerLayout->addWidget(scroll);
|
||||
outerLayout->addLayout(btnRow);
|
||||
|
||||
auto rowsState = std::make_shared<QHash<QString, RowWidgets>>();
|
||||
|
||||
auto rebuild = [outer, serversLayout, summaryLabel, rowsState]() {
|
||||
clearLayout(serversLayout);
|
||||
rowsState->clear();
|
||||
|
||||
const auto connections = Mcp::McpClientsManager::instance().connections();
|
||||
if (connections.isEmpty()) {
|
||||
auto *empty = new QLabel(
|
||||
QString("<p style=\"color:%1\">%2</p>")
|
||||
.arg(mutedColorHex(),
|
||||
tr("No servers configured. Add a preset below or edit the JSON.")),
|
||||
outer);
|
||||
serversLayout->addWidget(empty);
|
||||
serversLayout->addStretch();
|
||||
summaryLabel->setText(
|
||||
QString("<i>%1</i>").arg(tr("0 server(s) defined.")));
|
||||
return;
|
||||
}
|
||||
|
||||
int enabled = 0;
|
||||
for (auto *conn : connections) {
|
||||
serversLayout->addWidget(makeRow(conn, rowsState.get(), outer));
|
||||
if (conn->config().enabled)
|
||||
++enabled;
|
||||
}
|
||||
serversLayout->addStretch();
|
||||
|
||||
summaryLabel->setText(
|
||||
QString("<i>%1</i>")
|
||||
.arg(tr("%1 server(s) defined, %2 enabled.")
|
||||
.arg(connections.size())
|
||||
.arg(enabled)));
|
||||
};
|
||||
|
||||
rebuild();
|
||||
|
||||
QObject::connect(
|
||||
&Mcp::McpClientsManager::instance(),
|
||||
&Mcp::McpClientsManager::serversChanged,
|
||||
outer,
|
||||
[rebuild, rowsState]() {
|
||||
const auto connections = Mcp::McpClientsManager::instance().connections();
|
||||
|
||||
QStringList currentNames;
|
||||
currentNames.reserve(connections.size());
|
||||
for (auto *c : connections)
|
||||
currentNames << c->config().name;
|
||||
|
||||
QStringList knownNames = rowsState->keys();
|
||||
std::sort(currentNames.begin(), currentNames.end());
|
||||
std::sort(knownNames.begin(), knownNames.end());
|
||||
|
||||
if (currentNames != knownNames) {
|
||||
rebuild();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto *conn : connections)
|
||||
applyState(rowsState->value(conn->config().name), conn);
|
||||
});
|
||||
|
||||
QObject::connect(refreshBtn, &QPushButton::clicked, outer, []() {
|
||||
Mcp::McpClientsManager::instance().reload();
|
||||
});
|
||||
QObject::connect(openBtn, &QPushButton::clicked, outer, []() {
|
||||
Core::EditorManager::openEditor(
|
||||
Utils::FilePath::fromString(Mcp::McpClientsManager::configFilePath()));
|
||||
});
|
||||
QObject::connect(
|
||||
&Mcp::McpClientsManager::instance(),
|
||||
&Mcp::McpClientsManager::writeFailed,
|
||||
outer,
|
||||
[outer](const QString &reason) {
|
||||
QMessageBox::warning(
|
||||
outer,
|
||||
McpClientsListAspect::tr("MCP configuration"),
|
||||
McpClientsListAspect::tr("Failed to write %1:\n%2")
|
||||
.arg(Mcp::McpClientsManager::configFilePath(), reason));
|
||||
});
|
||||
auto syncEnabled = [outer]() {
|
||||
outer->setEnabled(mcpSettings().enableMcpClients.volatileValue());
|
||||
};
|
||||
syncEnabled();
|
||||
QObject::connect(
|
||||
&mcpSettings().enableMcpClients,
|
||||
&Utils::BoolAspect::volatileValueChanged,
|
||||
outer,
|
||||
syncEnabled);
|
||||
|
||||
QObject::connect(
|
||||
presetsCombo,
|
||||
&QComboBox::currentIndexChanged,
|
||||
outer,
|
||||
[presetsCombo, presets](int idx) {
|
||||
if (idx <= 0)
|
||||
return;
|
||||
const int presetIdx = idx - 1;
|
||||
if (presetIdx < 0 || presetIdx >= presets.size()) {
|
||||
presetsCombo->setCurrentIndex(0);
|
||||
return;
|
||||
}
|
||||
Mcp::McpClientsManager::instance().addServer(
|
||||
presets[presetIdx].defaultName, presets[presetIdx].body);
|
||||
// Snap back to the placeholder so the next pick fires again even
|
||||
// if the user chooses the same preset twice.
|
||||
presetsCombo->setCurrentIndex(0);
|
||||
});
|
||||
|
||||
parent.addItem(outer);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
22
sources/settings/McpClientsListAspect.hpp
Normal file
22
sources/settings/McpClientsListAspect.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class McpClientsListAspect : public Utils::BaseAspect
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit McpClientsListAspect(Utils::AspectContainer *container = nullptr);
|
||||
|
||||
void addToLayoutImpl(Layouting::Layout &parent) override;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
350
sources/settings/McpSettings.cpp
Normal file
350
sources/settings/McpSettings.cpp
Normal file
@@ -0,0 +1,350 @@
|
||||
// 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 "McpSettings.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QDir>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFontDatabase>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "SettingsUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
McpSettings &mcpSettings()
|
||||
{
|
||||
static McpSettings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
McpSettings::McpSettings()
|
||||
{
|
||||
setAutoApply(false);
|
||||
|
||||
setDisplayName(Tr::tr("MCP"));
|
||||
|
||||
enableMcpServer.setSettingsKey(Constants::MCP_ENABLE_SERVER);
|
||||
enableMcpServer.setLabelText(Tr::tr("Enable MCP server"));
|
||||
enableMcpServer.setToolTip(
|
||||
Tr::tr("Expose QodeAssist tools to external MCP clients over HTTP. "
|
||||
"Which tools are visible is controlled on the client side."));
|
||||
enableMcpServer.setDefaultValue(false);
|
||||
|
||||
mcpServerPort.setSettingsKey(Constants::MCP_SERVER_PORT);
|
||||
mcpServerPort.setLabelText(Tr::tr("Server port"));
|
||||
mcpServerPort.setToolTip(
|
||||
Tr::tr("TCP port the MCP server listens on (localhost only). "
|
||||
"Requires restart of the server after change."));
|
||||
mcpServerPort.setRange(1, 65535);
|
||||
mcpServerPort.setDefaultValue(3456);
|
||||
|
||||
enableMcpClients.setSettingsKey(Constants::MCP_ENABLE_CLIENTS);
|
||||
enableMcpClients.setLabelText(Tr::tr("Connect to external MCP servers"));
|
||||
enableMcpClients.setToolTip(
|
||||
Tr::tr("Connect to MCP servers listed in mcp-server.json and expose their tools "
|
||||
"to chat/quick-refactor/code-completion. Toggling this off disconnects all "
|
||||
"currently running MCP client sessions."));
|
||||
enableMcpClients.setDefaultValue(false);
|
||||
|
||||
mcpClientExtraPaths.setSettingsKey(Constants::MCP_CLIENT_EXTRA_PATHS);
|
||||
mcpClientExtraPaths.setLabelText(Tr::tr("Extra PATH for stdio servers"));
|
||||
mcpClientExtraPaths.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
mcpClientExtraPaths.setToolTip(
|
||||
Tr::tr("Directories to prepend to PATH when launching stdio MCP servers. "
|
||||
"Useful when Qt Creator is started from the dock and doesn't see Homebrew, "
|
||||
"nvm, uv, etc. Separate multiple entries with '%1'. "
|
||||
"Per-server 'env' overrides in mcp-server.json still win.")
|
||||
.arg(QDir::listSeparator()));
|
||||
#ifdef Q_OS_MACOS
|
||||
mcpClientExtraPaths.setDefaultValue(
|
||||
QStringLiteral("/opt/homebrew/bin:/usr/local/bin"));
|
||||
#else
|
||||
mcpClientExtraPaths.setDefaultValue(QString{});
|
||||
#endif
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
showConnectionInstructions.m_buttonText = Tr::tr("How to connect...");
|
||||
|
||||
readSettings();
|
||||
|
||||
setupConnections();
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Server")),
|
||||
Column{
|
||||
enableMcpServer,
|
||||
mcpServerPort,
|
||||
Row{Stretch{1}, showConnectionInstructions}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Clients")),
|
||||
Column{enableMcpClients, mcpClientExtraPaths, mcpClientsList}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
void McpSettings::setupConnections()
|
||||
{
|
||||
connect(
|
||||
&resetToDefaults,
|
||||
&ButtonAspect::clicked,
|
||||
this,
|
||||
&McpSettings::resetSettingsToDefaults);
|
||||
|
||||
connect(
|
||||
&showConnectionInstructions,
|
||||
&ButtonAspect::clicked,
|
||||
this,
|
||||
&McpSettings::showConnectionInstructionsDialog);
|
||||
|
||||
auto syncServerSubgroup = [this]() {
|
||||
const bool on = enableMcpServer.volatileValue();
|
||||
mcpServerPort.setEnabled(on);
|
||||
};
|
||||
auto syncClientsSubgroup = [this]() {
|
||||
const bool on = enableMcpClients.volatileValue();
|
||||
mcpClientExtraPaths.setEnabled(on);
|
||||
mcpClientsList.setEnabled(on);
|
||||
};
|
||||
connect(
|
||||
&enableMcpServer,
|
||||
&Utils::BoolAspect::volatileValueChanged,
|
||||
this,
|
||||
syncServerSubgroup);
|
||||
connect(
|
||||
&enableMcpClients,
|
||||
&Utils::BoolAspect::volatileValueChanged,
|
||||
this,
|
||||
syncClientsSubgroup);
|
||||
syncServerSubgroup();
|
||||
syncClientsSubgroup();
|
||||
}
|
||||
|
||||
void McpSettings::resetSettingsToDefaults()
|
||||
{
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr("Reset Settings"),
|
||||
Tr::tr("Are you sure you want to reset all settings to default values?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(enableMcpServer);
|
||||
resetAspect(mcpServerPort);
|
||||
resetAspect(enableMcpClients);
|
||||
resetAspect(mcpClientExtraPaths);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void McpSettings::showConnectionInstructionsDialog()
|
||||
{
|
||||
const quint16 port = static_cast<quint16>(mcpServerPort.volatileValue());
|
||||
const QString serverUrl = QString("http://127.0.0.1:%1/mcp").arg(port);
|
||||
|
||||
const QString bridgeUrl = QStringLiteral("https://github.com/Palm1r/llmqore/releases");
|
||||
|
||||
const QString directJson = QString(R"({
|
||||
"mcpServers": {
|
||||
"qodeassist": {
|
||||
"type": "sse",
|
||||
"url": "%1"
|
||||
}
|
||||
}
|
||||
})").arg(serverUrl);
|
||||
|
||||
const QString claudeCodeCmd
|
||||
= QString("claude mcp add --transport sse qodeassist %1").arg(serverUrl);
|
||||
|
||||
const QString vscodeJson = QString(R"({
|
||||
"servers": {
|
||||
"qodeassist": {
|
||||
"type": "sse",
|
||||
"url": "%1"
|
||||
}
|
||||
}
|
||||
})").arg(serverUrl);
|
||||
|
||||
const QString bridgeJson = QString(R"({
|
||||
"port": 8808,
|
||||
"host": "127.0.0.1",
|
||||
"mcpServers": {
|
||||
"qodeassist": {
|
||||
"type": "sse",
|
||||
"url": "%1"
|
||||
}
|
||||
}
|
||||
})").arg(serverUrl);
|
||||
|
||||
const QString claudeDesktopJson = QStringLiteral(R"({
|
||||
"mcpServers": {
|
||||
"qodeassist": {
|
||||
"command": "<path-to>/mcp-bridge",
|
||||
"args": ["--stdio", "<path-to>/mcp-bridge.json"]
|
||||
}
|
||||
}
|
||||
})");
|
||||
|
||||
auto *dialog = new QDialog(Core::ICore::dialogParent());
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->setWindowTitle(Tr::tr("Connect to QodeAssist MCP"));
|
||||
dialog->resize(640, 480);
|
||||
|
||||
auto *intro = new QLabel(
|
||||
Tr::tr("Server URL: <code>%1</code>. If your MCP client speaks HTTP/SSE "
|
||||
"natively, use the <b>Direct</b> tab. If it only speaks stdio "
|
||||
"(e.g. Claude Desktop), use the <b>Bridge</b> tab.")
|
||||
.arg(serverUrl),
|
||||
dialog);
|
||||
intro->setWordWrap(true);
|
||||
intro->setTextFormat(Qt::RichText);
|
||||
|
||||
auto makeCodeBlock = [](QWidget *parent, const QString &text) -> QWidget * {
|
||||
auto *container = new QWidget(parent);
|
||||
auto *layout = new QVBoxLayout(container);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto *edit = new QPlainTextEdit(text, container);
|
||||
edit->setReadOnly(true);
|
||||
edit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
edit->setLineWrapMode(QPlainTextEdit::NoWrap);
|
||||
edit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
edit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
const int lines = text.count(QLatin1Char('\n')) + 1;
|
||||
const QFontMetrics fm(edit->font());
|
||||
const int contentHeight = lines * fm.lineSpacing();
|
||||
const int frame = 2 * edit->frameWidth();
|
||||
edit->setFixedHeight(contentHeight + frame + 8);
|
||||
|
||||
auto *copyBtn = new QPushButton(Tr::tr("Copy"), container);
|
||||
copyBtn->setAutoDefault(false);
|
||||
copyBtn->setDefault(false);
|
||||
QObject::connect(copyBtn, &QPushButton::clicked, container, [edit] {
|
||||
QApplication::clipboard()->setText(edit->toPlainText());
|
||||
});
|
||||
|
||||
auto *btnRow = new QHBoxLayout();
|
||||
btnRow->addStretch(1);
|
||||
btnRow->addWidget(copyBtn);
|
||||
|
||||
layout->addWidget(edit);
|
||||
layout->addLayout(btnRow);
|
||||
return container;
|
||||
};
|
||||
|
||||
auto *tabs = new QTabWidget(dialog);
|
||||
|
||||
// Direct tab
|
||||
{
|
||||
auto *tab = new QWidget(tabs);
|
||||
auto *layout = new QVBoxLayout(tab);
|
||||
|
||||
auto *hint = new QLabel(Tr::tr("<b>Claude Code</b> (CLI): run once —"), tab);
|
||||
hint->setTextFormat(Qt::RichText);
|
||||
layout->addWidget(hint);
|
||||
layout->addWidget(makeCodeBlock(tab, claudeCodeCmd));
|
||||
|
||||
auto *vscodeHint = new QLabel(
|
||||
Tr::tr("<b>VS Code</b>: save as <code>.vscode/mcp.json</code> in the workspace:"),
|
||||
tab);
|
||||
vscodeHint->setTextFormat(Qt::RichText);
|
||||
vscodeHint->setWordWrap(true);
|
||||
layout->addWidget(vscodeHint);
|
||||
layout->addWidget(makeCodeBlock(tab, vscodeJson));
|
||||
|
||||
auto *jsonHint = new QLabel(
|
||||
Tr::tr("Any other client that reads an <code>mcpServers</code> JSON block:"),
|
||||
tab);
|
||||
jsonHint->setTextFormat(Qt::RichText);
|
||||
jsonHint->setWordWrap(true);
|
||||
layout->addWidget(jsonHint);
|
||||
layout->addWidget(makeCodeBlock(tab, directJson));
|
||||
layout->addStretch(1);
|
||||
|
||||
tabs->addTab(tab, Tr::tr("Direct (HTTP/SSE)"));
|
||||
}
|
||||
|
||||
// Bridge tab
|
||||
{
|
||||
auto *tab = new QWidget(tabs);
|
||||
auto *layout = new QVBoxLayout(tab);
|
||||
|
||||
auto *step1 = new QLabel(
|
||||
Tr::tr("<b>1.</b> Download <code>mcp-bridge</code> for your OS from "
|
||||
"<a href=\"%1\">%1</a>.")
|
||||
.arg(bridgeUrl),
|
||||
tab);
|
||||
step1->setTextFormat(Qt::RichText);
|
||||
step1->setWordWrap(true);
|
||||
step1->setOpenExternalLinks(true);
|
||||
layout->addWidget(step1);
|
||||
|
||||
auto *step2 = new QLabel(
|
||||
Tr::tr("<b>2.</b> Save the following as <code>mcp-bridge.json</code>:"), tab);
|
||||
step2->setTextFormat(Qt::RichText);
|
||||
layout->addWidget(step2);
|
||||
layout->addWidget(makeCodeBlock(tab, bridgeJson));
|
||||
|
||||
auto *step3 = new QLabel(
|
||||
Tr::tr("<b>3.</b> Point the stdio-only client at the bridge. "
|
||||
"Example for <code>claude_desktop_config.json</code>:"),
|
||||
tab);
|
||||
step3->setTextFormat(Qt::RichText);
|
||||
step3->setWordWrap(true);
|
||||
layout->addWidget(step3);
|
||||
layout->addWidget(makeCodeBlock(tab, claudeDesktopJson));
|
||||
layout->addStretch(1);
|
||||
|
||||
tabs->addTab(tab, Tr::tr("Bridge (stdio)"));
|
||||
}
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, dialog);
|
||||
QObject::connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::close);
|
||||
|
||||
auto *layout = new QVBoxLayout(dialog);
|
||||
layout->addWidget(intro);
|
||||
layout->addWidget(tabs, 1);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
dialog->show();
|
||||
}
|
||||
|
||||
class McpSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
McpSettingsPage()
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_MCP_SETTINGS_PAGE_ID);
|
||||
setDisplayName(Tr::tr("MCP"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setSettingsProvider([] { return &mcpSettings(); });
|
||||
}
|
||||
};
|
||||
|
||||
const McpSettingsPage mcpSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
38
sources/settings/McpSettings.hpp
Normal file
38
sources/settings/McpSettings.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ButtonAspect.hpp"
|
||||
#include "McpClientsListAspect.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class McpSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
McpSettings();
|
||||
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
Utils::BoolAspect enableMcpServer{this};
|
||||
Utils::IntegerAspect mcpServerPort{this};
|
||||
|
||||
ButtonAspect showConnectionInstructions{this};
|
||||
|
||||
Utils::BoolAspect enableMcpClients{this};
|
||||
Utils::StringAspect mcpClientExtraPaths{this};
|
||||
McpClientsListAspect mcpClientsList{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
void showConnectionInstructionsDialog();
|
||||
};
|
||||
|
||||
McpSettings &mcpSettings();
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
84
sources/settings/PluginUpdater.cpp
Normal file
84
sources/settings/PluginUpdater.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 "PluginUpdater.hpp"
|
||||
|
||||
#include <coreplugin/coreconstants.h>
|
||||
#include <coreplugin/coreplugin.h>
|
||||
#include <extensionsystem/pluginmanager.h>
|
||||
#include <extensionsystem/pluginspec.h>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
PluginUpdater::PluginUpdater(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_networkManager(new QNetworkAccessManager(this))
|
||||
{}
|
||||
|
||||
void PluginUpdater::checkForUpdates()
|
||||
{
|
||||
if (m_isCheckingUpdate)
|
||||
return;
|
||||
|
||||
m_isCheckingUpdate = true;
|
||||
QNetworkRequest request((QUrl(getUpdateUrl())));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QNetworkReply *reply = m_networkManager->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
handleUpdateResponse(reply);
|
||||
m_isCheckingUpdate = false;
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void PluginUpdater::handleUpdateResponse(QNetworkReply *reply)
|
||||
{
|
||||
UpdateInfo info;
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
emit updateCheckFinished(info);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||
QJsonObject obj = doc.object();
|
||||
|
||||
info.version = obj["tag_name"].toString();
|
||||
if (info.version.startsWith('v'))
|
||||
info.version.remove(0, 1);
|
||||
|
||||
info.changeLog = obj["body"].toString();
|
||||
|
||||
info.isUpdateAvailable = QVersionNumber::fromString(info.version)
|
||||
> QVersionNumber::fromString(currentVersion());
|
||||
|
||||
m_lastUpdateInfo = info;
|
||||
emit updateCheckFinished(info);
|
||||
}
|
||||
|
||||
QString PluginUpdater::currentVersion() const
|
||||
{
|
||||
const auto pluginSpecs = ExtensionSystem::PluginManager::plugins();
|
||||
for (const ExtensionSystem::PluginSpec *spec : pluginSpecs) {
|
||||
if (spec->name() == QLatin1String("QodeAssist"))
|
||||
return spec->version();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool PluginUpdater::isUpdateAvailable() const
|
||||
{
|
||||
return m_lastUpdateInfo.isUpdateAvailable;
|
||||
}
|
||||
|
||||
QString PluginUpdater::getUpdateUrl() const
|
||||
{
|
||||
return "https://api.github.com/repos/Palm1r/qodeassist/releases/latest";
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
45
sources/settings/PluginUpdater.hpp
Normal file
45
sources/settings/PluginUpdater.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QVersionNumber>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class PluginUpdater : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
struct UpdateInfo
|
||||
{
|
||||
QString version;
|
||||
QString changeLog;
|
||||
bool isUpdateAvailable = false;
|
||||
};
|
||||
|
||||
explicit PluginUpdater(QObject *parent = nullptr);
|
||||
~PluginUpdater() = default;
|
||||
|
||||
void checkForUpdates();
|
||||
QString currentVersion() const;
|
||||
bool isUpdateAvailable() const;
|
||||
|
||||
signals:
|
||||
void updateCheckFinished(const UpdateInfo &info);
|
||||
|
||||
private:
|
||||
void handleUpdateResponse(QNetworkReply *reply);
|
||||
QString getUpdateUrl() const;
|
||||
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
UpdateInfo m_lastUpdateInfo;
|
||||
bool m_isCheckingUpdate = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
76
sources/settings/ProjectSettings.cpp
Normal file
76
sources/settings/ProjectSettings.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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 "ProjectSettings.hpp"
|
||||
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include <coreplugin/icore.h>
|
||||
#include <projectexplorer/project.h>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
ProjectSettings::ProjectSettings(ProjectExplorer::Project *project)
|
||||
{
|
||||
setAutoApply(true);
|
||||
|
||||
useGlobalSettings.setSettingsKey(Constants::QODE_ASSIST_USE_GLOBAL_SETTINGS);
|
||||
useGlobalSettings.setDefaultValue(true);
|
||||
|
||||
enableQodeAssist.setSettingsKey(Constants::QODE_ASSIST_ENABLE_IN_PROJECT);
|
||||
enableQodeAssist.setDisplayName(Tr::tr("Enable QodeAssist"));
|
||||
enableQodeAssist.setLabelText(Tr::tr("Enable QodeAssist"));
|
||||
enableQodeAssist.setDefaultValue(false);
|
||||
|
||||
chatHistoryPath.setSettingsKey(Constants::QODE_ASSIST_CHAT_HISTORY_PATH);
|
||||
chatHistoryPath.setExpectedKind(Utils::PathChooser::ExistingDirectory);
|
||||
chatHistoryPath.setLabelText(Tr::tr("Chat History Path:"));
|
||||
|
||||
QString projectChatHistoryPath = QString("%1/qodeassist/chat_history")
|
||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
||||
|
||||
chatHistoryPath.setDefaultValue(projectChatHistoryPath);
|
||||
|
||||
projectSkillDirs.setSettingsKey(Constants::SK_PROJECT_SKILL_DIRS);
|
||||
projectSkillDirs.setLabelText(Tr::tr("Skill directories:"));
|
||||
projectSkillDirs.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
projectSkillDirs.setToolTip(
|
||||
Tr::tr("Project-relative subdirectories scanned for Agent Skills, one per line. "
|
||||
"Resolved against the project root. These take priority over the global "
|
||||
"skill directories when a skill name appears in both."));
|
||||
projectSkillDirs.setDefaultValue(".qodeassist/skills\n.claude/skills");
|
||||
|
||||
Utils::Store map = Utils::storeFromVariant(
|
||||
project->namedSettings(Constants::QODE_ASSIST_PROJECT_SETTINGS_ID));
|
||||
fromMap(map);
|
||||
|
||||
enableQodeAssist.addOnChanged(this, [this, project] { save(project); });
|
||||
useGlobalSettings.addOnChanged(this, [this, project] { save(project); });
|
||||
chatHistoryPath.addOnChanged(this, [this, project] { save(project); });
|
||||
projectSkillDirs.addOnChanged(this, [this, project] { save(project); });
|
||||
}
|
||||
|
||||
void ProjectSettings::setUseGlobalSettings(bool useGlobal)
|
||||
{
|
||||
useGlobalSettings.setValue(useGlobal);
|
||||
}
|
||||
|
||||
bool ProjectSettings::isEnabled() const
|
||||
{
|
||||
if (useGlobalSettings())
|
||||
return generalSettings().enableQodeAssist();
|
||||
return enableQodeAssist();
|
||||
}
|
||||
|
||||
void ProjectSettings::save(ProjectExplorer::Project *project)
|
||||
{
|
||||
Utils::Store map;
|
||||
toMap(map);
|
||||
project
|
||||
->setNamedSettings(Constants::QODE_ASSIST_PROJECT_SETTINGS_ID, Utils::variantFromStore(map));
|
||||
generalSettings().apply();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
30
sources/settings/ProjectSettings.hpp
Normal file
30
sources/settings/ProjectSettings.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
namespace ProjectExplorer {
|
||||
class Project;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class ProjectSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
explicit ProjectSettings(ProjectExplorer::Project *project);
|
||||
void save(ProjectExplorer::Project *project);
|
||||
|
||||
void setUseGlobalSettings(bool useGlobalSettings);
|
||||
bool isEnabled() const;
|
||||
|
||||
Utils::BoolAspect enableQodeAssist{this};
|
||||
Utils::BoolAspect useGlobalSettings{this};
|
||||
Utils::FilePathAspect chatHistoryPath{this};
|
||||
Utils::StringAspect projectSkillDirs{this};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
126
sources/settings/ProjectSettingsPanel.cpp
Normal file
126
sources/settings/ProjectSettingsPanel.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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 "ProjectSettingsPanel.hpp"
|
||||
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectpanelfactory.h>
|
||||
#include <projectexplorer/projectsettingswidget.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "SkillsSettings.hpp"
|
||||
#include "skills/SkillsLoader.hpp"
|
||||
#include "skills/SkillsManager.hpp"
|
||||
|
||||
using namespace ProjectExplorer;
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class ProjectSettingsWidget final : public ProjectExplorer::ProjectSettingsWidget
|
||||
{
|
||||
public:
|
||||
ProjectSettingsWidget()
|
||||
{
|
||||
setGlobalSettingsId(Constants::QODE_ASSIST_GENERAL_OPTIONS_ID);
|
||||
setUseGlobalSettingsCheckBoxVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
static ProjectSettingsWidget *createProjectPanel(Project *project)
|
||||
{
|
||||
using namespace Layouting;
|
||||
|
||||
auto widget = new ProjectSettingsWidget;
|
||||
auto settings = new ProjectSettings(project);
|
||||
settings->setParent(widget);
|
||||
|
||||
QObject::connect(
|
||||
widget,
|
||||
&ProjectSettingsWidget::useGlobalSettingsChanged,
|
||||
settings,
|
||||
&ProjectSettings::setUseGlobalSettings);
|
||||
|
||||
widget->setUseGlobalSettings(settings->useGlobalSettings());
|
||||
|
||||
auto generalWidget = new QWidget;
|
||||
Column{
|
||||
settings->enableQodeAssist,
|
||||
Space{8},
|
||||
settings->chatHistoryPath,
|
||||
}
|
||||
.attachTo(generalWidget);
|
||||
|
||||
generalWidget->setEnabled(!settings->useGlobalSettings());
|
||||
QObject::connect(
|
||||
widget,
|
||||
&ProjectSettingsWidget::useGlobalSettingsChanged,
|
||||
generalWidget,
|
||||
[generalWidget](bool useGlobal) { generalWidget->setEnabled(!useGlobal); });
|
||||
|
||||
auto skillsList = new QListWidget;
|
||||
skillsList->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
skillsList->setMaximumHeight(160);
|
||||
|
||||
auto refreshSkills = [skillsList, project, settings] {
|
||||
skillsList->clear();
|
||||
|
||||
// Project-only roots: the global page shows global skills separately.
|
||||
const QStringList roots = Skills::SkillsManager::resolveRoots(
|
||||
project->projectDirectory().toFSPathString(),
|
||||
{},
|
||||
SkillsSettings::splitLines(settings->projectSkillDirs()));
|
||||
|
||||
const QVector<Skills::AgentSkill> skills = Skills::SkillsLoader::scan(roots);
|
||||
for (const Skills::AgentSkill &skill : skills) {
|
||||
auto *item = new QListWidgetItem(
|
||||
QStringLiteral("%1 — %2").arg(skill.name, skill.description), skillsList);
|
||||
item->setToolTip(skill.skillDir);
|
||||
}
|
||||
if (skills.isEmpty())
|
||||
new QListWidgetItem(Tr::tr("No skills discovered."), skillsList);
|
||||
};
|
||||
refreshSkills();
|
||||
QObject::connect(
|
||||
&settings->projectSkillDirs, &Utils::BaseAspect::changed, skillsList, refreshSkills);
|
||||
|
||||
Column{
|
||||
generalWidget,
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Skills")),
|
||||
Column{
|
||||
settings->projectSkillDirs,
|
||||
new QLabel(Tr::tr("Discovered project skills:")),
|
||||
skillsList,
|
||||
},
|
||||
},
|
||||
}
|
||||
.attachTo(widget);
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
class ProjectPanelFactory final : public ProjectExplorer::ProjectPanelFactory
|
||||
{
|
||||
public:
|
||||
ProjectPanelFactory()
|
||||
{
|
||||
setPriority(1000);
|
||||
setDisplayName(Tr::tr("QodeAssist"));
|
||||
setCreateWidgetFunction(&createProjectPanel);
|
||||
}
|
||||
};
|
||||
|
||||
void setupProjectPanel()
|
||||
{
|
||||
static ProjectPanelFactory theProjectPanelFactory;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
11
sources/settings/ProjectSettingsPanel.hpp
Normal file
11
sources/settings/ProjectSettingsPanel.hpp
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
void setupProjectPanel();
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
30
sources/settings/ProviderNameMigration.hpp
Normal file
30
sources/settings/ProviderNameMigration.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
// Maps legacy provider names (used before multi-API providers were introduced
|
||||
// with parenthetical suffixes) to their current canonical names. Returns the
|
||||
// input unchanged if it is not a known legacy name.
|
||||
inline QString migrateProviderName(const QString &oldName)
|
||||
{
|
||||
static const QHash<QString, QString> renames{
|
||||
{QStringLiteral("Ollama"), QStringLiteral("Ollama (Native)")},
|
||||
{QStringLiteral("Ollama Compatible"), QStringLiteral("Ollama (OpenAI-compatible)")},
|
||||
{QStringLiteral("OpenAI"), QStringLiteral("OpenAI (Chat Completions)")},
|
||||
{QStringLiteral("OpenAI Responses"), QStringLiteral("OpenAI (Responses API)")},
|
||||
{QStringLiteral("LM Studio"), QStringLiteral("LM Studio (Chat Completions)")},
|
||||
{QStringLiteral("LM Studio Responses"), QStringLiteral("LM Studio (Responses API)")},
|
||||
};
|
||||
|
||||
const auto it = renames.constFind(oldName);
|
||||
return it != renames.constEnd() ? it.value() : oldName;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
261
sources/settings/ProviderSettings.cpp
Normal file
261
sources/settings/ProviderSettings.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
// 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 "ProviderSettings.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "SettingsUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
ProviderSettings &providerSettings()
|
||||
{
|
||||
static ProviderSettings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
ProviderSettings::ProviderSettings()
|
||||
{
|
||||
setAutoApply(false);
|
||||
|
||||
setDisplayName(Tr::tr("Provider Settings"));
|
||||
|
||||
// OpenRouter Settings
|
||||
openRouterApiKey.setSettingsKey(Constants::OPEN_ROUTER_API_KEY);
|
||||
openRouterApiKey.setLabelText(Tr::tr("OpenRouter API Key:"));
|
||||
openRouterApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
openRouterApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
openRouterApiKey.setHistoryCompleter(Constants::OPEN_ROUTER_API_KEY_HISTORY);
|
||||
openRouterApiKey.setDefaultValue("");
|
||||
openRouterApiKey.setAutoApply(true);
|
||||
|
||||
// OpenAI Compatible Settings
|
||||
openAiCompatApiKey.setSettingsKey(Constants::OPEN_AI_COMPAT_API_KEY);
|
||||
openAiCompatApiKey.setLabelText(Tr::tr("OpenAI Compatible API Key:"));
|
||||
openAiCompatApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
openAiCompatApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
openAiCompatApiKey.setHistoryCompleter(Constants::OPEN_AI_COMPAT_API_KEY_HISTORY);
|
||||
openAiCompatApiKey.setDefaultValue("");
|
||||
openAiCompatApiKey.setAutoApply(true);
|
||||
|
||||
// Claude Compatible Settings
|
||||
claudeApiKey.setSettingsKey(Constants::CLAUDE_API_KEY);
|
||||
claudeApiKey.setLabelText(Tr::tr("Claude API Key:"));
|
||||
claudeApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
claudeApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
claudeApiKey.setHistoryCompleter(Constants::CLAUDE_API_KEY_HISTORY);
|
||||
claudeApiKey.setDefaultValue("");
|
||||
claudeApiKey.setAutoApply(true);
|
||||
|
||||
claudeEnablePromptCaching.setSettingsKey(Constants::CLAUDE_ENABLE_PROMPT_CACHING);
|
||||
claudeEnablePromptCaching.setLabelText(Tr::tr("Enable prompt caching"));
|
||||
claudeEnablePromptCaching.setToolTip(
|
||||
Tr::tr("Marks the system prompt, tool definitions, and stable chat history with "
|
||||
"cache_control so Anthropic caches the request prefix (5-minute TTL). "
|
||||
"Reduces cost and latency on repeated turns."));
|
||||
claudeEnablePromptCaching.setDefaultValue(false);
|
||||
claudeEnablePromptCaching.setAutoApply(true);
|
||||
|
||||
claudeUseExtendedCacheTTL.setSettingsKey(Constants::CLAUDE_USE_EXTENDED_CACHE_TTL);
|
||||
claudeUseExtendedCacheTTL.setLabelText(Tr::tr("Use 1h cache TTL (beta)"));
|
||||
claudeUseExtendedCacheTTL.setToolTip(
|
||||
Tr::tr("Requests Anthropic's 1-hour cache TTL instead of the default 5 minutes. "
|
||||
"Sends the extended-cache-ttl-2025-04-11 beta header."));
|
||||
claudeUseExtendedCacheTTL.setDefaultValue(false);
|
||||
claudeUseExtendedCacheTTL.setAutoApply(true);
|
||||
|
||||
// OpenAI Settings
|
||||
openAiApiKey.setSettingsKey(Constants::OPEN_AI_API_KEY);
|
||||
openAiApiKey.setLabelText(Tr::tr("OpenAI API Key:"));
|
||||
openAiApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
openAiApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
openAiApiKey.setHistoryCompleter(Constants::OPEN_AI_API_KEY_HISTORY);
|
||||
openAiApiKey.setDefaultValue("");
|
||||
openAiApiKey.setAutoApply(true);
|
||||
|
||||
// MistralAI Settings
|
||||
mistralAiApiKey.setSettingsKey(Constants::MISTRAL_AI_API_KEY);
|
||||
mistralAiApiKey.setLabelText(Tr::tr("Mistral AI API Key:"));
|
||||
mistralAiApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
mistralAiApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
mistralAiApiKey.setHistoryCompleter(Constants::MISTRAL_AI_API_KEY_HISTORY);
|
||||
mistralAiApiKey.setDefaultValue("");
|
||||
mistralAiApiKey.setAutoApply(true);
|
||||
|
||||
codestralApiKey.setSettingsKey(Constants::CODESTRAL_API_KEY);
|
||||
codestralApiKey.setLabelText(Tr::tr("Codestral API Key:"));
|
||||
codestralApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
codestralApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
codestralApiKey.setHistoryCompleter(Constants::CODESTRAL_API_KEY_HISTORY);
|
||||
codestralApiKey.setDefaultValue("");
|
||||
codestralApiKey.setAutoApply(true);
|
||||
|
||||
// GoogleAI Settings
|
||||
googleAiApiKey.setSettingsKey(Constants::GOOGLE_AI_API_KEY);
|
||||
googleAiApiKey.setLabelText(Tr::tr("Google AI API Key:"));
|
||||
googleAiApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
googleAiApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
googleAiApiKey.setHistoryCompleter(Constants::GOOGLE_AI_API_KEY_HISTORY);
|
||||
googleAiApiKey.setDefaultValue("");
|
||||
googleAiApiKey.setAutoApply(true);
|
||||
|
||||
// Ollama with BasicAuth Settings
|
||||
ollamaBasicAuthApiKey.setSettingsKey(Constants::OLLAMA_BASIC_AUTH_API_KEY);
|
||||
ollamaBasicAuthApiKey.setLabelText(Tr::tr("Ollama(Bearer) API Key:"));
|
||||
ollamaBasicAuthApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
ollamaBasicAuthApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
ollamaBasicAuthApiKey.setHistoryCompleter(Constants::OLLAMA_BASIC_AUTH_API_KEY_HISTORY);
|
||||
ollamaBasicAuthApiKey.setDefaultValue("");
|
||||
ollamaBasicAuthApiKey.setAutoApply(true);
|
||||
|
||||
// llama.cpp Settings
|
||||
llamaCppApiKey.setSettingsKey(Constants::LLAMA_CPP_API_KEY);
|
||||
llamaCppApiKey.setLabelText(Tr::tr("llama.cpp API Key:"));
|
||||
llamaCppApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
llamaCppApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
llamaCppApiKey.setHistoryCompleter(Constants::LLAMA_CPP_API_KEY_HISTORY);
|
||||
llamaCppApiKey.setDefaultValue("");
|
||||
llamaCppApiKey.setAutoApply(true);
|
||||
|
||||
// Qwen (Alibaba) Settings
|
||||
qwenApiKey.setSettingsKey(Constants::QWEN_API_KEY);
|
||||
qwenApiKey.setLabelText(Tr::tr("Qwen API Key:"));
|
||||
qwenApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
qwenApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
qwenApiKey.setHistoryCompleter(Constants::QWEN_API_KEY_HISTORY);
|
||||
qwenApiKey.setDefaultValue("");
|
||||
qwenApiKey.setAutoApply(true);
|
||||
|
||||
// DeepSeek Settings
|
||||
deepSeekApiKey.setSettingsKey(Constants::DEEPSEEK_API_KEY);
|
||||
deepSeekApiKey.setLabelText(Tr::tr("DeepSeek API Key:"));
|
||||
deepSeekApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
deepSeekApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
deepSeekApiKey.setHistoryCompleter(Constants::DEEPSEEK_API_KEY_HISTORY);
|
||||
deepSeekApiKey.setDefaultValue("");
|
||||
deepSeekApiKey.setAutoApply(true);
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
|
||||
readSettings();
|
||||
|
||||
setupConnections();
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("OpenRouter Settings")), Column{openRouterApiKey}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("OpenAI Settings")), Column{openAiApiKey}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("OpenAI Compatible Settings")), Column{openAiCompatApiKey}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Claude Settings")),
|
||||
Column{claudeApiKey, claudeEnablePromptCaching, claudeUseExtendedCacheTTL}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Mistral AI Settings")), Column{mistralAiApiKey, codestralApiKey}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Google AI Settings")), Column{googleAiApiKey}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{ollamaBasicAuthApiKey}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("llama.cpp Settings")), Column{llamaCppApiKey}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Qwen (Alibaba) Settings")), Column{qwenApiKey}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("DeepSeek Settings")), Column{deepSeekApiKey}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
void ProviderSettings::setupConnections()
|
||||
{
|
||||
connect(
|
||||
&resetToDefaults, &ButtonAspect::clicked, this, &ProviderSettings::resetSettingsToDefaults);
|
||||
connect(&openRouterApiKey, &ButtonAspect::changed, this, [this]() {
|
||||
openRouterApiKey.writeSettings();
|
||||
});
|
||||
connect(&openAiCompatApiKey, &ButtonAspect::changed, this, [this]() {
|
||||
openAiCompatApiKey.writeSettings();
|
||||
});
|
||||
connect(&claudeApiKey, &ButtonAspect::changed, this, [this]() { claudeApiKey.writeSettings(); });
|
||||
connect(&claudeEnablePromptCaching, &Utils::BoolAspect::changed, this, [this]() {
|
||||
claudeEnablePromptCaching.writeSettings();
|
||||
});
|
||||
connect(&claudeUseExtendedCacheTTL, &Utils::BoolAspect::changed, this, [this]() {
|
||||
claudeUseExtendedCacheTTL.writeSettings();
|
||||
});
|
||||
connect(&openAiApiKey, &ButtonAspect::changed, this, [this]() { openAiApiKey.writeSettings(); });
|
||||
connect(&mistralAiApiKey, &ButtonAspect::changed, this, [this]() {
|
||||
mistralAiApiKey.writeSettings();
|
||||
});
|
||||
connect(&codestralApiKey, &ButtonAspect::changed, this, [this]() {
|
||||
codestralApiKey.writeSettings();
|
||||
});
|
||||
connect(&googleAiApiKey, &ButtonAspect::changed, this, [this]() {
|
||||
googleAiApiKey.writeSettings();
|
||||
});
|
||||
connect(&ollamaBasicAuthApiKey, &ButtonAspect::changed, this, [this]() {
|
||||
ollamaBasicAuthApiKey.writeSettings();
|
||||
});
|
||||
connect(&llamaCppApiKey, &ButtonAspect::changed, this, [this]() {
|
||||
llamaCppApiKey.writeSettings();
|
||||
});
|
||||
connect(&qwenApiKey, &ButtonAspect::changed, this, [this]() { qwenApiKey.writeSettings(); });
|
||||
connect(&deepSeekApiKey, &ButtonAspect::changed, this, [this]() {
|
||||
deepSeekApiKey.writeSettings();
|
||||
});
|
||||
}
|
||||
|
||||
void ProviderSettings::resetSettingsToDefaults()
|
||||
{
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr("Reset Settings"),
|
||||
Tr::tr("Are you sure you want to reset all settings to default values?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(openRouterApiKey);
|
||||
resetAspect(openAiCompatApiKey);
|
||||
resetAspect(claudeApiKey);
|
||||
resetAspect(claudeEnablePromptCaching);
|
||||
resetAspect(claudeUseExtendedCacheTTL);
|
||||
resetAspect(openAiApiKey);
|
||||
resetAspect(mistralAiApiKey);
|
||||
resetAspect(googleAiApiKey);
|
||||
resetAspect(ollamaBasicAuthApiKey);
|
||||
resetAspect(llamaCppApiKey);
|
||||
resetAspect(qwenApiKey);
|
||||
resetAspect(deepSeekApiKey);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
class ProviderSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
ProviderSettingsPage()
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
|
||||
setDisplayName(Tr::tr("Provider Settings"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setSettingsProvider([] { return &providerSettings(); });
|
||||
}
|
||||
};
|
||||
|
||||
const ProviderSettingsPage providerSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
42
sources/settings/ProviderSettings.hpp
Normal file
42
sources/settings/ProviderSettings.hpp
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ButtonAspect.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class ProviderSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
ProviderSettings();
|
||||
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
// API Keys
|
||||
Utils::StringAspect openRouterApiKey{this};
|
||||
Utils::StringAspect openAiCompatApiKey{this};
|
||||
Utils::StringAspect claudeApiKey{this};
|
||||
Utils::BoolAspect claudeEnablePromptCaching{this};
|
||||
Utils::BoolAspect claudeUseExtendedCacheTTL{this};
|
||||
Utils::StringAspect openAiApiKey{this};
|
||||
Utils::StringAspect mistralAiApiKey{this};
|
||||
Utils::StringAspect codestralApiKey{this};
|
||||
Utils::StringAspect googleAiApiKey{this};
|
||||
Utils::StringAspect ollamaBasicAuthApiKey{this};
|
||||
Utils::StringAspect llamaCppApiKey{this};
|
||||
Utils::StringAspect qwenApiKey{this};
|
||||
Utils::StringAspect deepSeekApiKey{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
};
|
||||
|
||||
ProviderSettings &providerSettings();
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
409
sources/settings/QuickRefactorSettings.cpp
Normal file
409
sources/settings/QuickRefactorSettings.cpp
Normal file
@@ -0,0 +1,409 @@
|
||||
// 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 "QuickRefactorSettings.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "SettingsUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
QuickRefactorSettings &quickRefactorSettings()
|
||||
{
|
||||
static QuickRefactorSettings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
QuickRefactorSettings::QuickRefactorSettings()
|
||||
{
|
||||
setAutoApply(false);
|
||||
|
||||
setDisplayName(Tr::tr("Quick Refactor"));
|
||||
|
||||
// General Parameters Settings
|
||||
temperature.setSettingsKey(Constants::QR_TEMPERATURE);
|
||||
temperature.setLabelText(Tr::tr("Temperature:"));
|
||||
temperature.setDefaultValue(0.5);
|
||||
temperature.setRange(0.0, 2.0);
|
||||
temperature.setSingleStep(0.1);
|
||||
|
||||
maxTokens.setSettingsKey(Constants::QR_MAX_TOKENS);
|
||||
maxTokens.setLabelText(Tr::tr("Max Tokens:"));
|
||||
maxTokens.setRange(-1, 200000);
|
||||
maxTokens.setDefaultValue(2000);
|
||||
|
||||
// Advanced Parameters
|
||||
useTopP.setSettingsKey(Constants::QR_USE_TOP_P);
|
||||
useTopP.setDefaultValue(false);
|
||||
useTopP.setLabelText(Tr::tr("Top P:"));
|
||||
|
||||
topP.setSettingsKey(Constants::QR_TOP_P);
|
||||
topP.setDefaultValue(0.9);
|
||||
topP.setRange(0.0, 1.0);
|
||||
topP.setSingleStep(0.1);
|
||||
|
||||
useTopK.setSettingsKey(Constants::QR_USE_TOP_K);
|
||||
useTopK.setDefaultValue(false);
|
||||
useTopK.setLabelText(Tr::tr("Top K:"));
|
||||
|
||||
topK.setSettingsKey(Constants::QR_TOP_K);
|
||||
topK.setDefaultValue(50);
|
||||
topK.setRange(1, 1000);
|
||||
|
||||
usePresencePenalty.setSettingsKey(Constants::QR_USE_PRESENCE_PENALTY);
|
||||
usePresencePenalty.setDefaultValue(false);
|
||||
usePresencePenalty.setLabelText(Tr::tr("Presence Penalty:"));
|
||||
|
||||
presencePenalty.setSettingsKey(Constants::QR_PRESENCE_PENALTY);
|
||||
presencePenalty.setDefaultValue(0.0);
|
||||
presencePenalty.setRange(-2.0, 2.0);
|
||||
presencePenalty.setSingleStep(0.1);
|
||||
|
||||
useFrequencyPenalty.setSettingsKey(Constants::QR_USE_FREQUENCY_PENALTY);
|
||||
useFrequencyPenalty.setDefaultValue(false);
|
||||
useFrequencyPenalty.setLabelText(Tr::tr("Frequency Penalty:"));
|
||||
|
||||
frequencyPenalty.setSettingsKey(Constants::QR_FREQUENCY_PENALTY);
|
||||
frequencyPenalty.setDefaultValue(0.0);
|
||||
frequencyPenalty.setRange(-2.0, 2.0);
|
||||
frequencyPenalty.setSingleStep(0.1);
|
||||
|
||||
// Ollama Settings
|
||||
ollamaLivetime.setSettingsKey(Constants::QR_OLLAMA_LIVETIME);
|
||||
ollamaLivetime.setToolTip(
|
||||
Tr::tr(
|
||||
"Time to suspend Ollama after completion request (in minutes), "
|
||||
"Only Ollama, -1 to disable"));
|
||||
ollamaLivetime.setLabelText("Livetime:");
|
||||
ollamaLivetime.setDefaultValue("5m");
|
||||
ollamaLivetime.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
|
||||
contextWindow.setSettingsKey(Constants::QR_OLLAMA_CONTEXT_WINDOW);
|
||||
contextWindow.setLabelText(Tr::tr("Context Window:"));
|
||||
contextWindow.setRange(-1, 10000);
|
||||
contextWindow.setDefaultValue(2048);
|
||||
|
||||
useTools.setSettingsKey(Constants::QR_USE_TOOLS);
|
||||
useTools.setLabelText(Tr::tr("Enable Tools"));
|
||||
useTools.setToolTip(
|
||||
Tr::tr(
|
||||
"Enable AI tools/functions for quick refactoring (allows reading project files, "
|
||||
"searching code, etc.)"));
|
||||
useTools.setDefaultValue(false);
|
||||
|
||||
useThinking.setSettingsKey(Constants::QR_USE_THINKING);
|
||||
useThinking.setLabelText(Tr::tr("Enable Thinking Mode"));
|
||||
useThinking.setToolTip(
|
||||
Tr::tr(
|
||||
"Enable extended thinking mode for complex refactoring tasks (supported by "
|
||||
"compatible models like Claude and Google AI)"));
|
||||
useThinking.setDefaultValue(false);
|
||||
|
||||
thinkingBudgetTokens.setSettingsKey(Constants::QR_THINKING_BUDGET_TOKENS);
|
||||
thinkingBudgetTokens.setLabelText(Tr::tr("Thinking Budget Tokens:"));
|
||||
thinkingBudgetTokens.setToolTip(
|
||||
Tr::tr(
|
||||
"Number of tokens allocated for thinking process. Use -1 for dynamic thinking "
|
||||
"(model decides), 0 to disable, or positive value for custom budget"));
|
||||
thinkingBudgetTokens.setRange(-1, 100000);
|
||||
thinkingBudgetTokens.setDefaultValue(10000);
|
||||
|
||||
thinkingMaxTokens.setSettingsKey(Constants::QR_THINKING_MAX_TOKENS);
|
||||
thinkingMaxTokens.setLabelText(Tr::tr("Thinking Max Output Tokens:"));
|
||||
thinkingMaxTokens.setToolTip(
|
||||
Tr::tr(
|
||||
"Maximum output tokens when thinking mode is enabled (includes thinking + response)"));
|
||||
thinkingMaxTokens.setRange(1000, 200000);
|
||||
thinkingMaxTokens.setDefaultValue(16000);
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
openAIResponsesReasoningEffort.setSettingsKey(Constants::QR_OPENAI_RESPONSES_REASONING_EFFORT);
|
||||
openAIResponsesReasoningEffort.setLabelText(Tr::tr("Reasoning effort:"));
|
||||
openAIResponsesReasoningEffort.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
openAIResponsesReasoningEffort.addOption("None");
|
||||
openAIResponsesReasoningEffort.addOption("Minimal");
|
||||
openAIResponsesReasoningEffort.addOption("Low");
|
||||
openAIResponsesReasoningEffort.addOption("Medium");
|
||||
openAIResponsesReasoningEffort.addOption("High");
|
||||
openAIResponsesReasoningEffort.setDefaultValue("Medium");
|
||||
openAIResponsesReasoningEffort.setToolTip(
|
||||
Tr::tr(
|
||||
"Constrains effort on reasoning for OpenAI gpt-5 and o-series models:\n\n"
|
||||
"None: No reasoning (gpt-5.1 only)\n"
|
||||
"Minimal: Minimal reasoning effort (o-series only)\n"
|
||||
"Low: Low reasoning effort\n"
|
||||
"Medium: Balanced reasoning (default for most models)\n"
|
||||
"High: Maximum reasoning effort (gpt-5-pro only supports this)\n\n"
|
||||
"Note: Reducing effort = faster responses + fewer tokens"));
|
||||
|
||||
// Context Settings
|
||||
readFullFile.setSettingsKey(Constants::QR_READ_FULL_FILE);
|
||||
readFullFile.setLabelText(Tr::tr("Read Full File"));
|
||||
readFullFile.setDefaultValue(false);
|
||||
|
||||
readFileParts.setLabelText(Tr::tr("Read Strings Before Cursor:"));
|
||||
readFileParts.setDefaultValue(true);
|
||||
|
||||
readStringsBeforeCursor.setSettingsKey(Constants::QR_READ_STRINGS_BEFORE_CURSOR);
|
||||
readStringsBeforeCursor.setLabelText(Tr::tr("Lines Before Cursor/Selection:"));
|
||||
readStringsBeforeCursor.setToolTip(
|
||||
Tr::tr("Number of lines to include before cursor or selection for context"));
|
||||
readStringsBeforeCursor.setRange(0, 10000);
|
||||
readStringsBeforeCursor.setDefaultValue(50);
|
||||
|
||||
readStringsAfterCursor.setSettingsKey(Constants::QR_READ_STRINGS_AFTER_CURSOR);
|
||||
readStringsAfterCursor.setLabelText(Tr::tr("Lines After Cursor/Selection:"));
|
||||
readStringsAfterCursor.setToolTip(
|
||||
Tr::tr("Number of lines to include after cursor or selection for context"));
|
||||
readStringsAfterCursor.setRange(0, 10000);
|
||||
readStringsAfterCursor.setDefaultValue(30);
|
||||
|
||||
displayMode.setSettingsKey(Constants::QR_DISPLAY_MODE);
|
||||
displayMode.setLabelText(Tr::tr("Display Mode:"));
|
||||
displayMode.setToolTip(
|
||||
Tr::tr(
|
||||
"Choose how to display refactoring suggestions:\n"
|
||||
"- Inline Widget: Shows refactor in a widget overlay with Apply/Decline buttons "
|
||||
"(default)\n"
|
||||
"- Qt Creator Suggestion: Uses Qt Creator's built-in suggestion system"));
|
||||
displayMode.addOption(Tr::tr("Inline Widget"));
|
||||
displayMode.addOption(Tr::tr("Qt Creator Suggestion"));
|
||||
displayMode.setDefaultValue(0);
|
||||
|
||||
widgetOrientation.setSettingsKey(Constants::QR_WIDGET_ORIENTATION);
|
||||
widgetOrientation.setLabelText(Tr::tr("Widget Orientation:"));
|
||||
widgetOrientation.setToolTip(
|
||||
Tr::tr(
|
||||
"Choose default orientation for refactor widget:\n"
|
||||
"- Horizontal: Original and refactored code side by side (default)\n"
|
||||
"- Vertical: Original and refactored code stacked vertically"));
|
||||
widgetOrientation.addOption(Tr::tr("Horizontal"));
|
||||
widgetOrientation.addOption(Tr::tr("Vertical"));
|
||||
widgetOrientation.setDefaultValue(0);
|
||||
|
||||
widgetMinWidth.setSettingsKey(Constants::QR_WIDGET_MIN_WIDTH);
|
||||
widgetMinWidth.setLabelText(Tr::tr("Widget Minimum Width:"));
|
||||
widgetMinWidth.setToolTip(Tr::tr("Minimum width for the refactor widget (in pixels)"));
|
||||
widgetMinWidth.setRange(200, 999999);
|
||||
widgetMinWidth.setDefaultValue(400);
|
||||
|
||||
widgetMaxWidth.setSettingsKey(Constants::QR_WIDGET_MAX_WIDTH);
|
||||
widgetMaxWidth.setLabelText(Tr::tr("Widget Maximum Width:"));
|
||||
widgetMaxWidth.setToolTip(Tr::tr("Maximum width for the refactor widget (in pixels)"));
|
||||
widgetMaxWidth.setRange(400, 999999);
|
||||
widgetMaxWidth.setDefaultValue(9999);
|
||||
|
||||
widgetMinHeight.setSettingsKey(Constants::QR_WIDGET_MIN_HEIGHT);
|
||||
widgetMinHeight.setLabelText(Tr::tr("Widget Minimum Height:"));
|
||||
widgetMinHeight.setToolTip(Tr::tr("Minimum height for the refactor widget (in pixels)"));
|
||||
widgetMinHeight.setRange(80, 999999);
|
||||
widgetMinHeight.setDefaultValue(100);
|
||||
|
||||
widgetMaxHeight.setSettingsKey(Constants::QR_WIDGET_MAX_HEIGHT);
|
||||
widgetMaxHeight.setLabelText(Tr::tr("Widget Maximum Height:"));
|
||||
widgetMaxHeight.setToolTip(Tr::tr("Maximum height for the refactor widget (in pixels)"));
|
||||
widgetMaxHeight.setRange(200, 999999);
|
||||
widgetMaxHeight.setDefaultValue(9999);
|
||||
|
||||
systemPrompt.setSettingsKey(Constants::QR_SYSTEM_PROMPT);
|
||||
systemPrompt.setLabelText(Tr::tr("System Prompt:"));
|
||||
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
systemPrompt.setDefaultValue(
|
||||
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
|
||||
"precise and contextually appropriate code completions to insert depending on user "
|
||||
"instructions.\n\n");
|
||||
|
||||
useOpenFilesInQuickRefactor.setSettingsKey(Constants::QR_USE_OPEN_FILES_IN_QUICK_REFACTOR);
|
||||
useOpenFilesInQuickRefactor.setLabelText(
|
||||
Tr::tr("Include context from open files in quick refactor"));
|
||||
useOpenFilesInQuickRefactor.setDefaultValue(false);
|
||||
|
||||
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
||||
|
||||
readSettings();
|
||||
|
||||
readFileParts.setValue(!readFullFile.value());
|
||||
|
||||
setupConnections();
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
auto genGrid = Grid{};
|
||||
genGrid.addRow({Row{temperature}});
|
||||
genGrid.addRow({Row{maxTokens}});
|
||||
|
||||
auto advancedGrid = Grid{};
|
||||
advancedGrid.addRow({useTopP, topP});
|
||||
advancedGrid.addRow({useTopK, topK});
|
||||
advancedGrid.addRow({usePresencePenalty, presencePenalty});
|
||||
advancedGrid.addRow({useFrequencyPenalty, frequencyPenalty});
|
||||
|
||||
auto ollamaGrid = Grid{};
|
||||
ollamaGrid.addRow({ollamaLivetime});
|
||||
ollamaGrid.addRow({contextWindow});
|
||||
|
||||
auto toolsGrid = Grid{};
|
||||
toolsGrid.addRow({useTools});
|
||||
toolsGrid.addRow({useThinking});
|
||||
toolsGrid.addRow({thinkingBudgetTokens});
|
||||
toolsGrid.addRow({thinkingMaxTokens});
|
||||
|
||||
auto openAIResponsesGrid = Grid{};
|
||||
openAIResponsesGrid.addRow({openAIResponsesReasoningEffort});
|
||||
|
||||
auto contextGrid = Grid{};
|
||||
contextGrid.addRow({Row{readFullFile}});
|
||||
contextGrid.addRow({
|
||||
Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor},
|
||||
});
|
||||
contextGrid.addRow({Row{useOpenFilesInQuickRefactor}});
|
||||
|
||||
auto displayGrid = Grid{};
|
||||
displayGrid.addRow({Row{displayMode}});
|
||||
displayGrid.addRow({Row{widgetOrientation}});
|
||||
displayGrid.addRow({Row{widgetMinWidth, widgetMaxWidth}});
|
||||
displayGrid.addRow({Row{widgetMinHeight, widgetMaxHeight}});
|
||||
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("General Parameters")),
|
||||
Row{genGrid, Stretch{1}},
|
||||
},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Tools Settings")), Column{Row{toolsGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("OpenAI Responses API")), Column{Row{openAIResponsesGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")), Column{Row{contextGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Display Settings")), Column{Row{displayGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Prompt Settings")), Column{Row{systemPrompt}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
void QuickRefactorSettings::setupConnections()
|
||||
{
|
||||
connect(
|
||||
&resetToDefaults,
|
||||
&ButtonAspect::clicked,
|
||||
this,
|
||||
&QuickRefactorSettings::resetSettingsToDefaults);
|
||||
|
||||
connect(&readFullFile, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
if (readFullFile.volatileValue()) {
|
||||
readFileParts.setValue(false);
|
||||
writeSettings();
|
||||
}
|
||||
});
|
||||
|
||||
connect(&readFileParts, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
if (readFileParts.volatileValue()) {
|
||||
readFullFile.setValue(false);
|
||||
writeSettings();
|
||||
}
|
||||
});
|
||||
|
||||
// Enable/disable widgetOrientation based on displayMode
|
||||
// 0 = Inline Widget, 1 = Qt Creator Suggestion
|
||||
auto updateWidgetOrientationEnabled = [this]() {
|
||||
bool isInlineWidget = (displayMode.volatileValue() == 0);
|
||||
widgetOrientation.setEnabled(isInlineWidget);
|
||||
};
|
||||
|
||||
connect(
|
||||
&displayMode,
|
||||
&Utils::SelectionAspect::volatileValueChanged,
|
||||
this,
|
||||
updateWidgetOrientationEnabled);
|
||||
|
||||
updateWidgetOrientationEnabled();
|
||||
|
||||
auto validateWidgetSizes = [this]() {
|
||||
if (widgetMinWidth.volatileValue() > widgetMaxWidth.volatileValue()) {
|
||||
widgetMaxWidth.setValue(widgetMinWidth.volatileValue());
|
||||
}
|
||||
if (widgetMinHeight.volatileValue() > widgetMaxHeight.volatileValue()) {
|
||||
widgetMaxHeight.setValue(widgetMinHeight.volatileValue());
|
||||
}
|
||||
};
|
||||
|
||||
connect(&widgetMinWidth, &Utils::IntegerAspect::volatileValueChanged, this, validateWidgetSizes);
|
||||
connect(&widgetMaxWidth, &Utils::IntegerAspect::volatileValueChanged, this, validateWidgetSizes);
|
||||
connect(&widgetMinHeight, &Utils::IntegerAspect::volatileValueChanged, this, validateWidgetSizes);
|
||||
connect(&widgetMaxHeight, &Utils::IntegerAspect::volatileValueChanged, this, validateWidgetSizes);
|
||||
}
|
||||
|
||||
void QuickRefactorSettings::resetSettingsToDefaults()
|
||||
{
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr("Reset Settings"),
|
||||
Tr::tr("Are you sure you want to reset all settings to default values?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
resetAspect(useTopP);
|
||||
resetAspect(topP);
|
||||
resetAspect(useTopK);
|
||||
resetAspect(topK);
|
||||
resetAspect(usePresencePenalty);
|
||||
resetAspect(presencePenalty);
|
||||
resetAspect(useFrequencyPenalty);
|
||||
resetAspect(frequencyPenalty);
|
||||
resetAspect(ollamaLivetime);
|
||||
resetAspect(contextWindow);
|
||||
resetAspect(useTools);
|
||||
resetAspect(useThinking);
|
||||
resetAspect(thinkingBudgetTokens);
|
||||
resetAspect(thinkingMaxTokens);
|
||||
resetAspect(openAIResponsesReasoningEffort);
|
||||
resetAspect(readFullFile);
|
||||
resetAspect(readFileParts);
|
||||
resetAspect(readStringsBeforeCursor);
|
||||
resetAspect(readStringsAfterCursor);
|
||||
resetAspect(displayMode);
|
||||
resetAspect(widgetOrientation);
|
||||
resetAspect(widgetMinWidth);
|
||||
resetAspect(widgetMaxWidth);
|
||||
resetAspect(widgetMinHeight);
|
||||
resetAspect(widgetMaxHeight);
|
||||
resetAspect(systemPrompt);
|
||||
resetAspect(useOpenFilesInQuickRefactor);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
class QuickRefactorSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
QuickRefactorSettingsPage()
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID);
|
||||
setDisplayName(Tr::tr("Quick Refactor"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setSettingsProvider([] { return &quickRefactorSettings(); });
|
||||
}
|
||||
};
|
||||
|
||||
const QuickRefactorSettingsPage quickRefactorSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
78
sources/settings/QuickRefactorSettings.hpp
Normal file
78
sources/settings/QuickRefactorSettings.hpp
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ButtonAspect.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class QuickRefactorSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
QuickRefactorSettings();
|
||||
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
// General Parameters Settings
|
||||
Utils::DoubleAspect temperature{this};
|
||||
Utils::IntegerAspect maxTokens{this};
|
||||
|
||||
// Advanced Parameters
|
||||
Utils::BoolAspect useTopP{this};
|
||||
Utils::DoubleAspect topP{this};
|
||||
|
||||
Utils::BoolAspect useTopK{this};
|
||||
Utils::IntegerAspect topK{this};
|
||||
|
||||
Utils::BoolAspect usePresencePenalty{this};
|
||||
Utils::DoubleAspect presencePenalty{this};
|
||||
|
||||
Utils::BoolAspect useFrequencyPenalty{this};
|
||||
Utils::DoubleAspect frequencyPenalty{this};
|
||||
|
||||
// Ollama Settings
|
||||
Utils::StringAspect ollamaLivetime{this};
|
||||
Utils::IntegerAspect contextWindow{this};
|
||||
|
||||
// Tools Settings
|
||||
Utils::BoolAspect useTools{this};
|
||||
|
||||
// Thinking Settings
|
||||
Utils::BoolAspect useThinking{this};
|
||||
Utils::IntegerAspect thinkingBudgetTokens{this};
|
||||
Utils::IntegerAspect thinkingMaxTokens{this};
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
Utils::SelectionAspect openAIResponsesReasoningEffort{this};
|
||||
|
||||
// Context Settings
|
||||
Utils::BoolAspect readFullFile{this};
|
||||
Utils::BoolAspect readFileParts{this};
|
||||
Utils::IntegerAspect readStringsBeforeCursor{this};
|
||||
Utils::IntegerAspect readStringsAfterCursor{this};
|
||||
|
||||
// Display Settings
|
||||
Utils::SelectionAspect displayMode{this};
|
||||
Utils::SelectionAspect widgetOrientation{this};
|
||||
Utils::IntegerAspect widgetMinWidth{this};
|
||||
Utils::IntegerAspect widgetMaxWidth{this};
|
||||
Utils::IntegerAspect widgetMinHeight{this};
|
||||
Utils::IntegerAspect widgetMaxHeight{this};
|
||||
|
||||
// Prompt Settings
|
||||
Utils::StringAspect systemPrompt{this};
|
||||
Utils::BoolAspect useOpenFilesInQuickRefactor{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
};
|
||||
|
||||
QuickRefactorSettings &quickRefactorSettings();
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
262
sources/settings/SettingsConstants.hpp
Normal file
262
sources/settings/SettingsConstants.hpp
Normal file
@@ -0,0 +1,262 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace QodeAssist::Constants {
|
||||
|
||||
// project settings
|
||||
|
||||
const char QODE_ASSIST_PROJECT_SETTINGS_ID[] = "QodeAssist.ProjectSettings";
|
||||
const char QODE_ASSIST_USE_GLOBAL_SETTINGS[] = "QodeAssist.UseGlobalSettings";
|
||||
const char QODE_ASSIST_ENABLE_IN_PROJECT[] = "QodeAssist.EnableInProject";
|
||||
const char QODE_ASSIST_CHAT_HISTORY_PATH[] = "QodeAssist.ChatHistoryPath";
|
||||
|
||||
// new settings
|
||||
const char CC_PROVIDER[] = "QodeAssist.ccProvider";
|
||||
const char CC_MODEL[] = "QodeAssist.ccModel";
|
||||
const char CC_MODEL_HISTORY[] = "QodeAssist.ccModelHistory";
|
||||
const char CC_TEMPLATE[] = "QodeAssist.ccTemplate";
|
||||
const char CC_URL[] = "QodeAssist.ccUrl";
|
||||
const char CC_URL_HISTORY[] = "QodeAssist.ccUrlHistory";
|
||||
const char CC_CUSTOM_ENDPOINT[] = "QodeAssist.ccCustomEndpoint";
|
||||
const char CC_CUSTOM_ENDPOINT_HISTORY[] = "QodeAssist.ccCustomEndpointHistory";
|
||||
|
||||
const char CA_PROVIDER[] = "QodeAssist.caProvider";
|
||||
const char CA_MODEL[] = "QodeAssist.caModel";
|
||||
const char CA_MODEL_HISTORY[] = "QodeAssist.caModelHistory";
|
||||
const char CA_TEMPLATE[] = "QodeAssist.caTemplate";
|
||||
const char CA_URL[] = "QodeAssist.caUrl";
|
||||
const char CA_URL_HISTORY[] = "QodeAssist.caUrlHistory";
|
||||
const char CA_CUSTOM_ENDPOINT[] = "QodeAssist.caCustomEndpoint";
|
||||
const char CA_CUSTOM_ENDPOINT_HISTORY[] = "QodeAssist.caCustomEndpointHistory";
|
||||
|
||||
// quick refactor settings
|
||||
const char QR_PROVIDER[] = "QodeAssist.qrProvider";
|
||||
const char QR_MODEL[] = "QodeAssist.qrModel";
|
||||
const char QR_MODEL_HISTORY[] = "QodeAssist.qrModelHistory";
|
||||
const char QR_TEMPLATE[] = "QodeAssist.qrTemplate";
|
||||
const char QR_URL[] = "QodeAssist.qrUrl";
|
||||
const char QR_URL_HISTORY[] = "QodeAssist.qrUrlHistory";
|
||||
const char QR_CUSTOM_ENDPOINT[] = "QodeAssist.qrCustomEndpoint";
|
||||
const char QR_CUSTOM_ENDPOINT_HISTORY[] = "QodeAssist.qrCustomEndpointHistory";
|
||||
|
||||
const char CC_SPECIFY_PRESET1[] = "QodeAssist.ccSpecifyPreset1";
|
||||
const char CC_PRESET1_LANGUAGE[] = "QodeAssist.ccPreset1Language";
|
||||
const char CC_PRESET1_PROVIDER[] = "QodeAssist.ccPreset1Provider";
|
||||
const char CC_PRESET1_MODEL[] = "QodeAssist.ccPreset1Model";
|
||||
const char CC_PRESET1_MODEL_HISTORY[] = "QodeAssist.ccPreset1ModelHistory";
|
||||
const char CC_PRESET1_TEMPLATE[] = "QodeAssist.ccPreset1Template";
|
||||
const char CC_PRESET1_URL[] = "QodeAssist.ccPreset1Url";
|
||||
const char CC_PRESET1_URL_HISTORY[] = "QodeAssist.ccPreset1UrlHistory";
|
||||
const char CC_PRESET1_CUSTOM_ENDPOINT[] = "QodeAssist.ccPreset1CustomEndpoint";
|
||||
const char CC_PRESET1_CUSTOM_ENDPOINT_HISTORY[] = "QodeAssist.ccPreset1CustomEndpointHistory";
|
||||
|
||||
// settings
|
||||
const char ENABLE_QODE_ASSIST[] = "QodeAssist.enableQodeAssist";
|
||||
const char CC_AUTO_COMPLETION[] = "QodeAssist.ccAutoCompletion";
|
||||
const char CC_SHOW_PROGRESS_WIDGET[] = "QodeAssist.ccShowProgressWidget";
|
||||
const char CC_USE_OPEN_FILES_CONTEXT[] = "QodeAssist.ccUseOpenFilesContext";
|
||||
const char ENABLE_LOGGING[] = "QodeAssist.enableLogging";
|
||||
const char ENABLE_CHECK_UPDATE[] = "QodeAssist.enableCheckUpdate";
|
||||
const char REQUEST_TIMEOUT[] = "QodeAssist.requestTimeout";
|
||||
|
||||
const char PROVIDER_PATHS[] = "QodeAssist.providerPaths";
|
||||
const char СС_START_SUGGESTION_TIMER[] = "QodeAssist.startSuggestionTimer";
|
||||
const char СС_AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCharThreshold";
|
||||
const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval";
|
||||
const char CC_COMPLETION_TRIGGER_MODE[] = "QodeAssist.ccCompletionTriggerMode";
|
||||
const char CC_COMPLETION_MODE[] = "QodeAssist.ccCompletionMode";
|
||||
const char CC_SMART_CONTEXT_TRIGGER[] = "QodeAssist.ccSmartContextTrigger";
|
||||
const char CC_RESPECT_QTC_POPUP[] = "QodeAssist.ccRespectQtcPopup";
|
||||
const char CC_CANCEL_ON_INPUT[] = "QodeAssist.ccCancelOnInput";
|
||||
const char CC_HINT_CHAR_THRESHOLD[] = "QodeAssist.ccHintCharThreshold";
|
||||
const char CC_HINT_HIDE_TIMEOUT[] = "QodeAssist.ccHintHideTimeout";
|
||||
const char CC_HINT_TRIGGER_KEY[] = "QodeAssist.ccHintTriggerKey";
|
||||
const char CC_ABORT_ASSIST_ON_REQUEST[] = "QodeAssist.ccAbortAssistOnRequest";
|
||||
const char CC_IGNORE_WHITESPACE_IN_CHAR_COUNT[] = "QodeAssist.ccIgnoreWhitespaceInCharCount";
|
||||
const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold";
|
||||
const char CC_MULTILINE_COMPLETION[] = "QodeAssist.ccMultilineCompletion";
|
||||
const char CC_MODEL_OUTPUT_HANDLER[] = "QodeAssist.ccModelOutputHandler";
|
||||
const char CA_AUTO_APPLY_FILE_EDITS[] = "QodeAssist.caAutoApplyFileEdits";
|
||||
const char CA_AUTO_COMPRESS[] = "QodeAssist.caAutoCompress";
|
||||
const char CA_AUTO_COMPRESS_THRESHOLD[] = "QodeAssist.caAutoCompressThreshold";
|
||||
const char CA_LINK_OPEN_FILES[] = "QodeAssist.caLinkOpenFiles";
|
||||
const char CA_AUTOSAVE[] = "QodeAssist.caAutosave";
|
||||
const char CC_CUSTOM_LANGUAGES[] = "QodeAssist.ccCustomLanguages";
|
||||
|
||||
const char CA_ENABLE_CHAT_IN_BOTTOM_TOOLBAR[] = "QodeAssist.caEnableChatInBottomToolbar";
|
||||
const char CA_ENABLE_CHAT_IN_NAVIGATION_PANEL[] = "QodeAssist.caEnableChatInNavigationPanel";
|
||||
const char CA_ENABLE_CHAT_TOOLS[] = "QodeAssist.caEnableChatTools";
|
||||
const char CA_USE_TOOLS[] = "QodeAssist.caUseTools";
|
||||
const char TOOLS_MAX_CONTINUATIONS[] = "QodeAssist.toolsMaxContinuations";
|
||||
const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject";
|
||||
const char CA_ENABLE_LIST_PROJECT_FILES_TOOL[] = "QodeAssist.caEnableListProjectFilesTool";
|
||||
const char CA_ENABLE_FIND_FILE_TOOL[] = "QodeAssist.caEnableFindFileTool";
|
||||
const char CA_ENABLE_READ_FILE_TOOL[] = "QodeAssist.caEnableReadFileTool";
|
||||
const char CA_ENABLE_PROJECT_SEARCH_TOOL[] = "QodeAssist.caEnableProjectSearchTool";
|
||||
const char CA_ENABLE_CREATE_NEW_FILE_TOOL[] = "QodeAssist.caEnableCreateNewFileTool";
|
||||
const char CA_ENABLE_GET_ISSUES_LIST_TOOL[] = "QodeAssist.caEnableGetIssuesListTool";
|
||||
const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileToolV2";
|
||||
const char CA_ENABLE_BUILD_PROJECT_TOOL[] = "QodeAssist.caEnableBuildProjectToolV2";
|
||||
const char CA_ENABLE_TERMINAL_COMMAND_TOOL[] = "QodeAssist.caEnableTerminalCommandToolV2";
|
||||
const char CA_ENABLE_TODO_TOOL[] = "QodeAssist.caEnableTodoToolV2";
|
||||
const char CA_ENABLE_READ_ORIGINAL_HISTORY_TOOL[]
|
||||
= "QodeAssist.caEnableReadOriginalHistoryTool";
|
||||
const char CA_ENABLE_SKILL_TOOL[] = "QodeAssist.caEnableSkillTool";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS[] = "QodeAssist.caAllowedTerminalCommands";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS_LINUX[] = "QodeAssist.caAllowedTerminalCommandsLinux";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS_MACOS[] = "QodeAssist.caAllowedTerminalCommandsMacOS";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS[] = "QodeAssist.caAllowedTerminalCommandsWindows";
|
||||
const char CA_TERMINAL_COMMAND_TIMEOUT[] = "QodeAssist.caTerminalCommandTimeout";
|
||||
|
||||
// Skills settings
|
||||
const char SK_ENABLE_SKILLS[] = "QodeAssist.skEnableSkills";
|
||||
const char SK_GLOBAL_SKILL_ROOTS[] = "QodeAssist.skGlobalSkillRoots";
|
||||
const char SK_PROJECT_SKILL_DIRS[] = "QodeAssist.skProjectSkillDirs";
|
||||
|
||||
// MCP server settings
|
||||
const char MCP_ENABLE_SERVER[] = "QodeAssist.mcpEnableServer";
|
||||
const char MCP_SERVER_PORT[] = "QodeAssist.mcpServerPort";
|
||||
const char MCP_ENABLE_CLIENTS[] = "QodeAssist.mcpEnableClients";
|
||||
const char MCP_CLIENT_EXTRA_PATHS[] = "QodeAssist.mcpClientExtraPaths";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
||||
const char QODE_ASSIST_CODE_COMPLETION_SETTINGS_PAGE_ID[]
|
||||
= "QodeAssist.2CodeCompletionSettingsPageId";
|
||||
const char QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID[]
|
||||
= "QodeAssist.3ChatAssistantSettingsPageId";
|
||||
const char QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID[]
|
||||
= "QodeAssist.4QuickRefactorSettingsPageId";
|
||||
const char QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID[] = "QodeAssist.5ToolsSettingsPageId";
|
||||
const char QODE_ASSIST_MCP_SETTINGS_PAGE_ID[] = "QodeAssist.6McpSettingsPageId";
|
||||
const char QODE_ASSIST_SKILLS_SETTINGS_PAGE_ID[] = "QodeAssist.8SkillsSettingsPageId";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist";
|
||||
|
||||
// Provider Settings Page ID
|
||||
const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.7ProviderSettingsPageId";
|
||||
|
||||
// Provider API Keys
|
||||
const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey";
|
||||
const char OPEN_ROUTER_API_KEY_HISTORY[] = "QodeAssist.openRouterApiKeyHistory";
|
||||
const char OPEN_AI_COMPAT_API_KEY[] = "QodeAssist.openAiCompatApiKey";
|
||||
const char OPEN_AI_COMPAT_API_KEY_HISTORY[] = "QodeAssist.openAiCompatApiKeyHistory";
|
||||
const char CLAUDE_API_KEY[] = "QodeAssist.claudeApiKey";
|
||||
const char CLAUDE_API_KEY_HISTORY[] = "QodeAssist.claudeApiKeyHistory";
|
||||
const char OPEN_AI_API_KEY[] = "QodeAssist.openAiApiKey";
|
||||
const char OPEN_AI_API_KEY_HISTORY[] = "QodeAssist.openAiApiKeyHistory";
|
||||
const char MISTRAL_AI_API_KEY[] = "QodeAssist.mistralAiApiKey";
|
||||
const char MISTRAL_AI_API_KEY_HISTORY[] = "QodeAssist.mistralAiApiKeyHistory";
|
||||
const char CODESTRAL_API_KEY[] = "QodeAssist.codestralApiKey";
|
||||
const char CODESTRAL_API_KEY_HISTORY[] = "QodeAssist.codestralApiKeyHistory";
|
||||
const char GOOGLE_AI_API_KEY[] = "QodeAssist.googleAiApiKey";
|
||||
const char GOOGLE_AI_API_KEY_HISTORY[] = "QodeAssist.googleAiApiKeyHistory";
|
||||
const char OLLAMA_BASIC_AUTH_API_KEY[] = "QodeAssist.ollamaBasicAuthApiKey";
|
||||
const char OLLAMA_BASIC_AUTH_API_KEY_HISTORY[] = "QodeAssist.ollamaBasicAuthApiKeyHistory";
|
||||
const char LLAMA_CPP_API_KEY[] = "QodeAssist.llamaCppApiKey";
|
||||
const char LLAMA_CPP_API_KEY_HISTORY[] = "QodeAssist.llamaCppApiKeyHistory";
|
||||
const char QWEN_API_KEY[] = "QodeAssist.qwenApiKey";
|
||||
const char QWEN_API_KEY_HISTORY[] = "QodeAssist.qwenApiKeyHistory";
|
||||
const char DEEPSEEK_API_KEY[] = "QodeAssist.deepSeekApiKey";
|
||||
const char DEEPSEEK_API_KEY_HISTORY[] = "QodeAssist.deepSeekApiKeyHistory";
|
||||
|
||||
const char CLAUDE_ENABLE_PROMPT_CACHING[] = "QodeAssist.claudeEnablePromptCaching";
|
||||
const char CLAUDE_USE_EXTENDED_CACHE_TTL[] = "QodeAssist.claudeUseExtendedCacheTTL";
|
||||
|
||||
// context settings
|
||||
const char CC_READ_FULL_FILE[] = "QodeAssist.ccReadFullFile";
|
||||
const char CC_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.ccReadStringsBeforeCursor";
|
||||
const char CC_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.ccReadStringsAfterCursor";
|
||||
const char CC_USE_SYSTEM_PROMPT[] = "QodeAssist.ccUseSystemPrompt";
|
||||
const char CC_SYSTEM_PROMPT[] = "QodeAssist.ccSystemPrompt";
|
||||
const char CC_SYSTEM_PROMPT_FOR_NON_FIM[] = "QodeAssist.ccSystemPromptForNonFim";
|
||||
const char CC_USE_USER_TEMPLATE[] = "QodeAssist.ccUseUserTemplate";
|
||||
const char CC_USER_TEMPLATE[] = "QodeAssist.ccUserTemplate";
|
||||
const char CC_USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.ccUseProjectChangesCache";
|
||||
const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize";
|
||||
const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt";
|
||||
const char CA_SYSTEM_PROMPT[] = "QodeAssist.chatSystemPrompt";
|
||||
|
||||
// preset prompt settings
|
||||
const char CC_TEMPERATURE[] = "QodeAssist.ccTemperature";
|
||||
const char CC_MAX_TOKENS[] = "QodeAssist.ccMaxTokens";
|
||||
const char CC_USE_TOP_P[] = "QodeAssist.ccUseTopP";
|
||||
const char CC_TOP_P[] = "QodeAssist.ccTopP";
|
||||
const char CC_USE_TOP_K[] = "QodeAssist.ccUseTopK";
|
||||
const char CC_TOP_K[] = "QodeAssist.ccTopK";
|
||||
const char CC_USE_PRESENCE_PENALTY[] = "QodeAssist.ccUsePresencePenalty";
|
||||
const char CC_PRESENCE_PENALTY[] = "QodeAssist.ccPresencePenalty";
|
||||
const char CC_USE_FREQUENCY_PENALTY[] = "QodeAssist.fimUseFrequencyPenalty";
|
||||
const char CC_FREQUENCY_PENALTY[] = "QodeAssist.fimFrequencyPenalty";
|
||||
const char CC_OLLAMA_LIVETIME[] = "QodeAssist.fimOllamaLivetime";
|
||||
const char CC_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.ccOllamaContextWindow";
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
const char CC_OPENAI_RESPONSES_REASONING_EFFORT[] = "QodeAssist.ccOpenAIResponsesReasoningEffort";
|
||||
|
||||
const char CA_TEMPERATURE[] = "QodeAssist.chatTemperature";
|
||||
const char CA_MAX_TOKENS[] = "QodeAssist.chatMaxTokens";
|
||||
const char CA_USE_TOP_P[] = "QodeAssist.chatUseTopP";
|
||||
const char CA_TOP_P[] = "QodeAssist.chatTopP";
|
||||
const char CA_USE_TOP_K[] = "QodeAssist.chatUseTopK";
|
||||
const char CA_TOP_K[] = "QodeAssist.chatTopK";
|
||||
const char CA_USE_PRESENCE_PENALTY[] = "QodeAssist.chatUsePresencePenalty";
|
||||
const char CA_PRESENCE_PENALTY[] = "QodeAssist.chatPresencePenalty";
|
||||
const char CA_USE_FREQUENCY_PENALTY[] = "QodeAssist.chatUseFrequencyPenalty";
|
||||
const char CA_FREQUENCY_PENALTY[] = "QodeAssist.chatFrequencyPenalty";
|
||||
const char CA_OLLAMA_LIVETIME[] = "QodeAssist.chatOllamaLivetime";
|
||||
const char CA_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.caOllamaContextWindow";
|
||||
const char CA_ENABLE_THINKING_MODE[] = "QodeAssist.caEnableThinkingMode";
|
||||
const char CA_THINKING_BUDGET_TOKENS[] = "QodeAssist.caThinkingBudgetTokens";
|
||||
const char CA_THINKING_MAX_TOKENS[] = "QodeAssist.caThinkingMaxTokens";
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
const char CA_OPENAI_RESPONSES_REASONING_EFFORT[] = "QodeAssist.caOpenAIResponsesReasoningEffort";
|
||||
|
||||
const char CA_TEXT_FONT_FAMILY[] = "QodeAssist.caTextFontFamily";
|
||||
const char CA_TEXT_FONT_SIZE[] = "QodeAssist.caTextFontSize";
|
||||
const char CA_CODE_FONT_FAMILY[] = "QodeAssist.caCodeFontFamily";
|
||||
const char CA_CODE_FONT_SIZE[] = "QodeAssist.caCodeFontSize";
|
||||
const char CA_TEXT_FORMAT[] = "QodeAssist.caTextFormat";
|
||||
const char CA_CHAT_RENDERER[] = "QodeAssist.caChatRenderer";
|
||||
|
||||
const char CA_LAST_USED_ROLE[] = "QodeAssist.caLastUsedRole";
|
||||
|
||||
// quick refactor preset prompt settings
|
||||
const char QR_TEMPERATURE[] = "QodeAssist.qrTemperature";
|
||||
const char QR_MAX_TOKENS[] = "QodeAssist.qrMaxTokens";
|
||||
const char QR_USE_TOP_P[] = "QodeAssist.qrUseTopP";
|
||||
const char QR_TOP_P[] = "QodeAssist.qrTopP";
|
||||
const char QR_USE_TOP_K[] = "QodeAssist.qrUseTopK";
|
||||
const char QR_TOP_K[] = "QodeAssist.qrTopK";
|
||||
const char QR_USE_PRESENCE_PENALTY[] = "QodeAssist.qrUsePresencePenalty";
|
||||
const char QR_PRESENCE_PENALTY[] = "QodeAssist.qrPresencePenalty";
|
||||
const char QR_USE_FREQUENCY_PENALTY[] = "QodeAssist.qrUseFrequencyPenalty";
|
||||
const char QR_FREQUENCY_PENALTY[] = "QodeAssist.qrFrequencyPenalty";
|
||||
const char QR_OLLAMA_LIVETIME[] = "QodeAssist.qrOllamaLivetime";
|
||||
const char QR_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.qrOllamaContextWindow";
|
||||
const char QR_USE_TOOLS[] = "QodeAssist.qrUseTools";
|
||||
const char QR_USE_THINKING[] = "QodeAssist.qrUseThinking";
|
||||
const char QR_THINKING_BUDGET_TOKENS[] = "QodeAssist.qrThinkingBudgetTokens";
|
||||
const char QR_THINKING_MAX_TOKENS[] = "QodeAssist.qrThinkingMaxTokens";
|
||||
|
||||
// OpenAI Responses API Settings
|
||||
const char QR_OPENAI_RESPONSES_REASONING_EFFORT[] = "QodeAssist.qrOpenAIResponsesReasoningEffort";
|
||||
|
||||
const char QR_READ_FULL_FILE[] = "QodeAssist.qrReadFullFile";
|
||||
const char QR_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.qrReadStringsBeforeCursor";
|
||||
const char QR_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.qrReadStringsAfterCursor";
|
||||
const char QR_SYSTEM_PROMPT[] = "QodeAssist.qrSystemPrompt";
|
||||
const char QR_USE_OPEN_FILES_IN_QUICK_REFACTOR[] = "QodeAssist.qrUseOpenFilesInQuickRefactor";
|
||||
const char QR_DISPLAY_MODE[] = "QodeAssist.qrDisplayMode";
|
||||
const char QR_WIDGET_ORIENTATION[] = "QodeAssist.qrWidgetOrientation";
|
||||
const char QR_WIDGET_MIN_WIDTH[] = "QodeAssist.qrWidgetMinWidth";
|
||||
const char QR_WIDGET_MAX_WIDTH[] = "QodeAssist.qrWidgetMaxWidth";
|
||||
const char QR_WIDGET_MIN_HEIGHT[] = "QodeAssist.qrWidgetMinHeight";
|
||||
const char QR_WIDGET_MAX_HEIGHT[] = "QodeAssist.qrWidgetMaxHeight";
|
||||
|
||||
} // namespace QodeAssist::Constants
|
||||
74
sources/settings/SettingsDialog.cpp
Normal file
74
sources/settings/SettingsDialog.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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 "SettingsDialog.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
SettingsDialog::SettingsDialog(const QString &title, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_mainLayout(new QVBoxLayout(this))
|
||||
, m_buttonLayout(nullptr)
|
||||
{
|
||||
setWindowTitle(title);
|
||||
m_mainLayout->setSizeConstraint(QLayout::SetMinAndMaxSize);
|
||||
}
|
||||
|
||||
QLabel *SettingsDialog::addLabel(const QString &text)
|
||||
{
|
||||
auto label = new QLabel(text, this);
|
||||
label->setWordWrap(true);
|
||||
label->setMinimumWidth(300);
|
||||
m_mainLayout->addWidget(label);
|
||||
return label;
|
||||
}
|
||||
|
||||
QLineEdit *SettingsDialog::addInputField(const QString &labelText, const QString &value)
|
||||
{
|
||||
auto inputLayout = new QGridLayout;
|
||||
auto inputLabel = new QLabel(labelText, this);
|
||||
auto inputField = new QLineEdit(value, this);
|
||||
inputField->setMinimumWidth(200);
|
||||
|
||||
inputLayout->addWidget(inputLabel, 0, 0);
|
||||
inputLayout->addWidget(inputField, 0, 1);
|
||||
inputLayout->setColumnStretch(1, 1);
|
||||
m_mainLayout->addLayout(inputLayout);
|
||||
|
||||
return inputField;
|
||||
}
|
||||
|
||||
void SettingsDialog::addSpacing(int space)
|
||||
{
|
||||
m_mainLayout->addSpacing(space);
|
||||
}
|
||||
|
||||
QHBoxLayout *SettingsDialog::buttonLayout()
|
||||
{
|
||||
if (!m_buttonLayout) {
|
||||
m_buttonLayout = new QHBoxLayout;
|
||||
m_buttonLayout->addStretch();
|
||||
m_mainLayout->addLayout(m_buttonLayout);
|
||||
}
|
||||
return m_buttonLayout;
|
||||
}
|
||||
|
||||
QComboBox *SettingsDialog::addComboBox(
|
||||
const QStringList &items, const QString ¤tText, bool editable)
|
||||
{
|
||||
auto comboBox = new QComboBox(this);
|
||||
comboBox->addItems(items);
|
||||
comboBox->setCurrentText(currentText);
|
||||
comboBox->setMinimumWidth(300);
|
||||
comboBox->setEditable(editable);
|
||||
m_mainLayout->addWidget(comboBox);
|
||||
return comboBox;
|
||||
}
|
||||
|
||||
QVBoxLayout *SettingsDialog::mainLayout() const
|
||||
{
|
||||
return m_mainLayout;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
35
sources/settings/SettingsDialog.hpp
Normal file
35
sources/settings/SettingsDialog.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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 <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QWidget>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class SettingsDialog : public QDialog
|
||||
{
|
||||
public:
|
||||
explicit SettingsDialog(const QString &title, QWidget *parent = Core::ICore::dialogParent());
|
||||
|
||||
QLabel *addLabel(const QString &text);
|
||||
QLineEdit *addInputField(const QString &labelText, const QString &value);
|
||||
void addSpacing(int space = 12);
|
||||
QHBoxLayout *buttonLayout();
|
||||
QVBoxLayout *mainLayout() const;
|
||||
|
||||
QComboBox *addComboBox(
|
||||
const QStringList &items, const QString ¤tText = QString(), bool editable = true);
|
||||
|
||||
private:
|
||||
QVBoxLayout *m_mainLayout;
|
||||
QHBoxLayout *m_buttonLayout;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
119
sources/settings/SettingsTr.hpp
Normal file
119
sources/settings/SettingsTr.hpp
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
namespace TrConstants {
|
||||
inline const char *ENABLE_QODE_ASSIST = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enable QodeAssist");
|
||||
inline const char *GENERAL = QT_TRANSLATE_NOOP("QtC::QodeAssist", "General");
|
||||
inline const char *RESET_TO_DEFAULTS
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Reset Page to Defaults");
|
||||
inline const char *CHECK_UPDATE = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Check Update");
|
||||
inline const char *SELECT = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Select...");
|
||||
inline const char *PROVIDER = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Provider:");
|
||||
inline const char *MODEL = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Model:");
|
||||
inline const char *TEMPLATE = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Template:");
|
||||
inline const char *URL = QT_TRANSLATE_NOOP("QtC::QodeAssist", "URL:");
|
||||
inline const char *STATUS = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Status:");
|
||||
inline const char *TEST = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Test");
|
||||
inline const char *ENABLE_LOG = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enable Logging");
|
||||
inline const char *ENABLE_LOG_TOOLTIP
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Log messages are visible in General Messages pane");
|
||||
inline const char *ENABLE_CHECK_UPDATE_ON_START
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Check for updates when Qt Creator starts");
|
||||
inline const char *ENABLE_CHAT = QT_TRANSLATE_NOOP(
|
||||
"QtC::QodeAssist",
|
||||
"Enable Chat(If you have performance issues try disabling this, need restart QtC)");
|
||||
inline const char *CUSTOM_ENDPOINT = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Custom endpoint:");
|
||||
|
||||
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 *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 *CONFIRMATION = QT_TRANSLATE_NOOP(
|
||||
"QtC::QodeAssist", "Are you sure you want to reset all settings to default values?");
|
||||
inline const char *CURRENT_TEMPLATE_DESCRIPTION
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Current template description:");
|
||||
|
||||
inline const char CONNECTION_ERROR[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Connection Error");
|
||||
inline const char NO_MODELS_FOUND[]
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Unable to retrieve the list of models from the server.");
|
||||
inline const char CHECK_CONNECTION[] = QT_TRANSLATE_NOOP(
|
||||
"QtC::QodeAssist",
|
||||
"Please verify the following:\n"
|
||||
"- Server is running and accessible\n"
|
||||
"- URL is correct\n"
|
||||
"- Provider is properly configured\n"
|
||||
"- API key is correctly set (if required)\n\n"
|
||||
"You can try selecting a different provider or changing the URL:");
|
||||
inline const char SELECT_PROVIDER[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Select Provider");
|
||||
inline const char SELECT_URL[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Select URL");
|
||||
inline const char CLOSE[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Close");
|
||||
inline const char MODEL_SELECTION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Model Selection");
|
||||
inline const char MODEL_LISTING_NOT_SUPPORTED_INFO[] = QT_TRANSLATE_NOOP(
|
||||
"QtC::QodeAssist",
|
||||
"Select from previously used models or enter a new model name.\n\n"
|
||||
"If entering a new model name:\n"
|
||||
"• For providers with automatic listing - ensure the model is installed\n"
|
||||
"• For providers without listing support - check provider's documentation\n"
|
||||
"• Make sure the model name matches exactly");
|
||||
inline const char MODEL_NAME[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Model name:");
|
||||
inline const char OK[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "OK");
|
||||
inline const char CANCEL[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Cancel");
|
||||
inline const char ENTER_MODEL_MANUALLY[]
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter Model Manually");
|
||||
inline const char CONFIGURE_API_KEY[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configure API Key");
|
||||
inline const char URL_SELECTION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "URL Selection");
|
||||
inline const char URL_SELECTION_INFO[] = QT_TRANSLATE_NOOP(
|
||||
"QtC::QodeAssist",
|
||||
"Select from the list of default and previously used URLs, or enter a custom one.\n"
|
||||
"Please ensure the selected URL is accessible and the service is running.");
|
||||
inline const char PREDEFINED_URL[]
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Use default provider URL or from history");
|
||||
inline const char CUSTOM_URL[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter custom URL");
|
||||
inline const char ENTER_MODEL_MANUALLY_BUTTON[]
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter Model Name Manually");
|
||||
inline const char AUTO_COMPLETION_SETTINGS[]
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Auto Completion Settings");
|
||||
|
||||
inline const char ADD_NEW_PRESET[]
|
||||
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Add new preset for language");
|
||||
|
||||
inline const char SAVE_CONFIG[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Save Config...");
|
||||
inline const char LOAD_CONFIG[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Load Config...");
|
||||
inline const char OPEN_CONFIG_FOLDER[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Open Folder");
|
||||
inline const char SAVE_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Save Configuration");
|
||||
inline const char LOAD_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Load Configuration");
|
||||
inline const char CONFIGURATION_NAME[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configuration name:");
|
||||
inline const char SELECT_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Select Configuration");
|
||||
inline const char NO_CONFIGURATIONS_FOUND[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "No saved configurations found.");
|
||||
inline const char CONFIGURATION_SAVED[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configuration saved successfully.");
|
||||
inline const char CONFIGURATION_LOADED[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configuration loaded successfully.");
|
||||
inline const char DELETE_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Delete");
|
||||
inline const char CONFIRM_DELETE_CONFIG[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Are you sure you want to delete this configuration?");
|
||||
|
||||
} // namespace TrConstants
|
||||
|
||||
struct Tr
|
||||
{
|
||||
Q_DECLARE_TR_FUNCTIONS(QtC::QodeAssist)
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
67
sources/settings/SettingsUtils.hpp
Normal file
67
sources/settings/SettingsUtils.hpp
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QCoreApplication>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPushButton>
|
||||
#include <QtCore/qtimer.h>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
inline bool pingUrl(const QUrl &url, int timeout = 5000)
|
||||
{
|
||||
if (!url.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QNetworkAccessManager manager;
|
||||
QNetworkRequest request(url);
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, true);
|
||||
|
||||
QScopedPointer<QNetworkReply> reply(manager.get(request));
|
||||
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply.data(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
|
||||
timer.start(timeout);
|
||||
loop.exec();
|
||||
|
||||
if (timer.isActive()) {
|
||||
timer.stop();
|
||||
return (reply->error() == QNetworkReply::NoError);
|
||||
} else {
|
||||
QObject::disconnect(reply.data(), &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
reply->abort();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename AspectType>
|
||||
void resetAspect(AspectType &aspect)
|
||||
{
|
||||
aspect.setVolatileValue(aspect.defaultValue());
|
||||
}
|
||||
|
||||
inline void initStringAspect(
|
||||
Utils::StringAspect &aspect,
|
||||
const Utils::Key &settingsKey,
|
||||
const QString &labelText,
|
||||
const QString &defaultValue)
|
||||
{
|
||||
aspect.setSettingsKey(settingsKey);
|
||||
aspect.setLabelText(labelText);
|
||||
aspect.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
aspect.setDefaultValue(defaultValue);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
136
sources/settings/SkillsSettings.cpp
Normal file
136
sources/settings/SkillsSettings.cpp
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "SkillsSettings.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "skills/SkillsLoader.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
SkillsSettings &skillsSettings()
|
||||
{
|
||||
static SkillsSettings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
QStringList SkillsSettings::splitLines(const QString &value)
|
||||
{
|
||||
QStringList lines;
|
||||
const auto parts = value.split('\n', Qt::SkipEmptyParts);
|
||||
for (const QString &part : parts) {
|
||||
const QString trimmed = part.trimmed();
|
||||
if (!trimmed.isEmpty())
|
||||
lines << trimmed;
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
QStringList SkillsSettings::splitPaths(const QString &value)
|
||||
{
|
||||
QStringList paths;
|
||||
for (QString path : splitLines(value)) {
|
||||
if (path == QLatin1String("~"))
|
||||
path = QDir::homePath();
|
||||
else if (path.startsWith(QLatin1String("~/")))
|
||||
path = QDir::homePath() + path.mid(1);
|
||||
paths << QDir::cleanPath(path);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
SkillsSettings::SkillsSettings()
|
||||
{
|
||||
setAutoApply(false);
|
||||
|
||||
setDisplayName(Tr::tr("Skills"));
|
||||
|
||||
enableSkills.setSettingsKey(Constants::SK_ENABLE_SKILLS);
|
||||
enableSkills.setLabelText(Tr::tr("Enable skills"));
|
||||
enableSkills.setToolTip(
|
||||
Tr::tr("Discover Agent Skills from the configured skill directories and expose them "
|
||||
"to the chat assistant. Each skill is a folder containing a SKILL.md file."));
|
||||
enableSkills.setDefaultValue(true);
|
||||
|
||||
const QString defaultGlobalRoots
|
||||
= Core::ICore::userResourcePath().toFSPathString() + "/qodeassist/skills\n"
|
||||
+ QDir::homePath() + "/.claude/skills";
|
||||
|
||||
globalSkillRoots.setSettingsKey(Constants::SK_GLOBAL_SKILL_ROOTS);
|
||||
globalSkillRoots.setLabelText(Tr::tr("Global skill directories:"));
|
||||
globalSkillRoots.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
globalSkillRoots.setToolTip(
|
||||
Tr::tr("Absolute paths scanned for skills, one per line. Each path is a directory "
|
||||
"whose subfolders contain SKILL.md files. A leading ~ expands to your home "
|
||||
"directory. Lets QodeAssist pick up skills shared with other agents "
|
||||
"(e.g. ~/.claude/skills)."));
|
||||
globalSkillRoots.setDefaultValue(defaultGlobalRoots);
|
||||
|
||||
readSettings();
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
auto skillsList = new QListWidget;
|
||||
skillsList->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
skillsList->setMaximumHeight(160);
|
||||
|
||||
auto refreshSkills = [skillsList, this] {
|
||||
skillsList->clear();
|
||||
const QVector<Skills::AgentSkill> skills
|
||||
= Skills::SkillsLoader::scan(splitPaths(globalSkillRoots()));
|
||||
for (const Skills::AgentSkill &skill : skills) {
|
||||
auto *item = new QListWidgetItem(
|
||||
QStringLiteral("%1 — %2").arg(skill.name, skill.description), skillsList);
|
||||
item->setToolTip(skill.skillDir);
|
||||
}
|
||||
if (skills.isEmpty())
|
||||
new QListWidgetItem(Tr::tr("No skills discovered."), skillsList);
|
||||
};
|
||||
refreshSkills();
|
||||
connect(&globalSkillRoots, &Utils::BaseAspect::changed, skillsList, refreshSkills);
|
||||
|
||||
return Column{
|
||||
Group{
|
||||
title(Tr::tr("Skills")),
|
||||
Column{
|
||||
Row{enableSkills, Stretch{1}},
|
||||
},
|
||||
},
|
||||
Group{
|
||||
title(Tr::tr("Skill Directories")),
|
||||
Column{
|
||||
globalSkillRoots,
|
||||
new QLabel(Tr::tr("Discovered global skills:")),
|
||||
skillsList,
|
||||
},
|
||||
},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
class SkillsSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
SkillsSettingsPage()
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_SKILLS_SETTINGS_PAGE_ID);
|
||||
setDisplayName(Tr::tr("Skills"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setSettingsProvider([] { return &skillsSettings(); });
|
||||
}
|
||||
};
|
||||
|
||||
const SkillsSettingsPage skillsSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
26
sources/settings/SkillsSettings.hpp
Normal file
26
sources/settings/SkillsSettings.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class SkillsSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
SkillsSettings();
|
||||
|
||||
Utils::BoolAspect enableSkills{this};
|
||||
|
||||
Utils::StringAspect globalSkillRoots{this};
|
||||
|
||||
static QStringList splitLines(const QString &value);
|
||||
static QStringList splitPaths(const QString &value);
|
||||
};
|
||||
|
||||
SkillsSettings &skillsSettings();
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
44
sources/settings/StatusDot.hpp
Normal file
44
sources/settings/StatusDot.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QWidget>
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class StatusDot : public QWidget
|
||||
{
|
||||
public:
|
||||
explicit StatusDot(QWidget *parent = nullptr)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setFixedSize(12, 12);
|
||||
}
|
||||
|
||||
void setColor(const QColor &color)
|
||||
{
|
||||
if (m_color == color)
|
||||
return;
|
||||
m_color = color;
|
||||
update();
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(m_color);
|
||||
p.drawEllipse(rect().adjusted(2, 2, -2, -2));
|
||||
}
|
||||
|
||||
private:
|
||||
QColor m_color{Qt::gray};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
278
sources/settings/ToolsSettings.cpp
Normal file
278
sources/settings/ToolsSettings.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
// 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 "ToolsSettings.hpp"
|
||||
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "SettingsTr.hpp"
|
||||
#include "SettingsUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
ToolsSettings &toolsSettings()
|
||||
{
|
||||
static ToolsSettings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
ToolsSettings::ToolsSettings()
|
||||
{
|
||||
setAutoApply(false);
|
||||
|
||||
setDisplayName(Tr::tr("Tools"));
|
||||
|
||||
allowAccessOutsideProject.setSettingsKey(Constants::CA_ALLOW_ACCESS_OUTSIDE_PROJECT);
|
||||
allowAccessOutsideProject.setLabelText(Tr::tr("Allow file access outside project"));
|
||||
allowAccessOutsideProject.setToolTip(
|
||||
Tr::tr("Allow tools to read, write, and create files outside the project scope "
|
||||
"(system headers, Qt files, external libraries)."));
|
||||
allowAccessOutsideProject.setDefaultValue(false);
|
||||
|
||||
autoApplyFileEdits.setSettingsKey(Constants::CA_AUTO_APPLY_FILE_EDITS);
|
||||
autoApplyFileEdits.setLabelText(Tr::tr("Automatically apply file edits"));
|
||||
autoApplyFileEdits.setToolTip(
|
||||
Tr::tr("When enabled, file edits suggested by AI are applied immediately. "
|
||||
"When disabled, each edit is staged for manual approval."));
|
||||
autoApplyFileEdits.setDefaultValue(false);
|
||||
|
||||
maxToolContinuations.setSettingsKey(Constants::TOOLS_MAX_CONTINUATIONS);
|
||||
maxToolContinuations.setLabelText(Tr::tr("Max tool continuations:"));
|
||||
maxToolContinuations.setToolTip(
|
||||
Tr::tr("Maximum number of consecutive tool-use rounds in a single request. "
|
||||
"Each round lets the model call tools and receive results before continuing. "
|
||||
"Higher values allow more complex multi-step tasks but increase token usage."));
|
||||
maxToolContinuations.setRange(1, 100);
|
||||
maxToolContinuations.setDefaultValue(30);
|
||||
|
||||
enableListProjectFilesTool.setSettingsKey(Constants::CA_ENABLE_LIST_PROJECT_FILES_TOOL);
|
||||
enableListProjectFilesTool.setLabelText(Tr::tr("List Project Files"));
|
||||
enableListProjectFilesTool.setToolTip(
|
||||
Tr::tr("Lists every source file tracked by the active Qt Creator project(s)."));
|
||||
enableListProjectFilesTool.setDefaultValue(true);
|
||||
|
||||
enableFindFileTool.setSettingsKey(Constants::CA_ENABLE_FIND_FILE_TOOL);
|
||||
enableFindFileTool.setLabelText(Tr::tr("Find File"));
|
||||
enableFindFileTool.setToolTip(
|
||||
Tr::tr("Locates a file in the project by name or partial path. Returns paths only, "
|
||||
"without file content."));
|
||||
enableFindFileTool.setDefaultValue(true);
|
||||
|
||||
enableReadFileTool.setSettingsKey(Constants::CA_ENABLE_READ_FILE_TOOL);
|
||||
enableReadFileTool.setLabelText(Tr::tr("Read File"));
|
||||
enableReadFileTool.setToolTip(
|
||||
Tr::tr("Reads the content of a file by absolute path or path relative to the project root."));
|
||||
enableReadFileTool.setDefaultValue(true);
|
||||
|
||||
enableProjectSearchTool.setSettingsKey(Constants::CA_ENABLE_PROJECT_SEARCH_TOOL);
|
||||
enableProjectSearchTool.setLabelText(Tr::tr("Search in Project"));
|
||||
enableProjectSearchTool.setToolTip(
|
||||
Tr::tr("Searches project files for text occurrences or C++ symbol definitions."));
|
||||
enableProjectSearchTool.setDefaultValue(true);
|
||||
|
||||
enableCreateNewFileTool.setSettingsKey(Constants::CA_ENABLE_CREATE_NEW_FILE_TOOL);
|
||||
enableCreateNewFileTool.setLabelText(Tr::tr("Create New File"));
|
||||
enableCreateNewFileTool.setToolTip(
|
||||
Tr::tr("Creates a new empty file at the given absolute path, making missing directories."));
|
||||
enableCreateNewFileTool.setDefaultValue(true);
|
||||
|
||||
enableEditFileTool.setSettingsKey(Constants::CA_ENABLE_EDIT_FILE_TOOL);
|
||||
enableEditFileTool.setLabelText(Tr::tr("Edit File"));
|
||||
enableEditFileTool.setToolTip(
|
||||
Tr::tr("Applies find-and-replace edits to files. See \"Automatically apply file edits\" "
|
||||
"to control whether edits apply immediately or wait for review."));
|
||||
enableEditFileTool.setDefaultValue(true);
|
||||
|
||||
enableBuildProjectTool.setSettingsKey(Constants::CA_ENABLE_BUILD_PROJECT_TOOL);
|
||||
enableBuildProjectTool.setLabelText(Tr::tr("Build Project"));
|
||||
enableBuildProjectTool.setToolTip(
|
||||
Tr::tr("Triggers a build of the active Qt Creator project and reports the result."));
|
||||
enableBuildProjectTool.setDefaultValue(true);
|
||||
|
||||
enableGetIssuesListTool.setSettingsKey(Constants::CA_ENABLE_GET_ISSUES_LIST_TOOL);
|
||||
enableGetIssuesListTool.setLabelText(Tr::tr("Get Issues List"));
|
||||
enableGetIssuesListTool.setToolTip(
|
||||
Tr::tr("Reads compiler/clang diagnostics from Qt Creator's Issues panel."));
|
||||
enableGetIssuesListTool.setDefaultValue(true);
|
||||
|
||||
enableTerminalCommandTool.setSettingsKey(Constants::CA_ENABLE_TERMINAL_COMMAND_TOOL);
|
||||
enableTerminalCommandTool.setLabelText(Tr::tr("Execute Terminal Command"));
|
||||
enableTerminalCommandTool.setToolTip(
|
||||
Tr::tr("Runs a command from the OS-specific allowed list below, in the project directory."));
|
||||
enableTerminalCommandTool.setDefaultValue(true);
|
||||
|
||||
enableTodoTool.setSettingsKey(Constants::CA_ENABLE_TODO_TOOL);
|
||||
enableTodoTool.setLabelText(Tr::tr("Todo"));
|
||||
enableTodoTool.setToolTip(
|
||||
Tr::tr("Lets the AI maintain a session-scoped todo list for multi-step workflows."));
|
||||
enableTodoTool.setDefaultValue(true);
|
||||
|
||||
enableReadOriginalHistoryTool.setSettingsKey(Constants::CA_ENABLE_READ_ORIGINAL_HISTORY_TOOL);
|
||||
enableReadOriginalHistoryTool.setLabelText(Tr::tr("Read Original History (Pre-Compression)"));
|
||||
enableReadOriginalHistoryTool.setToolTip(
|
||||
Tr::tr("Lets the AI read the original, full chat history from before the conversation "
|
||||
"was compressed into a summary. Useful when a detail is missing from the "
|
||||
"summary currently in context. Has no effect if the chat was never compressed."));
|
||||
enableReadOriginalHistoryTool.setDefaultValue(true);
|
||||
|
||||
enableSkillTool.setSettingsKey(Constants::CA_ENABLE_SKILL_TOOL);
|
||||
enableSkillTool.setLabelText(Tr::tr("Load Skill"));
|
||||
enableSkillTool.setToolTip(
|
||||
Tr::tr("Lets the AI load the full instructions of a skill on demand. The Available "
|
||||
"Skills catalog in the system prompt lists each skill; this tool pulls a "
|
||||
"skill's complete instructions into context when needed."));
|
||||
enableSkillTool.setDefaultValue(true);
|
||||
|
||||
allowedTerminalCommandsLinux.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_LINUX);
|
||||
allowedTerminalCommandsLinux.setLabelText(Tr::tr("Allowed Commands (Linux)"));
|
||||
allowedTerminalCommandsLinux.setToolTip(
|
||||
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Linux. "
|
||||
"Example: git, ls, cat, grep, find, cmake"));
|
||||
allowedTerminalCommandsLinux.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
allowedTerminalCommandsLinux.setDefaultValue(
|
||||
"git, ls, cat, grep, find, pwd, echo, head, tail, wc, which, file, stat, tree, uname, "
|
||||
"date, whoami, hostname");
|
||||
|
||||
allowedTerminalCommandsMacOS.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_MACOS);
|
||||
allowedTerminalCommandsMacOS.setLabelText(Tr::tr("Allowed Commands (macOS)"));
|
||||
allowedTerminalCommandsMacOS.setToolTip(
|
||||
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on macOS. "
|
||||
"Example: git, ls, cat, grep, find, cmake"));
|
||||
allowedTerminalCommandsMacOS.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
allowedTerminalCommandsMacOS.setDefaultValue(
|
||||
"git, ls, cat, grep, find, pwd, echo, head, tail, wc, which, file, stat, tree, uname, "
|
||||
"date, whoami, hostname");
|
||||
|
||||
allowedTerminalCommandsWindows.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS);
|
||||
allowedTerminalCommandsWindows.setLabelText(Tr::tr("Allowed Commands (Windows)"));
|
||||
allowedTerminalCommandsWindows.setToolTip(
|
||||
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Windows. "
|
||||
"Example: git, dir, type, findstr, where, cmake"));
|
||||
allowedTerminalCommandsWindows.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
allowedTerminalCommandsWindows.setDefaultValue(
|
||||
"git, dir, type, findstr, where, echo, whoami, hostname, ver, tree, fc");
|
||||
|
||||
terminalCommandTimeout.setSettingsKey(Constants::CA_TERMINAL_COMMAND_TIMEOUT);
|
||||
terminalCommandTimeout.setLabelText(Tr::tr("Command Timeout (seconds)"));
|
||||
terminalCommandTimeout.setToolTip(
|
||||
Tr::tr("Maximum time in seconds to wait for a terminal command to complete. "
|
||||
"Increase for long-running commands like builds."));
|
||||
terminalCommandTimeout.setRange(5, 3600);
|
||||
terminalCommandTimeout.setDefaultValue(30);
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
|
||||
readSettings();
|
||||
|
||||
setupConnections();
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
auto ¤tOsCommands = allowedTerminalCommandsLinux;
|
||||
#elif defined(Q_OS_MACOS)
|
||||
auto ¤tOsCommands = allowedTerminalCommandsMacOS;
|
||||
#elif defined(Q_OS_WIN)
|
||||
auto ¤tOsCommands = allowedTerminalCommandsWindows;
|
||||
#else
|
||||
auto ¤tOsCommands = allowedTerminalCommandsLinux; // fallback
|
||||
#endif
|
||||
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Tools")),
|
||||
Column{
|
||||
enableListProjectFilesTool,
|
||||
enableFindFileTool,
|
||||
enableReadFileTool,
|
||||
enableProjectSearchTool,
|
||||
enableCreateNewFileTool,
|
||||
enableEditFileTool,
|
||||
enableBuildProjectTool,
|
||||
enableGetIssuesListTool,
|
||||
enableTerminalCommandTool,
|
||||
enableTodoTool,
|
||||
enableReadOriginalHistoryTool,
|
||||
enableSkillTool}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Tool Settings")),
|
||||
Column{
|
||||
allowAccessOutsideProject,
|
||||
Row{maxToolContinuations, Stretch{1}},
|
||||
Space{4},
|
||||
Group{
|
||||
title(Tr::tr("Edit File")),
|
||||
Column{autoApplyFileEdits}},
|
||||
Group{
|
||||
title(Tr::tr("Execute Terminal Command")),
|
||||
Column{currentOsCommands, terminalCommandTimeout}}}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
void ToolsSettings::setupConnections()
|
||||
{
|
||||
connect(
|
||||
&resetToDefaults,
|
||||
&ButtonAspect::clicked,
|
||||
this,
|
||||
&ToolsSettings::resetSettingsToDefaults);
|
||||
}
|
||||
|
||||
void ToolsSettings::resetSettingsToDefaults()
|
||||
{
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr("Reset Settings"),
|
||||
Tr::tr("Are you sure you want to reset all settings to default values?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(allowAccessOutsideProject);
|
||||
resetAspect(autoApplyFileEdits);
|
||||
resetAspect(maxToolContinuations);
|
||||
resetAspect(enableListProjectFilesTool);
|
||||
resetAspect(enableFindFileTool);
|
||||
resetAspect(enableReadFileTool);
|
||||
resetAspect(enableProjectSearchTool);
|
||||
resetAspect(enableCreateNewFileTool);
|
||||
resetAspect(enableEditFileTool);
|
||||
resetAspect(enableBuildProjectTool);
|
||||
resetAspect(enableGetIssuesListTool);
|
||||
resetAspect(enableTerminalCommandTool);
|
||||
resetAspect(enableTodoTool);
|
||||
resetAspect(enableReadOriginalHistoryTool);
|
||||
resetAspect(enableSkillTool);
|
||||
resetAspect(allowedTerminalCommandsLinux);
|
||||
resetAspect(allowedTerminalCommandsMacOS);
|
||||
resetAspect(allowedTerminalCommandsWindows);
|
||||
resetAspect(terminalCommandTimeout);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
class ToolsSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
ToolsSettingsPage()
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID);
|
||||
setDisplayName(Tr::tr("Tools"));
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setSettingsProvider([] { return &toolsSettings(); });
|
||||
}
|
||||
};
|
||||
|
||||
const ToolsSettingsPage toolsSettingsPage;
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
49
sources/settings/ToolsSettings.hpp
Normal file
49
sources/settings/ToolsSettings.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ButtonAspect.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class ToolsSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
ToolsSettings();
|
||||
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
Utils::BoolAspect allowAccessOutsideProject{this};
|
||||
Utils::BoolAspect autoApplyFileEdits{this};
|
||||
Utils::IntegerAspect maxToolContinuations{this};
|
||||
|
||||
Utils::BoolAspect enableListProjectFilesTool{this};
|
||||
Utils::BoolAspect enableFindFileTool{this};
|
||||
Utils::BoolAspect enableReadFileTool{this};
|
||||
Utils::BoolAspect enableProjectSearchTool{this};
|
||||
Utils::BoolAspect enableCreateNewFileTool{this};
|
||||
Utils::BoolAspect enableEditFileTool{this};
|
||||
Utils::BoolAspect enableBuildProjectTool{this};
|
||||
Utils::BoolAspect enableGetIssuesListTool{this};
|
||||
Utils::BoolAspect enableTerminalCommandTool{this};
|
||||
Utils::BoolAspect enableTodoTool{this};
|
||||
Utils::BoolAspect enableReadOriginalHistoryTool{this};
|
||||
Utils::BoolAspect enableSkillTool{this};
|
||||
|
||||
Utils::StringAspect allowedTerminalCommandsLinux{this};
|
||||
Utils::StringAspect allowedTerminalCommandsMacOS{this};
|
||||
Utils::StringAspect allowedTerminalCommandsWindows{this};
|
||||
Utils::IntegerAspect terminalCommandTimeout{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
};
|
||||
|
||||
ToolsSettings &toolsSettings();
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
238
sources/settings/UpdateDialog.cpp
Normal file
238
sources/settings/UpdateDialog.cpp
Normal file
@@ -0,0 +1,238 @@
|
||||
// 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 "UpdateDialog.hpp"
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <extensionsystem/pluginmanager.h>
|
||||
#include <extensionsystem/pluginspec.h>
|
||||
#include <QClipboard>
|
||||
#include <QDesktopServices>
|
||||
#include <QFrame>
|
||||
#include <QGroupBox>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
UpdateDialog::UpdateDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_updater(new PluginUpdater(this))
|
||||
{
|
||||
setWindowTitle(tr("QodeAssist Update"));
|
||||
setFixedSize(700, 720);
|
||||
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setSpacing(12);
|
||||
|
||||
auto *supportLabel = new QLabel(
|
||||
tr("QodeAssist is an open-source project that helps\n"
|
||||
"developers write better code. If you find it useful, please"),
|
||||
this);
|
||||
supportLabel->setAlignment(Qt::AlignCenter);
|
||||
m_layout->addWidget(supportLabel);
|
||||
|
||||
auto *supportLink = new QLabel(
|
||||
"<a href='https://ko-fi.com/qodeassist' style='color: #0066cc;'>Support on Ko-fi "
|
||||
"☕</a>",
|
||||
this);
|
||||
supportLink->setOpenExternalLinks(true);
|
||||
supportLink->setTextFormat(Qt::RichText);
|
||||
supportLink->setAlignment(Qt::AlignCenter);
|
||||
m_layout->addWidget(supportLink);
|
||||
auto *githubSupportLink = new QLabel(
|
||||
"<a "
|
||||
"href='https://github.com/Palm1r/"
|
||||
"QodeAssist?tab=readme-ov-file#support-the-development-of-qodeassist' style='color: #0066cc;' > Support page on github </a>",
|
||||
this);
|
||||
githubSupportLink->setOpenExternalLinks(true);
|
||||
githubSupportLink->setTextFormat(Qt::RichText);
|
||||
githubSupportLink->setAlignment(Qt::AlignCenter);
|
||||
m_layout->addWidget(githubSupportLink);
|
||||
|
||||
auto *paypalLink = new QLabel(
|
||||
"<a href='https://www.paypal.com/paypalme/palm1r' style='color: #0066cc;'>Support via PayPal "
|
||||
"💳</a>",
|
||||
this);
|
||||
paypalLink->setOpenExternalLinks(true);
|
||||
paypalLink->setTextFormat(Qt::RichText);
|
||||
paypalLink->setAlignment(Qt::AlignCenter);
|
||||
m_layout->addWidget(paypalLink);
|
||||
|
||||
m_layout->addSpacing(20);
|
||||
|
||||
auto *registryGroup = new QGroupBox(tr("Install via Extension Registry (recommended)"), this);
|
||||
auto *registryLayout = new QVBoxLayout(registryGroup);
|
||||
registryLayout->setSpacing(10);
|
||||
|
||||
auto *registryInfoLabel = new QLabel(
|
||||
tr("In Qt Creator open Extensions → Browser tab, enable \"Use External Repository\", "
|
||||
"add one of the URLs below and click Apply to install QodeAssist. Updates are then "
|
||||
"installed from the same screen."),
|
||||
registryGroup);
|
||||
registryInfoLabel->setWordWrap(true);
|
||||
registryLayout->addWidget(registryInfoLabel);
|
||||
|
||||
const auto addRegistryRow = [&](const QString &description, const QString &url) {
|
||||
auto *card = new QFrame(registryGroup);
|
||||
card->setFrameShape(QFrame::StyledPanel);
|
||||
auto *cardLayout = new QHBoxLayout(card);
|
||||
cardLayout->setContentsMargins(10, 8, 10, 8);
|
||||
cardLayout->setSpacing(10);
|
||||
|
||||
auto *textLayout = new QVBoxLayout;
|
||||
textLayout->setSpacing(2);
|
||||
|
||||
auto *desc = new QLabel(description, card);
|
||||
desc->setStyleSheet("font-weight: bold;");
|
||||
textLayout->addWidget(desc);
|
||||
|
||||
auto *link = new QLabel(
|
||||
QString("<a href='%1' style='color: #0066cc;'>%1</a>").arg(url), card);
|
||||
link->setOpenExternalLinks(true);
|
||||
link->setTextFormat(Qt::RichText);
|
||||
link->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||
link->setWordWrap(true);
|
||||
textLayout->addWidget(link);
|
||||
|
||||
cardLayout->addLayout(textLayout, 1);
|
||||
|
||||
auto *copyButton = new QPushButton(tr("Copy"), card);
|
||||
copyButton->setMaximumWidth(70);
|
||||
connect(copyButton, &QPushButton::clicked, this, [url]() {
|
||||
QGuiApplication::clipboard()->setText(url);
|
||||
});
|
||||
cardLayout->addWidget(copyButton, 0, Qt::AlignVCenter);
|
||||
|
||||
registryLayout->addWidget(card);
|
||||
};
|
||||
|
||||
addRegistryRow(
|
||||
tr("Latest (for the newest Qt Creator, always up to date)"),
|
||||
"https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz");
|
||||
addRegistryRow(
|
||||
tr("Only for Qt Creator %1").arg(QODEASSIST_QT_CREATOR_VERSION_MAJOR),
|
||||
QString("https://github.com/Palm1r/extension-registry/archive/refs/heads/"
|
||||
"qodeassist-qtc%1.tar.gz")
|
||||
.arg(QODEASSIST_QT_CREATOR_VERSION_MAJOR));
|
||||
|
||||
m_layout->addWidget(registryGroup);
|
||||
|
||||
auto *updaterGroup = new QGroupBox(tr("Alternative: QodeAssistUpdater"), this);
|
||||
auto *updaterLayout = new QVBoxLayout(updaterGroup);
|
||||
updaterLayout->setSpacing(10);
|
||||
|
||||
auto *updaterInfoLabel = new QLabel(
|
||||
tr("A standalone tool for installing and updating the plugin."), updaterGroup);
|
||||
updaterInfoLabel->setWordWrap(true);
|
||||
updaterLayout->addWidget(updaterInfoLabel);
|
||||
|
||||
m_buttonOpenUpdaterRelease = new QPushButton(tr("Download QodeAssistUpdater"), updaterGroup);
|
||||
m_buttonOpenUpdaterRelease->setMaximumWidth(250);
|
||||
auto *updaterButtonLayout = new QHBoxLayout;
|
||||
updaterButtonLayout->addStretch();
|
||||
updaterButtonLayout->addWidget(m_buttonOpenUpdaterRelease);
|
||||
updaterButtonLayout->addStretch();
|
||||
updaterLayout->addLayout(updaterButtonLayout);
|
||||
|
||||
m_layout->addWidget(updaterGroup);
|
||||
|
||||
m_layout->addSpacing(10);
|
||||
|
||||
m_titleLabel = new QLabel(tr("A new version of QodeAssist is available!"), this);
|
||||
m_titleLabel->setStyleSheet("font-weight: bold; font-size: 14px;");
|
||||
m_titleLabel->setAlignment(Qt::AlignCenter);
|
||||
m_layout->addWidget(m_titleLabel);
|
||||
|
||||
m_versionLabel = new QLabel(
|
||||
tr("Version %1 is now available - you have %2").arg("", m_updater->currentVersion()), this);
|
||||
m_versionLabel->setAlignment(Qt::AlignCenter);
|
||||
m_layout->addWidget(m_versionLabel);
|
||||
|
||||
m_changelogLabel = new QLabel(tr("Release Notes:"), this);
|
||||
m_layout->addWidget(m_changelogLabel);
|
||||
|
||||
m_changelogText = new QTextEdit(this);
|
||||
m_changelogText->setReadOnly(true);
|
||||
m_changelogText->setMinimumHeight(100);
|
||||
m_layout->addWidget(m_changelogText);
|
||||
|
||||
auto *buttonLayout = new QHBoxLayout;
|
||||
m_buttonOpenReleasePage = new QPushButton(tr("Open Release Page"), this);
|
||||
buttonLayout->addWidget(m_buttonOpenReleasePage);
|
||||
|
||||
m_buttonOpenPluginFolder = new QPushButton(tr("Open Plugin Folder"), this);
|
||||
buttonLayout->addWidget(m_buttonOpenPluginFolder);
|
||||
|
||||
m_closeButton = new QPushButton(tr("Close"), this);
|
||||
buttonLayout->addWidget(m_closeButton);
|
||||
|
||||
m_layout->addLayout(buttonLayout);
|
||||
|
||||
connect(m_updater, &PluginUpdater::updateCheckFinished, this, &UpdateDialog::handleUpdateInfo);
|
||||
connect(m_buttonOpenReleasePage, &QPushButton::clicked, this, &UpdateDialog::openReleasePage);
|
||||
connect(m_buttonOpenPluginFolder, &QPushButton::clicked, this, &UpdateDialog::openPluginFolder);
|
||||
connect(m_buttonOpenUpdaterRelease, &QPushButton::clicked, this, &UpdateDialog::openUpdaterReleasePage);
|
||||
connect(m_closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
|
||||
m_updater->checkForUpdates();
|
||||
}
|
||||
|
||||
void UpdateDialog::checkForUpdatesAndShow(QWidget *parent)
|
||||
{
|
||||
auto *dialog = new UpdateDialog(parent);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->show();
|
||||
}
|
||||
|
||||
void UpdateDialog::handleUpdateInfo(const PluginUpdater::UpdateInfo &info)
|
||||
{
|
||||
m_updateInfo = info;
|
||||
|
||||
if (!info.isUpdateAvailable) {
|
||||
m_titleLabel->setText(tr("QodeAssist is up to date"));
|
||||
m_versionLabel->setText(
|
||||
tr("You are using the latest version: %1").arg(m_updater->currentVersion()));
|
||||
return;
|
||||
}
|
||||
|
||||
m_titleLabel->setText(tr("A new version of QodeAssist is available!"));
|
||||
m_versionLabel->setText(tr("Version %1 is now available - you have %2")
|
||||
.arg(info.version, m_updater->currentVersion()));
|
||||
|
||||
if (!info.changeLog.isEmpty()) {
|
||||
m_changelogText->setText(info.changeLog);
|
||||
} else {
|
||||
m_changelogText->setText(
|
||||
tr("No release notes available. Check the release page for more information."));
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateDialog::openReleasePage()
|
||||
{
|
||||
QDesktopServices::openUrl(QUrl("https://github.com/Palm1r/QodeAssist/releases/latest"));
|
||||
accept();
|
||||
}
|
||||
|
||||
void UpdateDialog::openPluginFolder()
|
||||
{
|
||||
const auto pluginSpecs = ExtensionSystem::PluginManager::plugins();
|
||||
for (const ExtensionSystem::PluginSpec *spec : pluginSpecs) {
|
||||
if (spec->name() == QLatin1String("QodeAssist")) {
|
||||
const auto pluginPath = spec->filePath().path();
|
||||
QFileInfo fileInfo(pluginPath);
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(fileInfo.absolutePath()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
void UpdateDialog::openUpdaterReleasePage()
|
||||
{
|
||||
QDesktopServices::openUrl(QUrl("https://github.com/Palm1r/QodeAssistUpdater"));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
47
sources/settings/UpdateDialog.hpp
Normal file
47
sources/settings/UpdateDialog.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "PluginUpdater.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class UpdateDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit UpdateDialog(QWidget *parent = nullptr);
|
||||
|
||||
static void checkForUpdatesAndShow(QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void handleUpdateInfo(const PluginUpdater::UpdateInfo &info);
|
||||
void openReleasePage();
|
||||
void openPluginFolder();
|
||||
void openUpdaterReleasePage();
|
||||
|
||||
private:
|
||||
PluginUpdater *m_updater;
|
||||
QVBoxLayout *m_layout;
|
||||
QLabel *m_titleLabel;
|
||||
QLabel *m_versionLabel;
|
||||
QLabel *m_changelogLabel;
|
||||
QTextEdit *m_changelogText;
|
||||
QPushButton *m_buttonOpenReleasePage;
|
||||
QPushButton *m_buttonOpenPluginFolder;
|
||||
QPushButton *m_buttonOpenUpdaterRelease;
|
||||
QPushButton *m_closeButton;
|
||||
PluginUpdater::UpdateInfo m_updateInfo;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
Reference in New Issue
Block a user