feat: Add predefined templates

This commit is contained in:
Petr Mironychev
2026-01-20 19:06:11 +01:00
parent b32433c336
commit a55c6ccfdb
5 changed files with 310 additions and 10 deletions

View File

@ -125,16 +125,30 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
## Configuration
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide:
### Quick Setup (Recommended for Beginners)
### Supported Providers
The Quick Setup feature provides one-click configuration for popular cloud AI models. Get started in 3 easy steps:
1. **Open QodeAssist Settings**
2. **Select a Preset** - Choose from the Quick Setup dropdown:
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
- **OpenAI** (gpt-5.2-codex)
- **Mistral AI** (Codestral 2501)
- **Google AI** (Gemini 2.5 Flash)
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
### Manual Provider Configuration
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider (includes Responses API support)
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
- **[Anthropic Claude](docs/claude-configuration.md)** - Cloud provider (manual setup)
- **[OpenAI](docs/openai-configuration.md)** - Cloud provider (includes Responses API support)
- **[Mistral AI](docs/mistral-configuration.md)** - Cloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Cloud provider
- **LM Studio** - Local LLM provider
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)

View File

@ -43,6 +43,93 @@ ConfigurationManager &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.5";
claudeOpus.provider = "Claude";
claudeOpus.model = "claude-opus-4-5-20251101";
claudeOpus.url = "https://api.anthropic.com";
claudeOpus.endpointMode = "Auto";
claudeOpus.customEndpoint = "";
claudeOpus.templateName = "Claude";
claudeOpus.type = type;
claudeOpus.isPredefined = true;
AIConfiguration claudeSonnet;
claudeSonnet.id = "preset_claude_sonnet";
claudeSonnet.name = "Claude Sonnet 4.5";
claudeSonnet.provider = "Claude";
claudeSonnet.model = "claude-sonnet-4-5-20250929";
claudeSonnet.url = "https://api.anthropic.com";
claudeSonnet.endpointMode = "Auto";
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.endpointMode = "Auto";
claudeHaiku.customEndpoint = "";
claudeHaiku.templateName = "Claude";
claudeHaiku.type = type;
claudeHaiku.isPredefined = true;
AIConfiguration mistralCodestral;
mistralCodestral.id = "preset_mistral_codestral";
mistralCodestral.name = "Mistral Codestral";
mistralCodestral.provider = "Mistral AI";
mistralCodestral.model = "codestral-2501";
mistralCodestral.url = "https://api.mistral.ai";
mistralCodestral.endpointMode = "Auto";
mistralCodestral.customEndpoint = "";
mistralCodestral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
mistralCodestral.type = type;
mistralCodestral.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.endpointMode = "Auto";
geminiFlash.customEndpoint = "";
geminiFlash.templateName = "Google AI";
geminiFlash.type = type;
geminiFlash.isPredefined = true;
AIConfiguration gpt52codex;
gpt52codex.id = "preset_gpt52codex";
gpt52codex.name = "gpt-5.2-codex";
gpt52codex.provider = "OpenAI Responses";
gpt52codex.model = "gpt-5.2-codex";
gpt52codex.url = "https://api.openai.com";
gpt52codex.endpointMode = "Auto";
gpt52codex.customEndpoint = "";
gpt52codex.templateName = "OpenAI Responses";
gpt52codex.type = type;
gpt52codex.isPredefined = true;
presets.append(claudeSonnet);
presets.append(claudeHaiku);
presets.append(claudeOpus);
presets.append(gpt52codex);
presets.append(mistralCodestral);
presets.append(geminiFlash);
return presets;
}
QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const
{
switch (type) {
@ -94,6 +181,9 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
configs->clear();
QVector<AIConfiguration> predefinedConfigs = getPredefinedConfigurations(type);
configs->append(predefinedConfigs);
if (!ensureDirectoryExists(type)) {
LOG_MESSAGE("Failed to create configuration directory");
return false;
@ -131,6 +221,7 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
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()));
@ -185,6 +276,12 @@ bool ConfigurationManager::saveConfiguration(const AIConfiguration &config)
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);

View File

@ -41,6 +41,7 @@ struct AIConfiguration
QString customEndpoint;
ConfigurationType type;
int formatVersion = CONFIGURATION_FORMAT_VERSION;
bool isPredefined = false;
};
class ConfigurationManager : public QObject
@ -58,6 +59,8 @@ public:
AIConfiguration getConfigurationById(const QString &id, ConfigurationType type) const;
QString getConfigurationDirectory(ConfigurationType type) const;
static QVector<AIConfiguration> getPredefinedConfigurations(ConfigurationType type);
signals:
void configurationsChanged(ConfigurationType type);

View File

@ -23,6 +23,7 @@
#include <coreplugin/icore.h>
#include <utils/layoutbuilder.h>
#include <utils/utilsicons.h>
#include <QComboBox>
#include <QDesktopServices>
#include <QDir>
#include <QInputDialog>
@ -88,6 +89,27 @@ GeneralSettings::GeneralSettings()
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE;
ccPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
ccPresetConfig.setLabelText(Tr::tr("Quick Setup"));
loadPresetConfigurations(ccPresetConfig, ConfigurationType::CodeCompletion);
ccConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
ccConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
caPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
caPresetConfig.setLabelText(Tr::tr("Quick Setup"));
loadPresetConfigurations(caPresetConfig, ConfigurationType::Chat);
caConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
caConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
qrPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
qrPresetConfig.setLabelText(Tr::tr("Quick Setup"));
loadPresetConfigurations(qrPresetConfig, ConfigurationType::QuickRefactor);
qrConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
qrConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
initStringAspect(ccProvider, Constants::CC_PROVIDER, TrConstants::PROVIDER, "Ollama");
ccProvider.setReadOnly(true);
@ -127,6 +149,7 @@ GeneralSettings::GeneralSettings()
ccSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
ccLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
ccLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
ccOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
ccOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
ccOpenConfigFolder.m_isCompact = true;
@ -218,6 +241,7 @@ GeneralSettings::GeneralSettings()
caSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
caLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
caLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
caOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
caOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
caOpenConfigFolder.m_isCompact = true;
@ -262,6 +286,7 @@ GeneralSettings::GeneralSettings()
qrSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
qrLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
qrLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
qrOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
qrOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
qrOpenConfigFolder.m_isCompact = true;
@ -325,17 +350,24 @@ GeneralSettings::GeneralSettings()
title(TrConstants::CODE_COMPLETION),
Column{
Row{ccSaveConfig, ccLoadConfig, ccOpenConfigFolder, Stretch{1}},
Row{ccPresetConfig, ccConfigureApiKey, Stretch{1}},
ccGrid,
Row{specifyPreset1, preset1Language, Stretch{1}},
ccPreset1Grid}};
auto caGroup = Group{
title(TrConstants::CHAT_ASSISTANT),
Column{Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}}, caGrid}};
Column{
Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}},
Row{caPresetConfig, caConfigureApiKey, Stretch{1}},
caGrid}};
auto qrGroup = Group{
title(TrConstants::QUICK_REFACTOR),
Column{Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}}, qrGrid}};
Column{
Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}},
Row{qrPresetConfig, qrConfigureApiKey, Stretch{1}},
qrGrid}};
auto rootLayout = Column{
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}},
@ -570,6 +602,33 @@ void GeneralSettings::setupConnections()
connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() {
QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent());
});
connect(&ccPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
applyPresetConfiguration(ccPresetConfig.volatileValue(), ConfigurationType::CodeCompletion);
ccPresetConfig.setValue(0);
});
connect(&ccConfigureApiKey, &ButtonAspect::clicked, this, []() {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&caPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
applyPresetConfiguration(caPresetConfig.volatileValue(), ConfigurationType::Chat);
caPresetConfig.setValue(0);
});
connect(&caConfigureApiKey, &ButtonAspect::clicked, this, []() {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&qrPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
applyPresetConfiguration(qrPresetConfig.volatileValue(), ConfigurationType::QuickRefactor);
qrPresetConfig.setValue(0);
});
connect(&qrConfigureApiKey, &ButtonAspect::clicked, this, []() {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
updatePreset1Visiblity(specifyPreset1.volatileValue());
@ -776,11 +835,33 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
SettingsDialog dialog(TrConstants::LOAD_CONFIGURATION);
dialog.addLabel(TrConstants::SELECT_CONFIGURATION);
int predefinedCount = 0;
for (const AIConfiguration &config : configs) {
if (config.isPredefined) {
predefinedCount++;
}
}
if (predefinedCount > 0) {
auto *hintLabel = dialog.addLabel(
Tr::tr("[Preset] configurations are predefined cloud models ready to use."));
QFont hintFont = hintLabel->font();
hintFont.setItalic(true);
hintFont.setPointSize(hintFont.pointSize() - 1);
hintLabel->setFont(hintFont);
hintLabel->setStyleSheet("color: gray;");
}
dialog.addSpacing();
QStringList configNames;
for (const AIConfiguration &config : configs) {
configNames.append(config.name);
QString displayName = config.name;
if (config.isPredefined) {
displayName = QString("[Preset] %1").arg(config.name);
}
configNames.append(displayName);
}
auto configList = dialog.addComboBox(configNames, QString());
@ -790,9 +871,31 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
auto *okButton = new QPushButton(TrConstants::OK);
auto *cancelButton = new QPushButton(TrConstants::CANCEL);
auto updateDeleteButtonState = [&]() {
int currentIndex = configList->currentIndex();
if (currentIndex >= 0 && currentIndex < configs.size()) {
deleteButton->setEnabled(!configs[currentIndex].isPredefined);
}
};
connect(configList,
QOverload<int>::of(&QComboBox::currentIndexChanged),
updateDeleteButtonState);
updateDeleteButtonState();
connect(deleteButton, &QPushButton::clicked, &dialog, [&]() {
int currentIndex = configList->currentIndex();
if (currentIndex >= 0 && currentIndex < configs.size()) {
const AIConfiguration &configToDelete = configs[currentIndex];
if (configToDelete.isPredefined) {
QMessageBox::information(
&dialog,
TrConstants::DELETE_CONFIGURATION,
Tr::tr("Predefined configurations cannot be deleted."));
return;
}
QMessageBox::StandardButton reply = QMessageBox::question(
&dialog,
TrConstants::DELETE_CONFIGURATION,
@ -800,7 +903,6 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
const AIConfiguration &configToDelete = configs[currentIndex];
if (manager.deleteConfiguration(configToDelete.id, type)) {
dialog.accept();
onLoadConfiguration(prefix);
@ -860,6 +962,73 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
dialog.exec();
}
void GeneralSettings::loadPresetConfigurations(Utils::SelectionAspect &aspect,
ConfigurationType type)
{
QVector<AIConfiguration> presets = ConfigurationManager::getPredefinedConfigurations(type);
if (type == ConfigurationType::CodeCompletion) {
m_ccPresets = presets;
} else if (type == ConfigurationType::Chat) {
m_caPresets = presets;
} else if (type == ConfigurationType::QuickRefactor) {
m_qrPresets = presets;
}
aspect.addOption(Tr::tr("-- Select Preset --"));
for (const AIConfiguration &config : presets) {
aspect.addOption(config.name);
}
aspect.setDefaultValue(0);
}
void GeneralSettings::applyPresetConfiguration(int index, ConfigurationType type)
{
if (index <= 0) {
return;
}
QVector<AIConfiguration> *presets = nullptr;
if (type == ConfigurationType::CodeCompletion) {
presets = &m_ccPresets;
} else if (type == ConfigurationType::Chat) {
presets = &m_caPresets;
} else if (type == ConfigurationType::QuickRefactor) {
presets = &m_qrPresets;
}
if (!presets || index - 1 >= presets->size()) {
return;
}
const AIConfiguration &config = presets->at(index - 1);
if (type == ConfigurationType::CodeCompletion) {
ccProvider.setValue(config.provider);
ccModel.setValue(config.model);
ccTemplate.setValue(config.templateName);
ccUrl.setValue(config.url);
ccEndpointMode.setValue(ccEndpointMode.indexForDisplay(config.endpointMode));
ccCustomEndpoint.setValue(config.customEndpoint);
} else if (type == ConfigurationType::Chat) {
caProvider.setValue(config.provider);
caModel.setValue(config.model);
caTemplate.setValue(config.templateName);
caUrl.setValue(config.url);
caEndpointMode.setValue(caEndpointMode.indexForDisplay(config.endpointMode));
caCustomEndpoint.setValue(config.customEndpoint);
} else if (type == ConfigurationType::QuickRefactor) {
qrProvider.setValue(config.provider);
qrModel.setValue(config.model);
qrTemplate.setValue(config.templateName);
qrUrl.setValue(config.url);
qrEndpointMode.setValue(qrEndpointMode.indexForDisplay(config.endpointMode));
qrCustomEndpoint.setValue(config.customEndpoint);
}
writeSettings();
}
class GeneralSettingsPage : public Core::IOptionsPage
{
public:

View File

@ -23,6 +23,7 @@
#include <QPointer>
#include "ButtonAspect.hpp"
#include "ConfigurationManager.hpp"
namespace Utils {
class DetailsWidget;
@ -46,6 +47,9 @@ public:
ButtonAspect resetToDefaults{this};
// code completion setttings
Utils::SelectionAspect ccPresetConfig{this};
ButtonAspect ccConfigureApiKey{this};
Utils::StringAspect ccProvider{this};
ButtonAspect ccSelectProvider{this};
@ -91,6 +95,9 @@ public:
ButtonAspect ccPreset1SelectTemplate{this};
// chat assistant settings
Utils::SelectionAspect caPresetConfig{this};
ButtonAspect caConfigureApiKey{this};
Utils::StringAspect caProvider{this};
ButtonAspect caSelectProvider{this};
@ -116,6 +123,9 @@ public:
ButtonAspect caOpenConfigFolder{this};
// quick refactor settings
Utils::SelectionAspect qrPresetConfig{this};
ButtonAspect qrConfigureApiKey{this};
Utils::StringAspect qrProvider{this};
ButtonAspect qrSelectProvider{this};
@ -162,10 +172,17 @@ public:
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();