feat: add support acp in common chat (#369)

This commit is contained in:
Petr Mironychev
2026-07-21 13:10:00 +02:00
committed by GitHub
parent af85efb554
commit 442263a6a7
158 changed files with 14270 additions and 4118 deletions

View File

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

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

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

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

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

@@ -1,43 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#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,56 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "AgentsSettings.hpp"
#include <QDir>
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
namespace QodeAssist::Settings {
AgentsSettings &agentsSettings()
{
static AgentsSettings settings;
return settings;
}
AgentsSettings::AgentsSettings()
{
setAutoApply(false);
setDisplayName(Tr::tr("Agents"));
agentExtraPaths.setSettingsKey(Constants::AGENT_EXTRA_PATHS);
agentExtraPaths.setLabelText(Tr::tr("Extra PATH for launching agents:"));
agentExtraPaths.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
agentExtraPaths.setToolTip(
Tr::tr(
"Directories to prepend to PATH when launching ACP agents, and to search for "
"the agent's executable. Useful when Qt Creator is started from the dock and "
"doesn't see Homebrew, nvm, uv, etc. Separate multiple entries with '%1'.")
.arg(QDir::listSeparator()));
#ifdef Q_OS_MACOS
agentExtraPaths.setDefaultValue(QStringLiteral("/opt/homebrew/bin:/usr/local/bin"));
#else
agentExtraPaths.setDefaultValue(QString{});
#endif
agentForwardedVariables.setSettingsKey(Constants::AGENT_FORWARDED_VARIABLES);
agentForwardedVariables.setLabelText(Tr::tr("Forward these variables to agents:"));
agentForwardedVariables.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
agentForwardedVariables.setToolTip(
Tr::tr(
"Names of environment variables (not their values) passed on to ACP agents, "
"separated by spaces or commas. A variable already present in Qt Creator's own "
"environment is used as is; on macOS and Linux the rest are read once from a login "
"shell, so a token exported in your shell profile reaches agents even when Qt "
"Creator was started from the dock."));
agentForwardedVariables.setDefaultValue(QStringLiteral("CLAUDE_CODE_OAUTH_TOKEN"));
readSettings();
}
} // namespace QodeAssist::Settings

View File

@@ -0,0 +1,22 @@
// 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 AgentsSettings : public Utils::AspectContainer
{
public:
AgentsSettings();
Utils::StringAspect agentExtraPaths{this};
Utils::StringAspect agentForwardedVariables{this};
};
AgentsSettings &agentsSettings();
} // namespace QodeAssist::Settings

View File

@@ -0,0 +1,348 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "AgentsWidget.hpp"
#include <QDesktopServices>
#include <QDir>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include <QTextBrowser>
#include <QUrl>
#include <QVBoxLayout>
#include <coreplugin/dialogs/ioptionspage.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include "AgentsSettings.hpp"
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
#include "acp/AgentCatalogStore.hpp"
#include "acp/AgentTester.hpp"
namespace QodeAssist::Settings {
namespace {
constexpr int agentIdRole = Qt::UserRole;
QString distributionSummary(const Acp::AgentDefinition &agent)
{
const QString kind = Acp::agentDistributionName(agent.distribution.kind);
switch (agent.distribution.kind) {
case Acp::AgentDistributionKind::Npx:
case Acp::AgentDistributionKind::Uvx:
return QStringLiteral("%1 (%2)").arg(kind, agent.distribution.package);
case Acp::AgentDistributionKind::Command:
return QStringLiteral("%1 (%2)").arg(kind, agent.distribution.command);
case Acp::AgentDistributionKind::Binary:
case Acp::AgentDistributionKind::Unknown:
return kind;
}
return kind;
}
QString linkRow(const QString &label, const QString &url)
{
if (url.isEmpty())
return {};
return QStringLiteral("<b>%1</b> <a href=\"%2\">%2</a><br>")
.arg(label.toHtmlEscaped(), url.toHtmlEscaped());
}
QString unavailableGuidance(const Acp::AgentDefinition &agent)
{
if (agent.distribution.kind == Acp::AgentDistributionKind::Binary) {
QString text = Tr::tr(
"This agent ships as a platform binary. QodeAssist does not download binaries yet: "
"install it manually, then add a JSON file to the agents folder with a "
"\"command\" distribution pointing at the executable.");
if (const Acp::AgentBinaryTarget *target = Acp::binaryTargetForCurrentPlatform(agent)) {
text += QStringLiteral("<br><br>")
+ Tr::tr("Download for %1:").arg(target->platform.toHtmlEscaped())
+ QStringLiteral(" <a href=\"%1\">%1</a>").arg(target->archive.toHtmlEscaped());
}
return text;
}
return Tr::tr("This agent has no distribution QodeAssist can launch.");
}
} // namespace
AgentsWidget::AgentsWidget(Acp::AgentCatalogStore *store)
: m_store(store)
, m_tester(new Acp::AgentTester(this))
{
setupUI();
connect(m_store, &Acp::AgentCatalogStore::catalogChanged, this, &AgentsWidget::populateList);
connect(
m_store,
&Acp::AgentCatalogStore::refreshFinished,
this,
[this](bool ok, const QString &error) {
showStatus(
ok ? Tr::tr("Agent list refreshed from the registry.")
: Tr::tr("Registry refresh failed: %1").arg(error));
updateButtons();
});
connect(m_tester, &Acp::AgentTester::finished, this, [this](bool ok, const QString &report) {
showStatus(ok ? Tr::tr("Agent responded.") : Tr::tr("Agent test failed."));
m_details->append(
QStringLiteral("<hr><b>%1</b><pre>%2</pre>")
.arg(ok ? Tr::tr("Test result") : Tr::tr("Test failed"), report.toHtmlEscaped()));
updateButtons();
});
populateList();
}
void AgentsWidget::apply()
{
auto &settings = agentsSettings();
settings.agentExtraPaths.setValue(m_extraPathsEdit->text());
settings.agentForwardedVariables.setValue(m_forwardedVariablesEdit->text());
settings.writeSettings();
}
QLineEdit *AgentsWidget::addSettingRow(QVBoxLayout *layout, Utils::StringAspect &aspect)
{
auto *row = new QHBoxLayout();
auto *label = new QLabel(aspect.labelText(), this);
label->setToolTip(aspect.toolTip());
auto *edit = new QLineEdit(aspect.volatileValue(), this);
edit->setToolTip(aspect.toolTip());
connect(edit, &QLineEdit::textChanged, &aspect, [&aspect](const QString &text) {
aspect.setVolatileValue(text);
});
row->addWidget(label);
row->addWidget(edit, 1);
layout->addLayout(row);
return edit;
}
void AgentsWidget::setupUI()
{
auto *mainLayout = new QVBoxLayout(this);
auto *headerLayout = new QHBoxLayout();
auto *info = new QLabel(
Tr::tr(
"ACP agents from your own JSON files, the agent registry, and the bundled "
"snapshot. Definitions with the same id override each other in that order."),
this);
info->setWordWrap(true);
headerLayout->addWidget(info, 1);
auto *openFolderButton = new QPushButton(Tr::tr("Open Agents Folder..."), this);
connect(openFolderButton, &QPushButton::clicked, this, &AgentsWidget::onOpenAgentsFolder);
headerLayout->addWidget(openFolderButton);
mainLayout->addLayout(headerLayout);
m_extraPathsEdit = addSettingRow(mainLayout, agentsSettings().agentExtraPaths);
m_forwardedVariablesEdit = addSettingRow(mainLayout, agentsSettings().agentForwardedVariables);
auto *contentLayout = new QHBoxLayout();
m_agentsList = new QListWidget(this);
m_agentsList->setSelectionMode(QAbstractItemView::SingleSelection);
connect(m_agentsList, &QListWidget::itemSelectionChanged, this, &AgentsWidget::showSelectedAgent);
contentLayout->addWidget(m_agentsList, 1);
auto *buttonsLayout = new QVBoxLayout();
m_testButton = new QPushButton(Tr::tr("Test"), this);
m_testButton->setToolTip(
Tr::tr("Start the agent, run the ACP handshake and report its capabilities."));
connect(m_testButton, &QPushButton::clicked, this, &AgentsWidget::onTest);
buttonsLayout->addWidget(m_testButton);
m_reloadButton = new QPushButton(Tr::tr("Reload"), this);
m_reloadButton->setToolTip(Tr::tr("Re-read the agent JSON files and the cached registry."));
connect(m_reloadButton, &QPushButton::clicked, this, [this]() {
m_store->reload();
showStatus(Tr::tr("Agent list reloaded from disk."));
});
buttonsLayout->addWidget(m_reloadButton);
m_refreshButton = new QPushButton(Tr::tr("Refresh from Registry"), this);
m_refreshButton->setToolTip(Tr::tr("Download the agent registry and update the local cache."));
connect(m_refreshButton, &QPushButton::clicked, this, &AgentsWidget::onRefresh);
buttonsLayout->addWidget(m_refreshButton);
buttonsLayout->addStretch();
contentLayout->addLayout(buttonsLayout);
mainLayout->addLayout(contentLayout, 1);
m_details = new QTextBrowser(this);
m_details->setOpenExternalLinks(true);
m_details->setMinimumHeight(160);
mainLayout->addWidget(m_details, 1);
m_status = new QLabel(this);
m_status->setWordWrap(true);
mainLayout->addWidget(m_status);
}
void AgentsWidget::populateList()
{
const QString selectedId = m_agentsList->currentItem()
? m_agentsList->currentItem()->data(agentIdRole).toString()
: QString();
m_agentsList->clear();
const QList<Acp::AgentDefinition> agents = m_store->catalog().agents();
for (const Acp::AgentDefinition &agent : agents) {
const QString label = agent.version.isEmpty()
? agent.name
: QStringLiteral("%1 %2").arg(agent.name, agent.version);
auto *item = new QListWidgetItem(label, m_agentsList);
item->setData(agentIdRole, agent.id);
if (agent.isLaunchable()) {
item->setToolTip(
QStringLiteral("%1 — %2")
.arg(distributionSummary(agent), Acp::agentSourceName(agent.source)));
} else {
item->setForeground(palette().brush(QPalette::Disabled, QPalette::Text));
item->setToolTip(unavailableGuidance(agent));
}
if (agent.id == selectedId)
m_agentsList->setCurrentItem(item);
}
showStatus(
m_store->hasCachedRegistry()
? QString()
: Tr::tr(
"Showing the bundled agent snapshot — press Refresh from Registry for the "
"published list."));
showSelectedAgent();
}
void AgentsWidget::showStatus(const QString &message)
{
const QStringList warnings = m_store->warnings();
if (warnings.isEmpty()) {
m_status->setText(message);
return;
}
const QString ignored = Tr::tr("Ignored entries: %1").arg(warnings.join(QStringLiteral("; ")));
m_status->setText(message.isEmpty() ? ignored : QStringLiteral("%1 %2").arg(message, ignored));
}
void AgentsWidget::showSelectedAgent()
{
updateButtons();
const auto agent = selectedAgent();
if (!agent) {
m_details->clear();
return;
}
QString html = QStringLiteral("<h3>%1</h3>").arg(agent->name.toHtmlEscaped());
if (!agent->description.isEmpty())
html += QStringLiteral("<p>%1</p>").arg(agent->description.toHtmlEscaped());
html += QStringLiteral("<p>");
html += QStringLiteral("<b>%1</b> %2<br>").arg(Tr::tr("Id:"), agent->id.toHtmlEscaped());
html += QStringLiteral("<b>%1</b> %2<br>")
.arg(Tr::tr("Distribution:"), distributionSummary(*agent).toHtmlEscaped());
html += QStringLiteral("<b>%1</b> %2<br>")
.arg(Tr::tr("Defined by:"), agent->origin.toHtmlEscaped());
if (!agent->license.isEmpty())
html += QStringLiteral("<b>%1</b> %2<br>")
.arg(Tr::tr("License:"), agent->license.toHtmlEscaped());
if (!agent->authors.isEmpty())
html += QStringLiteral("<b>%1</b> %2<br>")
.arg(
Tr::tr("Authors:"),
agent->authors.join(QStringLiteral(", ")).toHtmlEscaped());
html += linkRow(Tr::tr("Repository:"), agent->repository);
html += linkRow(Tr::tr("Website:"), agent->website);
html += QStringLiteral("</p>");
if (!agent->isLaunchable())
html += QStringLiteral("<p>%1</p>").arg(unavailableGuidance(*agent));
m_details->setHtml(html);
}
void AgentsWidget::updateButtons()
{
const auto agent = selectedAgent();
const bool testing = m_tester->isRunning();
m_testButton->setText(testing ? Tr::tr("Cancel Test") : Tr::tr("Test"));
m_testButton->setEnabled(testing || (agent && agent->isLaunchable()));
m_refreshButton->setEnabled(!m_store->isRefreshing());
m_refreshButton->setText(
m_store->isRefreshing() ? Tr::tr("Refreshing...") : Tr::tr("Refresh from Registry"));
}
void AgentsWidget::onTest()
{
if (m_tester->isRunning()) {
m_tester->cancel();
return;
}
const auto agent = selectedAgent();
if (!agent)
return;
showStatus(Tr::tr("Starting %1...").arg(agent->name));
m_tester->start(*agent, testWorkingDirectory());
updateButtons();
}
void AgentsWidget::onRefresh()
{
showStatus(Tr::tr("Downloading the agent registry..."));
m_store->refreshFromRegistry();
updateButtons();
}
void AgentsWidget::onOpenAgentsFolder()
{
QDesktopServices::openUrl(QUrl::fromLocalFile(Acp::AgentCatalogStore::userAgentsDirectory()));
}
std::optional<Acp::AgentDefinition> AgentsWidget::selectedAgent() const
{
const QListWidgetItem *item = m_agentsList->currentItem();
if (!item)
return std::nullopt;
return m_store->catalog().agent(item->data(agentIdRole).toString());
}
QString AgentsWidget::testWorkingDirectory()
{
if (const ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject())
return project->projectDirectory().path();
return QDir::homePath();
}
AgentsSettingsPage::AgentsSettingsPage(Acp::AgentCatalogStore *store)
{
setId(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
setDisplayName(Tr::tr("Agents"));
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
setWidgetCreator([store]() { return new AgentsWidget(store); });
}
} // namespace QodeAssist::Settings

View File

@@ -0,0 +1,74 @@
// 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 <optional>
#include <coreplugin/dialogs/ioptionspage.h>
#include <utils/aspects.h>
#include "acp/AgentDefinition.hpp"
QT_BEGIN_NAMESPACE
class QLabel;
class QLineEdit;
class QListWidget;
class QPushButton;
class QTextBrowser;
class QVBoxLayout;
QT_END_NAMESPACE
namespace QodeAssist::Acp {
class AgentCatalogStore;
class AgentTester;
} // namespace QodeAssist::Acp
namespace QodeAssist::Settings {
class AgentsWidget : public Core::IOptionsPageWidget
{
Q_OBJECT
public:
explicit AgentsWidget(Acp::AgentCatalogStore *store);
void apply() override;
private:
void setupUI();
void populateList();
void updateButtons();
void showSelectedAgent();
void showStatus(const QString &message);
void onTest();
void onRefresh();
void onOpenAgentsFolder();
std::optional<Acp::AgentDefinition> selectedAgent() const;
static QString testWorkingDirectory();
Acp::AgentCatalogStore *m_store = nullptr;
Acp::AgentTester *m_tester = nullptr;
QLineEdit *addSettingRow(QVBoxLayout *layout, Utils::StringAspect &aspect);
QLineEdit *m_extraPathsEdit = nullptr;
QLineEdit *m_forwardedVariablesEdit = nullptr;
QListWidget *m_agentsList = nullptr;
QTextBrowser *m_details = nullptr;
QLabel *m_status = nullptr;
QPushButton *m_testButton = nullptr;
QPushButton *m_reloadButton = nullptr;
QPushButton *m_refreshButton = nullptr;
};
class AgentsSettingsPage : public Core::IOptionsPage
{
public:
explicit AgentsSettingsPage(Acp::AgentCatalogStore *store);
};
} // namespace QodeAssist::Settings

View File

@@ -18,9 +18,6 @@ add_library(QodeAssistSettings STATIC
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

View File

@@ -14,7 +14,6 @@
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
#include "SettingsUtils.hpp"
#include "AgentRolesWidget.hpp"
namespace QodeAssist::Settings {
@@ -30,10 +29,6 @@ ChatAssistantSettings::ChatAssistantSettings()
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"));
@@ -252,9 +247,6 @@ ChatAssistantSettings::ChatAssistantSettings()
chatRenderer.setDefaultValue("rhi");
#endif
lastUsedRoleId.setSettingsKey(Constants::CA_LAST_USED_ROLE);
lastUsedRoleId.setDefaultValue("");
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
readSettings();
@@ -297,7 +289,6 @@ ChatAssistantSettings::ChatAssistantSettings()
Group{
title(Tr::tr("Chat Settings")),
Column{
linkOpenFiles,
autosave,
Row{autoCompress, autoCompressThreshold, Stretch{1}}}},
Space{8},
@@ -371,7 +362,6 @@ void ChatAssistantSettings::resetSettingsToDefaults()
resetAspect(thinkingBudgetTokens);
resetAspect(thinkingMaxTokens);
resetAspect(openAIResponsesReasoningEffort);
resetAspect(linkOpenFiles);
resetAspect(enableChatTools);
resetAspect(textFontFamily);
resetAspect(codeFontFamily);
@@ -397,18 +387,4 @@ public:
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

@@ -6,7 +6,6 @@
#include <utils/aspects.h>
#include "AgentRole.hpp"
#include "ButtonAspect.hpp"
namespace QodeAssist::Settings {
@@ -19,7 +18,6 @@ public:
ButtonAspect resetToDefaults{this};
// Chat settings
Utils::BoolAspect linkOpenFiles{this};
Utils::BoolAspect autosave{this};
Utils::BoolAspect enableChatInBottomToolBar{this};
Utils::BoolAspect enableChatInNavigationPanel{this};
@@ -69,8 +67,6 @@ public:
Utils::SelectionAspect chatRenderer{this};
Utils::StringAspect lastUsedRoleId{this};
private:
void setupConnections();
void resetSettingsToDefaults();

View File

@@ -303,14 +303,6 @@ CodeCompletionSettings::CodeCompletionSettings()
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(
@@ -389,8 +381,7 @@ CodeCompletionSettings::CodeCompletionSettings()
systemPromptForNonFimModels,
userMessageTemplateForCC,
customLanguages,
}},
Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}};
}}};
auto generalSettings = Column{
autoCompletion,
@@ -486,8 +477,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
resetAspect(readStringsAfterCursor);
resetAspect(useSystemPrompt);
resetAspect(systemPrompt);
resetAspect(useProjectChangesCache);
resetAspect(maxChangesCacheSize);
resetAspect(ollamaLivetime);
resetAspect(contextWindow);
resetAspect(openAIResponsesReasoningEffort);

View File

@@ -68,8 +68,6 @@ public:
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};

View File

@@ -82,7 +82,6 @@ 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";
@@ -122,6 +121,10 @@ const char MCP_SERVER_PORT[] = "QodeAssist.mcpServerPort";
const char MCP_ENABLE_CLIENTS[] = "QodeAssist.mcpEnableClients";
const char MCP_CLIENT_EXTRA_PATHS[] = "QodeAssist.mcpClientExtraPaths";
// ACP agent settings
const char AGENT_EXTRA_PATHS[] = "QodeAssist.agentExtraPaths";
const char AGENT_FORWARDED_VARIABLES[] = "QodeAssist.agentForwardedVariables";
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[]
@@ -133,6 +136,7 @@ const char QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID[]
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_AGENTS_SETTINGS_PAGE_ID[] = "QodeAssist.9AgentsSettingsPageId";
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist";
@@ -176,8 +180,6 @@ 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";
@@ -224,7 +226,6 @@ 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";