refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore

This commit is contained in:
Petr Mironychev
2026-07-14 02:50:43 +02:00
parent adef7972ed
commit 34d4f2c517
348 changed files with 809 additions and 852 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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;
};

View 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
)

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,176 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#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

View 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(" &nbsp; <span style=\"color:%1\">env: %2</span>")
.arg(muted, envKeys.join(", "));
}
return QString("<b>%1</b> &nbsp; <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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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 &currentText, 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

View 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 &currentText = QString(), bool editable = true);
private:
QVBoxLayout *m_mainLayout;
QHBoxLayout *m_buttonLayout;
};
} // namespace QodeAssist::Settings

View 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

View 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

View 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

View 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

View 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

View 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 &currentOsCommands = allowedTerminalCommandsLinux;
#elif defined(Q_OS_MACOS)
auto &currentOsCommands = allowedTerminalCommandsMacOS;
#elif defined(Q_OS_WIN)
auto &currentOsCommands = allowedTerminalCommandsWindows;
#else
auto &currentOsCommands = 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

View 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

View 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

View 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