refactor: Move to agent-session architecture

This commit is contained in:
Petr Mironychev
2026-06-01 11:47:52 +02:00
parent 02c11ee5a0
commit 6220308a93
66 changed files with 1764 additions and 4039 deletions

View File

@@ -126,7 +126,6 @@ add_qtc_plugin(QodeAssist
chat/ChatDocument.hpp chat/ChatDocument.cpp chat/ChatDocument.hpp chat/ChatDocument.cpp
chat/ChatEditor.hpp chat/ChatEditor.cpp chat/ChatEditor.hpp chat/ChatEditor.cpp
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
@@ -166,7 +165,7 @@ add_qtc_plugin(QodeAssist
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
) )
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines) target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable find_program(QtCreatorExecutable

View File

@@ -1,124 +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 "AgentRoleController.hpp"
#include <utils/aspects.h>
#include "AgentRole.hpp"
#include "ChatAssistantSettings.hpp"
#include "GeneralSettings.hpp"
namespace QodeAssist::Chat {
AgentRoleController::AgentRoleController(QObject *parent)
: QObject(parent)
{
connect(
&Settings::chatAssistantSettings().systemPrompt,
&Utils::BaseAspect::changed,
this,
&AgentRoleController::baseSystemPromptChanged);
loadAvailableRoles();
}
QStringList AgentRoleController::availableRoles() const
{
return m_availableRoles;
}
QString AgentRoleController::currentRole() const
{
return m_currentRole;
}
QString AgentRoleController::baseSystemPrompt() const
{
return Settings::chatAssistantSettings().systemPrompt();
}
QString AgentRoleController::currentRoleDescription() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
return role.description;
}
QString AgentRoleController::currentRoleSystemPrompt() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return QString();
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return QString();
return role.systemPrompt;
}
void AgentRoleController::loadAvailableRoles()
{
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
m_availableRoles.clear();
m_availableRoles.append(Settings::AgentRolesManager::getNoRole().name);
for (const auto &role : roles)
m_availableRoles.append(role.name);
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
m_currentRole = Settings::AgentRolesManager::getNoRole().name;
if (!lastRoleId.isEmpty()) {
for (const auto &role : roles) {
if (role.id == lastRoleId) {
m_currentRole = role.name;
break;
}
}
}
emit availableRolesChanged();
emit currentRoleChanged();
}
void AgentRoleController::applyRole(const QString &roleName)
{
auto &settings = Settings::chatAssistantSettings();
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
settings.lastUsedRoleId.setValue("");
settings.writeSettings();
m_currentRole = roleName;
emit currentRoleChanged();
return;
}
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
for (const auto &role : roles) {
if (role.name == roleName) {
settings.lastUsedRoleId.setValue(role.id);
settings.writeSettings();
m_currentRole = role.name;
emit currentRoleChanged();
break;
}
}
}
void AgentRoleController::openSettings()
{
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
}
} // namespace QodeAssist::Chat

View File

@@ -1,39 +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 <QObject>
#include <QStringList>
namespace QodeAssist::Chat {
class AgentRoleController : public QObject
{
Q_OBJECT
public:
explicit AgentRoleController(QObject *parent = nullptr);
QStringList availableRoles() const;
QString currentRole() const;
QString baseSystemPrompt() const;
QString currentRoleDescription() const;
QString currentRoleSystemPrompt() const;
void loadAvailableRoles();
void applyRole(const QString &roleName);
void openSettings();
signals:
void availableRolesChanged();
void currentRoleChanged();
void baseSystemPromptChanged();
private:
QStringList m_availableRoles;
QString m_currentRole;
};
} // namespace QodeAssist::Chat

View File

@@ -75,8 +75,7 @@ qt_add_qml_module(QodeAssistChatView
FileItem.hpp FileItem.cpp FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.cpp ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp ChatCompressor.hpp ChatCompressor.cpp
AgentRoleController.hpp AgentRoleController.cpp ChatAgentController.hpp ChatAgentController.cpp
ChatConfigurationController.hpp ChatConfigurationController.cpp
FileEditController.hpp FileEditController.cpp FileEditController.hpp FileEditController.cpp
InputTokenCounter.hpp InputTokenCounter.cpp InputTokenCounter.hpp InputTokenCounter.cpp
ChatHistoryStore.hpp ChatHistoryStore.cpp ChatHistoryStore.hpp ChatHistoryStore.cpp
@@ -92,13 +91,14 @@ target_link_libraries(QodeAssistChatView
Qt::Network Qt::Network
QtCreator::Core QtCreator::Core
QtCreator::Utils QtCreator::Utils
PluginLLMCore
QodeAssistSettings QodeAssistSettings
Context Context
QodeAssistUIControlsplugin QodeAssistUIControlsplugin
QodeAssistLogger QodeAssistLogger
LLMQore LLMQore
Skills Skills
Agents
Session
) )
target_include_directories(QodeAssistChatView target_include_directories(QodeAssistChatView

View File

@@ -0,0 +1,91 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatAgentController.hpp"
#include <QSettings>
#include <coreplugin/icore.h>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
namespace QodeAssist::Chat {
namespace {
const char kChatAgentKey[] = "QodeAssist.chatActiveAgent";
}
ChatAgentController::ChatAgentController(QObject *parent)
: QObject(parent)
{
if (auto *settings = Core::ICore::settings())
m_currentAgent = settings->value(kChatAgentKey).toString();
}
void ChatAgentController::setAgentFactory(AgentFactory *factory)
{
m_agentFactory = factory;
reload();
}
QStringList ChatAgentController::availableAgents() const
{
return m_availableAgents;
}
QString ChatAgentController::currentAgent() const
{
return m_currentAgent;
}
void ChatAgentController::setCurrentAgent(const QString &name)
{
if (name == m_currentAgent || !m_availableAgents.contains(name))
return;
m_currentAgent = name;
if (auto *settings = Core::ICore::settings())
settings->setValue(kChatAgentKey, m_currentAgent);
emit currentAgentChanged();
}
void ChatAgentController::reload()
{
m_availableAgents = m_agentFactory ? m_agentFactory->configNames() : QStringList{};
emit availableAgentsChanged();
ensureValidCurrent();
}
void ChatAgentController::ensureValidCurrent()
{
if (m_availableAgents.contains(m_currentAgent))
return;
const QString next = m_availableAgents.isEmpty() ? QString() : m_availableAgents.first();
if (next == m_currentAgent)
return;
m_currentAgent = next;
if (auto *settings = Core::ICore::settings())
settings->setValue(kChatAgentKey, m_currentAgent);
emit currentAgentChanged();
}
bool ChatAgentController::currentSupportsThinking() const
{
if (!m_agentFactory || m_currentAgent.isEmpty())
return false;
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
return config && config->enableThinking;
}
bool ChatAgentController::currentSupportsTools() const
{
if (!m_agentFactory || m_currentAgent.isEmpty())
return false;
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
return config && config->enableTools;
}
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,47 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QPointer>
#include <QString>
#include <QStringList>
namespace QodeAssist {
class AgentFactory;
}
namespace QodeAssist::Chat {
class ChatAgentController : public QObject
{
Q_OBJECT
public:
explicit ChatAgentController(QObject *parent = nullptr);
void setAgentFactory(AgentFactory *factory);
QStringList availableAgents() const;
QString currentAgent() const;
void setCurrentAgent(const QString &name);
bool currentSupportsThinking() const;
bool currentSupportsTools() const;
void reload();
signals:
void availableAgentsChanged();
void currentAgentChanged();
private:
void ensureValidCurrent();
QPointer<AgentFactory> m_agentFactory;
QStringList m_availableAgents;
QString m_currentAgent;
};
} // namespace QodeAssist::Chat

View File

@@ -4,13 +4,21 @@
#include "ChatCompressor.hpp" #include "ChatCompressor.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include <QDateTime> #include <QDateTime>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@@ -25,6 +33,16 @@ ChatCompressor::ChatCompressor(QObject *parent)
: QObject(parent) : QObject(parent)
{} {}
void ChatCompressor::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void ChatCompressor::setActiveAgent(const QString &agentName)
{
m_activeAgent = agentName;
}
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel) void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
{ {
if (m_isCompressing) { if (m_isCompressing) {
@@ -42,20 +60,23 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
return; return;
} }
auto providerName = Settings::generalSettings().caProvider(); if (!m_sessionManager) {
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); emit compressionFailed(tr("Chat session manager is not available"));
if (!m_provider) {
emit compressionFailed(tr("No provider available"));
return; return;
} }
auto templateName = Settings::generalSettings().caTemplate(); QString sessionError;
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName( Session *session = m_sessionManager->createSession(m_activeAgent, &sessionError);
templateName); if (!session) {
emit compressionFailed(
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
return;
}
if (!promptTemplate) { auto *client = session->client();
emit compressionFailed(tr("No template available")); if (!client) {
m_sessionManager->removeSession(session);
emit compressionFailed(tr("Chat agent has no live client"));
return; return;
} }
@@ -63,23 +84,52 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
m_chatModel = chatModel; m_chatModel = chatModel;
m_originalChatPath = chatFilePath; m_originalChatPath = chatFilePath;
m_accumulatedSummary.clear(); m_accumulatedSummary.clear();
m_session = session;
emit compressionStarted(); emit compressionStarted();
connectProviderSignals(); session->systemPrompt()->setLayer(
QStringLiteral("compression"),
QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of "
"discussion."));
QJsonObject payload{ auto *history = session->history();
{"model", Settings::generalSettings().caModel()}, {"stream", true}}; for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit
|| msg.role == ChatModel::ChatRole::Thinking)
continue;
if (msg.content.trimmed().isEmpty())
continue;
buildRequestPayload(payload, promptTemplate); Message apiMessage(
msg.role == ChatModel::ChatRole::User ? Message::Role::User : Message::Role::Assistant);
apiMessage.appendBlock(std::make_unique<LLMQore::TextContent>(msg.content));
history->append(std::move(apiMessage));
}
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint(); m_connections.append(connect(
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint client, &::LLMQore::BaseClient::chunkReceived,
: promptTemplate->endpoint(); this, &ChatCompressor::onPartialResponseReceived, Qt::UniqueConnection));
m_provider->client()->setTransferTimeout( m_connections.append(connect(
client, &::LLMQore::BaseClient::requestCompleted,
this, &ChatCompressor::onFullResponseReceived, Qt::UniqueConnection));
m_connections.append(connect(
client, &::LLMQore::BaseClient::requestFailed,
this, &ChatCompressor::onRequestFailed, Qt::UniqueConnection));
client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000)); static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
m_currentRequestId = m_provider->sendRequest(
QUrl(Settings::generalSettings().caUrl()), payload, endpoint); std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<LLMQore::TextContent>(buildCompressionPrompt()));
m_currentRequestId = session->send(std::move(blocks), /*toolsOverride=*/false);
if (m_currentRequestId.isEmpty()) {
handleCompressionError(tr("Failed to start compression request"));
return;
}
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId)); LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
} }
@@ -94,10 +144,6 @@ void ChatCompressor::cancelCompression()
return; return;
LOG_MESSAGE("Cancelling compression request"); LOG_MESSAGE("Cancelling compression request");
if (m_provider && !m_currentRequestId.isEmpty())
m_provider->cancelRequest(m_currentRequestId);
cleanupState(); cleanupState();
emit compressionFailed(tr("Compression cancelled")); emit compressionFailed(tr("Compression cancelled"));
} }
@@ -120,14 +166,18 @@ void ChatCompressor::onFullResponseReceived(const QString &requestId, const QStr
LOG_MESSAGE( LOG_MESSAGE(
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length())); QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
QString compressedPath = createCompressedChatPath(m_originalChatPath); const QString compressedPath = createCompressedChatPath(m_originalChatPath);
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) { const QString summary = m_accumulatedSummary;
handleCompressionError(tr("Failed to save compressed chat")); const QString sourcePath = m_originalChatPath;
cleanupState();
if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
emit compressionFailed(tr("Failed to save compressed chat"));
return; return;
} }
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath)); LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
cleanupState();
emit compressionCompleted(compressedPath); emit compressionCompleted(compressedPath);
} }
@@ -168,39 +218,6 @@ QString ChatCompressor::buildCompressionPrompt() const
"Create the summary now:"); "Create the summary now:");
} }
void ChatCompressor::buildRequestPayload(
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
{
PluginLLMCore::ContextData context;
context.systemPrompt = QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of discussion.");
QVector<PluginLLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool
|| msg.role == ChatModel::ChatRole::FileEdit
|| msg.role == ChatModel::ChatRole::Thinking)
continue;
PluginLLMCore::Message apiMessage;
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
apiMessage.content = msg.content;
messages.append(apiMessage);
}
PluginLLMCore::Message compressionRequest;
compressionRequest.role = "user";
compressionRequest.content = buildCompressionPrompt();
messages.append(compressionRequest);
context.history = messages;
m_provider->prepareRequest(
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
}
bool ChatCompressor::createCompressedChatFile( bool ChatCompressor::createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary) const QString &sourcePath, const QString &destPath, const QString &summary)
{ {
@@ -247,32 +264,6 @@ bool ChatCompressor::createCompressedChatFile(
return true; return true;
} }
void ChatCompressor::connectProviderSignals()
{
auto *c = m_provider->client();
m_connections.append(connect(
c,
&::LLMQore::BaseClient::chunkReceived,
this,
&ChatCompressor::onPartialResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestCompleted,
this,
&ChatCompressor::onFullResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestFailed,
this,
&ChatCompressor::onRequestFailed,
Qt::UniqueConnection));
}
void ChatCompressor::disconnectAllSignals() void ChatCompressor::disconnectAllSignals()
{ {
for (const auto &connection : std::as_const(m_connections)) for (const auto &connection : std::as_const(m_connections))
@@ -284,12 +275,17 @@ void ChatCompressor::cleanupState()
{ {
disconnectAllSignals(); disconnectAllSignals();
Session *session = m_session;
m_isCompressing = false; m_isCompressing = false;
m_currentRequestId.clear(); m_currentRequestId.clear();
m_originalChatPath.clear(); m_originalChatPath.clear();
m_accumulatedSummary.clear(); m_accumulatedSummary.clear();
m_chatModel = nullptr; m_chatModel = nullptr;
m_provider = nullptr; m_session = nullptr;
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
} }
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -4,15 +4,15 @@
#pragma once #pragma once
#include <QJsonObject>
#include <QList> #include <QList>
#include <QObject> #include <QObject>
#include <QPointer>
#include <QString> #include <QString>
namespace QodeAssist::PluginLLMCore { namespace QodeAssist {
class Provider; class SessionManager;
class PromptTemplate; class Session;
} // namespace QodeAssist::PluginLLMCore }
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -25,6 +25,9 @@ class ChatCompressor : public QObject
public: public:
explicit ChatCompressor(QObject *parent = nullptr); explicit ChatCompressor(QObject *parent = nullptr);
void setSessionManager(SessionManager *sessionManager);
void setActiveAgent(const QString &agentName);
void startCompression(const QString &chatFilePath, ChatModel *chatModel); void startCompression(const QString &chatFilePath, ChatModel *chatModel);
bool isCompressing() const; bool isCompressing() const;
@@ -45,17 +48,17 @@ private:
QString buildCompressionPrompt() const; QString buildCompressionPrompt() const;
bool createCompressedChatFile( bool createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary); const QString &sourcePath, const QString &destPath, const QString &summary);
void connectProviderSignals();
void disconnectAllSignals(); void disconnectAllSignals();
void cleanupState(); void cleanupState();
void handleCompressionError(const QString &error); void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false; bool m_isCompressing = false;
QString m_currentRequestId; QString m_currentRequestId;
QString m_originalChatPath; QString m_originalChatPath;
QString m_accumulatedSummary; QString m_accumulatedSummary;
PluginLLMCore::Provider *m_provider = nullptr; QPointer<SessionManager> m_sessionManager;
QString m_activeAgent;
QPointer<Session> m_session;
ChatModel *m_chatModel = nullptr; ChatModel *m_chatModel = nullptr;
QList<QMetaObject::Connection> m_connections; QList<QMetaObject::Connection> m_connections;

View File

@@ -1,100 +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 "ChatConfigurationController.hpp"
#include <utils/aspects.h>
#include "ConfigurationManager.hpp"
#include "GeneralSettings.hpp"
namespace QodeAssist::Chat {
ChatConfigurationController::ChatConfigurationController(QObject *parent)
: QObject(parent)
{
auto &settings = Settings::generalSettings();
connect(
&settings.caProvider,
&Utils::BaseAspect::changed,
this,
&ChatConfigurationController::updateCurrentConfiguration);
connect(
&settings.caModel,
&Utils::BaseAspect::changed,
this,
&ChatConfigurationController::updateCurrentConfiguration);
loadAvailableConfigurations();
}
QStringList ChatConfigurationController::availableConfigurations() const
{
return m_availableConfigurations;
}
QString ChatConfigurationController::currentConfiguration() const
{
return m_currentConfiguration;
}
void ChatConfigurationController::updateCurrentConfiguration()
{
auto &settings = Settings::generalSettings();
m_currentConfiguration
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
emit currentConfigurationChanged();
}
void ChatConfigurationController::loadAvailableConfigurations()
{
auto &manager = Settings::ConfigurationManager::instance();
manager.loadConfigurations(Settings::ConfigurationType::Chat);
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::Chat);
m_availableConfigurations.clear();
m_availableConfigurations.append(QObject::tr("Current Settings"));
for (const Settings::AIConfiguration &config : configs) {
m_availableConfigurations.append(config.name);
}
updateCurrentConfiguration();
emit availableConfigurationsChanged();
}
void ChatConfigurationController::applyConfiguration(const QString &configName)
{
if (configName == QObject::tr("Current Settings")) {
return;
}
auto &manager = Settings::ConfigurationManager::instance();
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::Chat);
for (const Settings::AIConfiguration &config : configs) {
if (config.name == configName) {
auto &settings = Settings::generalSettings();
settings.caProvider.setValue(config.provider);
settings.caModel.setValue(config.model);
settings.caTemplate.setValue(config.templateName);
settings.caUrl.setValue(config.url);
settings.caCustomEndpoint.setValue(config.customEndpoint);
settings.writeSettings();
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
emit currentConfigurationChanged();
break;
}
}
}
} // namespace QodeAssist::Chat

View File

@@ -1,36 +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 <QObject>
#include <QStringList>
namespace QodeAssist::Chat {
class ChatConfigurationController : public QObject
{
Q_OBJECT
public:
explicit ChatConfigurationController(QObject *parent = nullptr);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
void loadAvailableConfigurations();
void applyConfiguration(const QString &configName);
signals:
void availableConfigurationsChanged();
void currentConfigurationChanged();
private:
void updateCurrentConfiguration();
QStringList m_availableConfigurations;
QString m_currentConfiguration;
};
} // namespace QodeAssist::Chat

View File

@@ -28,9 +28,11 @@
#include "QodeAssistConstants.hpp" #include "QodeAssistConstants.hpp"
#include "AgentRoleController.hpp" #include <AgentFactory.hpp>
#include <SessionManager.hpp>
#include "ChatAgentController.hpp"
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatConfigurationController.hpp"
#include "ChatCompressor.hpp" #include "ChatCompressor.hpp"
#include "ChatHistoryStore.hpp" #include "ChatHistoryStore.hpp"
#include "FileEditController.hpp" #include "FileEditController.hpp"
@@ -38,10 +40,8 @@
#include "InputTokenCounter.hpp" #include "InputTokenCounter.hpp"
#include "SettingsConstants.hpp" #include "SettingsConstants.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "SessionFileRegistry.hpp" #include "SessionFileRegistry.hpp"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "pluginllmcore/RulesLoader.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
#include "SkillsSettings.hpp" #include "SkillsSettings.hpp"
#include "sources/skills/SkillsManager.hpp" #include "sources/skills/SkillsManager.hpp"
@@ -74,13 +74,11 @@ QKeySequence sendMessageKeySequence()
ChatRootView::ChatRootView(QQuickItem *parent) ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent) : QQuickItem(parent)
, m_chatModel(new ChatModel(this)) , m_chatModel(new ChatModel(this))
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance()) , m_clientInterface(new ClientInterface(m_chatModel, this))
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_fileManager(new ChatFileManager(this)) , m_fileManager(new ChatFileManager(this))
, m_isRequestInProgress(false) , m_isRequestInProgress(false)
, m_chatCompressor(new ChatCompressor(this)) , m_chatCompressor(new ChatCompressor(this))
, m_agentRoleController(new AgentRoleController(this)) , m_agentController(new ChatAgentController(this))
, m_configurationController(new ChatConfigurationController(this))
, m_fileEditController(new FileEditController(m_chatModel, this)) , m_fileEditController(new FileEditController(m_chatModel, this))
, m_tokenCounter( , m_tokenCounter(
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this)) new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
@@ -109,22 +107,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
auto &settings = Settings::generalSettings();
connect(
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
connect(
m_configurationController,
&ChatConfigurationController::availableConfigurationsChanged,
this,
&ChatRootView::availableConfigurationsChanged);
connect(
m_configurationController,
&ChatConfigurationController::currentConfigurationChanged,
this,
&ChatRootView::currentConfigurationChanged);
connect( connect(
m_clientInterface, m_clientInterface,
&ClientInterface::messageReceivedCompletely, &ClientInterface::messageReceivedCompletely,
@@ -171,20 +153,20 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::inputTokensCountChanged); &ChatRootView::inputTokensCountChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::availableRolesChanged, &ChatAgentController::availableAgentsChanged,
this, this,
&ChatRootView::availableAgentRolesChanged); &ChatRootView::availableChatAgentsChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::currentRoleChanged, &ChatAgentController::currentAgentChanged,
this, this,
&ChatRootView::currentAgentRoleChanged); &ChatRootView::currentChatAgentChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::baseSystemPromptChanged, &ChatAgentController::currentAgentChanged,
this, this,
&ChatRootView::baseSystemPromptChanged); &ChatRootView::isThinkingSupportChanged);
auto editors = Core::EditorManager::instance(); auto editors = Core::EditorManager::instance();
@@ -266,14 +248,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect( connect(
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory); m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
refreshRules();
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
this,
&ChatRootView::refreshRules);
connect( connect(
ProjectExplorer::ProjectManager::instance(), ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectAdded, &ProjectExplorer::ProjectManager::projectAdded,
@@ -298,12 +272,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::useThinkingChanged); &ChatRootView::useThinkingChanged);
connect(
&Settings::generalSettings().caProvider,
&Utils::BaseAspect::changed,
this,
&ChatRootView::isThinkingSupportChanged);
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) { connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
m_lastErrorMessage = error; m_lastErrorMessage = error;
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
@@ -373,6 +341,48 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
return m_skillsManager; return m_skillsManager;
} }
AgentFactory *ChatRootView::agentFactory() const
{
if (!m_agentFactory) {
if (auto *engine = qmlEngine(this)) {
m_agentFactory = qobject_cast<AgentFactory *>(
engine->rootContext()->contextProperty("agentFactory").value<QObject *>());
}
}
return m_agentFactory;
}
SessionManager *ChatRootView::sessionManager() const
{
if (!m_sessionManager) {
if (auto *engine = qmlEngine(this)) {
m_sessionManager = qobject_cast<SessionManager *>(
engine->rootContext()->contextProperty("sessionManager").value<QObject *>());
}
}
return m_sessionManager;
}
void ChatRootView::loadAvailableChatAgents()
{
m_agentController->setAgentFactory(agentFactory());
}
QStringList ChatRootView::availableChatAgents() const
{
return m_agentController->availableAgents();
}
QString ChatRootView::currentChatAgent() const
{
return m_agentController->currentAgent();
}
void ChatRootView::setCurrentChatAgent(const QString &name)
{
m_agentController->setCurrentAgent(name);
}
QVariantList ChatRootView::searchSkills(const QString &query) const QVariantList ChatRootView::searchSkills(const QString &query) const
{ {
QVariantList results; QVariantList results;
@@ -380,7 +390,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
if (!manager || !Settings::skillsSettings().enableSkills()) if (!manager || !Settings::skillsSettings().enableSkills())
return results; return results;
auto *project = PluginLLMCore::RulesLoader::getActiveProject(); auto *project = ProjectExplorer::ProjectManager::startupProject();
QStringList projectSkillDirs; QStringList projectSkillDirs;
if (project) { if (project) {
Settings::ProjectSettings projectSettings(project); Settings::ProjectSettings projectSettings(project);
@@ -481,7 +491,12 @@ void ChatRootView::dispatchSend(
m_tokenCounter->recordSent(); m_tokenCounter->recordSent();
if (currentChatAgent().isEmpty())
loadAvailableChatAgents();
m_clientInterface->setSkillsManager(skillsManager()); m_clientInterface->setSkillsManager(skillsManager());
m_clientInterface->setSessionManager(sessionManager());
m_clientInterface->setActiveAgent(currentChatAgent());
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg); m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
m_fileManager->clearIntermediateStorage(); m_fileManager->clearIntermediateStorage();
@@ -527,12 +542,6 @@ void ChatRootView::clearMessages()
clearLinkedFiles(); clearLinkedFiles();
} }
QString ChatRootView::currentTemplate() const
{
auto &settings = Settings::generalSettings();
return settings.caModel();
}
void ChatRootView::saveHistory(const QString &filePath) void ChatRootView::saveHistory(const QString &filePath)
{ {
if (filePath != m_recentFilePath) { if (filePath != m_recentFilePath) {
@@ -821,25 +830,6 @@ void ChatRootView::openChatHistoryFolder()
m_historyStore->openHistoryFolder(); m_historyStore->openHistoryFolder();
} }
void ChatRootView::openRulesFolder()
{
auto project = ProjectExplorer::ProjectManager::startupProject();
if (!project) {
return;
}
QString projectPath = project->projectDirectory().toFSPathString();
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
QDir dir(rulesPath);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
void ChatRootView::openSettings() void ChatRootView::openSettings()
{ {
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
@@ -1120,51 +1110,6 @@ QString ChatRootView::lastErrorMessage() const
return m_lastErrorMessage; return m_lastErrorMessage;
} }
QVariantList ChatRootView::activeRules() const
{
return m_activeRules;
}
int ChatRootView::activeRulesCount() const
{
return m_activeRules.size();
}
QString ChatRootView::getRuleContent(int index)
{
if (index < 0 || index >= m_activeRules.size())
return QString();
return PluginLLMCore::RulesLoader::loadRuleFileContent(
m_activeRules[index].toMap()["filePath"].toString());
}
void ChatRootView::refreshRules()
{
m_activeRules.clear();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (!project) {
emit activeRulesChanged();
emit activeRulesCountChanged();
return;
}
auto ruleFiles
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
for (const auto &ruleFile : ruleFiles) {
QVariantMap ruleMap;
ruleMap["filePath"] = ruleFile.filePath;
ruleMap["fileName"] = ruleFile.fileName;
ruleMap["category"] = ruleFile.category;
m_activeRules.append(ruleMap);
}
emit activeRulesChanged();
emit activeRulesCountChanged();
}
bool ChatRootView::useTools() const bool ChatRootView::useTools() const
{ {
return Settings::chatAssistantSettings().enableChatTools(); return Settings::chatAssistantSettings().enableChatTools();
@@ -1249,10 +1194,7 @@ QString ChatRootView::lastInfoMessage() const
bool ChatRootView::isThinkingSupport() const bool ChatRootView::isThinkingSupport() const
{ {
auto providerName = Settings::generalSettings().caProvider(); return m_agentController->currentSupportsThinking();
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
} }
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
@@ -1273,66 +1215,6 @@ bool ChatRootView::isImageFile(const QString &filePath) const
return imageExtensions.contains(fileInfo.suffix().toLower()); return imageExtensions.contains(fileInfo.suffix().toLower());
} }
void ChatRootView::loadAvailableConfigurations()
{
m_configurationController->loadAvailableConfigurations();
}
void ChatRootView::applyConfiguration(const QString &configName)
{
m_configurationController->applyConfiguration(configName);
}
QStringList ChatRootView::availableConfigurations() const
{
return m_configurationController->availableConfigurations();
}
QString ChatRootView::currentConfiguration() const
{
return m_configurationController->currentConfiguration();
}
void ChatRootView::loadAvailableAgentRoles()
{
m_agentRoleController->loadAvailableRoles();
}
void ChatRootView::applyAgentRole(const QString &roleName)
{
m_agentRoleController->applyRole(roleName);
}
QStringList ChatRootView::availableAgentRoles() const
{
return m_agentRoleController->availableRoles();
}
QString ChatRootView::currentAgentRole() const
{
return m_agentRoleController->currentRole();
}
QString ChatRootView::baseSystemPrompt() const
{
return m_agentRoleController->baseSystemPrompt();
}
QString ChatRootView::currentAgentRoleDescription() const
{
return m_agentRoleController->currentRoleDescription();
}
QString ChatRootView::currentAgentRoleSystemPrompt() const
{
return m_agentRoleController->currentRoleSystemPrompt();
}
void ChatRootView::openAgentRolesSettings()
{
m_agentRoleController->openSettings();
}
void ChatRootView::compressCurrentChat() void ChatRootView::compressCurrentChat()
{ {
if (m_chatCompressor->isCompressing()) { if (m_chatCompressor->isCompressing()) {
@@ -1349,6 +1231,10 @@ void ChatRootView::compressCurrentChat()
autosave(); autosave();
if (currentChatAgent().isEmpty())
loadAvailableChatAgents();
m_chatCompressor->setSessionManager(sessionManager());
m_chatCompressor->setActiveAgent(currentChatAgent());
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel); m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
} }

View File

@@ -11,18 +11,21 @@
#include "ChatFileManager.hpp" #include "ChatFileManager.hpp"
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include "pluginllmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Skills { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
namespace QodeAssist {
class AgentFactory;
class SessionManager;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatCompressor; class ChatCompressor;
class AgentRoleController; class ChatAgentController;
class ChatConfigurationController;
class FileEditController; class FileEditController;
class InputTokenCounter; class InputTokenCounter;
class ChatHistoryStore; class ChatHistoryStore;
@@ -32,7 +35,6 @@ class ChatRootView : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL) Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL) Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL) Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL) Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
@@ -46,8 +48,6 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL) Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL) Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL) Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL) Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL) Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL) Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
@@ -57,13 +57,8 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL) Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL) Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL) Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL) Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
@@ -75,7 +70,6 @@ public:
~ChatRootView() override; ~ChatRootView() override;
ChatModel *chatModel() const; ChatModel *chatModel() const;
QString currentTemplate() const;
void saveHistory(const QString &filePath); void saveHistory(const QString &filePath);
void loadHistory(const QString &filePath); void loadHistory(const QString &filePath);
@@ -104,7 +98,6 @@ public:
QString sendShortcutText() const; QString sendShortcutText() const;
Q_INVOKABLE void setIsSyncOpenFiles(bool state); Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder(); Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void openRulesFolder();
Q_INVOKABLE void openSettings(); Q_INVOKABLE void openSettings();
Q_INVOKABLE void openFileInEditor(const QString &filePath); Q_INVOKABLE void openFileInEditor(const QString &filePath);
@@ -139,11 +132,6 @@ public:
void setRequestProgressStatus(bool state); void setRequestProgressStatus(bool state);
QString lastErrorMessage() const; QString lastErrorMessage() const;
QVariantList activeRules() const;
int activeRulesCount() const;
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
Q_INVOKABLE QVariantList searchSkills(const QString &query) const; Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
@@ -161,23 +149,14 @@ public:
Q_INVOKABLE void undoAllFileEditsForCurrentMessage(); Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats(); Q_INVOKABLE void updateCurrentMessageEditsStats();
Q_INVOKABLE void loadAvailableConfigurations();
Q_INVOKABLE void applyConfiguration(const QString &configName);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
Q_INVOKABLE void compressCurrentChat(); Q_INVOKABLE void compressCurrentChat();
Q_INVOKABLE void cancelCompression(); Q_INVOKABLE void cancelCompression();
Q_INVOKABLE void loadAvailableAgentRoles(); Q_INVOKABLE void loadAvailableChatAgents();
Q_INVOKABLE void applyAgentRole(const QString &roleId); QStringList availableChatAgents() const;
Q_INVOKABLE void openAgentRolesSettings(); QString currentChatAgent() const;
QStringList availableAgentRoles() const; void setCurrentChatAgent(const QString &name);
QString currentAgentRole() const;
QString baseSystemPrompt() const;
QString currentAgentRoleDescription() const;
QString currentAgentRoleSystemPrompt() const;
int currentMessageTotalEdits() const; int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const; int currentMessageAppliedEdits() const;
int currentMessagePendingEdits() const; int currentMessagePendingEdits() const;
@@ -206,7 +185,6 @@ public slots:
signals: signals:
void chatModelChanged(); void chatModelChanged();
void currentTemplateChanged();
void attachmentFilesChanged(); void attachmentFilesChanged();
void linkedFilesChanged(); void linkedFilesChanged();
void inputTokensCountChanged(); void inputTokensCountChanged();
@@ -223,20 +201,15 @@ signals:
void lastErrorMessageChanged(); void lastErrorMessageChanged();
void lastInfoMessageChanged(); void lastInfoMessageChanged();
void sendShortcutTextChanged(); void sendShortcutTextChanged();
void activeRulesChanged();
void activeRulesCountChanged();
void useToolsChanged(); void useToolsChanged();
void useThinkingChanged(); void useThinkingChanged();
void currentMessageEditsStatsChanged(); void currentMessageEditsStatsChanged();
void isThinkingSupportChanged(); void isThinkingSupportChanged();
void availableConfigurationsChanged();
void currentConfigurationChanged();
void availableAgentRolesChanged(); void availableChatAgentsChanged();
void currentAgentRoleChanged(); void currentChatAgentChanged();
void baseSystemPromptChanged();
void isCompressingChanged(); void isCompressingChanged();
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
@@ -269,12 +242,12 @@ private:
SessionFileRegistry *sessionFileRegistry() const; SessionFileRegistry *sessionFileRegistry() const;
Skills::SkillsManager *skillsManager() const; Skills::SkillsManager *skillsManager() const;
AgentFactory *agentFactory() const;
SessionManager *sessionManager() const;
ChatModel *m_chatModel; ChatModel *m_chatModel;
PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
ChatFileManager *m_fileManager; ChatFileManager *m_fileManager;
QString m_currentTemplate;
QString m_recentFilePath; QString m_recentFilePath;
QStringList m_attachmentFiles; QStringList m_attachmentFiles;
QStringList m_linkedFiles; QStringList m_linkedFiles;
@@ -294,13 +267,11 @@ private:
QList<Core::IEditor *> m_currentEditors; QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress; bool m_isRequestInProgress;
QString m_lastErrorMessage; QString m_lastErrorMessage;
QVariantList m_activeRules;
QString m_lastInfoMessage; QString m_lastInfoMessage;
ChatCompressor *m_chatCompressor; ChatCompressor *m_chatCompressor;
AgentRoleController *m_agentRoleController; ChatAgentController *m_agentController;
ChatConfigurationController *m_configurationController;
FileEditController *m_fileEditController; FileEditController *m_fileEditController;
InputTokenCounter *m_tokenCounter; InputTokenCounter *m_tokenCounter;
ChatHistoryStore *m_historyStore; ChatHistoryStore *m_historyStore;
@@ -308,6 +279,8 @@ private:
mutable bool m_sessionFileRegistryResolved = false; mutable bool m_sessionFileRegistryResolved = false;
mutable QPointer<Skills::SkillsManager> m_skillsManager; mutable QPointer<Skills::SkillsManager> m_skillsManager;
mutable bool m_skillsManagerResolved = false; mutable bool m_skillsManagerResolved = false;
mutable QPointer<AgentFactory> m_agentFactory;
mutable QPointer<SessionManager> m_sessionManager;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -4,65 +4,75 @@
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include <LLMQore/BaseClient.hpp> #include <memory>
#include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/idocument.h>
#include <projectexplorer/buildconfiguration.h> #include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/target.h> #include <projectexplorer/target.h>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QImageReader>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QRegularExpression> #include <QRegularExpression>
#include <QUuid> #include <QUuid>
#include <coreplugin/editormanager/editormanager.h> #include <ConversationHistory.hpp>
#include <coreplugin/editormanager/ieditor.h> #include <Message.hpp>
#include <coreplugin/idocument.h> #include <Session.hpp>
#include <projectexplorer/project.h> #include <SessionManager.hpp>
#include <projectexplorer/projectexplorer.h> #include <SystemPromptBuilder.hpp>
#include <projectexplorer/projectmanager.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <LLMQore/ToolsManager.hpp>
#include "tools/ReadOriginalHistoryTool.hpp" #include "tools/ReadOriginalHistoryTool.hpp"
#include "tools/TodoTool.hpp" #include "tools/TodoTool.hpp"
#include "tools/ToolsRegistration.hpp"
#include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
#include "ProvidersManager.hpp"
#include "SkillsSettings.hpp" #include "SkillsSettings.hpp"
#include "ToolsSettings.hpp" #include "ToolsSettings.hpp"
#include <RulesLoader.hpp>
#include <context/ChangesManager.h> #include <context/ChangesManager.h>
#include <sources/skills/SkillsManager.hpp> #include <sources/skills/SkillsManager.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ClientInterface::ClientInterface( ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
: QObject(parent) : QObject(parent)
, m_promptProvider(promptProvider)
, m_chatModel(chatModel) , m_chatModel(chatModel)
, m_contextManager(new Context::ContextManager(this)) , m_contextManager(new Context::ContextManager(this))
{} {}
ClientInterface::~ClientInterface()
{
cancelRequest();
}
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager) void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
{ {
m_skillsManager = skillsManager; m_skillsManager = skillsManager;
} }
ClientInterface::~ClientInterface() void ClientInterface::setSessionManager(SessionManager *sessionManager)
{ {
cancelRequest(); m_sessionManager = sessionManager;
}
void ClientInterface::setActiveAgent(const QString &agentName)
{
m_activeAgent = agentName;
} }
void ClientInterface::sendMessage( void ClientInterface::sendMessage(
@@ -72,6 +82,8 @@ void ClientInterface::sendMessage(
bool useTools, bool useTools,
bool useThinking) bool useThinking)
{ {
Q_UNUSED(useThinking)
if (message.trimmed().isEmpty() && attachments.isEmpty()) { if (message.trimmed().isEmpty() && attachments.isEmpty()) {
LOG_MESSAGE("Ignoring empty chat message"); LOG_MESSAGE("Ignoring empty chat message");
return; return;
@@ -84,13 +96,11 @@ void ClientInterface::sendMessage(
QList<QString> imageFiles; QList<QString> imageFiles;
QList<QString> textFiles; QList<QString> textFiles;
for (const QString &filePath : attachments) { for (const QString &filePath : attachments) {
if (isImageFile(filePath)) { if (isImageFile(filePath))
imageFiles.append(filePath); imageFiles.append(filePath);
} else { else
textFiles.append(filePath); textFiles.append(filePath);
}
} }
QList<Context::ContentFile> storedAttachments; QList<Context::ContentFile> storedAttachments;
@@ -116,9 +126,8 @@ void ClientInterface::sendMessage(
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) { if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
for (const QString &imagePath : imageFiles) { for (const QString &imagePath : imageFiles) {
QString base64Data = encodeImageToBase64(imagePath); QString base64Data = encodeImageToBase64(imagePath);
if (base64Data.isEmpty()) { if (base64Data.isEmpty())
continue; continue;
}
QString storedPath; QString storedPath;
QFileInfo fileInfo(imagePath); QFileInfo fileInfo(imagePath);
@@ -129,7 +138,6 @@ void ClientInterface::sendMessage(
imageAttachment.storedPath = storedPath; imageAttachment.storedPath = storedPath;
imageAttachment.mediaType = getMediaTypeForImage(imagePath); imageAttachment.mediaType = getMediaTypeForImage(imagePath);
imageAttachments.append(imageAttachment); imageAttachments.append(imageAttachment);
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath)); LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
} }
} }
@@ -138,318 +146,302 @@ void ClientInterface::sendMessage(
.arg(imageFiles.size())); .arg(imageFiles.size()));
} }
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments); if (!m_sessionManager) {
const QString error = QStringLiteral("Chat session manager is not available");
auto &chatAssistantSettings = Settings::chatAssistantSettings(); LOG_MESSAGE(error);
emit errorOccurred(error);
auto providerName = Settings::generalSettings().caProvider();
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
return; return;
} }
auto templateName = Settings::generalSettings().caTemplate(); // Snapshot prior turns BEFORE the new user message is appended to the model.
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); const QVector<ChatModel::Message> priorHistory = m_chatModel->getChatHistory();
if (!promptTemplate) { m_chatModel
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); ->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
QString sessionError;
Session *session = m_sessionManager->createSession(m_activeAgent, &sessionError);
if (!session) {
const QString error = sessionError.isEmpty()
? QStringLiteral("No chat agent selected")
: sessionError;
LOG_MESSAGE(error);
emit errorOccurred(error);
return; return;
} }
PluginLLMCore::ContextData context; auto *client = session->client();
if (!client) {
const bool isToolsEnabled = useTools; const QString error = QStringLiteral("Chat agent has no live client");
LOG_MESSAGE(error);
if (chatAssistantSettings.useSystemPrompt()) { m_sessionManager->removeSession(session);
QString systemPrompt = chatAssistantSettings.systemPrompt(); emit errorOccurred(error);
return;
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
if (!lastRoleId.isEmpty()) {
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (!role.id.isEmpty())
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
}
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
systemPrompt += QString("\n# Active project: %1").arg(project->displayName());
systemPrompt += QString(
"\n# Project source root: %1"
"\n# All new source files, headers, QML and CMake edits MUST be "
"created or modified under this directory. Use absolute paths "
"rooted here, or project-relative paths.")
.arg(project->projectDirectory().toUrlishString());
if (auto target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) {
systemPrompt
+= QString(
"\n# Build output directory (compiler artifacts only — do NOT "
"create or edit source files here): %1")
.arg(buildConfig->buildDirectory().toUrlishString());
}
}
QString projectRules
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
if (!projectRules.isEmpty()) {
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
}
} else {
systemPrompt += QString("\n# No active project in IDE");
}
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
QStringList projectSkillDirs;
if (project) {
Settings::ProjectSettings projectSettings(project);
projectSkillDirs = Settings::SkillsSettings::splitLines(
projectSettings.projectSkillDirs());
}
m_skillsManager->configure(
project ? project->projectDirectory().toFSPathString() : QString(),
Settings::SkillsSettings::splitPaths(
Settings::skillsSettings().globalSkillRoots()),
projectSkillDirs);
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
if (!alwaysOnSkills.isEmpty())
systemPrompt += QString("\n\n") + alwaysOnSkills;
const QString skillsCatalog = m_skillsManager->catalogText();
if (!skillsCatalog.isEmpty())
systemPrompt += QString("\n\n") + skillsCatalog;
static const QRegularExpression skillCommand(
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
QStringList invokedSkillNames;
auto skillMatch = skillCommand.globalMatch(message);
while (skillMatch.hasNext()) {
const QString skillName = skillMatch.next().captured(1);
if (invokedSkillNames.contains(skillName))
continue;
const auto invokedSkill = m_skillsManager->findByName(skillName);
if (invokedSkill && !invokedSkill->body.isEmpty()) {
invokedSkillNames << skillName;
systemPrompt += QString("\n\n# Invoked Skill: %1\n\n%2")
.arg(invokedSkill->name, invokedSkill->body);
}
}
}
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
context.systemPrompt = systemPrompt;
} }
const bool toolHistory = promptTemplate->supportsToolHistory(); Tools::registerQodeAssistTools(client->tools());
if (m_skillsManager)
QVector<PluginLLMCore::Message> messages; Tools::registerSkillTool(client->tools(), m_skillsManager);
int toolCallMsgIdx = -1; client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
for (const auto &msg : m_chatModel->getChatHistory()) { client->setTransferTimeout(
if (msg.role == ChatModel::ChatRole::Tool) {
if (!toolHistory || msg.toolName.isEmpty()) {
continue;
}
if (toolCallMsgIdx < 0) {
PluginLLMCore::Message assistantCall;
assistantCall.role = "assistant";
messages.append(assistantCall);
toolCallMsgIdx = messages.size() - 1;
}
PluginLLMCore::ToolCall call;
call.id = msg.id;
call.name = msg.toolName;
call.arguments = msg.toolArguments;
messages[toolCallMsgIdx].toolCalls.append(call);
PluginLLMCore::Message toolResult;
toolResult.role = "tool";
toolResult.toolCallId = msg.id;
toolResult.toolName = msg.toolName;
toolResult.content = msg.toolResult;
messages.append(toolResult);
continue;
}
toolCallMsgIdx = -1;
if (msg.role == ChatModel::ChatRole::FileEdit) {
continue;
}
PluginLLMCore::Message apiMessage;
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content;
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
apiMessage.content += "\n\nAttached files:";
for (const auto &attachment : msg.attachments) {
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
if (!fileContent.isEmpty()) {
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
.arg(attachment.filename, decodedContent);
}
}
}
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature;
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
auto apiImages = loadImagesFromStorage(msg.images);
if (!apiImages.isEmpty()) {
apiMessage.images = apiImages;
}
}
messages.append(apiMessage);
}
if (!imageFiles.isEmpty()
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
.arg(provider->name(), QString::number(imageFiles.size())));
}
context.history = messages;
QJsonObject payload{
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
provider->prepareRequest(
payload,
promptTemplate,
context,
PluginLLMCore::RequestType::Chat,
useTools,
useThinking);
provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations());
provider->client()->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000)); static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
connect( const QString chatContext = buildChatContextLayer(message, linkedFiles);
provider->client(), if (!chatContext.isEmpty())
&::LLMQore::BaseClient::chunkReceived, session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
this,
&ClientInterface::handlePartialResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&ClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolStarted,
this,
&ClientInterface::handleToolExecutionStarted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolResultReady,
this,
&ClientInterface::handleToolExecutionCompleted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::thinkingBlockReceived,
this,
&ClientInterface::handleThinkingBlockReceived,
Qt::UniqueConnection);
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint(); seedHistory(*session->history(), priorHistory);
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider, !toolHistory}; QString userText = message;
if (!storedAttachments.isEmpty() && !m_chatFilePath.isEmpty()) {
userText += "\n\nAttached files:";
for (const auto &attachment : storedAttachments) {
QString fileContent
= ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
if (!fileContent.isEmpty()) {
QString decoded = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
userText += QString("\n\nFile: %1\n```\n%2\n```").arg(attachment.filename, decoded);
}
}
}
emit requestStarted(requestId); std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<LLMQore::TextContent>(userText));
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) { if (!imageAttachments.isEmpty() && session->supportsImages() && !m_chatFilePath.isEmpty()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>( for (const auto &image : imageAttachments) {
provider->toolsManager()->tool("todo_tool"))) { QString base64
= ChatSerializer::loadContentFromStorage(m_chatFilePath, image.storedPath);
if (base64.isEmpty())
continue;
blocks.push_back(std::make_unique<LLMQore::ImageContent>(
base64, image.mediaType, LLMQore::ImageContent::ImageSourceType::Base64));
}
} else if (!imageAttachments.isEmpty() && !session->supportsImages()) {
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored")
.arg(m_activeAgent)
.arg(imageAttachments.size()));
}
connect(
client, &::LLMQore::BaseClient::chunkReceived,
this, &ClientInterface::handlePartialResponse, Qt::UniqueConnection);
connect(
client, &::LLMQore::BaseClient::requestCompleted,
this, &ClientInterface::handleFullResponse, Qt::UniqueConnection);
connect(
client, &::LLMQore::BaseClient::requestFinalized,
this, &ClientInterface::handleRequestFinalized, Qt::UniqueConnection);
connect(
client, &::LLMQore::BaseClient::requestFailed,
this, &ClientInterface::handleRequestFailed, Qt::UniqueConnection);
connect(
client, &::LLMQore::BaseClient::toolStarted,
this, &ClientInterface::handleToolExecutionStarted, Qt::UniqueConnection);
connect(
client, &::LLMQore::BaseClient::toolResultReady,
this, &ClientInterface::handleToolExecutionCompleted, Qt::UniqueConnection);
connect(
client, &::LLMQore::BaseClient::thinkingBlockReceived,
this, &ClientInterface::handleThinkingBlockReceived, Qt::UniqueConnection);
if (!m_chatFilePath.isEmpty()) {
if (auto *todoTool
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
todoTool->setCurrentSessionId(m_chatFilePath); todoTool->setCurrentSessionId(m_chatFilePath);
} }
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>( if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
provider->toolsManager()->tool("read_original_history"))) { client->tools()->tool("read_original_history"))) {
historyTool->setCurrentSessionId(m_chatFilePath); historyTool->setCurrentSessionId(m_chatFilePath);
} }
} }
const LLMQore::RequestID requestId = session->send(std::move(blocks), useTools);
if (requestId.isEmpty()) {
const QString error = QStringLiteral("Failed to start chat request for agent: %1")
.arg(m_activeAgent);
LOG_MESSAGE(error);
m_sessionManager->removeSession(session);
emit errorOccurred(error);
return;
}
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, session, /*dropPreToolText=*/false};
emit requestStarted(requestId);
}
void ClientInterface::seedHistory(
ConversationHistory &history, const QVector<ChatModel::Message> &messages) const
{
int i = 0;
while (i < messages.size()) {
const ChatModel::Message &msg = messages[i];
if (msg.role == ChatModel::ChatRole::Tool) {
Message assistant(Message::Role::Assistant);
Message toolResults(Message::Role::User);
while (i < messages.size() && messages[i].role == ChatModel::ChatRole::Tool) {
const ChatModel::Message &toolMsg = messages[i];
if (!toolMsg.toolName.isEmpty()) {
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
toolMsg.id, toolMsg.toolName, toolMsg.toolArguments));
toolResults.appendBlock(
std::make_unique<LLMQore::ToolResultContent>(toolMsg.id, toolMsg.toolResult));
}
++i;
}
if (!assistant.blocks().empty()) {
history.append(std::move(assistant));
history.append(std::move(toolResults));
}
continue;
}
++i;
if (msg.role == ChatModel::ChatRole::FileEdit
|| msg.role == ChatModel::ChatRole::Thinking) {
continue;
}
if (msg.role == ChatModel::ChatRole::User) {
Message userMessage(Message::Role::User);
QString content = msg.content;
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
content += "\n\nAttached files:";
for (const auto &attachment : msg.attachments) {
QString fileContent = ChatSerializer::loadContentFromStorage(
m_chatFilePath, attachment.content);
if (!fileContent.isEmpty()) {
QString decoded
= QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
content
+= QString("\n\nFile: %1\n```\n%2\n```").arg(attachment.filename, decoded);
}
}
}
userMessage.appendBlock(std::make_unique<LLMQore::TextContent>(content));
if (!msg.images.isEmpty() && !m_chatFilePath.isEmpty()) {
for (const auto &image : msg.images) {
QString base64 = ChatSerializer::loadContentFromStorage(
m_chatFilePath, image.storedPath);
if (base64.isEmpty())
continue;
userMessage.appendBlock(std::make_unique<LLMQore::ImageContent>(
base64, image.mediaType, LLMQore::ImageContent::ImageSourceType::Base64));
}
}
history.append(std::move(userMessage));
} else { // Assistant
if (msg.content.trimmed().isEmpty())
continue;
Message assistant(Message::Role::Assistant);
assistant.appendBlock(std::make_unique<LLMQore::TextContent>(msg.content));
history.append(std::move(assistant));
}
}
} }
void ClientInterface::clearMessages() QString ClientInterface::buildChatContextLayer(
const QString &message, const QList<QString> &linkedFiles) const
{ {
const auto providerName = Settings::generalSettings().caProvider(); QString context;
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider && !m_chatFilePath.isEmpty() auto *project = ProjectExplorer::ProjectManager::startupProject();
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools) if (project) {
&& provider->toolsManager()) { context += QString("# Active project: %1").arg(project->displayName());
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>( context += QString(
provider->toolsManager()->tool("todo_tool"))) { "\n# Project source root: %1"
todoTool->clearSession(m_chatFilePath); "\n# All new source files, headers, QML and CMake edits MUST be "
"created or modified under this directory. Use absolute paths "
"rooted here, or project-relative paths.")
.arg(project->projectDirectory().toUrlishString());
if (auto target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) {
context += QString(
"\n# Build output directory (compiler artifacts only — do NOT "
"create or edit source files here): %1")
.arg(buildConfig->buildDirectory().toUrlishString());
}
}
} else {
context += QString("# No active project in IDE");
}
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
QStringList projectSkillDirs;
if (project) {
Settings::ProjectSettings projectSettings(project);
projectSkillDirs
= Settings::SkillsSettings::splitLines(projectSettings.projectSkillDirs());
}
m_skillsManager->configure(
project ? project->projectDirectory().toFSPathString() : QString(),
Settings::SkillsSettings::splitPaths(Settings::skillsSettings().globalSkillRoots()),
projectSkillDirs);
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
if (!alwaysOnSkills.isEmpty())
context += QString("\n\n") + alwaysOnSkills;
const QString skillsCatalog = m_skillsManager->catalogText();
if (!skillsCatalog.isEmpty())
context += QString("\n\n") + skillsCatalog;
static const QRegularExpression skillCommand(
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
QStringList invokedSkillNames;
auto skillMatch = skillCommand.globalMatch(message);
while (skillMatch.hasNext()) {
const QString skillName = skillMatch.next().captured(1);
if (invokedSkillNames.contains(skillName))
continue;
const auto invokedSkill = m_skillsManager->findByName(skillName);
if (invokedSkill && !invokedSkill->body.isEmpty()) {
invokedSkillNames << skillName;
context += QString("\n\n# Invoked Skill: %1\n\n%2")
.arg(invokedSkill->name, invokedSkill->body);
}
} }
} }
if (!linkedFiles.isEmpty()) {
context += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles)
context += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
}
return context;
}
void ClientInterface::clearMessages()
{
m_chatModel->clear(); m_chatModel->clear();
} }
void ClientInterface::cancelRequest() void ClientInterface::cancelRequest()
{ {
QSet<PluginLLMCore::Provider *> providers; const auto requests = m_activeRequests;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear(); m_activeRequests.clear();
m_accumulatedResponses.clear(); m_accumulatedResponses.clear();
m_awaitingContinuation.clear(); m_awaitingContinuation.clear();
LOG_MESSAGE("All requests cancelled and state cleared"); for (auto it = requests.begin(); it != requests.end(); ++it) {
Session *session = it.value().session;
if (!session)
continue;
if (auto *client = session->client())
disconnect(client, nullptr, this, nullptr);
if (m_sessionManager)
m_sessionManager->removeSession(session);
}
LOG_MESSAGE("All chat requests cancelled and state cleared");
} }
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request) void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
@@ -486,23 +478,6 @@ QString ClientInterface::getCurrentFileContext() const
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content); return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
} }
QString ClientInterface::getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const
{
QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
}
}
return updatedPrompt;
}
Context::ContextManager *ClientInterface::contextManager() const Context::ContextManager *ClientInterface::contextManager() const
{ {
return m_contextManager; return m_contextManager;
@@ -532,7 +507,8 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
const RequestContext &ctx = it.value(); const QJsonObject originalRequest = it.value().originalRequest;
Session *session = it.value().session;
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId]; QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
@@ -546,13 +522,16 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
} }
LOG_MESSAGE( LOG_MESSAGE(
"Message completed. Final response for message " + ctx.originalRequest["id"].toString() "Message completed. Final response for message " + originalRequest["id"].toString() + ": "
+ ": " + finalText); + finalText);
emit messageReceivedCompletely(); emit messageReceivedCompletely();
m_activeRequests.erase(it); m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId); m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId); m_awaitingContinuation.remove(requestId);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
} }
void ClientInterface::handleRequestFinalized( void ClientInterface::handleRequestFinalized(
@@ -584,12 +563,17 @@ void ClientInterface::handleRequestFailed(const QString &requestId, const QStrin
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
Session *session = it.value().session;
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
emit errorOccurred(error); emit errorOccurred(error);
m_activeRequests.erase(it); m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId); m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId); m_awaitingContinuation.remove(requestId);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
} }
void ClientInterface::handleThinkingBlockReceived( void ClientInterface::handleThinkingBlockReceived(
@@ -693,46 +677,8 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
return imageData.toBase64(); return imageData.toBase64();
} }
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
const QList<ChatModel::ImageAttachment> &storedImages) const
{
QVector<PluginLLMCore::ImageAttachment> apiImages;
for (const auto &storedImage : storedImages) {
QString base64Data
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
if (base64Data.isEmpty()) {
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
continue;
}
PluginLLMCore::ImageAttachment apiImage;
apiImage.data = base64Data;
apiImage.mediaType = storedImage.mediaType;
apiImage.isUrl = false;
apiImages.append(apiImage);
}
return apiImages;
}
void ClientInterface::setChatFilePath(const QString &filePath) void ClientInterface::setChatFilePath(const QString &filePath)
{ {
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
}
m_chatFilePath = filePath; m_chatFilePath = filePath;
m_chatModel->setChatFilePath(filePath); m_chatModel->setChatFilePath(filePath);
} }

View File

@@ -5,16 +5,21 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QPointer>
#include <QSet> #include <QSet>
#include <QString> #include <QString>
#include <QVector> #include <QVector>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "Provider.hpp"
#include "pluginllmcore/IPromptProvider.hpp"
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
}
namespace QodeAssist::Skills { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
@@ -26,11 +31,12 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface( explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void setSkillsManager(Skills::SkillsManager *skillsManager); void setSkillsManager(Skills::SkillsManager *skillsManager);
void setSessionManager(SessionManager *sessionManager);
void setActiveAgent(const QString &agentName);
void sendMessage( void sendMessage(
const QString &message, const QString &message,
@@ -42,7 +48,7 @@ public:
void cancelRequest(); void cancelRequest();
Context::ContextManager *contextManager() const; Context::ContextManager *contextManager() const;
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
@@ -74,24 +80,26 @@ private slots:
private: private:
void handleLLMResponse(const QString &response, const QJsonObject &request); void handleLLMResponse(const QString &response, const QJsonObject &request);
QString getCurrentFileContext() const; QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles( QString buildChatContextLayer(
const QString &basePrompt, const QList<QString> &linkedFiles) const; const QString &message, const QList<QString> &linkedFiles) const;
void seedHistory(
ConversationHistory &history, const QVector<ChatModel::Message> &messages) const;
bool isImageFile(const QString &filePath) const; bool isImageFile(const QString &filePath) const;
QString getMediaTypeForImage(const QString &filePath) const; QString getMediaTypeForImage(const QString &filePath) const;
QString encodeImageToBase64(const QString &filePath) const; QString encodeImageToBase64(const QString &filePath) const;
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
bool dropPreToolText = false; bool dropPreToolText = false;
}; };
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel; ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
Skills::SkillsManager *m_skillsManager = nullptr; Skills::SkillsManager *m_skillsManager = nullptr;
QPointer<SessionManager> m_sessionManager;
QString m_activeAgent;
QString m_chatFilePath; QString m_chatFilePath;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;

View File

@@ -6,17 +6,11 @@
#include <algorithm> #include <algorithm>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray>
#include <QJsonDocument>
#include <utils/aspects.h> #include <utils/aspects.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp" #include "context/TokenUtils.hpp"
@@ -42,12 +36,6 @@ InputTokenCounter::InputTokenCounter(
this, this,
&InputTokenCounter::recompute); &InputTokenCounter::recompute);
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
rewireToolsChangedConnection();
recompute();
});
rewireToolsChangedConnection();
recompute(); recompute();
} }
@@ -74,24 +62,6 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
recompute(); recompute();
} }
void InputTokenCounter::rewireToolsChangedConnection()
{
if (m_toolsChangedConn)
QObject::disconnect(m_toolsChangedConn);
m_toolsChangedConn = {};
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider)
return;
auto *tm = provider->toolsManager();
if (!tm)
return;
m_toolsChangedConn = connect(
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
}
void InputTokenCounter::recompute() void InputTokenCounter::recompute()
{ {
int inputTokens = m_messageTokens; int inputTokens = m_messageTokens;
@@ -136,21 +106,6 @@ void InputTokenCounter::recompute()
inputTokens += 4; // + role inputTokens += 4; // + role
} }
if (settings.enableChatTools()) {
const auto providerName = Settings::generalSettings().caProvider();
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
providerName)) {
if (auto *tm = provider->toolsManager()) {
const QJsonArray toolDefs = tm->getToolsDefinitions();
if (!toolDefs.isEmpty()) {
const QByteArray serialized
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
inputTokens += static_cast<int>(serialized.size() / 4);
}
}
}
}
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor); m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
emit inputTokensChanged(); emit inputTokensChanged();
} }

View File

@@ -37,11 +37,8 @@ signals:
void inputTokensChanged(); void inputTokensChanged();
private: private:
void rewireToolsChangedConnection();
ChatModel *m_chatModel; ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QMetaObject::Connection m_toolsChangedConn;
QStringList m_attachments; QStringList m_attachments;
QStringList m_linkedFiles; QStringList m_linkedFiles;

View File

@@ -152,29 +152,17 @@ ChatRootView {
} }
} }
settingsButton.onClicked: root.openSettings() settingsButton.onClicked: root.openSettings()
configSelector { agentSelector {
model: root.availableConfigurations model: root.availableChatAgents
displayText: root.currentConfiguration displayText: root.currentChatAgent
onActivated: function(index) { onActivated: function(index) {
if (index > 0) { root.currentChatAgent = root.availableChatAgents[index]
root.applyConfiguration(root.availableConfigurations[index])
}
} }
Component.onCompleted: root.loadAvailableChatAgents()
popup.onAboutToShow: { popup.onAboutToShow: {
root.loadAvailableConfigurations() root.loadAvailableChatAgents()
}
}
roleSelector {
model: root.availableAgentRoles
displayText: root.currentAgentRole
onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index])
}
popup.onAboutToShow: {
root.loadAvailableAgentRoles()
} }
} }
} }
@@ -839,20 +827,7 @@ ChatRootView {
x: (parent.width - width) / 2 x: (parent.width - width) / 2
y: (parent.height - height) / 2 y: (parent.height - height) / 2
baseSystemPrompt: root.baseSystemPrompt
currentAgentRole: root.currentAgentRole
currentAgentRoleDescription: root.currentAgentRoleDescription
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
activeRules: root.activeRules
activeRulesCount: root.activeRulesCount
onOpenSettings: root.openSettings() onOpenSettings: root.openSettings()
onOpenAgentRolesSettings: root.openAgentRolesSettings()
onOpenRulesFolder: root.openRulesFolder()
onRefreshRules: root.refreshRules()
onRuleSelected: function(index) {
contextViewer.selectedRuleContent = root.getRuleContent(index)
}
} }
Connections { Connections {

View File

@@ -26,8 +26,7 @@ Rectangle {
property alias toolsButton: toolsButtonId property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId property alias settingsButton: settingsButtonId
property alias configSelector: configSelectorId property alias agentSelector: agentSelectorId
property alias roleSelector: roleSelector
property alias relocateTooltip: relocateTooltipId property alias relocateTooltip: relocateTooltipId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
@@ -134,7 +133,7 @@ Rectangle {
} }
QoAComboBox { QoAComboBox {
id: configSelectorId id: agentSelectorId
implicitHeight: 25 implicitHeight: 25
@@ -142,24 +141,9 @@ Rectangle {
currentIndex: 0 currentIndex: 0
QoAToolTip { QoAToolTip {
visible: configSelectorId.hovered visible: agentSelectorId.hovered
delay: 250 delay: 250
text: qsTr("Switch saved AI configuration") text: qsTr("Select chat agent (provider, model and role come from the agent)")
}
}
QoAComboBox {
id: roleSelector
implicitHeight: 25
model: []
currentIndex: 0
QoAToolTip {
visible: roleSelector.hovered
delay: 250
text: qsTr("Switch agent role (different system prompts)")
} }
} }
} }

View File

@@ -1,236 +0,0 @@
// Copyright (C) 2025-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 <settings/ButtonAspect.hpp>
#include <QTimer>
#include "QodeAssisttr.h"
namespace QodeAssist {
ConfigurationManager &ConfigurationManager::instance()
{
static ConfigurationManager instance;
return instance;
}
void ConfigurationManager::init()
{
setupConnections();
updateAllTemplateDescriptions();
checkAllTemplate();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (!templ) {
return;
}
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
m_generalSettings.qrTemplateDescription.setValue(templ->description());
}
}
void ConfigurationManager::updateAllTemplateDescriptions()
{
updateTemplateDescription(m_generalSettings.ccTemplate);
updateTemplateDescription(m_generalSettings.caTemplate);
updateTemplateDescription(m_generalSettings.qrTemplate);
}
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
{
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (templ->name() == templateAspect.value())
return;
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplate.setValue(templ->name());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplate.setValue(templ->name());
}
}
void ConfigurationManager::checkAllTemplate()
{
checkTemplate(m_generalSettings.ccTemplate);
checkTemplate(m_generalSettings.caTemplate);
}
ConfigurationManager::ConfigurationManager(QObject *parent)
: QObject(parent)
, m_generalSettings(Settings::generalSettings())
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
{}
void ConfigurationManager::setupConnections()
{
using Config = ConfigurationManager;
using Button = ButtonAspect;
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.qrSelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
connect(&m_generalSettings.qrSelectModel, &Button::clicked, this, &Config::selectModel);
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.qrSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.qrSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.ccTemplate);
});
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.caTemplate);
});
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.qrTemplate);
});
}
void ConfigurationManager::selectProvider()
{
const auto providersList = m_providersManager.providersNames();
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
? m_generalSettings.ccProvider
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
? m_generalSettings.ccPreset1Provider
: settingsButton == &m_generalSettings.qrSelectProvider
? m_generalSettings.qrProvider
: m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
m_generalSettings.showSelectionDialog(
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
});
}
void ConfigurationManager::selectModel()
{
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
: m_generalSettings.caUrl.volatileValue();
auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
: isPreset1 ? m_generalSettings.ccPreset1Model
: isQuickRefactor ? m_generalSettings.qrModel
: m_generalSettings.caModel);
if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
return;
}
provider->getInstalledModels(providerUrl)
.then(this, [this, targetSettings](const QList<QString> &modelList) {
if (modelList.isEmpty()) {
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
return;
}
m_generalSettings.showSelectionDialog(
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
});
}
}
void ConfigurationManager::selectTemplate()
{
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
const auto templateList = isCodeCompletion || isPreset1
? m_templateManger.getFimTemplatesForProvider(providerID)
: m_templateManger.getChatTemplatesForProvider(providerID);
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template
: isQuickRefactor ? m_generalSettings.qrTemplate
: m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
m_generalSettings.showSelectionDialog(
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
});
}
void ConfigurationManager::selectUrl()
{
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
QStringList urls;
for (const auto &name : m_providersManager.providersNames()) {
const auto url = m_providersManager.getProviderByName(name)->url();
if (!urls.contains(url))
urls.append(url);
}
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
: settingsButton == &m_generalSettings.ccPreset1SetUrl
? m_generalSettings.ccPreset1Url
: settingsButton == &m_generalSettings.qrSetUrl
? m_generalSettings.qrUrl
: m_generalSettings.caUrl;
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
});
}
} // namespace QodeAssist

View File

@@ -1,48 +0,0 @@
// Copyright (C) 2025-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 "pluginllmcore/PromptTemplateManager.hpp"
#include "pluginllmcore/ProvidersManager.hpp"
#include "settings/GeneralSettings.hpp"
namespace QodeAssist {
class ConfigurationManager : public QObject
{
Q_OBJECT
public:
static ConfigurationManager &instance();
void init();
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
void checkTemplate(const Utils::StringAspect &templateAspect);
void checkAllTemplate();
public slots:
void selectProvider();
void selectModel();
void selectTemplate();
void selectUrl();
private:
explicit ConfigurationManager(QObject *parent = nullptr);
~ConfigurationManager() = default;
ConfigurationManager(const ConfigurationManager &) = delete;
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::ProvidersManager &m_providersManager;
PluginLLMCore::PromptTemplateManager &m_templateManger;
void setupConnections();
};
} // namespace QodeAssist

View File

@@ -9,27 +9,39 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <ResponseEvent.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include "sources/common/ContextData.hpp"
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include "context/DocumentContextReader.hpp" #include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp" #include "context/Utils.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include <pluginllmcore/RulesLoader.hpp> #include "sources/settings/PipelinesConfig.hpp"
namespace QodeAssist { namespace QodeAssist {
LLMClientInterface::LLMClientInterface( LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry, SessionManager &sessionManager,
PluginLLMCore::IPromptProvider *promptProvider, AgentFactory &agentFactory,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger) IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings) : m_generalSettings(generalSettings)
, m_completeSettings(completeSettings) , m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry) , m_sessionManager(sessionManager)
, m_promptProvider(promptProvider) , m_agentFactory(agentFactory)
, m_documentReader(documentReader) , m_documentReader(documentReader)
, m_performanceLogger(performanceLogger) , m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this)) , m_contextManager(new Context::ContextManager(this))
@@ -51,58 +63,64 @@ void LLMClientInterface::startImpl()
emit started(); emit started();
} }
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText) void LLMClientInterface::onSessionEvent(const QString &requestId, const ResponseEvent &event)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
const RequestContext &ctx = it.value(); if (event.kind() == ResponseEvent::Kind::TextDelta) {
sendCompletionToClient(fullText, ctx.originalRequest, true); if (const auto *delta = event.as<ResponseEvents::TextDelta>())
it.value().accumulated += delta->text;
m_activeRequests.erase(it); }
m_performanceLogger.endTimeMeasurement(requestId);
} }
void LLMClientInterface::handleRequestFinalized( void LLMClientInterface::onSessionFinished(const QString &requestId)
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId) || !info.usage)
return;
const auto &u = *info.usage;
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
const RequestContext &ctx = it.value(); const QString fullText = it.value().accumulated;
const QJsonObject originalRequest = it.value().originalRequest;
sendCompletionToClient(fullText, originalRequest, true);
finishRequest(requestId);
}
void LLMClientInterface::onSessionFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response; QJsonObject response;
response["jsonrpc"] = "2.0"; response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"]; response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"];
QJsonObject errorObject; QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code errorObject["code"] = -32603; // Internal error code
errorObject["message"] = error; errorObject["message"] = error;
response["error"] = errorObject; response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
finishRequest(requestId);
}
void LLMClientInterface::finishRequest(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
m_activeRequests.erase(it); m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId); m_performanceLogger.endTimeMeasurement(requestId);
if (session)
m_sessionManager.removeSession(session);
} }
void LLMClientInterface::sendData(const QByteArray &data) void LLMClientInterface::sendData(const QByteArray &data)
@@ -135,26 +153,15 @@ void LLMClientInterface::sendData(const QByteArray &data)
void LLMClientInterface::handleCancelRequest() void LLMClientInterface::handleCancelRequest()
{ {
QSet<PluginLLMCore::Provider *> providers; const auto requests = m_activeRequests;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear(); m_activeRequests.clear();
for (auto it = requests.begin(); it != requests.end(); ++it) {
m_performanceLogger.endTimeMeasurement(it.key());
if (it.value().session)
m_sessionManager.removeSession(it.value().session);
}
LOG_MESSAGE("All requests cancelled and state cleared"); LOG_MESSAGE("All requests cancelled and state cleared");
} }
@@ -237,133 +244,86 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
return; return;
} }
auto updatedContext = prepareContext(request, documentInfo); const QString agentName = pickCompletionAgent(filePath);
if (agentName.isEmpty()) {
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo); QString error = QString("No code completion agent matches: %1").arg(filePath);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error); LOG_MESSAGE(error);
sendErrorResponse(request, error); sendErrorResponse(request, error);
return; return;
} }
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() QString sessionError;
: m_generalSettings.ccPreset1Template(); Session *session = m_sessionManager.createSession(agentName, &sessionError);
if (!session) {
LOG_MESSAGE(sessionError);
sendErrorResponse(request, sessionError);
return;
}
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); Templates::ContextData context = prepareContext(request, documentInfo);
if (!promptTemplate) { QString editorContext;
QString error = QString("No template found with name: %1").arg(templateName); if (context.fileContext.has_value())
editorContext.append(context.fileContext.value());
if (m_completeSettings.useOpenFilesContext())
editorContext.append(m_contextManager->openedFilesContext({filePath}));
if (!editorContext.isEmpty())
context.systemPrompt = editorContext;
connect(session, &Session::event, this, [this, session](const ResponseEvent &event) {
onSessionEvent(requestIdForSession(session), event);
});
connect(session, &Session::finished, this, [this, session](const LLMQore::RequestID &, const QString &) {
onSessionFinished(requestIdForSession(session));
});
connect(session, &Session::failed, this, [this, session](const LLMQore::RequestID &, const QString &error) {
onSessionFailed(requestIdForSession(session), error);
});
if (auto *client = session->client())
client->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
const LLMQore::RequestID requestId = session->sendCompletion(std::move(context));
if (requestId.isEmpty()) {
m_sessionManager.removeSession(session);
QString error = QString("Failed to start completion request for agent: %1").arg(agentName);
LOG_MESSAGE(error); LOG_MESSAGE(error);
sendErrorResponse(request, error); sendErrorResponse(request, error);
return; return;
} }
QJsonObject payload{{"model", modelName}, {"stream", true}}; m_activeRequests[requestId] = {request, session, QString()};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for completion");
}
}
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<PluginLLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
PluginLLMCore::RequestType::CodeCompletion,
false,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&LLMClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&LLMClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection);
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider};
m_performanceLogger.startTimeMeasurement(requestId); m_performanceLogger.startTimeMeasurement(requestId);
} }
PluginLLMCore::ContextData LLMClientInterface::prepareContext( QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const
{
const QStringList roster = Settings::PipelinesConfig::load().rosters.codeCompletion;
if (roster.isEmpty())
return {};
AgentRouter::Context ctx;
ctx.filePath = filePath;
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath)))
ctx.projectName = project->displayName();
return AgentRouter::pickAgent(roster, ctx, m_agentFactory);
}
QString LLMClientInterface::requestIdForSession(Session *session) const
{
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
if (it.value().session == session)
return it.key();
}
return {};
}
Templates::ContextData LLMClientInterface::prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo) const QJsonObject &request, const Context::DocumentInfo &documentInfo)
{ {
QJsonObject params = request["params"].toObject(); QJsonObject params = request["params"].toObject();
@@ -374,15 +334,14 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext(
Context::DocumentContextReader Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath); reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings); const PluginLLMCore::ContextData legacy
} = reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
QString LLMClientInterface::resolveEndpoint( Templates::ContextData context;
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const context.prefix = legacy.prefix;
{ context.suffix = legacy.suffix;
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint() context.fileContext = legacy.fileContext;
: m_generalSettings.ccCustomEndpoint(); return context;
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
} }
Context::ContextManager *LLMClientInterface::contextManager() const Context::ContextManager *LLMClientInterface::contextManager() const
@@ -393,15 +352,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const
void LLMClientInterface::sendCompletionToClient( void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete) const QString &completion, const QJsonObject &request, bool isComplete)
{ {
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject(); QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response; QJsonObject response;

View File

@@ -8,12 +8,11 @@
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <QPointer>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp> #include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp> #include <context/ProgrammingLanguage.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp> #include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp> #include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp> #include <settings/GeneralSettings.hpp>
@@ -23,6 +22,15 @@ class QNetworkAccessManager;
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class AgentFactory;
class Session;
class ResponseEvent;
namespace Templates {
struct ContextData;
}
class LLMClientInterface : public LanguageClient::BaseClientInterface class LLMClientInterface : public LanguageClient::BaseClientInterface
{ {
Q_OBJECT Q_OBJECT
@@ -31,8 +39,8 @@ public:
LLMClientInterface( LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry, SessionManager &sessionManager,
PluginLLMCore::IPromptProvider *promptProvider, AgentFactory &agentFactory,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger); IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override; ~LLMClientInterface() override;
@@ -52,12 +60,6 @@ public:
protected: protected:
void startImpl() override; void startImpl() override;
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
private: private:
void handleInitialize(const QJsonObject &request); void handleInitialize(const QJsonObject &request);
void handleShutdown(const QJsonObject &request); void handleShutdown(const QJsonObject &request);
@@ -67,22 +69,28 @@ private:
void handleCancelRequest(); void handleCancelRequest();
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage); void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
void onSessionEvent(const QString &requestId, const ResponseEvent &event);
void onSessionFinished(const QString &requestId);
void onSessionFailed(const QString &requestId, const QString &error);
void finishRequest(const QString &requestId);
QString requestIdForSession(Session *session) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
QString accumulated;
}; };
PluginLLMCore::ContextData prepareContext( Templates::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo); const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString resolveEndpoint( QString pickCompletionAgent(const QString &filePath) const;
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
const Settings::CodeCompletionSettings &m_completeSettings; const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings; const Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr; SessionManager &m_sessionManager;
PluginLLMCore::IProviderRegistry &m_providerRegistry; AgentFactory &m_agentFactory;
Context::IDocumentReader &m_documentReader; Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger; IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer; QElapsedTimer m_completionTimer;

View File

@@ -159,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_refactorWidgetHandler = new RefactorWidgetHandler(this); m_refactorWidgetHandler = new RefactorWidgetHandler(this);
} }
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
QodeAssistClient::~QodeAssistClient() QodeAssistClient::~QodeAssistClient()
{ {
cleanupConnections(); cleanupConnections();
@@ -319,6 +329,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!m_refactorHandler) { if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this); m_refactorHandler = new QuickRefactorHandler(this);
m_refactorHandler->setSessionManager(m_sessionManager);
m_refactorHandler->setAgentFactory(m_agentFactory);
connect( connect(
m_refactorHandler, m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted, &QuickRefactorHandler::refactoringCompleted,

View File

@@ -6,6 +6,7 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QPointer>
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp" #include "LSPCompletion.hpp"
@@ -21,6 +22,9 @@
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class AgentFactory;
class QodeAssistClient : public LanguageClient::Client class QodeAssistClient : public LanguageClient::Client
{ {
Q_OBJECT Q_OBJECT
@@ -28,6 +32,9 @@ public:
explicit QodeAssistClient(LLMClientInterface *clientInterface); explicit QodeAssistClient(LLMClientInterface *clientInterface);
~QodeAssistClient() override; ~QodeAssistClient() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void openDocument(TextEditor::TextDocument *document) override; void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override; bool canOpenProject(ProjectExplorer::Project *project) override;
@@ -68,6 +75,8 @@ private:
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr}; RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr}; RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient; LLMClientInterface *m_llmClient;
SessionManager *m_sessionManager{nullptr};
AgentFactory *m_agentFactory{nullptr};
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -4,24 +4,38 @@
#include "QuickRefactorHandler.hpp" #include "QuickRefactorHandler.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <context/DocumentContextReader.hpp> #include <context/DocumentContextReader.hpp>
#include <pluginllmcore/ResponseCleaner.hpp>
#include <context/DocumentReaderQtCreator.hpp> #include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp> #include <context/Utils.hpp>
#include <pluginllmcore/PromptTemplateManager.hpp>
#include <pluginllmcore/ProvidersManager.hpp>
#include <pluginllmcore/RulesLoader.hpp>
#include <logger/Logger.hpp> #include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp> #include <pluginllmcore/ResponseCleaner.hpp>
#include <settings/GeneralSettings.hpp>
#include <settings/QuickRefactorSettings.hpp> #include <settings/QuickRefactorSettings.hpp>
#include <settings/ToolsSettings.hpp> #include <settings/ToolsSettings.hpp>
#include "sources/common/ContextData.hpp"
#include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include "sources/settings/PipelinesConfig.hpp"
#include "tools/ToolsRegistration.hpp"
namespace QodeAssist { namespace QodeAssist {
QuickRefactorHandler::QuickRefactorHandler(QObject *parent) QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
@@ -34,6 +48,16 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
QuickRefactorHandler::~QuickRefactorHandler() {} QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
void QuickRefactorHandler::sendRefactorRequest( void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions) TextEditor::TextEditorWidget *editor, const QString &instructions)
{ {
@@ -88,61 +112,70 @@ void QuickRefactorHandler::sendRefactorRequest(
prepareAndSendRequest(editor, instructions, range); prepareAndSendRequest(editor, instructions, range);
} }
QString QuickRefactorHandler::pickRefactorAgent(const QString &filePath) const
{
const QStringList roster = Settings::PipelinesConfig::load().rosters.quickRefactor;
if (roster.isEmpty() || !m_agentFactory)
return {};
AgentRouter::Context ctx;
ctx.filePath = filePath;
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath)))
ctx.projectName = project->displayName();
return AgentRouter::pickAgent(roster, ctx, *m_agentFactory);
}
void QuickRefactorHandler::prepareAndSendRequest( void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor,
const QString &instructions, const QString &instructions,
const Utils::Text::Range &range) const Utils::Text::Range &range)
{ {
auto &settings = Settings::generalSettings(); const auto emitError = [this, editor](const QString &error) {
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
const auto providerName = settings.qrProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error); LOG_MESSAGE(error);
RefactorResult result; RefactorResult result;
result.success = false; result.success = false;
result.errorMessage = error; result.errorMessage = error;
result.editor = editor; result.editor = editor;
emit refactoringCompleted(result); emit refactoringCompleted(result);
};
if (!m_sessionManager) {
emitError(QStringLiteral("Quick refactor session manager is not available"));
return; return;
} }
const auto templateName = settings.qrTemplate(); const QString filePath = editor->textDocument()->filePath().toUrlishString();
auto promptTemplate = promptManager.getChatTemplateByName(templateName); const QString agentName = pickRefactorAgent(filePath);
if (agentName.isEmpty()) {
if (!promptTemplate) { emitError(QStringLiteral("No quick refactor agent matches: %1").arg(filePath));
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
return; return;
} }
QJsonObject payload{ QString sessionError;
{"model", Settings::generalSettings().qrModel()}, {"stream", true}}; Session *session = m_sessionManager->createSession(agentName, &sessionError);
if (!session) {
emitError(sessionError.isEmpty() ? QStringLiteral("No quick refactor agent selected")
: sessionError);
return;
}
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions); auto *client = session->client();
if (!client) {
m_sessionManager->removeSession(session);
emitError(QStringLiteral("Quick refactor agent has no live client"));
return;
}
bool enableTools = Settings::quickRefactorSettings().useTools(); const bool enableTools = Settings::quickRefactorSettings().useTools();
bool enableThinking = Settings::quickRefactorSettings().useThinking(); if (enableTools) {
provider->prepareRequest( Tools::registerQodeAssistTools(client->tools());
payload, client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
promptTemplate, }
context,
PluginLLMCore::RequestType::QuickRefactoring,
enableTools,
enableThinking);
provider->client()->setMaxToolContinuations( session->systemPrompt()->setLayer(
Settings::toolsSettings().maxToolContinuations()); QStringLiteral("refactor"), buildSystemPrompt(editor, range));
provider->client()->setTransferTimeout( provider->client()->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000)); static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
@@ -150,43 +183,38 @@ void QuickRefactorHandler::prepareAndSendRequest(
m_isRefactoringInProgress = true; m_isRefactoringInProgress = true;
connect( connect(
provider->client(), client, &::LLMQore::BaseClient::requestCompleted,
&::LLMQore::BaseClient::requestCompleted, this, &QuickRefactorHandler::handleFullResponse, Qt::UniqueConnection);
this,
&QuickRefactorHandler::handleFullResponse,
Qt::UniqueConnection);
connect( connect(
provider->client(), client, &::LLMQore::BaseClient::requestFinalized,
&::LLMQore::BaseClient::requestFinalized, this, &QuickRefactorHandler::handleRequestFinalized, Qt::UniqueConnection);
this,
&QuickRefactorHandler::handleRequestFinalized,
Qt::UniqueConnection);
connect( connect(
provider->client(), client, &::LLMQore::BaseClient::requestFailed,
&::LLMQore::BaseClient::requestFailed, this, &QuickRefactorHandler::handleRequestFailed, Qt::UniqueConnection);
this,
&QuickRefactorHandler::handleRequestFailed, std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
Qt::UniqueConnection); const QString userMessage = instructions.isEmpty()
? QStringLiteral("Refactor the code to improve its quality and maintainability.")
: instructions;
blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
const LLMQore::RequestID requestId = session->send(std::move(blocks), enableTools);
if (requestId.isEmpty()) {
m_isRefactoringInProgress = false;
m_sessionManager->removeSession(session);
emitError(QStringLiteral("Failed to start quick refactor request for agent: %1")
.arg(agentName));
return;
}
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
m_lastRequestId = requestId; m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}}; m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
m_activeRequests[requestId] = {request, provider};
} }
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( QString QuickRefactorHandler::buildSystemPrompt(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
const Utils::Text::Range &range,
const QString &instructions)
{ {
PluginLLMCore::ContextData context; Q_UNUSED(range)
auto textDocument = editor->textDocument(); auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader; Context::DocumentReaderQtCreator documentReader;
@@ -194,7 +222,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
if (!documentInfo.document) { if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available"); LOG_MESSAGE("Error: Document is not available");
return context; return Settings::quickRefactorSettings().systemPrompt();
} }
QTextCursor cursor = editor->textCursor(); QTextCursor cursor = editor->textCursor();
@@ -270,17 +298,6 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt(); QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
project, PluginLLMCore::RulesContext::QuickRefactor);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for quick refactor");
}
}
systemPrompt += "\n\nFile information:"; systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType; systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath; systemPrompt += "\nFile path: " + documentInfo.filePath;
@@ -294,7 +311,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
"\n- Your output will completely replace the selected code" "\n- Your output will completely replace the selected code"
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position" : "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
"\n- Your output will be inserted at the cursor location"; "\n- Your output will be inserted at the cursor location";
systemPrompt += "\n\n## Formatting Rules:" systemPrompt += "\n\n## Formatting Rules:"
"\n- Output ONLY the code itself, without ANY explanations or descriptions" "\n- Output ONLY the code itself, without ANY explanations or descriptions"
"\n- Do NOT include markdown code blocks (no ```, no language tags)" "\n- Do NOT include markdown code blocks (no ```, no language tags)"
@@ -302,9 +319,9 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
"\n- Do NOT repeat existing code, be precise with context" "\n- Do NOT repeat existing code, be precise with context"
"\n- Do NOT send in answer <cursor> or </cursor> and other tags" "\n- Do NOT send in answer <cursor> or </cursor> and other tags"
"\n- The output must be ready to insert directly into the editor as-is"; "\n- The output must be ready to insert directly into the editor as-is";
systemPrompt += "\n\n## Indentation and Whitespace:"; systemPrompt += "\n\n## Indentation and Whitespace:";
if (cursor.hasSelection()) { if (cursor.hasSelection()) {
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart()); QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
int leadingSpaces = 0; int leadingSpaces = 0;
@@ -336,7 +353,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
.arg(leadingSpaces); .arg(leadingSpaces);
} }
} }
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code" systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
"\n- Maintain consistent indentation for nested blocks" "\n- Maintain consistent indentation for nested blocks"
"\n- Do NOT remove or reduce the base indentation level" "\n- Do NOT remove or reduce the base indentation level"
@@ -349,16 +366,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath}); systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
} }
context.systemPrompt = systemPrompt; return systemPrompt;
QVector<PluginLLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
} }
void QuickRefactorHandler::handleLLMResponse( void QuickRefactorHandler::handleLLMResponse(
@@ -398,10 +406,14 @@ void QuickRefactorHandler::cancelRequest()
auto it = m_activeRequests.find(id); auto it = m_activeRequests.find(id);
if (it != m_activeRequests.end()) { if (it != m_activeRequests.end()) {
auto provider = it.value().provider; Session *session = it.value().session;
m_activeRequests.erase(it); m_activeRequests.erase(it);
if (provider) if (session) {
provider->cancelRequest(id); if (auto *client = session->client())
disconnect(client, nullptr, this, nullptr);
if (m_sessionManager)
m_sessionManager->removeSession(session);
}
} }
RefactorResult result; RefactorResult result;
@@ -412,11 +424,19 @@ void QuickRefactorHandler::cancelRequest()
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText) void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
{ {
if (requestId == m_lastRequestId) { if (requestId != m_lastRequestId)
m_activeRequests.remove(requestId); return;
QJsonObject request{{"id", requestId}};
handleLLMResponse(fullText, request, true); auto it = m_activeRequests.find(requestId);
} Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
if (it != m_activeRequests.end())
m_activeRequests.erase(it);
QJsonObject request{{"id", requestId}};
handleLLMResponse(fullText, request, true);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
} }
void QuickRefactorHandler::handleRequestFinalized( void QuickRefactorHandler::handleRequestFinalized(
@@ -437,15 +457,23 @@ void QuickRefactorHandler::handleRequestFinalized(
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error) void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
{ {
if (requestId == m_lastRequestId) { if (requestId != m_lastRequestId)
m_activeRequests.remove(requestId); return;
m_isRefactoringInProgress = false;
RefactorResult result; auto it = m_activeRequests.find(requestId);
result.success = false; Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
result.errorMessage = error; if (it != m_activeRequests.end())
result.editor = m_currentEditor; m_activeRequests.erase(it);
emit refactoringCompleted(result);
} m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = m_currentEditor;
emit refactoringCompleted(result);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
} }
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -6,6 +6,7 @@
#include <QJsonObject> #include <QJsonObject>
#include <QObject> #include <QObject>
#include <QPointer>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
@@ -13,11 +14,13 @@
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp> #include <context/IDocumentReader.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class Session;
class AgentFactory;
struct RefactorResult struct RefactorResult
{ {
QString newText; QString newText;
@@ -35,6 +38,9 @@ public:
explicit QuickRefactorHandler(QObject *parent = nullptr); explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override; ~QuickRefactorHandler() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions); void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest(); void cancelRequest();
@@ -56,17 +62,18 @@ private:
const Utils::Text::Range &range); const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
PluginLLMCore::ContextData prepareContext( QString buildSystemPrompt(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
const Utils::Text::Range &range, QString pickRefactorAgent(const QString &filePath) const;
const QString &instructions);
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
}; };
QPointer<SessionManager> m_sessionManager;
QPointer<AgentFactory> m_agentFactory;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
TextEditor::TextEditorWidget *m_currentEditor; TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange; Utils::Text::Range m_currentRange;

View File

@@ -100,13 +100,9 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
{ {
const auto &generalSettings = Settings::generalSettings(); Q_UNUSED(documentInfo)
// Language-specific completion presets were replaced by agent match rules.
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo); return false;
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
} }
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const

258
docs/architecture.md Normal file
View File

@@ -0,0 +1,258 @@
# QodeAssist Architecture
This document describes the runtime architecture of QodeAssist after the
migration of all LLM runtime paths onto the agent / `Session` stack
("Stack B"). Every runtime LLM path — code completion, chat (send/stream +
compression + token counting), and quick refactor — now goes through agents,
`Session`, and the `Providers::GenericProvider` layer.
> Legend: ✅ = on Stack B (active runtime), 🔴 = legacy Stack A (isolated, no
> runtime consumers left).
---
## 1. Top level: ownership and dependency injection
The plugin (`qodeassist.cpp`) owns everything via `new` + parent (no plugin-wide
singletons; each feature receives its dependencies explicitly).
```
QodeAssistPlugin
Stack B infrastructure:
• Providers::registerBuiltinProviders() — registers 13 client_api types
• ProviderInstanceFactory — 14 instances from TOML
• ProviderSecretsStore
• AgentFactory — agents from TOML
• SessionManager(agentFactory)
• m_engine (QQmlEngine)
rootContext: "agentFactory", "sessionManager" — DI for chat (QML)
Wired into consumers:
• QodeAssistClient ← LLMClientInterface(*sessionManager, *agentFactory)
← setSessionManager / setAgentFactory (for quick refactor)
```
Chat lives in QML (`ChatRootView` is a `QML_ELEMENT`), so `AgentFactory` and
`SessionManager` are exposed as **context properties on the engine's root
context** and resolved in `ChatRootView` via
`qmlEngine(this)->rootContext()->contextProperty(...)`.
---
## 2. Stack B core (agent / Session)
```
AgentFactory.create(name)
configByName(name) → AgentConfig (TOML)
providerInstance, model, endpoint, role, messageFormat,
sampling, enableTools, enableThinking, match{filePatterns,...}
buildProviderForAgent:
instance = ProviderInstanceFactory.instanceByName(cfg.providerInstance)
provider = ProviderFactory::create(instance.clientApi) ◄── keystone
provider.setUrl(instance.url)
provider.setApiKey(secrets.read(instance.apiKeyRef))
Agent(config, provider)
promptTemplate = JsonPromptTemplate::fromConfig(cfg.messageFormat) (inja)
SessionManager.createSession(agentName) → Session(agent)
├─ ConversationHistory — messages as ContentBlocks
├─ SystemPromptBuilder — layers: agent.role + caller layers
└─ ResponseRouter(client) — emits ResponseEvent
Session API:
• send(blocks, toolsOverride) — chat/refactor: append user msg + dispatch
• sendCompletion(ContextData) — completion: FIM prefix/suffix
• client() — agent's LLMQore::BaseClient (direct streaming)
• systemPrompt()->setLayer(...) — dynamic context layers
• supportsImages() — provider Image capability
• history() — for seeding from ChatModel
```
`Session::sendCompletion` and `dispatch` compose `SystemPromptBuilder` layers
(`agent.role` + caller-provided) into the request system prompt.
---
## 3. Provider layer — the keystone (implemented during migration)
The Stack B provider layer previously existed only as an abstract base +
empty factory (`registerType` was never called, no concrete providers). This
blocked every agent from obtaining a working provider. It is now implemented
via a single configuration-driven `GenericProvider`.
```
ProviderFactory (sources/providers, namespace functions)
registerType(name, fn) / create(name, parent) / knownNames()
│ registerBuiltinProviders() — client_api → provider table
GenericProvider : Providers::Provider
• owns an LLMQore::BaseClient (created by a ClientFactory)
• prepareRequest — inherited from Provider base:
delegates to PromptTemplate::buildFullRequest
• client() / providerID() / capabilities() / getInstalledModels()
```
### client_api → provider table
| client_api | LLMQore client | ProviderID | capabilities |
|--------------------------------|-------------------------|------------------|-------------------------|
| Claude | ClaudeClient | Claude | Tools·Thinking·Image·ModelListing |
| Google AI | GoogleAIClient | GoogleAI | Tools·Thinking·Image·ModelListing |
| llama.cpp | LlamaCppClient | LlamaCpp | Tools·Thinking·Image·ModelListing |
| Mistral AI | MistralClient | MistralAI | Tools·Thinking·Image·ModelListing |
| Codestral | MistralClient | MistralAI | Tools·Image |
| Ollama (Native) | OllamaClient | Ollama | Tools·Thinking·Image·ModelListing |
| Ollama (OpenAI-compatible) | OpenAIClient | OpenAICompatible | Tools·Thinking·Image·ModelListing |
| OpenAI (Chat Completions) | OpenAIClient | OpenAI | Tools·Thinking·Image·ModelListing |
| OpenAI (Responses API) | OpenAIResponsesClient | OpenAIResponses | Tools·Thinking·Image·ModelListing |
| OpenAI Compatible | OpenAIClient | OpenAICompatible | Tools·Image·Thinking |
| OpenRouter | OpenAIClient | OpenRouter | Tools·Image·Thinking·ModelListing |
| LM Studio (Chat Completions) | OpenAIClient | LMStudio | Tools·Thinking·Image·ModelListing |
| LM Studio (Responses API) | OpenAIResponsesClient | OpenAIResponses | Tools·Thinking·Image·ModelListing |
Request *shape* comes from the agent's prompt template (jinja `messageFormat`),
so a single provider class covers every API by varying only the client factory
and metadata.
---
## 4. Runtime paths (all on Stack B)
### 4a. Code completion ✅
```
Qt Creator LSP (getCompletionsCycling)
LLMClientInterface
pickCompletionAgent: AgentRouter.pickAgent(roster.codeCompletion, {file, project})
session = sessionManager.createSession(agent)
ctx = Templates::ContextData{ prefix, suffix,
systemPrompt = fileContext + openFiles }
session.sendCompletion(ctx)
▼ stream from session.client():
requestCompleted → sendCompletionToClient → CodeHandler → LSP
system prompt = agent.role; FIM template renders prefix/suffix
```
### 4b. Chat ✅
```
ChatRootView (QML)
resolve agentFactory()/sessionManager() = qmlEngine(this)->rootContext()
ChatAgentController: agent list (configNames), active agent (persisted),
supportsThinking/Tools
QML agent picker (TopBar.agentSelector) — replaced provider/model/template combos
▼ dispatchSend
ClientInterface
session = sessionManager.createSession(currentChatAgent)
registerQodeAssistTools(session.client().tools()) + registerSkillTool
systemPrompt layer "chat.context" = project info + skills + linked files
seedHistory(session.history() ← ChatModel: user/assistant/tool-call+result)
session.send(userBlocks{text + images}, useTools)
▼ stream from session.client() → existing handlers → ChatModel:
chunk→addMessage thinking→addThinkingBlock
tool→addToolExecutionStatus / updateToolResult
finalized→usage completed→messageReceivedCompletely → removeSession
ChatCompressor → createSession(agent) → seed history → layer "compression" → send(prompt)
InputTokenCounter → estimate without provider (calibrated by server usage)
```
### 4c. Quick refactor ✅
```
QodeAssistClient.requestQuickRefactor → QuickRefactorHandler (setSessionManager/setAgentFactory)
pickRefactorAgent: AgentRouter.pickAgent(roster.quickRefactor, {file, project})
session = createSession(agent)
if useTools: registerQodeAssistTools(session.client().tools())
systemPrompt layer "refactor" = buildSystemPrompt(tagged content +
output requirements + indentation rules)
session.send(blocks{instructions}, useTools)
▼ stream from session.client():
requestCompleted → ResponseCleaner → RefactorResult → insert into editor
```
---
## 5. Configuration sources
```
~/.config/.../qodeassist/config/
providers/*.toml → ProviderInstance { name, client_api, url, api_key_ref }
agents/*.toml → AgentConfig { providerInstance, model, endpoint, role,
messageFormat, sampling, match, enable* }
pipelines rosters → codeCompletion / chatAssistant / chatCompression / quickRefactor
consumed by AgentRouter.pickAgent(roster, {filePath, projectName})
Editor policy (NOT agent config):
CodeCompletionSettings — triggers, modelOutputHandler, context extraction,
useOpenFilesContext
(sampling / prompt-generation fields removed)
```
---
## 6. Remaining Stack A (runtime does NOT depend on it)
```
🔴 Settings UI: provider/model/template selection pages
(ccProvider / caProvider / qrProvider) + ConfigurationManager
→ use ProvidersManager
🔴 root providers/* (PluginLLMCore::Provider, 14 classes)
→ read only chat/quick-refactor sampling settings
🔴 pluginllmcore/* (ProvidersManager, PromptTemplateManager, ResponseCleaner,
PromptProviderChat/Fim, ContextData)
🔴 qodeassist.cpp:144-146 registerProviders() / registerTemplates() (Stack A registration)
🔴 qodeassist.cpp:185 MCP skill-tool loop on Stack A providers (effectively dead)
🔴 ChatAssistantSettings / QuickRefactorSettings — sampling fields (read only by root providers)
ResponseCleaner (pluginllmcore) is still used by QuickRefactorHandler as a text
utility — orthogonal to the provider stack.
```
### Removed during the migration
- Rules subsystem (`RulesLoader` + chat "active rules" UI + QuickRefactor rules block)
- `ChatConfigurationController`, `AgentRoleController` (chat config/role presets)
- `m_promptProvider` (`PromptProviderFim`) in the plugin
- `RequestType::CodeCompletion` branch in all 14 root providers
- Sampling / prompt-generation fields in `CodeCompletionSettings`
- ChatView no longer links `PluginLLMCore`
---
## 7. Dependency summary
```
┌──────────────── Stack B (active runtime) ────────────────┐
LLMClientInterface ─┐ │
ClientInterface ────┼─► SessionManager ─► Session ─► Agent ─► GenericProvider ─► LLMQore::*Client
QuickRefactorHandler─┘ │ │ │ │
ChatCompressor ──────────────┘ │ AgentFactory ProviderFactory
AgentRouter (rosters) │ │
ProviderInstanceFactory (TOML)
└──────────────────────────────────────────────────────────┘
Stack A (settings UI + ConfigurationManager + MCP loop) — isolated,
no runtime consumers remain.
```
---
## 8. Open follow-ups (optional)
1. **Chat picker filtering** — show only `chatAssistant`-roster agents (currently
lists all non-hidden agents; the auto-default may land on a FIM agent).
Requires wiring ChatView to `PipelinesConfig` (watch for OBJECT-library
symbol duplication).
2. **MCP tools on agent clients** — MCP skill tools are registered only on Stack A
providers; to expose MCP tools to chat agents, register them on the session
client alongside `registerQodeAssistTools`.
3. **Physical Stack A teardown** — remove the provider/model/template settings UI,
`ConfigurationManager`, root `providers/*`, `pluginllmcore/*`, and the
registration + MCP loop in `qodeassist.cpp`. Runtime no longer depends on them.
4. **Per-message session cost** — chat/refactor create a fresh agent/provider/client
(and read secrets) per request; a session pool could reduce latency.
```

View File

@@ -10,7 +10,6 @@ add_library(PluginLLMCore STATIC
PromptTemplate.hpp PromptTemplate.hpp
PromptTemplateManager.hpp PromptTemplateManager.cpp PromptTemplateManager.hpp PromptTemplateManager.cpp
ProviderID.hpp ProviderID.hpp
RulesLoader.hpp RulesLoader.cpp
ResponseCleaner.hpp ResponseCleaner.hpp
) )

View File

@@ -1,166 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "RulesLoader.hpp"
#include <QDir>
#include <QFile>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
namespace QodeAssist::PluginLLMCore {
QString RulesLoader::loadRules(const QString &projectPath, RulesContext context)
{
if (projectPath.isEmpty()) {
return QString();
}
QString combined;
QString basePath = projectPath + "/.qodeassist/rules";
switch (context) {
case RulesContext::Completions:
combined += loadAllMarkdownFiles(basePath + "/completions");
break;
case RulesContext::Chat:
combined += loadAllMarkdownFiles(basePath + "/common");
combined += loadAllMarkdownFiles(basePath + "/chat");
break;
case RulesContext::QuickRefactor:
combined += loadAllMarkdownFiles(basePath + "/common");
combined += loadAllMarkdownFiles(basePath + "/quickrefactor");
break;
}
return combined;
}
QString RulesLoader::loadRulesForProject(ProjectExplorer::Project *project, RulesContext context)
{
if (!project) {
return QString();
}
QString projectPath = getProjectPath(project);
return loadRules(projectPath, context);
}
ProjectExplorer::Project *RulesLoader::getActiveProject()
{
auto currentEditor = Core::EditorManager::currentEditor();
if (currentEditor && currentEditor->document()) {
Utils::FilePath filePath = currentEditor->document()->filePath();
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
if (project) {
return project;
}
}
return ProjectExplorer::ProjectManager::startupProject();
}
QString RulesLoader::loadAllMarkdownFiles(const QString &dirPath)
{
QString combined;
QDir dir(dirPath);
if (!dir.exists()) {
return QString();
}
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
for (const QString &fileName : mdFiles) {
QFile file(dir.filePath(fileName));
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
combined += file.readAll();
combined += "\n\n";
}
}
return combined;
}
QString RulesLoader::getProjectPath(ProjectExplorer::Project *project)
{
if (!project) {
return QString();
}
return project->projectDirectory().toUrlishString();
}
QVector<RuleFileInfo> RulesLoader::getRuleFiles(const QString &projectPath, RulesContext context)
{
if (projectPath.isEmpty()) {
return QVector<RuleFileInfo>();
}
QVector<RuleFileInfo> result;
QString basePath = projectPath + "/.qodeassist/rules";
// Always include common rules
result.append(collectMarkdownFiles(basePath + "/common", "common"));
// Add context-specific rules
switch (context) {
case RulesContext::Completions:
result.append(collectMarkdownFiles(basePath + "/completions", "completions"));
break;
case RulesContext::Chat:
result.append(collectMarkdownFiles(basePath + "/chat", "chat"));
break;
case RulesContext::QuickRefactor:
result.append(collectMarkdownFiles(basePath + "/quickrefactor", "quickrefactor"));
break;
}
return result;
}
QVector<RuleFileInfo> RulesLoader::getRuleFilesForProject(
ProjectExplorer::Project *project, RulesContext context)
{
if (!project) {
return QVector<RuleFileInfo>();
}
QString projectPath = getProjectPath(project);
return getRuleFiles(projectPath, context);
}
QString RulesLoader::loadRuleFileContent(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return QString();
}
return file.readAll();
}
QVector<RuleFileInfo> RulesLoader::collectMarkdownFiles(
const QString &dirPath, const QString &category)
{
QVector<RuleFileInfo> result;
QDir dir(dirPath);
if (!dir.exists()) {
return result;
}
QStringList mdFiles = dir.entryList({"*.md"}, QDir::Files, QDir::Name);
for (const QString &fileName : mdFiles) {
QString fullPath = dir.filePath(fileName);
result.append({fullPath, fileName, category});
}
return result;
}
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,42 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QString>
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::PluginLLMCore {
enum class RulesContext { Completions, Chat, QuickRefactor };
struct RuleFileInfo
{
QString filePath;
QString fileName;
QString category; // "common", "chat", "completions", "quickrefactor"
};
class RulesLoader
{
public:
static QString loadRules(const QString &projectPath, RulesContext context);
static QString loadRulesForProject(ProjectExplorer::Project *project, RulesContext context);
static ProjectExplorer::Project *getActiveProject();
// New methods for getting rule files info
static QVector<RuleFileInfo> getRuleFiles(const QString &projectPath, RulesContext context);
static QVector<RuleFileInfo> getRuleFilesForProject(ProjectExplorer::Project *project, RulesContext context);
static QString loadRuleFileContent(const QString &filePath);
private:
static QString loadAllMarkdownFiles(const QString &dirPath);
static QVector<RuleFileInfo> collectMarkdownFiles(const QString &dirPath, const QString &category);
static QString getProjectPath(ProjectExplorer::Project *project);
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -94,10 +94,7 @@ void ClaudeProvider::prepareRequest(
request["temperature"] = 1.0; request["temperature"] = 1.0;
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
request["temperature"] = Settings::codeCompletionSettings().temperature();
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings(); const auto &qrSettings = Settings::quickRefactorSettings();
applyModelParams(qrSettings); applyModelParams(qrSettings);

View File

@@ -86,9 +86,7 @@ void DeepSeekProvider::prepareRequest(
request["presence_penalty"] = settings.presencePenalty(); request["presence_penalty"] = settings.presencePenalty();
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings()); applyModelParams(Settings::quickRefactorSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());

View File

@@ -92,9 +92,7 @@ void GoogleAIProvider::prepareRequest(
request["generationConfig"] = generationConfig; request["generationConfig"] = generationConfig;
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings(); const auto &qrSettings = Settings::quickRefactorSettings();
if (isThinkingEnabled) { if (isThinkingEnabled) {

View File

@@ -90,9 +90,7 @@ void LMStudioProvider::prepareRequest(
request["presence_penalty"] = settings.presencePenalty(); request["presence_penalty"] = settings.presencePenalty();
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings()); applyModelParams(Settings::quickRefactorSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());

View File

@@ -82,9 +82,7 @@ void LMStudioResponsesProvider::prepareRequest(
request["include"] = include; request["include"] = include;
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings(); const auto &qrSettings = Settings::quickRefactorSettings();
applyModelParams(qrSettings); applyModelParams(qrSettings);

View File

@@ -75,9 +75,7 @@ void LlamaCppProvider::prepareRequest(
request["chat_template_kwargs"] = chatTemplateKwargs; request["chat_template_kwargs"] = chatTemplateKwargs;
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings()); applyModelParams(Settings::quickRefactorSettings());
if (isThinkingEnabled) { if (isThinkingEnabled) {
applyThinkingMode(); applyThinkingMode();

View File

@@ -88,9 +88,7 @@ void MistralAIProvider::prepareRequest(
request["presence_penalty"] = settings.presencePenalty(); request["presence_penalty"] = settings.presencePenalty();
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings()); applyModelParams(Settings::quickRefactorSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());

View File

@@ -70,9 +70,7 @@ void OllamaCompatProvider::prepareRequest(
request["presence_penalty"] = settings.presencePenalty(); request["presence_penalty"] = settings.presencePenalty();
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings()); applyModelParams(Settings::quickRefactorSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());

View File

@@ -82,9 +82,7 @@ void OllamaProvider::prepareRequest(
request["options"] = options; request["options"] = options;
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applySettings(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings(); const auto &qrSettings = Settings::quickRefactorSettings();
applySettings(qrSettings); applySettings(qrSettings);

View File

@@ -69,9 +69,7 @@ void OpenAICompatProvider::prepareRequest(
request["presence_penalty"] = settings.presencePenalty(); request["presence_penalty"] = settings.presencePenalty();
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings()); applyModelParams(Settings::quickRefactorSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());

View File

@@ -88,9 +88,7 @@ void OpenAIProvider::prepareRequest(
request["presence_penalty"] = settings.presencePenalty(); request["presence_penalty"] = settings.presencePenalty();
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings()); applyModelParams(Settings::quickRefactorSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());

View File

@@ -80,9 +80,7 @@ void OpenAIResponsesProvider::prepareRequest(
request["include"] = include; request["include"] = include;
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings(); const auto &qrSettings = Settings::quickRefactorSettings();
applyModelParams(qrSettings); applyModelParams(qrSettings);

View File

@@ -88,9 +88,7 @@ void QwenProvider::prepareRequest(
request["presence_penalty"] = settings.presencePenalty(); request["presence_penalty"] = settings.presencePenalty();
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::quickRefactorSettings()); applyModelParams(Settings::quickRefactorSettings());
} else { } else {
applyModelParams(Settings::chatAssistantSettings()); applyModelParams(Settings::chatAssistantSettings());

View File

@@ -80,9 +80,7 @@ void QwenResponsesProvider::prepareRequest(
request["include"] = include; request["include"] = include;
}; };
if (type == PluginLLMCore::RequestType::CodeCompletion) { if (type == PluginLLMCore::RequestType::QuickRefactoring) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings(); const auto &qrSettings = Settings::quickRefactorSettings();
applyModelParams(qrSettings); applyModelParams(qrSettings);

View File

@@ -34,7 +34,6 @@
#include <QTranslator> #include <QTranslator>
#include <QInputDialog> #include <QInputDialog>
#include "ConfigurationManager.hpp"
#include "QodeAssistClient.hpp" #include "QodeAssistClient.hpp"
#include "UpdateStatusWidget.hpp" #include "UpdateStatusWidget.hpp"
#include "Version.hpp" #include "Version.hpp"
@@ -43,7 +42,6 @@
#include "chat/ChatOutputPane.h" #include "chat/ChatOutputPane.h"
#include "chat/NavigationPanel.hpp" #include "chat/NavigationPanel.hpp"
#include "context/DocumentReaderQtCreator.hpp" #include "context/DocumentReaderQtCreator.hpp"
#include "pluginllmcore/PromptProviderFim.hpp"
#include "pluginllmcore/ProvidersManager.hpp" #include "pluginllmcore/ProvidersManager.hpp"
#include "logger/RequestPerformanceLogger.hpp" #include "logger/RequestPerformanceLogger.hpp"
#include "mcp/McpClientsManager.hpp" #include "mcp/McpClientsManager.hpp"
@@ -56,7 +54,6 @@
#include "settings/ProjectSettingsPanel.hpp" #include "settings/ProjectSettingsPanel.hpp"
#include "settings/AgentsSettingsPage.hpp" #include "settings/AgentsSettingsPage.hpp"
#include "settings/ProvidersSettingsPage.hpp" #include "settings/ProvidersSettingsPage.hpp"
#include "sources/settings/AgentPipelinesPage.hpp"
#include "settings/QuickRefactorSettings.hpp" #include "settings/QuickRefactorSettings.hpp"
#include "settings/SettingsConstants.hpp" #include "settings/SettingsConstants.hpp"
@@ -65,6 +62,8 @@
#include "ProviderSecretsStore.hpp" #include "ProviderSecretsStore.hpp"
#include <AgentFactory.hpp> #include <AgentFactory.hpp>
#include <GenericProvider.hpp>
#include <SessionManager.hpp>
#include "templates/Templates.hpp" #include "templates/Templates.hpp"
#include "widgets/CustomInstructionsManager.hpp" #include "widgets/CustomInstructionsManager.hpp"
#include "widgets/QuickRefactorDialog.hpp" #include "widgets/QuickRefactorDialog.hpp"
@@ -74,6 +73,7 @@
#include <ChatView/ChatWidget.hpp> #include <ChatView/ChatWidget.hpp>
#include <ChatView/SessionFileRegistry.hpp> #include <ChatView/SessionFileRegistry.hpp>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <QQmlContext>
#include <QUuid> #include <QUuid>
#include <coreplugin/actionmanager/actioncontainer.h> #include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h> #include <coreplugin/actionmanager/actionmanager.h>
@@ -95,7 +95,6 @@ class QodeAssistPlugin final : public ExtensionSystem::IPlugin
public: public:
QodeAssistPlugin() QodeAssistPlugin()
: m_updater(new PluginUpdater(this)) : m_updater(new PluginUpdater(this))
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
{} {}
~QodeAssistPlugin() final ~QodeAssistPlugin() final
@@ -203,8 +202,8 @@ public:
m_engine, m_sessionFileRegistry, m_skillsManager}; m_engine, m_sessionFileRegistry, m_skillsManager};
Settings::setupProjectPanel(); Settings::setupProjectPanel();
ConfigurationManager::instance().init();
Providers::registerBuiltinProviders();
m_providerInstanceFactory = new Providers::ProviderInstanceFactory(this); m_providerInstanceFactory = new Providers::ProviderInstanceFactory(this);
m_providerSecretsStore = new Providers::ProviderSecretsStore(this); m_providerSecretsStore = new Providers::ProviderSecretsStore(this);
m_providerLauncher = new Providers::ProviderLauncher(this); m_providerLauncher = new Providers::ProviderLauncher(this);
@@ -216,6 +215,9 @@ public:
m_providersPageNavigator); m_providersPageNavigator);
m_agentFactory = new AgentFactory(m_providerInstanceFactory, m_providerSecretsStore, this); m_agentFactory = new AgentFactory(m_providerInstanceFactory, m_providerSecretsStore, this);
m_sessionManager = new SessionManager(m_agentFactory, this);
m_engine->rootContext()->setContextProperty("agentFactory", m_agentFactory);
m_engine->rootContext()->setContextProperty("sessionManager", m_sessionManager);
m_agentsPageNavigator = new Settings::AgentsPageNavigator(this); m_agentsPageNavigator = new Settings::AgentsPageNavigator(this);
m_agentsOptionsPage = Settings::createAgentsSettingsPage( m_agentsOptionsPage = Settings::createAgentsSettingsPage(
m_agentFactory, m_agentsPageNavigator); m_agentFactory, m_agentsPageNavigator);
@@ -342,10 +344,12 @@ public:
m_qodeAssistClient = new QodeAssistClient(new LLMClientInterface( m_qodeAssistClient = new QodeAssistClient(new LLMClientInterface(
Settings::generalSettings(), Settings::generalSettings(),
Settings::codeCompletionSettings(), Settings::codeCompletionSettings(),
PluginLLMCore::ProvidersManager::instance(), *m_sessionManager,
&m_promptProvider, *m_agentFactory,
m_documentReader, m_documentReader,
m_performanceLogger)); m_performanceLogger));
m_qodeAssistClient->setSessionManager(m_sessionManager);
m_qodeAssistClient->setAgentFactory(m_agentFactory);
} }
bool delayedInitialize() final bool delayedInitialize() final
@@ -503,7 +507,6 @@ private:
} }
QPointer<QodeAssistClient> m_qodeAssistClient; QPointer<QodeAssistClient> m_qodeAssistClient;
PluginLLMCore::PromptProviderFim m_promptProvider;
Context::DocumentReaderQtCreator m_documentReader; Context::DocumentReaderQtCreator m_documentReader;
RequestPerformanceLogger m_performanceLogger; RequestPerformanceLogger m_performanceLogger;
QPointer<Chat::ChatOutputPane> m_chatOutputPane; QPointer<Chat::ChatOutputPane> m_chatOutputPane;
@@ -524,6 +527,7 @@ private:
QPointer<Settings::ProvidersPageNavigator> m_providersPageNavigator; QPointer<Settings::ProvidersPageNavigator> m_providersPageNavigator;
std::unique_ptr<Core::IOptionsPage> m_providersOptionsPage; std::unique_ptr<Core::IOptionsPage> m_providersOptionsPage;
QPointer<AgentFactory> m_agentFactory; QPointer<AgentFactory> m_agentFactory;
QPointer<SessionManager> m_sessionManager;
QPointer<Settings::AgentsPageNavigator> m_agentsPageNavigator; QPointer<Settings::AgentsPageNavigator> m_agentsPageNavigator;
std::unique_ptr<Core::IOptionsPage> m_agentsOptionsPage; std::unique_ptr<Core::IOptionsPage> m_agentsOptionsPage;
QPointer<Settings::AgentPipelinesPageNavigator> m_agentPipelinesPageNavigator; QPointer<Settings::AgentPipelinesPageNavigator> m_agentPipelinesPageNavigator;

View File

@@ -1,6 +1,5 @@
add_library(QodeAssistSettings STATIC add_library(QodeAssistSettings STATIC
GeneralSettings.hpp GeneralSettings.cpp GeneralSettings.hpp GeneralSettings.cpp
ConfigurationManager.hpp ConfigurationManager.cpp
SettingsUtils.hpp SettingsUtils.hpp
SettingsConstants.hpp SettingsConstants.hpp
ButtonAspect.hpp ButtonAspect.hpp
@@ -47,5 +46,9 @@ target_link_libraries(QodeAssistSettings
ProvidersConfig ProvidersConfig
Agents Agents
Skills Skills
QodeAssistAgentPipelines
)
target_include_directories(QodeAssistSettings
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_SOURCE_DIR}/sources/settings
) )
target_include_directories(QodeAssistSettings PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -165,54 +165,6 @@ CodeCompletionSettings::CodeCompletionSettings()
"for triggering completions. This helps trigger completions based on actual code " "for triggering completions. This helps trigger completions based on actual code "
"characters only.")); "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 // Context Settings
readFullFile.setSettingsKey(Constants::CC_READ_FULL_FILE); readFullFile.setSettingsKey(Constants::CC_READ_FULL_FILE);
readFullFile.setLabelText(Tr::tr("Read Full File")); readFullFile.setLabelText(Tr::tr("Read Full File"));
@@ -240,43 +192,6 @@ CodeCompletionSettings::CodeCompletionSettings()
"You are an expert C++, Qt, and QML code completion assistant. Your task is to provide " "You are an expert C++, Qt, and QML code completion assistant. Your task is to provide "
"precise and contextually appropriate code completions.\n\n"); "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.setSettingsKey(Constants::CC_CUSTOM_LANGUAGES);
customLanguages.setLabelText( customLanguages.setLabelText(
Tr::tr("Additional Programming Languages for handling: Example: rust,//,rust rs,rs")); Tr::tr("Additional Programming Languages for handling: Example: rust,//,rust rs,rs"));
@@ -311,39 +226,6 @@ CodeCompletionSettings::CodeCompletionSettings()
maxChangesCacheSize.setRange(2, 1000); maxChangesCacheSize.setRange(2, 1000);
maxChangesCacheSize.setDefaultValue(10); 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"); resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
readSettings(); readSettings();
@@ -357,23 +239,6 @@ CodeCompletionSettings::CodeCompletionSettings()
setLayouter([this]() { setLayouter([this]() {
using namespace Layouting; 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{}; auto contextGrid = Grid{};
contextGrid.addRow({Row{readFullFile}}); contextGrid.addRow({Row{readFullFile}});
contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}}); contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}});
@@ -382,14 +247,7 @@ CodeCompletionSettings::CodeCompletionSettings()
Row{contextGrid, Stretch{1}}, Row{contextGrid, Stretch{1}},
Row{useSystemPrompt, Stretch{1}}, Row{useSystemPrompt, Stretch{1}},
Group{title(Tr::tr("Prompts for FIM models")), Column{systemPrompt}}, Group{title(Tr::tr("Prompts for FIM models")), Column{systemPrompt}},
Group{ customLanguages,
title(Tr::tr("Prompts for Non FIM models")),
Column{
Row{useUserMessageTemplateForCC, Stretch{1}},
systemPromptForNonFimModels,
userMessageTemplateForCC,
customLanguages,
}},
Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}}; Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}};
auto generalSettings = Column{ auto generalSettings = Column{
@@ -418,19 +276,7 @@ CodeCompletionSettings::CodeCompletionSettings()
Space{8}, Space{8},
Group{title(Tr::tr("Automatic Trigger Mode")), autoTriggerSettings}}}, Group{title(Tr::tr("Automatic Trigger Mode")), autoTriggerSettings}}},
Space{8}, 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}, 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}}; Stretch{1}};
}); });
} }
@@ -470,16 +316,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
if (reply == QMessageBox::Yes) { if (reply == QMessageBox::Yes) {
resetAspect(autoCompletion); resetAspect(autoCompletion);
resetAspect(multiLineCompletion); 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(readFullFile);
resetAspect(readFileParts); resetAspect(readFileParts);
resetAspect(readStringsBeforeCursor); resetAspect(readStringsBeforeCursor);
@@ -488,12 +324,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
resetAspect(systemPrompt); resetAspect(systemPrompt);
resetAspect(useProjectChangesCache); resetAspect(useProjectChangesCache);
resetAspect(maxChangesCacheSize); resetAspect(maxChangesCacheSize);
resetAspect(ollamaLivetime);
resetAspect(contextWindow);
resetAspect(openAIResponsesReasoningEffort);
resetAspect(useUserMessageTemplateForCC);
resetAspect(userMessageTemplateForCC);
resetAspect(systemPromptForNonFimModels);
resetAspect(customLanguages); resetAspect(customLanguages);
resetAspect(showProgressWidget); resetAspect(showProgressWidget);
resetAspect(useOpenFilesContext); resetAspect(useOpenFilesContext);
@@ -527,14 +357,6 @@ void CodeCompletionSettings::migrateCompletionMode()
writeSettings(); 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 class CodeCompletionSettingsPage : public Core::IOptionsPage
{ {
public: public:

View File

@@ -41,23 +41,6 @@ public:
Utils::BoolAspect abortAssistOnRequest{this}; Utils::BoolAspect abortAssistOnRequest{this};
Utils::BoolAspect useOpenFilesContext{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 // Context Settings
Utils::BoolAspect readFullFile{this}; Utils::BoolAspect readFullFile{this};
Utils::BoolAspect readFileParts{this}; Utils::BoolAspect readFileParts{this};
@@ -65,21 +48,9 @@ public:
Utils::IntegerAspect readStringsAfterCursor{this}; Utils::IntegerAspect readStringsAfterCursor{this};
Utils::BoolAspect useSystemPrompt{this}; Utils::BoolAspect useSystemPrompt{this};
Utils::StringAspect systemPrompt{this}; Utils::StringAspect systemPrompt{this};
Utils::BoolAspect useUserMessageTemplateForCC{this};
Utils::StringAspect systemPromptForNonFimModels{this};
Utils::StringAspect userMessageTemplateForCC{this};
Utils::BoolAspect useProjectChangesCache{this}; Utils::BoolAspect useProjectChangesCache{this};
Utils::IntegerAspect maxChangesCacheSize{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: private:
void setupConnections(); void setupConnections();
void resetSettingsToDefaults(); void resetSettingsToDefaults();

View File

@@ -1,373 +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 "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

@@ -1,66 +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 <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

@@ -4,19 +4,23 @@
#pragma once #pragma once
#include <memory>
#include <QObject>
#include <QString>
#include <utils/aspects.h> #include <utils/aspects.h>
#include <QPointer>
#include "ButtonAspect.hpp" #include "ButtonAspect.hpp"
#include "ConfigurationManager.hpp"
namespace Utils { namespace Core {
class DetailsWidget; class IOptionsPage;
} }
namespace QodeAssist::PluginLLMCore { namespace QodeAssist {
class Provider; class AgentFactory;
} }
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
class GeneralSettings : public Utils::AspectContainer class GeneralSettings : public Utils::AspectContainer
@@ -33,139 +37,9 @@ public:
ButtonAspect checkUpdate{this}; ButtonAspect checkUpdate{this};
ButtonAspect resetToDefaults{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: private:
void setupConnections(); void setupConnections();
void resetPageToDefaults(); void resetPageToDefaults();
QVector<AIConfiguration> m_ccPresets;
QVector<AIConfiguration> m_caPresets;
QVector<AIConfiguration> m_qrPresets;
}; };
GeneralSettings &generalSettings(); GeneralSettings &generalSettings();
@@ -173,4 +47,22 @@ GeneralSettings &generalSettings();
void showSettings(const Utils::Id page); void showSettings(const Utils::Id page);
void showSettings(const Utils::Id page, Utils::Id item); void showSettings(const Utils::Id page, Utils::Id item);
class AgentsPageNavigator;
class AgentPipelinesPageNavigator : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(AgentPipelinesPageNavigator)
public:
explicit AgentPipelinesPageNavigator(QObject *parent = nullptr);
signals:
void editAgentRequested(const QString &agentName);
};
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
AgentFactory *agentFactory,
AgentPipelinesPageNavigator *navigator,
AgentsPageNavigator *agentsNavigator);
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

@@ -179,31 +179,11 @@ const char CC_READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.ccReadStringsBeforeCurs
const char CC_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.ccReadStringsAfterCursor"; const char CC_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.ccReadStringsAfterCursor";
const char CC_USE_SYSTEM_PROMPT[] = "QodeAssist.ccUseSystemPrompt"; const char CC_USE_SYSTEM_PROMPT[] = "QodeAssist.ccUseSystemPrompt";
const char CC_SYSTEM_PROMPT[] = "QodeAssist.ccSystemPrompt"; 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_USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.ccUseProjectChangesCache";
const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize"; const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize";
const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt"; const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt";
const char CA_SYSTEM_PROMPT[] = "QodeAssist.chatSystemPrompt"; 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_TEMPERATURE[] = "QodeAssist.chatTemperature";
const char CA_MAX_TOKENS[] = "QodeAssist.chatMaxTokens"; const char CA_MAX_TOKENS[] = "QodeAssist.chatMaxTokens";
const char CA_USE_TOP_P[] = "QodeAssist.chatUseTopP"; const char CA_USE_TOP_P[] = "QodeAssist.chatUseTopP";

View File

@@ -5,5 +5,6 @@ add_subdirectory(common)
add_subdirectory(providers) add_subdirectory(providers)
add_subdirectory(templates) add_subdirectory(templates)
add_subdirectory(agents) add_subdirectory(agents)
add_subdirectory(Session)
add_subdirectory(providersConfig) add_subdirectory(providersConfig)
add_subdirectory(settings) add_subdirectory(settings)

View File

@@ -106,6 +106,19 @@ bool Session::isInFlight() const noexcept
return !m_inFlight.isEmpty(); return !m_inFlight.isEmpty();
} }
LLMQore::BaseClient *Session::client() const noexcept
{
auto *provider = m_agent ? m_agent->provider() : nullptr;
return provider ? provider->client() : nullptr;
}
bool Session::supportsImages() const noexcept
{
auto *provider = m_agent ? m_agent->provider() : nullptr;
return provider
&& provider->capabilities().testFlag(Providers::ProviderCapability::Image);
}
void Session::setContentLoader(ContentLoader loader) void Session::setContentLoader(ContentLoader loader)
{ {
m_contentLoader = std::move(loader); m_contentLoader = std::move(loader);
@@ -186,6 +199,13 @@ LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx)
auto *tmpl = m_agent->promptTemplate(); auto *tmpl = m_agent->promptTemplate();
const auto &cfg = m_agent->config(); const auto &cfg = m_agent->config();
const QString rolePrompt = m_systemPrompt ? m_systemPrompt->compose() : QString();
if (!rolePrompt.isEmpty()) {
ctx.systemPrompt = (ctx.systemPrompt && !ctx.systemPrompt->isEmpty())
? rolePrompt + QStringLiteral("\n\n") + *ctx.systemPrompt
: rolePrompt;
}
QJsonObject payload{{QStringLiteral("model"), cfg.model}}; QJsonObject payload{{QStringLiteral("model"), cfg.model}};
if (!provider->prepareRequest(payload, tmpl, ctx, /*tools=*/false, /*thinking=*/false)) if (!provider->prepareRequest(payload, tmpl, ctx, /*tools=*/false, /*thinking=*/false))
return {}; return {};

View File

@@ -55,6 +55,9 @@ public:
ConversationHistory *history() const noexcept { return m_history; } ConversationHistory *history() const noexcept { return m_history; }
SystemPromptBuilder *systemPrompt() const noexcept { return m_systemPrompt; } SystemPromptBuilder *systemPrompt() const noexcept { return m_systemPrompt; }
LLMQore::BaseClient *client() const noexcept;
bool supportsImages() const noexcept;
void setContextBindings(Templates::ContextRenderer::Bindings bindings); void setContextBindings(Templates::ContextRenderer::Bindings bindings);
QString renderAgentContext() const; QString renderAgentContext() const;

View File

@@ -2,6 +2,7 @@ add_library(Providers STATIC
ProviderID.hpp ProviderID.hpp
Provider.hpp Provider.cpp Provider.hpp Provider.cpp
ProviderFactory.hpp ProviderFactory.cpp ProviderFactory.hpp ProviderFactory.cpp
GenericProvider.hpp GenericProvider.cpp
) )
target_link_libraries(Providers target_link_libraries(Providers

View File

@@ -0,0 +1,113 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "GenericProvider.hpp"
#include <utility>
#include <LLMQore/BaseClient.hpp>
#include <LLMQore/ClaudeClient.hpp>
#include <LLMQore/GoogleAIClient.hpp>
#include <LLMQore/LlamaCppClient.hpp>
#include <LLMQore/MistralClient.hpp>
#include <LLMQore/OllamaClient.hpp>
#include <LLMQore/OpenAIClient.hpp>
#include <LLMQore/OpenAIResponsesClient.hpp>
#include "ProviderFactory.hpp"
namespace QodeAssist::Providers {
GenericProvider::GenericProvider(
QString name,
ProviderID id,
ProviderCapabilities capabilities,
const ClientFactory &clientFactory,
QObject *parent)
: Provider(parent)
, m_name(std::move(name))
, m_id(id)
, m_capabilities(capabilities)
, m_client(clientFactory(this))
{}
QString GenericProvider::name() const
{
return m_name;
}
ProviderID GenericProvider::providerID() const
{
return m_id;
}
ProviderCapabilities GenericProvider::capabilities() const
{
return m_capabilities;
}
::LLMQore::BaseClient *GenericProvider::client() const
{
return m_client;
}
QFuture<QList<QString>> GenericProvider::getInstalledModels(const QString &url)
{
m_client->setUrl(url);
m_client->setApiKey(apiKey());
return m_client->listModels();
}
namespace {
using Cap = ProviderCapability;
template<typename ClientT>
GenericProvider::ClientFactory makeFactory()
{
return [](QObject *parent) -> ::LLMQore::BaseClient * {
return new ClientT(QString(), QString(), QString(), parent);
};
}
} // namespace
void registerBuiltinProviders()
{
const auto reg = [](const QString &api,
ProviderID id,
ProviderCapabilities caps,
GenericProvider::ClientFactory factory) {
ProviderFactory::registerType(api, [=](QObject *parent) -> Provider * {
return new GenericProvider(api, id, caps, factory, parent);
});
};
const ProviderCapabilities full
= Cap::Tools | Cap::Thinking | Cap::Image | Cap::ModelListing;
reg("Claude", ProviderID::Claude, full, makeFactory<::LLMQore::ClaudeClient>());
reg("Google AI", ProviderID::GoogleAI, full, makeFactory<::LLMQore::GoogleAIClient>());
reg("llama.cpp", ProviderID::LlamaCpp, full, makeFactory<::LLMQore::LlamaCppClient>());
reg("LM Studio (Chat Completions)", ProviderID::LMStudio, full,
makeFactory<::LLMQore::OpenAIClient>());
reg("LM Studio (Responses API)", ProviderID::OpenAIResponses, full,
makeFactory<::LLMQore::OpenAIResponsesClient>());
reg("Mistral AI", ProviderID::MistralAI, full, makeFactory<::LLMQore::MistralClient>());
reg("Codestral", ProviderID::MistralAI, Cap::Tools | Cap::Image,
makeFactory<::LLMQore::MistralClient>());
reg("Ollama (Native)", ProviderID::Ollama, full, makeFactory<::LLMQore::OllamaClient>());
reg("Ollama (OpenAI-compatible)", ProviderID::OpenAICompatible, full,
makeFactory<::LLMQore::OpenAIClient>());
reg("OpenAI (Chat Completions)", ProviderID::OpenAI, full,
makeFactory<::LLMQore::OpenAIClient>());
reg("OpenAI (Responses API)", ProviderID::OpenAIResponses, full,
makeFactory<::LLMQore::OpenAIResponsesClient>());
reg("OpenAI Compatible", ProviderID::OpenAICompatible,
Cap::Tools | Cap::Image | Cap::Thinking, makeFactory<::LLMQore::OpenAIClient>());
reg("OpenRouter", ProviderID::OpenRouter,
Cap::Tools | Cap::Image | Cap::Thinking | Cap::ModelListing,
makeFactory<::LLMQore::OpenAIClient>());
}
} // namespace QodeAssist::Providers

View File

@@ -0,0 +1,50 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <functional>
#include "Provider.hpp"
namespace LLMQore {
class BaseClient;
}
namespace QodeAssist::Providers {
// A configuration-driven provider: it owns an LLMQore client and exposes a
// fixed identity/capability set. Concrete behaviour (request shape) comes from
// the agent's prompt template via Provider::prepareRequest, so a single class
// covers every client_api by varying the client factory + metadata.
class GenericProvider : public Provider
{
Q_OBJECT
public:
using ClientFactory = std::function<::LLMQore::BaseClient *(QObject *)>;
GenericProvider(
QString name,
ProviderID id,
ProviderCapabilities capabilities,
const ClientFactory &clientFactory,
QObject *parent = nullptr);
QString name() const override;
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
ProviderID providerID() const override;
ProviderCapabilities capabilities() const override;
::LLMQore::BaseClient *client() const override;
private:
QString m_name;
ProviderID m_id;
ProviderCapabilities m_capabilities;
::LLMQore::BaseClient *m_client;
};
// Registers every built-in client_api into ProviderFactory. Must be called once
// at plugin startup before any agent/session is created.
void registerBuiltinProviders();
} // namespace QodeAssist::Providers

View File

@@ -1,274 +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 "AgentPipelinesPage.hpp"
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/icore.h>
#include <QColor>
#include <QFont>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QScrollArea>
#include <QTimer>
#include <QVBoxLayout>
#include "../../Version.hpp"
#include "AgentRosterWidget.hpp"
#include "AgentsSettingsPage.hpp"
#include "Logger.hpp"
#include "PipelinesConfig.hpp"
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
#include <AgentFactory.hpp>
namespace QodeAssist::Settings {
AgentPipelinesPageNavigator::AgentPipelinesPageNavigator(QObject *parent)
: QObject(parent)
{}
namespace {
constexpr int kSaveDebounceMs = 300;
struct SlotMeta
{
const char *title;
const char *hint;
};
const SlotMeta kSlotMeta[] = {
{TrConstants::CODE_COMPLETION, TrConstants::SLOT_HINT_CODE_COMPLETION},
{TrConstants::CHAT_ASSISTANT, TrConstants::SLOT_HINT_CHAT_ASSISTANT},
{TrConstants::CHAT_COMPRESSION, TrConstants::SLOT_HINT_CHAT_COMPRESSION},
{TrConstants::QUICK_REFACTOR, TrConstants::SLOT_HINT_QUICK_REFACTOR},
};
class AgentPipelinesPageWidget : public Core::IOptionsPageWidget
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(AgentPipelinesPageWidget)
public:
AgentPipelinesPageWidget(
const QPointer<AgentFactory> &agentFactory,
const QPointer<AgentPipelinesPageNavigator> &navigator,
const QPointer<AgentsPageNavigator> &agentsNavigator)
: m_agentFactory(agentFactory)
, m_navigator(navigator)
, m_agentsNavigator(agentsNavigator)
{
m_titleLabel = new QLabel(Tr::tr(TrConstants::AGENT_PIPELINES), this);
QFont tf = m_titleLabel->font();
tf.setBold(true);
tf.setPixelSize(13);
m_titleLabel->setFont(tf);
m_resetBtn = new QPushButton(Tr::tr(TrConstants::RESET_TO_DEFAULTS), this);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(8);
headerRow->addWidget(m_titleLabel);
headerRow->addStretch(1);
headerRow->addWidget(m_resetBtn);
auto *headerSep = new QFrame(this);
headerSep->setFrameShape(QFrame::HLine);
headerSep->setFrameShadow(QFrame::Sunken);
m_rosters[0] = new AgentRosterWidget(this);
m_rosters[1] = new AgentRosterWidget(this);
m_rosters[2] = new AgentRosterWidget(this);
m_rosters[3] = new AgentRosterWidget(this);
for (int i = 0; i < kRosterCount; ++i)
m_rosters[i]->setSlot(Tr::tr(kSlotMeta[i].title), Tr::tr(kSlotMeta[i].hint), {});
auto *content = new QWidget(this);
auto *contentLay = new QVBoxLayout(content);
contentLay->setContentsMargins(0, 0, 0, 0);
contentLay->setSpacing(12);
for (int i = 0; i < kRosterCount; ++i)
contentLay->addWidget(m_rosters[i]);
contentLay->addStretch(1);
auto *scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setWidget(content);
auto *root = new QVBoxLayout(this);
root->setContentsMargins(8, 8, 8, 8);
root->setSpacing(6);
root->addLayout(headerRow);
root->addWidget(headerSep);
root->addWidget(scroll, 1);
m_saveDebounce = new QTimer(this);
m_saveDebounce->setSingleShot(true);
m_saveDebounce->setInterval(kSaveDebounceMs);
connect(m_saveDebounce, &QTimer::timeout, this, [this]() { persistRosters(); });
loadFromSettings();
connect(m_resetBtn, &QPushButton::clicked, this, &AgentPipelinesPageWidget::onReset);
for (int i = 0; i < kRosterCount; ++i) {
connect(m_rosters[i], &AgentRosterWidget::editAgentRequested, this,
&AgentPipelinesPageWidget::onEditAgent);
connect(m_rosters[i], &AgentRosterWidget::rosterChanged, this,
[this](const QStringList &) { m_saveDebounce->start(); });
}
}
~AgentPipelinesPageWidget() override
{
if (m_saveDebounce && m_saveDebounce->isActive()) {
m_saveDebounce->stop();
persistRosters();
}
}
void apply() final
{
if (m_saveDebounce && m_saveDebounce->isActive())
m_saveDebounce->stop();
persistRosters();
}
private:
static constexpr int kRosterCount = 4;
void persistRosters()
{
PipelineRosters rosters;
rosters.codeCompletion = m_rosters[0]->roster();
rosters.chatAssistant = m_rosters[1]->roster();
rosters.chatCompression = m_rosters[2]->roster();
rosters.quickRefactor = m_rosters[3]->roster();
QString err;
if (!PipelinesConfig::save(rosters, &err)) {
LOG_MESSAGE(QStringLiteral("[Pipelines] save failed (%1): %2")
.arg(PipelinesConfig::filePath(), err));
if (!m_saveErrorShown) {
m_saveErrorShown = true;
QMessageBox::warning(
Core::ICore::dialogParent(),
Tr::tr(TrConstants::AGENT_PIPELINES),
tr("Failed to save pipelines.toml:\n%1\n\n"
"Further save failures will only be logged.")
.arg(err));
}
} else {
m_saveErrorShown = false;
}
}
void onReset()
{
const auto reply = QMessageBox::question(
Core::ICore::dialogParent(),
Tr::tr(TrConstants::RESET_SETTINGS),
Tr::tr(TrConstants::CONFIRMATION),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
QString err;
if (!PipelinesConfig::save(PipelineRosters::defaults(), &err))
LOG_MESSAGE(QStringLiteral("[Pipelines] failed to reset rosters: %1").arg(err));
m_saveErrorShown = false;
loadFromSettings();
}
void onEditAgent(const QString &name)
{
if (m_agentsNavigator)
m_agentsNavigator->requestSelectAgent(name);
if (m_navigator)
emit m_navigator->editAgentRequested(name);
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 83)
Core::ICore::showSettings(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
#else
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
#endif
}
void loadFromSettings()
{
const PipelinesLoadResult lr = PipelinesConfig::load();
if (lr.status == PipelinesLoadStatus::ParseError
|| lr.status == PipelinesLoadStatus::SchemaError) {
QMessageBox::warning(
Core::ICore::dialogParent(),
Tr::tr(TrConstants::AGENT_PIPELINES),
tr("pipelines.toml has issues — using defaults for affected entries:\n%1\n\n"
"Click OK to continue. Changes you make here will overwrite the file.")
.arg(lr.message));
}
AgentFactory *factory = m_agentFactory.data();
m_rosters[0]->setRoster(lr.rosters.codeCompletion, factory);
m_rosters[1]->setRoster(lr.rosters.chatAssistant, factory);
m_rosters[2]->setRoster(lr.rosters.chatCompression, factory);
m_rosters[3]->setRoster(lr.rosters.quickRefactor, factory);
}
QPointer<AgentFactory> m_agentFactory;
QPointer<AgentPipelinesPageNavigator> m_navigator;
QPointer<AgentsPageNavigator> m_agentsNavigator;
QLabel *m_titleLabel = nullptr;
QPushButton *m_resetBtn = nullptr;
AgentRosterWidget *m_rosters[kRosterCount] = {};
QTimer *m_saveDebounce = nullptr;
bool m_saveErrorShown = false;
};
class AgentPipelinesOptionsPage final : public Core::IOptionsPage
{
public:
AgentPipelinesOptionsPage(
AgentFactory *agentFactory,
AgentPipelinesPageNavigator *navigator,
AgentsPageNavigator *agentsNavigator)
{
setId(Constants::QODE_ASSIST_AGENT_PIPELINES_PAGE_ID);
setDisplayName(Tr::tr(TrConstants::AGENT_PIPELINES));
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
const QPointer<AgentFactory> factoryPtr(agentFactory);
const QPointer<AgentPipelinesPageNavigator> navPtr(navigator);
const QPointer<AgentsPageNavigator> agentsNavPtr(agentsNavigator);
setWidgetCreator([factoryPtr, navPtr, agentsNavPtr] {
return new AgentPipelinesPageWidget(factoryPtr, navPtr, agentsNavPtr);
});
}
};
} // namespace
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
AgentFactory *agentFactory,
AgentPipelinesPageNavigator *navigator,
AgentsPageNavigator *agentsNavigator)
{
return std::make_unique<AgentPipelinesOptionsPage>(
agentFactory, navigator, agentsNavigator);
}
} // namespace QodeAssist::Settings
#include "AgentPipelinesPage.moc"

View File

@@ -1,40 +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 <memory>
#include <QObject>
#include <QString>
namespace Core {
class IOptionsPage;
}
namespace QodeAssist {
class AgentFactory;
}
namespace QodeAssist::Settings {
class AgentsPageNavigator;
class AgentPipelinesPageNavigator : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(AgentPipelinesPageNavigator)
public:
explicit AgentPipelinesPageNavigator(QObject *parent = nullptr);
signals:
void editAgentRequested(const QString &agentName);
};
std::unique_ptr<Core::IOptionsPage> createAgentPipelinesSettingsPage(
AgentFactory *agentFactory,
AgentPipelinesPageNavigator *navigator,
AgentsPageNavigator *agentsNavigator);
} // namespace QodeAssist::Settings

View File

@@ -1,5 +1,4 @@
add_library(QodeAssistAgentPipelines OBJECT add_library(QodeAssistAgentPipelines STATIC
AgentPipelinesPage.hpp AgentPipelinesPage.cpp
PipelinesConfig.hpp PipelinesConfig.cpp PipelinesConfig.hpp PipelinesConfig.cpp
AgentRosterWidget.hpp AgentRosterWidget.cpp AgentRosterWidget.hpp AgentRosterWidget.cpp
AgentSlotWidget.hpp AgentSlotWidget.cpp AgentSlotWidget.hpp AgentSlotWidget.cpp

View File

@@ -1,6 +1,5 @@
add_executable(QodeAssistTest add_executable(QodeAssistTest
../CodeHandler.cpp ../CodeHandler.cpp
../LLMClientInterface.cpp
../LLMSuggestion.cpp ../LLMSuggestion.cpp
CodeHandlerTest.cpp CodeHandlerTest.cpp
ClaudeCacheControlTest.cpp ClaudeCacheControlTest.cpp

View File

@@ -7,7 +7,6 @@
#include "CustomInstructionsManager.hpp" #include "CustomInstructionsManager.hpp"
#include "QodeAssisttr.h" #include "QodeAssisttr.h"
#include "settings/ConfigurationManager.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include "settings/QuickRefactorSettings.hpp" #include "settings/QuickRefactorSettings.hpp"
#include "settings/SettingsConstants.hpp" #include "settings/SettingsConstants.hpp"
@@ -113,11 +112,6 @@ void QuickRefactorDialog::setupUi()
actionsLayout->addWidget(m_alternativeButton); actionsLayout->addWidget(m_alternativeButton);
actionsLayout->addStretch(); actionsLayout->addStretch();
m_configComboBox = new QComboBox(this);
m_configComboBox->setMinimumWidth(200);
m_configComboBox->setToolTip(Tr::tr("Switch AI configuration"));
actionsLayout->addWidget(m_configComboBox);
Utils::Theme *theme = Utils::creatorTheme(); Utils::Theme *theme = Utils::creatorTheme();
QColor iconColor = theme ? theme->color(Utils::Theme::TextColorNormal) : QColor(Qt::white); QColor iconColor = theme ? theme->color(Utils::Theme::TextColorNormal) : QColor(Qt::white);
@@ -244,13 +238,6 @@ void QuickRefactorDialog::setupUi()
&QuickRefactorDialog::onOpenInstructionsFolder); &QuickRefactorDialog::onOpenInstructionsFolder);
loadCustomCommands(); loadCustomCommands();
loadAvailableConfigurations();
connect(
m_configComboBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
this,
&QuickRefactorDialog::onConfigurationChanged);
QDialogButtonBox *buttonBox QDialogButtonBox *buttonBox
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
@@ -577,60 +564,6 @@ void QuickRefactorDialog::onOpenSettings()
Settings::showSettings(Constants::QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID); Settings::showSettings(Constants::QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID);
} }
QString QuickRefactorDialog::selectedConfiguration() const
{
return m_selectedConfiguration;
}
void QuickRefactorDialog::loadAvailableConfigurations()
{
auto &manager = Settings::ConfigurationManager::instance();
manager.loadConfigurations(Settings::ConfigurationType::QuickRefactor);
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::QuickRefactor);
m_configComboBox->clear();
m_configComboBox->addItem(Tr::tr("Current"), QString());
for (const Settings::AIConfiguration &config : configs) {
m_configComboBox->addItem(config.name, config.id);
}
auto &settings = Settings::generalSettings();
QString currentProvider = settings.qrProvider.value();
QString currentModel = settings.qrModel.value();
QString currentConfigText = QString("%1/%2").arg(currentProvider, currentModel);
m_configComboBox->setItemText(0, Tr::tr("Current (%1)").arg(currentConfigText));
}
void QuickRefactorDialog::onConfigurationChanged(int index)
{
if (index == 0) {
m_selectedConfiguration.clear();
return;
}
QString configId = m_configComboBox->itemData(index).toString();
m_selectedConfiguration = m_configComboBox->itemText(index);
auto &manager = Settings::ConfigurationManager::instance();
Settings::AIConfiguration config
= manager.getConfigurationById(configId, Settings::ConfigurationType::QuickRefactor);
if (!config.id.isEmpty()) {
auto &settings = Settings::generalSettings();
settings.qrProvider.setValue(config.provider);
settings.qrModel.setValue(config.model);
settings.qrTemplate.setValue(config.templateName);
settings.qrUrl.setValue(config.url);
settings.qrCustomEndpoint.setValue(config.customEndpoint);
settings.writeSettings();
}
}
void QuickRefactorDialog::validateAndAccept() void QuickRefactorDialog::validateAndAccept()
{ {
QString instruction = m_instructionEdit->toPlainText().trimmed(); QString instruction = m_instructionEdit->toPlainText().trimmed();

View File

@@ -33,8 +33,6 @@ public:
Action selectedAction() const; Action selectedAction() const;
QString selectedConfiguration() const;
bool eventFilter(QObject *watched, QEvent *event) override; bool eventFilter(QObject *watched, QEvent *event) override;
void keyPressEvent(QKeyEvent *event) override; void keyPressEvent(QKeyEvent *event) override;
@@ -50,8 +48,6 @@ private slots:
void onOpenInstructionsFolder(); void onOpenInstructionsFolder();
void onOpenSettings(); void onOpenSettings();
void loadCustomCommands(); void loadCustomCommands();
void loadAvailableConfigurations();
void onConfigurationChanged(int index);
void validateAndAccept(); void validateAndAccept();
private: private:
@@ -71,12 +67,10 @@ private:
QToolButton *m_toolsButton; QToolButton *m_toolsButton;
QToolButton *m_thinkingButton; QToolButton *m_thinkingButton;
QComboBox *m_commandsComboBox; QComboBox *m_commandsComboBox;
QComboBox *m_configComboBox;
Action m_selectedAction = Action::Custom; Action m_selectedAction = Action::Custom;
QString m_lastInstructions; QString m_lastInstructions;
QString m_selectedConfiguration;
QIcon m_toolsIconOn; QIcon m_toolsIconOn;
QIcon m_toolsIconOff; QIcon m_toolsIconOff;
QIcon m_thinkingIconOn; QIcon m_thinkingIconOn;